未验证 提交 8f426abd 编写于 作者: B Boris Sekachev 提交者: GitHub

Added filters and sorting options for job list, added tooltip for tasks filters (#3030)

* Added filters & sorters for job list

* Added tooltip

* Updated version & changelog

* Added tooltip for project search

* Fixed eslint
上级 390ff3eb
......@@ -33,7 +33,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 (<https://github.com/openvinotoolkit/cvat/pull/3017>)
- [ICDAR](https://rrc.cvc.uab.es/?ch=2) format support (<https://github.com/openvinotoolkit/cvat/pull/2866>)
- Added switcher to maintain poylgon crop behaviour (<https://github.com/openvinotoolkit/cvat/pull/3021>)
- Added switcher to maintain poylgon crop behaviour (<https://github.com/openvinotoolkit/cvat/pull/3021>
- Filters and sorting options for job list, added tooltip for tasks filters (<https://github.com/openvinotoolkit/cvat/pull/3030>)
### Changed
......
{
"name": "cvat-ui",
"version": "1.17.0",
"version": "1.18.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
{
"name": "cvat-ui",
"version": "1.17.0",
"version": "1.18.0",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
......
// 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 (
<Search
defaultValue={getSearchField(gettingQuery)}
onSearch={handleSearch}
size='large'
placeholder='Search'
/>
<SearchTooltip instance='project'>
<Search
defaultValue={getSearchField(gettingQuery)}
onSearch={handleSearch}
size='large'
placeholder='Search'
/>
</SearchTooltip>
);
}
// 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 (
<CVATTooltip
overlayClassName={`cvat-${instance}s-search-tooltip`}
title={(
<>
<Paragraph>
<Text strong>owner: admin</Text>
<Text>
all
{instances}
created by the user who has the substring
<q>admin</q>
in their username
</Text>
</Paragraph>
<Paragraph>
<Text strong>assignee: employee</Text>
<Text>
all
{instances}
which are assigned to a user who has the substring
<q>admin</q>
in their username
</Text>
</Paragraph>
<Paragraph>
<Text strong>name: training</Text>
<Text>
all
{instances}
with the substring
<q>training</q>
in its name
</Text>
</Paragraph>
{instance === 'task' ? (
<Paragraph>
<Text strong>mode: annotation</Text>
<Text>
annotation tasks are tasks with images, interpolation tasks are tasks with videos
</Text>
</Paragraph>
) : null}
<Paragraph>
<Text strong>status: annotation</Text>
<Text>annotation, validation, or completed</Text>
</Paragraph>
<Paragraph>
<Text strong>id: 5</Text>
<Text>
the
{` ${instance} `}
with id 5
</Text>
</Paragraph>
<Paragraph>
<Text>
Filters can be combined (to the exclusion of id) using the keyword AND. Example:
<Text type='warning'>
<q>status: annotation AND owner: admin</q>
</Text>
</Text>
</Paragraph>
<Paragraph>
<Text type='success'>Search within all the string fields by default</Text>
</Paragraph>
</>
)}
>
{children}
</CVATTooltip>
);
}
// 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: ' - ';
}
}
......@@ -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<string | null>(
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 {
</Text>
);
},
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,
});
......
// 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
<Row justify='center' align='middle'>
<Col md={11} lg={9} xl={8} xxl={7}>
<Text className='cvat-title'>Tasks</Text>
<Input.Search
className='cvat-task-page-search-task'
defaultValue={searchValue}
onSearch={onSearch}
size='large'
placeholder='Search'
/>
<SearchTooltip instance='task'>
<Input.Search
className='cvat-task-page-search-task'
defaultValue={searchValue}
onSearch={onSearch}
size='large'
placeholder='Search'
/>
</SearchTooltip>
</Col>
<Col md={{ span: 11 }} lg={{ span: 9 }} xl={{ span: 8 }} xxl={{ span: 7 }}>
<Button
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册