From 370de73169b0c48aecfbc77f25e8e380ee89ae0d Mon Sep 17 00:00:00 2001 From: Joaquim Rocha Date: Thu, 12 Oct 2023 16:09:42 +0100 Subject: [PATCH 1/2] frontend: Add a way for plugins to react to Headlamp events Plugins may be interested in reacting to when e.g. all plugins are loaded, in order to show a notification or perform some different action. These changes allows plugins to do that by registering callbacks that are run when some events in the app happen. Co-Authored-By: Ashu Ghildiyal Signed-off-by: Joaquim Rocha --- .../DetailsViewSection/DetailsViewSection.tsx | 9 +- .../common/ErrorBoundary/ErrorBoundary.tsx | 5 + .../components/common/ErrorPage/ErrorPage.tsx | 86 ++++ .../src/components/common/ErrorPage/index.tsx | 88 +--- .../src/components/common/ObjectEventList.tsx | 10 +- .../common/Resource/CreateButton.tsx | 14 +- .../common/Resource/DeleteButton.tsx | 18 +- .../components/common/Resource/EditButton.tsx | 19 +- .../components/common/Resource/Resource.tsx | 16 + .../common/Resource/ResourceTable.tsx | 10 + .../common/Resource/RestartButton.tsx | 17 +- .../common/Resource/ScaleButton.tsx | 22 +- frontend/src/components/pod/Details.tsx | 52 +- frontend/src/components/pod/List.tsx | 11 + frontend/src/plugin/index.ts | 24 + frontend/src/plugin/lib.ts | 21 + frontend/src/plugin/registry.tsx | 63 +++ frontend/src/redux/actions/actions.tsx | 9 +- .../src/redux/headlampEventSlice.test.tsx | 66 +++ frontend/src/redux/headlampEventSlice.ts | 453 ++++++++++++++++++ frontend/src/redux/reducers/reducers.tsx | 3 + frontend/src/redux/stores/store.tsx | 3 +- 22 files changed, 913 insertions(+), 106 deletions(-) create mode 100644 frontend/src/components/common/ErrorPage/ErrorPage.tsx create mode 100644 frontend/src/redux/headlampEventSlice.test.tsx create mode 100644 frontend/src/redux/headlampEventSlice.ts diff --git a/frontend/src/components/DetailsViewSection/DetailsViewSection.tsx b/frontend/src/components/DetailsViewSection/DetailsViewSection.tsx index d841f8ae7c..e9a518956e 100644 --- a/frontend/src/components/DetailsViewSection/DetailsViewSection.tsx +++ b/frontend/src/components/DetailsViewSection/DetailsViewSection.tsx @@ -1,5 +1,6 @@ -import { isValidElement, ReactElement, ReactNode, useMemo } from 'react'; +import React, { isValidElement, ReactElement, ReactNode, useMemo } from 'react'; import { KubeObject } from '../../lib/k8s/cluster'; +import { HeadlampEventType, useEventCallback } from '../../redux/headlampEventSlice'; import { useTypedSelector } from '../../redux/reducers/reducers'; import ErrorBoundary from '../common/ErrorBoundary'; @@ -20,6 +21,12 @@ export type DetailsViewSectionType = export default function DetailsViewSection(props: DetailsViewSectionProps) { const { resource } = props; const detailViews = useTypedSelector(state => state.detailsViewSection.detailViews); + const dispatchHeadlampEvent = useEventCallback(HeadlampEventType.DETAILS_VIEW); + + React.useEffect(() => { + dispatchHeadlampEvent({ resource }); + }, [resource]); + const memoizedComponents = useMemo( () => detailViews.map((Component, index) => { diff --git a/frontend/src/components/common/ErrorBoundary/ErrorBoundary.tsx b/frontend/src/components/common/ErrorBoundary/ErrorBoundary.tsx index 642a768c57..4c72ea5c4a 100644 --- a/frontend/src/components/common/ErrorBoundary/ErrorBoundary.tsx +++ b/frontend/src/components/common/ErrorBoundary/ErrorBoundary.tsx @@ -1,4 +1,6 @@ import { Children, Component, ComponentType, isValidElement, ReactElement } from 'react'; +import { eventAction, HeadlampEventType } from '../../../redux/headlampEventSlice'; +import store from '../../../redux/stores/store'; export interface ErrorBoundaryProps { fallback?: ComponentType<{ error: Error }> | ReactElement | null; @@ -41,6 +43,9 @@ export default class ErrorBoundary extends Component render() { const { error } = this.state; + if (error) { + store.dispatch(eventAction({ type: HeadlampEventType.ERROR_BOUNDARY, data: error })); + } if (!error) { return this.props.children; } diff --git a/frontend/src/components/common/ErrorPage/ErrorPage.tsx b/frontend/src/components/common/ErrorPage/ErrorPage.tsx new file mode 100644 index 0000000000..fd1a75b48f --- /dev/null +++ b/frontend/src/components/common/ErrorPage/ErrorPage.tsx @@ -0,0 +1,86 @@ +import { Grid, Typography } from '@mui/material'; +import Link from '@mui/material/Link'; +import makeStyles from '@mui/styles/makeStyles'; +import React from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import headlampBrokenImage from '../../../assets/headlamp-broken.svg'; + +const useStyles = makeStyles(() => ({ + root: { + textAlign: 'center', + }, + h1: { + fontSize: '2.125rem', + lineHeight: 1.2, + fontWeight: 400, + }, + h2: { + fontSize: '1.25rem', + lineHeight: 3.6, + fontWeight: 500, + }, + img: { + width: '100%', + }, +})); + +export interface ErrorComponentProps { + /** The main title to display. By default it is: "Uh-oh! Something went wrong." */ + title?: React.ReactNode; + /** The message to display. By default it is: "Head back home." */ + message?: React.ReactNode; + /** The graphic or element to display as a main graphic. If used as a string, it will be + * used as the source for displaying an image. By default it is "headlamp-broken.svg". */ + graphic?: React.ReactChild; + /** Whether to use Typography or not. By default it is true. */ + withTypography?: boolean; +} + +export default function ErrorComponent(props: ErrorComponentProps) { + const { t } = useTranslation(); + const { + title = t('Uh-oh! Something went wrong.'), + message = '', + withTypography = true, + graphic = headlampBrokenImage, + } = props; + const classes = useStyles(); + return ( + + + {typeof graphic === 'string' ? ( + + ) : ( + graphic + )} + {withTypography ? ( + + {title} + + ) : ( + title + )} + {withTypography ? ( + + {!!message ? ( + message + ) : ( + + Head back home. + + )} + + ) : ( + message + )} + + + ); +} diff --git a/frontend/src/components/common/ErrorPage/index.tsx b/frontend/src/components/common/ErrorPage/index.tsx index fd1a75b48f..f88a5664ed 100644 --- a/frontend/src/components/common/ErrorPage/index.tsx +++ b/frontend/src/components/common/ErrorPage/index.tsx @@ -1,86 +1,4 @@ -import { Grid, Typography } from '@mui/material'; -import Link from '@mui/material/Link'; -import makeStyles from '@mui/styles/makeStyles'; -import React from 'react'; -import { Trans, useTranslation } from 'react-i18next'; -import headlampBrokenImage from '../../../assets/headlamp-broken.svg'; +import ErrorComponent from './ErrorPage'; -const useStyles = makeStyles(() => ({ - root: { - textAlign: 'center', - }, - h1: { - fontSize: '2.125rem', - lineHeight: 1.2, - fontWeight: 400, - }, - h2: { - fontSize: '1.25rem', - lineHeight: 3.6, - fontWeight: 500, - }, - img: { - width: '100%', - }, -})); - -export interface ErrorComponentProps { - /** The main title to display. By default it is: "Uh-oh! Something went wrong." */ - title?: React.ReactNode; - /** The message to display. By default it is: "Head back home." */ - message?: React.ReactNode; - /** The graphic or element to display as a main graphic. If used as a string, it will be - * used as the source for displaying an image. By default it is "headlamp-broken.svg". */ - graphic?: React.ReactChild; - /** Whether to use Typography or not. By default it is true. */ - withTypography?: boolean; -} - -export default function ErrorComponent(props: ErrorComponentProps) { - const { t } = useTranslation(); - const { - title = t('Uh-oh! Something went wrong.'), - message = '', - withTypography = true, - graphic = headlampBrokenImage, - } = props; - const classes = useStyles(); - return ( - - - {typeof graphic === 'string' ? ( - - ) : ( - graphic - )} - {withTypography ? ( - - {title} - - ) : ( - title - )} - {withTypography ? ( - - {!!message ? ( - message - ) : ( - - Head back home. - - )} - - ) : ( - message - )} - - - ); -} +export * from './ErrorPage'; +export default ErrorComponent; diff --git a/frontend/src/components/common/ObjectEventList.tsx b/frontend/src/components/common/ObjectEventList.tsx index a0cb906ab5..bdf41bb31c 100644 --- a/frontend/src/components/common/ObjectEventList.tsx +++ b/frontend/src/components/common/ObjectEventList.tsx @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next'; import { KubeObject } from '../../lib/k8s/cluster'; import Event, { KubeEvent } from '../../lib/k8s/event'; import { localeDate, timeAgo } from '../../lib/util'; +import { HeadlampEventType, useEventCallback } from '../../redux/headlampEventSlice'; import { HoverInfoLabel, SectionBox, SimpleTable } from '../common'; import ShowHideLabel from './ShowHideLabel'; @@ -11,7 +12,14 @@ export interface ObjectEventListProps { } export default function ObjectEventList(props: ObjectEventListProps) { - const [events, setEvents] = useState([]); + const [events, setEvents] = useState([]); + const dispatchEventList = useEventCallback(HeadlampEventType.OBJECT_EVENTS); + + useEffect(() => { + if (events) { + dispatchEventList(events, props.object); + } + }, [events]); async function fetchEvents() { const events = await Event.objectEvents(props.object); diff --git a/frontend/src/components/common/Resource/CreateButton.tsx b/frontend/src/components/common/Resource/CreateButton.tsx index 3bc9addfcc..27fb9ae189 100644 --- a/frontend/src/components/common/Resource/CreateButton.tsx +++ b/frontend/src/components/common/Resource/CreateButton.tsx @@ -7,6 +7,11 @@ import { useLocation } from 'react-router-dom'; import { apply } from '../../../lib/k8s/apiProxy'; import { KubeObjectInterface } from '../../../lib/k8s/cluster'; import { clusterAction } from '../../../redux/clusterActionSlice'; +import { + EventStatus, + HeadlampEventType, + useEventCallback, +} from '../../../redux/headlampEventSlice'; import ActionButton from '../ActionButton'; import EditorDialog from './EditorDialog'; @@ -21,6 +26,7 @@ export default function CreateButton(props: CreateButtonProps) { const [errorMessage, setErrorMessage] = React.useState(''); const location = useLocation(); const { t } = useTranslation(['translation']); + const dispatchCreateEvent = useEventCallback(HeadlampEventType.CREATE_RESOURCE); const applyFunc = async (newItems: KubeObjectInterface[]) => { await Promise.allSettled(newItems.map(newItem => apply(newItem))).then((values: any) => { @@ -89,6 +95,10 @@ export default function CreateButton(props: CreateButtonProps) { cancelUrl, }) ); + + dispatchCreateEvent({ + status: EventStatus.CONFIRMED, + }); } return ( @@ -105,7 +115,9 @@ export default function CreateButton(props: CreateButtonProps) { /> ) : (