Skip to content

Commit

Permalink
frontend: refactor row actions and header actions
Browse files Browse the repository at this point in the history
Signed-off-by: farodin91 <[email protected]>
  • Loading branch information
farodin91 committed Dec 14, 2024
1 parent d94fde1 commit 68179b8
Show file tree
Hide file tree
Showing 6 changed files with 144 additions and 154 deletions.
2 changes: 1 addition & 1 deletion frontend/src/components/common/Resource/EditButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ export default function EditButton(props: EditButtonProps) {
}

if (isReadOnly) {
return <ViewButton item={item} />;
return <ViewButton item={item} buttonStyle={buttonStyle} />;
}

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,19 +1,9 @@
import { has } from 'lodash';
import React, { isValidElement } from 'react';
import React from 'react';
import { useLocation } from 'react-router-dom';
import { KubeObject } from '../../../../lib/k8s/KubeObject';
import {
DefaultHeaderAction,
HeaderAction,
HeaderActionType,
} from '../../../../redux/actionButtonsSlice';
import { useTypedSelector } from '../../../../redux/reducers/reducers';
import ErrorBoundary from '../../ErrorBoundary';
import { HeaderAction } from '../../../../redux/actionButtonsSlice';
import SectionHeader, { HeaderStyle } from '../../SectionHeader';
import DeleteButton from '../DeleteButton';
import EditButton from '../EditButton';
import { RestartButton } from '../RestartButton';
import ScaleButton from '../ScaleButton';
import { generateActions } from '../generateHeaderActions';

export interface MainInfoHeaderProps<T extends KubeObject> {
resource: T | null;
Expand All @@ -32,90 +22,7 @@ export interface MainInfoHeaderProps<T extends KubeObject> {

export function MainInfoHeader<T extends KubeObject>(props: MainInfoHeaderProps<T>) {
const { resource, title, actions = [], headerStyle = 'main', noDefaultActions = false } = props;
const headerActions = useTypedSelector(state => state.actionButtons.headerActions);
const headerActionsProcessors = useTypedSelector(
state => state.actionButtons.headerActionsProcessors
);
function setupAction(headerAction: HeaderAction) {
let Action = has(headerAction, 'action') ? (headerAction as any).action : headerAction;

if (!noDefaultActions && has(headerAction, 'id')) {
switch ((headerAction as HeaderAction).id) {
case DefaultHeaderAction.RESTART:
Action = RestartButton;
break;
case DefaultHeaderAction.SCALE:
Action = ScaleButton;
break;
case DefaultHeaderAction.EDIT:
Action = EditButton;
break;
case DefaultHeaderAction.DELETE:
Action = DeleteButton;
break;
default:
break;
}
}

if (!Action || (headerAction as unknown as HeaderAction).action === null) {
return null;
}

if (isValidElement(Action)) {
return <ErrorBoundary>{Action}</ErrorBoundary>;
} else if (Action === null) {
return null;
} else if (typeof Action === 'function') {
return (
<ErrorBoundary>
<Action item={resource} />
</ErrorBoundary>
);
}
}

const defaultActions = [
{
id: DefaultHeaderAction.RESTART,
},
{
id: DefaultHeaderAction.SCALE,
},
{
id: DefaultHeaderAction.EDIT,
},
{
id: DefaultHeaderAction.DELETE,
},
];

let hAccs: HeaderAction[] = [];
const accs = typeof actions === 'function' ? actions(resource) || [] : actions;
if (accs !== null) {
hAccs = [...accs].map((action, i): HeaderAction => {
if ((action as HeaderAction)?.id !== undefined) {
return action as HeaderAction;
} else {
return { id: `gen-${i}`, action: action as HeaderActionType };
}
});
}

let actionsProcessed = [...headerActions, ...hAccs, ...defaultActions];
if (headerActionsProcessors.length > 0) {
for (const headerProcessor of headerActionsProcessors) {
actionsProcessed = headerProcessor.processor(resource, actionsProcessed);
}
}

const allActions = React.Children.toArray(
(function propsActions() {
const pluginAddedActions = actionsProcessed.map(setupAction);
return React.Children.toArray(pluginAddedActions);
})()
);

const allActions = generateActions(resource, 'action', actions, noDefaultActions);
return (
<SectionHeader
title={title || (resource ? `${resource.kind}: ${resource.getName()}` : '')}
Expand Down
58 changes: 7 additions & 51 deletions frontend/src/components/common/Resource/ResourceTable.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { MenuItem, TableCellProps } from '@mui/material';
import { TableCellProps } from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { MRT_FilterFns, MRT_Row, MRT_SortingFn, MRT_TableInstance } from 'material-react-table';
import { ComponentProps, ReactNode, useEffect, useMemo, useRef, useState } from 'react';
Expand All @@ -9,7 +9,7 @@ import { ApiError } from '../../../lib/k8s/apiProxy';
import { KubeObject } from '../../../lib/k8s/KubeObject';
import { KubeObjectClass } from '../../../lib/k8s/KubeObject';
import { useFilterFunc } from '../../../lib/util';
import { DefaultHeaderAction, RowAction } from '../../../redux/actionButtonsSlice';
import { HeaderAction } from '../../../redux/actionButtonsSlice';
import { useNamespaces } from '../../../redux/filterSlice';
import { HeadlampEventType, useEventCallback } from '../../../redux/headlampEventSlice';
import { useTypedSelector } from '../../../redux/reducers/reducers';
Expand All @@ -18,12 +18,8 @@ import { ClusterGroupErrorMessage } from '../../cluster/ClusterGroupErrorMessage
import { DateLabel } from '../Label';
import Link from '../Link';
import Table, { TableColumn } from '../Table';
import DeleteButton from './DeleteButton';
import EditButton from './EditButton';
import generateRowActionsMenu from './generateHeaderActions';
import ResourceTableMultiActions from './ResourceTableMultiActions';
import { RestartButton } from './RestartButton';
import ScaleButton from './ScaleButton';
import ViewButton from './ViewButton';

export type ResourceTableColumn<RowItem> = {
/** Unique id for the column, not required but recommended */
Expand Down Expand Up @@ -87,7 +83,7 @@ export interface ResourceTableProps<RowItem> {
enableRowActions?: boolean;
/** Show or hide row selections and actions @default false*/
enableRowSelection?: boolean;
actions?: null | RowAction[];
actions?: null | HeaderAction[];
/** Provide a list of columns that won't be shown and cannot be turned on */
hideColumns?: string[] | null;
/** ID for the table. Will be used by plugins to identify this table.
Expand Down Expand Up @@ -416,52 +412,12 @@ function ResourceTableContent<RowItem extends KubeObject>(props: ResourceTablePr
tableSettings,
]);

const defaultActions: RowAction[] = [
{
id: DefaultHeaderAction.RESTART,
action: ({ item }) => <RestartButton item={item} buttonStyle="menu" />,
},
{
id: DefaultHeaderAction.SCALE,
action: ({ item }) => <ScaleButton item={item} buttonStyle="menu" />,
},
{
id: DefaultHeaderAction.EDIT,
action: ({ item, closeMenu }) => (
<EditButton item={item} buttonStyle="menu" afterConfirm={closeMenu} />
),
},
{
id: DefaultHeaderAction.VIEW,
action: ({ item }) => <ViewButton item={item} buttonStyle="menu" />,
},
{
id: DefaultHeaderAction.DELETE,
action: ({ item, closeMenu }) => (
<DeleteButton item={item} buttonStyle="menu" afterConfirm={closeMenu} />
),
},
];
let hAccs: RowAction[] = [];
if (actions !== undefined && actions !== null) {
hAccs = actions;
}

const actionsProcessed: RowAction[] = [...hAccs, ...defaultActions];

const renderRowActionMenuItems = useMemo(() => {
if (actionsProcessed.length === 0) {
if (!enableRowActions) {
return null;
}
return ({ closeMenu, row }: { closeMenu: () => void; row: MRT_Row<Record<string, any>> }) => {
return actionsProcessed.map(action => {
if (action.action === undefined || action.action === null) {
return <MenuItem />;
}
return action.action({ item: row.original, closeMenu });
});
};
}, [actionsProcessed]);
return generateRowActionsMenu(actions);
}, [actions, enableRowActions]);

const wrappedEnableRowSelection = useMemo(() => {
if (import.meta.env.REACT_APP_HEADLAMP_ENABLE_ROW_SELECTION === 'false') {
Expand Down
130 changes: 130 additions & 0 deletions frontend/src/components/common/Resource/generateHeaderActions.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { has } from 'lodash';
import { MRT_Row } from 'material-react-table';
import { isValidElement } from 'react';
import React from 'react';
import { KubeObject } from '../../../lib/k8s/KubeObject';
import {
DefaultHeaderAction,
HeaderAction,
HeaderActionType,
} from '../../../redux/actionButtonsSlice';
import { useTypedSelector } from '../../../redux/reducers/reducers';
import { ButtonStyle } from '../ActionButton/ActionButton';
import ErrorBoundary from '../ErrorBoundary';
import DeleteButton from './DeleteButton';
import EditButton from './EditButton';
import { RestartButton } from './RestartButton';
import ScaleButton from './ScaleButton';

export function generateActions<T extends KubeObject>(
resource: T | null,
buttonStyle: ButtonStyle,
actions:
| ((resource: T | null) => React.ReactNode[] | HeaderAction[] | null)
| React.ReactNode[]
| null
| HeaderAction[],
noDefaultActions?: boolean,
closeMenu?: () => void
): React.ReactNode[] {
const headerActions = useTypedSelector(state => state.actionButtons.headerActions);
const headerActionsProcessors = useTypedSelector(
state => state.actionButtons.headerActionsProcessors
);
function setupAction(headerAction: HeaderAction) {
let Action = has(headerAction, 'action') ? (headerAction as any).action : headerAction;

if (!noDefaultActions && has(headerAction, 'id')) {
switch ((headerAction as HeaderAction).id) {
case DefaultHeaderAction.RESTART:
Action = RestartButton;
break;
case DefaultHeaderAction.SCALE:
Action = ScaleButton;
break;
case DefaultHeaderAction.EDIT:
Action = EditButton;
break;
case DefaultHeaderAction.DELETE:
Action = DeleteButton;
break;
default:
break;
}
}

if (!Action || (headerAction as unknown as HeaderAction).action === null) {
return null;
}

if (isValidElement(Action)) {
return <ErrorBoundary>{Action}</ErrorBoundary>;
} else if (Action === null) {
return null;
} else if (typeof Action === 'function') {
return (
<ErrorBoundary>
<Action item={resource} buttonStyle={buttonStyle} closeMenu={closeMenu} />
</ErrorBoundary>
);
}
}

const defaultActions = [
{
id: DefaultHeaderAction.RESTART,
},
{
id: DefaultHeaderAction.SCALE,
},
{
id: DefaultHeaderAction.EDIT,
},
{
id: DefaultHeaderAction.DELETE,
},
];

let hAccs: HeaderAction[] = [];
const accs = typeof actions === 'function' ? actions(resource) || [] : actions;
if (accs !== null) {
hAccs = [...accs].map((action, i): HeaderAction => {
if ((action as HeaderAction)?.id !== undefined) {
return action as HeaderAction;
} else {
return { id: `gen-${i}`, action: action as HeaderActionType };
}
});
}

let actionsProcessed = [...headerActions, ...hAccs, ...defaultActions];
if (headerActionsProcessors.length > 0) {
for (const headerProcessor of headerActionsProcessors) {
actionsProcessed = headerProcessor.processor(resource, actionsProcessed);
}
}

const allActions = React.Children.toArray(
(function propsActions() {
const pluginAddedActions = actionsProcessed.map(setupAction);
return React.Children.toArray(pluginAddedActions);
})()
);
return allActions;
}

export default function generateRowActionsMenu(actions: HeaderAction[] | null | undefined) {
return ({ closeMenu, row }: { closeMenu: () => void; row: MRT_Row<Record<string, any>> }) => {
const actionsProcessed = generateActions(
row.original as any,
'menu',
actions || [],
false,
closeMenu
);
if (actionsProcessed.length === 0) {
return null;
}
return actionsProcessed;
};
}
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 @@ -35,6 +35,7 @@ const checkExports = [
'SimpleEditor',
'ViewButton',
'AuthVisible',
'generateHeaderActions',
];

function getFilesToVerify() {
Expand Down
6 changes: 1 addition & 5 deletions frontend/src/redux/actionButtonsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,13 @@ export type HeaderActionType = ((...args: any[]) => ReactNode) | null | ReactEle
export type DetailsViewFunc = HeaderActionType;

export type AppBarActionType = ((...args: any[]) => ReactNode) | null | ReactElement | ReactNode;
export type RowActionType = ((item: any) => JSX.Element | null | ReactNode) | null;

export type HeaderAction = {
id: string;
action?: HeaderActionType;
};

export type RowAction = {
id: string;
action?: RowActionType;
};
export type RowAction = HeaderAction;

export type AppBarAction = {
id: string;
Expand Down

0 comments on commit 68179b8

Please sign in to comment.