Skip to content

Commit

Permalink
feat(KFLUXUI-217): add contexts to show page
Browse files Browse the repository at this point in the history
- Add the context values to the integration show page.
- Add a modal to allow easy edits.
- Use spinner for loading instead of text.
  • Loading branch information
CryptoRodeo committed Nov 21, 2024
1 parent 8259a84 commit 97fe747
Show file tree
Hide file tree
Showing 5 changed files with 280 additions and 3 deletions.
6 changes: 4 additions & 2 deletions src/components/IntegrationTests/ContextsField.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as React from 'react';
import { useParams } from 'react-router-dom';
import { FormGroup } from '@patternfly/react-core';
import { Bullseye, FormGroup, Spinner } from '@patternfly/react-core';
import { FieldArray, useField, FieldArrayRenderProps } from 'formik';
import { getFieldId } from '../../../src/shared/components/formik-fields/field-utils';
import { useComponents } from '../../hooks/useComponents';
Expand Down Expand Up @@ -106,7 +106,9 @@ const ContextsField: React.FC<IntegrationTestContextProps> = ({ heading, fieldNa
)}
/>
) : (
'Loading Additional Component Context options'
<Bullseye>
<Spinner size="xl" />
</Bullseye>
)}
</FormGroup>
);
Expand Down
127 changes: 127 additions & 0 deletions src/components/IntegrationTests/EditContextsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import * as React from 'react';
import {
Alert,
AlertVariant,
Button,
ButtonType,
ButtonVariant,
ModalVariant,
Stack,
StackItem,
} from '@patternfly/react-core';
import { Formik, FormikValues } from 'formik';
import { k8sPatchResource } from '../../k8s/k8s-fetch';
import { IntegrationTestScenarioModel } from '../../models';
import { IntegrationTestScenarioKind, Context } from '../../types/coreBuildService';
import { ComponentProps, createModalLauncher } from '../modal/createModalLauncher';

Check warning on line 16 in src/components/IntegrationTests/EditContextsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/IntegrationTests/EditContextsModal.tsx#L15-L16

Added lines #L15 - L16 were not covered by tests
import ContextsField from './ContextsField';
import { UnformattedContexts, formatContexts } from './IntegrationTestForm/utils/create-utils';

type EditContextsModalProps = ComponentProps & {
intTest: IntegrationTestScenarioKind;
};

export const EditContextsModal: React.FC<React.PropsWithChildren<EditContextsModalProps>> = ({
intTest,
onClose,
}) => {
const [error, setError] = React.useState<string>();

const getFormContextValues = (contexts: Context[] = []) => {
return contexts.map(({ name, description }) => ({ name, description }));
};

const updateIntegrationTest = async (values: FormikValues) => {
try {
await k8sPatchResource({
model: IntegrationTestScenarioModel,
queryOptions: {
name: intTest.metadata.name,
ns: intTest.metadata.namespace,
},
patches: [
{

Check warning on line 43 in src/components/IntegrationTests/EditContextsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/IntegrationTests/EditContextsModal.tsx#L43

Added line #L43 was not covered by tests
op: 'replace',
path: '/spec/contexts',
value: formatContexts(values.contexts as UnformattedContexts),

Check warning on line 46 in src/components/IntegrationTests/EditContextsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/IntegrationTests/EditContextsModal.tsx#L46

Added line #L46 was not covered by tests
},
],
});
onClose(null, { submitClicked: true });
} catch (e) {
const errMsg = e.message || e.toString();

Check warning on line 52 in src/components/IntegrationTests/EditContextsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/IntegrationTests/EditContextsModal.tsx#L52

Added line #L52 was not covered by tests
setError(errMsg as string);
}
};

const onReset = () => {
onClose(null, { submitClicked: false });
};

const initialContexts = getFormContextValues(intTest?.spec?.contexts);

Check warning on line 62 in src/components/IntegrationTests/EditContextsModal.tsx

View check run for this annotation

Codecov / codecov/patch

src/components/IntegrationTests/EditContextsModal.tsx#L62

Added line #L62 was not covered by tests
// When a user presses enter, make sure the form doesn't submit.
// Enter should be used to select values from the drop down,
// when using the keyboard, not submit the form.
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.preventDefault(); // Prevent form submission on Enter key
}
};

return (
<Formik
onSubmit={updateIntegrationTest}
initialValues={{ contexts: initialContexts, confirm: false }}
onReset={onReset}
>
{({ handleSubmit, handleReset, isSubmitting, values }) => {
const isChanged = values.contexts !== initialContexts;
const showConfirmation = isChanged && values.strategy === 'Automatic';
const isValid = isChanged && (showConfirmation ? values.confirm : true);

return (
<div data-test={'edit-contexts-modal'} onKeyDown={handleKeyDown}>
<Stack hasGutter>
<StackItem>
<ContextsField fieldName="contexts" editing={true} />
</StackItem>
<StackItem>
{error && (
<Alert isInline variant={AlertVariant.danger} title="An error occurred">
{error}
</Alert>
)}
<Button
type={ButtonType.submit}
isLoading={isSubmitting}
onClick={(e) => {
e.preventDefault();
handleSubmit();
}}
isDisabled={!isValid || isSubmitting}
data-test={'update-contexts'}
>
Save
</Button>
<Button
variant={ButtonVariant.link}
onClick={handleReset}
data-test={'cancel-update-contexts'}
>
Cancel
</Button>
</StackItem>
</Stack>
</div>
);
}}
</Formik>
);
};

export const createEditContextsModal = createModalLauncher(EditContextsModal, {
'data-test': `edit-its-contexts`,
variant: ModalVariant.medium,
title: `Edit contexts`,
});
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import ExternalLink from '../../../../shared/components/links/ExternalLink';
import MetadataList from '../../../MetadataList';
import { useModalLauncher } from '../../../modal/ModalProvider';
import { useWorkspaceInfo } from '../../../Workspace/useWorkspaceInfo';
import { createEditContextsModal } from '../../EditContextsModal';
import { createEditParamsModal } from '../../EditParamsModal';
import { IntegrationTestLabels } from '../../IntegrationTestForm/types';
import {
Expand All @@ -46,6 +47,7 @@ const IntegrationTestOverviewTab: React.FC<React.PropsWithChildren> = () => {
const showModal = useModalLauncher();

const params = integrationTest?.spec?.params;
const contexts = integrationTest?.spec?.contexts;

Check warning on line 50 in src/components/IntegrationTests/IntegrationTestDetails/tabs/IntegrationTestOverviewTab.tsx

View check run for this annotation

Codecov / codecov/patch

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

Added line #L50 was not covered by tests

return (
<>
Expand Down Expand Up @@ -142,6 +144,36 @@ const IntegrationTestOverviewTab: React.FC<React.PropsWithChildren> = () => {
})}
</>
)}
{contexts && (
<DescriptionListGroup data-test="its-overview-contexts">
<DescriptionListTerm>
Contexts{' '}
<Tooltip content="Contexts where the integration test can be applied.">
<OutlinedQuestionCircleIcon />
</Tooltip>
</DescriptionListTerm>
<DescriptionListDescription>
{pluralize(contexts.length, 'context')}
<div>
{' '}
<Button
variant={ButtonVariant.link}
className="pf-v5-u-pl-0"
onClick={() =>
showModal(
createEditContextsModal({
intTest: integrationTest,
}),
)
}
data-test="edit-context-button"
>
Edit contexts
</Button>
</div>
</DescriptionListDescription>
</DescriptionListGroup>
)}
{params && (
<DescriptionListGroup data-test="its-overview-params">
<DescriptionListTerm>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,11 @@ export const formatParams = (params): Param[] => {
return newParams.length > 0 ? newParams : null;
};

export const formatContexts = (contexts = [], setDefault = false): Context[] | null => {
export type UnformattedContexts = { name: string; description: string }[];
export const formatContexts = (
contexts: UnformattedContexts = [],
setDefault: boolean = false,
): Context[] | null => {
const defaultContext = {
name: 'application',
description: 'execute the integration test in all cases - this would be the default state',
Expand Down
112 changes: 112 additions & 0 deletions src/components/IntegrationTests/__tests__/EditContextsModal.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { screen, fireEvent, waitFor } from '@testing-library/react';
import '@testing-library/jest-dom';
import { useComponents } from '../../../hooks/useComponents';
import { k8sPatchResource } from '../../../k8s/k8s-fetch';
import { formikRenderer } from '../../../utils/test-utils';
import { EditContextsModal } from '../EditContextsModal';
import { IntegrationTestFormValues } from '../IntegrationTestForm/types';
import { MockIntegrationTests } from '../IntegrationTestsListView/__data__/mock-integration-tests';
import { contextOptions } from '../utils';

// Mock external dependencies
jest.mock('../../../k8s/k8s-fetch', () => ({
k8sPatchResource: jest.fn(),
}));
jest.mock('../../../hooks/useComponents', () => ({
useComponents: jest.fn(),
}));
jest.mock('../../Workspace/useWorkspaceInfo', () => ({
useWorkspaceInfo: jest.fn(() => ({ namespace: 'test-ns', workspace: 'test-ws' })),
}));

const useComponentsMock = useComponents as jest.Mock;
const patchResourceMock = k8sPatchResource as jest.Mock;
const onCloseMock = jest.fn();

const intTest = MockIntegrationTests[0];
const initialValues: IntegrationTestFormValues = {
name: intTest.metadata.name,
url: 'test-url',
optional: true,
contexts: intTest.spec.contexts,
};

const setup = () =>
formikRenderer(<EditContextsModal intTest={intTest} onClose={onCloseMock} />, initialValues);

beforeEach(() => {
jest.clearAllMocks();
useComponentsMock.mockReturnValue([[], true]);
});

describe('EditContextsModal', () => {
it('should render correct contexts', () => {
setup();
const contextOptionNames = contextOptions.map((ctx) => ctx.name);

screen.getByText('Contexts');
contextOptionNames.forEach((ctxName) => screen.queryByText(ctxName));
});

it('should show Save and Cancel buttons', () => {
setup();
// Save
screen.getByTestId('update-contexts');
// Cancel
screen.getByTestId('cancel-update-contexts');
});

it('should call onClose callback when cancel button is clicked', () => {
setup();
fireEvent.click(screen.getByRole('button', { name: 'Cancel' }));
expect(onCloseMock).toHaveBeenCalledWith(null, { submitClicked: false });
});

it('prevents form submission when pressing Enter', () => {
setup();
const form = screen.getByTestId('edit-contexts-modal');
fireEvent.keyDown(form, { key: 'Enter', code: 'Enter' });
expect(k8sPatchResource).not.toHaveBeenCalled();
});

it('calls updateIntegrationTest and onClose on form submission', async () => {
patchResourceMock.mockResolvedValue({});

setup();
const clearButton = screen.getByTestId('clear-button');
// Clear all selections
fireEvent.click(clearButton);
// Save button should now be active
fireEvent.click(screen.getByRole('button', { name: 'Save' }));

await waitFor(() => {
expect(patchResourceMock).toHaveBeenCalledTimes(1);
});

expect(patchResourceMock).toHaveBeenCalledWith(
expect.objectContaining({
queryOptions: { name: 'test-app-test-1', ns: 'test-namespace' },
patches: [{ op: 'replace', path: '/spec/contexts', value: null }],
}),
);
expect(onCloseMock).toHaveBeenCalledWith(null, { submitClicked: true });
});

it('displays an error message if k8sPatchResource fails', async () => {
patchResourceMock.mockRejectedValue('Failed to update contexts');
setup();

const clearButton = screen.getByTestId('clear-button');
// Clear all selections
fireEvent.click(clearButton);
// Click Save button
fireEvent.click(screen.getByRole('button', { name: 'Save' }));

// wait for the error message to appear
await waitFor(() => {
expect(patchResourceMock).toHaveBeenCalledTimes(1);
expect(screen.getByText('An error occurred')).toBeInTheDocument();
expect(screen.queryByText('Failed to update contexts')).toBeInTheDocument();
});
});
});

0 comments on commit 97fe747

Please sign in to comment.