Skip to content

Commit 68a7e90

Browse files
Assem-UberCopilot
andauthored
Add start button to domain page (#1031)
* Support optional fields in start api * Create button component with skeleton loading Signed-off-by: Assem Hafez <[email protected]> * remove button constants Signed-off-by: Assem Hafez <[email protected]> * remove extra opacity field and unused styles Signed-off-by: Assem Hafez <[email protected]> * Add optional fields section to form Signed-off-by: Assem Hafez <[email protected]> * Fix cron test case Signed-off-by: Assem Hafez <[email protected]> * Update src/components/button/__tests__/button.test.tsx Co-authored-by: Copilot <[email protected]> * use userEvent from rtl Signed-off-by: Assem Hafez <[email protected]> * Update src/views/workflow-actions/workflow-action-start-form/workflow-action-start-form.tsx Co-authored-by: Copilot <[email protected]> * Update src/views/workflow-actions/workflow-action-start-form/__tests__/workflow-action-start-form.test.tsx Co-authored-by: Copilot <[email protected]> * Add start workflow feature flag Signed-off-by: Assem Hafez <[email protected]> * fix type issue Signed-off-by: Assem Hafez <[email protected]> * add start to workflow actions configs Signed-off-by: Assem Hafez <[email protected]> * add start button to domain page Signed-off-by: Assem Hafez <[email protected]> * fix lint issue Signed-off-by: Assem Hafez <[email protected]> * revert changes workflow-action-start-form * delete added files * Fix nits and help button size Signed-off-by: Assem Hafez <[email protected]> * fix type check Signed-off-by: Assem Hafez <[email protected]> * Update domain button size Signed-off-by: Assem Hafez <[email protected]> --------- Signed-off-by: Assem Hafez <[email protected]> Co-authored-by: Copilot <[email protected]>
1 parent 8674f75 commit 68a7e90

File tree

7 files changed

+336
-3
lines changed

7 files changed

+336
-3
lines changed

src/views/domain-page/domain-page-help/domain-page-help.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,8 @@ export default function DomainPageHelp() {
3939
<Button
4040
size="compact"
4141
kind="secondary"
42-
startEnhancer={<MdSupport size={20} />}
43-
endEnhancer={<MdArrowDropDown size={20} />}
42+
startEnhancer={<MdSupport size={16} />}
43+
endEnhancer={<MdArrowDropDown size={16} />}
4444
>
4545
Help
4646
</Button>
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
import React from 'react';
2+
3+
import { waitFor } from '@testing-library/react';
4+
import { userEvent } from '@testing-library/user-event';
5+
import { HttpResponse } from 'msw';
6+
7+
import { render, screen } from '@/test-utils/rtl';
8+
9+
import { type WorkflowActionEnabledConfigValue } from '@/config/dynamic/resolvers/workflow-actions-enabled.types';
10+
import mockResolvedConfigValues from '@/utils/config/__fixtures__/resolved-config-values';
11+
import {
12+
mockWorkflowActionsConfig,
13+
mockStartActionConfig,
14+
} from '@/views/workflow-actions/__fixtures__/workflow-actions-config';
15+
import getActionDisabledReason from '@/views/workflow-actions/workflow-actions-menu/helpers/get-action-disabled-reason';
16+
17+
import DomainPageStartWorkflowButton from '../domain-page-start-workflow-button';
18+
import type { Props } from '../domain-page-start-workflow-button.types';
19+
20+
jest.mock('../../../workflow-actions/config/workflow-actions.config', () => {
21+
return {
22+
default: mockWorkflowActionsConfig,
23+
startWorkflowActionConfig: mockStartActionConfig,
24+
};
25+
});
26+
27+
jest.mock('@/components/button/button', () =>
28+
jest.fn((props) => {
29+
return (
30+
<button onClick={props.onClick} data-testid="start-workflow-button">
31+
{JSON.stringify({
32+
isLoading: props.isLoading,
33+
disabled: props.disabled,
34+
})}
35+
</button>
36+
);
37+
})
38+
);
39+
40+
// mock StatefulTooltip
41+
jest.mock('baseui/tooltip', () => {
42+
return {
43+
...jest.requireActual('baseui/tooltip'),
44+
StatefulTooltip: jest.fn((props) => {
45+
return (
46+
<>
47+
<div data-testid="tooltip">{props.content}</div>
48+
{props.children}
49+
</>
50+
);
51+
}),
52+
};
53+
});
54+
55+
jest.mock(
56+
'../../../workflow-actions/workflow-actions-menu/helpers/get-action-disabled-reason',
57+
() =>
58+
jest.fn(
59+
({
60+
actionEnabledConfig,
61+
}: {
62+
actionEnabledConfig?: WorkflowActionEnabledConfigValue;
63+
}) =>
64+
actionEnabledConfig === 'ENABLED'
65+
? undefined
66+
: 'Mock workflow action disabled reason'
67+
)
68+
);
69+
jest.mock(
70+
'../../../workflow-actions/workflow-actions-modal/workflow-actions-modal',
71+
() =>
72+
jest.fn((props) => {
73+
return (
74+
<div data-testid="actions-modal">
75+
Actions Modal
76+
<button data-testid="close-modal-button" onClick={props.onClose}>
77+
Close
78+
</button>
79+
</div>
80+
);
81+
})
82+
);
83+
84+
jest.mock(
85+
'@/views/workflow-actions/workflow-actions-menu/helpers/get-action-disabled-reason'
86+
);
87+
const mockGetActionDisabledReason = getActionDisabledReason as jest.Mock;
88+
89+
describe('DomainPageStartWorkflowButton', () => {
90+
const defaultProps: Props = {
91+
domain: 'test-domain',
92+
cluster: 'test-cluster',
93+
};
94+
95+
beforeEach(() => {
96+
jest.clearAllMocks();
97+
mockGetActionDisabledReason.mockReturnValue(undefined);
98+
});
99+
100+
it('renders the start workflow button', async () => {
101+
await setup(defaultProps);
102+
103+
const button = screen.getByTestId('start-workflow-button');
104+
expect(button).toBeInTheDocument();
105+
});
106+
107+
it('calls getActionDisabledReason with correct parameters', async () => {
108+
setup(defaultProps, {
109+
startActionEnabledConfig: 'ENABLED',
110+
});
111+
112+
await waitFor(() => {
113+
expect(mockGetActionDisabledReason).toHaveBeenCalledWith({
114+
actionEnabledConfig: 'ENABLED',
115+
actionRunnableStatus: 'RUNNABLE',
116+
});
117+
});
118+
});
119+
120+
it('should pass isConfigLoading to the button', async () => {
121+
setup(defaultProps, {
122+
isConfigLoading: true,
123+
});
124+
expect(screen.getByTestId('start-workflow-button')).toHaveTextContent(
125+
/"isLoading":true/
126+
);
127+
});
128+
129+
it('disables button when action is disabled', async () => {
130+
const disabledReason = 'Workflow action has been disabled';
131+
mockGetActionDisabledReason.mockReturnValue(disabledReason);
132+
133+
await setup(defaultProps);
134+
135+
const button = screen.getByTestId('start-workflow-button');
136+
expect(button).toHaveTextContent(/"disabled":true/);
137+
});
138+
139+
it('shows tooltip with disabled reason when button is disabled', async () => {
140+
const disabledReason = 'Not authorized to perform this action';
141+
mockGetActionDisabledReason.mockReturnValue(disabledReason);
142+
143+
setup(defaultProps);
144+
expect(screen.getByTestId('tooltip')).toHaveTextContent(disabledReason);
145+
});
146+
147+
it('opens modal when button is clicked', async () => {
148+
const { user } = await setup(defaultProps, {
149+
startActionEnabledConfig: 'ENABLED',
150+
isConfigLoading: false,
151+
isConfigError: false,
152+
});
153+
154+
const button = screen.getByTestId('start-workflow-button');
155+
expect(screen.queryByTestId('actions-modal')).not.toBeInTheDocument();
156+
await user.click(button);
157+
158+
expect(screen.getByTestId('actions-modal')).toBeInTheDocument();
159+
});
160+
161+
it('closes modal when onClose is called', async () => {
162+
const { user } = await setup(defaultProps);
163+
164+
const button = screen.getByTestId('start-workflow-button');
165+
expect(button).toBeInTheDocument();
166+
167+
await user.click(button);
168+
169+
expect(screen.getByTestId('actions-modal')).toBeInTheDocument();
170+
await user.click(screen.getByTestId('close-modal-button'));
171+
172+
expect(screen.queryByTestId('actions-modal')).not.toBeInTheDocument();
173+
});
174+
175+
it('show loading indicator when config errors', async () => {
176+
await setup(defaultProps, {
177+
isConfigError: true,
178+
});
179+
180+
const button = screen.getByTestId('start-workflow-button');
181+
expect(button).toHaveTextContent(/"isLoading":true/);
182+
});
183+
});
184+
185+
function setup(
186+
props: Props,
187+
options: {
188+
startActionEnabledConfig?: string;
189+
isConfigLoading?: boolean;
190+
isConfigError?: boolean;
191+
} = {}
192+
) {
193+
const user = userEvent.setup();
194+
const {
195+
startActionEnabledConfig = mockResolvedConfigValues.WORKFLOW_ACTIONS_ENABLED
196+
.start,
197+
isConfigLoading = false,
198+
isConfigError = false,
199+
} = options;
200+
201+
const renderResult = render(<DomainPageStartWorkflowButton {...props} />, {
202+
endpointsMocks: [
203+
{
204+
path: '/api/config',
205+
httpMethod: 'GET',
206+
mockOnce: true,
207+
httpResolver: async () => {
208+
if (isConfigError) {
209+
return HttpResponse.json(
210+
{ error: 'Config error' },
211+
{ status: 500 }
212+
);
213+
}
214+
if (isConfigLoading) {
215+
return new Promise(() => {});
216+
}
217+
218+
const response = {
219+
...mockResolvedConfigValues.WORKFLOW_ACTIONS_ENABLED,
220+
start: startActionEnabledConfig,
221+
};
222+
return HttpResponse.json(response);
223+
},
224+
},
225+
],
226+
});
227+
228+
return {
229+
user,
230+
...renderResult,
231+
};
232+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
'use client';
2+
import React, { useState } from 'react';
3+
4+
import { StatefulTooltip } from 'baseui/tooltip';
5+
6+
import Button from '@/components/button/button';
7+
import useConfigValue from '@/hooks/use-config-value/use-config-value';
8+
import { startWorkflowActionConfig } from '@/views/workflow-actions/config/workflow-actions.config';
9+
import getActionDisabledReason from '@/views/workflow-actions/workflow-actions-menu/helpers/get-action-disabled-reason';
10+
import WorkflowActionsModal from '@/views/workflow-actions/workflow-actions-modal/workflow-actions-modal';
11+
12+
import type { Props } from './domain-page-start-workflow-button.types';
13+
14+
export default function DomainPageStartWorkflowButton({
15+
domain,
16+
cluster,
17+
}: Props) {
18+
const [showStartNewWorkflowModal, setShowStartNewWorkflowModal] =
19+
useState(false);
20+
21+
const {
22+
data: actionsEnabledConfig,
23+
isLoading: isActionsEnabledLoading,
24+
isError: isActionsEnabledError,
25+
} = useConfigValue('WORKFLOW_ACTIONS_ENABLED', {
26+
domain,
27+
cluster,
28+
});
29+
30+
const disabledReason = getActionDisabledReason({
31+
actionEnabledConfig: actionsEnabledConfig?.start,
32+
actionRunnableStatus: 'RUNNABLE',
33+
});
34+
35+
return (
36+
<>
37+
<StatefulTooltip
38+
content={disabledReason ?? null}
39+
ignoreBoundary
40+
placement="auto"
41+
showArrow
42+
>
43+
<div>
44+
<Button
45+
onClick={() => {
46+
setShowStartNewWorkflowModal(true);
47+
}}
48+
size="compact"
49+
kind="secondary"
50+
loadingIndicatorType="skeleton"
51+
isLoading={isActionsEnabledLoading || isActionsEnabledError}
52+
disabled={Boolean(disabledReason)}
53+
startEnhancer={<startWorkflowActionConfig.icon size={16} />}
54+
aria-label={disabledReason ?? undefined}
55+
>
56+
Start new workflow
57+
</Button>
58+
</div>
59+
</StatefulTooltip>
60+
{showStartNewWorkflowModal && (
61+
<WorkflowActionsModal
62+
domain={domain}
63+
cluster={cluster}
64+
runId=""
65+
workflowId=""
66+
action={startWorkflowActionConfig}
67+
onClose={() => {
68+
setShowStartNewWorkflowModal(false);
69+
}}
70+
/>
71+
)}
72+
</>
73+
);
74+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import type { DomainPageTabsParams } from '../domain-page-tabs/domain-page-tabs.types';
2+
3+
export type Props = Pick<DomainPageTabsParams, 'domain' | 'cluster'>;

src/views/domain-page/domain-page-tabs/__tests__/domain-page-tabs.test.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,14 @@ jest.mock('next/navigation', () => ({
2323
}),
2424
}));
2525

26+
jest.mock(
27+
'../../domain-page-start-workflow-button/domain-page-start-workflow-button',
28+
() =>
29+
jest.fn(() => (
30+
<button data-testid="start-workflow-button">Start Workflow</button>
31+
))
32+
);
33+
2634
jest.mock('../../config/domain-page-tabs.config', () => ({
2735
workflows: {
2836
title: 'Workflows',
@@ -104,4 +112,10 @@ describe('DomainPageTabs', () => {
104112
expect(screen.getByTestId('domain-page-help')).toBeInTheDocument();
105113
expect(screen.getByText('Help Button')).toBeInTheDocument();
106114
});
115+
116+
it('renders the start workflow button', () => {
117+
render(<DomainPageTabs />);
118+
119+
expect(screen.getByTestId('start-workflow-button')).toBeInTheDocument();
120+
});
107121
});

src/views/domain-page/domain-page-tabs/domain-page-tabs.styles.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,8 @@ export const styled = {
44
PageTabsContainer: createStyled('div', ({ $theme }: { $theme: Theme }) => ({
55
marginTop: $theme.sizing.scale950,
66
})),
7+
EndButtonsContainer: createStyled('div', ({ $theme }: { $theme: Theme }) => ({
8+
display: 'flex',
9+
gap: $theme.sizing.scale300,
10+
})),
711
};

src/views/domain-page/domain-page-tabs/domain-page-tabs.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import decodeUrlParams from '@/utils/decode-url-params';
88

99
import domainPageTabsConfig from '../config/domain-page-tabs.config';
1010
import DomainPageHelp from '../domain-page-help/domain-page-help';
11+
import DomainPageStartWorkflowButton from '../domain-page-start-workflow-button/domain-page-start-workflow-button';
1112

1213
import { styled } from './domain-page-tabs.styles';
1314
import type { DomainPageTabsParams } from './domain-page-tabs.types';
@@ -37,7 +38,12 @@ export default function DomainPageTabs() {
3738
`${encodeURIComponent(newTab.toString())}${window.location.search}`
3839
);
3940
}}
40-
endEnhancer={<DomainPageHelp />}
41+
endEnhancer={
42+
<styled.EndButtonsContainer>
43+
<DomainPageStartWorkflowButton {...decodedParams} />
44+
<DomainPageHelp />
45+
</styled.EndButtonsContainer>
46+
}
4147
/>
4248
</styled.PageTabsContainer>
4349
);

0 commit comments

Comments
 (0)