Skip to content

Commit

Permalink
feat: add search + status executions filters (#986)
Browse files Browse the repository at this point in the history
* feat: add search + status executions filters

* refactor: filters container styling

* fix: linting

* fix: linting 2
  • Loading branch information
topliceanurazvan authored Jan 15, 2024
1 parent 2296ad9 commit ae29abe
Show file tree
Hide file tree
Showing 10 changed files with 299 additions and 45 deletions.
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {FC, PropsWithChildren, useEffect, useMemo} from 'react';
import {FC, PropsWithChildren, useEffect, useMemo} from 'react';
import {useAsync, useInterval} from 'react-use';
import useWebSocket from 'react-use-websocket';

Expand Down Expand Up @@ -55,21 +55,27 @@ const EntityDetailsLayer: FC<PropsWithChildren<EntityDetailsLayerProps>> = ({
const [metrics, setMetrics] = useEntityDetailsField('metrics');
const [, setCurrentPage] = useEntityDetailsField('currentPage');
const [executions, setExecutions] = useEntityDetailsField('executions');
const {details: storeDetails} = useEntityDetailsPick('details');
const [, setExecutionsLoading] = useEntityDetailsField('executionsLoading');
const [, setIsFirstTimeLoading] = useEntityDetailsField('isFirstTimeLoading');
const [daysFilterValue, setDaysFilterValue] = useEntityDetailsField('daysFilterValue');
const {executionsFilters} = useEntityDetailsPick('executionsFilters');

const isClusterAvailable = useSystemAccess(SystemAccess.agent);
const isSystemAvailable = useSystemAccess(SystemAccess.system);
const wsRoot = useWsEndpoint();

const {data: rawExecutions, refetch} = useGetExecutions(
{id, last: daysFilterValue},
const {
data: rawExecutions,
isFetching,
refetch,
} = useGetExecutions(
{id, last: daysFilterValue, ...executionsFilters},
{
pollingInterval: PollingIntervals.long,
skip: !isSystemAvailable,
}
);

const {data: rawMetrics, refetch: refetchMetrics} = useGetMetrics(
{id, last: daysFilterValue},
{skip: !isSystemAvailable}
Expand All @@ -78,8 +84,9 @@ const EntityDetailsLayer: FC<PropsWithChildren<EntityDetailsLayerProps>> = ({
pollingInterval: PollingIntervals.long,
skip: !isSystemAvailable,
});

const isV2 = isTestSuiteV2(rawDetails);
const details = useMemo(() => (isV2 ? convertTestSuiteV2ExecutionToV3(rawDetails) : rawDetails), [rawDetails]);
const details = useMemo(() => (isV2 ? convertTestSuiteV2ExecutionToV3(rawDetails) : rawDetails), [isV2, rawDetails]);

const onWebSocketData = (wsData: WSDataWithTestExecution | WSDataWithTestSuiteExecution) => {
try {
Expand Down Expand Up @@ -196,6 +203,10 @@ const EntityDetailsLayer: FC<PropsWithChildren<EntityDetailsLayerProps>> = ({
!tokenLoading && isClusterAvailable
);

useEffect(() => {
setExecutionsLoading(isFetching);
}, [isFetching, setExecutionsLoading]);

useEffect(() => {
if (execId && executions?.results.length > 0) {
const executionDetails = executions?.results?.find((execution: any) => execution.id === execId);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import {Input} from 'antd';

import styled from 'styled-components';

export const FiltersContainer = styled.div`
display: grid;
grid-template-columns: repeat(2, minmax(120px, 248px));
grid-column-gap: 16px;
align-items: center;
width: 100%;
margin: 8px 0 24px;
`;

export const SearchInput = styled(Input)`
height: 46px;
width: auto;
`;
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {useEffect, useState} from 'react';
import {useDebounce} from 'react-use';

import {SearchOutlined} from '@ant-design/icons';

import {useEntityDetailsField, useEntityDetailsPick} from '@store/entityDetails';

import Colors from '@styles/Colors';

import * as S from './ExecutionsFilters.styled';
import ExecutionsStatusFilter from './ExecutionsStatusFilter';

const ExecutionsFilters: React.FC = () => {
const [executionsFilters, setExecutionsFilters] = useEntityDetailsField('executionsFilters');
const {executionsLoading} = useEntityDetailsPick('executionsLoading');

const [searchInputValue, setSearchInputValue] = useState(executionsFilters.textSearch);

const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchInputValue(e.target.value);
};

const [, cancel] = useDebounce(
() => {
setExecutionsFilters({...executionsFilters, textSearch: searchInputValue});
},
300,
[searchInputValue]
);

useEffect(() => {
return () => {
cancel();
};
}, [cancel]);

return (
<S.FiltersContainer>
<S.SearchInput
prefix={<SearchOutlined style={{color: Colors.slate500}} />}
placeholder="Search execution"
data-cy="executions-search-filter"
value={searchInputValue}
onChange={onChange}
disabled={executionsLoading}
/>
<ExecutionsStatusFilter />
</S.FiltersContainer>
);
};

export default ExecutionsFilters;
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import {useCallback, useMemo, useState} from 'react';

import {FilterFilled} from '@ant-design/icons';

import {capitalize} from 'lodash';

import {ExecutionStatusEnum, executionStatusList} from '@models/execution';

import {
FilterMenuFooter,
StyledFilterCheckbox,
StyledFilterDropdown,
StyledFilterLabel,
StyledFilterMenu,
StyledFilterMenuItem,
} from '@molecules/FilterMenu';

import {useEntityDetailsField, useEntityDetailsPick} from '@store/entityDetails';

import Colors from '@styles/Colors';

const ExecutionsStatusFilter: React.FC = () => {
const [executionsFilters, setExecutionsFilters] = useEntityDetailsField('executionsFilters');
const {executionsLoading} = useEntityDetailsPick('executionsLoading');

const [isVisible, setVisibilityState] = useState(false);

const handleClick = useCallback(
(status: ExecutionStatusEnum) => {
if (executionsFilters.status.includes(status)) {
setExecutionsFilters({
...executionsFilters,
status: executionsFilters.status.filter((currentStatus: string) => {
return status !== currentStatus;
}),
});
} else {
setExecutionsFilters({
...executionsFilters,
status: [...executionsFilters.status, status],
});
}
},
[executionsFilters, setExecutionsFilters]
);

const renderedStatuses = useMemo(() => {
return executionStatusList.map(status => {
return (
<StyledFilterMenuItem key={status}>
<StyledFilterCheckbox
checked={executionsFilters.status.includes(status)}
onChange={() => handleClick(status)}
data-testid={status}
>
{capitalize(status)}
</StyledFilterCheckbox>
</StyledFilterMenuItem>
);
});
}, [executionsFilters.status, handleClick]);

const resetFilter = () => {
setExecutionsFilters({...executionsFilters, status: []});
setVisibilityState(false);
};

const menu = (
<StyledFilterMenu data-cy="status-filter-dropdown">
{renderedStatuses}
<FilterMenuFooter onReset={resetFilter} onOk={() => setVisibilityState(false)} />
</StyledFilterMenu>
);

return (
<StyledFilterDropdown
overlay={menu}
trigger={['click']}
placement="bottom"
onOpenChange={(value: boolean) => setVisibilityState(value)}
open={isVisible}
disabled={executionsLoading}
>
<StyledFilterLabel
onClick={e => e.preventDefault()}
data-cy="executions-status-filter-button"
$disabled={executionsLoading}
>
Status <FilterFilled style={{color: executionsFilters.status.length ? Colors.purple : Colors.slate500}} />
</StyledFilterLabel>
</StyledFilterDropdown>
);
};

export default ExecutionsStatusFilter;
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {default} from './ExecutionsFilters';
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@ import {Skeleton} from '@custom-antd';

import {SystemAccess, useSystemAccess} from '@hooks/useSystemAccess';

import EmptyDataWithFilters from '@organisms/EntityView/EmptyDataWithFilters';

import {useEntityDetailsField, useEntityDetailsPick} from '@store/entityDetails';
import {useExecutionDetailsPick} from '@store/executionDetails';

import EmptyExecutionsListContent from './EmptyExecutionsListContent';
import ExecutionsFilters from './ExecutionsFilters';
import TableRow from './TableRow';

interface ExecutionsTableProps {
Expand All @@ -25,7 +28,13 @@ const getKey = (record: any) => record.id;

const ExecutionsTable: React.FC<ExecutionsTableProps> = ({onRun, useAbortExecution}) => {
const [currentPage, setCurrentPage] = useEntityDetailsField('currentPage');
const {executions, id, isFirstTimeLoading} = useEntityDetailsPick('executions', 'id', 'isFirstTimeLoading');
const [executionsFilters, setExecutionsFilters] = useEntityDetailsField('executionsFilters');
const {executions, executionsLoading, id, isFirstTimeLoading} = useEntityDetailsPick(
'executions',
'id',
'isFirstTimeLoading',
'executionsLoading'
);
const {id: execId, open} = useExecutionDetailsPick('id', 'open');
const isWritable = useSystemAccess(SystemAccess.agent);

Expand All @@ -45,10 +54,14 @@ const ExecutionsTable: React.FC<ExecutionsTableProps> = ({onRun, useAbortExecuti
onChange: setCurrentPage,
showSizeChanger: false,
}),
[currentPage]
[currentPage, setCurrentPage]
);

const isEmptyExecutions = !executions?.results || !executions?.results.length;
const isFiltering = useMemo(
() => Boolean(executionsFilters.textSearch.trim().length || executionsFilters.status.length),
[executionsFilters]
);
const isEmptyExecutions = useMemo(() => !isFiltering && !executions?.results.length, [executions, isFiltering]);

const [abortExecution] = useAbortExecution();
const onAbortExecution = useCallback(
Expand All @@ -75,30 +88,44 @@ const ExecutionsTable: React.FC<ExecutionsTableProps> = ({onRun, useAbortExecuti
);

if (isFirstTimeLoading) {
return (
<>
<Skeleton additionalStyles={{lineHeight: 40}} />
<Skeleton additionalStyles={{lineHeight: 40}} />
<Skeleton additionalStyles={{lineHeight: 40}} />
</>
);
return <LoadingSkeleton />;
}

if (isEmptyExecutions) {
if (isEmptyExecutions && !isFiltering) {
return <EmptyExecutionsListContent onRun={onRun} />;
}

return (
<Table
className="custom-table"
showHeader={false}
dataSource={executions?.results}
columns={columns}
onRow={onRow}
rowSelection={rowSelection}
rowKey={getKey}
pagination={pagination}
/>
<>
<ExecutionsFilters />

{isFiltering && executionsLoading ? (
<LoadingSkeleton />
) : isFiltering && !executionsLoading && !executions?.results.length ? (
<EmptyDataWithFilters resetFilters={() => setExecutionsFilters({textSearch: '', status: []})} />
) : (
<Table
className="custom-table"
showHeader={false}
dataSource={executions?.results}
columns={columns}
onRow={onRow}
rowSelection={rowSelection}
rowKey={getKey}
pagination={pagination}
/>
)}
</>
);
};

const LoadingSkeleton = () => {
return (
<>
<Skeleton additionalStyles={{lineHeight: 40}} />
<Skeleton additionalStyles={{lineHeight: 40}} />
<Skeleton additionalStyles={{lineHeight: 40}} />
</>
);
};

Expand Down
18 changes: 17 additions & 1 deletion packages/web/src/models/execution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import type {TestContent} from '@models/test';
import type {TestExecutor} from '@models/testExecutors';
import type {Variables} from '@models/variable';

export type ExecutionStatusEnum = 'running' | 'passed' | 'failed' | 'queued' | 'cancelled' | 'aborted' | 'pending';
export const executionStatusList = ['running', 'passed', 'failed', 'queued', 'timeout', 'aborted'] as const;

export type ExecutionStatusEnum = (typeof executionStatusList)[number];
export type ExecutionResultOutputTypeEnum = 'text/plain' | 'application/junit+xml' | 'application/json';
export type ExecutionStepResultStatusEnum = 'success' | 'error';

Expand Down Expand Up @@ -66,3 +68,17 @@ export type ExecutionRequest = {
httpsProxy: string;
activeDeadlineSeconds?: number;
};

export type ExecutionTotals = {
results: number;
passed: number;
failed: number;
queued: number;
running: number;
};

export type ExecutionsResponse = {
results: Execution[];
totals: ExecutionTotals;
filtered: ExecutionTotals;
};
Loading

0 comments on commit ae29abe

Please sign in to comment.