Skip to content

Commit

Permalink
feat(release): trigger release form (#911)
Browse files Browse the repository at this point in the history
* feat(release): trigger release plan

* feat(release): add bug to release

* feat(releases): bugs

* feat(releases): add cve
  • Loading branch information
abhinandan13jan authored Mar 18, 2024
1 parent d24cb92 commit cc55262
Show file tree
Hide file tree
Showing 32 changed files with 2,155 additions and 5 deletions.
14 changes: 14 additions & 0 deletions config/remotePlugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -634,6 +634,19 @@ const routeExtensions = [
required: ['SIGNUP'],
},
},
{
type: 'core.page/route',
properties: {
path: '/application-pipeline/release/workspaces/:workspaceName/release-plan/trigger',
exact: true,
component: {
$codeRef: 'TriggerReleasePlan',
},
},
flags: {
required: ['SIGNUP'],
},
},
{
type: 'core.page/route',
properties: {
Expand Down Expand Up @@ -761,6 +774,7 @@ module.exports = {
CreateEnvironment: resolve(__dirname, '../src/pages/CreateEnvironmentPage'),
ReleaseListPage: resolve(__dirname, '../src/pages/ReleaseServicesListPage'),
CreateReleasePlan: resolve(__dirname, '../src/pages/CreateReleasePlanPage'),
TriggerReleasePlan: resolve(__dirname, '../src/pages/TriggerReleasePlanPage'),
EditReleasePlan: resolve(__dirname, '../src/pages/EditReleasePlanPage'),
WorkspaceContext: resolve(__dirname, '../src/utils/workspace-context-utils'),
WorkspacedPage: resolve(__dirname, '../src/pages/WorkspacedPage'),
Expand Down
5 changes: 5 additions & 0 deletions src/components/ImportForm/utils/validation-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ export const containerImageRegex = /^(https:\/\/)?quay.io\/([a-z0-9-_]+\/)?[^/.]
export const MAX_RESOURCE_NAME_LENGTH = 63;
export const RESOURCE_NAME_LENGTH_ERROR_MSG = `Must be no more than ${MAX_RESOURCE_NAME_LENGTH} characters.`;

export const urlRegex =
/^(?:(?:(?:https?|ftp):)?\/\/)(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u00a1-\uffff][a-z0-9\u00a1-\uffff_-]{0,62})?[a-z0-9\u00a1-\uffff]\.)+(?:[a-z\u00a1-\uffff]{2,}\.?))(?::\d{2,5})?(?:[/?#]\S*)?$/i;

export const URL_ERROR_MSG = `Invalid URL.`;

export const resourceNameRegex = /^[a-z]([-a-z0-9]*[a-z0-9])?$/;
export const RESOURCE_NAME_REGEX_MSG =
'Must start with a letter and end with a letter or number. Valid characters include lowercase letters from a to z, numbers from 0 to 9, and hyphens ( - ).';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import * as React from 'react';
import { Button, Modal, ModalVariant } from '@patternfly/react-core';
import { Formik } from 'formik';
import * as yup from 'yup';
import { URL_ERROR_MSG, urlRegex } from '../../../../ImportForm/utils/validation-utils';
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().matches(urlRegex, URL_ERROR_MSG).required('Required'),
});

export const AddIssueModal: React.FC<React.PropsWithChildren<AddIssueModalProps>> = ({
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 (
<>
<Button variant="primary" onClick={handleModalToggle} data-test="modal-launch-btn">
{isBug ? 'Add a bug' : 'Add a CVE'}
</Button>
<Modal
variant={ModalVariant.medium}
title={isBug ? 'Add a bug fix' : 'Add a CVE'}
isOpen={isModalOpen}
onClose={handleModalToggle}
data-test="add-issue-modal"
>
<Formik
onSubmit={setValues}
initialValues={
isBug
? { key: '', url: '', summary: '', uploadDate: dateFormat(new Date()) }
: {
key: '',
components: [],
url: '',
summary: '',
uploadDate: dateFormat(new Date()),
}
}
validationSchema={IssueFormSchema}
>
{isBug ? (
<BugFormContent modalToggle={handleModalToggle} />
) : (
<CVEFormContent modalToggle={handleModalToggle} />
)}
</Formik>
</Modal>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
.add-bug-section {
&__emptyMsg {
padding: 0;
margin: 0;
padding-top: var(--pf-v5-global--spacer--sm);
width: 100%;
text-align: center;
}
}
Original file line number Diff line number Diff line change
@@ -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<React.PropsWithChildren<AddIssueSectionProps>> = ({
field,
issueType,
}) => {
const [nameFilter, setNameFilter] = useSearchParam(field, '');
const [{ value: issues }, ,] = useField<IssueObject[]>(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 ? (
<FilteredEmptyState onClearFilters={onClearFilters} variant={EmptyStateVariant.xs} />
) : (
<EmptyState className="pf-v5-u-m-0 pf-v5-u-p-0" variant={EmptyStateVariant.xs}>
<EmptyStateBody className="pf-v5-u-m-0 pf-v5-u-p-0">
{type === IssueType.BUG ? 'No Bugs found' : 'No CVEs found'}
</EmptyStateBody>
</EmptyState>
);

return (
<FieldArray
name={field}
render={(arrayHelper) => {
const addNewBug = (bug) => {
arrayHelper.push(bug);
};

return (
<>
<TextContent className="pf-v5-u-mt-xs">
<Text component={TextVariants.h4} className="pf-v5-u-mt-0 pf-v5-u-pt-0">
{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?'}
</Text>
</TextContent>
<Toolbar
data-test="pipelinerun-list-toolbar"
clearAllFilters={onClearFilters}
className="pf-v5-u-mb-0 pf-v5-u-pb-0 pf-v5-u-pl-0"
>
<ToolbarContent>
<ToolbarGroup align={{ default: 'alignLeft' }}>
<ToolbarItem className="pf-v5-u-ml-0">
<SearchInput
name="nameInput"
data-test={`${field}-input-filter`}
type="search"
aria-label="name filter"
placeholder="Filter by name..."
onChange={(e, n) => onNameInput(n)}
value={nameFilter}
/>
</ToolbarItem>
<ToolbarItem>
<AddIssueModal bugArrayHelper={addNewBug} issueType={issueType} />
</ToolbarItem>
</ToolbarGroup>
</ToolbarContent>
</Toolbar>
<div className="pf-v5-u-mb-md">
<Table
aria-label="Simple table"
variant="compact"
borders
className="pf-v5-u-m-0 pf-v5-u-p-0"
>
{isBug ? (
<Thead>
<Tr>
<Th className={issueTableColumnClass.issueKey}>Bug issue key</Th>
<Th className={issueTableColumnClass.bugUrl}>URL</Th>
<Th className={issueTableColumnClass.summary}>Summary</Th>
<Th className={issueTableColumnClass.uploadDate}>Last updated</Th>
<Th className={issueTableColumnClass.status}>Status</Th>
</Tr>
</Thead>
) : (
<Thead>
<Tr>
<Th className={issueTableColumnClass.issueKey}>CVE key</Th>
<Th className={issueTableColumnClass.cveUrl}>URL</Th>
<Th className={issueTableColumnClass.components}>Components</Th>
<Th className={issueTableColumnClass.summary}>Summary</Th>
<Th className={issueTableColumnClass.uploadDate}>Last updated</Th>
<Th className={issueTableColumnClass.status}>Status</Th>
</Tr>
</Thead>
)}

{Array.isArray(filteredIssues) && filteredIssues.length > 0 && (
<Tbody data-test="issue-table-body">
{filteredIssues.map((issue, i) => (
<Tr key={issue.key}>
<Td className={issueTableColumnClass.issueKey} data-test="issue-key">
{issue.key ?? '-'}
</Td>
<Td
className={
isBug ? issueTableColumnClass.bugUrl : issueTableColumnClass.bugUrl
}
data-test="issue-url"
>
<Truncate content={issue.url} />
</Td>
{!isBug && (
<Td className={issueTableColumnClass.components}>
{issue.components &&
Array.isArray(issue.components) &&
issue.components.length > 0
? issue.components?.map((component) => (
<span key={component} className="pf-v5-u-mr-sm">
{component}
</span>
))
: '-'}
</Td>
)}
<Td className={issueTableColumnClass.summary} data-test="issue-summary">
{issue.summary ? <Truncate content={issue.summary} /> : '-'}
</Td>
<Td
className={issueTableColumnClass.uploadDate}
data-test="issue-uploadDate"
>
{issue.uploadDate ?? '-'}
</Td>
<Td className={issueTableColumnClass.status} data-test="issue-status">
{issue.status ?? '-'}
</Td>
<Td className={issueTableColumnClass.kebab}>
<ActionMenu
actions={[
{
cta: () => arrayHelper.remove(i),
id: 'delete-bug',
label: isBug ? 'Delete bug' : 'Delete CVE',
},
]}
/>
</Td>
</Tr>
))}
</Tbody>
)}
</Table>
{!filteredIssues ||
(filteredIssues?.length === 0 && (
<div className="add-issue-section__emptyMsg">{EmptyMsg(issueType)}</div>
))}
</div>
</>
);
}}
/>
);
};
Loading

0 comments on commit cc55262

Please sign in to comment.