Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

frontend: refactor row action menus #2621

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
frontend: refactor row actions and header actions
Signed-off-by: farodin91 <github@jan-jansen.net>
  • Loading branch information
farodin91 committed Dec 14, 2024
commit 68179b893b4a064cbce519275b2846c4fbdb64da
2 changes: 1 addition & 1 deletion frontend/src/components/common/Resource/EditButton.tsx
Original file line number Diff line number Diff line change
@@ -87,7 +87,7 @@ export default function EditButton(props: EditButtonProps) {
}

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

return (
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;
@@ -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()}` : '')}
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';
@@ -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';
@@ -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 */
@@ -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 */
Comment on lines -90 to +86
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to leave this with the previous type because plugins may be using it and it would mean a type break for them.

hideColumns?: string[] | null;
/** ID for the table. Will be used by plugins to identify this table.
@@ -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') {
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
@@ -35,6 +35,7 @@ const checkExports = [
'SimpleEditor',
'ViewButton',
'AuthVisible',
'generateHeaderActions',
];

function getFilesToVerify() {
6 changes: 1 addition & 5 deletions frontend/src/redux/actionButtonsSlice.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading