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 ## \[2.1.0] - Unreleased
### Added ### Added
- TDB - Advanced filtration and sorting for a list of tasks/projects/cloudstorages (<https://github.com/openvinotoolkit/cvat/pull/4403>)
### Changed ### Changed
- TDB - TDB

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

@ -1,6 +1,6 @@
{ {
"name": "cvat-core", "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", "description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js", "main": "babel.config.js",
"scripts": { "scripts": {

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

@ -87,16 +87,6 @@
return true; 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 { class FieldUpdateTrigger {
constructor() { constructor() {
let updatedFlags = {}; let updatedFlags = {};
@ -136,7 +126,6 @@
checkFilter, checkFilter,
checkObjectType, checkObjectType,
checkExclusiveFields, checkExclusiveFields,
camelToSnake,
FieldUpdateTrigger, FieldUpdateTrigger,
}; };
})(); })();

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

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

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

@ -5,7 +5,7 @@
import { Dispatch, ActionCreator } from 'redux'; import { Dispatch, ActionCreator } from 'redux';
import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
import { CloudStoragesQuery, CloudStorage } from 'reducers/interfaces'; import { CloudStoragesQuery, CloudStorage, Indexable } from 'reducers/interfaces';
const cvat = getCore(); const cvat = getCore();
@ -103,38 +103,14 @@ export type CloudStorageActions = ActionUnion<typeof cloudStoragesActions>;
export function getCloudStoragesAsync(query: Partial<CloudStoragesQuery>): ThunkAction { export function getCloudStoragesAsync(query: Partial<CloudStoragesQuery>): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { 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.getCloudStorages());
dispatch(cloudStoragesActions.updateCloudStoragesGettingQuery(query)); dispatch(cloudStoragesActions.updateCloudStoragesGettingQuery(query));
const filteredQuery = { ...query }; const filteredQuery = { ...query };
for (const key in filteredQuery) { for (const key in filteredQuery) {
if (filteredQuery[key] === null) { if ((filteredQuery as Indexable)[key] === null) {
delete filteredQuery[key]; 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; let result = null;

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -52,7 +52,7 @@ export const importDatasetAsync = (instance: any, format: string, file: File): T
} }
dispatch(importActions.importDatasetSuccess()); 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 { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
import { JobsQuery } from 'reducers/interfaces'; import { Indexable, JobsQuery } from 'reducers/interfaces';
const cvat = getCore(); const cvat = getCore();
@ -30,12 +30,13 @@ export type JobsActions = ActionUnion<typeof jobsActions>;
export const getJobsAsync = (query: JobsQuery): ThunkAction => async (dispatch) => { export const getJobsAsync = (query: JobsQuery): ThunkAction => async (dispatch) => {
try { try {
// Remove all keys with null values from the query // We remove all keys with null values from the query
const filteredQuery: Partial<JobsQuery> = { ...query }; const filteredQuery = { ...query };
if (filteredQuery.page === null) delete filteredQuery.page; for (const key of Object.keys(query)) {
if (filteredQuery.filter === null) delete filteredQuery.filter; if ((filteredQuery as Indexable)[key] === null) {
if (filteredQuery.sort === null) delete filteredQuery.sort; delete (filteredQuery as Indexable)[key];
if (filteredQuery.search === null) delete filteredQuery.search; }
}
dispatch(jobsActions.getJobs(filteredQuery)); dispatch(jobsActions.getJobs(filteredQuery));
const jobs = await cvat.jobs.get(filteredQuery); const jobs = await cvat.jobs.get(filteredQuery);

@ -5,7 +5,9 @@
import { Dispatch, ActionCreator } from 'redux'; import { Dispatch, ActionCreator } from 'redux';
import { ActionUnion, createAction, ThunkAction } from 'utils/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 { getTasksAsync } from 'actions/tasks-actions';
import { getCVATStore } from 'cvat-store'; import { getCVATStore } from 'cvat-store';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
@ -80,17 +82,19 @@ const projectActions = {
export type ProjectActions = ActionUnion<typeof projectActions>; export type ProjectActions = ActionUnion<typeof projectActions>;
export function getProjectTasksAsync(tasksQuery: Partial<TasksQuery> = {}): ThunkAction<void> { export function getProjectTasksAsync(tasksQuery: Partial<TasksQuery> = {}): ThunkAction<void> {
return (dispatch: ActionCreator<Dispatch>): void => { return (dispatch: ActionCreator<Dispatch>, getState: () => CombinedState): void => {
const store = getCVATStore(); const store = getCVATStore();
const state: CombinedState = store.getState(); const state: CombinedState = store.getState();
dispatch(projectActions.updateProjectsGettingQuery({}, tasksQuery)); dispatch(projectActions.updateProjectsGettingQuery(
getState().projects.gettingQuery,
tasksQuery,
));
const query: Partial<TasksQuery> = { const query: Partial<TasksQuery> = {
...state.projects.tasksGettingQuery, ...state.projects.tasksGettingQuery,
page: 1,
...tasksQuery, ...tasksQuery,
}; };
dispatch(getTasksAsync(query)); dispatch(getTasksAsync(query, false));
}; };
} }
@ -107,27 +111,11 @@ export function getProjectsAsync(
...query, ...query,
}; };
for (const key in filteredQuery) { for (const key of Object.keys(filteredQuery)) {
if (filteredQuery[key] === null || typeof filteredQuery[key] === 'undefined') { const value = (filteredQuery as Indexable)[key];
delete filteredQuery[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; let result = null;

@ -4,7 +4,7 @@
import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { AnyAction, Dispatch, ActionCreator } from 'redux';
import { ThunkAction } from 'redux-thunk'; import { ThunkAction } from 'redux-thunk';
import { TasksQuery, CombinedState } from 'reducers/interfaces'; import { TasksQuery, CombinedState, Indexable } from 'reducers/interfaces';
import { getCVATStore } from 'cvat-store'; import { getCVATStore } from 'cvat-store';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
import { getInferenceStatusAsync } from './models-actions'; import { getInferenceStatusAsync } from './models-actions';
@ -41,10 +41,11 @@ export enum TasksActionTypes {
SWITCH_MOVE_TASK_MODAL_VISIBLE = 'SWITCH_MOVE_TASK_MODAL_VISIBLE', SWITCH_MOVE_TASK_MODAL_VISIBLE = 'SWITCH_MOVE_TASK_MODAL_VISIBLE',
} }
function getTasks(query: TasksQuery): AnyAction { function getTasks(query: TasksQuery, updateQuery: boolean): AnyAction {
const action = { const action = {
type: TasksActionTypes.GET_TASKS, type: TasksActionTypes.GET_TASKS,
payload: { payload: {
updateQuery,
query, query,
}, },
}; };
@ -74,35 +75,18 @@ function getTasksFailed(error: any): AnyAction {
return action; 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> => { 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 }; const filteredQuery = { ...query };
for (const key in filteredQuery) { for (const key of Object.keys(query)) {
if (filteredQuery[key] === null) { if ((filteredQuery as Indexable)[key] === null) {
delete filteredQuery[key]; 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; let result = null;
try { try {
result = await cvat.tasks.get(filteredQuery); 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(() => '')); const promises = array.map((task): string => (task as any).frames.preview().catch(() => ''));
dispatch(getInferenceStatusAsync()); dispatch(getInferenceStatusAsync());
dispatch(getTasksSuccess(array, await Promise.all(promises), result.count)); 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 // SPDX-License-Identifier: MIT
import './styles.scss'; import './styles.scss';
import React, { useCallback, useEffect } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { useDispatch, useSelector } from 'react-redux';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import Spin from 'antd/lib/spin'; 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 { getCloudStoragesAsync } from 'actions/cloud-storage-actions';
import { updateHistoryFromQuery } from 'components/resource-sorting-filtering';
import CloudStoragesListComponent from './cloud-storages-list'; import CloudStoragesListComponent from './cloud-storages-list';
import EmptyCloudStorageListComponent from './empty-cloud-storages-list'; import EmptyCloudStorageListComponent from './empty-cloud-storages-list';
import TopBarComponent from './top-bar'; import TopBarComponent from './top-bar';
@ -18,21 +19,40 @@ import TopBarComponent from './top-bar';
export default function StoragesPageComponent(): JSX.Element { export default function StoragesPageComponent(): JSX.Element {
const dispatch = useDispatch(); const dispatch = useDispatch();
const history = useHistory(); const history = useHistory();
const { search } = history.location; const [isMounted, setIsMounted] = useState(false);
const totalCount = useSelector((state: CombinedState) => state.cloudStorages.count); 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 current = useSelector((state: CombinedState) => state.cloudStorages.current);
const query = useSelector((state: CombinedState) => state.cloudStorages.gettingQuery); const query = useSelector((state: CombinedState) => state.cloudStorages.gettingQuery);
const onSearch = useCallback(
(_query: CloudStoragesQuery) => { const queryParams = new URLSearchParams(history.location.search);
if (!isFetching) dispatch(getCloudStoragesAsync(_query)); const updatedQuery = { ...query };
}, for (const key of Object.keys(updatedQuery)) {
[isFetching], (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( const onChangePage = useCallback(
(page: number) => { (page: number) => {
if (!isFetching && page !== query.page) dispatch(getCloudStoragesAsync({ ...query, page })); if (!fetching && page !== query.page) {
dispatch(getCloudStoragesAsync({ ...query, page }));
}
}, },
[query], [query],
); );
@ -44,51 +64,9 @@ export default function StoragesPageComponent(): JSX.Element {
xxl: 16, xxl: 16,
}; };
useEffect(() => { const anySearch = Object.keys(query)
const searchParams = new URLSearchParams(); .some((value: string) => value !== 'page' && (query as any)[value] !== null);
for (const [key, value] of Object.entries(query)) { const content = current.length ? (
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>
);
}
return (
<Row className='cvat-cloud-storages-page' justify='center' align='top'>
<Col {...dimensions}>
<TopBarComponent query={query} onSearch={onSearch} />
{current.length ? (
<CloudStoragesListComponent <CloudStoragesListComponent
totalCount={totalCount} totalCount={totalCount}
page={query.page} page={query.page}
@ -96,8 +74,47 @@ export default function StoragesPageComponent(): JSX.Element {
onChangePage={onChangePage} onChangePage={onChangePage}
/> />
) : ( ) : (
<EmptyCloudStorageListComponent notFound={searchWasUsed} /> <EmptyCloudStorageListComponent notFound={anySearch} />
)} );
return (
<Row className='cvat-cloud-storages-page' justify='center' align='top'>
<Col {...dimensions}>
<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> </Col>
</Row> </Row>
); );

@ -26,16 +26,29 @@
} }
.cvat-cloud-storages-list-top-bar { .cvat-cloud-storages-list-top-bar {
> div:first-child { > div {
.cvat-title { display: flex;
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; margin-right: $grid-unit-size;
} }
display: flex; display: flex;
margin-right: $grid-unit-size * 4;
} }
> div:last-child { .cvat-cloudstorages-page-tasks-search-bar {
text-align: right; width: $grid-unit-size * 32;
}
}
} }
} }

@ -1,42 +1,92 @@
// Copyright (C) 2021 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React, { useState } from 'react';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
import Text from 'antd/lib/typography/Text';
import { PlusOutlined } from '@ant-design/icons'; import { PlusOutlined } from '@ant-design/icons';
import SearchField from 'components/search-field/search-field';
import { CloudStoragesQuery } from 'reducers/interfaces'; 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 { interface Props {
onSearch(query: CloudStoragesQuery): void; onApplyFilter(filter: string | null): void;
onApplySorting(sorting: string | null): void;
onApplySearch(search: string | null): void;
query: CloudStoragesQuery; query: CloudStoragesQuery;
} }
export default function StoragesTopBar(props: Props): JSX.Element { export default function StoragesTopBar(props: Props): JSX.Element {
const { onSearch, query } = props; const {
query, onApplyFilter, onApplySorting, onApplySearch,
} = props;
const history = useHistory(); const history = useHistory();
const [visibility, setVisibility] = useState(defaultVisibility);
return ( return (
<Row justify='space-between' align='middle' className='cvat-cloud-storages-list-top-bar'> <Row justify='space-between' align='middle' className='cvat-cloud-storages-list-top-bar'>
<Col md={11} lg={9} xl={9} xxl={9}> <Col span={24}>
<Text className='cvat-title'>Cloud Storages</Text> <div className='cvat-cloudstorages-page-filters-wrapper'>
<SearchField instance='cloudstorage' onSearch={onSearch} query={query} /> <Input.Search
</Col> enterButton
<Col md={{ span: 11 }} lg={{ span: 9 }} xl={{ span: 9 }} xxl={{ span: 9 }}> 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 <Button
size='large' size='large'
className='cvat-attach-cloud-storage-button' className='cvat-attach-cloud-storage-button'
type='primary' type='primary'
onClick={(): void => history.push('/cloudstorages/create')} onClick={(): void => history.push('/cloudstorages/create')}
icon={<PlusOutlined />} icon={<PlusOutlined />}
> />
Attach a new storage
</Button>
</Col> </Col>
</Row> </Row>
); );

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

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

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

@ -1,23 +1,20 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React from 'react';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import Icon from '@ant-design/icons'; import Empty from 'antd/lib/empty';
import consts from 'consts'; import consts from 'consts';
import { EmptyTasksIcon as EmptyModelsIcon } from 'icons';
export default function EmptyListComponent(): JSX.Element { export default function EmptyListComponent(): JSX.Element {
return ( return (
<div className='cvat-empty-models-list'> <Empty
<Row justify='center' align='middle'> className='cvat-empty-models-list'
<Col> description={(
<Icon className='cvat-empty-models-icon' component={EmptyModelsIcon} /> <div>
</Col>
</Row>
<Row justify='center' align='middle'> <Row justify='center' align='middle'>
<Col> <Col>
<Text strong>No models deployed yet...</Text> <Text strong>No models deployed yet...</Text>
@ -35,5 +32,7 @@ export default function EmptyListComponent(): JSX.Element {
</Col> </Col>
</Row> </Row>
</div> </div>
)}
/>
); );
} }

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

@ -13,39 +13,15 @@
width: 100%; width: 100%;
> div:nth-child(1) { > div:nth-child(1) {
margin-bottom: 10px; margin-bottom: $grid-unit-size;
> div:nth-child(1) {
display: flex;
}
> div:nth-child(2) {
display: flex;
justify-content: flex-end;
}
} }
} }
.cvat-empty-models-list { .cvat-empty-models-list {
/* empty-models icon */ position: absolute;
> div:nth-child(1) { top: 50%;
margin-top: 50px; left: 50%;
} transform: translate(-50%, -50%);
/* 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;
}
} }
.cvat-models-list { .cvat-models-list {
@ -58,8 +34,8 @@
height: auto; height: auto;
border: 1px solid $border-color-1; border: 1px solid $border-color-1;
border-radius: 3px; border-radius: 3px;
margin-bottom: 15px; margin-bottom: $grid-unit-size * 2;
padding: 15px; padding: $grid-unit-size * 2;
background: $background-color-1; background: $background-color-1;
&:hover { &:hover {
@ -83,7 +59,3 @@
overflow: hidden; 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 // SPDX-License-Identifier: MIT
import './styles.scss'; import './styles.scss';
import React, { useEffect, useCallback } from 'react'; import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux'; 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 Spin from 'antd/lib/spin';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import Result from 'antd/lib/result'; import Result from 'antd/lib/result';
@ -13,19 +13,30 @@ import Button from 'antd/lib/button';
import Title from 'antd/lib/typography/Title'; import Title from 'antd/lib/typography/Title';
import Pagination from 'antd/lib/pagination'; import Pagination from 'antd/lib/pagination';
import { PlusOutlined } from '@ant-design/icons'; 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 { getProjectsAsync, getProjectTasksAsync } from 'actions/projects-actions';
import { cancelInferenceAsync } from 'actions/models-actions'; import { cancelInferenceAsync } from 'actions/models-actions';
import TaskItem from 'components/tasks-page/task-item'; 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 MoveTaskModal from 'components/move-task-modal/move-task-modal';
import ModelRunnerDialog from 'components/model-runner-modal/model-runner-dialog'; import ModelRunnerDialog from 'components/model-runner-modal/model-runner-dialog';
import ImportDatasetModal from 'components/import-dataset-modal/import-dataset-modal'; 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 DetailsComponent from './details';
import ProjectTopBar from './top-bar'; import ProjectTopBar from './top-bar';
import {
localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues, config,
} from './project-tasks-filter-configuration';
const FilteringComponent = ResourceFilterHOC(
config, localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues,
);
interface ParamType { interface ParamType {
id: string; id: string;
} }
@ -34,7 +45,6 @@ export default function ProjectPageComponent(): JSX.Element {
const id = +useParams<ParamType>().id; const id = +useParams<ParamType>().id;
const dispatch = useDispatch(); const dispatch = useDispatch();
const history = useHistory(); const history = useHistory();
const { search } = useLocation();
const projects = useSelector((state: CombinedState) => state.projects.current).map((project) => project.instance); const projects = useSelector((state: CombinedState) => state.projects.current).map((project) => project.instance);
const projectsFetching = useSelector((state: CombinedState) => state.projects.fetching); const projectsFetching = useSelector((state: CombinedState) => state.projects.fetching);
const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes); 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 tasksActiveInferences = useSelector((state: CombinedState) => state.models.inferences);
const tasks = useSelector((state: CombinedState) => state.tasks.current); const tasks = useSelector((state: CombinedState) => state.tasks.current);
const tasksCount = useSelector((state: CombinedState) => state.tasks.count); 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 [project] = projects.filter((_project) => _project.id === id);
const projectSubsets: Array<string> = []; 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); 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(() => { useEffect(() => {
const searchParams: Partial<TasksQuery> = {}; if (!project) {
for (const [param, value] of new URLSearchParams(search)) { dispatch(getProjectsAsync({ id }, updatedQuery));
searchParams[param] = ['page'].includes(param) ? Number.parseInt(value, 10) : value;
} }
dispatch(getProjectsAsync({ id }, searchParams));
}, []); }, []);
useDidUpdateEffect(() => { useEffect(() => {
const searchParams = new URLSearchParams(); if (isMounted) {
for (const [name, value] of Object.entries(tasksGettingQuery)) { history.replace({
if (value !== null && typeof value !== 'undefined' && !['projectId', 'ordering'].includes(name)) { search: updateHistoryFromQuery(tasksQuery),
searchParams.append(name, value.toString());
}
}
history.push({
pathname: `/projects/${id}`,
search: `?${searchParams.toString()}`,
}); });
}, [tasksGettingQuery, id]); }
}, [tasksQuery]);
if (deleteActivity) { useEffect(() => {
if (project && id in deletes && deletes[id]) {
history.push('/projects'); history.push('/projects');
} }
}, [deletes]);
if (projectsFetching) { if (projectsFetching) {
return <Spin size='large' className='cvat-spinner' />; return <Spin size='large' className='cvat-spinner' />;
@ -102,40 +112,8 @@ export default function ProjectPageComponent(): JSX.Element {
); );
} }
const paginationDimensions = { const content = tasksCount ? (
md: 22, <>
lg: 18,
xl: 16,
xxl: 16,
};
return (
<Row justify='center' align='top' className='cvat-project-page'>
<Col md={22} lg={18} xl={16} xxl={14}>
<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>
<Button
size='large'
type='primary'
icon={<PlusOutlined />}
id='cvat-create-task-button'
onClick={() => history.push(`/tasks/create?projectId=${id}`)}
>
Create new task
</Button>
</Col>
</Row>
{projectSubsets.map((subset: string) => ( {projectSubsets.map((subset: string) => (
<React.Fragment key={subset}> <React.Fragment key={subset}>
{subset && <Title level={4}>{subset}</Title>} {subset && <Title level={4}>{subset}</Title>}
@ -156,20 +134,111 @@ export default function ProjectPageComponent(): JSX.Element {
))} ))}
</React.Fragment> </React.Fragment>
))} ))}
<Row justify='center'> <Row justify='center' align='middle'>
<Col {...paginationDimensions}> <Col md={22} lg={18} xl={16} xxl={14}>
<Pagination <Pagination
className='cvat-project-tasks-pagination' className='cvat-project-tasks-pagination'
onChange={onPageChange} onChange={(page: number) => {
dispatch(getProjectTasksAsync({
...tasksQuery,
projectId: id,
page,
}));
}}
showSizeChanger={false} showSizeChanger={false}
total={tasksCount} total={tasksCount}
pageSize={10} pageSize={10}
current={tasksGettingQuery.page} current={tasksQuery.page}
showQuickJumper showQuickJumper
/> />
</Col> </Col>
</Row> </Row>
</>
) : (
<Empty description='No tasks found' />
);
return (
<Row justify='center' align='top' className='cvat-project-page'>
<Col md={22} lg={18} xl={16} xxl={14}>
<ProjectTopBar projectInstance={project} />
<DetailsComponent project={project} />
<Row justify='space-between' align='middle' className='cvat-project-page-tasks-bar'>
<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
type='primary'
icon={<PlusOutlined />}
className='cvat-create-task-button'
onClick={() => history.push(`/tasks/create?projectId=${id}`)}
/>
</Col>
</Row>
{ tasksFetching ? (
<Spin size='large' className='cvat-spinner' />
) : content }
</Col> </Col>
<MoveTaskModal /> <MoveTaskModal />
<ModelRunnerDialog /> <ModelRunnerDialog />
<ImportDatasetModal /> <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 { .cvat-project-page {
overflow-y: auto; overflow-y: auto;
height: 100%; 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 { .cvat-project-details {
@ -38,10 +71,6 @@
} }
} }
.cvat-project-page-tasks-bar {
margin: $grid-unit-size * 2 0;
}
.ant-menu.cvat-project-actions-menu { .ant-menu.cvat-project-actions-menu {
box-shadow: 0 0 17px rgba(0, 0, 0, 0.2); box-shadow: 0 0 17px rgba(0, 0, 0, 0.2);
@ -65,11 +94,3 @@
display: flex; display: flex;
justify-content: center; 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 // SPDX-License-Identifier: MIT
@ -6,29 +6,18 @@ import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import Icon from '@ant-design/icons'; import Empty from 'antd/lib/empty';
import { EmptyTasksIcon } from 'icons';
interface Props { interface Props {
notFound?: boolean; notFound: boolean;
} }
export default function EmptyListComponent(props: Props): JSX.Element { export default function EmptyListComponent(props: Props): JSX.Element {
const { notFound } = props; const { notFound } = props;
return ( return (
<div className='cvat-empty-projects-list'> <div className='cvat-empty-projects-list'>
<Row justify='center' align='middle'> <Empty description={notFound ? (
<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> <Text strong>No results matched your search...</Text>
</Col>
</Row>
) : ( ) : (
<> <>
<Row justify='center' align='middle'> <Row justify='center' align='middle'>
@ -48,6 +37,7 @@ export default function EmptyListComponent(props: Props): JSX.Element {
</Row> </Row>
</> </>
)} )}
/>
</div> </div>
); );
} }

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

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

@ -2,78 +2,134 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import Button from 'antd/lib/button'; 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 { PlusOutlined, UploadOutlined, LoadingOutlined } from '@ant-design/icons';
import Upload from 'antd/lib/upload'; import Upload from 'antd/lib/upload';
import SearchField from 'components/search-field/search-field'; import { usePrevious } from 'utils/hooks';
import { CombinedState, ProjectsQuery } from 'reducers/interfaces'; import { ProjectsQuery } from 'reducers/interfaces';
import { getProjectsAsync, restoreProjectAsync } from 'actions/projects-actions'; 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 history = useHistory();
const dispatch = useDispatch();
const query = useSelector((state: CombinedState) => state.projects.gettingQuery);
const isImporting = useSelector((state: CombinedState) => state.projects.restoring);
return ( return (
<Row className='cvat-projects-page-top-bar' justify='center' align='middle'> <Row className='cvat-projects-page-top-bar' justify='center' align='middle'>
<Col md={22} lg={18} xl={16} xxl={16}> <Col md={22} lg={18} xl={16} xxl={16}>
<Row justify='space-between' align='bottom'> <div className='cvat-projects-page-filters-wrapper'>
<Col> <Input.Search
<Text className='cvat-title'>Projects</Text> enterButton
<SearchField onSearch={(phrase: string) => {
query={query} onApplySearch(phrase);
instance='project' }}
onSearch={(_query: ProjectsQuery) => dispatch(getProjectsAsync(_query))} defaultValue={query.search || ''}
className='cvat-projects-page-search-bar'
placeholder='Search ...'
/> />
</Col> <div>
<Col> <SortingComponent
<Row gutter={8}> visible={visibility.sorting}
<Col> 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}
/>
</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 <Upload
accept='.zip' accept='.zip'
multiple={false} multiple={false}
showUploadList={false} showUploadList={false}
beforeUpload={(file: File): boolean => { beforeUpload={(file: File): boolean => {
dispatch(restoreProjectAsync(file)); onImportProject(file);
return false; return false;
}} }}
className='cvat-import-project' className='cvat-import-project'
> >
<Button <Button
size='large' className='cvat-import-project-button'
id='cvat-import-project-button'
type='primary' type='primary'
disabled={isImporting} disabled={importing}
icon={<UploadOutlined />} icon={<UploadOutlined />}
> >
Create from backup Create from backup
{isImporting && <LoadingOutlined id='cvat-import-project-button-loading' />} {importing && <LoadingOutlined className='cvat-import-project-button-loading' />}
</Button> </Button>
</Upload> </Upload>
</Col> </div>
<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 type='primary' className='cvat-create-project-dropdown' icon={<PlusOutlined />} />
</Button> </Dropdown>
</Col> </div>
</Row>
</Col>
</Row>
</Col> </Col>
</Row> </Row>
); );
} }
export default React.memo(TopBarComponent);

@ -14,16 +14,17 @@ import {
import Dropdown from 'antd/lib/dropdown'; import Dropdown from 'antd/lib/dropdown';
import Space from 'antd/lib/space'; import Space from 'antd/lib/space';
import Button from 'antd/lib/button'; 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 Checkbox, { CheckboxChangeEvent } from 'antd/lib/checkbox/Checkbox';
import Menu from 'antd/lib/menu'; 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 { interface ResourceFilterProps {
predefinedVisible: boolean; predefinedVisible: boolean;
recentVisible: boolean; recentVisible: boolean;
builderVisible: boolean; builderVisible: boolean;
value: string | null;
onPredefinedVisibleChange(visible: boolean): void; onPredefinedVisibleChange(visible: boolean): void;
onBuilderVisibleChange(visible: boolean): void; onBuilderVisibleChange(visible: boolean): void;
onRecentVisibleChange(visible: boolean): void; onRecentVisibleChange(visible: boolean): void;
@ -35,7 +36,6 @@ export default function ResourceFilterHOC(
localStorageRecentKeyword: string, localStorageRecentKeyword: string,
localStorageRecentCapacity: number, localStorageRecentCapacity: number,
predefinedFilterValues: Record<string, string>, predefinedFilterValues: Record<string, string>,
defaultEnabledFilters: string[],
): React.FunctionComponent<ResourceFilterProps> { ): React.FunctionComponent<ResourceFilterProps> {
const config: Config = { ...AntdConfig, ...filtrationCfg }; const config: Config = { ...AntdConfig, ...filtrationCfg };
const defaultTree = QbUtils.checkTree( const defaultTree = QbUtils.checkTree(
@ -86,58 +86,78 @@ export default function ResourceFilterHOC(
built: null, 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 { function ResourceFilterComponent(props: ResourceFilterProps): JSX.Element {
const { const {
predefinedVisible, builderVisible, recentVisible, predefinedVisible, builderVisible, recentVisible, value,
onPredefinedVisibleChange, onBuilderVisibleChange, onRecentVisibleChange, onApplyFilter, onPredefinedVisibleChange, onBuilderVisibleChange, onRecentVisibleChange, onApplyFilter,
} = props; } = props;
const user = useSelector((state: CombinedState) => state.auth.user); const user = useSelector((state: CombinedState) => state.auth.user);
const [isMounted, setIsMounted] = useState<boolean>(false); const [isMounted, setIsMounted] = useState<boolean>(false);
const [recentFilters, setRecentFilters] = useState<Record<string, string>>({}); const [recentFilters, setRecentFilters] = useState<Record<string, string>>({});
const [predefinedFilters, setPredefinedFilters] = useState<Record<string, string>>({}); const [appliedFilter, setAppliedFilter] = useState(defaultAppliedFilter);
const [appliedFilter, setAppliedFilter] = useState<typeof defaultAppliedFilter>(defaultAppliedFilter);
const [state, setState] = useState<ImmutableTree>(defaultTree); const [state, setState] = useState<ImmutableTree>(defaultTree);
useEffect(() => { useEffect(() => {
setRecentFilters(receiveRecentFilters()); setRecentFilters(receiveRecentFilters());
setIsMounted(true); setIsMounted(true);
}, []);
useEffect(() => { const listener = (event: MouseEvent): void => {
if (user) { const path: HTMLElement[] = event.composedPath()
const result: Record<string, string> = {}; .filter((el: EventTarget) => el instanceof HTMLElement) as HTMLElement[];
for (const key of Object.keys(predefinedFilterValues)) { if (path.some((el: HTMLElement) => el.id === 'root') && !path.some((el: HTMLElement) => el.classList.contains('ant-btn'))) {
result[key] = predefinedFilterValues[key].replace('<username>', `${user.username}`); onBuilderVisibleChange(false);
onRecentVisibleChange(false);
} }
};
setPredefinedFilters(result); try {
if (value) {
const tree = QbUtils.loadFromJsonLogic(JSON.parse(value), config);
if (tree && isValidTree(tree)) {
setAppliedFilter({ setAppliedFilter({
...appliedFilter, ...appliedFilter,
predefined: defaultEnabledFilters built: JSON.stringify(QbUtils.jsonLogicFormat(tree, config).logic),
.filter((filterKey: string) => filterKey in result)
.map((filterKey: string) => result[filterKey]),
}); });
setState(tree);
} }
}, [user]);
useEffect(() => {
function unite(filters: string[]): string {
if (filters.length > 1) {
return JSON.stringify({
and: filters.map((filter: string): JSON => JSON.parse(filter)),
});
} }
} catch (_: any) {
return filters[0]; // nothing to do
} }
function isValidTree(tree: ImmutableTree): boolean { window.addEventListener('click', listener);
return (QbUtils.queryString(tree, config) || '').trim().length > 0 && QbUtils.isValidTree(tree); return () => window.removeEventListener('click', listener);
} }, []);
useEffect(() => {
if (!isMounted) { if (!isMounted) {
// do not request jobs before until on mount hook is done // do not request resources until on mount hook is done
return; return;
} }
@ -150,7 +170,9 @@ export default function ResourceFilterHOC(
setState(tree); setState(tree);
} }
} else if (appliedFilter.built) { } else if (appliedFilter.built) {
if (value !== appliedFilter.built) {
onApplyFilter(appliedFilter.built); onApplyFilter(appliedFilter.built);
}
} else { } else {
onApplyFilter(null); onApplyFilter(null);
setState(defaultTree); setState(defaultTree);
@ -165,14 +187,15 @@ export default function ResourceFilterHOC(
</div> </div>
); );
const predefinedFilters = getPredefinedFilters(user);
return ( return (
<div className='cvat-jobs-page-filters'> <div className='cvat-resource-page-filters'>
<Dropdown <Dropdown
destroyPopupOnHide destroyPopupOnHide
visible={predefinedVisible} visible={predefinedVisible}
placement='bottomLeft' placement='bottomLeft'
overlay={( 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 => ( {Object.keys(predefinedFilters).map((key: string): JSX.Element => (
<Checkbox <Checkbox
checked={appliedFilter.predefined?.includes(predefinedFilters[key])} checked={appliedFilter.predefined?.includes(predefinedFilters[key])}
@ -216,14 +239,14 @@ export default function ResourceFilterHOC(
visible={builderVisible} visible={builderVisible}
destroyPopupOnHide destroyPopupOnHide
overlay={( overlay={(
<div className='cvat-jobs-page-filters-builder'> <div className='cvat-resource-page-filters-builder'>
{ Object.keys(recentFilters).length ? ( { Object.keys(recentFilters).length ? (
<Dropdown <Dropdown
placement='bottomRight' placement='bottomRight'
visible={recentVisible} visible={recentVisible}
destroyPopupOnHide destroyPopupOnHide
overlay={( overlay={(
<div className='cvat-jobs-page-recent-filters-list'> <div className='cvat-resource-page-recent-filters-list'>
<Menu selectable={false}> <Menu selectable={false}>
{Object.keys(recentFilters).map((key: string): JSX.Element | null => { {Object.keys(recentFilters).map((key: string): JSX.Element | null => {
const tree = QbUtils.loadFromJsonLogic(JSON.parse(key), config); const tree = QbUtils.loadFromJsonLogic(JSON.parse(key), config);
@ -275,7 +298,7 @@ export default function ResourceFilterHOC(
value={state} value={state}
renderBuilder={renderBuilder} renderBuilder={renderBuilder}
/> />
<Space className='cvat-jobs-page-filters-space'> <Space className='cvat-resource-page-filters-space'>
<Button <Button
disabled={!QbUtils.queryString(state, config)} disabled={!QbUtils.queryString(state, config)}
size='small' 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>; appliedSorting: Record<string, string>;
setAppliedSorting: (arg: Record<string, string>) => void; 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) => ( { items.map((value: string, index: number) => (
<SortableItem <SortableItem
key={`item-${value}`} key={`item-${value}`}
@ -111,12 +111,28 @@ function SortingModalComponent(props: Props): JSX.Element {
return acc; return acc;
}, {}), }, {}),
); );
const [isMounted, setIsMounted] = useState<boolean>(false);
const [sortingFields, setSortingFields] = useState<string[]>( const [sortingFields, setSortingFields] = useState<string[]>(
Array.from(new Set([...Object.keys(appliedSorting), ANCHOR_KEYWORD, ...sortingFieldsProp])), Array.from(new Set([...Object.keys(appliedSorting), ANCHOR_KEYWORD, ...sortingFieldsProp])),
); );
const [appliedOrder, setAppliedOrder] = useState<string[]>([...defaultFields]); const [appliedOrder, setAppliedOrder] = useState<string[]>([...defaultFields]);
useEffect(() => { 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 anchorIdx = sortingFields.indexOf(ANCHOR_KEYWORD);
const appliedSortingCopy = { ...appliedSorting }; const appliedSortingCopy = { ...appliedSorting };
const slicedSortingFields = sortingFields.slice(0, anchorIdx); 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 // 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 // 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 anchorIdx = sortingFields.indexOf(ANCHOR_KEYWORD);
const sortingString = sortingFields.slice(0, anchorIdx) const sortingString = sortingFields.slice(0, anchorIdx)
.map((field: string): string => appliedSorting[field]) .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 // SPDX-License-Identifier: MIT
@ -6,18 +6,21 @@ import React from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { Row, Col } from 'antd/lib/grid'; 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 ( return (
<div className='cvat-empty-tasks-list'> <div className='cvat-empty-tasks-list'>
<Row justify='center' align='middle'> <Empty description={!query.filter && !query.search && !query.page ? (
<Col> <>
<Icon className='cvat-empty-tasks-icon' component={EmptyTasksIcon} />
</Col>
</Row>
<Row justify='center' align='middle'> <Row justify='center' align='middle'>
<Col> <Col>
<Text strong>No tasks created yet ...</Text> <Text strong>No tasks created yet ...</Text>
@ -35,6 +38,11 @@ export default function EmptyListComponent(): JSX.Element {
<Link to='/projects/create'>create a new project</Link> <Link to='/projects/create'>create a new project</Link>
</Col> </Col>
</Row> </Row>
</>
) : (<Text>No results matched your search</Text>)}
/>
</div> </div>
); );
} }
export default React.memo(EmptyListComponent);

@ -5,6 +5,31 @@
@import '../../base.scss'; @import '../../base.scss';
@import '../../styles.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 { .cvat-tasks-page {
padding-top: $grid-unit-size * 2; padding-top: $grid-unit-size * 2;
padding-bottom: $grid-unit-size; padding-bottom: $grid-unit-size;
@ -12,14 +37,27 @@
width: 100%; width: 100%;
.cvat-tasks-page-top-bar { .cvat-tasks-page-top-bar {
> div:nth-child(1) { > div {
> div:nth-child(1) { display: flex;
> div:nth-child(1) { justify-content: space-between;
> .cvat-tasks-page-filters-wrapper {
display: flex; display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
> .cvat-title { > div {
> *:not(:last-child) {
margin-right: $grid-unit-size; 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 { .cvat-empty-tasks-list {
> div:nth-child(1) { .ant-empty {
margin-top: 50px; top: 50%;
} left: 50%;
position: absolute;
> div:nth-child(2) { transform: translate(-50%, -50%);
> 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;
} }
} }
@ -165,15 +188,3 @@
.cvat-item-task-name { .cvat-item-task-name {
@extend .cvat-text-color; @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 // SPDX-License-Identifier: MIT
import React from 'react'; import React from 'react';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import Pagination from 'antd/lib/pagination';
import ModelRunnerModal from 'components/model-runner-modal/model-runner-dialog'; import ModelRunnerModal from 'components/model-runner-modal/model-runner-dialog';
import MoveTaskModal from 'components/move-task-modal/move-task-modal'; import MoveTaskModal from 'components/move-task-modal/move-task-modal';
import TaskItem from 'containers/tasks-page/task-item'; import TaskItem from 'containers/tasks-page/task-item';
export interface ContentListProps { export interface Props {
onSwitchPage(page: number): void;
currentTasksIndexes: number[]; currentTasksIndexes: number[];
currentPage: number;
numberOfTasks: number;
} }
export default function TaskListComponent(props: ContentListProps): JSX.Element { function TaskListComponent(props: Props): JSX.Element {
const { const { currentTasksIndexes } = props;
currentTasksIndexes, numberOfTasks, currentPage, onSwitchPage,
} = props;
const taskViews = currentTasksIndexes.map((tid, id): JSX.Element => <TaskItem idx={id} taskID={tid} key={tid} />); const taskViews = currentTasksIndexes.map((tid, id): JSX.Element => <TaskItem idx={id} taskID={tid} key={tid} />);
return ( return (
@ -30,21 +24,13 @@ export default function TaskListComponent(props: ContentListProps): JSX.Element
{taskViews} {taskViews}
</Col> </Col>
</Row> </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 /> <ModelRunnerModal />
<MoveTaskModal /> <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,98 +1,76 @@
// Copyright (C) 2020-2021 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import './styles.scss'; import './styles.scss';
import React from 'react'; import { useDispatch } from 'react-redux';
import { RouteComponentProps } from 'react-router'; import React, { useEffect, useState } from 'react';
import { withRouter } from 'react-router-dom'; import { useHistory } from 'react-router';
import Spin from 'antd/lib/spin'; import Spin from 'antd/lib/spin';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
import message from 'antd/lib/message'; import message from 'antd/lib/message';
import Text from 'antd/lib/typography/Text'; 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 FeedbackComponent from 'components/feedback/feedback';
import { updateHistoryFromQuery } from 'components/resource-sorting-filtering';
import TaskListContainer from 'containers/tasks-page/tasks-list'; import TaskListContainer from 'containers/tasks-page/tasks-list';
import { getTasksAsync, hideEmptyTasks, importTaskAsync } from 'actions/tasks-actions';
import TopBar from './top-bar'; import TopBar from './top-bar';
import EmptyListComponent from './empty-list'; import EmptyListComponent from './empty-list';
interface TasksPageProps { interface Props {
tasksFetching: boolean; fetching: boolean;
gettingQuery: TasksQuery; importing: boolean;
numberOfTasks: number; query: TasksQuery;
numberOfVisibleTasks: number; count: number;
numberOfHiddenTasks: number; countInvisible: number;
onGetTasks: (gettingQuery: TasksQuery) => void;
hideEmptyTasks: (hideEmpty: boolean) => void;
onImportTask: (file: File) => void;
taskImporting: boolean;
} }
function updateQuery(previousQuery: TasksQuery, searchString: string): TasksQuery { function TasksPageComponent(props: Props): JSX.Element {
const params = new URLSearchParams(searchString); const {
const query = { ...previousQuery }; query, fetching, importing, count, countInvisible,
for (const field of Object.keys(query)) { } = props;
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;
}
}
return query; const dispatch = useDispatch();
} const history = useHistory();
const [isMounted, setIsMounted] = useState(false);
class TasksPageComponent extends React.PureComponent<TasksPageProps & RouteComponentProps> { const queryParams = new URLSearchParams(history.location.search);
public componentDidMount(): void { const updatedQuery = { ...query };
const { gettingQuery, location, onGetTasks } = this.props; for (const key of Object.keys(updatedQuery)) {
const query = updateQuery(gettingQuery, location.search); (updatedQuery as Indexable)[key] = queryParams.get(key) || null;
onGetTasks(query); if (key === 'page') {
updatedQuery.page = updatedQuery.page ? +updatedQuery.page : 1;
}
} }
public componentDidUpdate(prevProps: TasksPageProps & RouteComponentProps): void { useEffect(() => {
const { dispatch(getTasksAsync({ ...updatedQuery }));
location, setIsMounted(true);
gettingQuery, }, []);
tasksFetching,
numberOfHiddenTasks,
onGetTasks,
hideEmptyTasks,
taskImporting,
} = this.props;
if ( useEffect(() => {
prevProps.location.search !== location.search || if (isMounted) {
(prevProps.taskImporting === true && taskImporting === false) history.replace({
) { search: updateHistoryFromQuery(query),
// get new tasks if any query changes });
const query = updateQuery(gettingQuery, location.search);
message.destroy();
onGetTasks(query);
return;
} }
}, [query]);
if (prevProps.tasksFetching && !tasksFetching) { useEffect(() => {
if (numberOfHiddenTasks) { if (countInvisible) {
message.destroy(); message.destroy();
message.info( message.info(
<> <>
<Text>Some tasks are temporary hidden since they are without any data</Text> <Text>Some tasks are temporary hidden because they are not fully created yet</Text>
<Button <Button
type='link' type='link'
onClick={(): void => { onClick={(): void => {
hideEmptyTasks(false); dispatch(hideEmptyTasks(true));
message.destroy(); message.destroy();
}} }}
> >
@ -102,66 +80,76 @@ class TasksPageComponent extends React.PureComponent<TasksPageProps & RouteCompo
5, 5,
); );
} }
} }, [countInvisible]);
}
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; const content = count ? (
if (oldQueryString !== queryString) { <>
history.push({ <TaskListContainer />
search: queryString.slice(0, -1), <Row justify='center' align='middle'>
}); <Col md={22} lg={18} xl={16} xxl={14}>
<Pagination
// force update if any changes className='cvat-tasks-pagination'
this.forceUpdate(); onChange={(page: number) => {
} dispatch(getTasksAsync({
}; ...query,
page,
public render(): JSX.Element { }));
const { }}
tasksFetching, gettingQuery, numberOfVisibleTasks, onImportTask, taskImporting, showSizeChanger={false}
} = this.props; total={count}
pageSize={10}
if (tasksFetching) { current={query.page}
return <Spin size='large' className='cvat-spinner' />; showQuickJumper
} />
</Col>
</Row>
</>
) : (
<EmptyListComponent query={query} />
);
return ( return (
<div className='cvat-tasks-page'> <div className='cvat-tasks-page'>
<TopBar <TopBar
onSearch={this.updateURL} onApplySearch={(search: string | null) => {
query={gettingQuery} dispatch(
onFileUpload={onImportTask} getTasksAsync({
taskImporting={taskImporting} ...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}
/> />
{numberOfVisibleTasks ? ( { fetching ? (
<TaskListContainer onSwitchPage={this.handlePagination} /> <div className='cvat-empty-tasks-list'>
) : ( <Spin size='large' className='cvat-spinner' />
<EmptyListComponent /> </div>
)} ) : content }
<FeedbackComponent /> <FeedbackComponent />
</div> </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 // SPDX-License-Identifier: MIT
import React from 'react'; import React, { useState, useEffect } from 'react';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import Dropdown from 'antd/lib/dropdown';
import { PlusOutlined, UploadOutlined, LoadingOutlined } from '@ant-design/icons'; import { PlusOutlined, UploadOutlined, LoadingOutlined } from '@ant-design/icons';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
import Text from 'antd/lib/typography/Text';
import Upload from 'antd/lib/upload'; 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 { 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 { interface VisibleTopBarProps {
onSearch: (query: TasksQuery) => void; onImportTask(file: File): void;
onFileUpload(file: File): void; onApplyFilter(filter: string | null): void;
onApplySorting(sorting: string | null): void;
onApplySearch(search: string | null): void;
query: TasksQuery; query: TasksQuery;
taskImporting: boolean; importing: boolean;
} }
export default function TopBarComponent(props: VisibleTopBarProps): JSX.Element { export default function TopBarComponent(props: VisibleTopBarProps): JSX.Element {
const { const {
query, onSearch, onFileUpload, taskImporting, importing, query, onApplyFilter, onApplySorting, onApplySearch, onImportTask,
} = props; } = props;
const [visibility, setVisibility] = useState(defaultVisibility);
const history = useHistory(); const history = useHistory();
const prevImporting = usePrevious(importing);
useEffect(() => {
if (prevImporting && !importing) {
onApplyFilter(query.filter);
}
}, [importing]);
return ( return (
<Row className='cvat-tasks-page-top-bar' justify='center' align='middle'> <Row className='cvat-tasks-page-top-bar' justify='center' align='middle'>
<Col md={22} lg={18} xl={16} xxl={14}> <Col md={22} lg={18} xl={16} xxl={14}>
<Row justify='space-between' align='bottom'> <div className='cvat-tasks-page-filters-wrapper'>
<Col> <Input.Search
<Text className='cvat-title'>Tasks</Text> enterButton
<SearchField instance='task' onSearch={onSearch} query={query} /> onSearch={(phrase: string) => {
</Col> onApplySearch(phrase);
<Col> }}
<Row gutter={8}> defaultValue={query.search || ''}
<Col> 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 <Upload
accept='.zip' accept='.zip'
multiple={false} multiple={false}
showUploadList={false} showUploadList={false}
beforeUpload={(file: File): boolean => { beforeUpload={(file: File): boolean => {
onFileUpload(file); onImportTask(file);
return false; return false;
}} }}
className='cvat-import-task' className='cvat-import-task'
> >
<Button <Button
size='large' className='cvat-import-task-button'
id='cvat-import-task-button'
type='primary' type='primary'
disabled={taskImporting} disabled={importing}
icon={<UploadOutlined />} icon={<UploadOutlined />}
> >
Create from backup Create from backup
{taskImporting && <LoadingOutlined id='cvat-import-task-button-loading' />} {importing && <LoadingOutlined />}
</Button> </Button>
</Upload> </Upload>
</Col> </div>
<Col> )}
<Button
size='large'
id='cvat-create-task-button'
type='primary'
onClick={(): void => history.push('/tasks/create')}
icon={<PlusOutlined />}
> >
Create new task <Button type='primary' className='cvat-create-task-dropdown' icon={<PlusOutlined />} />
</Button> </Dropdown>
</Col> </div>
</Row>
</Col>
</Row>
</Col> </Col>
</Row> </Row>
); );

@ -1,14 +1,11 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React from 'react';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { TasksState, TasksQuery, CombinedState } from 'reducers/interfaces'; import { TasksState, TasksQuery, CombinedState } from 'reducers/interfaces';
import TasksListComponent from 'components/tasks-page/task-list'; import TasksListComponent from 'components/tasks-page/task-list';
import { getTasksAsync } from 'actions/tasks-actions'; import { getTasksAsync } from 'actions/tasks-actions';
interface StateToProps { interface StateToProps {
@ -19,10 +16,6 @@ interface DispatchToProps {
getTasks: (query: TasksQuery) => void; getTasks: (query: TasksQuery) => void;
} }
interface OwnProps {
onSwitchPage: (page: number) => void;
}
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
return { return {
tasks: state.tasks, 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 { function TasksListContainer(props: TasksListContainerProps): JSX.Element {
const { tasks, onSwitchPage } = props; const { tasks } = props;
return ( return (
<TasksListComponent <TasksListComponent
onSwitchPage={onSwitchPage}
currentTasksIndexes={tasks.current.map((task): number => task.instance.id)} 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 // SPDX-License-Identifier: MIT
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { Task, TasksQuery, CombinedState } from 'reducers/interfaces'; import { Task, TasksQuery, CombinedState } from 'reducers/interfaces';
import TasksPageComponent from 'components/tasks-page/tasks-page'; import TasksPageComponent from 'components/tasks-page/tasks-page';
import { getTasksAsync, hideEmptyTasks, importTaskAsync } from 'actions/tasks-actions';
interface StateToProps { interface StateToProps {
tasksFetching: boolean; fetching: boolean;
gettingQuery: TasksQuery; query: TasksQuery;
numberOfTasks: number; count: number;
numberOfVisibleTasks: number; countInvisible: number;
numberOfHiddenTasks: number; importing: boolean;
taskImporting: boolean;
}
interface DispatchToProps {
onGetTasks: (gettingQuery: TasksQuery) => void;
hideEmptyTasks: (hideEmpty: boolean) => void;
onImportTask: (file: File) => void;
} }
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
const { tasks } = state; const { tasks } = state;
return { return {
tasksFetching: state.tasks.fetching, fetching: state.tasks.fetching,
gettingQuery: tasks.gettingQuery, query: tasks.gettingQuery,
numberOfTasks: state.tasks.count, count: state.tasks.count,
numberOfVisibleTasks: state.tasks.current.length, countInvisible: tasks.hideEmpty ?
numberOfHiddenTasks: tasks.hideEmpty ?
tasks.current.filter((task: Task): boolean => !task.instance.jobs.length).length : tasks.current.filter((task: Task): boolean => !task.instance.jobs.length).length :
0, 0,
taskImporting: state.tasks.importing, importing: 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));
},
}; };
} }
export default connect(mapStateToProps, mapDispatchToProps)(TasksPageComponent); export default connect(mapStateToProps)(TasksPageComponent);

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

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

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

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

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

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -25,11 +25,8 @@ const defaultState: TasksState = {
page: 1, page: 1,
id: null, id: null,
search: null, search: null,
owner: null, filter: null,
assignee: null, sort: null,
name: null,
status: null,
mode: null,
projectId: null, projectId: null,
}, },
activities: { activities: {
@ -59,7 +56,7 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
hideEmpty: true, hideEmpty: true,
count: 0, count: 0,
current: [], current: [],
gettingQuery: { ...action.payload.query }, gettingQuery: action.payload.updateQuery ? { ...action.payload.query } : state.gettingQuery,
}; };
case TasksActionTypes.GET_TASKS_SUCCESS: { case TasksActionTypes.GET_TASKS_SUCCESS: {
const combinedWithPreviews = action.payload.array.map( 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 // 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 // eslint-disable-next-line import/prefer-default-export
export function usePrevious<T>(value: T): T | undefined { export function usePrevious<T>(value: T): T | undefined {
@ -12,17 +12,6 @@ export function usePrevious<T>(value: T): T | undefined {
return ref.current; 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 { export interface ICardHeightHOC {
numberOfRows: number; numberOfRows: number;
paddings: number; paddings: number;

@ -256,7 +256,7 @@ class ProjectViewSet(viewsets.ModelViewSet):
# NOTE: The search_fields attribute should be a list of names of text # NOTE: The search_fields attribute should be a list of names of text
# type fields on the model,such as CharField or TextField # type fields on the model,such as CharField or TextField
search_fields = ('name', 'owner', 'assignee', 'status') 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_fields = filter_fields
ordering = "-id" ordering = "-id"
lookup_fields = {'owner': 'owner__username', 'assignee': 'assignee__username'} lookup_fields = {'owner': 'owner__username', 'assignee': 'assignee__username'}
@ -563,7 +563,7 @@ class TaskViewSet(UploadMixin, viewsets.ModelViewSet):
serializer_class = TaskSerializer serializer_class = TaskSerializer
lookup_fields = {'project_name': 'project__name', 'owner': 'owner__username', 'assignee': 'assignee__username'} lookup_fields = {'project_name': 'project__name', 'owner': 'owner__username', 'assignee': 'assignee__username'}
search_fields = ('project_name', 'name', 'owner', 'status', 'assignee', 'subset', 'mode', 'dimension') 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_fields = filter_fields
ordering = "-id" ordering = "-id"
iam_organization_field = 'organization' 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.', () => { it('Creating a project with copying labels from the task.', () => {
cy.goToProjectsList(); cy.goToProjectsList();
cy.get('.cvat-create-project-dropdown').click();
cy.get('.cvat-create-project-button').click(); cy.get('.cvat-create-project-button').click();
cy.get('#name').type(projectName); cy.get('#name').type(projectName);
cy.contains('[role="tab"]', 'Raw').click(); 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 // SPDX-License-Identifier: MIT
@ -95,8 +95,10 @@ context('Move a task between projects.', () => {
afterEach(() => { afterEach(() => {
cy.goToProjectsList(); cy.goToProjectsList();
cy.get('.cvat-spinner').should('not.exist');
cy.openProject(firtsProject.name); cy.openProject(firtsProject.name);
cy.deleteProjectViaActions(firtsProject.name); cy.deleteProjectViaActions(firtsProject.name);
cy.get('.cvat-spinner').should('not.exist');
cy.openProject(secondProject.name); cy.openProject(secondProject.name);
cy.deleteProjectViaActions(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 // SPDX-License-Identifier: MIT
@ -44,7 +44,7 @@ context('Create more than one task per time when create from project.', () => {
describe(`Testing "Issue ${issueID}"`, () => { describe(`Testing "Issue ${issueID}"`, () => {
it('Create more than one task per time from project.', () => { 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.firstTask);
createTask(taskName.secondTask); createTask(taskName.secondTask);
}); });

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -27,7 +27,8 @@ context('Create a task with set an issue tracker.', () => {
cy.login(); cy.login();
cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount); cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount);
cy.createZipArchive(directoryToArchive, archivePath); 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(() => { 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 // SPDX-License-Identifier: MIT
@ -25,7 +25,8 @@ context('Try to create a task without necessary arguments.', () => {
cy.login(); cy.login();
cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount); cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount);
cy.createZipArchive(directoryToArchive, archivePath); 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(() => { after(() => {

@ -13,7 +13,8 @@ context('Add/delete labels and attributes.', () => {
before(() => { before(() => {
cy.visit('auth/login'); cy.visit('auth/login');
cy.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}"`, () => { describe(`Testing "${labelName}"`, () => {

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -12,7 +12,8 @@ context('Changing a label name via label constructor.', () => {
before(() => { before(() => {
cy.visit('auth/login'); cy.visit('auth/login');
cy.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}"`, () => { describe(`Testing case "${caseId}"`, () => {

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

@ -94,6 +94,7 @@ context('Export, import an annotation task.', { browser: '!firefox' }, () => {
it('Import the task. Check id, labels, shape.', () => { it('Import the task. Check id, labels, shape.', () => {
cy.intercept('POST', '/api/tasks/backup?**').as('importTask'); 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.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', { timeout: 5000 }).its('response.statusCode').should('equal', 202);
cy.wait('@importTask').its('response.statusCode').should('equal', 201); 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.', () => { it('Check select files from "Cloud Storage" when creating a task.', () => {
cy.contains('.cvat-header-button', 'Tasks').click(); 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.get('.cvat-create-task-content').should('be.visible').within(() => {
cy.contains('[role="tab"]', 'Cloud Storage').click(); cy.contains('[role="tab"]', 'Cloud Storage').click();
cy.get('#cloudStorageSelect').should('exist'); cy.get('#cloudStorageSelect').should('exist');

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -12,7 +12,8 @@ context('Connected file share.', () => {
const assetLocalPath = `cypress/integration/actions_tasks3/assets/case_${caseId}`; const assetLocalPath = `cypress/integration/actions_tasks3/assets/case_${caseId}`;
function createOpenTaskWithShare() { 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.get('#name').type(taskName);
cy.addNewLabel(labelName); cy.addNewLabel(labelName);
cy.contains('[role="tab"]', 'Connected file share').click(); cy.contains('[role="tab"]', 'Connected file share').click();
@ -76,7 +77,7 @@ context('Connected file share.', () => {
expect(fileRenameCommand.code).to.be.eq(0); 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) => { (findFilesCommand) => {
// [image_case_107_2.png, image_case_107_3.png] // [image_case_107_2.png, image_case_107_3.png]
expect(findFilesCommand.stdout.split('\n').length).to.be.eq(2); 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 // SPDX-License-Identifier: MIT
@ -15,7 +15,8 @@ context('Create a task with files from remote sources.', () => {
before(() => { before(() => {
cy.visit('auth/login'); cy.visit('auth/login');
cy.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(() => { after(() => {

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -40,7 +40,8 @@ context('Wrong attribute is removed in label constructor.', () => {
describe(`Testing issue "${issueId}"`, () => { describe(`Testing issue "${issueId}"`, () => {
it('Open the create task page.', () => { 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.', () => { it('Go to Raw labels editor. Insert values.', () => {
cy.get('[role="tab"]').contains('Raw').click(); 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 // SPDX-License-Identifier: MIT
@ -18,7 +18,7 @@ context('Displaying attached files when creating a task.', () => {
const archivePath = `cypress/fixtures/${archiveName}`; const archivePath = `cypress/fixtures/${archiveName}`;
const imagesFolder = `cypress/fixtures/${imageFileName}`; const imagesFolder = `cypress/fixtures/${imageFileName}`;
const directoryToArchive = imagesFolder; const directoryToArchive = imagesFolder;
let imageListToAttach = []; const imageListToAttach = [];
for (let i = 1; i <= imagesCount; i++) { for (let i = 1; i <= imagesCount; i++) {
imageListToAttach.push(`${imageFileName}/${imageFileName}_${i}.png`); imageListToAttach.push(`${imageFileName}/${imageFileName}_${i}.png`);
} }
@ -28,7 +28,8 @@ context('Displaying attached files when creating a task.', () => {
cy.login(); cy.login();
cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount); cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount);
cy.createZipArchive(directoryToArchive, archivePath); 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}"`, () => { describe(`Testing "${labelName}"`, () => {

@ -177,7 +177,12 @@ Cypress.Commands.add(
expectedResult = 'success', expectedResult = 'success',
projectSubsetFieldValue = 'Test', projectSubsetFieldValue = 'Test',
) => { ) => {
cy.get('#cvat-create-task-button').click({ force: true }); cy.url().then(($url) => {
if (!$url.includes('projects')) {
cy.get('.cvat-create-task-dropdown').click();
}
cy.get('.cvat-create-task-button').click({ force: true });
cy.url().should('include', '/tasks/create'); cy.url().should('include', '/tasks/create');
cy.get('[id="name"]').type(taskName); cy.get('[id="name"]').type(taskName);
if (!forProject) { if (!forProject) {
@ -220,6 +225,7 @@ Cypress.Commands.add(
} else { } else {
cy.goToProjectsList(); cy.goToProjectsList();
} }
});
}, },
); );

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

Loading…
Cancel
Save