Skip to content

Commit

Permalink
Merge pull request #1455 from headlamp-k8s/headlamp-events
Browse files Browse the repository at this point in the history
frontend: Add a way for plugins to react to Headlamp events
  • Loading branch information
joaquimrocha authored Feb 7, 2024
2 parents 06660f0 + b38d029 commit 253e709
Show file tree
Hide file tree
Showing 31 changed files with 41,007 additions and 106 deletions.
11 changes: 11 additions & 0 deletions docs/development/plugins/functionality.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,3 +155,14 @@ Change what tables across Headlamp show with [registerResourceTableColumnsProces

- Example plugin shows [How to add a context menu to each row in the pods list table](https://github.com/kinvolk/headlamp/tree/main/plugins/examples/tables).
- API reference for [registerResourceTableColumnsProcessor](../api/modules/plugin_registry.md#registerresourcetablecolumnsprocessor)

### Headlamp Events

Headlamp has the concept of "Headlamp events". Those are fired when something relevant happens in Headlamp.

React to Headlamp events with [registerHeadlampEventCallback](../api/modules/plugin_registry.md#registerheadlampeventcallback).

![screenshot of a snackbar notification when an event occurred](./images/event-snackbar.png)

- Example plugin shows [How to show snackbars for Headlamp events](https://github.com/kinvolk/headlamp/tree/main/plugins/examples/headlamp-events).
- API reference for [registerHeadlampEventCallback](../api/modules/plugin_registry.md#registerheadlampeventcallback)
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -41,6 +43,9 @@ export default class ErrorBoundary extends Component<ErrorBoundaryProps, State>

render() {
const { error } = this.state;
if (error) {
store.dispatch(eventAction({ type: HeadlampEventType.ERROR_BOUNDARY, data: error }));
}
if (!error) {
return this.props.children;
}
Expand Down
86 changes: 86 additions & 0 deletions frontend/src/components/common/ErrorPage/ErrorPage.tsx
Original file line number Diff line number Diff line change
@@ -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 <a href="..."> home</a>." */
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 (
<Grid
container
spacing={0}
direction="column"
alignItems="center"
justifyContent="center"
className={classes.root}
>
<Grid item xs={12}>
{typeof graphic === 'string' ? (
<img src={graphic} alt="" className={classes.img} />
) : (
graphic
)}
{withTypography ? (
<Typography variant="h1" className={classes.h1}>
{title}
</Typography>
) : (
title
)}
{withTypography ? (
<Typography variant="h2" className={classes.h2}>
{!!message ? (
message
) : (
<Trans t={t}>
Head back <Link href="/">home</Link>.
</Trans>
)}
</Typography>
) : (
message
)}
</Grid>
</Grid>
);
}
88 changes: 3 additions & 85 deletions frontend/src/components/common/ErrorPage/index.tsx
Original file line number Diff line number Diff line change
@@ -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 <a href="..."> home</a>." */
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 (
<Grid
container
spacing={0}
direction="column"
alignItems="center"
justifyContent="center"
className={classes.root}
>
<Grid item xs={12}>
{typeof graphic === 'string' ? (
<img src={graphic} alt="" className={classes.img} />
) : (
graphic
)}
{withTypography ? (
<Typography variant="h1" className={classes.h1}>
{title}
</Typography>
) : (
title
)}
{withTypography ? (
<Typography variant="h2" className={classes.h2}>
{!!message ? (
message
) : (
<Trans t={t}>
Head back <Link href="/">home</Link>.
</Trans>
)}
</Typography>
) : (
message
)}
</Grid>
</Grid>
);
}
export * from './ErrorPage';
export default ErrorComponent;
10 changes: 9 additions & 1 deletion frontend/src/components/common/ObjectEventList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -11,7 +12,14 @@ export interface ObjectEventListProps {
}

export default function ObjectEventList(props: ObjectEventListProps) {
const [events, setEvents] = useState([]);
const [events, setEvents] = useState<Event[]>([]);
const dispatchEventList = useEventCallback(HeadlampEventType.OBJECT_EVENTS);

useEffect(() => {
if (events) {
dispatchEventList(events, props.object);
}
}, [events]);

async function fetchEvents() {
const events = await Event.objectEvents(props.object);
Expand Down
14 changes: 13 additions & 1 deletion frontend/src/components/common/Resource/CreateButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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) => {
Expand Down Expand Up @@ -89,6 +95,10 @@ export default function CreateButton(props: CreateButtonProps) {
cancelUrl,
})
);

dispatchCreateEvent({
status: EventStatus.CONFIRMED,
});
}

return (
Expand All @@ -105,7 +115,9 @@ export default function CreateButton(props: CreateButtonProps) {
/>
) : (
<Button
onClick={() => setOpenDialog(true)}
onClick={() => {
setOpenDialog(true);
}}
startIcon={<InlineIcon icon="mdi:plus" />}
color="primary"
variant="contained"
Expand Down
18 changes: 16 additions & 2 deletions frontend/src/components/common/Resource/DeleteButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { KubeObject } from '../../../lib/k8s/cluster';
import { CallbackActionOptions, clusterAction } from '../../../redux/clusterActionSlice';
import {
EventStatus,
HeadlampEventType,
useEventCallback,
} from '../../../redux/headlampEventSlice';
import ActionButton from '../ActionButton';
import { ConfirmDialog } from '../Dialog';
import AuthVisible from './AuthVisible';
Expand All @@ -19,6 +24,7 @@ export default function DeleteButton(props: DeleteButtonProps) {
const [openAlert, setOpenAlert] = React.useState(false);
const location = useLocation();
const { t } = useTranslation(['translation']);
const dispatchDeleteEvent = useEventCallback(HeadlampEventType.DELETE_RESOURCE);

const deleteFunc = React.useCallback(
() => {
Expand Down Expand Up @@ -62,15 +68,23 @@ export default function DeleteButton(props: DeleteButtonProps) {
>
<ActionButton
description={t('translation|Delete')}
onClick={() => setOpenAlert(true)}
onClick={() => {
setOpenAlert(true);
}}
icon="mdi:delete"
/>
<ConfirmDialog
open={openAlert}
title={t('translation|Delete item')}
description={t('translation|Are you sure you want to delete this item?')}
handleClose={() => setOpenAlert(false)}
onConfirm={() => deleteFunc()}
onConfirm={() => {
deleteFunc();
dispatchDeleteEvent({
resource: item,
status: EventStatus.CONFIRMED,
});
}}
/>
</AuthVisible>
);
Expand Down
19 changes: 18 additions & 1 deletion frontend/src/components/common/Resource/EditButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { KubeObject, KubeObjectInterface } from '../../../lib/k8s/cluster';
import { CallbackActionOptions, clusterAction } from '../../../redux/clusterActionSlice';
import {
EventStatus,
HeadlampEventType,
useEventCallback,
} from '../../../redux/headlampEventSlice';
import ActionButton from '../ActionButton';
import AuthVisible from './AuthVisible';
import EditorDialog from './EditorDialog';
Expand All @@ -22,6 +27,7 @@ export default function EditButton(props: EditButtonProps) {
const [errorMessage, setErrorMessage] = React.useState<string>('');
const location = useLocation();
const { t } = useTranslation(['translation', 'resource']);
const dispatchHeadlampEditEvent = useEventCallback(HeadlampEventType.EDIT_RESOURCE);

function makeErrorMessage(err: any) {
const status: number = err.status;
Expand Down Expand Up @@ -62,6 +68,11 @@ export default function EditButton(props: EditButtonProps) {
...options,
})
);

dispatchHeadlampEditEvent({
resource: item,
status: EventStatus.CLOSED,
});
}

if (!item) {
Expand All @@ -86,7 +97,13 @@ export default function EditButton(props: EditButtonProps) {
>
<ActionButton
description={t('translation|Edit')}
onClick={() => setOpenDialog(true)}
onClick={() => {
setOpenDialog(true);
dispatchHeadlampEditEvent({
resource: item,
status: EventStatus.OPENED,
});
}}
icon="mdi:pencil"
/>
{openDialog && (
Expand Down
Loading

0 comments on commit 253e709

Please sign in to comment.