diff --git a/CHANGELOG.md b/CHANGELOG.md index 97c021e0457..64b6c557f26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -32,7 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Annotations filters UI using react-awesome-query-builder (https://github.com/openvinotoolkit/cvat/issues/1418) - Storing settings in local storage to keep them between browser sessions () - [ICDAR](https://rrc.cvc.uab.es/?ch=2) format support () -- Added switcher to maintain poylgon crop behaviour () +- Added switcher to maintain poylgon crop behaviour ( +- Filters and sorting options for job list, added tooltip for tasks filters () ### Changed diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index bd6409da7fc..7b66356be41 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.17.0", + "version": "1.18.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index dc70de606b3..d885b6129e7 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.17.0", + "version": "1.18.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/components/projects-page/search-field.tsx b/cvat-ui/src/components/projects-page/search-field.tsx index bce356aa434..9d40fa887c2 100644 --- a/cvat-ui/src/components/projects-page/search-field.tsx +++ b/cvat-ui/src/components/projects-page/search-field.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -7,6 +7,7 @@ import React from 'react'; import { useDispatch, useSelector } from 'react-redux'; import Search from 'antd/lib/input/Search'; +import SearchTooltip from 'components/search-tooltip/search-tooltip'; import { CombinedState, ProjectsQuery } from 'reducers/interfaces'; import { getProjectsAsync } from 'actions/projects-actions'; @@ -37,7 +38,10 @@ export default function ProjectSearchField(): JSX.Element { const handleSearch = (value: string): void => { const query = { ...gettingQuery }; - const search = value.replace(/\s+/g, ' ').replace(/\s*:+\s*/g, ':').trim(); + const search = value + .replace(/\s+/g, ' ') + .replace(/\s*:+\s*/g, ':') + .trim(); const fields = Object.keys(query).filter((key) => key !== 'page'); for (const field of fields) { @@ -71,11 +75,13 @@ export default function ProjectSearchField(): JSX.Element { }; return ( - + + + ); } diff --git a/cvat-ui/src/components/search-tooltip/search-tooltip.tsx b/cvat-ui/src/components/search-tooltip/search-tooltip.tsx new file mode 100644 index 00000000000..4e8a8bb0e5c --- /dev/null +++ b/cvat-ui/src/components/search-tooltip/search-tooltip.tsx @@ -0,0 +1,94 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; + +import Text from 'antd/lib/typography/Text'; +import Paragraph from 'antd/lib/typography/Paragraph'; + +import './styles.scss'; +import CVATTooltip from 'components/common/cvat-tooltip'; + +interface Props { + instance: 'task' | 'project'; + children: JSX.Element; +} + +export default function SearchTooltip(props: Props): JSX.Element { + const { instance, children } = props; + const instances = ` ${instance}s `; + + return ( + + + owner: admin + + all + {instances} + created by the user who has the substring + admin + in their username + + + + assignee: employee + + all + {instances} + which are assigned to a user who has the substring + admin + in their username + + + + name: training + + all + {instances} + with the substring + training + in its name + + + {instance === 'task' ? ( + + mode: annotation + + annotation tasks are tasks with images, interpolation tasks are tasks with videos + + + ) : null} + + status: annotation + annotation, validation, or completed + + + id: 5 + + the + {` ${instance} `} + with id 5 + + + + + Filters can be combined (to the exclusion of id) using the keyword AND. Example: + + status: annotation AND owner: admin + + + + + Search within all the string fields by default + + + )} + > + {children} + + ); +} diff --git a/cvat-ui/src/components/search-tooltip/styles.scss b/cvat-ui/src/components/search-tooltip/styles.scss new file mode 100644 index 00000000000..3f1d9487932 --- /dev/null +++ b/cvat-ui/src/components/search-tooltip/styles.scss @@ -0,0 +1,16 @@ +// Copyright (C) 2021 Intel Corporation +// +// SPDX-License-Identifier: MIT + +@import '../../base.scss'; + +.cvat-projects-search-tooltip, +.cvat-tasks-search-tooltip { + span { + color: white; + } + + strong::after { + content: ' - '; + } +} diff --git a/cvat-ui/src/components/task-page/job-list.tsx b/cvat-ui/src/components/task-page/job-list.tsx index a7cd55a7cc9..55cde2d16a4 100644 --- a/cvat-ui/src/components/task-page/job-list.tsx +++ b/cvat-ui/src/components/task-page/job-list.tsx @@ -7,6 +7,7 @@ import { RouteComponentProps } from 'react-router'; import { withRouter } from 'react-router-dom'; import { Row, Col } from 'antd/lib/grid'; import { LoadingOutlined, QuestionCircleOutlined, CopyOutlined } from '@ant-design/icons'; +import { ColumnFilterItem } from 'antd/lib/table/interface'; import Table from 'antd/lib/table'; import Button from 'antd/lib/button'; import Text from 'antd/lib/typography/Text'; @@ -101,6 +102,46 @@ function JobListComponent(props: Props & RouteComponentProps): JSX.Element { } = props; const { jobs, id: taskId } = taskInstance; + + function sorter(path: string) { + return (obj1: any, obj2: any): number => { + let currentObj1 = obj1; + let currentObj2 = obj2; + let field1: string | null = null; + let field2: string | null = null; + for (const pathSegment of path.split('.')) { + field1 = currentObj1 && pathSegment in currentObj1 ? currentObj1[pathSegment] : null; + field2 = currentObj2 && pathSegment in currentObj2 ? currentObj2[pathSegment] : null; + currentObj1 = currentObj1 && pathSegment in currentObj1 ? currentObj1[pathSegment] : null; + currentObj2 = currentObj2 && pathSegment in currentObj2 ? currentObj2[pathSegment] : null; + } + + if (field1 && field2) { + return field1.localeCompare(field2); + } + + if (field1 === null) { + return 1; + } + + return -1; + }; + } + + function collectUsers(path: string): ColumnFilterItem[] { + return Array.from( + new Set( + jobs.map((job: any) => { + if (job[path] === null) { + return null; + } + + return job[path].username; + }), + ), + ).map((value: string | null) => ({ text: value || 'Is Empty', value: value || false })); + } + const columns = [ { title: 'Job', @@ -152,6 +193,13 @@ function JobListComponent(props: Props & RouteComponentProps): JSX.Element { ); }, + sorter: sorter('status.status'), + filters: [ + { text: 'annotation', value: 'annotation' }, + { text: 'validation', value: 'validation' }, + { text: 'completed', value: 'completed' }, + ], + onFilter: (value: string | number | boolean, record: any) => record.status.status === value, }, { title: 'Started on', @@ -180,6 +228,10 @@ function JobListComponent(props: Props & RouteComponentProps): JSX.Element { }} /> ), + sorter: sorter('assignee.assignee.username'), + filters: collectUsers('assignee'), + onFilter: (value: string | number | boolean, record: any) => + (record.assignee.assignee?.username || false) === value, }, { title: 'Reviewer', @@ -196,6 +248,10 @@ function JobListComponent(props: Props & RouteComponentProps): JSX.Element { }} /> ), + sorter: sorter('reviewer.reviewer.username'), + filters: collectUsers('reviewer'), + onFilter: (value: string | number | boolean, record: any) => + (record.reviewer.reviewer?.username || false) === value, }, ]; @@ -207,13 +263,14 @@ function JobListComponent(props: Props & RouteComponentProps): JSX.Element { const created = moment(props.taskInstance.createdDate); + const now = moment(moment.now()); acc.push({ key: job.id, job: job.id, frames: `${job.startFrame}-${job.stopFrame}`, status: job, started: `${created.format('MMMM Do YYYY HH:MM')}`, - duration: `${moment.duration(moment(moment.now()).diff(created)).humanize()}`, + duration: `${moment.duration(now.diff(created)).humanize()}`, assignee: job, reviewer: job, }); diff --git a/cvat-ui/src/components/tasks-page/top-bar.tsx b/cvat-ui/src/components/tasks-page/top-bar.tsx index b3c64d3209e..1e4ac48674d 100644 --- a/cvat-ui/src/components/tasks-page/top-bar.tsx +++ b/cvat-ui/src/components/tasks-page/top-bar.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -10,6 +10,8 @@ import Button from 'antd/lib/button'; import Input from 'antd/lib/input'; import Text from 'antd/lib/typography/Text'; +import SearchTooltip from 'components/search-tooltip/search-tooltip'; + interface VisibleTopBarProps { onSearch: (value: string) => void; searchValue: string; @@ -25,13 +27,15 @@ export default function TopBarComponent(props: VisibleTopBarProps): JSX.Element Tasks - + + +