Skip to content

Commit 812ece1

Browse files
committed
feat(KFLUXUI-217): add contexts to show page
- Add the context values to the integration show page. - Add a modal to allow easy edits. - Use spinner for loading instead of text.
1 parent 8041520 commit 812ece1

File tree

5 files changed

+280
-3
lines changed

5 files changed

+280
-3
lines changed

src/components/IntegrationTests/ContextsField.tsx

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import * as React from 'react';
22
import { useParams } from 'react-router-dom';
3-
import { FormGroup } from '@patternfly/react-core';
3+
import { Bullseye, FormGroup, Spinner } from '@patternfly/react-core';
44
import { FieldArray, useField, FieldArrayRenderProps } from 'formik';
55
import { getFieldId } from '../../../src/shared/components/formik-fields/field-utils';
66
import { useComponents } from '../../hooks/useComponents';
@@ -106,7 +106,9 @@ const ContextsField: React.FC<IntegrationTestContextProps> = ({ heading, fieldNa
106106
)}
107107
/>
108108
) : (
109-
'Loading Additional Component Context options'
109+
<Bullseye>
110+
<Spinner size="xl" />
111+
</Bullseye>
110112
)}
111113
</FormGroup>
112114
);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import * as React from 'react';
2+
import {
3+
Alert,
4+
AlertVariant,
5+
Button,
6+
ButtonType,
7+
ButtonVariant,
8+
ModalVariant,
9+
Stack,
10+
StackItem,
11+
} from '@patternfly/react-core';
12+
import { Formik, FormikValues } from 'formik';
13+
import { k8sPatchResource } from '../../k8s/k8s-fetch';
14+
import { IntegrationTestScenarioModel } from '../../models';
15+
import { IntegrationTestScenarioKind, Context } from '../../types/coreBuildService';
16+
import { ComponentProps, createModalLauncher } from '../modal/createModalLauncher';
17+
import ContextsField from './ContextsField';
18+
import { UnformattedContexts, formatContexts } from './IntegrationTestForm/utils/create-utils';
19+
20+
type EditContextsModalProps = ComponentProps & {
21+
intTest: IntegrationTestScenarioKind;
22+
};
23+
24+
export const EditContextsModal: React.FC<React.PropsWithChildren<EditContextsModalProps>> = ({
25+
intTest,
26+
onClose,
27+
}) => {
28+
const [error, setError] = React.useState<string>();
29+
30+
const getFormContextValues = (contexts: Context[] = []) => {
31+
return contexts.map(({ name, description }) => ({ name, description }));
32+
};
33+
34+
const updateIntegrationTest = async (values: FormikValues) => {
35+
try {
36+
await k8sPatchResource({
37+
model: IntegrationTestScenarioModel,
38+
queryOptions: {
39+
name: intTest.metadata.name,
40+
ns: intTest.metadata.namespace,
41+
},
42+
patches: [
43+
{
44+
op: 'replace',
45+
path: '/spec/contexts',
46+
value: formatContexts(values.contexts as UnformattedContexts),
47+
},
48+
],
49+
});
50+
onClose(null, { submitClicked: true });
51+
} catch (e) {
52+
const errMsg = e.message || e.toString();
53+
setError(errMsg as string);
54+
}
55+
};
56+
57+
const onReset = () => {
58+
onClose(null, { submitClicked: false });
59+
};
60+
61+
const initialContexts = getFormContextValues(intTest?.spec?.contexts);
62+
63+
// When a user presses enter, make sure the form doesn't submit.
64+
// Enter should be used to select values from the drop down,
65+
// when using the keyboard, not submit the form.
66+
const handleKeyDown = (e: React.KeyboardEvent) => {
67+
if (e.key === 'Enter') {
68+
e.preventDefault(); // Prevent form submission on Enter key
69+
}
70+
};
71+
72+
return (
73+
<Formik
74+
onSubmit={updateIntegrationTest}
75+
initialValues={{ contexts: initialContexts, confirm: false }}
76+
onReset={onReset}
77+
>
78+
{({ handleSubmit, handleReset, isSubmitting, values }) => {
79+
const isChanged = values.contexts !== initialContexts;
80+
const showConfirmation = isChanged && values.strategy === 'Automatic';
81+
const isValid = isChanged && (showConfirmation ? values.confirm : true);
82+
83+
return (
84+
<div data-test={'edit-contexts-modal'} onKeyDown={handleKeyDown}>
85+
<Stack hasGutter>
86+
<StackItem>
87+
<ContextsField fieldName="contexts" editing={true} />
88+
</StackItem>
89+
<StackItem>
90+
{error && (
91+
<Alert isInline variant={AlertVariant.danger} title="An error occurred">
92+
{error}
93+
</Alert>
94+
)}
95+
<Button
96+
type={ButtonType.submit}
97+
isLoading={isSubmitting}
98+
onClick={(e) => {
99+
e.preventDefault();
100+
handleSubmit();
101+
}}
102+
isDisabled={!isValid || isSubmitting}
103+
data-test={'update-contexts'}
104+
>
105+
Save
106+
</Button>
107+
<Button
108+
variant={ButtonVariant.link}
109+
onClick={handleReset}
110+
data-test={'cancel-update-contexts'}
111+
>
112+
Cancel
113+
</Button>
114+
</StackItem>
115+
</Stack>
116+
</div>
117+
);
118+
}}
119+
</Formik>
120+
);
121+
};
122+
123+
export const createEditContextsModal = createModalLauncher(EditContextsModal, {
124+
'data-test': `edit-its-contexts`,
125+
variant: ModalVariant.medium,
126+
title: `Edit contexts`,
127+
});

src/components/IntegrationTests/IntegrationTestDetails/tabs/IntegrationTestOverviewTab.tsx

+32
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import ExternalLink from '../../../../shared/components/links/ExternalLink';
2121
import MetadataList from '../../../MetadataList';
2222
import { useModalLauncher } from '../../../modal/ModalProvider';
2323
import { useWorkspaceInfo } from '../../../Workspace/useWorkspaceInfo';
24+
import { createEditContextsModal } from '../../EditContextsModal';
2425
import { createEditParamsModal } from '../../EditParamsModal';
2526
import { IntegrationTestLabels } from '../../IntegrationTestForm/types';
2627
import {
@@ -46,6 +47,7 @@ const IntegrationTestOverviewTab: React.FC<React.PropsWithChildren> = () => {
4647
const showModal = useModalLauncher();
4748

4849
const params = integrationTest?.spec?.params;
50+
const contexts = integrationTest?.spec?.contexts;
4951

5052
return (
5153
<>
@@ -142,6 +144,36 @@ const IntegrationTestOverviewTab: React.FC<React.PropsWithChildren> = () => {
142144
})}
143145
</>
144146
)}
147+
{contexts && (
148+
<DescriptionListGroup data-test="its-overview-contexts">
149+
<DescriptionListTerm>
150+
Contexts{' '}
151+
<Tooltip content="Contexts where the integration test can be applied.">
152+
<OutlinedQuestionCircleIcon />
153+
</Tooltip>
154+
</DescriptionListTerm>
155+
<DescriptionListDescription>
156+
{pluralize(contexts.length, 'context')}
157+
<div>
158+
{' '}
159+
<Button
160+
variant={ButtonVariant.link}
161+
className="pf-v5-u-pl-0"
162+
onClick={() =>
163+
showModal(
164+
createEditContextsModal({
165+
intTest: integrationTest,
166+
}),
167+
)
168+
}
169+
data-test="edit-context-button"
170+
>
171+
Edit contexts
172+
</Button>
173+
</div>
174+
</DescriptionListDescription>
175+
</DescriptionListGroup>
176+
)}
145177
{params && (
146178
<DescriptionListGroup data-test="its-overview-params">
147179
<DescriptionListTerm>

src/components/IntegrationTests/IntegrationTestForm/utils/create-utils.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,11 @@ export const formatParams = (params): Param[] => {
4646
return newParams.length > 0 ? newParams : null;
4747
};
4848

49-
export const formatContexts = (contexts = [], setDefault = false): Context[] | null => {
49+
export type UnformattedContexts = { name: string; description: string }[];
50+
export const formatContexts = (
51+
contexts: UnformattedContexts = [],
52+
setDefault: boolean = false,
53+
): Context[] | null => {
5054
const defaultContext = {
5155
name: 'application',
5256
description: 'execute the integration test in all cases - this would be the default state',
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import { screen, fireEvent, waitFor } from '@testing-library/react';
2+
import '@testing-library/jest-dom';
3+
import { useComponents } from '../../../hooks/useComponents';
4+
import { k8sPatchResource } from '../../../k8s/k8s-fetch';
5+
import { formikRenderer } from '../../../utils/test-utils';
6+
import { EditContextsModal } from '../EditContextsModal';
7+
import { IntegrationTestFormValues } from '../IntegrationTestForm/types';
8+
import { MockIntegrationTests } from '../IntegrationTestsListView/__data__/mock-integration-tests';
9+
import { contextOptions } from '../utils';
10+
11+
// Mock external dependencies
12+
jest.mock('../../../k8s/k8s-fetch', () => ({
13+
k8sPatchResource: jest.fn(),
14+
}));
15+
jest.mock('../../../hooks/useComponents', () => ({
16+
useComponents: jest.fn(),
17+
}));
18+
jest.mock('../../Workspace/useWorkspaceInfo', () => ({
19+
useWorkspaceInfo: jest.fn(() => ({ namespace: 'test-ns', workspace: 'test-ws' })),
20+
}));
21+
22+
const useComponentsMock = useComponents as jest.Mock;
23+
const patchResourceMock = k8sPatchResource as jest.Mock;
24+
const onCloseMock = jest.fn();
25+
26+
const intTest = MockIntegrationTests[0];
27+
const initialValues: IntegrationTestFormValues = {
28+
name: intTest.metadata.name,
29+
url: 'test-url',
30+
optional: true,
31+
contexts: intTest.spec.contexts,
32+
};
33+
34+
const setup = () =>
35+
formikRenderer(<EditContextsModal intTest={intTest} onClose={onCloseMock} />, initialValues);
36+
37+
beforeEach(() => {
38+
jest.clearAllMocks();
39+
useComponentsMock.mockReturnValue([[], true]);
40+
});
41+
42+
describe('EditContextsModal', () => {
43+
it('should render correct contexts', () => {
44+
setup();
45+
const contextOptionNames = contextOptions.map((ctx) => ctx.name);
46+
47+
screen.getByText('Contexts');
48+
contextOptionNames.forEach((ctxName) => screen.queryByText(ctxName));
49+
});
50+
51+
it('should show Save and Cancel buttons', () => {
52+
setup();
53+
// Save
54+
screen.getByTestId('update-contexts');
55+
// Cancel
56+
screen.getByTestId('cancel-update-contexts');
57+
});
58+
59+
it('should call onClose callback when cancel button is clicked', () => {
60+
setup();
61+
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
62+
expect(onCloseMock).toHaveBeenCalledWith(null, { submitClicked: false });
63+
});
64+
65+
it('prevents form submission when pressing Enter', () => {
66+
setup();
67+
const form = screen.getByTestId('edit-contexts-modal');
68+
fireEvent.keyDown(form, { key: 'Enter', code: 'Enter' });
69+
expect(k8sPatchResource).not.toHaveBeenCalled();
70+
});
71+
72+
it('calls updateIntegrationTest and onClose on form submission', async () => {
73+
patchResourceMock.mockResolvedValue({});
74+
75+
setup();
76+
const clearButton = screen.getByTestId('clear-button');
77+
// Clear all selections
78+
fireEvent.click(clearButton);
79+
// Save button should now be active
80+
fireEvent.click(screen.getByRole('button', { name: 'Save' }));
81+
82+
await waitFor(() => {
83+
expect(patchResourceMock).toHaveBeenCalledTimes(1);
84+
});
85+
86+
expect(patchResourceMock).toHaveBeenCalledWith(
87+
expect.objectContaining({
88+
queryOptions: { name: 'test-app-test-1', ns: 'test-namespace' },
89+
patches: [{ op: 'replace', path: '/spec/contexts', value: null }],
90+
}),
91+
);
92+
expect(onCloseMock).toHaveBeenCalledWith(null, { submitClicked: true });
93+
});
94+
95+
it('displays an error message if k8sPatchResource fails', async () => {
96+
patchResourceMock.mockRejectedValue('Failed to update contexts');
97+
setup();
98+
99+
const clearButton = screen.getByTestId('clear-button');
100+
// Clear all selections
101+
fireEvent.click(clearButton);
102+
// Click Save button
103+
fireEvent.click(screen.getByRole('button', { name: 'Save' }));
104+
105+
// wait for the error message to appear
106+
await waitFor(() => {
107+
expect(patchResourceMock).toHaveBeenCalledTimes(1);
108+
expect(screen.getByText('An error occurred')).toBeInTheDocument();
109+
expect(screen.queryByText('Failed to update contexts')).toBeInTheDocument();
110+
});
111+
});
112+
});

0 commit comments

Comments
 (0)