diff --git a/plugins/shared-react/src/types/pipeline/computedStatus.ts b/plugins/shared-react/src/types/pipeline/computedStatus.ts index e69de1c8a0..4d11e2677a 100644 --- a/plugins/shared-react/src/types/pipeline/computedStatus.ts +++ b/plugins/shared-react/src/types/pipeline/computedStatus.ts @@ -3,6 +3,7 @@ export enum TerminatedReasons { } export enum ComputedStatus { + All = 'All', Cancelling = 'Cancelling', Succeeded = 'Succeeded', Failed = 'Failed', @@ -14,7 +15,7 @@ export enum ComputedStatus { Cancelled = 'Cancelled', Pending = 'Pending', Idle = 'Idle', - Other = '-', + Other = 'Other', } export enum SucceedConditionReason { @@ -45,19 +46,3 @@ export type TaskStatusTypes = { Failed: number; Skipped: number; }; - -export const computedStatus: { [key: string]: string | ComputedStatus } = { - All: '', - Cancelling: ComputedStatus.Cancelling, - Succeeded: ComputedStatus.Succeeded, - Failed: ComputedStatus.Failed, - Running: ComputedStatus.Running, - 'In Progress': ComputedStatus['In Progress'], - FailedToStart: ComputedStatus.FailedToStart, - PipelineNotStarted: ComputedStatus.PipelineNotStarted, - Skipped: ComputedStatus.Skipped, - Cancelled: ComputedStatus.Cancelled, - Pending: ComputedStatus.Pending, - Idle: ComputedStatus.Idle, - Other: ComputedStatus.Other, -}; diff --git a/plugins/shared-react/src/utils/pipeline/pipeline.test.ts b/plugins/shared-react/src/utils/pipeline/pipeline.test.ts index 67a60b1f3d..e0935b4ad5 100644 --- a/plugins/shared-react/src/utils/pipeline/pipeline.test.ts +++ b/plugins/shared-react/src/utils/pipeline/pipeline.test.ts @@ -13,15 +13,14 @@ describe('getRunStatusColor should handle ComputedStatus values', () => { it('should expect all but PipelineNotStarted to produce a non-default result', () => { // Verify that we cover colour states for all the ComputedStatus values const failCase = 'PipelineNotStarted'; - const defaultCase = getRunStatusColor(ComputedStatus[failCase]); - const allOtherStatuses = Object.keys(ComputedStatus) - .filter( - status => - status !== failCase && - ComputedStatus[status as keyof typeof ComputedStatus] !== - ComputedStatus.Other, - ) - .map(status => ComputedStatus[status as keyof typeof ComputedStatus]); + const defaultCase = getRunStatusColor(failCase); + const allStatuses = Object.keys(ComputedStatus) as ComputedStatus[]; + const allOtherStatuses = allStatuses.filter( + status => + status !== ComputedStatus.All && + status !== ComputedStatus.Other && + status !== failCase, + ); expect(allOtherStatuses).not.toHaveLength(0); allOtherStatuses.forEach(statusValue => { diff --git a/plugins/tekton/src/components/PipelineRunList/PipelineRunList.test.tsx b/plugins/tekton/src/components/PipelineRunList/PipelineRunList.test.tsx index c5b27cdc5f..c2d1394fdb 100644 --- a/plugins/tekton/src/components/PipelineRunList/PipelineRunList.test.tsx +++ b/plugins/tekton/src/components/PipelineRunList/PipelineRunList.test.tsx @@ -3,7 +3,7 @@ import { BrowserRouter } from 'react-router-dom'; import { render } from '@testing-library/react'; -import { computedStatus } from '@janus-idp/shared-react'; +import { ComputedStatus } from '@janus-idp/shared-react'; import { mockKubernetesPlrResponse } from '../../__fixtures__/1-pipelinesData'; import { TektonResourcesContext } from '../../hooks/TektonResourcesContext'; @@ -35,7 +35,7 @@ describe('PipelineRunList', () => { selectedClusterErrors: [], clusters: ['ocp'], setSelectedCluster: () => {}, - selectedStatus: '', + selectedStatus: ComputedStatus.All, setSelectedStatus: () => {}, setIsExpanded: () => {}, }; @@ -59,7 +59,7 @@ describe('PipelineRunList', () => { selectedClusterErrors: [], clusters: [], setSelectedCluster: () => {}, - selectedStatus: '', + selectedStatus: ComputedStatus.All, setSelectedStatus: () => {}, setIsExpanded: () => {}, }; @@ -87,7 +87,7 @@ describe('PipelineRunList', () => { selectedClusterErrors: [], clusters: [], setSelectedCluster: () => {}, - selectedStatus: '', + selectedStatus: ComputedStatus.All, setSelectedStatus: () => {}, setIsExpanded: () => {}, }; @@ -116,7 +116,7 @@ describe('PipelineRunList', () => { selectedClusterErrors: [], clusters: [], setSelectedCluster: () => {}, - selectedStatus: '', + selectedStatus: ComputedStatus.All, setSelectedStatus: () => {}, setIsExpanded: () => {}, }; @@ -146,7 +146,7 @@ describe('PipelineRunList', () => { selectedClusterErrors: [{ message: '403 - forbidden' }], clusters: ['ocp'], setSelectedCluster: () => {}, - selectedStatus: '', + selectedStatus: ComputedStatus.All, setSelectedStatus: () => {}, setIsExpanded: () => {}, }; @@ -175,7 +175,7 @@ describe('PipelineRunList', () => { selectedClusterErrors: [], clusters: ['ocp'], setSelectedCluster: () => {}, - selectedStatus: computedStatus.Succeeded, + selectedStatus: ComputedStatus.Succeeded, setSelectedStatus: () => {}, setIsExpanded: () => {}, }; @@ -206,7 +206,7 @@ describe('PipelineRunList', () => { selectedClusterErrors: [], clusters: ['ocp'], setSelectedCluster: () => {}, - selectedStatus: computedStatus.Cancelled, + selectedStatus: ComputedStatus.Cancelled, setSelectedStatus: () => {}, setIsExpanded: () => {}, }; diff --git a/plugins/tekton/src/components/PipelineRunList/PipelineRunList.tsx b/plugins/tekton/src/components/PipelineRunList/PipelineRunList.tsx index f26392140a..9feb0a5501 100644 --- a/plugins/tekton/src/components/PipelineRunList/PipelineRunList.tsx +++ b/plugins/tekton/src/components/PipelineRunList/PipelineRunList.tsx @@ -16,7 +16,7 @@ import { } from '@material-ui/core'; import { - computedStatus, + ComputedStatus, PipelineRunKind, pipelineRunStatus, } from '@janus-idp/shared-react'; @@ -91,36 +91,61 @@ const PipelineRunList = () => { watchResourcesData, selectedClusterErrors, clusters, + selectedCluster, selectedStatus, } = React.useContext(TektonResourcesContext); + const [search, setSearch] = React.useState(''); const [order, setOrder] = React.useState('desc'); const [orderBy, setOrderBy] = React.useState('status.startTime'); const [orderById, setOrderById] = React.useState('startTime'); // 2 columns have the same field const [page, setPage] = React.useState(0); const [rowsPerPage, setRowsPerPage] = React.useState(5); - const [filteredPipelineRuns, setFilteredPipelineRuns] = React.useState< - PipelineRunKind[] | undefined - >(); - - const totalPlrs = watchResourcesData?.pipelineruns?.data?.map(d => ({ - ...d, - id: d.metadata.uid, - })); - - const pipelineRunsData = React.useMemo( - () => - selectedStatus === computedStatus.All - ? totalPlrs - : // eslint-disable-next-line consistent-return - totalPlrs?.filter(plr => { - if (pipelineRunStatus(plr) === selectedStatus) { - return plr; - } - }), - [selectedStatus, totalPlrs], - ); - const pipelineRuns = filteredPipelineRuns || pipelineRunsData; + // Jump to first page when cluster, status and search filter changed + const updateStateOnFilterChanges = React.useRef(false); + React.useEffect(() => { + if (updateStateOnFilterChanges.current) { + setPage(0); + } else { + updateStateOnFilterChanges.current = true; + } + }, [selectedCluster, selectedStatus, search]); + + const allPipelineRuns = React.useMemo(() => { + const plrs = + watchResourcesData?.pipelineruns?.data?.map(d => ({ + ...d, + id: d.metadata.uid, + })) ?? []; + return plrs as PipelineRunKind[]; + }, [watchResourcesData]); + + const filteredPipelineRuns = React.useMemo(() => { + let plrs = allPipelineRuns; + + if (selectedStatus && selectedStatus !== ComputedStatus.All) { + plrs = plrs.filter(plr => pipelineRunStatus(plr) === selectedStatus); + } + + if (search) { + const f = search.toUpperCase(); + plrs = plrs.filter((plr: PipelineRunKind) => { + const n = plr.metadata?.name?.toUpperCase(); + return n?.includes(f); + }); + } + + plrs = plrs.sort(getComparator(order, orderBy, orderById)); + + return plrs; + }, [allPipelineRuns, selectedStatus, search, order, orderBy, orderById]); + + const visibleRows = React.useMemo(() => { + return filteredPipelineRuns.slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage, + ); + }, [filteredPipelineRuns, page, rowsPerPage]); const handleRequestSort = ( _event: React.MouseEvent, @@ -147,19 +172,12 @@ const PipelineRunList = () => { // Avoid a layout jump when reaching the last page with empty rows. const emptyRows = page > 0 - ? Math.max(0, (1 + page) * rowsPerPage - (pipelineRuns?.length ?? 0)) + ? Math.max( + 0, + (1 + page) * rowsPerPage - (filteredPipelineRuns.length ?? 0), + ) : 0; - const visibleRows = React.useMemo( - () => - pipelineRuns - ?.sort(getComparator(order, orderBy, orderById)) - .slice( - page * rowsPerPage, - page * rowsPerPage + rowsPerPage, - ) as PipelineRunKind[], - [pipelineRuns, order, orderBy, orderById, page, rowsPerPage], - ); const classes = useStyles(); const allErrors: ClusterErrors = [ @@ -186,10 +204,7 @@ const PipelineRunList = () => { Pipeline Runs - + { {emptyRows > 0 && ( - + )} @@ -221,7 +232,7 @@ const PipelineRunList = () => { { value: 10, label: '10 rows' }, { value: 25, label: '25 rows' }, ]} - count={pipelineRuns?.length ?? 0} + count={filteredPipelineRuns.length} rowsPerPage={rowsPerPage} page={page} onPageChange={handleChangePage} diff --git a/plugins/tekton/src/components/PipelineRunList/PipelineRunListSearchBar.test.tsx b/plugins/tekton/src/components/PipelineRunList/PipelineRunListSearchBar.test.tsx index f9d4c62161..d6d7fb1b84 100644 --- a/plugins/tekton/src/components/PipelineRunList/PipelineRunListSearchBar.test.tsx +++ b/plugins/tekton/src/components/PipelineRunList/PipelineRunListSearchBar.test.tsx @@ -4,14 +4,10 @@ import { fireEvent, render, screen } from '@testing-library/react'; import { PipelineRunListSearchBar } from './PipelineRunListSearchBar'; -jest.mock('react-use/lib/useDebounce', () => { - return jest.fn(fn => fn); -}); - describe('PipelineRunListSearchBar', () => { test('renders PipelineRunListSearchBar component', () => { const { getByPlaceholderText } = render( - {}} />, + {}} />, ); screen.logTestingPlaygroundURL(); @@ -20,28 +16,29 @@ describe('PipelineRunListSearchBar', () => { }); test('handles search input change', () => { - const { getByPlaceholderText } = render( - {}} />, + const onChange = jest.fn(); + const { getByPlaceholderText, getByTestId } = render( + , ); const searchInput = getByPlaceholderText('Search'); + const clearButton = getByTestId('clear-search'); + expect(clearButton.getAttribute('disabled')).toBe(''); // disabled fireEvent.change(searchInput, { target: { value: 'example' } }); - expect((searchInput as HTMLInputElement).value).toBe('example'); + expect(onChange).toHaveBeenCalledWith('example'); }); test('clears search input', () => { - const { getByPlaceholderText, getByTestId } = render( - {}} />, + const onChange = jest.fn(); + const { getByTestId } = render( + , ); - const searchInput = getByPlaceholderText('Search'); - - fireEvent.change(searchInput, { target: { value: 'example' } }); - - expect((searchInput as HTMLInputElement).value).toBe('example'); + const clearButton = getByTestId('clear-search'); + expect(clearButton.getAttribute('disabled')).toBe(null); // not disabled fireEvent.click(getByTestId('clear-search')); - expect((searchInput as HTMLInputElement).value).toBe(''); + expect(onChange).toHaveBeenCalledWith(''); }); }); diff --git a/plugins/tekton/src/components/PipelineRunList/PipelineRunListSearchBar.tsx b/plugins/tekton/src/components/PipelineRunList/PipelineRunListSearchBar.tsx index 75babfa95d..826d82b575 100644 --- a/plugins/tekton/src/components/PipelineRunList/PipelineRunListSearchBar.tsx +++ b/plugins/tekton/src/components/PipelineRunList/PipelineRunListSearchBar.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import useDebounce from 'react-use/lib/useDebounce'; import { FormControl, @@ -11,11 +10,9 @@ import { import Clear from '@material-ui/icons/Clear'; import Search from '@material-ui/icons/Search'; -import { PipelineRunKind } from '@janus-idp/shared-react'; - type PipelineRunListSearchBarProps = { - pipelineRuns?: PipelineRunKind[]; - onSearch: React.Dispatch>; + value: string; + onChange: (filter: string) => void; }; const useStyles = makeStyles({ @@ -26,40 +23,19 @@ const useStyles = makeStyles({ }); export const PipelineRunListSearchBar = ({ - pipelineRuns, - onSearch, + value, + onChange, }: PipelineRunListSearchBarProps) => { - const [search, setSearch] = React.useState(''); const classes = useStyles(); - const searchByName = () => { - const filteredPipelineRuns = - pipelineRuns && search - ? pipelineRuns.filter((plr: PipelineRunKind) => { - const s = search.toUpperCase(); - const n = plr.metadata?.name?.toUpperCase(); - return n?.includes(s); - }) - : undefined; - onSearch(filteredPipelineRuns); - }; - - useDebounce( - () => { - searchByName(); - }, - 100, - [search], - ); - return ( setSearch(event.target.value)} - value={search} + onChange={event => onChange(event.target.value)} + value={value} startAdornment={ @@ -69,9 +45,9 @@ export const PipelineRunListSearchBar = ({ setSearch('')} + onClick={() => onChange('')} edge="end" - disabled={search.length === 0} + disabled={!value} data-testid="clear-search" > diff --git a/plugins/tekton/src/components/common/ClusterSelector.tsx b/plugins/tekton/src/components/common/ClusterSelector.tsx index 5712f96122..a22a2cc95a 100644 --- a/plugins/tekton/src/components/common/ClusterSelector.tsx +++ b/plugins/tekton/src/components/common/ClusterSelector.tsx @@ -52,7 +52,6 @@ export const ClusterSelector = () => { items={clusterOptions} selected={clusterSelected} margin="dense" - native /> ); diff --git a/plugins/tekton/src/components/common/StatusSelector.tsx b/plugins/tekton/src/components/common/StatusSelector.tsx index 7741377f9e..77da3e44a4 100644 --- a/plugins/tekton/src/components/common/StatusSelector.tsx +++ b/plugins/tekton/src/components/common/StatusSelector.tsx @@ -6,7 +6,7 @@ import { makeStyles, Theme, Typography } from '@material-ui/core'; import './StatusSelector.css'; -import { computedStatus } from '@janus-idp/shared-react'; +import { ComputedStatus } from '@janus-idp/shared-react'; import { TektonResourcesContext } from '../../hooks/TektonResourcesContext'; @@ -19,18 +19,18 @@ const useStyles = makeStyles(theme => ({ }, })); -export const statusOptions = Object.keys(computedStatus) - .sort((a, b) => { - if (a === b) { +export const statusOptions = Object.entries(ComputedStatus) + .sort(([keyA], [keyB]) => { + if (keyA === keyB) { return 0; - } else if (a < b) { + } else if (keyA < keyB) { return -1; } return 1; }) - .map((status: string) => ({ - value: computedStatus[status], - label: status, + .map(([key, value]) => ({ + value: key, + label: value, })); export const StatusSelector = () => { @@ -40,7 +40,7 @@ export const StatusSelector = () => { ); const onStatusChange = (status: SelectedItems) => { - setSelectedStatus(status as string); + setSelectedStatus(status as ComputedStatus); }; return ( @@ -52,7 +52,6 @@ export const StatusSelector = () => { items={statusOptions} selected={selectedStatus} margin="dense" - native /> ); diff --git a/plugins/tekton/src/components/pipeline-topology/PipelineVisualizationView.test.tsx b/plugins/tekton/src/components/pipeline-topology/PipelineVisualizationView.test.tsx index e71677264a..3a76da688b 100644 --- a/plugins/tekton/src/components/pipeline-topology/PipelineVisualizationView.test.tsx +++ b/plugins/tekton/src/components/pipeline-topology/PipelineVisualizationView.test.tsx @@ -3,6 +3,8 @@ import { BrowserRouter } from 'react-router-dom'; import { render } from '@testing-library/react'; +import { ComputedStatus } from '@janus-idp/shared-react'; + import { mockKubernetesPlrResponse } from '../../__fixtures__/1-pipelinesData'; import { TektonResourcesContext } from '../../hooks/TektonResourcesContext'; import { PipelineVisualizationView } from './PipelineVisualizationView'; @@ -28,7 +30,7 @@ describe('PipelineVisualizationView', () => { selectedClusterErrors: [], clusters: [], setSelectedCluster: () => {}, - selectedStatus: '', + selectedStatus: ComputedStatus.All, setSelectedStatus: () => {}, setIsExpanded: () => {}, }; diff --git a/plugins/tekton/src/hooks/TektonResourcesContext.ts b/plugins/tekton/src/hooks/TektonResourcesContext.ts index d79d76ec34..9e71f3621b 100644 --- a/plugins/tekton/src/hooks/TektonResourcesContext.ts +++ b/plugins/tekton/src/hooks/TektonResourcesContext.ts @@ -1,13 +1,13 @@ import React from 'react'; -import { computedStatus } from '@janus-idp/shared-react'; +import { ComputedStatus } from '@janus-idp/shared-react'; import { TektonResourcesContextData } from '../types/types'; export const TektonResourcesContext = React.createContext({ clusters: [], - selectedStatus: computedStatus.All, + selectedStatus: ComputedStatus.All, setSelectedCluster: () => {}, setSelectedStatus: () => {}, setIsExpanded: () => {}, diff --git a/plugins/tekton/src/hooks/useTektonObjectsResponse.ts b/plugins/tekton/src/hooks/useTektonObjectsResponse.ts index 9cd0a8c294..d77102daf8 100644 --- a/plugins/tekton/src/hooks/useTektonObjectsResponse.ts +++ b/plugins/tekton/src/hooks/useTektonObjectsResponse.ts @@ -6,7 +6,7 @@ import { useKubernetesObjects } from '@backstage/plugin-kubernetes'; import { isEqual } from 'lodash'; import { - computedStatus, + ComputedStatus, useDebounceCallback, useDeepCompareMemoize, } from '@janus-idp/shared-react'; @@ -21,8 +21,8 @@ export const useTektonObjectsResponse = ( const { entity } = useEntity(); const { kubernetesObjects, loading, error } = useKubernetesObjects(entity); const [selectedCluster, setSelectedCluster] = React.useState(0); - const [selectedStatus, setSelectedStatus] = React.useState( - computedStatus.All, + const [selectedStatus, setSelectedStatus] = React.useState( + ComputedStatus.All, ); const [isExpanded, setIsExpanded] = React.useState(false); const [loaded, setLoaded] = React.useState(false); diff --git a/plugins/tekton/src/types/types.ts b/plugins/tekton/src/types/types.ts index 52ae6f117f..8ac200fc2b 100644 --- a/plugins/tekton/src/types/types.ts +++ b/plugins/tekton/src/types/types.ts @@ -1,3 +1,5 @@ +import { ComputedStatus } from '@janus-idp/shared-react'; + export const tektonGroupColor = '#38812f'; export type GroupVersionKind = { @@ -27,8 +29,8 @@ export type TektonResourcesContextData = { clusters: string[]; selectedCluster?: number; setSelectedCluster: React.Dispatch>; - selectedStatus: string; - setSelectedStatus: React.Dispatch>; + selectedStatus: ComputedStatus; + setSelectedStatus: React.Dispatch>; isExpanded?: boolean; setIsExpanded: React.Dispatch>; }; diff --git a/plugins/tekton/src/utils/pipeline-step-utils.test.ts b/plugins/tekton/src/utils/pipeline-step-utils.test.ts index c1676fde5e..e46359781f 100644 --- a/plugins/tekton/src/utils/pipeline-step-utils.test.ts +++ b/plugins/tekton/src/utils/pipeline-step-utils.test.ts @@ -87,7 +87,7 @@ describe('createStepStatus', () => { expect(stepStatus).toEqual({ duration: undefined, name: '', - status: '-', + status: ComputedStatus.Other, }); }); });