Skip to content

Commit e72e21e

Browse files
committed
update actions menu
1 parent c30e8fa commit e72e21e

11 files changed

Lines changed: 147 additions & 68 deletions

src/views/schedule-actions/__fixtures__/schedule-actions-config.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export const mockPauseActionConfig: ScheduleAction<
2727
modal: {
2828
banner: {
2929
kind: 'warning',
30-
icon: ({ size }) => <MdOutlineWarningAmber size={size} />,
30+
icon: MdOutlineWarningAmber,
3131
render: () => 'Mock pause banner message',
3232
},
3333
withForm: false,

src/views/schedule-actions/config/schedule-actions-banner-icons.tsx

Lines changed: 0 additions & 7 deletions
This file was deleted.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import { type ScheduleActionDisabledValue } from '@/config/dynamic/resolvers/schedule-actions-enabled.types';
2+
3+
const SCHEDULE_ACTIONS_DISABLED_REASONS_CONFIG = {
4+
DISABLED_DEFAULT: 'Schedule action has been disabled',
5+
DISABLED_UNAUTHORIZED: 'Not authorized to perform this action',
6+
} as const satisfies Record<ScheduleActionDisabledValue, string>;
7+
8+
export default SCHEDULE_ACTIONS_DISABLED_REASONS_CONFIG;
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
const SCHEDULE_ACTIONS_NON_RUNNABLE_STATUSES_CONFIG = [
22
'NOT_RUNNABLE_SCHEDULE_ALREADY_PAUSED',
33
'NOT_RUNNABLE_SCHEDULE_NOT_PAUSED',
4-
] as const;
4+
] as const satisfies Array<`NOT_RUNNABLE_${string}`>;
55

66
export default SCHEDULE_ACTIONS_NON_RUNNABLE_STATUSES_CONFIG;

src/views/schedule-actions/config/schedule-actions.config.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { MdPauseCircleOutline, MdPlayCircleOutline } from 'react-icons/md';
1+
import {
2+
MdOutlineWarningAmber,
3+
MdPauseCircleOutline,
4+
MdPlayCircleOutline,
5+
} from 'react-icons/md';
26

37
import { type PauseScheduleResponse } from '@/route-handlers/pause-schedule/pause-schedule.types';
48
import { type UnpauseScheduleResponse } from '@/route-handlers/unpause-schedule/unpause-schedule.types';
@@ -8,7 +12,6 @@ import {
812
type ScheduleAction,
913
} from '../schedule-actions.types';
1014

11-
import { pauseScheduleBannerIcon } from './schedule-actions-banner-icons';
1215
import { PAUSE_SCHEDULE_MODAL_BANNER_MESSAGE } from './schedule-actions.constants';
1316

1417
const pauseScheduleActionConfig: ScheduleAction<
@@ -22,7 +25,7 @@ const pauseScheduleActionConfig: ScheduleAction<
2225
modal: {
2326
banner: {
2427
kind: 'warning',
25-
icon: pauseScheduleBannerIcon,
28+
icon: MdOutlineWarningAmber,
2629
render: () => PAUSE_SCHEDULE_MODAL_BANNER_MESSAGE,
2730
},
2831
withForm: false,
@@ -34,6 +37,7 @@ const pauseScheduleActionConfig: ScheduleAction<
3437
: 'RUNNABLE',
3538
apiRoute: (params) =>
3639
`/api/domains/${encodeURIComponent(params.domain)}/${encodeURIComponent(params.cluster)}/schedules/${encodeURIComponent(params.scheduleId)}/pause`,
40+
// TODO: get reason from UI form
3741
getConfirmSubmissionData: () => ({
3842
reason: 'Paused from Cadence Web UI',
3943
}),

src/views/schedule-actions/schedule-actions-menu/__tests__/schedule-actions-menu.test.tsx

Lines changed: 46 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,12 @@ import React from 'react';
33
import { render, screen, userEvent, within } from '@/test-utils/rtl';
44

55
import {
6-
getMockPausedDescribeScheduleResponse,
7-
getMockRunningDescribeScheduleResponse,
8-
mockDescribeScheduleResponse,
9-
} from '@/route-handlers/describe-schedule/__fixtures__/mock-describe-schedule-response';
6+
type ScheduleActionEnabledConfigValue,
7+
type ScheduleActionsEnabledConfig,
8+
} from '@/config/dynamic/resolvers/schedule-actions-enabled.types';
9+
import mockResolvedConfigValues from '@/utils/config/__fixtures__/resolved-config-values';
10+
11+
import { mockDescribeScheduleResponse } from '@/route-handlers/describe-schedule/__fixtures__/mock-describe-schedule-response';
1012

1113
import { mockScheduleActionsConfig } from '../../__fixtures__/schedule-actions-config';
1214
import ScheduleActionsMenu from '../schedule-actions-menu';
@@ -16,72 +18,74 @@ jest.mock(
1618
() => mockScheduleActionsConfig
1719
);
1820

21+
jest.mock('../helpers/get-action-disabled-reason', () =>
22+
jest.fn(
23+
({
24+
actionEnabledConfig,
25+
}: {
26+
actionEnabledConfig?: ScheduleActionEnabledConfigValue;
27+
}) =>
28+
actionEnabledConfig === 'ENABLED'
29+
? undefined
30+
: 'Mock schedule action disabled reason'
31+
)
32+
);
33+
1934
describe(ScheduleActionsMenu.name, () => {
2035
beforeEach(() => {
2136
jest.clearAllMocks();
2237
});
2338

2439
it('renders the menu items correctly', () => {
25-
setup({ schedule: mockDescribeScheduleResponse });
40+
setup({
41+
actionsEnabledConfig: mockResolvedConfigValues.SCHEDULE_ACTIONS_ENABLED,
42+
});
2643

2744
const menuButtons = screen.getAllByRole('button');
2845
expect(menuButtons).toHaveLength(2);
2946

3047
expect(within(menuButtons[0]).getByText('Mock pause')).toBeInTheDocument();
48+
expect(
49+
within(menuButtons[0]).getByText('Mock pause a schedule')
50+
).toBeInTheDocument();
3151
expect(menuButtons[0]).not.toBeDisabled();
3252

3353
expect(within(menuButtons[1]).getByText('Mock resume')).toBeInTheDocument();
34-
expect(menuButtons[1]).toBeDisabled();
35-
});
36-
37-
it('disables pause and enables resume when schedule is paused', () => {
38-
setup({ schedule: getMockPausedDescribeScheduleResponse() });
39-
40-
const menuButtons = screen.getAllByRole('button');
41-
expect(menuButtons[0]).toBeDisabled();
42-
expect(menuButtons[1]).not.toBeDisabled();
43-
});
44-
45-
it('shows tooltip when pause is disabled for a paused schedule', async () => {
46-
const { user } = setup({
47-
schedule: getMockPausedDescribeScheduleResponse(),
48-
});
49-
50-
await user.hover(screen.getAllByRole('button')[0]);
5154
expect(
52-
await screen.findByText('Schedule is already paused')
55+
within(menuButtons[1]).getByText('Mock resume a schedule')
5356
).toBeInTheDocument();
57+
expect(menuButtons[1]).not.toBeDisabled();
5458
});
5559

56-
it('shows tooltip when resume is disabled for a running schedule', async () => {
60+
it('disables menu items and shows tooltip if they are disabled from config', async () => {
5761
const { user } = setup({
58-
schedule: getMockRunningDescribeScheduleResponse(),
62+
actionsEnabledConfig: {
63+
pause: 'DISABLED_DEFAULT',
64+
resume: 'DISABLED_DEFAULT',
65+
},
5966
});
6067

61-
await user.hover(screen.getAllByRole('button')[1]);
68+
const menuButtons = screen.getAllByRole('button');
69+
expect(menuButtons[0]).toBeDisabled();
70+
expect(menuButtons[1]).toBeDisabled();
71+
72+
await user.hover(menuButtons[0]);
6273
expect(
63-
await screen.findByText('Schedule is not paused')
74+
await screen.findByText('Mock schedule action disabled reason')
6475
).toBeInTheDocument();
6576
});
6677

67-
it('disables all actions while schedule is loading', () => {
68-
setup({ schedule: undefined });
78+
it('disables menu items if no config is passed', () => {
79+
setup({ actionsEnabledConfig: undefined });
6980

7081
const menuButtons = screen.getAllByRole('button');
7182
expect(menuButtons[0]).toBeDisabled();
7283
expect(menuButtons[1]).toBeDisabled();
7384
});
7485

75-
it('shows tooltip while schedule is loading', async () => {
76-
const { user } = setup({ schedule: undefined });
77-
78-
await user.hover(screen.getAllByRole('button')[0]);
79-
expect(await screen.findByText('Loading schedule...')).toBeInTheDocument();
80-
});
81-
8286
it('calls onActionSelect when an enabled action button is clicked', async () => {
8387
const { user, mockOnActionSelect } = setup({
84-
schedule: mockDescribeScheduleResponse,
88+
actionsEnabledConfig: mockResolvedConfigValues.SCHEDULE_ACTIONS_ENABLED,
8589
});
8690

8791
await user.click(screen.getAllByRole('button')[0]);
@@ -92,16 +96,19 @@ describe(ScheduleActionsMenu.name, () => {
9296
});
9397

9498
function setup({
95-
schedule,
99+
schedule = mockDescribeScheduleResponse,
100+
actionsEnabledConfig,
96101
}: {
97-
schedule: typeof mockDescribeScheduleResponse | undefined;
102+
schedule?: typeof mockDescribeScheduleResponse | undefined;
103+
actionsEnabledConfig?: ScheduleActionsEnabledConfig;
98104
}) {
99105
const user = userEvent.setup();
100106
const mockOnActionSelect = jest.fn();
101107

102108
render(
103109
<ScheduleActionsMenu
104110
schedule={schedule}
111+
{...(actionsEnabledConfig && { actionsEnabledConfig })}
105112
onActionSelect={mockOnActionSelect}
106113
/>
107114
);

src/views/schedule-actions/schedule-actions-menu/helpers/__tests__/get-action-disabled-reason.test.ts

Lines changed: 58 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,49 @@
1+
import { type ScheduleActionEnabledConfigValue } from '@/config/dynamic/resolvers/schedule-actions-enabled.types';
2+
13
import { type ScheduleActionRunnableStatus } from '../../../schedule-actions.types';
24
import getActionDisabledReason from '../get-action-disabled-reason';
35

46
describe(getActionDisabledReason.name, () => {
5-
it('returns undefined when action is runnable', () => {
7+
it('returns undefined when action is enabled and runnable', () => {
68
expect(
7-
getActionDisabledReason({ actionRunnableStatus: 'RUNNABLE' })
9+
getActionDisabledReason({
10+
actionEnabledConfig: 'ENABLED',
11+
actionRunnableStatus: 'RUNNABLE',
12+
})
813
).toBeUndefined();
914
});
1015

16+
it('returns reason when action is disabled by default', () => {
17+
expect(
18+
getActionDisabledReason({
19+
actionEnabledConfig: 'DISABLED_DEFAULT',
20+
actionRunnableStatus: 'RUNNABLE',
21+
})
22+
).toBe('Schedule action has been disabled');
23+
});
24+
25+
it('returns reason when action is disabled due to unauthorized access', () => {
26+
expect(
27+
getActionDisabledReason({
28+
actionEnabledConfig: 'DISABLED_UNAUTHORIZED',
29+
actionRunnableStatus: 'RUNNABLE',
30+
})
31+
).toBe('Not authorized to perform this action');
32+
});
33+
34+
it('returns disabled reason over non-runnable reason when both apply', () => {
35+
expect(
36+
getActionDisabledReason({
37+
actionEnabledConfig: 'DISABLED_DEFAULT',
38+
actionRunnableStatus: 'NOT_RUNNABLE_SCHEDULE_ALREADY_PAUSED',
39+
})
40+
).toBe('Schedule action has been disabled');
41+
});
42+
1143
it('returns reason when schedule is already paused', () => {
1244
expect(
1345
getActionDisabledReason({
46+
actionEnabledConfig: 'ENABLED',
1447
actionRunnableStatus: 'NOT_RUNNABLE_SCHEDULE_ALREADY_PAUSED',
1548
})
1649
).toBe('Schedule is already paused');
@@ -19,14 +52,37 @@ describe(getActionDisabledReason.name, () => {
1952
it('returns reason when schedule is not paused', () => {
2053
expect(
2154
getActionDisabledReason({
55+
actionEnabledConfig: 'ENABLED',
2256
actionRunnableStatus: 'NOT_RUNNABLE_SCHEDULE_NOT_PAUSED',
2357
})
2458
).toBe('Schedule is not paused');
2559
});
2660

61+
it('returns disabled default reason when no config is provided', () => {
62+
expect(
63+
getActionDisabledReason({
64+
actionRunnableStatus: 'RUNNABLE',
65+
})
66+
).toBe('Schedule action has been disabled');
67+
});
68+
2769
it('returns undefined when runnable status is not provided', () => {
2870
expect(
2971
getActionDisabledReason({
72+
actionEnabledConfig: 'ENABLED',
73+
actionRunnableStatus: undefined as
74+
| ScheduleActionRunnableStatus
75+
| undefined,
76+
})
77+
).toBeUndefined();
78+
});
79+
80+
it('returns undefined when neither config nor runnable status is provided', () => {
81+
expect(
82+
getActionDisabledReason({
83+
actionEnabledConfig: undefined as
84+
| ScheduleActionEnabledConfigValue
85+
| undefined,
3086
actionRunnableStatus: undefined as
3187
| ScheduleActionRunnableStatus
3288
| undefined,

src/views/schedule-actions/schedule-actions-menu/helpers/get-action-disabled-reason.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,26 @@
1+
import { type ScheduleActionEnabledConfigValue } from '@/config/dynamic/resolvers/schedule-actions-enabled.types';
2+
3+
import SCHEDULE_ACTIONS_DISABLED_REASONS_CONFIG from '../../config/schedule-actions-disabled-reasons.config';
14
import SCHEDULE_ACTIONS_NON_RUNNABLE_REASONS_CONFIG from '../../config/schedule-actions-non-runnable-reasons.config';
25
import { type ScheduleActionRunnableStatus } from '../../schedule-actions.types';
36

47
export default function getActionDisabledReason({
8+
actionEnabledConfig,
59
actionRunnableStatus,
610
}: {
11+
actionEnabledConfig?: ScheduleActionEnabledConfigValue;
712
actionRunnableStatus?: ScheduleActionRunnableStatus;
813
}): string | undefined {
914
if (!actionRunnableStatus) {
1015
return undefined;
1116
}
1217

18+
if (actionEnabledConfig !== 'ENABLED') {
19+
return SCHEDULE_ACTIONS_DISABLED_REASONS_CONFIG[
20+
actionEnabledConfig ?? 'DISABLED_DEFAULT'
21+
];
22+
}
23+
1324
if (actionRunnableStatus !== 'RUNNABLE') {
1425
return SCHEDULE_ACTIONS_NON_RUNNABLE_REASONS_CONFIG[actionRunnableStatus];
1526
}

src/views/schedule-actions/schedule-actions-menu/schedule-actions-menu.tsx

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,20 +7,20 @@ import getActionDisabledReason from './helpers/get-action-disabled-reason';
77
import { overrides, styled } from './schedule-actions-menu.styles';
88
import { type Props } from './schedule-actions-menu.types';
99

10-
const SCHEDULE_LOADING_DISABLED_REASON = 'Loading schedule...';
11-
1210
export default function ScheduleActionsMenu({
1311
schedule,
12+
actionsEnabledConfig,
1413
onActionSelect,
1514
}: Props) {
1615
return (
1716
<styled.MenuItemsContainer>
1817
{scheduleActionsConfig.map((action) => {
19-
const actionDisabledReason = schedule
20-
? getActionDisabledReason({
21-
actionRunnableStatus: action.getRunnableStatus(schedule),
22-
})
23-
: SCHEDULE_LOADING_DISABLED_REASON;
18+
const actionDisabledReason = getActionDisabledReason({
19+
actionEnabledConfig: actionsEnabledConfig?.[action.id],
20+
actionRunnableStatus: schedule
21+
? action.getRunnableStatus(schedule)
22+
: undefined,
23+
});
2424

2525
return (
2626
<StatefulTooltip
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import { type ScheduleActionsEnabledConfig } from '@/config/dynamic/resolvers/schedule-actions-enabled.types';
12
import { type DescribeScheduleResponse } from '@/route-handlers/describe-schedule/describe-schedule.types';
23

34
import { type ScheduleAction } from '../schedule-actions.types';
45

56
export type Props = {
67
schedule: DescribeScheduleResponse | undefined;
8+
actionsEnabledConfig?: ScheduleActionsEnabledConfig;
79
onActionSelect: (action: ScheduleAction<any, any, any>) => void;
810
};

0 commit comments

Comments
 (0)