From 93d716c4b1363bcfb5c023d2e10916c0dcc43fcd Mon Sep 17 00:00:00 2001 From: Vincent T Date: Thu, 7 Nov 2024 11:30:01 -0500 Subject: [PATCH] frontend: DetailsDrawer: Add details drawer / drawer mode Signed-off-by: Vincent T --- frontend/src/components/App/Layout.tsx | 4 + .../App/Settings/DrawerModeButton.tsx | 55 +++ .../src/components/App/Settings/Settings.tsx | 5 + .../Settings.General.stories.storyshot | 43 +++ .../__snapshots__/Settings.stories.storyshot | 329 ++++++++++++++++++ .../DetailsDrawer/DetailsDrawer.tsx | 40 +++ .../src/components/common/Link.stories.tsx | 14 +- frontend/src/components/common/Link.tsx | 54 ++- .../common/Resource/ResourceTable.stories.tsx | 17 + frontend/src/i18n/locales/de/translation.json | 5 +- frontend/src/i18n/locales/en/translation.json | 3 + frontend/src/i18n/locales/es/translation.json | 3 + frontend/src/i18n/locales/fr/translation.json | 3 + frontend/src/i18n/locales/pt/translation.json | 3 + frontend/src/redux/drawerModeSlice.ts | 31 ++ frontend/src/redux/reducers/reducers.tsx | 2 + 16 files changed, 597 insertions(+), 14 deletions(-) create mode 100644 frontend/src/components/App/Settings/DrawerModeButton.tsx create mode 100644 frontend/src/components/App/Settings/__snapshots__/Settings.stories.storyshot create mode 100644 frontend/src/components/DetailsDrawer/DetailsDrawer.tsx create mode 100644 frontend/src/redux/drawerModeSlice.ts diff --git a/frontend/src/components/App/Layout.tsx b/frontend/src/components/App/Layout.tsx index 912d32238f..e9ac99edf5 100644 --- a/frontend/src/components/App/Layout.tsx +++ b/frontend/src/components/App/Layout.tsx @@ -18,6 +18,7 @@ import store from '../../redux/stores/store'; import { fetchStatelessClusterKubeConfigs, isEqualClusterConfigs } from '../../stateless/'; import ActionsNotifier from '../common/ActionsNotifier'; import AlertNotification from '../common/AlertNotification'; +import DetailsDrawer from '../DetailsDrawer/DetailsDrawer'; import Sidebar, { NavigationTabs } from '../Sidebar'; import RouteSwitcher from './RouteSwitcher'; import TopBar from './TopBar'; @@ -104,6 +105,8 @@ export default function Layout({}: LayoutProps) { const clusterInURL = getCluster(); const theme = useTheme(); + const isDetailDrawerEnabled = useTypedSelector(state => state.drawerMode.isDetailDrawerEnabled); + /** This fetches the cluster config from the backend and updates the redux store on an interval. * When stateless clusters are enabled, it also fetches the stateless cluster config from the * indexDB and then sends the backend to parse it and then updates the parsed value into redux @@ -232,6 +235,7 @@ export default function Layout({}: LayoutProps) { + {isDetailDrawerEnabled && } diff --git a/frontend/src/components/App/Settings/DrawerModeButton.tsx b/frontend/src/components/App/Settings/DrawerModeButton.tsx new file mode 100644 index 0000000000..491d864bbd --- /dev/null +++ b/frontend/src/components/App/Settings/DrawerModeButton.tsx @@ -0,0 +1,55 @@ +import { FormControlLabel, Switch } from '@mui/material'; +import React from 'react'; +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; +import { setDetailDrawerEnabled } from '../../../redux/drawerModeSlice'; +import { useTypedSelector } from '../../../redux/reducers/reducers'; +import { TooltipIcon } from '../../common'; + +export default function DrawerModeButton() { + const dispatch = useDispatch(); + const { t } = useTranslation('translation'); + + const [isDrawerEnabled, changeDetailDrawerEnabled] = useState( + useTypedSelector(state => state.drawerMode.isDetailDrawerEnabled) + ); + + if (isDrawerEnabled) { + dispatch(setDetailDrawerEnabled(true)); + } else { + dispatch(setDetailDrawerEnabled(false)); + } + + // the useEffect will run everytime the isDrawerEnabled state changes, which is everytime the user clicks the switch button because the switch button changes the state of isDrawerEnabled + useEffect(() => { + dispatch(setDetailDrawerEnabled(isDrawerEnabled)); + }, [isDrawerEnabled]); + + // this function takes in the current changes and updates it, this kicks off the useEffect that is listening for changes to newDrawerEnabled + function drawerModeToggle() { + changeDetailDrawerEnabled(!isDrawerEnabled); + } + + // NOTICE THAT WE DO NOT USE isDrawerEnabled TO DETERMINE HOW THE SWITCH IS RENDERED UNDER THE CHECKED PROP, THIS IS BECAUSE THE USEEFFECT WILL RERENDER THE COMPONENT WITH THE NEW STATE + return ( + + } + label={ + <> + {t('translation|Drawer Mode')} + + {t('translation|Enable details to render in side drawer window')} + + + } + /> + ); +} diff --git a/frontend/src/components/App/Settings/Settings.tsx b/frontend/src/components/App/Settings/Settings.tsx index 3e0fc2c54e..8dedefec13 100644 --- a/frontend/src/components/App/Settings/Settings.tsx +++ b/frontend/src/components/App/Settings/Settings.tsx @@ -8,6 +8,7 @@ import { setAppSettings } from '../../../redux/configSlice'; import { defaultTableRowsPerPageOptions } from '../../../redux/configSlice'; import { ActionButton, NameValueTable, SectionBox } from '../../common'; import TimezoneSelect from '../../common/TimezoneSelect'; +import DrawerModeButton from './DrawerModeButton'; import { useSettings } from './hook'; import NumRowsInput from './NumRowsInput'; import ThemeChangeButton from './ThemeChangeButton'; @@ -57,6 +58,10 @@ export default function Settings() { name: t('translation|Theme'), value: , }, + { + name: t('translation|Details on list view'), + value: , + }, { name: t('translation|Number of rows for tables'), value: ( diff --git a/frontend/src/components/App/Settings/__snapshots__/Settings.General.stories.storyshot b/frontend/src/components/App/Settings/__snapshots__/Settings.General.stories.storyshot index cde6bd51c0..d15a3c422a 100644 --- a/frontend/src/components/App/Settings/__snapshots__/Settings.General.stories.storyshot +++ b/frontend/src/components/App/Settings/__snapshots__/Settings.General.stories.storyshot @@ -151,6 +151,49 @@ +
+ Details on list view +
+
+
diff --git a/frontend/src/components/App/Settings/__snapshots__/Settings.stories.storyshot b/frontend/src/components/App/Settings/__snapshots__/Settings.stories.storyshot new file mode 100644 index 0000000000..8905651a3e --- /dev/null +++ b/frontend/src/components/App/Settings/__snapshots__/Settings.stories.storyshot @@ -0,0 +1,329 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Storyshots Settings General 1`] = ` +
+ +
+
+
+
+

+ General Settings +

+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ Language +
+
+
+
+ + + +
+
+
+
+ Theme +
+
+
+ + +
+
+
+ Drawer Mode +
+
+ +
+
+ Number of rows for tables +
+
+
+
+ + + +
+
+
+
+ Timezone to display for dates +
+
+
+
+
+ +
+ +
+ +
+
+
+
+
+
+
+
+
+
+`; diff --git a/frontend/src/components/DetailsDrawer/DetailsDrawer.tsx b/frontend/src/components/DetailsDrawer/DetailsDrawer.tsx new file mode 100644 index 0000000000..593b9d7e3c --- /dev/null +++ b/frontend/src/components/DetailsDrawer/DetailsDrawer.tsx @@ -0,0 +1,40 @@ +import { Box, Button, Drawer } from '@mui/material'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { useDispatch } from 'react-redux'; +import { setSelectedResource } from '../../redux/drawerModeSlice'; +import { useTypedSelector } from '../../redux/reducers/reducers'; +import { KubeObjectDetails } from '../resourceMap/details/KubeNodeDetails'; + +export default function DetailsDrawer() { + const { t } = useTranslation(); + + const selectedResource = useTypedSelector(state => state.drawerMode.selectedResource); + + const dispatch = useDispatch(); + + function closeDrawer() { + dispatch(setSelectedResource(undefined)); + } + + console.log({ selectedResource }); + + return ( + <> + {selectedResource && ( + closeDrawer()}> + + + + + + + + + + )} + + ); +} diff --git a/frontend/src/components/common/Link.stories.tsx b/frontend/src/components/common/Link.stories.tsx index 54dfdf80b5..905fa67338 100644 --- a/frontend/src/components/common/Link.stories.tsx +++ b/frontend/src/components/common/Link.stories.tsx @@ -1,15 +1,23 @@ import { Meta, StoryFn } from '@storybook/react'; +import React from 'react'; +import { Provider } from 'react-redux'; import { MemoryRouter } from 'react-router-dom'; +import { createStore } from 'redux'; +import reducers from '../../redux/reducers/reducers'; import Link, { LinkProps } from './Link'; +const store = createStore(reducers); + export default { title: 'Link', component: Link, decorators: [ Story => ( - - - + + + + + ), ], } as Meta; diff --git a/frontend/src/components/common/Link.tsx b/frontend/src/components/common/Link.tsx index 0551ee387e..aaa5424f40 100644 --- a/frontend/src/components/common/Link.tsx +++ b/frontend/src/components/common/Link.tsx @@ -1,8 +1,11 @@ import MuiLink from '@mui/material/Link'; import React from 'react'; +import { useDispatch } from 'react-redux'; import { Link as RouterLink } from 'react-router-dom'; import { KubeObject } from '../../lib/k8s/KubeObject'; import { createRouteURL, RouteURLProps } from '../../lib/router'; +import { setSelectedResource } from '../../redux/drawerModeSlice'; +import { useTypedSelector } from '../../redux/reducers/reducers'; import { LightTooltip } from './Tooltip'; export interface LinkBaseProps { @@ -21,6 +24,7 @@ export interface LinkProps extends LinkBaseProps { state?: { [prop: string]: any; }; + drawerEnabled?: boolean; } export interface LinkObjectProps extends LinkBaseProps { @@ -29,16 +33,8 @@ export interface LinkObjectProps extends LinkBaseProps { } function PureLink(props: React.PropsWithChildren) { - if ((props as LinkObjectProps).kubeObject) { - const { kubeObject, ...otherProps } = props as LinkObjectProps; - return ( - - {props.children || kubeObject!.getName()} - - ); - } + const { routeName, params = {}, search, state, ...otherProps } = props as LinkObjectProps; - const { routeName, params = {}, search, state, ...otherProps } = props as LinkProps; return ( ) { } export default function Link(props: React.PropsWithChildren) { - const { tooltip, ...otherProps } = props; + const drawerEnabled = useTypedSelector(state => state.drawerMode.isDetailDrawerEnabled); + const dispatch = useDispatch(); + + const { tooltip, kubeObject, ...otherProps } = props as LinkObjectProps; + if (tooltip) { let tooltipText = ''; if (typeof tooltip === 'string') { @@ -77,5 +77,39 @@ export default function Link(props: React.PropsWithChildren { + if (drawerEnabled) { + dispatch(setSelectedResource(kubeJSON!)); + + /** + * NOTE: we are using window.history.pushState to update the URL without causing a page reload. + * currently there is no way to update the URL without navigation to the details page which would make the drawer redundant. + */ + window.history.pushState( + { path: kubeObject.getDetailsLink() }, + '', + kubeObject.getDetailsLink() + ); + } + }} + > + {props.children || kubeObject.getName()} + + ); + } else { + return ( + + {props.children || kubeObject.getName()} + + ); + } + } + return ; } diff --git a/frontend/src/components/common/Resource/ResourceTable.stories.tsx b/frontend/src/components/common/Resource/ResourceTable.stories.tsx index a86259197c..ad7257fb3b 100644 --- a/frontend/src/components/common/Resource/ResourceTable.stories.tsx +++ b/frontend/src/components/common/Resource/ResourceTable.stories.tsx @@ -1,14 +1,29 @@ import { configureStore } from '@reduxjs/toolkit'; import { Meta, StoryFn } from '@storybook/react'; +import { Provider } from 'react-redux'; +import { MemoryRouter } from 'react-router-dom'; +import { createStore } from 'redux'; import { useMockListQuery } from '../../../helpers/testHelpers'; import Pod, { KubePod } from '../../../lib/k8s/pod'; +import reducers from '../../../redux/reducers/reducers'; import { INITIAL_STATE as UI_INITIAL_STATE } from '../../../redux/reducers/ui'; import { TestContext } from '../../../test'; import ResourceTable, { ResourceTableFromResourceClassProps } from './ResourceTable'; +const store = createStore(reducers); + export default { title: 'ResourceTable', component: ResourceTable, + decorators: [ + Story => ( + + + + + + ), + ], argTypes: {}, } as Meta; @@ -25,6 +40,7 @@ const TemplateWithFilter: StoryFn<{ filter: { namespaces: new Set(), search: '' }, config: { settings: { tableRowsPerPageOptions: [10, 20, 50, 100] } }, ui: UI_INITIAL_STATE, + drawerMode: { isDetailDrawerEnabled: false }, } ) => state, preloadedState: { @@ -41,6 +57,7 @@ const TemplateWithFilter: StoryFn<{ resourceTable: { tableColumnsProcessors: [], }, + drawerMode: { isDetailDrawerEnabled: false }, }, }); diff --git a/frontend/src/i18n/locales/de/translation.json b/frontend/src/i18n/locales/de/translation.json index ab76e21f71..28f8b68e3a 100644 --- a/frontend/src/i18n/locales/de/translation.json +++ b/frontend/src/i18n/locales/de/translation.json @@ -53,8 +53,10 @@ "Delete Plugin": "", "Are you sure you want to delete this plugin?": "", "Save": "", - "Uh-oh! Something went wrong.": "Oh-oh! Etwas ist schief gelaufen.", + "Uh-oh! Something went wrong.": "", "Error loading {{ routeName }}": "", + "Drawer Mode": "", + "Enable details to render in side drawer window": "", "Enter a value between {{ minRows }} and {{ maxRows }}.": "Geben Sie einen Wert zwischen {{ minRows }} und {{ maxRows }} ein.", "Custom row value": "Benutzerdefinierter Zeilenwert", "Apply": "Anwenden", @@ -63,6 +65,7 @@ "Version": "Version", "Language": "Sprache", "Theme": "Design", + "Details on list view": "", "Number of rows for tables": "Zeilen pro Tabelle", "Timezone to display for dates": "Zeitzone", "Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.": "Namespaces dürfen nur alphanumerische Kleinbuchstaben oder \"-\" enthalten und müssen mit einem alphanumerischen Zeichen beginnen und enden.", diff --git a/frontend/src/i18n/locales/en/translation.json b/frontend/src/i18n/locales/en/translation.json index 7aadd30716..fc46fde44d 100644 --- a/frontend/src/i18n/locales/en/translation.json +++ b/frontend/src/i18n/locales/en/translation.json @@ -55,6 +55,8 @@ "Save": "Save", "Uh-oh! Something went wrong.": "Uh-oh! Something went wrong.", "Error loading {{ routeName }}": "Error loading {{ routeName }}", + "Drawer Mode": "Drawer Mode", + "Enable details to render in side drawer window": "Enable details to render in side drawer window", "Enter a value between {{ minRows }} and {{ maxRows }}.": "Enter a value between {{ minRows }} and {{ maxRows }}.", "Custom row value": "Custom row value", "Apply": "Apply", @@ -63,6 +65,7 @@ "Version": "Version", "Language": "Language", "Theme": "Theme", + "Details on list view": "Details on list view", "Number of rows for tables": "Number of rows for tables", "Timezone to display for dates": "Timezone to display for dates", "Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.": "Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.", diff --git a/frontend/src/i18n/locales/es/translation.json b/frontend/src/i18n/locales/es/translation.json index 394ea32c76..27b0f75b1d 100644 --- a/frontend/src/i18n/locales/es/translation.json +++ b/frontend/src/i18n/locales/es/translation.json @@ -55,6 +55,8 @@ "Save": "", "Uh-oh! Something went wrong.": "¡Ups! Algo ha fallado.", "Error loading {{ routeName }}": "", + "Drawer Mode": "", + "Enable details to render in side drawer window": "", "Enter a value between {{ minRows }} and {{ maxRows }}.": "Introduzca un valor entre {{ minRows }} y {{ maxRows }}.", "Custom row value": "Núm. de líneas personalizado", "Apply": "Aplicar", @@ -63,6 +65,7 @@ "Version": "Versión", "Language": "Idioma", "Theme": "Tema", + "Details on list view": "", "Number of rows for tables": "Núm. de líneas para las tablas", "Timezone to display for dates": "Huso horario para mostrar en fechas", "Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.": "Los espacios de nombre deben contener solo caracteres alfanuméricos en minúsculas o '-', y deben comenzar y terminar con un carácter alfanumérico.", diff --git a/frontend/src/i18n/locales/fr/translation.json b/frontend/src/i18n/locales/fr/translation.json index b5682ee4c1..0597c6171f 100644 --- a/frontend/src/i18n/locales/fr/translation.json +++ b/frontend/src/i18n/locales/fr/translation.json @@ -55,6 +55,8 @@ "Save": "", "Uh-oh! Something went wrong.": "Uh-oh ! Quelque chose s'est mal passé.", "Error loading {{ routeName }}": "", + "Drawer Mode": "", + "Enable details to render in side drawer window": "", "Enter a value between {{ minRows }} and {{ maxRows }}.": "Entrez une valeur entre {{ minRows }} et {{ maxRows }}.", "Custom row value": "Valeur de ligne personnalisée", "Apply": "Appliquer", @@ -63,6 +65,7 @@ "Version": "Version", "Language": "Langue", "Theme": "Thème", + "Details on list view": "", "Number of rows for tables": "Nombre de lignes pour les tableaux", "Timezone to display for dates": "Fuseau horaire à afficher pour les dates", "Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.": "Les espaces de noms ne doivent contenir que des caractères alphanumériques minuscules ou '-', et doivent commencer et se terminer par un caractère alphanumérique.", diff --git a/frontend/src/i18n/locales/pt/translation.json b/frontend/src/i18n/locales/pt/translation.json index 16e786f568..942246618b 100644 --- a/frontend/src/i18n/locales/pt/translation.json +++ b/frontend/src/i18n/locales/pt/translation.json @@ -55,6 +55,8 @@ "Save": "", "Uh-oh! Something went wrong.": "Oh-oh! Algo correu mal.", "Error loading {{ routeName }}": "", + "Drawer Mode": "", + "Enable details to render in side drawer window": "", "Enter a value between {{ minRows }} and {{ maxRows }}.": "Introduza um valor entre {{ minRows }} e {{ maxRows }}.", "Custom row value": "Núm. de linhas personalizado", "Apply": "Aplicar", @@ -63,6 +65,7 @@ "Version": "Versão", "Language": "Idioma", "Theme": "Tema", + "Details on list view": "", "Number of rows for tables": "Núm. de linhas para as tabelas", "Timezone to display for dates": "Fuso horário para mostrar em datas", "Namespaces must contain only lowercase alphanumeric characters or '-', and must start and end with an alphanumeric character.": "Os namespaces devem conter apenas caracteres alfanuméricos minúsculos ou '-', e devem começar e terminar com um carácter alfanumérico.", diff --git a/frontend/src/redux/drawerModeSlice.ts b/frontend/src/redux/drawerModeSlice.ts new file mode 100644 index 0000000000..2b5d672d7d --- /dev/null +++ b/frontend/src/redux/drawerModeSlice.ts @@ -0,0 +1,31 @@ +import { createSlice, PayloadAction } from '@reduxjs/toolkit'; +import { KubeObject } from '../lib/k8s/KubeObject'; + +interface DrawerModeState { + isDetailDrawerEnabled: boolean; + selectedResource: KubeObject | undefined; +} + +const getLocalDrawerStatus = (key: string) => localStorage.getItem(key) === 'true'; + +const initialState: DrawerModeState = { + isDetailDrawerEnabled: getLocalDrawerStatus('detailDrawerEnabled'), + selectedResource: undefined, +}; + +const drawerModeSlice = createSlice({ + name: 'drawerMode', + initialState, + reducers: { + setDetailDrawerEnabled: (state, action: PayloadAction) => { + state.isDetailDrawerEnabled = action.payload; + localStorage.setItem('detailDrawerEnabled', `${action.payload}`); + }, + setSelectedResource: (state, action: any) => { + state.selectedResource = action.payload; + }, + }, +}); + +export const { setDetailDrawerEnabled, setSelectedResource } = drawerModeSlice.actions; +export default drawerModeSlice.reducer; diff --git a/frontend/src/redux/reducers/reducers.tsx b/frontend/src/redux/reducers/reducers.tsx index 7ce7debc45..beca4c85cb 100644 --- a/frontend/src/redux/reducers/reducers.tsx +++ b/frontend/src/redux/reducers/reducers.tsx @@ -6,6 +6,7 @@ import pluginsReducer from '../../plugin/pluginsSlice'; import actionButtons from '../actionButtonsSlice'; import clusterAction from '../clusterActionSlice'; import configReducer from '../configSlice'; +import drawerModeSlice from '../drawerModeSlice'; import filterReducer from '../filterSlice'; import eventCallbackReducer from '../headlampEventSlice'; import routesReducer from '../routesSlice'; @@ -31,6 +32,7 @@ const reducers = combineReducers({ detailsViewSections: detailsViewSectionReducer, eventCallbackReducer, pluginConfigs: pluginConfigReducer, + drawerMode: drawerModeSlice, }); export type RootState = ReturnType;