Skip to content

Commit

Permalink
frontend: add restart bulk action for resources list
Browse files Browse the repository at this point in the history
Signed-off-by: adwait-godbole <adwaitngodbole@gmail.com>
  • Loading branch information
adwait-godbole committed Feb 2, 2025
1 parent f979bf5 commit a8b566a
Show file tree
Hide file tree
Showing 48 changed files with 777 additions and 128 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,20 @@
>
<div
class="MuiGrid-root MuiGrid-item css-13i4rnv-MuiGrid-root"
/>
>
<button
aria-label="Restart"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeMedium css-whz9ym-MuiButtonBase-root-MuiIconButton-root"
data-mui-internal-clone-element="true"
tabindex="0"
type="button"
>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
<div />
</div>
<div
class="MuiGrid-root MuiGrid-item css-13i4rnv-MuiGrid-root"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,20 @@
>
<div
class="MuiGrid-root MuiGrid-item css-13i4rnv-MuiGrid-root"
/>
>
<button
aria-label="Restart"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeMedium css-whz9ym-MuiButtonBase-root-MuiIconButton-root"
data-mui-internal-clone-element="true"
tabindex="0"
type="button"
>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
<div />
</div>
<div
class="MuiGrid-root MuiGrid-item css-13i4rnv-MuiGrid-root"
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { MRT_TableInstance } from 'material-react-table';
import { useCallback } from 'react';
import { KubeObject } from '../../../lib/k8s/KubeObject';
import DeleteMultipleButton from './DeleteMultipleButton';
import { isRestartableResource } from './RestartButton';
import RestartMultipleButton from './RestartMultipleButton';

export interface ResourceTableMultiActionsProps<RowItem extends Record<string, any>> {
table: MRT_TableInstance<RowItem>;
Expand All @@ -12,16 +14,26 @@ export default function ResourceTableMultiActions<RowItem extends Record<string,
props: ResourceTableMultiActionsProps<RowItem>
) {
const { table } = props;

const items = table.getSelectedRowModel().rows.map(t => t.original as unknown as KubeObject);
const restartableItems = items.filter(isRestartableResource);

const afterConfirm = useCallback(() => {
table.resetRowSelection();
}, [table]);

return (
<Grid item>
<Grid item container alignItems="center" justifyContent="flex-end">
<DeleteMultipleButton items={items} afterConfirm={afterConfirm} />
</Grid>
<Grid container spacing={2}>
{restartableItems.length > 0 && (
<Grid item>
<RestartMultipleButton items={restartableItems} afterConfirm={afterConfirm} />
</Grid>
)}
{items.length > 0 && (
<Grid item>
<DeleteMultipleButton items={items} afterConfirm={afterConfirm} />
</Grid>
)}
</Grid>
);
}
115 changes: 38 additions & 77 deletions frontend/src/components/common/Resource/RestartButton.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,12 @@
import {
Button,
Dialog,
DialogActions,
DialogContent,
DialogContentText,
DialogTitle,
} from '@mui/material';
import _ from 'lodash';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router';
import { apply } from '../../../lib/k8s/apiProxy';
import DaemonSet from '../../../lib/k8s/daemonSet';
import Deployment from '../../../lib/k8s/deployment';
import { KubeObject } from '../../../lib/k8s/KubeObject';
import ReplicaSet from '../../../lib/k8s/replicaSet';
import StatefulSet from '../../../lib/k8s/statefulSet';
import { clusterAction } from '../../../redux/clusterActionSlice';
import {
Expand All @@ -23,19 +16,30 @@ import {
} from '../../../redux/headlampEventSlice';
import { AppDispatch } from '../../../redux/stores/store';
import ActionButton, { ButtonStyle } from '../ActionButton';
import ConfirmDialog from '../ConfirmDialog';
import AuthVisible from './AuthVisible';

export type RestartableResource = Deployment | StatefulSet | DaemonSet;

export function isRestartableResource(item: KubeObject): item is RestartableResource {
return item instanceof Deployment || item instanceof StatefulSet || item instanceof DaemonSet;
}

interface RestartButtonProps {
item: Deployment | StatefulSet | ReplicaSet;
item: RestartableResource;
buttonStyle?: ButtonStyle;
afterConfirm?: () => void;
}

export function RestartButton(props: RestartButtonProps) {
const { item, buttonStyle } = props;
const { t } = useTranslation();
const [openDialog, setOpenDialog] = useState(false);
const dispatch: AppDispatch = useDispatch();

const { item, buttonStyle, afterConfirm } = props;
const [openDialog, setOpenDialog] = useState(false);
const location = useLocation();
const { t } = useTranslation(['translation']);
const dispatchRestartEvent = useEventCallback(HeadlampEventType.RESTART_RESOURCE);

function applyFunc() {
try {
const clonedItem = _.cloneDeep(item);
Expand All @@ -49,33 +53,22 @@ export function RestartButton(props: RestartButtonProps) {
}
}

function handleClose() {
setOpenDialog(false);
}

function handleSave() {
const cancelUrl = location.pathname;
const itemName = item.metadata.name;

setOpenDialog(false);

// setOpenDialog(false);
dispatch(
clusterAction(() => applyFunc(), {
startMessage: t('Restarting {{ itemName }}…', { itemName }),
cancelledMessage: t('Cancelled restarting {{ itemName }}.', { itemName }),
successMessage: t('Restarted {{ itemName }}.', { itemName }),
errorMessage: t('Failed to restart {{ itemName }}.', { itemName }),
cancelUrl,
errorUrl: cancelUrl,
cancelUrl: location.pathname,
startUrl: item.getListLink(),
errorUrl: item.getListLink(),
})
);
}

if (!item || !['Deployment', 'StatefulSet', 'DaemonSet'].includes(item.kind)) {
return null;
}

return (
<AuthVisible
item={item}
Expand All @@ -92,56 +85,24 @@ export function RestartButton(props: RestartButtonProps) {
}}
icon="mdi:restart"
/>
<RestartDialog resource={item} open={openDialog} onClose={handleClose} onSave={handleSave} />
<ConfirmDialog
open={openDialog}
title={t('translation|Restart')}
description={t('translation|Are you sure you want to restart {{ itemName }}?', {
itemName: item.metadata.name,
})}
handleClose={() => setOpenDialog(false)}
onConfirm={() => {
handleSave();
dispatchRestartEvent({
resource: item,
status: EventStatus.CONFIRMED,
});
if (afterConfirm) {
afterConfirm();
}
}}
/>
</AuthVisible>
);
}

interface RestartDialogProps {
resource: KubeObject;
open: boolean;
onClose: () => void;
onSave: () => void;
}

function RestartDialog(props: RestartDialogProps) {
const { resource, open, onClose, onSave } = props;
const { t } = useTranslation();
const dispatchRestartEvent = useEventCallback(HeadlampEventType.RESTART_RESOURCE);

return (
<Dialog
open={open}
onClose={onClose}
aria-labelledby="form-dialog-title"
maxWidth="xs"
fullWidth
>
<DialogTitle id="form-dialog-title">{t('translation|Restart')}</DialogTitle>
<DialogContent>
<DialogContentText>
{t('translation|Are you sure you want to restart {{ name }}?', {
name: resource.metadata.name,
})}
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={onClose} color="primary">
{t('translation|Cancel')}
</Button>
<Button
onClick={() => {
dispatchRestartEvent({
resource: resource,
status: EventStatus.CONFIRMED,
});
onSave();
}}
color="primary"
>
{t('translation|Restart')}
</Button>
</DialogActions>
</Dialog>
);
}
108 changes: 108 additions & 0 deletions frontend/src/components/common/Resource/RestartMultipleButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import _ from 'lodash';
import React from 'react';
import { useTranslation } from 'react-i18next';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import { apply } from '../../../lib/k8s/apiProxy';
import { clusterAction } from '../../../redux/clusterActionSlice';
import {
EventStatus,
HeadlampEventType,
useEventCallback,
} from '../../../redux/headlampEventSlice';
import { AppDispatch } from '../../../redux/stores/store';
import ActionButton, { ButtonStyle } from '../ActionButton';
import ConfirmDialog from '../ConfirmDialog';
import { RestartableResource } from './RestartButton';

interface RestartMultipleButtonProps {
items: RestartableResource[];
buttonStyle?: ButtonStyle;
afterConfirm?: () => void;
}

function RestartMultipleButtonDescription(props: Pick<RestartMultipleButtonProps, 'items'>) {
const { t } = useTranslation(['translation']);
return (
<p>
{t('Are you sure you want to restart the following items?')}
<ul>
{props.items.map(item => (
<li key={item.metadata.uid}>{item.metadata.name}</li>
))}
</ul>
</p>
);
}

export default function RestartMultipleButton(props: RestartMultipleButtonProps) {
const dispatch: AppDispatch = useDispatch();
const { items, buttonStyle, afterConfirm } = props;
const [openDialog, setOpenDialog] = React.useState(false);
const { t } = useTranslation(['translation']);
const location = useLocation();
const dispatchRestartEvent = useEventCallback(HeadlampEventType.RESTART_RESOURCES);

function applyFunc() {
return Promise.all(
items.map(item => {
try {
const clonedItem = _.cloneDeep(item);
clonedItem.spec.template.metadata.annotations = {
...clonedItem.spec.template.metadata.annotations,
'kubectl.kubernetes.io/restartedAt': new Date().toISOString(),
};
return apply(clonedItem.jsonData);
} catch (err) {
console.error('Error while restarting resource:', err);
return Promise.reject(err);
}
})
);
}

const handleSave = () => {
const itemsLength = items.length;

dispatch(
clusterAction(() => applyFunc(), {
startMessage: t('Restarting {{ itemsLength }} items…', { itemsLength }),
cancelledMessage: t('Cancelled restarting {{ itemsLength }} items.', { itemsLength }),
successMessage: t('Restarted {{ itemsLength }} items.', { itemsLength }),
errorMessage: t('Failed to restart {{ itemsLength }} items.', { itemsLength }),
cancelUrl: location.pathname,
startUrl: location.pathname,
errorUrl: location.pathname,
})
);
};

return (
<>
<ActionButton
description={t('translation|Restart items')}
buttonStyle={buttonStyle}
onClick={() => {
setOpenDialog(true);
}}
icon="mdi:restart"
/>
<ConfirmDialog
open={openDialog}
title={t('translation|Restart items')}
description={<RestartMultipleButtonDescription items={items} />}
handleClose={() => setOpenDialog(false)}
onConfirm={() => {
handleSave();
dispatchRestartEvent({
resources: items,
status: EventStatus.CONFIRMED,
});
if (afterConfirm) {
afterConfirm();
}
}}
/>
</>
);
}
1 change: 1 addition & 0 deletions frontend/src/components/common/Resource/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const checkExports = [
'resourceTableSlice',
'ResourceTableColumnChooser',
'RestartButton',
'RestartMultipleButton',
'ScaleButton',
'SimpleEditor',
'ViewButton',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,20 @@
>
<div
class="MuiGrid-root MuiGrid-item css-13i4rnv-MuiGrid-root"
/>
>
<button
aria-label="Restart"
class="MuiButtonBase-root MuiIconButton-root MuiIconButton-sizeMedium css-whz9ym-MuiButtonBase-root-MuiIconButton-root"
data-mui-internal-clone-element="true"
tabindex="0"
type="button"
>
<span
class="MuiTouchRipple-root css-8je8zh-MuiTouchRipple-root"
/>
</button>
<div />
</div>
<div
class="MuiGrid-root MuiGrid-item css-13i4rnv-MuiGrid-root"
/>
Expand Down
Loading

0 comments on commit a8b566a

Please sign in to comment.