Enhanced filtration/sorting for tasks/projects/tasks in projects/cloud storages (#4409)

main
Boris Sekachev 4 years ago committed by GitHub
parent 53f6699d40
commit 1225fbb1bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,7 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## \[2.1.0] - Unreleased
### Added
- TDB
- Advanced filtration and sorting for a list of tasks/projects/cloudstorages (<https://github.com/openvinotoolkit/cvat/pull/4403>)
### Changed
- TDB

@ -1,12 +1,12 @@
{
"name": "cvat-core",
"version": "4.2.1",
"version": "5.0.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-core",
"version": "4.2.1",
"version": "5.0.0",
"license": "MIT",
"dependencies": {
"axios": "^0.21.4",

@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "4.2.1",
"version": "5.0.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {

@ -14,7 +14,6 @@ const config = require('./config');
isString,
checkFilter,
checkExclusiveFields,
camelToSnake,
checkObjectType,
} = require('./common');
@ -171,7 +170,14 @@ const config = require('./config');
}
}
const jobsData = await serverProxy.jobs.get(filter);
const searchParams = {};
for (const key of Object.keys(filter)) {
if (['page', 'sort', 'search', 'filter'].includes(key)) {
searchParams[key] = filter[key];
}
}
const jobsData = await serverProxy.jobs.get(searchParams);
const jobs = jobsData.results.map((jobData) => new Job(jobData));
jobs.count = jobsData.count;
return jobs;
@ -182,32 +188,33 @@ const config = require('./config');
page: isInteger,
projectId: isInteger,
id: isInteger,
sort: isString,
search: isString,
filter: isString,
ordering: isString,
});
checkExclusiveFields(filter, ['id', 'projectId'], ['page']);
const searchParams = {};
for (const field of [
'filter',
'search',
'ordering',
'id',
'page',
'projectId',
]) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams[camelToSnake(field)] = filter[field];
for (const key of Object.keys(filter)) {
if (['page', 'id', 'sort', 'search', 'filter', 'ordering'].includes(key)) {
searchParams[key] = filter[key];
}
}
const tasksData = await serverProxy.tasks.get(searchParams);
const tasks = tasksData.map((task) => new Task(task));
let tasksData = null;
if (filter.projectId) {
if (searchParams.filter) {
const parsed = JSON.parse(searchParams.filter);
searchParams.filter = JSON.stringify({ and: [parsed, { '==': [{ var: 'project_id' }, filter.projectId] }] });
} else {
searchParams.filter = JSON.stringify({ and: [{ '==': [{ var: 'project_id' }, filter.projectId] }] });
}
}
tasksData = await serverProxy.tasks.get(searchParams);
const tasks = tasksData.map((task) => new Task(task));
tasks.count = tasksData.count;
return tasks;
};
@ -216,15 +223,15 @@ const config = require('./config');
id: isInteger,
page: isInteger,
search: isString,
sort: isString,
filter: isString,
});
checkExclusiveFields(filter, ['id'], ['page']);
const searchParams = {};
for (const field of ['filter', 'search', 'status', 'id', 'page']) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams[camelToSnake(field)] = filter[field];
for (const key of Object.keys(filter)) {
if (['id', 'page', 'search', 'sort', 'page'].includes(key)) {
searchParams[key] = filter[key];
}
}
@ -246,25 +253,19 @@ const config = require('./config');
checkFilter(filter, {
page: isInteger,
filter: isString,
sort: isString,
id: isInteger,
search: isString,
});
checkExclusiveFields(filter, ['id', 'search'], ['page']);
const searchParams = new URLSearchParams();
for (const field of [
'filter',
'search',
'id',
'page',
]) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(camelToSnake(field), filter[field]);
const searchParams = {};
for (const key of Object.keys(filter)) {
if (['page', 'filter', 'sort', 'id', 'search'].includes(key)) {
searchParams[key] = filter[key];
}
}
const cloudStoragesData = await serverProxy.cloudStorages.get(searchParams.toString());
const cloudStoragesData = await serverProxy.cloudStorages.get(searchParams);
const cloudStorages = cloudStoragesData.map((cloudStorage) => new CloudStorage(cloudStorage));
cloudStorages.count = cloudStoragesData.count;
return cloudStorages;

@ -87,16 +87,6 @@
return true;
}
function camelToSnake(str) {
if (typeof str !== 'string') {
throw new ArgumentError('str is expected to be string');
}
return (
str[0].toLowerCase() + str.slice(1, str.length).replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
);
}
class FieldUpdateTrigger {
constructor() {
let updatedFlags = {};
@ -136,7 +126,6 @@
checkFilter,
checkObjectType,
checkExclusiveFields,
camelToSnake,
FieldUpdateTrigger,
};
})();

@ -1521,13 +1521,15 @@
}
}
async function getCloudStorages(filter = '') {
async function getCloudStorages(filter = {}) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/cloudstorages?page_size=12&${filter}`, {
response = await Axios.get(`${backendAPI}/cloudstorages`, {
proxy: config.proxy,
params: filter,
page_size: 12,
});
} catch (errorData) {
throw generateError(errorData);

@ -1,12 +1,12 @@
{
"name": "cvat-ui",
"version": "1.36.0",
"version": "1.37.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-ui",
"version": "1.36.0",
"version": "1.37.0",
"license": "MIT",
"dependencies": {
"@ant-design/icons": "^4.6.3",

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.36.0",
"version": "1.37.0",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {

@ -5,7 +5,7 @@
import { Dispatch, ActionCreator } from 'redux';
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core-wrapper';
import { CloudStoragesQuery, CloudStorage } from 'reducers/interfaces';
import { CloudStoragesQuery, CloudStorage, Indexable } from 'reducers/interfaces';
const cvat = getCore();
@ -103,40 +103,16 @@ export type CloudStorageActions = ActionUnion<typeof cloudStoragesActions>;
export function getCloudStoragesAsync(query: Partial<CloudStoragesQuery>): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
function camelToSnake(str: string): string {
return (
str[0].toLowerCase() + str.slice(1, str.length)
.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`)
);
}
dispatch(cloudStoragesActions.getCloudStorages());
dispatch(cloudStoragesActions.updateCloudStoragesGettingQuery(query));
const filteredQuery = { ...query };
for (const key in filteredQuery) {
if (filteredQuery[key] === null) {
delete filteredQuery[key];
if ((filteredQuery as Indexable)[key] === null) {
delete (filteredQuery as Indexable)[key];
}
}
// Temporary hack to do not change UI currently for cloud storages
// Will be redesigned in a different PR
const filter = {
and: ['displayName', 'resource', 'description', 'owner', 'providerType', 'credentialsType'].reduce<object[]>((acc, filterField) => {
if (filterField in filteredQuery) {
acc.push({ '==': [{ var: camelToSnake(filterField) }, filteredQuery[filterField]] });
delete filteredQuery[filterField];
}
return acc;
}, []),
};
if (filter.and.length) {
filteredQuery.filter = JSON.stringify(filter);
}
let result = null;
try {
result = await cvat.cloudStorages.get(filteredQuery);

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -52,7 +52,7 @@ export const importDatasetAsync = (instance: any, format: string, file: File): T
}
dispatch(importActions.importDatasetSuccess());
dispatch(getProjectsAsync({ id: instance.id }));
dispatch(getProjectsAsync({ id: instance.id }, getState().projects.tasksGettingQuery));
}
);

@ -4,7 +4,7 @@
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core-wrapper';
import { JobsQuery } from 'reducers/interfaces';
import { Indexable, JobsQuery } from 'reducers/interfaces';
const cvat = getCore();
@ -30,12 +30,13 @@ export type JobsActions = ActionUnion<typeof jobsActions>;
export const getJobsAsync = (query: JobsQuery): ThunkAction => async (dispatch) => {
try {
// Remove all keys with null values from the query
const filteredQuery: Partial<JobsQuery> = { ...query };
if (filteredQuery.page === null) delete filteredQuery.page;
if (filteredQuery.filter === null) delete filteredQuery.filter;
if (filteredQuery.sort === null) delete filteredQuery.sort;
if (filteredQuery.search === null) delete filteredQuery.search;
// We remove all keys with null values from the query
const filteredQuery = { ...query };
for (const key of Object.keys(query)) {
if ((filteredQuery as Indexable)[key] === null) {
delete (filteredQuery as Indexable)[key];
}
}
dispatch(jobsActions.getJobs(filteredQuery));
const jobs = await cvat.jobs.get(filteredQuery);

@ -5,7 +5,9 @@
import { Dispatch, ActionCreator } from 'redux';
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import { ProjectsQuery, TasksQuery, CombinedState } from 'reducers/interfaces';
import {
ProjectsQuery, TasksQuery, CombinedState, Indexable,
} from 'reducers/interfaces';
import { getTasksAsync } from 'actions/tasks-actions';
import { getCVATStore } from 'cvat-store';
import getCore from 'cvat-core-wrapper';
@ -80,17 +82,19 @@ const projectActions = {
export type ProjectActions = ActionUnion<typeof projectActions>;
export function getProjectTasksAsync(tasksQuery: Partial<TasksQuery> = {}): ThunkAction<void> {
return (dispatch: ActionCreator<Dispatch>): void => {
return (dispatch: ActionCreator<Dispatch>, getState: () => CombinedState): void => {
const store = getCVATStore();
const state: CombinedState = store.getState();
dispatch(projectActions.updateProjectsGettingQuery({}, tasksQuery));
dispatch(projectActions.updateProjectsGettingQuery(
getState().projects.gettingQuery,
tasksQuery,
));
const query: Partial<TasksQuery> = {
...state.projects.tasksGettingQuery,
page: 1,
...tasksQuery,
};
dispatch(getTasksAsync(query));
dispatch(getTasksAsync(query, false));
};
}
@ -107,29 +111,13 @@ export function getProjectsAsync(
...query,
};
for (const key in filteredQuery) {
if (filteredQuery[key] === null || typeof filteredQuery[key] === 'undefined') {
delete filteredQuery[key];
for (const key of Object.keys(filteredQuery)) {
const value = (filteredQuery as Indexable)[key];
if (value === null || typeof value === 'undefined') {
delete (filteredQuery as Indexable)[key];
}
}
// Temporary hack to do not change UI currently for projects
// Will be redesigned in a different PR
const filter = {
and: ['owner', 'assignee', 'name', 'status'].reduce<object[]>((acc, filterField) => {
if (filterField in filteredQuery) {
acc.push({ '==': [{ var: filterField }, filteredQuery[filterField]] });
delete filteredQuery[filterField];
}
return acc;
}, []),
};
if (filter.and.length) {
filteredQuery.filter = JSON.stringify(filter);
}
let result = null;
try {
result = await cvat.projects.get(filteredQuery);

@ -4,7 +4,7 @@
import { AnyAction, Dispatch, ActionCreator } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { TasksQuery, CombinedState } from 'reducers/interfaces';
import { TasksQuery, CombinedState, Indexable } from 'reducers/interfaces';
import { getCVATStore } from 'cvat-store';
import getCore from 'cvat-core-wrapper';
import { getInferenceStatusAsync } from './models-actions';
@ -41,10 +41,11 @@ export enum TasksActionTypes {
SWITCH_MOVE_TASK_MODAL_VISIBLE = 'SWITCH_MOVE_TASK_MODAL_VISIBLE',
}
function getTasks(query: TasksQuery): AnyAction {
function getTasks(query: TasksQuery, updateQuery: boolean): AnyAction {
const action = {
type: TasksActionTypes.GET_TASKS,
payload: {
updateQuery,
query,
},
};
@ -74,35 +75,18 @@ function getTasksFailed(error: any): AnyAction {
return action;
}
export function getTasksAsync(query: TasksQuery): ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function getTasksAsync(query: TasksQuery, updateQuery = true): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(getTasks(query));
dispatch(getTasks(query, updateQuery));
// We need remove all keys with null values from query
// We remove all keys with null values from the query
const filteredQuery = { ...query };
for (const key in filteredQuery) {
if (filteredQuery[key] === null) {
delete filteredQuery[key];
for (const key of Object.keys(query)) {
if ((filteredQuery as Indexable)[key] === null) {
delete (filteredQuery as Indexable)[key];
}
}
// Temporary hack to do not change UI currently for tasks
// Will be redesigned in a different PR
const filter = {
and: ['owner', 'assignee', 'name', 'status', 'mode', 'dimension'].reduce<object[]>((acc, filterField) => {
if (filterField in filteredQuery) {
acc.push({ '==': [{ var: filterField }, filteredQuery[filterField]] });
delete filteredQuery[filterField];
}
return acc;
}, []),
};
if (filter.and.length) {
filteredQuery.filter = JSON.stringify(filter);
}
let result = null;
try {
result = await cvat.tasks.get(filteredQuery);
@ -115,7 +99,6 @@ export function getTasksAsync(query: TasksQuery): ThunkAction<Promise<void>, {},
const promises = array.map((task): string => (task as any).frames.preview().catch(() => ''));
dispatch(getInferenceStatusAsync());
dispatch(getTasksSuccess(array, await Promise.all(promises), result.count));
};
}

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,16 +1,17 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, { useCallback, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import React, { useCallback, useEffect, useState } from 'react';
import { useHistory } from 'react-router';
import { useDispatch, useSelector } from 'react-redux';
import { Row, Col } from 'antd/lib/grid';
import Spin from 'antd/lib/spin';
import { CloudStoragesQuery, CombinedState } from 'reducers/interfaces';
import { CombinedState, Indexable } from 'reducers/interfaces';
import { getCloudStoragesAsync } from 'actions/cloud-storage-actions';
import { updateHistoryFromQuery } from 'components/resource-sorting-filtering';
import CloudStoragesListComponent from './cloud-storages-list';
import EmptyCloudStorageListComponent from './empty-cloud-storages-list';
import TopBarComponent from './top-bar';
@ -18,21 +19,40 @@ import TopBarComponent from './top-bar';
export default function StoragesPageComponent(): JSX.Element {
const dispatch = useDispatch();
const history = useHistory();
const { search } = history.location;
const [isMounted, setIsMounted] = useState(false);
const totalCount = useSelector((state: CombinedState) => state.cloudStorages.count);
const isFetching = useSelector((state: CombinedState) => state.cloudStorages.fetching);
const fetching = useSelector((state: CombinedState) => state.cloudStorages.fetching);
const current = useSelector((state: CombinedState) => state.cloudStorages.current);
const query = useSelector((state: CombinedState) => state.cloudStorages.gettingQuery);
const onSearch = useCallback(
(_query: CloudStoragesQuery) => {
if (!isFetching) dispatch(getCloudStoragesAsync(_query));
},
[isFetching],
);
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;
}
}
useEffect(() => {
dispatch(getCloudStoragesAsync({ ...updatedQuery }));
setIsMounted(true);
}, []);
useEffect(() => {
if (isMounted) {
// do not update URL from previous query which might exist if we left page of SPA before and returned here
history.replace({
search: updateHistoryFromQuery(query),
});
}
}, [query]);
const onChangePage = useCallback(
(page: number) => {
if (!isFetching && page !== query.page) dispatch(getCloudStoragesAsync({ ...query, page }));
if (!fetching && page !== query.page) {
dispatch(getCloudStoragesAsync({ ...query, page }));
}
},
[query],
);
@ -44,60 +64,57 @@ export default function StoragesPageComponent(): JSX.Element {
xxl: 16,
};
useEffect(() => {
const searchParams = new URLSearchParams();
for (const [key, value] of Object.entries(query)) {
if (value !== null && typeof value !== 'undefined') {
searchParams.append(key, value.toString());
}
}
history.push({
pathname: '/cloudstorages',
search: `?${searchParams.toString()}`,
});
}, [query]);
useEffect(() => {
const searchParams = { ...query };
for (const [key, value] of new URLSearchParams(search)) {
if (key in searchParams) {
searchParams[key] = ['page', 'id'].includes(key) ? +value : value;
}
}
onSearch(searchParams);
}, []);
const searchWasUsed = Object.entries(query).some(([key, value]) => {
if (key === 'page') {
return value && Number.isInteger(value) && value > 1;
}
return !!value;
});
if (isFetching) {
return (
<Row className='cvat-cloud-storages-page' justify='center' align='middle'>
<Spin size='large' />
</Row>
);
}
const anySearch = Object.keys(query)
.some((value: string) => value !== 'page' && (query as any)[value] !== null);
const content = current.length ? (
<CloudStoragesListComponent
totalCount={totalCount}
page={query.page}
storages={current}
onChangePage={onChangePage}
/>
) : (
<EmptyCloudStorageListComponent notFound={anySearch} />
);
return (
<Row className='cvat-cloud-storages-page' justify='center' align='top'>
<Col {...dimensions}>
<TopBarComponent query={query} onSearch={onSearch} />
{current.length ? (
<CloudStoragesListComponent
totalCount={totalCount}
page={query.page}
storages={current}
onChangePage={onChangePage}
/>
) : (
<EmptyCloudStorageListComponent notFound={searchWasUsed} />
)}
<TopBarComponent
onApplySearch={(_search: string | null) => {
dispatch(
getCloudStoragesAsync({
...query,
search: _search,
page: 1,
}),
);
}}
onApplyFilter={(filter: string | null) => {
dispatch(
getCloudStoragesAsync({
...query,
filter,
page: 1,
}),
);
}}
onApplySorting={(sorting: string | null) => {
dispatch(
getCloudStoragesAsync({
...query,
sort: sorting,
page: 1,
}),
);
}}
query={updatedQuery}
/>
{ fetching ? (
<Row className='cvat-cloud-storages-page' justify='center' align='middle'>
<Spin size='large' />
</Row>
) : content }
</Col>
</Row>
);

@ -26,16 +26,29 @@
}
.cvat-cloud-storages-list-top-bar {
> div:first-child {
.cvat-title {
margin-right: $grid-unit-size;
}
> div {
display: flex;
}
> div:last-child {
text-align: right;
justify-content: space-between;
> .cvat-cloudstorages-page-filters-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
> div {
> *:not(:last-child) {
margin-right: $grid-unit-size;
}
display: flex;
margin-right: $grid-unit-size * 4;
}
.cvat-cloudstorages-page-tasks-search-bar {
width: $grid-unit-size * 32;
}
}
}
}

@ -1,42 +1,92 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import React, { useState } from 'react';
import { useHistory } from 'react-router';
import { Row, Col } from 'antd/lib/grid';
import Button from 'antd/lib/button';
import Text from 'antd/lib/typography/Text';
import { PlusOutlined } from '@ant-design/icons';
import SearchField from 'components/search-field/search-field';
import { CloudStoragesQuery } from 'reducers/interfaces';
import Input from 'antd/lib/input';
import { SortingComponent, ResourceFilterHOC, defaultVisibility } from 'components/resource-sorting-filtering';
import {
localStorageRecentKeyword, localStorageRecentCapacity,
predefinedFilterValues, config,
} from './cloud-storages-filter-configuration';
const FilteringComponent = ResourceFilterHOC(
config, localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues,
);
interface Props {
onSearch(query: CloudStoragesQuery): void;
onApplyFilter(filter: string | null): void;
onApplySorting(sorting: string | null): void;
onApplySearch(search: string | null): void;
query: CloudStoragesQuery;
}
export default function StoragesTopBar(props: Props): JSX.Element {
const { onSearch, query } = props;
const {
query, onApplyFilter, onApplySorting, onApplySearch,
} = props;
const history = useHistory();
const [visibility, setVisibility] = useState(defaultVisibility);
return (
<Row justify='space-between' align='middle' className='cvat-cloud-storages-list-top-bar'>
<Col md={11} lg={9} xl={9} xxl={9}>
<Text className='cvat-title'>Cloud Storages</Text>
<SearchField instance='cloudstorage' onSearch={onSearch} query={query} />
</Col>
<Col md={{ span: 11 }} lg={{ span: 9 }} xl={{ span: 9 }} xxl={{ span: 9 }}>
<Col span={24}>
<div className='cvat-cloudstorages-page-filters-wrapper'>
<Input.Search
enterButton
onSearch={(phrase: string) => {
onApplySearch(phrase);
}}
defaultValue={query.search || ''}
className='cvat-cloudstorages-page-tasks-search-bar'
placeholder='Search ...'
/>
<div>
<SortingComponent
visible={visibility.sorting}
onVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, sorting: visible })
)}
defaultFields={query.sort?.split(',') || ['-ID']}
sortingFields={['ID', 'Provider type', 'Updated date', 'Display name', 'Resource', 'Credentials type', 'Owner', 'Description']}
onApplySorting={(sorting: string | null) => {
onApplySorting(sorting);
}}
/>
<FilteringComponent
value={query.filter}
predefinedVisible={visibility.predefined}
builderVisible={visibility.builder}
recentVisible={visibility.recent}
onPredefinedVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, predefined: visible })
)}
onBuilderVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, builder: visible })
)}
onRecentVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, builder: visibility.builder, recent: visible })
)}
onApplyFilter={(filter: string | null) => {
onApplyFilter(filter);
}}
/>
</div>
</div>
<Button
size='large'
className='cvat-attach-cloud-storage-button'
type='primary'
onClick={(): void => history.push('/cloudstorages/create')}
icon={<PlusOutlined />}
>
Attach a new storage
</Button>
/>
</Col>
</Row>
);

@ -118,4 +118,3 @@ export const predefinedFilterValues = {
'Assigned to me': '{"and":[{"==":[{"var":"assignee"},"<username>"]}]}',
'Not completed': '{"!":{"or":[{"==":[{"var":"state"},"completed"]},{"==":[{"var":"stage"},"acceptance"]}]}}',
};
export const defaultEnabledFilters = ['Not completed'];

@ -3,14 +3,18 @@
// SPDX-License-Identifier: MIT
import './styles.scss';
import React from 'react';
import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router';
import { useDispatch, useSelector } from 'react-redux';
import Spin from 'antd/lib/spin';
import { Col, Row } from 'antd/lib/grid';
import Pagination from 'antd/lib/pagination';
import Empty from 'antd/lib/empty';
import Text from 'antd/lib/typography/Text';
import { CombinedState } from 'reducers/interfaces';
import FeedbackComponent from 'components/feedback/feedback';
import { updateHistoryFromQuery } from 'components/resource-sorting-filtering';
import { CombinedState, Indexable } from 'reducers/interfaces';
import { getJobsAsync } from 'actions/jobs-actions';
import TopBarComponent from './top-bar';
@ -18,22 +22,39 @@ import JobsContentComponent from './jobs-content';
function JobsPageComponent(): JSX.Element {
const dispatch = useDispatch();
const history = useHistory();
const [isMounted, setIsMounted] = useState(false);
const query = useSelector((state: CombinedState) => state.jobs.query);
const fetching = useSelector((state: CombinedState) => state.jobs.fetching);
const count = useSelector((state: CombinedState) => state.jobs.count);
const dimensions = {
md: 22,
lg: 18,
xl: 16,
xxl: 16,
};
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;
}
}
useEffect(() => {
dispatch(getJobsAsync({ ...updatedQuery }));
setIsMounted(true);
}, []);
useEffect(() => {
if (isMounted) {
history.replace({
search: updateHistoryFromQuery(query),
});
}
}, [query]);
const content = count ? (
<>
<JobsContentComponent />
<Row justify='space-around' about='middle'>
<Col {...dimensions}>
<Col md={22} lg={18} xl={16} xxl={16}>
<Pagination
className='cvat-jobs-page-pagination'
onChange={(page: number) => {
@ -51,12 +72,12 @@ function JobsPageComponent(): JSX.Element {
</Col>
</Row>
</>
) : <Empty />;
) : <Empty description={<Text>No results matched your search...</Text>} />;
return (
<div className='cvat-jobs-page'>
<TopBarComponent
query={query}
query={updatedQuery}
onApplySearch={(search: string | null) => {
dispatch(
getJobsAsync({
@ -88,7 +109,7 @@ function JobsPageComponent(): JSX.Element {
{ fetching ? (
<Spin size='large' className='cvat-spinner' />
) : content }
<FeedbackComponent />
</div>
);
}

@ -114,133 +114,6 @@
}
}
.cvat-jobs-filter-dropdown-users {
padding: $grid-unit-size;
}
.cvat-jobs-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-jobs-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-jobs-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--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;
}
}
}
}
}
.cvat-jobs-page-sorting-list,
.cvat-jobs-page-predefined-filters-list,
.cvat-jobs-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-jobs-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-jobs-page-filters-space {
justify-content: right;
align-items: center;
display: flex;
}
.cvat-jobs-page-top-bar {
> div {
display: flex;

@ -7,30 +7,15 @@ import { Col, Row } from 'antd/lib/grid';
import Input from 'antd/lib/input';
import { JobsQuery } from 'reducers/interfaces';
import SortingComponent from './sorting';
import ResourceFilterHOC from './filtering';
import { SortingComponent, ResourceFilterHOC, defaultVisibility } from 'components/resource-sorting-filtering';
import {
localStorageRecentKeyword, localStorageRecentCapacity,
predefinedFilterValues, defaultEnabledFilters, config,
localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues, config,
} from './jobs-filter-configuration';
const FilteringComponent = ResourceFilterHOC(
config, localStorageRecentKeyword, localStorageRecentCapacity,
predefinedFilterValues, defaultEnabledFilters,
config, localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues,
);
const defaultVisibility: {
predefined: boolean;
recent: boolean;
builder: boolean;
sorting: boolean;
} = {
predefined: false,
recent: false,
builder: false,
sorting: false,
};
interface Props {
query: JobsQuery;
onApplyFilter(filter: string | null): void;
@ -42,7 +27,7 @@ function TopBarComponent(props: Props): JSX.Element {
const {
query, onApplyFilter, onApplySorting, onApplySearch,
} = props;
const [visibility, setVisibility] = useState<typeof defaultVisibility>(defaultVisibility);
const [visibility, setVisibility] = useState(defaultVisibility);
return (
<Row className='cvat-jobs-page-top-bar' justify='center' align='middle'>
@ -55,7 +40,7 @@ function TopBarComponent(props: Props): JSX.Element {
}}
defaultValue={query.search || ''}
className='cvat-jobs-page-search-bar'
placeholder='Search ..'
placeholder='Search ...'
/>
<div>
<SortingComponent
@ -63,11 +48,12 @@ function TopBarComponent(props: Props): JSX.Element {
onVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, sorting: visible })
)}
defaultFields={query.sort?.split(',') || ['ID']}
defaultFields={query.sort?.split(',') || ['-ID']}
sortingFields={['ID', 'Assignee', 'Updated date', 'Stage', 'State', 'Task ID', 'Project ID', 'Task name', 'Project name']}
onApplySorting={onApplySorting}
/>
<FilteringComponent
value={query.filter}
predefinedVisible={visibility.predefined}
builderVisible={visibility.builder}
recentVisible={visibility.recent}

@ -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,11 +1,10 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React from 'react';
import TopBarComponent from './top-bar';
import DeployedModelsList from './deployed-models-list';
import EmptyListComponent from './empty-list';
import FeedbackComponent from '../feedback/feedback';
@ -19,13 +18,14 @@ interface Props {
}
export default function ModelsPageComponent(props: Props): JSX.Element {
const { interactors, detectors, trackers, reid } = props;
const {
interactors, detectors, trackers, reid,
} = props;
const deployedModels = [...detectors, ...interactors, ...trackers, ...reid];
return (
<div className='cvat-models-page'>
<TopBarComponent />
{deployedModels.length ? <DeployedModelsList models={deployedModels} /> : <EmptyListComponent />}
<FeedbackComponent />
</div>

@ -13,39 +13,15 @@
width: 100%;
> div:nth-child(1) {
margin-bottom: 10px;
> div:nth-child(1) {
display: flex;
}
> div:nth-child(2) {
display: flex;
justify-content: flex-end;
}
margin-bottom: $grid-unit-size;
}
}
.cvat-empty-models-list {
/* empty-models icon */
> div:nth-child(1) {
margin-top: 50px;
}
/* No models uploaded yet */
> div:nth-child(2) > div {
margin-top: 20px;
> span {
font-size: 20px;
color: $text-color;
}
}
/* To annotate your task automatically */
> div:nth-child(3) {
margin-top: 10px;
}
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.cvat-models-list {
@ -58,8 +34,8 @@
height: auto;
border: 1px solid $border-color-1;
border-radius: 3px;
margin-bottom: 15px;
padding: 15px;
margin-bottom: $grid-unit-size * 2;
padding: $grid-unit-size * 2;
background: $background-color-1;
&:hover {
@ -83,7 +59,3 @@
overflow: hidden;
}
}
#cvat-create-model-button {
padding: 0 30px;
}

@ -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>
);
}

@ -1,11 +1,11 @@
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, { useEffect, useCallback } from 'react';
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useHistory, useParams, useLocation } from 'react-router';
import { useHistory, useParams } from 'react-router';
import Spin from 'antd/lib/spin';
import { Row, Col } from 'antd/lib/grid';
import Result from 'antd/lib/result';
@ -13,19 +13,30 @@ import Button from 'antd/lib/button';
import Title from 'antd/lib/typography/Title';
import Pagination from 'antd/lib/pagination';
import { PlusOutlined } from '@ant-design/icons';
import Empty from 'antd/lib/empty';
import Input from 'antd/lib/input';
import { CombinedState, Task, TasksQuery } from 'reducers/interfaces';
import { CombinedState, Task, Indexable } from 'reducers/interfaces';
import { getProjectsAsync, getProjectTasksAsync } from 'actions/projects-actions';
import { cancelInferenceAsync } from 'actions/models-actions';
import TaskItem from 'components/tasks-page/task-item';
import SearchField from 'components/search-field/search-field';
import MoveTaskModal from 'components/move-task-modal/move-task-modal';
import ModelRunnerDialog from 'components/model-runner-modal/model-runner-dialog';
import ImportDatasetModal from 'components/import-dataset-modal/import-dataset-modal';
import { useDidUpdateEffect } from 'utils/hooks';
import {
SortingComponent, ResourceFilterHOC, defaultVisibility, updateHistoryFromQuery,
} from 'components/resource-sorting-filtering';
import DetailsComponent from './details';
import ProjectTopBar from './top-bar';
import {
localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues, config,
} from './project-tasks-filter-configuration';
const FilteringComponent = ResourceFilterHOC(
config, localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues,
);
interface ParamType {
id: string;
}
@ -34,7 +45,6 @@ export default function ProjectPageComponent(): JSX.Element {
const id = +useParams<ParamType>().id;
const dispatch = useDispatch();
const history = useHistory();
const { search } = useLocation();
const projects = useSelector((state: CombinedState) => state.projects.current).map((project) => project.instance);
const projectsFetching = useSelector((state: CombinedState) => state.projects.fetching);
const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes);
@ -42,7 +52,24 @@ export default function ProjectPageComponent(): JSX.Element {
const tasksActiveInferences = useSelector((state: CombinedState) => state.models.inferences);
const tasks = useSelector((state: CombinedState) => state.tasks.current);
const tasksCount = useSelector((state: CombinedState) => state.tasks.count);
const tasksGettingQuery = useSelector((state: CombinedState) => state.projects.tasksGettingQuery);
const tasksQuery = useSelector((state: CombinedState) => state.projects.tasksGettingQuery);
const tasksFetching = useSelector((state: CombinedState) => state.tasks.fetching);
const [isMounted, setIsMounted] = useState(false);
const [visibility, setVisibility] = useState(defaultVisibility);
const queryParams = new URLSearchParams(history.location.search);
const updatedQuery = { ...tasksQuery };
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;
}
}
useEffect(() => {
dispatch(getProjectTasksAsync({ ...updatedQuery, projectId: id }));
setIsMounted(true);
}, []);
const [project] = projects.filter((_project) => _project.id === id);
const projectSubsets: Array<string> = [];
@ -50,42 +77,25 @@ export default function ProjectPageComponent(): JSX.Element {
if (!projectSubsets.includes(task.instance.subset)) projectSubsets.push(task.instance.subset);
}
const deleteActivity = project && id in deletes ? deletes[id] : null;
const onPageChange = useCallback(
(p: number) => {
dispatch(getProjectTasksAsync({
projectId: id,
page: p,
}));
},
[],
);
useEffect(() => {
const searchParams: Partial<TasksQuery> = {};
for (const [param, value] of new URLSearchParams(search)) {
searchParams[param] = ['page'].includes(param) ? Number.parseInt(value, 10) : value;
if (!project) {
dispatch(getProjectsAsync({ id }, updatedQuery));
}
dispatch(getProjectsAsync({ id }, searchParams));
}, []);
useDidUpdateEffect(() => {
const searchParams = new URLSearchParams();
for (const [name, value] of Object.entries(tasksGettingQuery)) {
if (value !== null && typeof value !== 'undefined' && !['projectId', 'ordering'].includes(name)) {
searchParams.append(name, value.toString());
}
useEffect(() => {
if (isMounted) {
history.replace({
search: updateHistoryFromQuery(tasksQuery),
});
}
history.push({
pathname: `/projects/${id}`,
search: `?${searchParams.toString()}`,
});
}, [tasksGettingQuery, id]);
if (deleteActivity) {
history.push('/projects');
}
}, [tasksQuery]);
useEffect(() => {
if (project && id in deletes && deletes[id]) {
history.push('/projects');
}
}, [deletes]);
if (projectsFetching) {
return <Spin size='large' className='cvat-spinner' />;
@ -102,12 +112,51 @@ export default function ProjectPageComponent(): JSX.Element {
);
}
const paginationDimensions = {
md: 22,
lg: 18,
xl: 16,
xxl: 16,
};
const content = tasksCount ? (
<>
{projectSubsets.map((subset: string) => (
<React.Fragment key={subset}>
{subset && <Title level={4}>{subset}</Title>}
{tasks
.filter((task) => task.instance.projectId === project.id && task.instance.subset === subset)
.map((task: Task) => (
<TaskItem
key={task.instance.id}
deleted={task.instance.id in taskDeletes ? taskDeletes[task.instance.id] : false}
hidden={false}
activeInference={tasksActiveInferences[task.instance.id] || null}
cancelAutoAnnotation={() => {
dispatch(cancelInferenceAsync(task.instance.id));
}}
previewImage={task.preview}
taskInstance={task.instance}
/>
))}
</React.Fragment>
))}
<Row justify='center' align='middle'>
<Col md={22} lg={18} xl={16} xxl={14}>
<Pagination
className='cvat-project-tasks-pagination'
onChange={(page: number) => {
dispatch(getProjectTasksAsync({
...tasksQuery,
projectId: id,
page,
}));
}}
showSizeChanger={false}
total={tasksCount}
pageSize={10}
current={tasksQuery.page}
showQuickJumper
/>
</Col>
</Row>
</>
) : (
<Empty description='No tasks found' />
);
return (
<Row justify='center' align='top' className='cvat-project-page'>
@ -115,61 +164,81 @@ export default function ProjectPageComponent(): JSX.Element {
<ProjectTopBar projectInstance={project} />
<DetailsComponent project={project} />
<Row justify='space-between' align='middle' className='cvat-project-page-tasks-bar'>
<Col className='cvat-project-tasks-title-search'>
<Title level={3}>Tasks</Title>
<SearchField
query={tasksGettingQuery}
instance='task'
skipFields={['ordering', 'projectId']}
onSearch={(query: TasksQuery) => dispatch(getProjectTasksAsync(query))}
/>
</Col>
<Col>
<Col span={24}>
<div className='cvat-project-page-tasks-filters-wrapper'>
<Input.Search
enterButton
onSearch={(_search: string) => {
dispatch(getProjectTasksAsync({
...tasksQuery,
page: 1,
projectId: id,
search: _search,
}));
}}
defaultValue={tasksQuery.search || ''}
className='cvat-project-page-tasks-search-bar'
placeholder='Search ...'
/>
<div>
<SortingComponent
visible={visibility.sorting}
onVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, sorting: visible })
)}
defaultFields={tasksQuery.sort?.split(',') || ['-ID']}
sortingFields={['ID', 'Owner', 'Status', 'Assignee', 'Updated date', 'Subset', 'Mode', 'Dimension', 'Name']}
onApplySorting={(sorting: string | null) => {
dispatch(getProjectTasksAsync({
...tasksQuery,
page: 1,
projectId: id,
sort: sorting,
}));
}}
/>
<FilteringComponent
value={updatedQuery.filter}
predefinedVisible={visibility.predefined}
builderVisible={visibility.builder}
recentVisible={visibility.recent}
onPredefinedVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, predefined: visible })
)}
onBuilderVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, builder: visible })
)}
onRecentVisibleChange={(visible: boolean) => (
setVisibility({
...defaultVisibility,
builder: visibility.builder,
recent: visible,
})
)}
onApplyFilter={(filter: string | null) => {
dispatch(getProjectTasksAsync({
...tasksQuery,
page: 1,
projectId: id,
filter,
}));
}}
/>
</div>
</div>
<Button
size='large'
type='primary'
icon={<PlusOutlined />}
id='cvat-create-task-button'
className='cvat-create-task-button'
onClick={() => history.push(`/tasks/create?projectId=${id}`)}
>
Create new task
</Button>
</Col>
</Row>
{projectSubsets.map((subset: string) => (
<React.Fragment key={subset}>
{subset && <Title level={4}>{subset}</Title>}
{tasks
.filter((task) => task.instance.projectId === project.id && task.instance.subset === subset)
.map((task: Task) => (
<TaskItem
key={task.instance.id}
deleted={task.instance.id in taskDeletes ? taskDeletes[task.instance.id] : false}
hidden={false}
activeInference={tasksActiveInferences[task.instance.id] || null}
cancelAutoAnnotation={() => {
dispatch(cancelInferenceAsync(task.instance.id));
}}
previewImage={task.preview}
taskInstance={task.instance}
/>
))}
</React.Fragment>
))}
<Row justify='center'>
<Col {...paginationDimensions}>
<Pagination
className='cvat-project-tasks-pagination'
onChange={onPageChange}
showSizeChanger={false}
total={tasksCount}
pageSize={10}
current={tasksGettingQuery.page}
showQuickJumper
/>
</Col>
</Row>
{ tasksFetching ? (
<Spin size='large' className='cvat-spinner' />
) : content }
</Col>
<MoveTaskModal />
<ModelRunnerDialog />
<ImportDatasetModal />

@ -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"]}]}}',
};

@ -7,6 +7,39 @@
.cvat-project-page {
overflow-y: auto;
height: 100%;
.cvat-spinner {
position: relative;
}
}
.cvat-project-page-tasks-bar {
margin: $grid-unit-size * 2 0;
> div {
display: flex;
justify-content: space-between;
> .cvat-project-page-tasks-filters-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
> div {
> *:not(:last-child) {
margin-right: $grid-unit-size;
}
display: flex;
margin-right: $grid-unit-size * 4;
}
.cvat-project-page-tasks-search-bar {
width: $grid-unit-size * 32;
}
}
}
}
.cvat-project-details {
@ -38,10 +71,6 @@
}
}
.cvat-project-page-tasks-bar {
margin: $grid-unit-size * 2 0;
}
.ant-menu.cvat-project-actions-menu {
box-shadow: 0 0 17px rgba(0, 0, 0, 0.2);
@ -65,11 +94,3 @@
display: flex;
justify-content: center;
}
.cvat-project-tasks-title-search {
display: flex;
> * {
margin-right: $grid-unit-size * 2;
}
}

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -6,29 +6,18 @@ import React from 'react';
import { Link } from 'react-router-dom';
import Text from 'antd/lib/typography/Text';
import { Row, Col } from 'antd/lib/grid';
import Icon from '@ant-design/icons';
import { EmptyTasksIcon } from 'icons';
import Empty from 'antd/lib/empty';
interface Props {
notFound?: boolean;
notFound: boolean;
}
export default function EmptyListComponent(props: Props): JSX.Element {
const { notFound } = props;
return (
<div className='cvat-empty-projects-list'>
<Row justify='center' align='middle'>
<Col>
<Icon className='cvat-empty-projects-icon' component={EmptyTasksIcon} />
</Col>
</Row>
{notFound ? (
<Row justify='center' align='middle'>
<Col>
<Text strong>No results matched your search...</Text>
</Col>
</Row>
<Empty description={notFound ? (
<Text strong>No results matched your search...</Text>
) : (
<>
<Row justify='center' align='middle'>
@ -48,6 +37,7 @@ export default function EmptyListComponent(props: Props): JSX.Element {
</Row>
</>
)}
/>
</div>
);
}

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -14,18 +14,19 @@ import ProjectItem from './project-item';
export default function ProjectListComponent(): JSX.Element {
const dispatch = useDispatch();
const projectsCount = useSelector((state: CombinedState) => state.projects.count);
const { page } = useSelector((state: CombinedState) => state.projects.gettingQuery);
const projects = useSelector((state: CombinedState) => state.projects.current);
const gettingQuery = useSelector((state: CombinedState) => state.projects.gettingQuery);
const tasksQuery = useSelector((state: CombinedState) => state.projects.tasksGettingQuery);
const { page } = gettingQuery;
const changePage = useCallback((p: number) => {
dispatch(
getProjectsAsync({
...gettingQuery,
page: p,
}),
}, tasksQuery),
);
}, [dispatch, getProjectsAsync, gettingQuery]);
}, [gettingQuery]);
const dimensions = {
md: 22,

@ -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"]}]}}',
};

@ -1,68 +1,96 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, { useEffect } from 'react';
import React, { useEffect, useState } from 'react';
import { useHistory } from 'react-router';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation, useHistory } from 'react-router';
import Spin from 'antd/lib/spin';
import { CombinedState, ProjectsQuery } from 'reducers/interfaces';
import { getProjectsAsync } from 'actions/projects-actions';
import { CombinedState, Indexable } from 'reducers/interfaces';
import { getProjectsAsync, restoreProjectAsync } from 'actions/projects-actions';
import FeedbackComponent from 'components/feedback/feedback';
import { updateHistoryFromQuery } from 'components/resource-sorting-filtering';
import ImportDatasetModal from 'components/import-dataset-modal/import-dataset-modal';
import EmptyListComponent from './empty-list';
import TopBarComponent from './top-bar';
import ProjectListComponent from './project-list';
export default function ProjectsPageComponent(): JSX.Element {
const { search } = useLocation();
const history = useHistory();
const dispatch = useDispatch();
const projectFetching = useSelector((state: CombinedState) => state.projects.fetching);
const projectsCount = useSelector((state: CombinedState) => state.projects.current.length);
const gettingQuery = useSelector((state: CombinedState) => state.projects.gettingQuery);
const isImporting = useSelector((state: CombinedState) => state.projects.restoring);
const anySearchQuery = !!Array.from(new URLSearchParams(search).keys()).filter((value) => value !== 'page').length;
const history = useHistory();
const fetching = useSelector((state: CombinedState) => state.projects.fetching);
const count = useSelector((state: CombinedState) => state.projects.current.length);
const query = useSelector((state: CombinedState) => state.projects.gettingQuery);
const tasksQuery = useSelector((state: CombinedState) => state.projects.tasksGettingQuery);
const importing = useSelector((state: CombinedState) => state.projects.restoring);
const [isMounted, setIsMounted] = useState(false);
const anySearch = Object.keys(query).some((value: string) => value !== 'page' && (query as any)[value] !== null);
const getSearchParams = (): Partial<ProjectsQuery> => {
const searchParams: Partial<ProjectsQuery> = {};
for (const [param, value] of new URLSearchParams(search)) {
searchParams[param] = ['page', 'id'].includes(param) ? Number.parseInt(value, 10) : value;
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 searchParams;
};
}
useEffect(() => {
const searchParams = new URLSearchParams();
for (const [name, value] of Object.entries(gettingQuery)) {
if (value !== null && typeof value !== 'undefined') {
searchParams.append(name, value.toString());
}
}
history.push({
pathname: '/projects',
search: `?${searchParams.toString()}`,
});
}, [gettingQuery]);
dispatch(getProjectsAsync({ ...updatedQuery }));
setIsMounted(true);
}, []);
useEffect(() => {
if (isImporting === false) {
dispatch(getProjectsAsync(getSearchParams()));
if (isMounted) {
history.replace({
search: updateHistoryFromQuery(query),
});
}
}, [isImporting]);
}, [query]);
if (projectFetching) {
return <Spin size='large' className='cvat-spinner' />;
}
const content = count ? <ProjectListComponent /> : <EmptyListComponent notFound={anySearch} />;
return (
<div className='cvat-projects-page'>
<TopBarComponent />
{projectsCount ? <ProjectListComponent /> : <EmptyListComponent notFound={anySearchQuery} />}
<TopBarComponent
onApplySearch={(search: string | null) => {
dispatch(
getProjectsAsync({
...query,
search,
page: 1,
}, { ...tasksQuery, page: 1 }),
);
}}
onApplyFilter={(filter: string | null) => {
dispatch(
getProjectsAsync({
...query,
filter,
page: 1,
}, { ...tasksQuery, page: 1 }),
);
}}
onApplySorting={(sorting: string | null) => {
dispatch(
getProjectsAsync({
...query,
sort: sorting,
page: 1,
}, { ...tasksQuery, page: 1 }),
);
}}
query={updatedQuery}
onImportProject={(file: File) => dispatch(restoreProjectAsync(file))}
importing={importing}
/>
{ fetching ? (
<div className='cvat-empty-project-list'>
<Spin size='large' className='cvat-spinner' />
</div>
) : content }
<FeedbackComponent />
<ImportDatasetModal />
</div>

@ -37,42 +37,68 @@
}
}
/* empty-projects icon */
.cvat-empty-projects-list {
> div:nth-child(1) {
margin-top: $grid-unit-size * 6;
.ant-empty {
top: 50%;
left: 50%;
position: absolute;
transform: translate(-50%, -50%);
}
}
> div:nth-child(2) {
> div {
margin-top: $grid-unit-size * 3;
.cvat-projects-page-control-buttons-wrapper {
display: flex;
flex-direction: column;
background: $background-color-1;
padding: $grid-unit-size;
border-radius: 4px;
box-shadow: $box-shadow-base;
> * {
&:not(:first-child) {
margin-top: $grid-unit-size;
}
/* No projects created yet */
> span {
font-size: 20px;
color: $text-color;
width: 100%;
.ant-upload {
width: 100%;
button {
width: 100%;
}
}
}
/* To get started with your annotation project .. */
> div:nth-child(3) {
margin-top: $grid-unit-size;
}
}
.cvat-projects-page-top-bar {
> div:nth-child(1) {
> div:nth-child(1) {
> div {
display: flex;
justify-content: space-between;
> .cvat-projects-page-filters-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
> div {
> *:not(:last-child) {
margin-right: $grid-unit-size;
}
display: flex;
margin-right: $grid-unit-size * 4;
}
.cvat-projects-page-search-bar {
width: $grid-unit-size * 32;
padding-left: $grid-unit-size * 0.5;
}
}
}
}
.cvat-create-project-button {
padding: 0 $grid-unit-size * 4;
}
.cvat-projects-pagination {
display: flex;
justify-content: center;
@ -152,14 +178,10 @@
flex-wrap: wrap;
}
#cvat-export-project-loading {
.cvat-export-project-loading {
margin-left: 10;
}
#cvat-import-project-button {
padding: 0 30px;
}
#cvat-import-project-button-loading {
.cvat-import-project-button-loading {
margin-left: 10;
}

@ -2,78 +2,134 @@
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import React, { useState, useEffect } from 'react';
import { useHistory } from 'react-router';
import { Row, Col } from 'antd/lib/grid';
import Button from 'antd/lib/button';
import Text from 'antd/lib/typography/Text';
import Dropdown from 'antd/lib/dropdown';
import Input from 'antd/lib/input';
import { PlusOutlined, UploadOutlined, LoadingOutlined } from '@ant-design/icons';
import Upload from 'antd/lib/upload';
import SearchField from 'components/search-field/search-field';
import { CombinedState, ProjectsQuery } from 'reducers/interfaces';
import { getProjectsAsync, restoreProjectAsync } from 'actions/projects-actions';
import { usePrevious } from 'utils/hooks';
import { ProjectsQuery } from 'reducers/interfaces';
import { SortingComponent, ResourceFilterHOC, defaultVisibility } from 'components/resource-sorting-filtering';
export default function TopBarComponent(): JSX.Element {
import {
localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues, config,
} from './projects-filter-configuration';
const FilteringComponent = ResourceFilterHOC(
config, localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues,
);
interface Props {
onImportProject(file: File): void;
onApplyFilter(filter: string | null): void;
onApplySorting(sorting: string | null): void;
onApplySearch(search: string | null): void;
query: ProjectsQuery;
importing: boolean;
}
function TopBarComponent(props: Props): JSX.Element {
const {
importing, query, onApplyFilter, onApplySorting, onApplySearch, onImportProject,
} = props;
const [visibility, setVisibility] = useState(defaultVisibility);
const prevImporting = usePrevious(importing);
useEffect(() => {
if (prevImporting && !importing) {
onApplyFilter(query.filter);
}
}, [importing]);
const history = useHistory();
const dispatch = useDispatch();
const query = useSelector((state: CombinedState) => state.projects.gettingQuery);
const isImporting = useSelector((state: CombinedState) => state.projects.restoring);
return (
<Row className='cvat-projects-page-top-bar' justify='center' align='middle'>
<Col md={22} lg={18} xl={16} xxl={16}>
<Row justify='space-between' align='bottom'>
<Col>
<Text className='cvat-title'>Projects</Text>
<SearchField
query={query}
instance='project'
onSearch={(_query: ProjectsQuery) => dispatch(getProjectsAsync(_query))}
<div className='cvat-projects-page-filters-wrapper'>
<Input.Search
enterButton
onSearch={(phrase: string) => {
onApplySearch(phrase);
}}
defaultValue={query.search || ''}
className='cvat-projects-page-search-bar'
placeholder='Search ...'
/>
<div>
<SortingComponent
visible={visibility.sorting}
onVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, sorting: visible })
)}
defaultFields={query.sort?.split(',') || ['-ID']}
sortingFields={['ID', 'Assignee', 'Owner', 'Status', 'Name', 'Updated date']}
onApplySorting={onApplySorting}
/>
<FilteringComponent
value={query.filter}
predefinedVisible={visibility.predefined}
builderVisible={visibility.builder}
recentVisible={visibility.recent}
onPredefinedVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, predefined: visible })
)}
onBuilderVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, builder: visible })
)}
onRecentVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, builder: visibility.builder, recent: visible })
)}
onApplyFilter={onApplyFilter}
/>
</Col>
<Col>
<Row gutter={8}>
<Col>
</div>
</div>
<div>
<Dropdown
trigger={['click']}
overlay={(
<div className='cvat-projects-page-control-buttons-wrapper'>
<Button
id='cvat-create-project-button'
className='cvat-create-project-button'
type='primary'
onClick={(): void => history.push('/projects/create')}
icon={<PlusOutlined />}
>
Create a new project
</Button>
<Upload
accept='.zip'
multiple={false}
showUploadList={false}
beforeUpload={(file: File): boolean => {
dispatch(restoreProjectAsync(file));
onImportProject(file);
return false;
}}
className='cvat-import-project'
>
<Button
size='large'
id='cvat-import-project-button'
className='cvat-import-project-button'
type='primary'
disabled={isImporting}
disabled={importing}
icon={<UploadOutlined />}
>
Create from backup
{isImporting && <LoadingOutlined id='cvat-import-project-button-loading' />}
{importing && <LoadingOutlined className='cvat-import-project-button-loading' />}
</Button>
</Upload>
</Col>
<Col>
<Button
size='large'
id='cvat-create-project-button'
className='cvat-create-project-button'
type='primary'
onClick={(): void => history.push('/projects/create')}
icon={<PlusOutlined />}
>
Create new project
</Button>
</Col>
</Row>
</Col>
</Row>
</div>
)}
>
<Button type='primary' className='cvat-create-project-dropdown' icon={<PlusOutlined />} />
</Dropdown>
</div>
</Col>
</Row>
);
}
export default React.memo(TopBarComponent);

@ -14,16 +14,17 @@ import {
import Dropdown from 'antd/lib/dropdown';
import Space from 'antd/lib/space';
import Button from 'antd/lib/button';
import { useSelector } from 'react-redux';
import { CombinedState } from 'reducers/interfaces';
import Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox/Checkbox';
import Menu from 'antd/lib/menu';
import { useSelector } from 'react-redux';
import { CombinedState } from 'reducers/interfaces';
import { User } from 'components/task-page/user-selector';
interface ResourceFilterProps {
predefinedVisible: boolean;
recentVisible: boolean;
builderVisible: boolean;
value: string | null;
onPredefinedVisibleChange(visible: boolean): void;
onBuilderVisibleChange(visible: boolean): void;
onRecentVisibleChange(visible: boolean): void;
@ -35,7 +36,6 @@ export default function ResourceFilterHOC(
localStorageRecentKeyword: string,
localStorageRecentCapacity: number,
predefinedFilterValues: Record<string, string>,
defaultEnabledFilters: string[],
): React.FunctionComponent<ResourceFilterProps> {
const config: Config = { ...AntdConfig, ...filtrationCfg };
const defaultTree = QbUtils.checkTree(
@ -86,58 +86,78 @@ export default function ResourceFilterHOC(
built: null,
};
function isValidTree(tree: ImmutableTree): boolean {
return (QbUtils.queryString(tree, config) || '').trim().length > 0 && QbUtils.isValidTree(tree);
}
function unite(filters: string[]): string {
if (filters.length > 1) {
return JSON.stringify({
and: filters.map((filter: string): JSON => JSON.parse(filter)),
});
}
return filters[0];
}
function getPredefinedFilters(user: User): Record<string, string> {
const result: Record<string, string> = {};
if (user) {
for (const key of Object.keys(predefinedFilterValues)) {
result[key] = predefinedFilterValues[key].replace('<username>', `${user.username}`);
}
}
return result;
}
function ResourceFilterComponent(props: ResourceFilterProps): JSX.Element {
const {
predefinedVisible, builderVisible, recentVisible,
predefinedVisible, builderVisible, recentVisible, value,
onPredefinedVisibleChange, onBuilderVisibleChange, onRecentVisibleChange, onApplyFilter,
} = props;
const user = useSelector((state: CombinedState) => state.auth.user);
const [isMounted, setIsMounted] = useState<boolean>(false);
const [recentFilters, setRecentFilters] = useState<Record<string, string>>({});
const [predefinedFilters, setPredefinedFilters] = useState<Record<string, string>>({});
const [appliedFilter, setAppliedFilter] = useState<typeof defaultAppliedFilter>(defaultAppliedFilter);
const [appliedFilter, setAppliedFilter] = useState(defaultAppliedFilter);
const [state, setState] = useState<ImmutableTree>(defaultTree);
useEffect(() => {
setRecentFilters(receiveRecentFilters());
setIsMounted(true);
}, []);
useEffect(() => {
if (user) {
const result: Record<string, string> = {};
for (const key of Object.keys(predefinedFilterValues)) {
result[key] = predefinedFilterValues[key].replace('<username>', `${user.username}`);
const listener = (event: MouseEvent): void => {
const path: HTMLElement[] = event.composedPath()
.filter((el: EventTarget) => el instanceof HTMLElement) as HTMLElement[];
if (path.some((el: HTMLElement) => el.id === 'root') && !path.some((el: HTMLElement) => el.classList.contains('ant-btn'))) {
onBuilderVisibleChange(false);
onRecentVisibleChange(false);
}
};
setPredefinedFilters(result);
setAppliedFilter({
...appliedFilter,
predefined: defaultEnabledFilters
.filter((filterKey: string) => filterKey in result)
.map((filterKey: string) => result[filterKey]),
});
}
}, [user]);
useEffect(() => {
function unite(filters: string[]): string {
if (filters.length > 1) {
return JSON.stringify({
and: filters.map((filter: string): JSON => JSON.parse(filter)),
});
try {
if (value) {
const tree = QbUtils.loadFromJsonLogic(JSON.parse(value), config);
if (tree && isValidTree(tree)) {
setAppliedFilter({
...appliedFilter,
built: JSON.stringify(QbUtils.jsonLogicFormat(tree, config).logic),
});
setState(tree);
}
}
return filters[0];
} catch (_: any) {
// nothing to do
}
function isValidTree(tree: ImmutableTree): boolean {
return (QbUtils.queryString(tree, config) || '').trim().length > 0 && QbUtils.isValidTree(tree);
}
window.addEventListener('click', listener);
return () => window.removeEventListener('click', listener);
}, []);
useEffect(() => {
if (!isMounted) {
// do not request jobs before until on mount hook is done
// do not request resources until on mount hook is done
return;
}
@ -150,7 +170,9 @@ export default function ResourceFilterHOC(
setState(tree);
}
} else if (appliedFilter.built) {
onApplyFilter(appliedFilter.built);
if (value !== appliedFilter.built) {
onApplyFilter(appliedFilter.built);
}
} else {
onApplyFilter(null);
setState(defaultTree);
@ -165,14 +187,15 @@ export default function ResourceFilterHOC(
</div>
);
const predefinedFilters = getPredefinedFilters(user);
return (
<div className='cvat-jobs-page-filters'>
<div className='cvat-resource-page-filters'>
<Dropdown
destroyPopupOnHide
visible={predefinedVisible}
placement='bottomLeft'
overlay={(
<div className='cvat-jobs-page-predefined-filters-list'>
<div className='cvat-resource-page-predefined-filters-list'>
{Object.keys(predefinedFilters).map((key: string): JSX.Element => (
<Checkbox
checked={appliedFilter.predefined?.includes(predefinedFilters[key])}
@ -216,14 +239,14 @@ export default function ResourceFilterHOC(
visible={builderVisible}
destroyPopupOnHide
overlay={(
<div className='cvat-jobs-page-filters-builder'>
<div className='cvat-resource-page-filters-builder'>
{ Object.keys(recentFilters).length ? (
<Dropdown
placement='bottomRight'
visible={recentVisible}
destroyPopupOnHide
overlay={(
<div className='cvat-jobs-page-recent-filters-list'>
<div className='cvat-resource-page-recent-filters-list'>
<Menu selectable={false}>
{Object.keys(recentFilters).map((key: string): JSX.Element | null => {
const tree = QbUtils.loadFromJsonLogic(JSON.parse(key), config);
@ -275,7 +298,7 @@ export default function ResourceFilterHOC(
value={state}
renderBuilder={renderBuilder}
/>
<Space className='cvat-jobs-page-filters-space'>
<Space className='cvat-resource-page-filters-space'>
<Button
disabled={!QbUtils.queryString(state, config)}
size='small'

@ -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,
};

@ -78,7 +78,7 @@ const SortableList = SortableContainer(
appliedSorting: Record<string, string>;
setAppliedSorting: (arg: Record<string, string>) => void;
}) => (
<div className='cvat-jobs-page-sorting-list'>
<div className='cvat-resource-page-sorting-list'>
{ items.map((value: string, index: number) => (
<SortableItem
key={`item-${value}`}
@ -111,12 +111,28 @@ function SortingModalComponent(props: Props): JSX.Element {
return acc;
}, {}),
);
const [isMounted, setIsMounted] = useState<boolean>(false);
const [sortingFields, setSortingFields] = useState<string[]>(
Array.from(new Set([...Object.keys(appliedSorting), ANCHOR_KEYWORD, ...sortingFieldsProp])),
);
const [appliedOrder, setAppliedOrder] = useState<string[]>([...defaultFields]);
useEffect(() => {
setIsMounted(true);
const listener = (event: MouseEvent): void => {
const path: HTMLElement[] = event.composedPath()
.filter((el: EventTarget) => el instanceof HTMLElement) as HTMLElement[];
if (path.some((el: HTMLElement) => el.id === 'root') && !path.some((el: HTMLElement) => el.classList.contains('ant-btn'))) {
onVisibleChange(false);
}
};
window.addEventListener('click', listener);
return () => window.removeEventListener('click', listener);
}, []);
useEffect(() => {
if (!isMounted) return;
const anchorIdx = sortingFields.indexOf(ANCHOR_KEYWORD);
const appliedSortingCopy = { ...appliedSorting };
const slicedSortingFields = sortingFields.slice(0, anchorIdx);
@ -143,6 +159,7 @@ function SortingModalComponent(props: Props): JSX.Element {
// because we do not want the hook to be called after changing sortingField
// sortingField value is always relevant because if order changes, the hook before will be called first
if (!isMounted) return;
const anchorIdx = sortingFields.indexOf(ANCHOR_KEYWORD);
const sortingString = sortingFields.slice(0, anchorIdx)
.map((field: string): string => appliedSorting[field])

@ -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: ' - ';
}
}

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -6,35 +6,43 @@ import React from 'react';
import { Link } from 'react-router-dom';
import Text from 'antd/lib/typography/Text';
import { Row, Col } from 'antd/lib/grid';
import Icon from '@ant-design/icons';
import { EmptyTasksIcon } from 'icons';
import { TasksQuery } from 'reducers/interfaces';
import Empty from 'antd/lib/empty';
interface Props {
query: TasksQuery;
}
function EmptyListComponent(props: Props): JSX.Element {
const { query } = props;
export default function EmptyListComponent(): JSX.Element {
return (
<div className='cvat-empty-tasks-list'>
<Row justify='center' align='middle'>
<Col>
<Icon className='cvat-empty-tasks-icon' component={EmptyTasksIcon} />
</Col>
</Row>
<Row justify='center' align='middle'>
<Col>
<Text strong>No tasks created yet ...</Text>
</Col>
</Row>
<Row justify='center' align='middle'>
<Col>
<Text type='secondary'>To get started with your annotation project</Text>
</Col>
</Row>
<Row justify='center' align='middle'>
<Col>
<Link to='/tasks/create'>create a new task</Link>
<Text type='secondary'> or try to </Text>
<Link to='/projects/create'>create a new project</Link>
</Col>
</Row>
<Empty description={!query.filter && !query.search && !query.page ? (
<>
<Row justify='center' align='middle'>
<Col>
<Text strong>No tasks created yet ...</Text>
</Col>
</Row>
<Row justify='center' align='middle'>
<Col>
<Text type='secondary'>To get started with your annotation project</Text>
</Col>
</Row>
<Row justify='center' align='middle'>
<Col>
<Link to='/tasks/create'>create a new task</Link>
<Text type='secondary'> or try to </Text>
<Link to='/projects/create'>create a new project</Link>
</Col>
</Row>
</>
) : (<Text>No results matched your search</Text>)}
/>
</div>
);
}
export default React.memo(EmptyListComponent);

@ -5,6 +5,31 @@
@import '../../base.scss';
@import '../../styles.scss';
.cvat-tasks-page-control-buttons-wrapper {
display: flex;
flex-direction: column;
background: $background-color-1;
padding: $grid-unit-size;
border-radius: 4px;
box-shadow: $box-shadow-base;
> * {
&:not(:first-child) {
margin-top: $grid-unit-size;
}
width: 100%;
.ant-upload {
width: 100%;
button {
width: 100%;
}
}
}
}
.cvat-tasks-page {
padding-top: $grid-unit-size * 2;
padding-bottom: $grid-unit-size;
@ -12,14 +37,27 @@
width: 100%;
.cvat-tasks-page-top-bar {
> div:nth-child(1) {
> div:nth-child(1) {
> div:nth-child(1) {
display: flex;
> div {
display: flex;
justify-content: space-between;
> .cvat-tasks-page-filters-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
> .cvat-title {
> div {
> *:not(:last-child) {
margin-right: $grid-unit-size;
}
display: flex;
margin-right: $grid-unit-size * 4;
}
.cvat-tasks-page-search-bar {
width: $grid-unit-size * 32;
}
}
}
@ -47,27 +85,12 @@
}
}
/* empty-tasks icon */
.cvat-empty-tasks-list {
> div:nth-child(1) {
margin-top: 50px;
}
> div:nth-child(2) {
> div {
margin-top: 20px;
/* No tasks created yet */
> span {
font-size: 20px;
color: $text-color;
}
}
}
/* To get started with your annotation project .. */
> div:nth-child(3) {
margin-top: 10px;
.ant-empty {
top: 50%;
left: 50%;
position: absolute;
transform: translate(-50%, -50%);
}
}
@ -165,15 +188,3 @@
.cvat-item-task-name {
@extend .cvat-text-color;
}
#cvat-create-task-button {
padding: 0 30px;
}
#cvat-import-task-button {
padding: 0 30px;
}
#cvat-import-task-button-loading {
margin-left: 10;
}

@ -1,26 +1,20 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { Row, Col } from 'antd/lib/grid';
import Pagination from 'antd/lib/pagination';
import ModelRunnerModal from 'components/model-runner-modal/model-runner-dialog';
import MoveTaskModal from 'components/move-task-modal/move-task-modal';
import TaskItem from 'containers/tasks-page/task-item';
export interface ContentListProps {
onSwitchPage(page: number): void;
export interface Props {
currentTasksIndexes: number[];
currentPage: number;
numberOfTasks: number;
}
export default function TaskListComponent(props: ContentListProps): JSX.Element {
const {
currentTasksIndexes, numberOfTasks, currentPage, onSwitchPage,
} = props;
function TaskListComponent(props: Props): JSX.Element {
const { currentTasksIndexes } = props;
const taskViews = currentTasksIndexes.map((tid, id): JSX.Element => <TaskItem idx={id} taskID={tid} key={tid} />);
return (
@ -30,21 +24,13 @@ export default function TaskListComponent(props: ContentListProps): JSX.Element
{taskViews}
</Col>
</Row>
<Row justify='center' align='middle'>
<Col md={22} lg={18} xl={16} xxl={14}>
<Pagination
className='cvat-tasks-pagination'
onChange={onSwitchPage}
showSizeChanger={false}
total={numberOfTasks}
pageSize={10}
current={currentPage}
showQuickJumper
/>
</Col>
</Row>
<ModelRunnerModal />
<MoveTaskModal />
</>
);
}
export default React.memo(TaskListComponent, (prev: Props, cur: Props) => (
prev.currentTasksIndexes.length !== cur.currentTasksIndexes.length || prev.currentTasksIndexes
.some((val: number, idx: number) => val !== cur.currentTasksIndexes[idx])
));

@ -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,79 +1,130 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import React, { useState, useEffect } from 'react';
import { useHistory } from 'react-router';
import { Row, Col } from 'antd/lib/grid';
import Dropdown from 'antd/lib/dropdown';
import { PlusOutlined, UploadOutlined, LoadingOutlined } from '@ant-design/icons';
import Button from 'antd/lib/button';
import Text from 'antd/lib/typography/Text';
import Upload from 'antd/lib/upload';
import Input from 'antd/lib/input';
import SearchField from 'components/search-field/search-field';
import { SortingComponent, ResourceFilterHOC, defaultVisibility } from 'components/resource-sorting-filtering';
import { TasksQuery } from 'reducers/interfaces';
import { usePrevious } from 'utils/hooks';
import {
localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues, config,
} from './tasks-filter-configuration';
const FilteringComponent = ResourceFilterHOC(
config, localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues,
);
interface VisibleTopBarProps {
onSearch: (query: TasksQuery) => void;
onFileUpload(file: File): void;
onImportTask(file: File): void;
onApplyFilter(filter: string | null): void;
onApplySorting(sorting: string | null): void;
onApplySearch(search: string | null): void;
query: TasksQuery;
taskImporting: boolean;
importing: boolean;
}
export default function TopBarComponent(props: VisibleTopBarProps): JSX.Element {
const {
query, onSearch, onFileUpload, taskImporting,
importing, query, onApplyFilter, onApplySorting, onApplySearch, onImportTask,
} = props;
const [visibility, setVisibility] = useState(defaultVisibility);
const history = useHistory();
const prevImporting = usePrevious(importing);
useEffect(() => {
if (prevImporting && !importing) {
onApplyFilter(query.filter);
}
}, [importing]);
return (
<Row className='cvat-tasks-page-top-bar' justify='center' align='middle'>
<Col md={22} lg={18} xl={16} xxl={14}>
<Row justify='space-between' align='bottom'>
<Col>
<Text className='cvat-title'>Tasks</Text>
<SearchField instance='task' onSearch={onSearch} query={query} />
</Col>
<Col>
<Row gutter={8}>
<Col>
<div className='cvat-tasks-page-filters-wrapper'>
<Input.Search
enterButton
onSearch={(phrase: string) => {
onApplySearch(phrase);
}}
defaultValue={query.search || ''}
className='cvat-tasks-page-search-bar'
placeholder='Search ...'
/>
<div>
<SortingComponent
visible={visibility.sorting}
onVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, sorting: visible })
)}
defaultFields={query.sort?.split(',') || ['-ID']}
sortingFields={['ID', 'Owner', 'Status', 'Assignee', 'Updated date', 'Subset', 'Mode', 'Dimension', 'Project ID', 'Name', 'Project name']}
onApplySorting={onApplySorting}
/>
<FilteringComponent
value={query.filter}
predefinedVisible={visibility.predefined}
builderVisible={visibility.builder}
recentVisible={visibility.recent}
onPredefinedVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, predefined: visible })
)}
onBuilderVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, builder: visible })
)}
onRecentVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, builder: visibility.builder, recent: visible })
)}
onApplyFilter={onApplyFilter}
/>
</div>
</div>
<div>
<Dropdown
trigger={['click']}
overlay={(
<div className='cvat-tasks-page-control-buttons-wrapper'>
<Button
className='cvat-create-task-button'
type='primary'
onClick={(): void => history.push('/tasks/create')}
icon={<PlusOutlined />}
>
Create a new task
</Button>
<Upload
accept='.zip'
multiple={false}
showUploadList={false}
beforeUpload={(file: File): boolean => {
onFileUpload(file);
onImportTask(file);
return false;
}}
className='cvat-import-task'
>
<Button
size='large'
id='cvat-import-task-button'
className='cvat-import-task-button'
type='primary'
disabled={taskImporting}
disabled={importing}
icon={<UploadOutlined />}
>
Create from backup
{taskImporting && <LoadingOutlined id='cvat-import-task-button-loading' />}
{importing && <LoadingOutlined />}
</Button>
</Upload>
</Col>
<Col>
<Button
size='large'
id='cvat-create-task-button'
type='primary'
onClick={(): void => history.push('/tasks/create')}
icon={<PlusOutlined />}
>
Create new task
</Button>
</Col>
</Row>
</Col>
</Row>
</div>
)}
>
<Button type='primary' className='cvat-create-task-dropdown' icon={<PlusOutlined />} />
</Dropdown>
</div>
</Col>
</Row>
);

@ -1,14 +1,11 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux';
import { TasksState, TasksQuery, CombinedState } from 'reducers/interfaces';
import TasksListComponent from 'components/tasks-page/task-list';
import { getTasksAsync } from 'actions/tasks-actions';
interface StateToProps {
@ -19,10 +16,6 @@ interface DispatchToProps {
getTasks: (query: TasksQuery) => void;
}
interface OwnProps {
onSwitchPage: (page: number) => void;
}
function mapStateToProps(state: CombinedState): StateToProps {
return {
tasks: state.tasks,
@ -37,17 +30,14 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
};
}
type TasksListContainerProps = StateToProps & DispatchToProps & OwnProps;
type TasksListContainerProps = StateToProps & DispatchToProps;
function TasksListContainer(props: TasksListContainerProps): JSX.Element {
const { tasks, onSwitchPage } = props;
const { tasks } = props;
return (
<TasksListComponent
onSwitchPage={onSwitchPage}
currentTasksIndexes={tasks.current.map((task): number => task.instance.id)}
currentPage={tasks.gettingQuery.page}
numberOfTasks={tasks.count}
/>
);
}

@ -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);

@ -5,7 +5,6 @@
import React from 'react';
import SVGCVATLogo from './assets/cvat-logo.svg';
import SVGEmptyTasksIcon from './assets/empty-tasks-icon.svg';
import SVGCursorIcon from './assets/cursor-icon.svg';
import SVGMoveIcon from './assets/move-icon.svg';
import SVGRotateIcon from './assets/rotate-icon.svg';
@ -54,7 +53,6 @@ import SVGCVATGoogleCloudProvider from './assets/google-cloud.svg';
import SVGOpenVINO from './assets/openvino.svg';
export const CVATLogo = React.memo((): JSX.Element => <SVGCVATLogo />);
export const EmptyTasksIcon = React.memo((): JSX.Element => <SVGEmptyTasksIcon />);
export const CursorIcon = React.memo((): JSX.Element => <SVGCursorIcon />);
export const MoveIcon = React.memo((): JSX.Element => <SVGMoveIcon />);
export const RotateIcon = React.memo((): JSX.Element => <SVGRotateIcon />);

@ -17,13 +17,8 @@ const defaultState: CloudStoragesState = {
page: 1,
id: null,
search: null,
owner: null,
displayName: null,
description: null,
resource: null,
providerType: null,
credentialsType: null,
status: null,
sort: null,
filter: null,
},
activities: {
creates: {

@ -27,11 +27,8 @@ export interface ProjectsQuery {
page: number;
id: number | null;
search: string | null;
owner: string | null;
name: string | null;
status: string | null;
assignee: string | null;
[key: string]: string | boolean | number | null | undefined;
filter: string | null;
sort: string | null;
}
export interface Project {
@ -45,7 +42,7 @@ export interface ProjectsState {
count: number;
current: Project[];
gettingQuery: ProjectsQuery;
tasksGettingQuery: TasksQuery;
tasksGettingQuery: TasksQuery & { ordering: string };
activities: {
creates: {
id: null | number;
@ -65,14 +62,9 @@ export interface TasksQuery {
page: number;
id: number | null;
search: string | null;
owner: string | null;
assignee: string | null;
name: string | null;
status: string | null;
mode: string | null;
filter: string | null;
sort: string | null;
projectId: number | null;
[key: string]: string | number | null;
}
export interface Task {
@ -156,13 +148,8 @@ export interface CloudStoragesQuery {
page: number;
id: number | null;
search: string | null;
owner: string | null;
displayName: string | null;
description: string | null;
resource: string | null;
providerType: string | null;
credentialsType: string | null;
[key: string]: string | number | null | undefined;
sort: string | null;
filter: string | null;
}
interface CloudStorageAdditional {
@ -788,3 +775,7 @@ export enum DimensionType {
DIM_3D = '3d',
DIM_2D = '2d',
}
export interface Indexable {
[index: string]: any;
}

@ -12,6 +12,7 @@ const defaultState: JobsState = {
page: 1,
filter: null,
sort: null,
search: null,
},
current: [],
previews: [],

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -19,20 +19,15 @@ const defaultState: ProjectsState = {
page: 1,
id: null,
search: null,
owner: null,
assignee: null,
name: null,
status: null,
filter: null,
sort: null,
},
tasksGettingQuery: {
page: 1,
id: null,
search: null,
owner: null,
assignee: null,
name: null,
status: null,
mode: null,
filter: null,
sort: null,
projectId: null,
ordering: 'subset',
},

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -25,11 +25,8 @@ const defaultState: TasksState = {
page: 1,
id: null,
search: null,
owner: null,
assignee: null,
name: null,
status: null,
mode: null,
filter: null,
sort: null,
projectId: null,
},
activities: {
@ -59,7 +56,7 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
hideEmpty: true,
count: 0,
current: [],
gettingQuery: { ...action.payload.query },
gettingQuery: action.payload.updateQuery ? { ...action.payload.query } : state.gettingQuery,
};
case TasksActionTypes.GET_TASKS_SUCCESS: {
const combinedWithPreviews = action.payload.array.map(

@ -1,7 +1,7 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useRef, useEffect, useState } from 'react';
import { useRef, useEffect, useState } from 'react';
// eslint-disable-next-line import/prefer-default-export
export function usePrevious<T>(value: T): T | undefined {
@ -12,17 +12,6 @@ export function usePrevious<T>(value: T): T | undefined {
return ref.current;
}
export function useDidUpdateEffect(effect: React.EffectCallback, deps?: React.DependencyList): void {
const didMountRef = useRef(false);
useEffect(() => {
if (didMountRef.current) {
effect();
} else {
didMountRef.current = true;
}
}, deps);
}
export interface ICardHeightHOC {
numberOfRows: number;
paddings: number;

@ -256,7 +256,7 @@ class ProjectViewSet(viewsets.ModelViewSet):
# NOTE: The search_fields attribute should be a list of names of text
# type fields on the model,such as CharField or TextField
search_fields = ('name', 'owner', 'assignee', 'status')
filter_fields = list(search_fields) + ['id']
filter_fields = list(search_fields) + ['id', 'updated_date']
ordering_fields = filter_fields
ordering = "-id"
lookup_fields = {'owner': 'owner__username', 'assignee': 'assignee__username'}
@ -563,7 +563,7 @@ class TaskViewSet(UploadMixin, viewsets.ModelViewSet):
serializer_class = TaskSerializer
lookup_fields = {'project_name': 'project__name', 'owner': 'owner__username', 'assignee': 'assignee__username'}
search_fields = ('project_name', 'name', 'owner', 'status', 'assignee', 'subset', 'mode', 'dimension')
filter_fields = list(search_fields) + ['id', 'project_id']
filter_fields = list(search_fields) + ['id', 'project_id', 'updated_date']
ordering_fields = filter_fields
ordering = "-id"
iam_organization_field = 'organization'

@ -57,6 +57,7 @@ context('Creating a project by inserting labels from a task.', { browser: '!fire
it('Creating a project with copying labels from the task.', () => {
cy.goToProjectsList();
cy.get('.cvat-create-project-dropdown').click();
cy.get('.cvat-create-project-button').click();
cy.get('#name').type(projectName);
cy.contains('[role="tab"]', 'Raw').click();

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -95,8 +95,10 @@ context('Move a task between projects.', () => {
afterEach(() => {
cy.goToProjectsList();
cy.get('.cvat-spinner').should('not.exist');
cy.openProject(firtsProject.name);
cy.deleteProjectViaActions(firtsProject.name);
cy.get('.cvat-spinner').should('not.exist');
cy.openProject(secondProject.name);
cy.deleteProjectViaActions(secondProject.name);
});

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -44,7 +44,7 @@ context('Create more than one task per time when create from project.', () => {
describe(`Testing "Issue ${issueID}"`, () => {
it('Create more than one task per time from project.', () => {
cy.get('#cvat-create-task-button').click();
cy.get('.cvat-create-task-button').click();
createTask(taskName.firstTask);
createTask(taskName.secondTask);
});

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -27,7 +27,8 @@ context('Create a task with set an issue tracker.', () => {
cy.login();
cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount);
cy.createZipArchive(directoryToArchive, archivePath);
cy.get('#cvat-create-task-button').click();
cy.get('.cvat-create-task-dropdown').click();
cy.get('.cvat-create-task-button').click();
});
after(() => {

@ -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');
});
});
});

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -25,7 +25,8 @@ context('Try to create a task without necessary arguments.', () => {
cy.login();
cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount);
cy.createZipArchive(directoryToArchive, archivePath);
cy.get('#cvat-create-task-button').click();
cy.get('.cvat-create-task-dropdown').click();
cy.get('.cvat-create-task-button').click();
});
after(() => {

@ -13,7 +13,8 @@ context('Add/delete labels and attributes.', () => {
before(() => {
cy.visit('auth/login');
cy.login();
cy.get('#cvat-create-task-button').click();
cy.get('.cvat-create-task-dropdown').click();
cy.get('.cvat-create-task-button').click();
});
describe(`Testing "${labelName}"`, () => {

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -12,7 +12,8 @@ context('Changing a label name via label constructor.', () => {
before(() => {
cy.visit('auth/login');
cy.login();
cy.get('#cvat-create-task-button').click();
cy.get('.cvat-create-task-dropdown').click();
cy.get('.cvat-create-task-button').click();
});
describe(`Testing case "${caseId}"`, () => {

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -28,7 +28,8 @@ context('Try to create a task with an incorrect dataset repository.', () => {
cy.login();
cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount);
cy.createZipArchive(directoryToArchive, archivePath);
cy.get('#cvat-create-task-button').click();
cy.get('.cvat-create-task-dropdown').click();
cy.get('.cvat-create-task-button').click();
});
describe(`Testing "${labelName}"`, () => {

@ -94,6 +94,7 @@ context('Export, import an annotation task.', { browser: '!firefox' }, () => {
it('Import the task. Check id, labels, shape.', () => {
cy.intercept('POST', '/api/tasks/backup?**').as('importTask');
cy.get('.cvat-create-task-dropdown').click();
cy.get('.cvat-import-task').click().find('input[type=file]').attachFile(taskBackupArchiveFullName);
cy.wait('@importTask', { timeout: 5000 }).its('response.statusCode').should('equal', 202);
cy.wait('@importTask').its('response.statusCode').should('equal', 201);

@ -151,7 +151,8 @@ context('Cloud storage.', () => {
it('Check select files from "Cloud Storage" when creating a task.', () => {
cy.contains('.cvat-header-button', 'Tasks').click();
cy.get('#cvat-create-task-button').should('be.visible').click();
cy.get('.cvat-create-task-dropdown').click();
cy.get('.cvat-create-task-button').should('be.visible').click();
cy.get('.cvat-create-task-content').should('be.visible').within(() => {
cy.contains('[role="tab"]', 'Cloud Storage').click();
cy.get('#cloudStorageSelect').should('exist');

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -12,7 +12,8 @@ context('Connected file share.', () => {
const assetLocalPath = `cypress/integration/actions_tasks3/assets/case_${caseId}`;
function createOpenTaskWithShare() {
cy.get('#cvat-create-task-button').should('be.visible').click();
cy.get('.cvat-create-task-dropdown').click();
cy.get('.cvat-create-task-button').should('be.visible').click();
cy.get('#name').type(taskName);
cy.addNewLabel(labelName);
cy.contains('[role="tab"]', 'Connected file share').click();
@ -76,7 +77,7 @@ context('Connected file share.', () => {
expect(fileRenameCommand.code).to.be.eq(0);
},
);
cy.exec(`docker exec -i cvat /bin/bash -c "find ~/share -name "*.png" -type f"`).then(
cy.exec('docker exec -i cvat /bin/bash -c "find ~/share -name "*.png" -type f"').then(
(findFilesCommand) => {
// [image_case_107_2.png, image_case_107_3.png]
expect(findFilesCommand.stdout.split('\n').length).to.be.eq(2);

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -15,7 +15,8 @@ context('Create a task with files from remote sources.', () => {
before(() => {
cy.visit('auth/login');
cy.login();
cy.get('#cvat-create-task-button').click();
cy.get('.cvat-create-task-dropdown').click();
cy.get('.cvat-create-task-button').click();
});
after(() => {

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -40,7 +40,8 @@ context('Wrong attribute is removed in label constructor.', () => {
describe(`Testing issue "${issueId}"`, () => {
it('Open the create task page.', () => {
cy.get('#cvat-create-task-button').click({ force: true });
cy.get('.cvat-create-task-dropdown').click();
cy.get('.cvat-create-task-button').click({ force: true });
});
it('Go to Raw labels editor. Insert values.', () => {
cy.get('[role="tab"]').contains('Raw').click();

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -18,7 +18,7 @@ context('Displaying attached files when creating a task.', () => {
const archivePath = `cypress/fixtures/${archiveName}`;
const imagesFolder = `cypress/fixtures/${imageFileName}`;
const directoryToArchive = imagesFolder;
let imageListToAttach = [];
const imageListToAttach = [];
for (let i = 1; i <= imagesCount; i++) {
imageListToAttach.push(`${imageFileName}/${imageFileName}_${i}.png`);
}
@ -28,7 +28,8 @@ context('Displaying attached files when creating a task.', () => {
cy.login();
cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount);
cy.createZipArchive(directoryToArchive, archivePath);
cy.get('#cvat-create-task-button').click();
cy.get('.cvat-create-task-dropdown').click();
cy.get('.cvat-create-task-button').click();
});
describe(`Testing "${labelName}"`, () => {

@ -177,49 +177,55 @@ Cypress.Commands.add(
expectedResult = 'success',
projectSubsetFieldValue = 'Test',
) => {
cy.get('#cvat-create-task-button').click({ force: true });
cy.url().should('include', '/tasks/create');
cy.get('[id="name"]').type(taskName);
if (!forProject) {
cy.get('.cvat-constructor-viewer-new-item').click();
cy.get('[placeholder="Label name"]').type(labelName);
cy.get('.cvat-new-attribute-button').click();
cy.get('[placeholder="Name"]').type(attrName);
cy.get('.cvat-attribute-type-input').click();
cy.get('.cvat-attribute-type-input-text').click();
cy.get('[placeholder="Default value"]').type(textDefaultValue);
if (multiAttrParams) {
cy.updateAttributes(multiAttrParams);
cy.url().then(($url) => {
if (!$url.includes('projects')) {
cy.get('.cvat-create-task-dropdown').click();
}
cy.contains('button', 'Done').click();
} else {
if (attachToProject) {
cy.get('.cvat-project-search-field').click();
cy.get('.ant-select-dropdown')
.not('.ant-select-dropdown-hidden')
.within(() => {
cy.get(`.ant-select-item-option[title="${projectName}"]`).click();
});
cy.get('.cvat-create-task-button').click({ force: true });
cy.url().should('include', '/tasks/create');
cy.get('[id="name"]').type(taskName);
if (!forProject) {
cy.get('.cvat-constructor-viewer-new-item').click();
cy.get('[placeholder="Label name"]').type(labelName);
cy.get('.cvat-new-attribute-button').click();
cy.get('[placeholder="Name"]').type(attrName);
cy.get('.cvat-attribute-type-input').click();
cy.get('.cvat-attribute-type-input-text').click();
cy.get('[placeholder="Default value"]').type(textDefaultValue);
if (multiAttrParams) {
cy.updateAttributes(multiAttrParams);
}
cy.contains('button', 'Done').click();
} else {
if (attachToProject) {
cy.get('.cvat-project-search-field').click();
cy.get('.ant-select-dropdown')
.not('.ant-select-dropdown-hidden')
.within(() => {
cy.get(`.ant-select-item-option[title="${projectName}"]`).click();
});
}
cy.get('.cvat-project-search-field').within(() => {
cy.get('[type="search"]').should('have.value', projectName);
});
cy.get('.cvat-project-subset-field').type(projectSubsetFieldValue);
cy.get('.cvat-constructor-viewer-new-item').should('not.exist');
}
cy.get('.cvat-project-search-field').within(() => {
cy.get('[type="search"]').should('have.value', projectName);
});
cy.get('.cvat-project-subset-field').type(projectSubsetFieldValue);
cy.get('.cvat-constructor-viewer-new-item').should('not.exist');
}
cy.get('input[type="file"]').attachFile(image, { subjectType: 'drag-n-drop' });
if (advancedConfigurationParams) {
cy.advancedConfiguration(advancedConfigurationParams);
}
cy.contains('button', 'Submit').click();
if (expectedResult === 'success') {
cy.get('.cvat-notification-create-task-success').should('exist').find('[data-icon="close"]').click();
}
if (!forProject) {
cy.goToTaskList();
} else {
cy.goToProjectsList();
}
cy.get('input[type="file"]').attachFile(image, { subjectType: 'drag-n-drop' });
if (advancedConfigurationParams) {
cy.advancedConfiguration(advancedConfigurationParams);
}
cy.contains('button', 'Submit').click();
if (expectedResult === 'success') {
cy.get('.cvat-notification-create-task-success').should('exist').find('[data-icon="close"]').click();
}
if (!forProject) {
cy.goToTaskList();
} else {
cy.goToProjectsList();
}
});
},
);

@ -13,7 +13,8 @@ Cypress.Commands.add('goToProjectsList', () => {
Cypress.Commands.add(
'createProjects',
(projectName, labelName, attrName, textDefaultValue, multiAttrParams, expectedResult = 'success') => {
cy.get('#cvat-create-project-button').click();
cy.get('.cvat-create-project-dropdown').click();
cy.get('.cvat-create-project-button').click();
cy.get('#name').type(projectName);
cy.get('.cvat-constructor-viewer-new-item').click();
cy.get('[placeholder="Label name"]').type(labelName);
@ -140,6 +141,7 @@ Cypress.Commands.add('backupProject', (projectName) => {
Cypress.Commands.add('restoreProject', (archiveWithBackup) => {
cy.intercept('POST', '/api/projects/backup?**').as('restoreProject');
cy.get('.cvat-create-project-dropdown').click();
cy.get('.cvat-import-project').click().find('input[type=file]').attachFile(archiveWithBackup);
cy.wait('@restoreProject', { timeout: 5000 }).its('response.statusCode').should('equal', 202);
cy.wait('@restoreProject').its('response.statusCode').should('equal', 201);

Loading…
Cancel
Save