From 8194ec00289f70f277ab4d1b4cc57378c8ef3abd Mon Sep 17 00:00:00 2001 From: Abhi Date: Wed, 13 Mar 2024 01:53:23 +0530 Subject: [PATCH] feat(releases): add cve --- .../AddBugSection/AddBugModal.tsx | 70 ------ .../AddBugSection/AddBugSection.tsx | 177 ------------- .../AddBugSection/BugFormContent.tsx | 89 ------- .../__tests__/AddBugSection.spec.tsx | 0 .../AddIssueSection/AddIssueModal.tsx | 82 ++++++ .../AddIssueSection.scss} | 0 .../AddIssueSection/AddIssueSection.tsx | 236 ++++++++++++++++++ .../AddIssueSection/BugFormContent.tsx | 87 +++++++ .../AddIssueSection/CVEFormContent.tsx | 91 +++++++ .../AddIssueSection/ComponentField.tsx | 69 +++++ .../StatusDropdown.tsx | 0 .../UploadDate.tsx | 17 +- .../__tests__/AddIssueModal.spec.tsx | 42 ++++ .../__tests__/AddIssueSection.spec.tsx | 204 +++++++++++++++ .../__tests__/BugFormContent.spec.tsx | 41 +++ .../__tests__/CVEFormContent.spec.tsx | 53 ++++ .../__tests__/ComponentField.spec.tsx | 101 ++++++++ .../__tests__/StatusDropdown.spec.tsx | 45 ++++ .../__tests__/UploadDate.spec.tsx | 29 +++ .../TriggerRelease/TriggerReleaseForm.tsx | 12 +- .../TriggerRelease/TriggerReleaseFormPage.tsx | 3 - .../__tests__/AddBugModal.spec.tsx | 0 .../__tests__/ReleasePlanDropdown.spec.tsx | 66 +++++ .../__tests__/SnapshotDropdown.spec.tsx | 66 +++++ .../__tests__/TriggerReleaseForm.spec.tsx | 72 ++++-- .../__tests__/TriggerReleaseFormPage.spec.tsx | 64 ++--- .../__tests__/form-utils.spec.ts | 120 +++++++++ .../ReleasePlan/TriggerRelease/form-utils.ts | 18 +- .../__tests__/releaseplan-actions.spec.tsx | 86 +++++++ .../empty-state/FilteredEmptyState.tsx | 8 +- src/types/coreBuildService.ts | 28 +++ 31 files changed, 1546 insertions(+), 430 deletions(-) delete mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/AddBugModal.tsx delete mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/AddBugSection.tsx delete mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/BugFormContent.tsx delete mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/__tests__/AddBugSection.spec.tsx create mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/AddIssueModal.tsx rename src/components/ReleaseService/ReleasePlan/TriggerRelease/{AddBugSection/AddBugSection.scss => AddIssueSection/AddIssueSection.scss} (100%) create mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/AddIssueSection.tsx create mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/BugFormContent.tsx create mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/CVEFormContent.tsx create mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/ComponentField.tsx rename src/components/ReleaseService/ReleasePlan/TriggerRelease/{AddBugSection => AddIssueSection}/StatusDropdown.tsx (100%) rename src/components/ReleaseService/ReleasePlan/TriggerRelease/{AddBugSection => AddIssueSection}/UploadDate.tsx (68%) create mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/AddIssueModal.spec.tsx create mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/AddIssueSection.spec.tsx create mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/BugFormContent.spec.tsx create mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/CVEFormContent.spec.tsx create mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/ComponentField.spec.tsx create mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/StatusDropdown.spec.tsx create mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/UploadDate.spec.tsx delete mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/AddBugModal.spec.tsx create mode 100644 src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/form-utils.spec.ts create mode 100644 src/components/ReleaseService/ReleasePlan/__tests__/releaseplan-actions.spec.tsx diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/AddBugModal.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/AddBugModal.tsx deleted file mode 100644 index fc96815b4..000000000 --- a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/AddBugModal.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from 'react'; -import { Button, Modal, ModalVariant } from '@patternfly/react-core'; -import { Formik } from 'formik'; -import * as yup from 'yup'; -import { ComponentProps } from '../../../../modal/createModalLauncher'; -import BugFormContent from './BugFormContent'; - -type AddBugModalProps = ComponentProps & { - bugArrayHelper: (values) => void; -}; - -const bugFormSchema = yup.object({ - issueKey: yup.string().required('Required'), - url: yup.string().required('Required'), -}); - -export const AddBugModal: React.FC> = ({ - onClose, - bugArrayHelper, -}) => { - const [isModalOpen, setIsModalOpen] = React.useState(false); - const [isTimePickerOpen, setIsTimePickerOpen] = React.useState(false); - const dateRef = React.useRef(null); - - const handleModalToggle = () => { - setIsModalOpen(!isModalOpen); - }; - - const onEscapePress = (event: KeyboardEvent) => { - if (dateRef && dateRef.current && dateRef.current.isCalendarOpen) { - dateRef.current.toggleCalendar(false, event.key); - } else if (isTimePickerOpen) { - setIsTimePickerOpen(false); - } else { - handleModalToggle(); - } - }; - - const setValues = React.useCallback( - (fields) => { - bugArrayHelper(fields); - onClose(); - }, - [onClose, bugArrayHelper], - ); - - return ( - <> - - - - - - - - ); -}; diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/AddBugSection.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/AddBugSection.tsx deleted file mode 100644 index 850f0148a..000000000 --- a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/AddBugSection.tsx +++ /dev/null @@ -1,177 +0,0 @@ -import * as React from 'react'; -import { - EmptyState, - EmptyStateBody, - SearchInput, - TextContent, - TextVariants, - Toolbar, - ToolbarContent, - ToolbarGroup, - ToolbarItem, - Text, -} from '@patternfly/react-core'; -import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; -import { FieldArray, useField } from 'formik'; -import { debounce } from 'lodash-es'; -import { useSearchParam } from '../../../../../hooks/useSearchParam'; -import ActionMenu from '../../../../../shared/components/action-menu/ActionMenu'; -import FilteredEmptyState from '../../../../../shared/components/empty-state/FilteredEmptyState'; -import { AddBugModal } from './AddBugModal'; - -import './AddBugSection.scss'; - -interface AddBugSectionProps { - field: string; -} - -export interface BugsObject { - issueKey: string; - summary: string; - url?: string; - uploadDate?: string; - status?: string; -} - -export const bugsTableColumnClass = { - issueKey: 'pf-m-width-15 wrap-column', - url: 'pf-m-width-30 pf-m-width-25-on-lg', - summary: 'pf-m-width-20 pf-m-width-20-on-lg pf-m-width-15-on-xl', - uploadDate: 'pf-m-hidden pf-m-visible-on-xl pf-m-width-20', - status: 'pf-m-hidden pf-m-visible-on-xl pf-m-width-15', - kebab: 'pf-v5-c-table__action', -}; - -const AddBugSection: React.FC> = ({ field }) => { - const [nameFilter, setNameFilter] = useSearchParam('name', ''); - const [{ value: issues }, ,] = useField(field); - - const [onLoadName, setOnLoadName] = React.useState(nameFilter); - React.useEffect(() => { - if (nameFilter) { - setOnLoadName(nameFilter); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const filteredBugs = React.useMemo( - () => - issues && Array.isArray(issues) - ? issues?.filter((bug) => !nameFilter || bug.issueKey.indexOf(nameFilter) >= 0) - : [], - [issues, nameFilter], - ); - - const onClearFilters = () => { - onLoadName.length && setOnLoadName(''); - setNameFilter(''); - }; - const onNameInput = debounce((n: string) => { - n.length === 0 && onLoadName.length && setOnLoadName(''); - - setNameFilter(n); - }, 600); - - const EmptyMsg = () => - nameFilter ? ( - - ) : ( - - No Bugs found - - ); - - return ( - { - const addNewBug = (bug) => { - arrayHelper.push(bug); - }; - - return ( - <> - - - Are there any bug fixes you would like to add to this release? - - - - - - - onNameInput(n)} - value={nameFilter} - /> - - - - - - - -
- - - - - - - - - - - - {Array.isArray(filteredBugs) && filteredBugs.length > 0 && ( - - {filteredBugs.map((bug, i) => ( - - - - - - - - - ))} - - )} -
Bug issue keyURLSummaryLast updatedStatus
{bug.issueKey}{bug.url}{bug.summary}{bug.uploadDate}{bug.status} - arrayHelper.remove(i), - id: 'delete-bug', - label: 'Delete bug', - }, - ]} - /> -
-
- {!filteredBugs || - (filteredBugs?.length === 0 && ( -
{EmptyMsg()}
- ))} - - ); - }} - /> - ); -}; - -export default AddBugSection; diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/BugFormContent.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/BugFormContent.tsx deleted file mode 100644 index a9757a052..000000000 --- a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/BugFormContent.tsx +++ /dev/null @@ -1,89 +0,0 @@ -import * as React from 'react'; -import { - Button, - ButtonType, - ButtonVariant, - Form, - Stack, - StackItem, - Text, - TextContent, - TextVariants, -} from '@patternfly/react-core'; -import { useFormikContext } from 'formik'; -import { isEmpty } from 'lodash-es'; -import { InputField, TextAreaField } from '../../../../../shared'; -import StatusDropdown from './StatusDropdown'; -import UploadDate from './UploadDate'; - -type BugFormValues = { - issueKey: string; - url: string; - uploadDate: string; - status: string; -}; - -type BugFormContentProps = { - modalToggle: () => void; -}; -const BugFormContent: React.FC = ({ modalToggle }) => { - const { handleSubmit, isSubmitting, errors, dirty } = useFormikContext(); - const formRef = React.useRef(); - - return ( -
-
- - - - - Provide information about a Bug that has already been resolved. - - - - - - - - - - - - - - - - - - - - - - - -
-
- ); -}; - -export default BugFormContent; diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/__tests__/AddBugSection.spec.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/__tests__/AddBugSection.spec.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/AddIssueModal.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/AddIssueModal.tsx new file mode 100644 index 000000000..8469e3384 --- /dev/null +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/AddIssueModal.tsx @@ -0,0 +1,82 @@ +import * as React from 'react'; +import { Button, Modal, ModalVariant } from '@patternfly/react-core'; +import { Formik } from 'formik'; +import * as yup from 'yup'; +import { ComponentProps } from '../../../../modal/createModalLauncher'; +import BugFormContent from './BugFormContent'; +import CVEFormContent from './CVEFormContent'; +import { dateFormat } from './UploadDate'; + +export enum IssueType { + BUG = 'bug', + CVE = 'cve', +} + +type AddIssueModalProps = ComponentProps & { + bugArrayHelper: (values) => void; + issueType: IssueType; +}; + +const IssueFormSchema = yup.object({ + key: yup.string().required('Required'), + url: yup.string().required('Required'), +}); + +export const AddIssueModal: React.FC> = ({ + onClose, + bugArrayHelper, + issueType, +}) => { + const [isModalOpen, setIsModalOpen] = React.useState(false); + + const isBug = issueType === IssueType.BUG; + + const handleModalToggle = () => { + setIsModalOpen(!isModalOpen); + }; + + const setValues = React.useCallback( + (fields) => { + bugArrayHelper(fields); + onClose(); + }, + [onClose, bugArrayHelper], + ); + + return ( + <> + + + + {isBug ? ( + + ) : ( + + )} + + + + ); +}; diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/AddBugSection.scss b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/AddIssueSection.scss similarity index 100% rename from src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/AddBugSection.scss rename to src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/AddIssueSection.scss diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/AddIssueSection.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/AddIssueSection.tsx new file mode 100644 index 000000000..95b68b82f --- /dev/null +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/AddIssueSection.tsx @@ -0,0 +1,236 @@ +import * as React from 'react'; +import { + EmptyState, + EmptyStateBody, + SearchInput, + TextContent, + TextVariants, + Toolbar, + ToolbarContent, + ToolbarGroup, + ToolbarItem, + Text, + EmptyStateVariant, + Truncate, +} from '@patternfly/react-core'; +import { Table, Tbody, Td, Th, Thead, Tr } from '@patternfly/react-table'; +import { FieldArray, useField } from 'formik'; +import { debounce } from 'lodash-es'; +import { useSearchParam } from '../../../../../hooks/useSearchParam'; +import ActionMenu from '../../../../../shared/components/action-menu/ActionMenu'; +import FilteredEmptyState from '../../../../../shared/components/empty-state/FilteredEmptyState'; +import { AddIssueModal, IssueType } from './AddIssueModal'; + +import './AddIssueSection.scss'; + +interface AddIssueSectionProps { + field: string; + issueType: IssueType; +} + +export interface IssueObject { + key: string; + summary: string; + url?: string; + components?: string[]; + uploadDate?: string; + status?: string; +} + +export const issueTableColumnClass = { + issueKey: 'pf-m-width-15 wrap-column ', + bugUrl: 'pf-m-width-20 ', + cveUrl: 'pf-m-width-15 ', + components: 'pf-m-width-15 ', + summary: 'pf-m-width-20 pf-m-width-15-on-xl ', + uploadDate: 'pf-m-width-15 pf-m-width-10-on-xl ', + status: 'pf-m-hidden pf-m-visible-on-xl pf-m-width-15 ', + kebab: 'pf-v5-c-table__action', +}; + +export const AddIssueSection: React.FC> = ({ + field, + issueType, +}) => { + const [nameFilter, setNameFilter] = useSearchParam(field, ''); + const [{ value: issues }, ,] = useField(field); + + const isBug = issueType === IssueType.BUG; + + const [onLoadName, setOnLoadName] = React.useState(nameFilter); + React.useEffect(() => { + if (nameFilter) { + setOnLoadName(nameFilter); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const filteredIssues = React.useMemo( + () => + issues && Array.isArray(issues) + ? issues?.filter( + (bug) => !nameFilter || bug.key.toLowerCase().indexOf(nameFilter.toLowerCase()) >= 0, + ) + : [], + [issues, nameFilter], + ); + + const onClearFilters = () => { + onLoadName.length && setOnLoadName(''); + setNameFilter(''); + }; + const onNameInput = debounce((n: string) => { + n.length === 0 && onLoadName.length && setOnLoadName(''); + + setNameFilter(n); + }, 600); + + const EmptyMsg = (type) => + nameFilter ? ( + + ) : ( + + + {type === IssueType.BUG ? 'No Bugs found' : 'No CVEs found'} + + + ); + + return ( + { + const addNewBug = (bug) => { + arrayHelper.push(bug); + }; + + return ( + <> + + + {isBug + ? 'Are there any bug fixes you would like to add to this release?' + : 'Are there any CVEs you would like to add to this release?'} + + + + + + + onNameInput(n)} + value={nameFilter} + /> + + + + + + + +
+ + {isBug ? ( + + + + + + + + + + ) : ( + + + + + + + + + + + )} + + {Array.isArray(filteredIssues) && filteredIssues.length > 0 && ( + + {filteredIssues.map((issue, i) => ( + + + + {!isBug && ( + + )} + + + + + + ))} + + )} +
Bug issue keyURLSummaryLast updatedStatus
CVE keyURLComponentsSummaryLast updatedStatus
+ {issue.key ?? '-'} + + + + {issue.components && + Array.isArray(issue.components) && + issue.components.length > 0 + ? issue.components?.map((component) => ( + + {component} + + )) + : '-'} + + {issue.summary ? : '-'} + + {issue.uploadDate ?? '-'} + + {issue.status ?? '-'} + + arrayHelper.remove(i), + id: 'delete-bug', + label: isBug ? 'Delete bug' : 'Delete CVE', + }, + ]} + /> +
+ {!filteredIssues || + (filteredIssues?.length === 0 && ( +
{EmptyMsg(issueType)}
+ ))} +
+ + ); + }} + /> + ); +}; diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/BugFormContent.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/BugFormContent.tsx new file mode 100644 index 000000000..fe4fdcba3 --- /dev/null +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/BugFormContent.tsx @@ -0,0 +1,87 @@ +import * as React from 'react'; +import { + Button, + ButtonType, + ButtonVariant, + Form, + Stack, + StackItem, + Text, + TextContent, + TextVariants, +} from '@patternfly/react-core'; +import { useFormikContext } from 'formik'; +import { isEmpty } from 'lodash-es'; +import { InputField, TextAreaField } from '../../../../../shared'; +import StatusDropdown from './StatusDropdown'; +import UploadDate from './UploadDate'; + +type BugFormValues = { + issueKey: string; + url: string; + uploadDate: string; + status: string; +}; + +type BugFormContentProps = { + modalToggle: () => void; +}; +const BugFormContent: React.FC = ({ modalToggle }) => { + const { handleSubmit, isSubmitting, errors, dirty } = useFormikContext(); + + return ( +
+ + + + + Provide information about a Bug that has already been resolved. + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default BugFormContent; diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/CVEFormContent.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/CVEFormContent.tsx new file mode 100644 index 000000000..ceda64f6d --- /dev/null +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/CVEFormContent.tsx @@ -0,0 +1,91 @@ +import * as React from 'react'; +import { + Button, + ButtonType, + ButtonVariant, + Form, + Stack, + StackItem, + Text, + TextContent, + TextVariants, +} from '@patternfly/react-core'; +import { useFormikContext } from 'formik'; +import { isEmpty } from 'lodash-es'; +import { InputField, TextAreaField } from '../../../../../shared'; +import ComponentField from './ComponentField'; +import StatusDropdown from './StatusDropdown'; +import UploadDate from './UploadDate'; + +type CVEFormValues = { + CVEKey: string; + components?: string[]; + uploadDate?: string; + status?: string; + summary?: string; +}; + +type CVEFormContentProps = { + modalToggle: () => void; +}; +const CVEFormContent: React.FC = ({ modalToggle }) => { + const { handleSubmit, isSubmitting, errors, dirty } = useFormikContext(); + + return ( +
+ + + + + Provide information about a CVE that has already been resolved. + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ ); +}; + +export default CVEFormContent; diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/ComponentField.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/ComponentField.tsx new file mode 100644 index 000000000..8110475bd --- /dev/null +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/ComponentField.tsx @@ -0,0 +1,69 @@ +import * as React from 'react'; +import { + Button, + ButtonVariant, + FormGroup, + InputGroup, + Stack, + StackItem, +} from '@patternfly/react-core'; +import { MinusCircleIcon } from '@patternfly/react-icons/dist/js/icons/minus-circle-icon'; +import { PlusCircleIcon } from '@patternfly/react-icons/dist/js/icons/plus-circle-icon'; +import { FieldArray, useField } from 'formik'; +import { InputField } from '../../../../../shared'; + +type ComponentFieldProps = { + name: string; +}; + +const ComponentField: React.FC> = ({ name }) => { + const [{ value: components }, ,] = useField(name); + + return ( + { + return ( + + + {Array.isArray(components) && + components.length > 0 && + components.map((val, i) => { + return ( + + + + + + + + ); + })} + + + + + + ); + }} + /> + ); +}; + +export default ComponentField; diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/StatusDropdown.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/StatusDropdown.tsx similarity index 100% rename from src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/StatusDropdown.tsx rename to src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/StatusDropdown.tsx diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/UploadDate.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/UploadDate.tsx similarity index 68% rename from src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/UploadDate.tsx rename to src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/UploadDate.tsx index 8f9777647..1833b94e3 100644 --- a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddBugSection/UploadDate.tsx +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/UploadDate.tsx @@ -6,13 +6,13 @@ type UploadDateProps = { name: string; }; -const UploadDate: React.FC> = ({ name }) => { - const [, , { setValue }] = useField(name); +export const dateFormat = (date: Date) => + date + .toLocaleDateString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit' }) + .replace(/\//g, '-'); - const dateFormat = (date: Date) => - date - .toLocaleDateString('en-US', { year: 'numeric', month: '2-digit', day: '2-digit' }) - .replace(/\//g, '-'); +const UploadDate: React.FC> = ({ name }) => { + const [{ value = dateFormat(new Date()) }, , { setValue }] = useField(name); const dateParse = (date: string) => { const split = date.split('-'); @@ -29,11 +29,12 @@ const UploadDate: React.FC> = ({ name } return ( setValue(value)} + onChange={(event, val) => setValue(val)} /> ); }; diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/AddIssueModal.spec.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/AddIssueModal.spec.tsx new file mode 100644 index 000000000..42326e866 --- /dev/null +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/AddIssueModal.spec.tsx @@ -0,0 +1,42 @@ +import * as React from 'react'; +import '@testing-library/jest-dom'; +import { render, screen, configure, fireEvent, act } from '@testing-library/react'; +import { AddIssueModal, IssueType } from '../AddIssueModal'; + +configure({ testIdAttribute: 'data-test' }); + +describe('AddIssueModal', () => { + it('should not show modal till Modal launcher button is clicked', () => { + render(); + expect(screen.queryByTestId('add-issue-modal')).not.toBeInTheDocument(); + }); + + it('should show modal when Modal launcher button is clicked', () => { + render(); + const launchModalBtn = screen.getByTestId('modal-launch-btn'); + act(() => { + fireEvent.click(launchModalBtn); + }); + expect(screen.queryByTestId('add-issue-modal')).toBeInTheDocument(); + }); + + it('should show Bug fields for Bug IssueType', () => { + render(); + const launchModalBtn = screen.getByTestId('modal-launch-btn'); + act(() => { + fireEvent.click(launchModalBtn); + }); + screen.getByText('Add a bug fix'); + screen.getByText('Provide information about a Bug that has already been resolved.'); + }); + + it('should show CVE fields for CVE IssueType', () => { + render(); + const launchModalBtn = screen.getByTestId('modal-launch-btn'); + act(() => { + fireEvent.click(launchModalBtn); + }); + screen.getByText('Add CVE'); + screen.getByText('Provide information about a CVE that has already been resolved.'); + }); +}); diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/AddIssueSection.spec.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/AddIssueSection.spec.tsx new file mode 100644 index 000000000..b2d933ff2 --- /dev/null +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/AddIssueSection.spec.tsx @@ -0,0 +1,204 @@ +import * as React from 'react'; +import '@testing-library/jest-dom'; +import { configure, fireEvent, screen, waitFor } from '@testing-library/react'; +import { formikRenderer } from '../../../../../../utils/test-utils'; +import { IssueType } from '../AddIssueModal'; +import { AddIssueSection } from '../AddIssueSection'; + +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { + ...actual, + Link: (props) => ( + + {props.children} + + ), + useNavigate: () => jest.fn(), + useSearchParams: () => React.useState(() => new URLSearchParams()), + useParams: jest.fn(), + }; +}); + +describe('AddIssueSection for Bugs', () => { + beforeEach(() => { + configure({ testIdAttribute: 'data-test' }); + }); + + it('should show correct heading ', async () => { + formikRenderer(); + expect( + screen.getByText('Are there any bug fixes you would like to add to this release?'), + ).toBeVisible(); + }); + + it('should show correct columns ', async () => { + formikRenderer(); + screen.getByText('Bug issue key'); + screen.getByText('URL'); + screen.getByText('Summary'); + screen.getByText('Last updated'); + screen.getByText('Status'); + }); + + it('should render correct bug list ', async () => { + formikRenderer(, { + bugs: [ + { key: 'bug-nodejs', url: 'url1' }, + { key: 'bug-java', url: 'url2' }, + { key: 'bug-python', url: 'url3' }, + ], + }); + expect(screen.getByText('bug-nodejs')).toBeInTheDocument(); + expect(screen.getByText('bug-java')).toBeInTheDocument(); + expect(screen.getByText('bug-python')).toBeInTheDocument(); + }); + + it('should have no search filter onLoad ', async () => { + formikRenderer(, { + bugs: [ + { key: 'bug-nodejs', url: 'url1' }, + { key: 'bug-java', url: 'url2' }, + { key: 'bug-python', url: 'url3' }, + ], + }); + const inputFilter = screen + .getByTestId('bugs-input-filter') + .querySelector('.pf-v5-c-text-input-group__text-input'); + expect((inputFilter as HTMLInputElement).value).toBe(''); + }); + + it('should filter bug list ', async () => { + formikRenderer(, { + bugs: [ + { key: 'bug-nodejs', url: 'url1' }, + { key: 'bug-java', url: 'url2' }, + { key: 'bug-python', url: 'url3' }, + ], + }); + const tableBody = screen.getByTestId('issue-table-body'); + expect(tableBody.children.length).toBe(3); + const inputFilter = screen + .getByTestId('bugs-input-filter') + .querySelector('.pf-v5-c-text-input-group__text-input'); + + fireEvent.change(inputFilter, { target: { value: 'java' } }); + await waitFor(() => { + expect(tableBody.children.length).toBe(1); + expect(tableBody.children[0].children[0].innerHTML).toBe('bug-java'); + }); + }); + + it('should show emptyState ', async () => { + formikRenderer(, { + bugs: [], + }); + expect(screen.getByText('No Bugs found')); + }); + + it('should show filteredEmptyState ', async () => { + formikRenderer(, { + bugs: [ + { key: 'bug-nodejs', url: 'url1' }, + { key: 'bug-java', url: 'url2' }, + { key: 'bug-python', url: 'url3' }, + ], + }); + const inputFilter = screen + .getByTestId('bugs-input-filter') + .querySelector('.pf-v5-c-text-input-group__text-input'); + + fireEvent.change(inputFilter, { target: { value: 'dotnet' } }); + await waitFor(() => { + expect( + screen.getByText('No results match this filter criteria. Clear all filters and try again.'), + ); + }); + }); + + it('should filter bug list ', async () => { + formikRenderer(, { + bugs: [ + { key: 'bug-nodejs', url: 'url1' }, + { key: 'bug-java', url: 'url2' }, + { key: 'bug-python', url: 'url3' }, + ], + }); + const tableBody = screen.getByTestId('issue-table-body'); + expect(tableBody.children.length).toBe(3); + const inputFilter = screen + .getByTestId('bugs-input-filter') + .querySelector('.pf-v5-c-text-input-group__text-input'); + + fireEvent.change(inputFilter, { target: { value: 'java' } }); + await waitFor(() => { + expect(tableBody.children.length).toBe(1); + expect(tableBody.children[0].children[0].innerHTML).toBe('bug-java'); + }); + }); +}); + +describe('AddIssueSection for CVEs', () => { + beforeEach(() => { + configure({ testIdAttribute: 'data-test' }); + }); + + it('should show correct heading ', async () => { + formikRenderer(); + expect( + screen.getByText('Are there any CVEs you would like to add to this release?'), + ).toBeVisible(); + }); + + it('should show correct columns ', async () => { + formikRenderer(); + screen.getByText('CVE key'); + screen.getByText('URL'); + screen.getByText('Components'); + screen.getByText('Summary'); + screen.getByText('Last updated'); + screen.getByText('Status'); + }); + + it('should render correct cve list ', async () => { + formikRenderer(, { + cves: [ + { key: 'cve-nodejs', url: 'url1' }, + { key: 'cve-java', url: 'url2' }, + { key: 'cve-python', url: 'url3' }, + ], + }); + expect(screen.getByText('cve-nodejs')).toBeInTheDocument(); + expect(screen.getByText('cve-java')).toBeInTheDocument(); + expect(screen.getByText('cve-python')).toBeInTheDocument(); + }); + + it('should render - when data is missing', async () => { + formikRenderer(, { + cves: [{ key: 'cve-nodejs', url: 'url1' }], + }); + expect(screen.getByTestId('issue-summary').innerHTML).toBe('-'); + expect(screen.getByTestId('issue-status').innerHTML).toBe('-'); + }); + + it('should filter cves list ', async () => { + formikRenderer(, { + cves: [ + { key: 'cve-nodejs', url: 'url1' }, + { key: 'cve-java', url: 'url2' }, + { key: 'cve-python', url: 'url3' }, + ], + }); + const tableBody = screen.getByTestId('issue-table-body'); + expect(tableBody.children.length).toBe(3); + const inputFilter = screen + .getByTestId('cves-input-filter') + .querySelector('.pf-v5-c-text-input-group__text-input'); + + fireEvent.change(inputFilter, { target: { value: 'java' } }); + await waitFor(() => { + expect(tableBody.children.length).toBe(1); + expect(tableBody.children[0].children[0].innerHTML).toBe('cve-java'); + }); + }); +}); diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/BugFormContent.spec.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/BugFormContent.spec.tsx new file mode 100644 index 000000000..cb8291a2a --- /dev/null +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/BugFormContent.spec.tsx @@ -0,0 +1,41 @@ +import * as React from 'react'; +import '@testing-library/jest-dom'; +import { configure, screen } from '@testing-library/react'; +import { formikRenderer } from '../../../../../../utils/test-utils'; +import BugFormContent from '../BugFormContent'; + +describe('BugFormContent', () => { + beforeEach(() => { + configure({ testIdAttribute: 'data-test' }); + }); + + it('should show correct heading ', async () => { + formikRenderer(); + expect( + screen.getByText('Provide information about a Bug that has already been resolved.'), + ).toBeVisible(); + }); + + it('should show correct input fields ', async () => { + formikRenderer(); + + expect(screen.getByRole('textbox', { name: 'Bug issue key' })).toBeVisible(); + expect(screen.getByRole('textbox', { name: 'Summary' })).toBeVisible(); + expect(screen.getByRole('textbox', { name: 'URL' })).toBeVisible(); + }); + + it('should show correct values', async () => { + formikRenderer(, { + key: 'RHTAP-120', + url: 'url1', + summary: 'summary', + }); + expect((screen.getByRole('textbox', { name: 'Bug issue key' }) as HTMLInputElement).value).toBe( + 'RHTAP-120', + ); + expect((screen.getByRole('textbox', { name: 'URL' }) as HTMLInputElement).value).toBe('url1'); + expect((screen.getByRole('textbox', { name: 'Summary' }) as HTMLInputElement).value).toBe( + 'summary', + ); + }); +}); diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/CVEFormContent.spec.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/CVEFormContent.spec.tsx new file mode 100644 index 000000000..3b9e163b6 --- /dev/null +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/CVEFormContent.spec.tsx @@ -0,0 +1,53 @@ +import * as React from 'react'; +import '@testing-library/jest-dom'; +import { configure, screen } from '@testing-library/react'; +import { formikRenderer } from '../../../../../../utils/test-utils'; +import CVEFormContent from '../CVEFormContent'; + +describe('CVEFormContent', () => { + beforeEach(() => { + configure({ testIdAttribute: 'data-test' }); + }); + + it('should show correct heading ', async () => { + formikRenderer(); + expect( + screen.getByText('Provide information about a CVE that has already been resolved.'), + ).toBeVisible(); + }); + + it('should show correct input fields ', async () => { + formikRenderer(); + expect(screen.getByRole('textbox', { name: 'CVE key' })).toBeVisible(); + expect(screen.getByRole('textbox', { name: 'Summary' })).toBeVisible(); + expect(screen.getByRole('textbox', { name: 'URL for the CVE' })).toBeVisible(); + }); + + it('should show correct values', async () => { + formikRenderer(, { + key: 'CVE-120', + url: 'url1', + summary: 'summary', + components: ['a', 'b', 'c'], + }); + expect((screen.getByRole('textbox', { name: 'CVE key' }) as HTMLInputElement).value).toBe( + 'CVE-120', + ); + expect( + (screen.getByRole('textbox', { name: 'URL for the CVE' }) as HTMLInputElement).value, + ).toBe('url1'); + expect((screen.getByRole('textbox', { name: 'Summary' }) as HTMLInputElement).value).toBe( + 'summary', + ); + }); + + it('should render component field ', async () => { + formikRenderer(, { + key: 'CVE-120', + url: 'url1', + summary: 'summary', + components: ['a', 'b', 'c'], + }); + screen.getByTestId('component-field'); + }); +}); diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/ComponentField.spec.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/ComponentField.spec.tsx new file mode 100644 index 000000000..b2724744b --- /dev/null +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/ComponentField.spec.tsx @@ -0,0 +1,101 @@ +import * as React from 'react'; +import '@testing-library/jest-dom'; +import { act, fireEvent, screen, configure } from '@testing-library/react'; +import { formikRenderer } from '../../../../../../utils/test-utils'; +import ComponentField from '../ComponentField'; + +configure({ testIdAttribute: 'data-test' }); + +describe('ComponentField', () => { + it('should initially load with no components and add component on button click', () => { + formikRenderer(, { components: [] }); + + const addCmpBtn = screen.queryByTestId('add-component-button'); + expect(addCmpBtn).toBeInTheDocument(); + + act(() => { + fireEvent.click(addCmpBtn); + }); + expect(screen.queryByTestId('component-0')).toBeInTheDocument(); + }); + + it('should load with formik component values', async () => { + formikRenderer(, { + components: ['cmp1', 'cmp2'], + }); + + expect(screen.queryByTestId('component-0')).toBeInTheDocument(); + const addCmpBtn = screen.queryByTestId('add-component-button'); + expect(addCmpBtn).toBeInTheDocument(); + + expect(screen.queryByTestId('component-0')).toBeInTheDocument(); + expect((screen.queryByTestId('component-0') as HTMLInputElement).value).toBe('cmp1'); + expect(screen.queryByTestId('component-1')).toBeInTheDocument(); + expect((screen.queryByTestId('component-1') as HTMLInputElement).value).toBe('cmp2'); + }); + + it('should change the value properly', async () => { + formikRenderer(, { + components: ['cmp1'], + }); + + expect(screen.queryByTestId('component-0')).toBeInTheDocument(); + const addCmpBtn = screen.queryByTestId('add-component-button'); + expect(addCmpBtn).toBeInTheDocument(); + + act(() => { + fireEvent.change(screen.queryByTestId('component-0'), { + target: { value: 'component2' }, + }); + }); + + expect(screen.queryByTestId('component-0')).toBeInTheDocument(); + expect((screen.queryByTestId('component-0') as HTMLInputElement).value).toBe('component2'); + }); + + it('should change the value properly', async () => { + formikRenderer(, { + components: ['cmp1'], + }); + + expect(screen.queryByTestId('component-0')).toBeInTheDocument(); + const addCmpBtn = screen.queryByTestId('add-component-button'); + expect(addCmpBtn).toBeInTheDocument(); + + act(() => { + fireEvent.change(screen.queryByTestId('component-0'), { + target: { value: 'component2' }, + }); + }); + + expect(screen.queryByTestId('component-0')).toBeInTheDocument(); + expect((screen.queryByTestId('component-0') as HTMLInputElement).value).toBe('component2'); + }); + + it('should show disabled remove button when only one component', async () => { + formikRenderer(, { + components: ['cmp1'], + }); + + expect(screen.queryByTestId('remove-component-0')).toBeInTheDocument(); + expect(screen.queryByTestId('remove-component-0')).toBeDisabled(); + }); + + it('should remove component when remove button in clicked', async () => { + formikRenderer(, { + components: ['remove-component', 'cmp2'], + }); + + expect(screen.queryByTestId('remove-component-0')).toBeInTheDocument(); + expect(screen.queryByTestId('component-0')).toBeInTheDocument(); + expect((screen.queryByTestId('component-0') as HTMLInputElement).value).toBe( + 'remove-component', + ); + + act(() => { + fireEvent.click(screen.queryByTestId('remove-component-0')); + }); + + expect((screen.queryByTestId('component-0') as HTMLInputElement).value).toBe('cmp2'); + }); +}); diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/StatusDropdown.spec.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/StatusDropdown.spec.tsx new file mode 100644 index 000000000..701183bcb --- /dev/null +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/StatusDropdown.spec.tsx @@ -0,0 +1,45 @@ +import * as React from 'react'; +import '@testing-library/jest-dom'; +import { act, configure, fireEvent, screen, waitFor } from '@testing-library/react'; +import { formikRenderer } from '../../../../../../utils/test-utils'; +import StatusDropdown from '../StatusDropdown'; + +describe('StatusDropdown', () => { + beforeEach(() => { + configure({ testIdAttribute: 'data-test' }); + }); + + it('should show dropdown options', async () => { + formikRenderer(); + await act(async () => { + fireEvent.click(screen.getByRole('button')); + }); + + expect(screen.getByRole('menuitem', { name: 'In progress' })).toBeVisible(); + expect(screen.getByRole('menuitem', { name: 'Resolved' })).toBeVisible(); + + expect(screen.getByRole('menuitem', { name: 'Closed' })).toBeVisible(); + }); + + it('should change the status dropdown value', async () => { + formikRenderer(, { + targets: { application: 'app' }, + }); + expect(screen.queryByRole('button')).toBeInTheDocument(); + + await act(async () => { + fireEvent.click(screen.getByRole('button')); + }); + + waitFor(() => { + expect(screen.getByRole('menu')).toBeInTheDocument(); + screen.getByText('In progress'); + }); + await act(async () => { + fireEvent.click(screen.getByText('Closed')); + }); + waitFor(() => { + expect(screen.getByText('Closed')); + }); + }); +}); diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/UploadDate.spec.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/UploadDate.spec.tsx new file mode 100644 index 000000000..f1879e608 --- /dev/null +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/AddIssueSection/__tests__/UploadDate.spec.tsx @@ -0,0 +1,29 @@ +import * as React from 'react'; +import '@testing-library/jest-dom'; +import { act, configure, fireEvent, screen } from '@testing-library/react'; +import { formikRenderer } from '../../../../../../utils/test-utils'; +import UploadDate, { dateFormat } from '../UploadDate'; + +describe('UploadDate', () => { + beforeEach(() => { + configure({ testIdAttribute: 'data-test' }); + }); + + it('dateFormat should format correct date', async () => { + const formattedDate = dateFormat(new Date('10-03-2024')); + expect(formattedDate).toEqual('10-03-2024'); + }); + + it('should show dropdown if applications are loaded', async () => { + formikRenderer(); + await act(async () => { + fireEvent.click(screen.getByRole('button')); + }); + + // node.closest('input') not working + const dateInput = + screen.getByTestId('upload-date-input').children[0].children[0].children[0].children[0] + .children[0]; + expect((dateInput as HTMLInputElement).value).toBe(dateFormat(new Date())); + }); +}); diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/TriggerReleaseForm.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/TriggerReleaseForm.tsx index 61dd5bb83..6526ede5a 100644 --- a/src/components/ReleaseService/ReleasePlan/TriggerRelease/TriggerReleaseForm.tsx +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/TriggerReleaseForm.tsx @@ -1,12 +1,13 @@ import * as React from 'react'; import { Form, PageSection, PageSectionVariants } from '@patternfly/react-core'; -import { FormikProps } from 'formik'; +import { FormikProps, useField } from 'formik'; import isEmpty from 'lodash-es/isEmpty'; import PageLayout from '../../../../components/PageLayout/PageLayout'; import { FormFooter, TextAreaField } from '../../../../shared'; import KeyValueField from '../../../../shared/components/formik-fields/key-value-input-field/KeyValueInputField'; import { useWorkspaceBreadcrumbs } from '../../../../utils/breadcrumb-utils'; -import AddBugSection from './AddBugSection/AddBugSection'; +import { IssueType } from './AddIssueSection/AddIssueModal'; +import { AddIssueSection } from './AddIssueSection/AddIssueSection'; import { TriggerReleaseFormValues } from './form-utils'; import { ReleasePlanDropdown } from './ReleasePlanDropdown'; import { SnapshotDropdown } from './SnapshotDropdown'; @@ -23,6 +24,8 @@ export const TriggerReleaseForm: React.FC = ({ }) => { const breadcrumbs = useWorkspaceBreadcrumbs(); + const [{ value: labels }] = useField('labels'); + return ( = ({ helpText="The release you want to release to the environments in your target workspace." required /> - + + + = ({ name="labels" label="Labels" description="You can add labels to provide more context or tag your release plan." + entries={labels} /> diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/TriggerReleaseFormPage.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/TriggerReleaseFormPage.tsx index aa31d0e3c..e42a160b9 100644 --- a/src/components/ReleaseService/ReleasePlan/TriggerRelease/TriggerReleaseFormPage.tsx +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/TriggerReleaseFormPage.tsx @@ -26,9 +26,6 @@ export const TriggerReleaseFormPage: React.FC = ({ releasePlan }) => { }); try { - // edit - // ? await editReleasePlan(releasePlan, values, workspace, true) - // : await createReleasePlan(values, namespace, workspace, true); const newRelease = await createRelease(values, namespace, releasePlan); track('Release plan triggered', { // eslint-disable-next-line camelcase diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/AddBugModal.spec.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/AddBugModal.spec.tsx deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/ReleasePlanDropdown.spec.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/ReleasePlanDropdown.spec.tsx index e69de29bb..c4fd43ba4 100644 --- a/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/ReleasePlanDropdown.spec.tsx +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/ReleasePlanDropdown.spec.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; +import '@testing-library/jest-dom'; +import { act, configure, fireEvent, screen, waitFor } from '@testing-library/react'; +import { useReleasePlans } from '../../../../../hooks/useReleasePlans'; +import { formikRenderer } from '../../../../../utils/test-utils'; +import { ReleasePlanDropdown } from '../ReleasePlanDropdown'; + +jest.mock('../../../../../hooks/useReleasePlans', () => ({ + useReleasePlans: jest.fn(), +})); + +const useReleasePlansMock = useReleasePlans as jest.Mock; + +describe('ReleasePlanDropdown', () => { + beforeEach(() => { + configure({ testIdAttribute: 'data-test' }); + }); + + it('should show loading indicator if release plans arent loaded', () => { + useReleasePlansMock.mockReturnValue([[], false]); + formikRenderer(); + expect(screen.getByText('Loading release plans...')).toBeVisible(); + }); + + it('should show dropdown if release plans are loaded', async () => { + useReleasePlansMock.mockReturnValue([ + [{ metadata: { name: 'rp1' } }, { metadata: { name: 'rp2' } }], + true, + ]); + formikRenderer(); + await act(async () => { + fireEvent.click(screen.getByRole('button')); + }); + + expect(screen.getByRole('menuitem', { name: 'rp1' })).toBeVisible(); + expect(screen.getByRole('menuitem', { name: 'rp2' })).toBeVisible(); + }); + + it('should change the release plan dropdown value', async () => { + useReleasePlansMock.mockReturnValue([ + [{ metadata: { name: 'rp1' } }, { metadata: { name: 'rp2' } }], + true, + ]); + + formikRenderer(, { + targets: { application: 'app' }, + }); + expect(screen.queryByRole('button')).toBeInTheDocument(); + + await act(async () => { + fireEvent.click(screen.getByRole('button')); + }); + + waitFor(() => { + expect(screen.getByRole('menu')).toBeInTheDocument(); + expect(screen.getByLabelText('Select release plan')); + screen.getByText('rp2'); + }); + await act(async () => { + fireEvent.click(screen.getByText('rp2')); + }); + waitFor(() => { + expect(screen.getByText('rp2')); + }); + }); +}); diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/SnapshotDropdown.spec.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/SnapshotDropdown.spec.tsx index e69de29bb..d668de1c5 100644 --- a/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/SnapshotDropdown.spec.tsx +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/SnapshotDropdown.spec.tsx @@ -0,0 +1,66 @@ +import * as React from 'react'; +import '@testing-library/jest-dom'; +import { act, configure, fireEvent, screen, waitFor } from '@testing-library/react'; +import { useSnapshots } from '../../../../../hooks/useSnapshots'; +import { formikRenderer } from '../../../../../utils/test-utils'; +import { SnapshotDropdown } from '../SnapshotDropdown'; + +jest.mock('../../../../../hooks/useSnapshots', () => ({ + useSnapshots: jest.fn(), +})); + +const useSnapshotsMock = useSnapshots as jest.Mock; + +describe('SnapshotDropdown', () => { + beforeEach(() => { + configure({ testIdAttribute: 'data-test' }); + }); + + it('should show loading indicator if snapshot arent loaded', () => { + useSnapshotsMock.mockReturnValue([[], false]); + formikRenderer(); + expect(screen.getByText('Loading snapshots...')).toBeVisible(); + }); + + it('should show dropdown if snapshots are loaded', async () => { + useSnapshotsMock.mockReturnValue([ + [{ metadata: { name: 'snapshot1' } }, { metadata: { name: 'snapshot2' } }], + true, + ]); + formikRenderer(); + await act(async () => { + fireEvent.click(screen.getByRole('button')); + }); + + expect(screen.getByRole('menuitem', { name: 'snapshot1' })).toBeVisible(); + expect(screen.getByRole('menuitem', { name: 'snapshot2' })).toBeVisible(); + }); + + it('should change the Snapshot dropdown value', async () => { + useSnapshotsMock.mockReturnValue([ + [{ metadata: { name: 'snapshot1' } }, { metadata: { name: 'snapshot2' } }], + true, + ]); + + formikRenderer(, { + targets: { application: 'app' }, + }); + expect(screen.queryByRole('button')).toBeInTheDocument(); + + await act(async () => { + fireEvent.click(screen.getByRole('button')); + }); + + waitFor(() => { + expect(screen.getByRole('menu')).toBeInTheDocument(); + expect(screen.getByLabelText('Select snapshot')); + screen.getByText('rp2'); + }); + await act(async () => { + fireEvent.click(screen.getByText('snapshot2')); + }); + waitFor(() => { + expect(screen.getByText('snapshot2')); + }); + }); +}); diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/TriggerReleaseForm.spec.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/TriggerReleaseForm.spec.tsx index 06282ce94..605bae2ea 100644 --- a/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/TriggerReleaseForm.spec.tsx +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/TriggerReleaseForm.spec.tsx @@ -1,49 +1,69 @@ import React from 'react'; +import { screen } from '@testing-library/react'; import '@testing-library/jest-dom'; import { FormikProps } from 'formik'; +import { useReleasePlans } from '../../../../../hooks/useReleasePlans'; +import { useSnapshots } from '../../../../../hooks/useSnapshots'; import { formikRenderer } from '../../../../../utils/test-utils'; -import { WorkspaceContext } from '../../../../../utils/workspace-context-utils'; import { TriggerReleaseForm } from '../TriggerReleaseForm'; -jest.mock('react-router-dom', () => ({ - ...(jest as any).requireActual('react-router-dom'), - useLocation: jest.fn(() => ({})), - Link: (props: any) => {props.children}, - useNavigate: () => jest.fn(), +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { + ...actual, + useNavigate: () => jest.fn(), + Link: (props: any) => {props.children}, + useLocation: () => jest.fn(), + }; +}); + +jest.mock('../AddIssueSection/AddIssueSection', () => ({ + AddIssueSection: (props) => {props.name}, })); -jest.mock('../../../../../utils/workspace-context-utils', () => ({ - useWorkspaceInfo: jest.fn(() => ({ workspace: 'test-ws' })), +jest.mock('../../../../../hooks/useSnapshots', () => ({ + useSnapshots: jest.fn(), })); -jest.mock('../../../../../hooks/useApplications', () => ({ - useApplications: jest.fn(() => [[], true]), +jest.mock('../../../../../hooks/useReleasePlans', () => ({ + useReleasePlans: jest.fn(), })); jest.mock('../../../../../shared/hooks/useScrollShadows', () => ({ useScrollShadows: jest.fn().mockReturnValue('none'), })); -jest.mock('react', () => { - const actual = jest.requireActual('react'); - return { - ...actual, - useContext: jest.fn((ctx) => - ctx === WorkspaceContext - ? { namespace: 'test-ns', workspaces: [], workspacesLoaded: true } - : actual.useContext(ctx), - ), - }; -}); +const useSnapshotsMock = useSnapshots as jest.Mock; +const useReleasePlansMock = useReleasePlans as jest.Mock; -describe('ReleasePlanForm', () => { - it('should show trigger form ', () => { +describe('TriggerReleaseForm', () => { + beforeEach(() => { + useReleasePlansMock.mockReturnValue([[], false]); + useSnapshotsMock.mockReturnValue([[], false]); + }); + it('should show trigger release button and heading', () => { const values = {}; const props = { values } as FormikProps; const result = formikRenderer(, values); - expect(result.getByRole('heading', { name: 'Trigger release ' })).toBeVisible(); + expect(result.getByRole('heading', { name: 'Trigger release plan' })).toBeVisible(); expect(result.getByRole('button', { name: 'Trigger' })).toBeVisible(); - expect(result.getByRole('button', { name: 'Create' })).toBeVisible(); - expect(result.getByText('synopsis')).toBeVisible(); + }); + + it('should show trigger release input fields', () => { + const values = {}; + const props = { values } as FormikProps; + const result = formikRenderer(, values); + expect(result.getByRole('textbox', { name: 'Synopsis' })).toBeVisible(); + expect(result.getByRole('textbox', { name: 'Description' })).toBeVisible(); + expect(result.getByRole('textbox', { name: 'Topic' })).toBeVisible(); + expect(result.getByRole('textbox', { name: 'References' })).toBeVisible(); + }); + + it('should show release & snapshot dropdown in loading state', () => { + const values = {}; + const props = { values } as FormikProps; + formikRenderer(, values); + expect(screen.getByText('Loading release plans...')).toBeVisible(); + expect(screen.getByText('Loading snapshots...')).toBeVisible(); }); }); diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/TriggerReleaseFormPage.spec.tsx b/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/TriggerReleaseFormPage.spec.tsx index 625c3ef26..dfc7040f4 100644 --- a/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/TriggerReleaseFormPage.spec.tsx +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/TriggerReleaseFormPage.spec.tsx @@ -12,6 +12,7 @@ jest.mock('../../../../../utils/analytics', () => ({ })); const navigateMock = jest.fn(); + jest.mock('react-router-dom', () => ({ ...(jest as any).requireActual('react-router-dom'), useLocation: jest.fn(() => ({})), @@ -21,13 +22,12 @@ jest.mock('react-router-dom', () => ({ jest.mock('../form-utils', () => ({ ...jest.requireActual('../form-utils'), - createReleasePlan: jest.fn(), - editReleasePlan: jest.fn(), - releasePlanFormSchema: yup.object(), + createRelease: jest.fn(), + triggerReleaseFormSchema: yup.object(), })); -jest.mock('../ReleasePlanForm', () => ({ - ReleasePlanForm: ({ handleSubmit, handleReset }) => ( +jest.mock('../TriggerReleaseForm', () => ({ + TriggerReleaseForm: ({ handleSubmit, handleReset }) => ( <> @@ -35,11 +35,11 @@ jest.mock('../ReleasePlanForm', () => ({ ), })); -const createReleasePlanMock = createRelease as jest.Mock; +const triggerReleasePlanMock = createRelease as jest.Mock; -describe('ReleaseFormPage', () => { - it('should navigate on successful creation', async () => { - createReleasePlanMock.mockResolvedValue({ metadata: {}, spec: {} }); +describe('TriggerReleaseFormPage', () => { + it('should navigate on successful trigger', async () => { + triggerReleasePlanMock.mockResolvedValue({ metadata: {}, spec: {} }); namespaceRenderer(, 'test-ns', { workspace: 'test-ws', }); @@ -48,49 +48,23 @@ describe('ReleaseFormPage', () => { fireEvent.click(screen.getByRole('button', { name: 'Submit' })); }); - expect(createReleasePlanMock).toHaveBeenCalled(); - expect(createReleasePlanMock).toHaveBeenCalledWith( + expect(triggerReleasePlanMock).toHaveBeenCalled(); + expect(triggerReleasePlanMock).toHaveBeenCalledWith( expect.objectContaining({ - name: '', - application: '', - autoRelease: false, - standingAttribution: false, - data: '', - params: [], - serviceAccount: '', - target: '', - git: { - url: '', - revision: '', - path: '', - }, + description: '', + labels: [{ key: '', value: '' }], + references: '', + releasePlan: '', + snapshot: '', + synopsis: '', + topic: '', }), 'test-ns', - 'test-ws', + undefined, ); - expect(createReleasePlanMock).toHaveBeenCalledTimes(2); expect(navigateMock).toHaveBeenCalledWith('/application-pipeline/release'); }); - // it('should navigate on successful edit', async () => { - // editReleasePlanMock.mockResolvedValue({ metadata: {}, spec: {} }); - // namespaceRenderer( - // , - // 'test-ns', - // { - // workspace: 'test-ws', - // }, - // ); - - // await act(async () => { - // fireEvent.click(screen.getByRole('button', { name: 'Submit' })); - // }); - - // expect(editReleasePlanMock).toHaveBeenCalled(); - // expect(editReleasePlanMock).toHaveBeenCalledTimes(2); - // expect(navigateMock).toHaveBeenCalledWith('/application-pipeline/release'); - // }); - it('should navigate to release list on reset', async () => { namespaceRenderer( , diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/form-utils.spec.ts b/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/form-utils.spec.ts new file mode 100644 index 000000000..4c465c8a1 --- /dev/null +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/__tests__/form-utils.spec.ts @@ -0,0 +1,120 @@ +import { k8sCreateResource } from '@openshift/dynamic-plugin-sdk-utils'; +import '@testing-library/jest-dom'; +import { createRelease } from '../form-utils'; + +jest.mock('@openshift/dynamic-plugin-sdk-utils', () => ({ + k8sCreateResource: jest.fn(), +})); + +const k8sCreateMock = k8sCreateResource as jest.Mock; + +describe('triggerReleasePlan', () => { + beforeEach(() => { + k8sCreateMock.mockImplementation((obj) => obj.resource); + }); + + it('should add snapshot & releasePlan to spec.snapshot & spec.releasePlan', async () => { + const result = await createRelease( + { + snapshot: 'test-snapshot', + releasePlan: 'test-releasePlan', + synopsis: null, + topic: null, + references: null, + labels: [], + }, + 'test-ns', + null, + ); + expect(result.spec).toEqual( + expect.objectContaining({ + snapshot: 'test-snapshot', + releasePlan: 'test-releasePlan', + }), + ); + }); + + it('should add Synopsis, Topic, Description, Reference to spec.data', async () => { + const result = await createRelease( + { + snapshot: 'test-plan', + synopsis: 'synopsis', + releasePlan: 'test-releasePlan', + description: 'short description', + topic: 'topic of release', + references: 'references', + labels: [], + }, + 'test-ns', + null, + ); + expect(result.spec.data.releaseNotes).toEqual( + expect.objectContaining({ + synopsis: 'synopsis', + topic: 'topic of release', + description: 'short description', + }), + ); + }); + + it('should add Bug Data to advisory', async () => { + const result = await createRelease( + { + snapshot: 'test-plan', + synopsis: 'synopsis', + releasePlan: 'test-releasePlan', + description: 'short description', + topic: 'topic of release', + references: 'references', + issues: [ + { issueKey: 'RHTAP-5560', summary: 'summary1', url: 'test-url' }, + { issueKey: 'RHTAP-5561', summary: 'summary2', url: 'test-url2' }, + { issueKey: 'RHTAP-5562', summary: 'summary3', url: 'test-url2' }, + ], + labels: [], + }, + 'test-ns', + null, + ); + + const advisoryIssues = result.spec.data.releaseNotes.issues; + expect(advisoryIssues.length).toEqual(3); + expect(advisoryIssues[0]).toEqual( + expect.objectContaining({ + issueKey: 'RHTAP-5560', + url: 'test-url', + summary: 'summary1', + }), + ); + }); + + it('should add CVE Data to advisory', async () => { + const result = await createRelease( + { + snapshot: 'test-plan', + synopsis: 'synopsis', + releasePlan: 'test-releasePlan', + description: 'short description', + topic: 'topic of release', + references: 'references', + cves: [ + { issueKey: 'cve1', components: ['a', 'b'], url: 'test-url' }, + { issueKey: 'cve2', components: ['c', 'd'], url: 'test-url2' }, + ], + labels: [], + }, + 'test-ns', + null, + ); + + const advisoryCVE = result.spec.data.releaseNotes.cves; + expect(advisoryCVE.length).toEqual(2); + expect(advisoryCVE[0]).toEqual( + expect.objectContaining({ + issueKey: 'cve1', + url: 'test-url', + components: ['a', 'b'], + }), + ); + }); +}); diff --git a/src/components/ReleaseService/ReleasePlan/TriggerRelease/form-utils.ts b/src/components/ReleaseService/ReleasePlan/TriggerRelease/form-utils.ts index af8a00a49..3eca23f85 100644 --- a/src/components/ReleaseService/ReleasePlan/TriggerRelease/form-utils.ts +++ b/src/components/ReleaseService/ReleasePlan/TriggerRelease/form-utils.ts @@ -6,7 +6,7 @@ import { RESOURCE_NAME_LENGTH_ERROR_MSG, RESOURCE_NAME_REGEX_MSG, } from '../../../../components/ImportForm/utils/validation-utils'; -import { ReleaseModel, ReleasePlanGroupVersionKind } from '../../../../models'; +import { ReleaseGroupVersionKind, ReleaseModel } from '../../../../models'; import { ReleaseKind, ReleasePlanKind } from '../../../../types/coreBuildService'; export enum ReleasePipelineLocation { @@ -21,8 +21,9 @@ export type TriggerReleaseFormValues = { topic: string; description?: string; solution?: string; - references: string; - issues?: string[]; + references?: string; + issues?: any[]; + cves?: any[]; labels?: { key: string; value: string }[]; }; @@ -47,20 +48,23 @@ export const createRelease = async ( const { releasePlan: rp, snapshot, + cves, topic, labels: labelPairs, description, solution, issues, + references, synopsis, } = values; const labels = labelPairs .filter((l) => !!l.key) .reduce((acc, o) => ({ ...acc, [o.key]: o.value }), {} as Record); + const resource: ReleaseKind = { - apiVersion: `${ReleasePlanGroupVersionKind.group}/${ReleasePlanGroupVersionKind.version}`, - kind: ReleasePlanGroupVersionKind.kind, + apiVersion: `${ReleaseGroupVersionKind.group}/${ReleaseGroupVersionKind.version}`, + kind: ReleaseGroupVersionKind.kind, metadata: { generateName: rp, namespace, @@ -74,8 +78,10 @@ export const createRelease = async ( releasePlan: rp, snapshot, data: { - advisory: { + releaseNotes: { + cves, issues, + references, synopsis, topic, description, diff --git a/src/components/ReleaseService/ReleasePlan/__tests__/releaseplan-actions.spec.tsx b/src/components/ReleaseService/ReleasePlan/__tests__/releaseplan-actions.spec.tsx new file mode 100644 index 000000000..4ab4c01fd --- /dev/null +++ b/src/components/ReleaseService/ReleasePlan/__tests__/releaseplan-actions.spec.tsx @@ -0,0 +1,86 @@ +import '@testing-library/jest-dom'; +import { useNavigate } from 'react-router-dom'; +import { renderHook } from '@testing-library/react-hooks'; +import { runStatus } from '../../../../utils/pipeline-utils'; +import { useAccessReviewForModel } from '../../../../utils/rbac'; +import { useReleasePlanActions } from '../releaseplan-actions'; + +jest.mock('../../../../utils/rbac', () => ({ + useAccessReviewForModel: jest.fn(() => [true, true]), +})); + +jest.mock('react-router-dom', () => { + const actual = jest.requireActual('react-router-dom'); + return { + ...actual, + useNavigate: jest.fn(), + }; +}); + +jest.mock('../../../../utils/workspace-context-utils', () => ({ + useWorkspaceInfo: jest.fn(() => ({ namespace: 'test-ns', workspace: 'test-ws' })), +})); + +const useAccessReviewForModelMock = useAccessReviewForModel as jest.Mock; +const useNavigateMock = useNavigate as jest.Mock; + +describe('useReleasePlanActions', () => { + let navigateMock: jest.Mock; + + beforeEach(() => { + navigateMock = jest.fn(); + useNavigateMock.mockImplementation(() => navigateMock); + useAccessReviewForModelMock.mockReturnValue([true, true]); + }); + + it('should contain trigger actions', async () => { + const { result } = renderHook(() => + useReleasePlanActions({ + metadata: { name: 'test-release-plan' }, + status: { conditions: [{ type: 'Succeeded', status: runStatus.Running }] }, + } as any), + ); + const actions = result.current; + + expect(actions[0]).toEqual( + expect.objectContaining({ + label: 'Trigger release plan', + cta: { + href: `/application-pipeline/release/workspaces/test-ws/release-plan/trigger`, + }, + }), + ); + }); + + it('should contain Edit actions', async () => { + const { result } = renderHook(() => + useReleasePlanActions({ + metadata: { name: 'test-release-plan' }, + status: { conditions: [{ type: 'Succeeded', status: runStatus.Running }] }, + } as any), + ); + const actions = result.current; + + expect(actions[1]).toEqual( + expect.objectContaining({ + label: 'Edit release plan', + cta: { + href: `/application-pipeline/release/workspaces/test-ws/release-plan/edit/test-release-plan`, + }, + }), + ); + }); + + it('should contain Delete actions', async () => { + const { result } = renderHook(() => + useReleasePlanActions({ + metadata: { name: 'test-release-plan' }, + status: { conditions: [{ type: 'Succeeded', status: runStatus.Running }] }, + } as any), + ); + const actions = result.current; + + expect(actions[2].label).toEqual('Delete release plan'); + expect(actions[2].id).toEqual('releaseplan-delete'); + }); +}); diff --git a/src/shared/components/empty-state/FilteredEmptyState.tsx b/src/shared/components/empty-state/FilteredEmptyState.tsx index a81d0a050..f7df95061 100644 --- a/src/shared/components/empty-state/FilteredEmptyState.tsx +++ b/src/shared/components/empty-state/FilteredEmptyState.tsx @@ -19,9 +19,11 @@ const EmptyStateImg = () => ( ); const FilteredEmptyState: React.FC< - React.PropsWithChildren & { onClearFilters: () => void }> -> = ({ onClearFilters, ...props }) => ( - + React.PropsWithChildren< + Omit & { variant?: string; onClearFilters: () => void } + > +> = ({ variant = EmptyStateVariant.full, onClearFilters, ...props }) => ( + } diff --git a/src/types/coreBuildService.ts b/src/types/coreBuildService.ts index 7e9e40e17..a34f40c1c 100644 --- a/src/types/coreBuildService.ts +++ b/src/types/coreBuildService.ts @@ -70,6 +70,23 @@ export type Env = { value: string; }; +export type CVE = { + issueKey: string; + url: string; + status?: string; + summary?: string; + uploadDate?: string; + components?: string[]; +}; + +export type Issue = { + issueKey: string; + url: string; + status?: string; + summary?: string; + uploadDate?: string; +}; + export type ReleaseKind = K8sResourceCommon & { spec: ReleaseSpec; status?: ReleaseStatus; @@ -78,6 +95,17 @@ export type ReleaseKind = K8sResourceCommon & { export type ReleaseSpec = { snapshot: string; releasePlan: string; + data?: { + releaseNotes?: { + topic?: string; + description: string; + synopsis: string; + cves: CVE[]; + issues: Issue[]; + solution?: string; + references?: string; + }; + }; }; export type ReleaseStatus = {