Enhanced filtration/sorting for tasks/projects/tasks in projects/cloud storages (#4409)
parent
53f6699d40
commit
1225fbb1bc
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 9.1 KiB |
@ -0,0 +1,83 @@
|
||||
// Copyright (C) 2022 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Config } from 'react-awesome-query-builder';
|
||||
|
||||
export const config: Partial<Config> = {
|
||||
fields: {
|
||||
id: {
|
||||
label: 'ID',
|
||||
type: 'number',
|
||||
operators: ['equal', 'between', 'greater', 'greater_or_equal', 'less', 'less_or_equal'],
|
||||
fieldSettings: { min: 0 },
|
||||
valueSources: ['value'],
|
||||
},
|
||||
provider_type: {
|
||||
label: 'Provider type',
|
||||
type: 'select',
|
||||
operators: ['select_equals'],
|
||||
valueSources: ['value'],
|
||||
fieldSettings: {
|
||||
listValues: [
|
||||
{ value: 'AWS_S3_BUCKET', title: 'AWS S3' },
|
||||
{ value: 'AZURE_CONTAINER', title: 'Azure' },
|
||||
{ value: 'GOOGLE_CLOUD_STORAGE', title: 'Google cloud' },
|
||||
],
|
||||
},
|
||||
},
|
||||
credentials_type: {
|
||||
label: 'Credentials type',
|
||||
type: 'select',
|
||||
operators: ['select_equals'],
|
||||
valueSources: ['value'],
|
||||
fieldSettings: {
|
||||
listValues: [
|
||||
{ value: 'KEY_SECRET_KEY_PAIR', title: 'Key & secret key' },
|
||||
{ value: 'ACCOUNT_NAME_TOKEN_PAIR', title: 'Account name & token' },
|
||||
{ value: 'ANONYMOUS_ACCESS', title: 'Anonymous access' },
|
||||
{ value: 'KEY_FILE_PATH', title: 'Key file' },
|
||||
],
|
||||
},
|
||||
},
|
||||
resource: {
|
||||
label: 'Resource name',
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
operators: ['like'],
|
||||
},
|
||||
display_name: {
|
||||
label: 'Display name',
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
operators: ['like'],
|
||||
},
|
||||
description: {
|
||||
label: 'Description',
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
operators: ['like'],
|
||||
},
|
||||
owner: {
|
||||
label: 'Owner',
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
operators: ['equal'],
|
||||
},
|
||||
updated_date: {
|
||||
label: 'Last updated',
|
||||
type: 'datetime',
|
||||
operators: ['between', 'greater', 'greater_or_equal', 'less', 'less_or_equal'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const localStorageRecentCapacity = 10;
|
||||
export const localStorageRecentKeyword = 'recentlyAppliedCloudStoragesFilters';
|
||||
|
||||
export const predefinedFilterValues = {
|
||||
'Owned by me': '{"and":[{"==":[{"var":"owner"},"<username>"]}]}',
|
||||
'AWS storages': '{"and":[{"==":[{"var":"provider_type"},"AWS_S3_BUCKET"]}]}',
|
||||
'Azure storages': '{"and":[{"==":[{"var":"provider_type"},"AZURE_CONTAINER"]}]}',
|
||||
'Google cloud storages': '{"and":[{"==":[{"var":"provider_type"},"GOOGLE_CLOUD_STORAGE"]}]}',
|
||||
};
|
||||
@ -1,39 +1,38 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
// Copyright (C) 2020-2022 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
import Icon from '@ant-design/icons';
|
||||
import Empty from 'antd/lib/empty';
|
||||
|
||||
import consts from 'consts';
|
||||
import { EmptyTasksIcon as EmptyModelsIcon } from 'icons';
|
||||
|
||||
export default function EmptyListComponent(): JSX.Element {
|
||||
return (
|
||||
<div className='cvat-empty-models-list'>
|
||||
<Row justify='center' align='middle'>
|
||||
<Col>
|
||||
<Icon className='cvat-empty-models-icon' component={EmptyModelsIcon} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row justify='center' align='middle'>
|
||||
<Col>
|
||||
<Text strong>No models deployed yet...</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row justify='center' align='middle'>
|
||||
<Col>
|
||||
<Text type='secondary'>To annotate your tasks automatically</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row justify='center' align='middle'>
|
||||
<Col>
|
||||
<Text type='secondary'>deploy a model with </Text>
|
||||
<a href={`${consts.NUCLIO_GUIDE}`}>nuclio</a>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
<Empty
|
||||
className='cvat-empty-models-list'
|
||||
description={(
|
||||
<div>
|
||||
<Row justify='center' align='middle'>
|
||||
<Col>
|
||||
<Text strong>No models deployed yet...</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row justify='center' align='middle'>
|
||||
<Col>
|
||||
<Text type='secondary'>To annotate your tasks automatically</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row justify='center' align='middle'>
|
||||
<Col>
|
||||
<Text type='secondary'>deploy a model with </Text>
|
||||
<a href={`${consts.NUCLIO_GUIDE}`}>nuclio</a>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
// Copyright (C) 2020 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import React from 'react';
|
||||
import { Row, Col } from 'antd/lib/grid';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
|
||||
export default function TopBarComponent(): JSX.Element {
|
||||
return (
|
||||
<Row justify='center' align='middle'>
|
||||
<Col md={22} lg={20} xl={16} xxl={14}>
|
||||
<Text className='cvat-title'>Models</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,90 @@
|
||||
// Copyright (C) 2022 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Config } from 'react-awesome-query-builder';
|
||||
|
||||
export const config: Partial<Config> = {
|
||||
fields: {
|
||||
dimension: {
|
||||
label: 'Dimension',
|
||||
type: 'select',
|
||||
operators: ['select_equals'],
|
||||
valueSources: ['value'],
|
||||
fieldSettings: {
|
||||
listValues: [
|
||||
{ value: '2d', title: '2D' },
|
||||
{ value: '3d', title: '3D' },
|
||||
],
|
||||
},
|
||||
},
|
||||
status: {
|
||||
label: 'Status',
|
||||
type: 'select',
|
||||
valueSources: ['value'],
|
||||
operators: ['select_equals', 'select_any_in', 'select_not_any_in'],
|
||||
fieldSettings: {
|
||||
listValues: [
|
||||
{ value: 'annotation', title: 'Annotation' },
|
||||
{ value: 'validation', title: 'Validation' },
|
||||
{ value: 'completed', title: 'Completed' },
|
||||
],
|
||||
},
|
||||
},
|
||||
mode: {
|
||||
label: 'Data',
|
||||
type: 'select',
|
||||
valueSources: ['value'],
|
||||
fieldSettings: {
|
||||
listValues: [
|
||||
{ value: 'interpolation', title: 'Video' },
|
||||
{ value: 'annotation', title: 'Images' },
|
||||
],
|
||||
},
|
||||
},
|
||||
subset: {
|
||||
label: 'Subset',
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
operators: ['equal'],
|
||||
},
|
||||
assignee: {
|
||||
label: 'Assignee',
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
operators: ['equal'],
|
||||
},
|
||||
owner: {
|
||||
label: 'Owner',
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
operators: ['equal'],
|
||||
},
|
||||
updated_date: {
|
||||
label: 'Last updated',
|
||||
type: 'datetime',
|
||||
operators: ['between', 'greater', 'greater_or_equal', 'less', 'less_or_equal'],
|
||||
},
|
||||
id: {
|
||||
label: 'ID',
|
||||
type: 'number',
|
||||
operators: ['equal', 'between', 'greater', 'greater_or_equal', 'less', 'less_or_equal'],
|
||||
fieldSettings: { min: 0 },
|
||||
valueSources: ['value'],
|
||||
},
|
||||
name: {
|
||||
label: 'Name',
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
operators: ['like'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const localStorageRecentCapacity = 10;
|
||||
export const localStorageRecentKeyword = 'recentlyAppliedProjectTasksFilters';
|
||||
export const predefinedFilterValues = {
|
||||
'Assigned to me': '{"and":[{"==":[{"var":"assignee"},"<username>"]}]}',
|
||||
'Owned by me': '{"and":[{"==":[{"var":"owner"},"<username>"]}]}',
|
||||
'Not completed': '{"!":{"and":[{"==":[{"var":"status"},"completed"]}]}}',
|
||||
};
|
||||
@ -0,0 +1,61 @@
|
||||
// Copyright (C) 2022 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Config } from 'react-awesome-query-builder';
|
||||
|
||||
export const config: Partial<Config> = {
|
||||
fields: {
|
||||
id: {
|
||||
label: 'ID',
|
||||
type: 'number',
|
||||
operators: ['equal', 'between', 'greater', 'greater_or_equal', 'less', 'less_or_equal'],
|
||||
fieldSettings: { min: 0 },
|
||||
valueSources: ['value'],
|
||||
},
|
||||
name: {
|
||||
label: 'Name',
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
operators: ['like'],
|
||||
},
|
||||
assignee: {
|
||||
label: 'Assignee',
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
operators: ['equal'],
|
||||
},
|
||||
owner: {
|
||||
label: 'Owner',
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
operators: ['equal'],
|
||||
},
|
||||
updated_date: {
|
||||
label: 'Last updated',
|
||||
type: 'datetime',
|
||||
operators: ['between', 'greater', 'greater_or_equal', 'less', 'less_or_equal'],
|
||||
},
|
||||
status: {
|
||||
label: 'Status',
|
||||
type: 'select',
|
||||
valueSources: ['value'],
|
||||
operators: ['select_equals', 'select_any_in', 'select_not_any_in'],
|
||||
fieldSettings: {
|
||||
listValues: [
|
||||
{ value: 'annotation', title: 'Annotation' },
|
||||
{ value: 'validation', title: 'Validation' },
|
||||
{ value: 'completed', title: 'Completed' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const localStorageRecentCapacity = 10;
|
||||
export const localStorageRecentKeyword = 'recentlyAppliedProjectsFilters';
|
||||
export const predefinedFilterValues = {
|
||||
'Assigned to me': '{"and":[{"==":[{"var":"assignee"},"<username>"]}]}',
|
||||
'Owned by me': '{"and":[{"==":[{"var":"owner"},"<username>"]}]}',
|
||||
'Not completed': '{"!":{"and":[{"==":[{"var":"status"},"completed"]}]}}',
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
// Copyright (C) 2022 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import './styles.scss';
|
||||
import { Indexable } from 'reducers/interfaces';
|
||||
import SortingComponent from './sorting';
|
||||
import ResourceFilterHOC from './filtering';
|
||||
|
||||
const defaultVisibility = {
|
||||
predefined: false,
|
||||
recent: false,
|
||||
builder: false,
|
||||
sorting: false,
|
||||
};
|
||||
|
||||
function updateHistoryFromQuery(query: Indexable): string {
|
||||
const search = new URLSearchParams({
|
||||
...(query.filter ? { filter: query.filter } : {}),
|
||||
...(query.search ? { search: query.search } : {}),
|
||||
...(query.sort ? { sort: query.sort } : {}),
|
||||
...(query.page ? { page: `${query.page}` } : {}),
|
||||
});
|
||||
|
||||
return decodeURIComponent(search.toString());
|
||||
}
|
||||
|
||||
export {
|
||||
SortingComponent,
|
||||
ResourceFilterHOC,
|
||||
defaultVisibility,
|
||||
updateHistoryFromQuery,
|
||||
};
|
||||
@ -0,0 +1,139 @@
|
||||
// Copyright (C) 2022 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@import '../../base.scss';
|
||||
|
||||
.cvat-resource-page-filters {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span[aria-label=down] {
|
||||
margin-right: $grid-unit-size;
|
||||
}
|
||||
|
||||
> button {
|
||||
margin-right: $grid-unit-size;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-resource-page-recent-filters-list {
|
||||
max-width: $grid-unit-size * 64;
|
||||
|
||||
.ant-menu {
|
||||
border: none;
|
||||
|
||||
.ant-menu-item {
|
||||
padding: $grid-unit-size;
|
||||
margin: 0;
|
||||
line-height: initial;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-resource-page-filters-builder {
|
||||
background: white;
|
||||
padding: $grid-unit-size;
|
||||
border-radius: 4px;
|
||||
box-shadow: $box-shadow-base;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
|
||||
// redefine default awesome react query builder styles below
|
||||
.query-builder {
|
||||
margin: $grid-unit-size;
|
||||
|
||||
.group.group-or-rule {
|
||||
background: none !important;
|
||||
border: none !important;
|
||||
|
||||
.group.group-or-rule {
|
||||
&::before {
|
||||
left: -13px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: -13px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.group--actions.group--actions--tr {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
.group--conjunctions {
|
||||
div.ant-btn-group {
|
||||
button.ant-btn {
|
||||
width: auto !important;
|
||||
opacity: 1 !important;
|
||||
margin-right: $grid-unit-size !important;
|
||||
padding: 0 $grid-unit-size !important;
|
||||
border-left-color: #d9d9d9 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-resource-page-sorting-list,
|
||||
.cvat-resource-page-predefined-filters-list,
|
||||
.cvat-resource-page-recent-filters-list {
|
||||
background: white;
|
||||
padding: $grid-unit-size;
|
||||
border-radius: 4px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-shadow: $box-shadow-base;
|
||||
|
||||
.ant-checkbox-wrapper {
|
||||
margin-bottom: $grid-unit-size;
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-resource-page-sorting-list {
|
||||
width: $grid-unit-size * 24;
|
||||
}
|
||||
|
||||
.cvat-sorting-field {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: $grid-unit-size;
|
||||
|
||||
.ant-radio-button-wrapper {
|
||||
width: $grid-unit-size * 16;
|
||||
user-select: none;
|
||||
cursor: move;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-sorting-anchor {
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
|
||||
&:first-child {
|
||||
margin-top: $grid-unit-size * 4;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: $grid-unit-size * 4;
|
||||
}
|
||||
}
|
||||
|
||||
.cvat-sorting-dragged-item {
|
||||
z-index: 10000;
|
||||
}
|
||||
|
||||
.cvat-resource-page-filters-space {
|
||||
justify-content: right;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
@ -1,103 +0,0 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import './styles.scss';
|
||||
import React from 'react';
|
||||
import Search from 'antd/lib/input/Search';
|
||||
import SearchTooltip from 'components/search-tooltip/search-tooltip';
|
||||
|
||||
interface Query {
|
||||
[key: string]: string | number | boolean | null | undefined;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
query: Query;
|
||||
instance: 'task' | 'project' | 'cloudstorage';
|
||||
skipFields?: string[];
|
||||
onSearch(query: object): void;
|
||||
}
|
||||
|
||||
export default function SearchField(props: Props): JSX.Element {
|
||||
const {
|
||||
onSearch,
|
||||
query,
|
||||
instance,
|
||||
skipFields,
|
||||
} = props;
|
||||
const skip = ['page'];
|
||||
if (typeof skipFields !== 'undefined') {
|
||||
skip.push(...skipFields);
|
||||
}
|
||||
|
||||
function parse(_query: Query, _skip: string[]): string {
|
||||
let searchString = '';
|
||||
for (const field of Object.keys(_query)) {
|
||||
const value = _query[field];
|
||||
if (value !== null && typeof value !== 'undefined' && !_skip.includes(field)) {
|
||||
if (field === 'search') {
|
||||
return _query[field] as string;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line
|
||||
if (typeof (_query[field] === 'number')) {
|
||||
searchString += `${field}: ${_query[field]} AND `;
|
||||
} else {
|
||||
searchString += `${field}: "${_query[field]}" AND `;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return searchString.slice(0, -5);
|
||||
}
|
||||
|
||||
const handleSearch = (value: string): void => {
|
||||
const currentQuery = { ...query };
|
||||
const search = value
|
||||
.replace(/\s+/g, ' ')
|
||||
.replace(/\s*:+\s*/g, ':')
|
||||
.trim();
|
||||
|
||||
const fields = Object.keys(query).filter((key) => !skip.includes(key));
|
||||
for (const field of fields) {
|
||||
currentQuery[field] = null;
|
||||
}
|
||||
currentQuery.search = null;
|
||||
|
||||
let specificRequest = false;
|
||||
for (const param of search.split(/[\s]+and[\s]+|[\s]+AND[\s]+/)) {
|
||||
if (param.includes(':')) {
|
||||
const [field, fieldValue] = param.split(':');
|
||||
if (fields.includes(field) && !!fieldValue) {
|
||||
specificRequest = true;
|
||||
if (field === 'id') {
|
||||
if (Number.isInteger(+fieldValue)) {
|
||||
currentQuery[field] = +fieldValue;
|
||||
}
|
||||
} else {
|
||||
currentQuery[field] = fieldValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
currentQuery.page = 1;
|
||||
if (!specificRequest && value) {
|
||||
currentQuery.search = value;
|
||||
}
|
||||
|
||||
onSearch(currentQuery);
|
||||
};
|
||||
|
||||
return (
|
||||
<SearchTooltip instance={instance}>
|
||||
<Search
|
||||
className='cvat-search-field'
|
||||
defaultValue={parse(query, skip)}
|
||||
onSearch={handleSearch}
|
||||
size='large'
|
||||
placeholder='Search'
|
||||
/>
|
||||
</SearchTooltip>
|
||||
);
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@import '../../base.scss';
|
||||
|
||||
.cvat-search-field {
|
||||
width: $grid-unit-size * 30;
|
||||
}
|
||||
@ -1,162 +0,0 @@
|
||||
// Copyright (C) 2021-2022 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' | 'cloudstorage';
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
// provider: isEnum.bind(CloudStorageProviderType),
|
||||
// credentialsType: isEnum.bind(CloudStorageCredentialsType),
|
||||
|
||||
export default function SearchTooltip(props: Props): JSX.Element {
|
||||
const { instance, children } = props;
|
||||
const instances = ` ${instance}s `;
|
||||
|
||||
return (
|
||||
<CVATTooltip
|
||||
overlayClassName={`cvat-${instance}s-search-tooltip cvat-search-tooltip`}
|
||||
title={(
|
||||
<>
|
||||
{instance === 'cloudstorage' ? (
|
||||
<Paragraph>
|
||||
<Text strong>displayName: Azure</Text>
|
||||
<Text>
|
||||
all
|
||||
{instances}
|
||||
where name includes the substring
|
||||
<q>Azure</q>
|
||||
</Text>
|
||||
</Paragraph>
|
||||
) : null}
|
||||
{instance === 'cloudstorage' ? (
|
||||
<Paragraph>
|
||||
<Text strong>description: Personal bucket</Text>
|
||||
<Text>
|
||||
all
|
||||
{instances}
|
||||
where description includes the substring
|
||||
<q>Personal bucket</q>
|
||||
</Text>
|
||||
</Paragraph>
|
||||
) : null}
|
||||
{instance === 'cloudstorage' ? (
|
||||
<Paragraph>
|
||||
<Text strong>resource: mycvatbucket</Text>
|
||||
<Text>
|
||||
all
|
||||
{instances}
|
||||
where a name of the resource includes the substring
|
||||
<q>mycvatbucket</q>
|
||||
</Text>
|
||||
</Paragraph>
|
||||
) : null}
|
||||
{instance === 'cloudstorage' ? (
|
||||
<Paragraph>
|
||||
<Text strong>providerType: AWS_S3_BUCKET</Text>
|
||||
<Text>
|
||||
<q>AWS_S3_BUCKET</q>
|
||||
or
|
||||
<q>AZURE_CONTAINER</q>
|
||||
or
|
||||
<q>GOOGLE_CLOUD_STORAGE</q>
|
||||
</Text>
|
||||
</Paragraph>
|
||||
) : null}
|
||||
{instance === 'cloudstorage' ? (
|
||||
<Paragraph>
|
||||
<Text strong>credentialsType: KEY_SECRET_KEY_PAIR</Text>
|
||||
<Text>
|
||||
<q>KEY_SECRET_KEY_PAIR</q>
|
||||
or
|
||||
<q>ACCOUNT_NAME_TOKEN_PAIR</q>
|
||||
or
|
||||
<q>KEY_FILE_PATH</q>
|
||||
or
|
||||
<q>ANONYMOUS_ACCESS</q>
|
||||
</Text>
|
||||
</Paragraph>
|
||||
) : null}
|
||||
<Paragraph>
|
||||
<Text strong>owner: admin</Text>
|
||||
<Text>
|
||||
all
|
||||
{instances}
|
||||
created by users who have the substring
|
||||
<q>admin</q>
|
||||
in their username
|
||||
</Text>
|
||||
</Paragraph>
|
||||
{instance !== 'cloudstorage' ? (
|
||||
<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>
|
||||
) : null}
|
||||
{instance !== 'cloudstorage' ? (
|
||||
<Paragraph>
|
||||
<Text strong>name: training</Text>
|
||||
<Text>
|
||||
all
|
||||
{instances}
|
||||
with the substring
|
||||
<q>training</q>
|
||||
in its name
|
||||
</Text>
|
||||
</Paragraph>
|
||||
) : null}
|
||||
{instance === 'task' ? (
|
||||
<Paragraph>
|
||||
<Text strong>mode: annotation</Text>
|
||||
<Text>
|
||||
annotation tasks are tasks with images, interpolation tasks are tasks with videos
|
||||
</Text>
|
||||
</Paragraph>
|
||||
) : null}
|
||||
{instance !== 'cloudstorage' ? (
|
||||
<Paragraph>
|
||||
<Text strong>status: annotation</Text>
|
||||
<Text>annotation, validation, or completed</Text>
|
||||
</Paragraph>
|
||||
) : null}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
// Copyright (C) 2021 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
@import '../../base.scss';
|
||||
|
||||
.cvat-search-tooltip {
|
||||
span {
|
||||
color: white;
|
||||
}
|
||||
|
||||
q {
|
||||
margin: 0 1em 0 1em;
|
||||
}
|
||||
|
||||
strong::after {
|
||||
content: ' - ';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,103 @@
|
||||
// Copyright (C) 2022 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { Config } from 'react-awesome-query-builder';
|
||||
|
||||
export const config: Partial<Config> = {
|
||||
fields: {
|
||||
dimension: {
|
||||
label: 'Dimension',
|
||||
type: 'select',
|
||||
operators: ['select_equals'],
|
||||
valueSources: ['value'],
|
||||
fieldSettings: {
|
||||
listValues: [
|
||||
{ value: '2d', title: '2D' },
|
||||
{ value: '3d', title: '3D' },
|
||||
],
|
||||
},
|
||||
},
|
||||
status: {
|
||||
label: 'Status',
|
||||
type: 'select',
|
||||
valueSources: ['value'],
|
||||
operators: ['select_equals', 'select_any_in', 'select_not_any_in'],
|
||||
fieldSettings: {
|
||||
listValues: [
|
||||
{ value: 'annotation', title: 'Annotation' },
|
||||
{ value: 'validation', title: 'Validation' },
|
||||
{ value: 'completed', title: 'Completed' },
|
||||
],
|
||||
},
|
||||
},
|
||||
mode: {
|
||||
label: 'Data',
|
||||
type: 'select',
|
||||
valueSources: ['value'],
|
||||
fieldSettings: {
|
||||
listValues: [
|
||||
{ value: 'interpolation', title: 'Video' },
|
||||
{ value: 'annotation', title: 'Images' },
|
||||
],
|
||||
},
|
||||
},
|
||||
subset: {
|
||||
label: 'Subset',
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
operators: ['equal'],
|
||||
},
|
||||
assignee: {
|
||||
label: 'Assignee',
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
operators: ['equal'],
|
||||
},
|
||||
owner: {
|
||||
label: 'Owner',
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
operators: ['equal'],
|
||||
},
|
||||
updated_date: {
|
||||
label: 'Last updated',
|
||||
type: 'datetime',
|
||||
operators: ['between', 'greater', 'greater_or_equal', 'less', 'less_or_equal'],
|
||||
},
|
||||
id: {
|
||||
label: 'ID',
|
||||
type: 'number',
|
||||
operators: ['equal', 'between', 'greater', 'greater_or_equal', 'less', 'less_or_equal'],
|
||||
fieldSettings: { min: 0 },
|
||||
valueSources: ['value'],
|
||||
},
|
||||
project_id: {
|
||||
label: 'Project ID',
|
||||
type: 'number',
|
||||
operators: ['equal', 'between', 'greater', 'greater_or_equal', 'less', 'less_or_equal'],
|
||||
fieldSettings: { min: 0 },
|
||||
valueSources: ['value'],
|
||||
},
|
||||
name: {
|
||||
label: 'Name',
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
operators: ['like'],
|
||||
},
|
||||
project_name: {
|
||||
label: 'Project name',
|
||||
type: 'text',
|
||||
valueSources: ['value'],
|
||||
operators: ['like'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const localStorageRecentCapacity = 10;
|
||||
export const localStorageRecentKeyword = 'recentlyAppliedTasksFilters';
|
||||
export const predefinedFilterValues = {
|
||||
'Assigned to me': '{"and":[{"==":[{"var":"assignee"},"<username>"]}]}',
|
||||
'Owned by me': '{"and":[{"==":[{"var":"owner"},"<username>"]}]}',
|
||||
'Not completed': '{"!":{"and":[{"==":[{"var":"status"},"completed"]}]}}',
|
||||
};
|
||||
@ -1,167 +1,155 @@
|
||||
// Copyright (C) 2020-2021 Intel Corporation
|
||||
// Copyright (C) 2020-2022 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import './styles.scss';
|
||||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useHistory } from 'react-router';
|
||||
import Spin from 'antd/lib/spin';
|
||||
import Button from 'antd/lib/button';
|
||||
import message from 'antd/lib/message';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import { Col, Row } from 'antd/lib/grid';
|
||||
import Pagination from 'antd/lib/pagination';
|
||||
|
||||
import { TasksQuery } from 'reducers/interfaces';
|
||||
import { TasksQuery, Indexable } from 'reducers/interfaces';
|
||||
import FeedbackComponent from 'components/feedback/feedback';
|
||||
import { updateHistoryFromQuery } from 'components/resource-sorting-filtering';
|
||||
import TaskListContainer from 'containers/tasks-page/tasks-list';
|
||||
import { getTasksAsync, hideEmptyTasks, importTaskAsync } from 'actions/tasks-actions';
|
||||
|
||||
import TopBar from './top-bar';
|
||||
import EmptyListComponent from './empty-list';
|
||||
|
||||
interface TasksPageProps {
|
||||
tasksFetching: boolean;
|
||||
gettingQuery: TasksQuery;
|
||||
numberOfTasks: number;
|
||||
numberOfVisibleTasks: number;
|
||||
numberOfHiddenTasks: number;
|
||||
onGetTasks: (gettingQuery: TasksQuery) => void;
|
||||
hideEmptyTasks: (hideEmpty: boolean) => void;
|
||||
onImportTask: (file: File) => void;
|
||||
taskImporting: boolean;
|
||||
interface Props {
|
||||
fetching: boolean;
|
||||
importing: boolean;
|
||||
query: TasksQuery;
|
||||
count: number;
|
||||
countInvisible: number;
|
||||
}
|
||||
|
||||
function updateQuery(previousQuery: TasksQuery, searchString: string): TasksQuery {
|
||||
const params = new URLSearchParams(searchString);
|
||||
const query = { ...previousQuery };
|
||||
for (const field of Object.keys(query)) {
|
||||
if (params.has(field)) {
|
||||
const value = params.get(field);
|
||||
if (value) {
|
||||
if (field === 'id' || field === 'page') {
|
||||
if (Number.isInteger(+value)) {
|
||||
query[field] = +value;
|
||||
}
|
||||
} else {
|
||||
query[field] = value;
|
||||
}
|
||||
}
|
||||
} else if (field === 'page') {
|
||||
query[field] = 1;
|
||||
} else {
|
||||
query[field] = null;
|
||||
function TasksPageComponent(props: Props): JSX.Element {
|
||||
const {
|
||||
query, fetching, importing, count, countInvisible,
|
||||
} = props;
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const history = useHistory();
|
||||
const [isMounted, setIsMounted] = useState(false);
|
||||
|
||||
const queryParams = new URLSearchParams(history.location.search);
|
||||
const updatedQuery = { ...query };
|
||||
for (const key of Object.keys(updatedQuery)) {
|
||||
(updatedQuery as Indexable)[key] = queryParams.get(key) || null;
|
||||
if (key === 'page') {
|
||||
updatedQuery.page = updatedQuery.page ? +updatedQuery.page : 1;
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
class TasksPageComponent extends React.PureComponent<TasksPageProps & RouteComponentProps> {
|
||||
public componentDidMount(): void {
|
||||
const { gettingQuery, location, onGetTasks } = this.props;
|
||||
const query = updateQuery(gettingQuery, location.search);
|
||||
onGetTasks(query);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: TasksPageProps & RouteComponentProps): void {
|
||||
const {
|
||||
location,
|
||||
gettingQuery,
|
||||
tasksFetching,
|
||||
numberOfHiddenTasks,
|
||||
onGetTasks,
|
||||
hideEmptyTasks,
|
||||
taskImporting,
|
||||
} = this.props;
|
||||
|
||||
if (
|
||||
prevProps.location.search !== location.search ||
|
||||
(prevProps.taskImporting === true && taskImporting === false)
|
||||
) {
|
||||
// get new tasks if any query changes
|
||||
const query = updateQuery(gettingQuery, location.search);
|
||||
message.destroy();
|
||||
onGetTasks(query);
|
||||
return;
|
||||
}
|
||||
|
||||
if (prevProps.tasksFetching && !tasksFetching) {
|
||||
if (numberOfHiddenTasks) {
|
||||
message.destroy();
|
||||
message.info(
|
||||
<>
|
||||
<Text>Some tasks are temporary hidden since they are without any data</Text>
|
||||
<Button
|
||||
type='link'
|
||||
onClick={(): void => {
|
||||
hideEmptyTasks(false);
|
||||
message.destroy();
|
||||
}}
|
||||
>
|
||||
Show all
|
||||
</Button>
|
||||
</>,
|
||||
5,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
dispatch(getTasksAsync({ ...updatedQuery }));
|
||||
setIsMounted(true);
|
||||
}, []);
|
||||
|
||||
private handlePagination = (page: number): void => {
|
||||
const { gettingQuery } = this.props;
|
||||
|
||||
// modify query object
|
||||
const query = { ...gettingQuery };
|
||||
query.page = page;
|
||||
|
||||
// update url according to new query object
|
||||
this.updateURL(query);
|
||||
};
|
||||
|
||||
private updateURL = (gettingQuery: TasksQuery): void => {
|
||||
const { history } = this.props;
|
||||
let queryString = '?';
|
||||
for (const field of Object.keys(gettingQuery)) {
|
||||
if (gettingQuery[field] !== null) {
|
||||
queryString += `${field}=${gettingQuery[field]}&`;
|
||||
}
|
||||
}
|
||||
|
||||
const oldQueryString = history.location.search;
|
||||
if (oldQueryString !== queryString) {
|
||||
history.push({
|
||||
search: queryString.slice(0, -1),
|
||||
useEffect(() => {
|
||||
if (isMounted) {
|
||||
history.replace({
|
||||
search: updateHistoryFromQuery(query),
|
||||
});
|
||||
|
||||
// force update if any changes
|
||||
this.forceUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
public render(): JSX.Element {
|
||||
const {
|
||||
tasksFetching, gettingQuery, numberOfVisibleTasks, onImportTask, taskImporting,
|
||||
} = this.props;
|
||||
}, [query]);
|
||||
|
||||
if (tasksFetching) {
|
||||
return <Spin size='large' className='cvat-spinner' />;
|
||||
useEffect(() => {
|
||||
if (countInvisible) {
|
||||
message.destroy();
|
||||
message.info(
|
||||
<>
|
||||
<Text>Some tasks are temporary hidden because they are not fully created yet</Text>
|
||||
<Button
|
||||
type='link'
|
||||
onClick={(): void => {
|
||||
dispatch(hideEmptyTasks(true));
|
||||
message.destroy();
|
||||
}}
|
||||
>
|
||||
Show all
|
||||
</Button>
|
||||
</>,
|
||||
5,
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='cvat-tasks-page'>
|
||||
<TopBar
|
||||
onSearch={this.updateURL}
|
||||
query={gettingQuery}
|
||||
onFileUpload={onImportTask}
|
||||
taskImporting={taskImporting}
|
||||
/>
|
||||
{numberOfVisibleTasks ? (
|
||||
<TaskListContainer onSwitchPage={this.handlePagination} />
|
||||
) : (
|
||||
<EmptyListComponent />
|
||||
)}
|
||||
<FeedbackComponent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}, [countInvisible]);
|
||||
|
||||
const content = count ? (
|
||||
<>
|
||||
<TaskListContainer />
|
||||
<Row justify='center' align='middle'>
|
||||
<Col md={22} lg={18} xl={16} xxl={14}>
|
||||
<Pagination
|
||||
className='cvat-tasks-pagination'
|
||||
onChange={(page: number) => {
|
||||
dispatch(getTasksAsync({
|
||||
...query,
|
||||
page,
|
||||
}));
|
||||
}}
|
||||
showSizeChanger={false}
|
||||
total={count}
|
||||
pageSize={10}
|
||||
current={query.page}
|
||||
showQuickJumper
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
) : (
|
||||
<EmptyListComponent query={query} />
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='cvat-tasks-page'>
|
||||
<TopBar
|
||||
onApplySearch={(search: string | null) => {
|
||||
dispatch(
|
||||
getTasksAsync({
|
||||
...query,
|
||||
search,
|
||||
page: 1,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
onApplyFilter={(filter: string | null) => {
|
||||
dispatch(
|
||||
getTasksAsync({
|
||||
...query,
|
||||
filter,
|
||||
page: 1,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
onApplySorting={(sorting: string | null) => {
|
||||
dispatch(
|
||||
getTasksAsync({
|
||||
...query,
|
||||
sort: sorting,
|
||||
page: 1,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
query={updatedQuery}
|
||||
onImportTask={(file: File) => dispatch(importTaskAsync(file))}
|
||||
importing={importing}
|
||||
/>
|
||||
{ fetching ? (
|
||||
<div className='cvat-empty-tasks-list'>
|
||||
<Spin size='large' className='cvat-spinner' />
|
||||
</div>
|
||||
) : content }
|
||||
<FeedbackComponent />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default withRouter(TasksPageComponent);
|
||||
export default React.memo(TasksPageComponent);
|
||||
|
||||
@ -1,57 +1,31 @@
|
||||
// Copyright (C) 2020-2021 Intel Corporation
|
||||
// Copyright (C) 2020-2022 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Task, TasksQuery, CombinedState } from 'reducers/interfaces';
|
||||
|
||||
import TasksPageComponent from 'components/tasks-page/tasks-page';
|
||||
|
||||
import { getTasksAsync, hideEmptyTasks, importTaskAsync } from 'actions/tasks-actions';
|
||||
|
||||
interface StateToProps {
|
||||
tasksFetching: boolean;
|
||||
gettingQuery: TasksQuery;
|
||||
numberOfTasks: number;
|
||||
numberOfVisibleTasks: number;
|
||||
numberOfHiddenTasks: number;
|
||||
taskImporting: boolean;
|
||||
}
|
||||
|
||||
interface DispatchToProps {
|
||||
onGetTasks: (gettingQuery: TasksQuery) => void;
|
||||
hideEmptyTasks: (hideEmpty: boolean) => void;
|
||||
onImportTask: (file: File) => void;
|
||||
fetching: boolean;
|
||||
query: TasksQuery;
|
||||
count: number;
|
||||
countInvisible: number;
|
||||
importing: boolean;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: CombinedState): StateToProps {
|
||||
const { tasks } = state;
|
||||
|
||||
return {
|
||||
tasksFetching: state.tasks.fetching,
|
||||
gettingQuery: tasks.gettingQuery,
|
||||
numberOfTasks: state.tasks.count,
|
||||
numberOfVisibleTasks: state.tasks.current.length,
|
||||
numberOfHiddenTasks: tasks.hideEmpty ?
|
||||
fetching: state.tasks.fetching,
|
||||
query: tasks.gettingQuery,
|
||||
count: state.tasks.count,
|
||||
countInvisible: tasks.hideEmpty ?
|
||||
tasks.current.filter((task: Task): boolean => !task.instance.jobs.length).length :
|
||||
0,
|
||||
taskImporting: state.tasks.importing,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||
return {
|
||||
onGetTasks: (query: TasksQuery): void => {
|
||||
dispatch(getTasksAsync(query));
|
||||
},
|
||||
hideEmptyTasks: (hideEmpty: boolean): void => {
|
||||
dispatch(hideEmptyTasks(hideEmpty));
|
||||
},
|
||||
onImportTask: (file: File): void => {
|
||||
dispatch(importTaskAsync(file));
|
||||
},
|
||||
importing: state.tasks.importing,
|
||||
};
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(TasksPageComponent);
|
||||
export default connect(mapStateToProps)(TasksPageComponent);
|
||||
|
||||
@ -1,48 +0,0 @@
|
||||
// Copyright (C) 2020-2022 Intel Corporation
|
||||
//
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
/// <reference types="cypress" />
|
||||
|
||||
import { taskName } from '../../support/const';
|
||||
|
||||
context('Search task feature.', () => {
|
||||
const caseId = '35';
|
||||
|
||||
function searchTask(option, result) {
|
||||
cy.intercept('GET', '/api/tasks**').as('searchTask');
|
||||
cy.get('.cvat-search-field').find('[placeholder="Search"]').clear().type(`${option}{Enter}`);
|
||||
cy.wait('@searchTask').its('response.statusCode').should('equal', 200);
|
||||
cy.get('.cvat-spinner').should('not.exist');
|
||||
cy.contains('.cvat-item-task-name', taskName).should(result);
|
||||
}
|
||||
|
||||
before(() => {
|
||||
cy.openTask(taskName);
|
||||
cy.assignTaskToUser(Cypress.env('user')); // Assign a task to an ures to check filter
|
||||
cy.goToTaskList();
|
||||
});
|
||||
|
||||
after(() => {
|
||||
cy.goToTaskList();
|
||||
cy.openTask(taskName);
|
||||
cy.assignTaskToUser('');
|
||||
});
|
||||
|
||||
// TODO: rework this test
|
||||
describe(`Testing case "${caseId}"`, () => {
|
||||
it('Tooltip task filter contain all the possible options.', () => {
|
||||
cy.get('.cvat-search-field').trigger('mouseover');
|
||||
cy.get('.cvat-tasks-search-tooltip').should('be.visible');
|
||||
});
|
||||
|
||||
it('Type to task search some filter and check result.', () => {
|
||||
searchTask(`${taskName.substring(0, 3)}`, 'exist');
|
||||
searchTask('121212', 'not.exist');
|
||||
searchTask(`owner: ${Cypress.env('user')}`, 'exist');
|
||||
searchTask(`mode: annotation AND assignee: ${Cypress.env('user')}`, 'exist');
|
||||
searchTask('status: annotation', 'exist');
|
||||
searchTask(`mode: interpolation AND owner: ${Cypress.env('user')}`, 'not.exist');
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue