Merge branch 'develop' into mk/share_without_copying_

main
Maya 5 years ago
commit c1da108f6c

@ -7,3 +7,4 @@ datumaro/
keys/ keys/
logs/ logs/
static/ static/
templates/

@ -4,14 +4,13 @@
module.exports = { module.exports = {
env: { env: {
node: false, node: true,
browser: true, browser: true,
es6: true, es6: true,
jquery: true,
qunit: true,
}, },
parserOptions: { parserOptions: {
sourceType: 'script', sourceType: 'module',
ecmaVersion: 2018,
}, },
plugins: ['eslint-plugin-header'], plugins: ['eslint-plugin-header'],
extends: ['eslint:recommended', 'prettier'], extends: ['eslint:recommended', 'prettier'],

4
.gitignore vendored

@ -30,3 +30,7 @@ yarn-debug.log*
yarn-error.log* yarn-error.log*
.DS_Store .DS_Store
#Ignore Cypress tests temp files
/tests/cypress/fixtures
/tests/cypress/screenshots

@ -7,3 +7,4 @@ datumaro/
keys/ keys/
logs/ logs/
static/ static/
templates/

@ -21,6 +21,21 @@
}, },
"smartStep": true, "smartStep": true,
}, },
{
"type": "node",
"request": "launch",
"name": "ui.js: test",
"cwd": "${workspaceRoot}/tests",
"runtimeExecutable": "${workspaceRoot}/tests/node_modules/.bin/cypress",
"args": [
"run",
"--headless",
"--browser",
"chrome"
],
"outputCapture": "std",
"console": "internalConsole"
},
{ {
"name": "server: django", "name": "server: django",
"type": "python", "type": "python",

@ -1,27 +1,22 @@
{ {
"python.pythonPath": ".env/bin/python", "python.pythonPath": ".env/bin/python",
"eslint.enable": true, "eslint.enable": true,
"eslint.validate": [ "eslint.probe": [
"javascript", "javascript",
"typescript", "typescript",
"typescriptreact", "typescriptreact"
], ],
"eslint.onIgnoredFiles": "warn",
"eslint.workingDirectories": [ "eslint.workingDirectories": [
{ {
"directory": "./cvat-core", "directory": "${cwd}",
"changeProcessCWD": true
}, },
{ {
"directory": "./cvat-canvas", "pattern": "cvat-*"
"changeProcessCWD": true
}, },
{ {
"directory": "./cvat-ui", "directory": "tests",
"changeProcessCWD": true "!cwd": true
},
{
"directory": ".",
"changeProcessCWD": true
} }
], ],
"python.linting.pylintEnabled": true, "python.linting.pylintEnabled": true,

@ -5,10 +5,43 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.2.0] - Unreleased ## [1.2.0-beta] - Unreleased
### Added ### Added
- Removed Z-Order flag from task creation process
- Added basic projects implementation (<https://github.com/openvinotoolkit/cvat/pull/2255>)
### Changed
- PATCH requests from cvat-core submit only changed fields (<https://github.com/openvinotoolkit/cvat/pull/2445>)
### Deprecated
-
### Removed
-
### Fixed
- Django templates for email and user guide (<https://github.com/openvinotoolkit/cvat/pull/2412>)
- Saving relative paths in dummy chunks instead of absolute (<https://github.com/openvinotoolkit/cvat/pull/2424>)
- Objects with a specific label cannot be displayed if at least one tag with the label exist (<https://github.com/openvinotoolkit/cvat/pull/2435>)
- Wrong attribute can be removed in labels editor (<https://github.com/openvinotoolkit/cvat/pull/2436>)
- UI fails with the error "Cannot read property 'label' of undefined" (<https://github.com/openvinotoolkit/cvat/pull/2442>)
- Exception: "Value must be a user instance" (<https://github.com/openvinotoolkit/cvat/pull/2441>)
- Reset zoom option doesn't work in tag annotation mode (<https://github.com/openvinotoolkit/cvat/pull/2443>)
- Canvas is busy error (<https://github.com/openvinotoolkit/cvat/pull/2437>)
### Security
-
## [1.2.0-alpha] - 2020-11-09
### Added
- Ability to login into CVAT-UI with token from api/v1/auth/login (<https://github.com/openvinotoolkit/cvat/pull/2234>) - Ability to login into CVAT-UI with token from api/v1/auth/login (<https://github.com/openvinotoolkit/cvat/pull/2234>)
- Added layout grids toggling ('ctrl + alt + Enter') - Added layout grids toggling ('ctrl + alt + Enter')
- Added password reset functionality (<https://github.com/opencv/cvat/pull/2058>) - Added password reset functionality (<https://github.com/opencv/cvat/pull/2058>)
@ -29,6 +62,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Optional chaining plugin for cvat-canvas and cvat-ui (<https://github.com/openvinotoolkit/cvat/pull/2249>) - Optional chaining plugin for cvat-canvas and cvat-ui (<https://github.com/openvinotoolkit/cvat/pull/2249>)
- MOTS png mask format support (<https://github.com/openvinotoolkit/cvat/pull/2198>) - MOTS png mask format support (<https://github.com/openvinotoolkit/cvat/pull/2198>)
- Ability to correct upload video with a rotation record in the metadata (<https://github.com/openvinotoolkit/cvat/pull/2218>) - Ability to correct upload video with a rotation record in the metadata (<https://github.com/openvinotoolkit/cvat/pull/2218>)
- User search field for assignee fields (<https://github.com/openvinotoolkit/cvat/pull/2370>)
### Changed ### Changed
@ -42,13 +76,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Track frames in skips between annotation is presented in MOT and MOTS formats are marked `outside` (<https://github.com/openvinotoolkit/cvat/pull/2198>) - Track frames in skips between annotation is presented in MOT and MOTS formats are marked `outside` (<https://github.com/openvinotoolkit/cvat/pull/2198>)
- UI packages installation with `npm ci` instead of `npm install` (<https://github.com/openvinotoolkit/cvat/pull/2350>) - UI packages installation with `npm ci` instead of `npm install` (<https://github.com/openvinotoolkit/cvat/pull/2350>)
### Deprecated
-
### Removed ### Removed
- - Removed Z-Order flag from task creation process
### Fixed ### Fixed
@ -72,10 +102,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 'List of tasks' Kibana visualization (<https://github.com/openvinotoolkit/cvat/pull/2361>) - 'List of tasks' Kibana visualization (<https://github.com/openvinotoolkit/cvat/pull/2361>)
- An error on exporting not `jpg` or `png` images in TF Detection API format (<https://github.com/openvinotoolkit/datumaro/issues/35>) - An error on exporting not `jpg` or `png` images in TF Detection API format (<https://github.com/openvinotoolkit/datumaro/issues/35>)
### Security
-
## [1.1.0] - 2020-08-31 ## [1.1.0] - 2020-08-31
### Added ### Added

@ -21,8 +21,6 @@ COPY cvat-canvas/package*.json /tmp/cvat-canvas/
COPY cvat-ui/package*.json /tmp/cvat-ui/ COPY cvat-ui/package*.json /tmp/cvat-ui/
COPY cvat-data/package*.json /tmp/cvat-data/ COPY cvat-data/package*.json /tmp/cvat-data/
RUN npm config set loglevel info
# Install cvat-data dependencies # Install cvat-data dependencies
WORKDIR /tmp/cvat-data/ WORKDIR /tmp/cvat-data/
RUN npm ci RUN npm ci

@ -124,4 +124,5 @@ Other ways to ask questions and get our support:
- [VentureBeat: Intel open-sources CVAT, a toolkit for data labeling](https://venturebeat.com/2019/03/05/intel-open-sources-cvat-a-toolkit-for-data-labeling/) - [VentureBeat: Intel open-sources CVAT, a toolkit for data labeling](https://venturebeat.com/2019/03/05/intel-open-sources-cvat-a-toolkit-for-data-labeling/)
## Projects using CVAT ## Projects using CVAT
- [Onepanel](https://github.com/onepanelio/core) - Onepanel is an open source vision AI platform that fully integrates CVAT with scalable data processing and parallelized training pipelines. - [Onepanel](https://github.com/onepanelio/core) - Onepanel is an open source vision AI platform that fully integrates CVAT with scalable data processing and parallelized training pipelines.

@ -0,0 +1 @@
webpack.config.js

@ -0,0 +1 @@
webpack.config.js

@ -4,11 +4,9 @@
module.exports = { module.exports = {
env: { env: {
node: false, node: true,
browser: true, browser: true,
es6: true, es6: true,
jquery: true,
qunit: true,
'jest/globals': true, 'jest/globals': true,
}, },
parserOptions: { parserOptions: {
@ -16,7 +14,7 @@ module.exports = {
sourceType: 'module', sourceType: 'module',
ecmaVersion: 2018, ecmaVersion: 2018,
}, },
plugins: ['security', 'jest', 'no-unsanitized', 'no-unsafe-innerhtml'], plugins: ['security', 'jest', 'no-unsafe-innerhtml'],
extends: ['eslint:recommended', 'plugin:security/recommended', 'plugin:no-unsanitized/DOM', 'airbnb-base'], extends: ['eslint:recommended', 'plugin:security/recommended', 'plugin:no-unsanitized/DOM', 'airbnb-base'],
rules: { rules: {
'no-await-in-loop': [0], 'no-await-in-loop': [0],

File diff suppressed because it is too large Load Diff

@ -1,6 +1,6 @@
{ {
"name": "cvat-core", "name": "cvat-core",
"version": "3.9.0", "version": "3.9.1",
"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": {
@ -33,13 +33,13 @@
"webpack-cli": "^3.3.2" "webpack-cli": "^3.3.2"
}, },
"dependencies": { "dependencies": {
"axios": "^0.20.0", "axios": "^0.21.0",
"browser-or-node": "^1.2.1", "browser-or-node": "^1.2.1",
"cvat-data": "../cvat-data", "cvat-data": "../cvat-data",
"detect-browser": "^5.2.0", "detect-browser": "^5.2.0",
"error-stack-parser": "^2.0.2", "error-stack-parser": "^2.0.2",
"form-data": "^2.5.0", "form-data": "^2.5.0",
"jest-config": "^24.8.0", "jest-config": "^26.6.3",
"js-cookie": "^2.2.0", "js-cookie": "^2.2.0",
"jsonpath": "^1.0.2", "jsonpath": "^1.0.2",
"platform": "^1.3.5", "platform": "^1.3.5",

@ -16,26 +16,7 @@
const { AnnotationFormats } = require('./annotation-formats'); const { AnnotationFormats } = require('./annotation-formats');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { Task } = require('./session'); const { Task } = require('./session');
const { Project } = require('./project');
function attachUsers(task, users) {
if (task.assignee !== null) {
[task.assignee] = users.filter((user) => user.id === task.assignee);
}
for (const segment of task.segments) {
for (const job of segment.jobs) {
if (job.assignee !== null) {
[job.assignee] = users.filter((user) => user.id === job.assignee);
}
}
}
if (task.owner !== null) {
[task.owner] = users.filter((user) => user.id === task.owner);
}
return task;
}
function implementAPI(cvat) { function implementAPI(cvat) {
cvat.plugins.list.implementation = PluginRegistry.list; cvat.plugins.list.implementation = PluginRegistry.list;
@ -120,9 +101,17 @@
return result; return result;
}; };
cvat.server.installedApps.implementation = async () => {
const result = await serverProxy.server.installedApps();
return result;
};
cvat.users.get.implementation = async (filter) => { cvat.users.get.implementation = async (filter) => {
checkFilter(filter, { checkFilter(filter, {
id: isInteger,
self: isBoolean, self: isBoolean,
search: isString,
limit: isInteger,
}); });
let users = null; let users = null;
@ -130,7 +119,13 @@
users = await serverProxy.users.getSelf(); users = await serverProxy.users.getSelf();
users = [users]; users = [users];
} else { } else {
users = await serverProxy.users.getUsers(); const searchParams = {};
for (const key in filter) {
if (filter[key] && key !== 'self') {
searchParams[key] = filter[key];
}
}
users = await serverProxy.users.getUsers(new URLSearchParams(searchParams).toString());
} }
users = users.map((user) => new User(user)); users = users.map((user) => new User(user));
@ -163,8 +158,7 @@
// If task was found by its id, then create task instance and get Job instance from it // If task was found by its id, then create task instance and get Job instance from it
if (tasks !== null && tasks.length) { if (tasks !== null && tasks.length) {
const users = (await serverProxy.users.getUsers()).map((userData) => new User(userData)); const task = new Task(tasks[0]);
const task = new Task(attachUsers(tasks[0], users));
return filter.jobID ? task.jobs.filter((job) => job.id === filter.jobID) : task.jobs; return filter.jobID ? task.jobs.filter((job) => job.id === filter.jobID) : task.jobs;
} }
@ -175,6 +169,7 @@
cvat.tasks.get.implementation = async (filter) => { cvat.tasks.get.implementation = async (filter) => {
checkFilter(filter, { checkFilter(filter, {
page: isInteger, page: isInteger,
projectId: isInteger,
name: isString, name: isString,
id: isInteger, id: isInteger,
owner: isString, owner: isString,
@ -196,27 +191,69 @@
} }
} }
if (
'projectId' in filter
&& (('page' in filter && Object.keys(filter).length > 2) || Object.keys(filter).length > 2)
) {
throw new ArgumentError('Do not use the filter field "projectId" with other');
}
const searchParams = new URLSearchParams(); const searchParams = new URLSearchParams();
for (const field of ['name', 'owner', 'assignee', 'search', 'status', 'mode', 'id', 'page']) { for (const field of ['name', 'owner', 'assignee', 'search', 'status', 'mode', 'id', 'page', 'projectId']) {
if (Object.prototype.hasOwnProperty.call(filter, field)) { if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(field, filter[field]); searchParams.set(field, filter[field]);
} }
} }
const users = (await serverProxy.users.getUsers()).map((userData) => new User(userData));
const tasksData = await serverProxy.tasks.getTasks(searchParams.toString()); const tasksData = await serverProxy.tasks.getTasks(searchParams.toString());
const tasks = tasksData.map((task) => attachUsers(task, users)).map((task) => new Task(task)); const tasks = tasksData.map((task) => new Task(task));
tasks.count = tasksData.count; tasks.count = tasksData.count;
return tasks; return tasks;
}; };
cvat.server.installedApps.implementation = async () => { cvat.projects.get.implementation = async (filter) => {
const result = await serverProxy.server.installedApps(); checkFilter(filter, {
return result; id: isInteger,
page: isInteger,
name: isString,
assignee: isString,
owner: isString,
search: isString,
status: isEnum.bind(TaskStatus),
});
if ('search' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError('Do not use the filter field "search" with others');
}
}
if ('id' in filter && Object.keys(filter).length > 1) {
if (!('page' in filter && Object.keys(filter).length === 2)) {
throw new ArgumentError('Do not use the filter field "id" with others');
}
}
const searchParams = new URLSearchParams();
for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page']) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(field, filter[field]);
}
}
const projectsData = await serverProxy.projects.get(searchParams.toString());
// prettier-ignore
const projects = projectsData.map((project) => new Project(project));
projects.count = projectsData.count;
return projects;
}; };
cvat.projects.searchNames.implementation = async (search, limit) => serverProxy.projects.searchNames(search, limit);
return cvat; return cvat;
} }

@ -14,6 +14,7 @@ function build() {
const ObjectState = require('./object-state'); const ObjectState = require('./object-state');
const Statistics = require('./statistics'); const Statistics = require('./statistics');
const { Job, Task } = require('./session'); const { Job, Task } = require('./session');
const { Project } = require('./project');
const { Attribute, Label } = require('./labels'); const { Attribute, Label } = require('./labels');
const MLModel = require('./ml-model'); const MLModel = require('./ml-model');
@ -274,6 +275,60 @@ function build() {
return result; return result;
}, },
}, },
/**
* Namespace is used for getting projects
* @namespace projects
* @memberof module:API.cvat
*/
projects: {
/**
* @typedef {Object} ProjectFilter
* @property {string} name Check if name contains this value
* @property {module:API.cvat.enums.ProjectStatus} status
* Check if status contains this value
* @property {integer} id Check if id equals this value
* @property {integer} page Get specific page
* (default REST API returns 20 projects per request.
* In order to get more, it is need to specify next page)
* @property {string} owner Check if owner user contains this value
* @property {string} search Combined search of contains among all fields
* @global
*/
/**
* Method returns list of projects corresponding to a filter
* @method get
* @async
* @memberof module:API.cvat.projects
* @param {ProjectFilter} [filter={}] project filter
* @returns {module:API.cvat.classes.Project[]}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async get(filter = {}) {
const result = await PluginRegistry.apiWrapper(cvat.projects.get, filter);
return result;
},
/**
* Method returns list of project names with project ids
* corresponding to a search phrase
* used for autocomplete field
* @method searchNames
* @async
* @memberof module:API.cvat.projects
* @param {string} [search = ''] search phrase
* @param {number} [limit = 10] number of returning project names
* @returns {module:API.cvat.classes.Project[]}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*
*/
async searchNames(search = '', limit = 10) {
const result = await PluginRegistry.apiWrapper(cvat.projects.searchNames, search, limit);
return result;
},
},
/** /**
* Namespace is used for getting tasks * Namespace is used for getting tasks
* @namespace tasks * @namespace tasks
@ -291,6 +346,7 @@ function build() {
* @property {integer} page Get specific page * @property {integer} page Get specific page
* (default REST API returns 20 tasks per request. * (default REST API returns 20 tasks per request.
* In order to get more, it is need to specify next page) * In order to get more, it is need to specify next page)
* @property {integer} projectId Check if project_id field contains this value
* @property {string} owner Check if owner user contains this value * @property {string} owner Check if owner user contains this value
* @property {string} assignee Check if assigneed contains this value * @property {string} assignee Check if assigneed contains this value
* @property {string} search Combined search of contains among all fields * @property {string} search Combined search of contains among all fields
@ -717,8 +773,9 @@ function build() {
* @memberof module:API.cvat * @memberof module:API.cvat
*/ */
classes: { classes: {
Task,
User, User,
Project,
Task,
Job, Job,
Log, Log,
Attribute, Attribute,
@ -730,6 +787,7 @@ function build() {
}; };
cvat.server = Object.freeze(cvat.server); cvat.server = Object.freeze(cvat.server);
cvat.projects = Object.freeze(cvat.projects);
cvat.tasks = Object.freeze(cvat.tasks); cvat.tasks = Object.freeze(cvat.tasks);
cvat.jobs = Object.freeze(cvat.jobs); cvat.jobs = Object.freeze(cvat.jobs);
cvat.users = Object.freeze(cvat.users); cvat.users = Object.freeze(cvat.users);

@ -0,0 +1,265 @@
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
const { Label } = require('./labels');
const User = require('./user');
/**
* Class representing a project
* @memberof module:API.cvat.classes
*/
class Project {
/**
* In a fact you need use the constructor only if you want to create a project
* @param {object} initialData - Object which is used for initalization
* <br> It can contain keys:
* <br> <li style="margin-left: 10px;"> name
* <br> <li style="margin-left: 10px;"> labels
*/
constructor(initialData) {
const data = {
id: undefined,
name: undefined,
status: undefined,
assignee: undefined,
owner: undefined,
bug_tracker: undefined,
created_date: undefined,
updated_date: undefined,
};
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property];
}
}
data.labels = [];
data.tasks = [];
if (Array.isArray(initialData.labels)) {
for (const label of initialData.labels) {
const classInstance = new Label(label);
data.labels.push(classInstance);
}
}
if (Array.isArray(initialData.tasks)) {
for (const task of initialData.tasks) {
const taskInstance = new Task(task);
data.tasks.push(taskInstance);
}
}
Object.defineProperties(
this,
Object.freeze({
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
id: {
get: () => data.id,
},
/**
* @name name
* @type {string}
* @memberof module:API.cvat.classes.Project
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
name: {
get: () => data.name,
set: (value) => {
if (!value.trim().length) {
throw new ArgumentError('Value must not be empty');
}
data.name = value;
},
},
/**
* @name status
* @type {module:API.cvat.enums.TaskStatus}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
status: {
get: () => data.status,
},
/**
* Instance of a user who was assigned for the project
* @name assignee
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
assignee: {
get: () => data.assignee,
set: (assignee) => {
if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError('Value must be a user instance');
}
data.assignee = assignee;
},
},
/**
* Instance of a user who has created the project
* @name owner
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
owner: {
get: () => data.owner,
},
/**
* @name bugTracker
* @type {string}
* @memberof module:API.cvat.classes.Project
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
bugTracker: {
get: () => data.bug_tracker,
set: (tracker) => {
data.bug_tracker = tracker;
},
},
/**
* @name createdDate
* @type {string}
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
*/
createdDate: {
get: () => data.created_date,
},
/**
* @name updatedDate
* @type {string}
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
*/
updatedDate: {
get: () => data.updated_date,
},
/**
* After project has been created value can be appended only.
* @name labels
* @type {module:API.cvat.classes.Label[]}
* @memberof module:API.cvat.classes.Project
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
labels: {
get: () => [...data.labels],
set: (labels) => {
if (!Array.isArray(labels)) {
throw new ArgumentError('Value must be an array of Labels');
}
if (!Array.isArray(labels) || labels.some((label) => !(label instanceof Label))) {
throw new ArgumentError(
`Each array value must be an instance of Label. ${typeof label} was found`,
);
}
data.labels = [...labels];
},
},
/**
* Tasks linked with the project
* @name tasks
* @type {module:API.cvat.classes.Task[]}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
tasks: {
get: () => [...data.tasks],
},
}),
);
}
/**
* Method updates data of a created project or creates new project from scratch
* @method save
* @returns {module:API.cvat.classes.Project}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async save() {
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.save);
return result;
}
/**
* Method deletes a task from a server
* @method delete
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async delete() {
const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.delete);
return result;
}
}
module.exports = {
Project,
};
Project.prototype.save.implementation = async function () {
if (typeof this.id !== 'undefined') {
const projectData = {
name: this.name,
assignee_id: this.assignee ? this.assignee.id : null,
bug_tracker: this.bugTracker,
labels: [...this.labels.map((el) => el.toJSON())],
};
await serverProxy.projects.save(this.id, projectData);
return this;
}
const projectSpec = {
name: this.name,
labels: [...this.labels.map((el) => el.toJSON())],
};
if (this.bugTracker) {
projectSpec.bug_tracker = this.bugTracker;
}
const project = await serverProxy.projects.create(projectSpec);
return new Project(project);
};
Project.prototype.delete.implementation = async function () {
const result = await serverProxy.projects.delete(this.id);
return result;
};
})();

@ -312,6 +312,82 @@
} }
} }
async function searchProjectNames(search, limit) {
const { backendAPI, proxy } = config;
let response = null;
try {
response = await Axios.get(
`${backendAPI}/projects?names_only=true&page=1&page_size=${limit}&search=${search}`,
{
proxy,
},
);
} catch (errorData) {
throw generateError(errorData);
}
response.data.results.count = response.data.count;
return response.data.results;
}
async function getProjects(filter = '') {
const { backendAPI, proxy } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/projects?page_size=12&${filter}`, {
proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
response.data.results.count = response.data.count;
return response.data.results;
}
async function saveProject(id, projectData) {
const { backendAPI } = config;
try {
await Axios.patch(`${backendAPI}/projects/${id}`, JSON.stringify(projectData), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
}
async function deleteProject(id) {
const { backendAPI } = config;
try {
await Axios.delete(`${backendAPI}/projects/${id}`);
} catch (errorData) {
throw generateError(errorData);
}
}
async function createProject(projectSpec) {
const { backendAPI } = config;
try {
const response = await Axios.post(`${backendAPI}/projects`, JSON.stringify(projectSpec), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
return response.data;
} catch (errorData) {
throw generateError(errorData);
}
}
async function getTasks(filter = '') { async function getTasks(filter = '') {
const { backendAPI } = config; const { backendAPI } = config;
@ -347,7 +423,12 @@
const { backendAPI } = config; const { backendAPI } = config;
try { try {
await Axios.delete(`${backendAPI}/tasks/${id}`); await Axios.delete(`${backendAPI}/tasks/${id}`, {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
} }
@ -500,20 +581,14 @@
} }
} }
async function getUsers(id = null) { async function getUsers(filter = 'page_size=all') {
const { backendAPI } = config; const { backendAPI } = config;
let response = null; let response = null;
try { try {
if (id === null) { response = await Axios.get(`${backendAPI}/users?${filter}`, {
response = await Axios.get(`${backendAPI}/users?page_size=all`, { proxy: config.proxy,
proxy: config.proxy, });
});
} else {
response = await Axios.get(`${backendAPI}/users/${id}`, {
proxy: config.proxy,
});
}
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
} }
@ -835,6 +910,17 @@
writable: false, writable: false,
}, },
projects: {
value: Object.freeze({
get: getProjects,
searchNames: searchProjectNames,
save: saveProject,
create: createProject,
delete: deleteProject,
}),
writable: false,
},
tasks: { tasks: {
value: Object.freeze({ value: Object.freeze({
getTasks, getTasks,

@ -674,6 +674,11 @@
task: undefined, task: undefined,
}; };
let updatedFields = {
assignee: false,
status: false,
};
for (const property in data) { for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property)) { if (Object.prototype.hasOwnProperty.call(data, property)) {
if (property in initialData) { if (property in initialData) {
@ -686,6 +691,8 @@
} }
} }
if (data.assignee) data.assignee = new User(data.assignee);
Object.defineProperties( Object.defineProperties(
this, this,
Object.freeze({ Object.freeze({
@ -713,6 +720,7 @@
if (assignee !== null && !(assignee instanceof User)) { if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError('Value must be a user instance'); throw new ArgumentError('Value must be a user instance');
} }
updatedFields.assignee = true;
data.assignee = assignee; data.assignee = assignee;
}, },
}, },
@ -741,6 +749,7 @@
); );
} }
updatedFields.status = true;
data.status = status; data.status = status;
}, },
}, },
@ -774,6 +783,12 @@
task: { task: {
get: () => data.task, get: () => data.task,
}, },
__updatedFields: {
get: () => updatedFields,
set: (fields) => {
updatedFields = fields;
},
},
}), }),
); );
@ -856,6 +871,7 @@
const data = { const data = {
id: undefined, id: undefined,
name: undefined, name: undefined,
project_id: undefined,
status: undefined, status: undefined,
size: undefined, size: undefined,
mode: undefined, mode: undefined,
@ -878,12 +894,22 @@
copy_data: undefined, copy_data: undefined,
}; };
let updatedFields = {
name: false,
assignee: false,
bug_tracker: false,
labels: false,
};
for (const property in data) { for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) { if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property]; data[property] = initialData[property];
} }
} }
if (data.assignee) data.assignee = new User(data.assignee);
if (data.owner) data.owner = new User(data.owner);
data.labels = []; data.labels = [];
data.jobs = []; data.jobs = [];
data.files = Object.freeze({ data.files = Object.freeze({
@ -944,9 +970,20 @@
if (!value.trim().length) { if (!value.trim().length) {
throw new ArgumentError('Value must not be empty'); throw new ArgumentError('Value must not be empty');
} }
updatedFields.name = true;
data.name = value; data.name = value;
}, },
}, },
/**
* @name projectId
* @type {integer|null}
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
*/
projectId: {
get: () => data.project_id,
},
/** /**
* @name status * @name status
* @type {module:API.cvat.enums.TaskStatus} * @type {module:API.cvat.enums.TaskStatus}
@ -1002,6 +1039,7 @@
if (assignee !== null && !(assignee instanceof User)) { if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError('Value must be a user instance'); throw new ArgumentError('Value must be a user instance');
} }
updatedFields.assignee = true;
data.assignee = assignee; data.assignee = assignee;
}, },
}, },
@ -1035,6 +1073,7 @@
bugTracker: { bugTracker: {
get: () => data.bug_tracker, get: () => data.bug_tracker,
set: (tracker) => { set: (tracker) => {
updatedFields.bug_tracker = true;
data.bug_tracker = tracker; data.bug_tracker = tracker;
}, },
}, },
@ -1157,6 +1196,7 @@
} }
} }
updatedFields.labels = true;
data.labels = [...labels]; data.labels = [...labels];
}, },
}, },
@ -1323,6 +1363,12 @@
dataChunkType: { dataChunkType: {
get: () => data.data_compressed_chunk_type, get: () => data.data_compressed_chunk_type,
}, },
__updatedFields: {
get: () => updatedFields,
set: (fields) => {
updatedFields = fields;
},
},
}), }),
); );
@ -1455,12 +1501,30 @@
Job.prototype.save.implementation = async function () { Job.prototype.save.implementation = async function () {
// TODO: Add ability to change an assignee // TODO: Add ability to change an assignee
if (this.id) { if (this.id) {
const jobData = { const jobData = {};
status: this.status,
assignee: this.assignee ? this.assignee.id : null, for (const [field, isUpdated] of Object.entries(this.__updatedFields)) {
}; if (isUpdated) {
switch (field) {
case 'status':
jobData.status = this.status;
break;
case 'assignee':
jobData.assignee_id = this.assignee ? this.assignee.id : null;
break;
default:
break;
}
}
}
await serverProxy.jobs.saveJob(this.id, jobData); await serverProxy.jobs.saveJob(this.id, jobData);
this.__updatedFields = {
status: false,
assignee: false,
};
return this; return this;
} }
@ -1661,18 +1725,42 @@
return this; return this;
}; };
Task.prototype.save.implementation = async function saveTaskImplementation(onUpdate) { Task.prototype.save.implementation = async function (onUpdate) {
// TODO: Add ability to change an owner and an assignee // TODO: Add ability to change an owner and an assignee
if (typeof this.id !== 'undefined') { if (typeof this.id !== 'undefined') {
// If the task has been already created, we update it // If the task has been already created, we update it
const taskData = { const taskData = {};
assignee: this.assignee ? this.assignee.id : null,
name: this.name, for (const [field, isUpdated] of Object.entries(this.__updatedFields)) {
bug_tracker: this.bugTracker, if (isUpdated) {
labels: [...this.labels.map((el) => el.toJSON())], switch (field) {
}; case 'assignee':
taskData.assignee_id = this.assignee ? this.assignee.id : null;
break;
case 'name':
taskData.name = this.name;
break;
case 'bug_tracker':
taskData.bug_tracker = this.bugTracker;
break;
case 'labels':
taskData.labels = [...this.labels.map((el) => el.toJSON())];
break;
default:
break;
}
}
}
await serverProxy.tasks.saveTask(this.id, taskData); await serverProxy.tasks.saveTask(this.id, taskData);
this.updatedFields = {
assignee: false,
name: false,
bugTracker: false,
labels: false,
};
return this; return this;
} }
@ -1690,6 +1778,9 @@
if (typeof this.overlap !== 'undefined') { if (typeof this.overlap !== 'undefined') {
taskSpec.overlap = this.overlap; taskSpec.overlap = this.overlap;
} }
if (typeof this.projectId !== 'undefined') {
taskSpec.project_id = this.projectId;
}
const taskDataSpec = { const taskDataSpec = {
client_files: this.clientFiles, client_files: this.clientFiles,

@ -0,0 +1,170 @@
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock');
return mock;
});
// Initialize api
window.cvat = require('../../src/api');
const { Task } = require('../../src/session');
const { Project } = require('../../src/project');
describe('Feature: get projects', () => {
test('get all projects', async () => {
const result = await window.cvat.projects.get();
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(2);
for (const el of result) {
expect(el).toBeInstanceOf(Project);
}
});
test('get project by id', async () => {
const result = await window.cvat.projects.get({
id: 2,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(Project);
expect(result[0].id).toBe(2);
expect(result[0].tasks).toHaveLength(1);
expect(result[0].tasks[0]).toBeInstanceOf(Task);
});
test('get a project by an unknown id', async () => {
const result = await window.cvat.projects.get({
id: 1,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(0);
});
test('get a project by an invalid id', async () => {
expect(
window.cvat.projects.get({
id: '1',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get projects by filters', async () => {
const result = await window.cvat.projects.get({
status: 'completed',
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(1);
expect(result[0]).toBeInstanceOf(Project);
expect(result[0].id).toBe(2);
expect(result[0].status).toBe('completed');
});
test('get projects by invalid filters', async () => {
expect(
window.cvat.projects.get({
unknown: '5',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
describe('Feature: save a project', () => {
test('save some changed fields in a project', async () => {
let result = await window.cvat.tasks.get({
id: 2,
});
result[0].bugTracker = 'newBugTracker';
result[0].name = 'New Project Name';
result[0].save();
result = await window.cvat.tasks.get({
id: 2,
});
expect(result[0].bugTracker).toBe('newBugTracker');
expect(result[0].name).toBe('New Project Name');
});
test('save some new labels in a project', async () => {
let result = await window.cvat.projects.get({
id: 6,
});
const labelsLength = result[0].labels.length;
const newLabel = new window.cvat.classes.Label({
name: "My boss's car",
attributes: [
{
default_value: 'false',
input_type: 'checkbox',
mutable: true,
name: 'parked',
values: ['false'],
},
],
});
result[0].labels = [...result[0].labels, newLabel];
result[0].save();
result = await window.cvat.projects.get({
id: 6,
});
expect(result[0].labels).toHaveLength(labelsLength + 1);
const appendedLabel = result[0].labels.filter((el) => el.name === "My boss's car");
expect(appendedLabel).toHaveLength(1);
expect(appendedLabel[0].attributes).toHaveLength(1);
expect(appendedLabel[0].attributes[0].name).toBe('parked');
expect(appendedLabel[0].attributes[0].defaultValue).toBe('false');
expect(appendedLabel[0].attributes[0].mutable).toBe(true);
expect(appendedLabel[0].attributes[0].inputType).toBe('checkbox');
});
test('save new project without an id', async () => {
const project = new window.cvat.classes.Project({
name: 'New Empty Project',
labels: [
{
name: 'car',
attributes: [
{
default_value: 'false',
input_type: 'checkbox',
mutable: true,
name: 'parked',
values: ['false'],
},
],
},
],
bug_tracker: 'bug tracker value',
});
const result = await project.save();
expect(typeof result.id).toBe('number');
});
});
describe('Feature: delete a project', () => {
test('delete a project', async () => {
let result = await window.cvat.projects.get({
id: 6,
});
await result[0].delete();
result = await window.cvat.projects.get({
id: 6,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(0);
});
});

@ -166,6 +166,19 @@ describe('Feature: save a task', () => {
const result = await task.save(); const result = await task.save();
expect(typeof result.id).toBe('number'); expect(typeof result.id).toBe('number');
}); });
test('save new task in project', async () => {
const task = new window.cvat.classes.Task({
name: 'New Task',
project_id: 2,
bug_tracker: 'bug tracker value',
image_quality: 50,
z_order: true,
});
const result = await task.save();
expect(result.projectId).toBe(2);
});
}); });
describe('Feature: delete a task', () => { describe('Feature: delete a task', () => {

@ -143,6 +143,174 @@ const shareDummyData = [
}, },
]; ];
const projectsDummyData = {
count: 2,
next: null,
previous: null,
results: [
{
url: 'http://192.168.0.139:7000/api/v1/projects/6',
id: 6,
name: 'Some empty project',
labels: [],
tasks: [],
owner: {
url: 'http://localhost:7000/api/v1/users/2',
id: 2,
username: 'bsekache',
},
assignee: {
url: 'http://localhost:7000/api/v1/users/2',
id: 2,
username: 'bsekache',
},
bug_tracker: '',
created_date: '2020-10-19T20:41:07.808029Z',
updated_date: '2020-10-19T20:41:07.808084Z',
status: 'annotation',
},
{
url: 'http://192.168.0.139:7000/api/v1/projects/1',
id: 2,
name: 'Test project with roads',
labels: [
{
id: 1,
name: 'car',
color: '#2080c0',
attributes: [
{
id: 199,
name: 'color',
mutable: false,
input_type: 'select',
default_value: 'red',
values: ['red', 'black', 'white', 'yellow', 'pink', 'green', 'blue', 'orange'],
},
],
},
],
tasks: [
{
url: 'http://192.168.0.139:7000/api/v1/tasks/2',
id: 2,
name: 'road 1',
project_id: 1,
mode: 'interpolation',
owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'admin',
},
assignee: null,
bug_tracker: '',
created_date: '2020-10-12T08:59:59.878083Z',
updated_date: '2020-10-18T21:02:20.831294Z',
overlap: 5,
segment_size: 100,
z_order: false,
status: 'completed',
labels: [
{
id: 1,
name: 'car',
color: '#2080c0',
attributes: [
{
id: 199,
name: 'color',
mutable: false,
input_type: 'select',
default_value: 'red',
values: ['red', 'black', 'white', 'yellow', 'pink', 'green', 'blue', 'orange'],
},
],
},
],
segments: [
{
start_frame: 0,
stop_frame: 99,
jobs: [
{
url: 'http://192.168.0.139:7000/api/v1/jobs/1',
id: 1,
assignee: null,
status: 'completed',
},
],
},
{
start_frame: 95,
stop_frame: 194,
jobs: [
{
url: 'http://192.168.0.139:7000/api/v1/jobs/2',
id: 2,
assignee: null,
status: 'completed',
},
],
},
{
start_frame: 190,
stop_frame: 289,
jobs: [
{
url: 'http://192.168.0.139:7000/api/v1/jobs/3',
id: 3,
assignee: null,
status: 'completed',
},
],
},
{
start_frame: 285,
stop_frame: 384,
jobs: [
{
url: 'http://192.168.0.139:7000/api/v1/jobs/4',
id: 4,
assignee: null,
status: 'completed',
},
],
},
{
start_frame: 380,
stop_frame: 431,
jobs: [
{
url: 'http://192.168.0.139:7000/api/v1/jobs/5',
id: 5,
assignee: null,
status: 'completed',
},
],
},
],
data_chunk_size: 36,
data_compressed_chunk_type: 'imageset',
data_original_chunk_type: 'video',
size: 432,
image_quality: 100,
data: 1,
},
],
owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'admin',
},
assignee: null,
bug_tracker: '',
created_date: '2020-10-12T08:21:56.558898Z',
updated_date: '2020-10-12T08:21:56.558982Z',
status: 'completed',
},
],
};
const tasksDummyData = { const tasksDummyData = {
count: 5, count: 5,
next: null, next: null,
@ -154,7 +322,11 @@ const tasksDummyData = {
name: 'Test', name: 'Test',
size: 1, size: 1,
mode: 'annotation', mode: 'annotation',
owner: 1, owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'admin',
},
assignee: null, assignee: null,
bug_tracker: '', bug_tracker: '',
created_date: '2019-09-05T11:59:22.987942Z', created_date: '2019-09-05T11:59:22.987942Z',
@ -194,7 +366,11 @@ const tasksDummyData = {
name: 'Image Task', name: 'Image Task',
size: 9, size: 9,
mode: 'annotation', mode: 'annotation',
owner: 1, owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'admin',
},
assignee: null, assignee: null,
bug_tracker: '', bug_tracker: '',
created_date: '2019-06-18T13:05:08.941304+03:00', created_date: '2019-06-18T13:05:08.941304+03:00',
@ -239,7 +415,11 @@ const tasksDummyData = {
name: 'Video Task', name: 'Video Task',
size: 5002, size: 5002,
mode: 'interpolation', mode: 'interpolation',
owner: 1, owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'admin',
},
assignee: null, assignee: null,
bug_tracker: '', bug_tracker: '',
created_date: '2019-06-21T16:34:49.199691+03:00', created_date: '2019-06-21T16:34:49.199691+03:00',
@ -558,7 +738,11 @@ const tasksDummyData = {
name: 'Test Task', name: 'Test Task',
size: 5002, size: 5002,
mode: 'interpolation', mode: 'interpolation',
owner: 2, owner: {
url: 'http://localhost:7000/api/v1/users/2',
id: 2,
username: 'bsekache',
},
assignee: null, assignee: null,
bug_tracker: '', bug_tracker: '',
created_date: '2019-05-16T13:08:00.621747+03:00', created_date: '2019-05-16T13:08:00.621747+03:00',
@ -767,7 +951,11 @@ const tasksDummyData = {
name: 'Video', name: 'Video',
size: 75, size: 75,
mode: 'interpolation', mode: 'interpolation',
owner: 1, owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'admin',
},
assignee: null, assignee: null,
bug_tracker: '', bug_tracker: '',
created_date: '2019-05-15T11:40:19.487999+03:00', created_date: '2019-05-15T11:40:19.487999+03:00',
@ -964,7 +1152,11 @@ const tasksDummyData = {
name: 'Labels Set', name: 'Labels Set',
size: 9, size: 9,
mode: 'annotation', mode: 'annotation',
owner: 1, owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'admin',
},
assignee: null, assignee: null,
bug_tracker: 'http://bugtracker.com/issue12345', bug_tracker: 'http://bugtracker.com/issue12345',
created_date: '2019-05-13T15:35:29.871003+03:00', created_date: '2019-05-13T15:35:29.871003+03:00',
@ -2328,6 +2520,7 @@ const frameMetaDummyData = {
module.exports = { module.exports = {
tasksDummyData, tasksDummyData,
projectsDummyData,
aboutDummyData, aboutDummyData,
shareDummyData, shareDummyData,
usersDummyData, usersDummyData,

@ -4,6 +4,7 @@
const { const {
tasksDummyData, tasksDummyData,
projectsDummyData,
aboutDummyData, aboutDummyData,
formatsDummyData, formatsDummyData,
shareDummyData, shareDummyData,
@ -13,6 +14,22 @@ const {
frameMetaDummyData, frameMetaDummyData,
} = require('./dummy-data.mock'); } = require('./dummy-data.mock');
function QueryStringToJSON(query) {
const pairs = [...new URLSearchParams(query).entries()];
const result = {};
for (const pair of pairs) {
const [key, value] = pair;
if (['id'].includes(key)) {
result[key] = +value;
} else {
result[key] = value;
}
}
return JSON.parse(JSON.stringify(result));
}
class ServerProxy { class ServerProxy {
constructor() { constructor() {
async function about() { async function about() {
@ -55,23 +72,65 @@ class ServerProxy {
return null; return null;
} }
async function getTasks(filter = '') { async function getProjects(filter = '') {
function QueryStringToJSON(query) { const queries = QueryStringToJSON(filter);
const pairs = [...new URLSearchParams(query).entries()]; const result = projectsDummyData.results.filter((x) => {
for (const key in queries) {
const result = {}; if (Object.prototype.hasOwnProperty.call(queries, key)) {
for (const pair of pairs) { // TODO: Particular match for some fields is not checked
const [key, value] = pair; if (queries[key] !== x[key]) {
if (['id'].includes(key)) { return false;
result[key] = +value; }
} else {
result[key] = value;
} }
} }
return JSON.parse(JSON.stringify(result)); return true;
});
return result;
}
async function saveProject(id, projectData) {
const object = projectsDummyData.results.filter((project) => project.id === id)[0];
for (const prop in projectData) {
if (
Object.prototype.hasOwnProperty.call(projectData, prop)
&& Object.prototype.hasOwnProperty.call(object, prop)
) {
object[prop] = projectData[prop];
}
}
}
async function createProject(projectData) {
const id = Math.max(...projectsDummyData.results.map((el) => el.id)) + 1;
projectsDummyData.results.push({
id,
url: `http://localhost:7000/api/v1/projects/${id}`,
name: projectData.name,
owner: 1,
assignee: null,
bug_tracker: projectData.bug_tracker,
created_date: '2019-05-16T13:08:00.621747+03:00',
updated_date: '2019-05-16T13:08:00.621797+03:00',
status: 'annotation',
tasks: [],
labels: JSON.parse(JSON.stringify(projectData.labels)),
});
const createdProject = await getProjects(`?id=${id}`);
return createdProject[0];
}
async function deleteProject(id) {
const projects = projectsDummyData.results;
const project = projects.filter((el) => el.id === id)[0];
if (project) {
projects.splice(projects.indexOf(project), 1);
} }
}
async function getTasks(filter = '') {
// Emulation of a query filter // Emulation of a query filter
const queries = QueryStringToJSON(filter); const queries = QueryStringToJSON(filter);
const result = tasksDummyData.results.filter((x) => { const result = tasksDummyData.results.filter((x) => {
@ -94,8 +153,8 @@ class ServerProxy {
const object = tasksDummyData.results.filter((task) => task.id === id)[0]; const object = tasksDummyData.results.filter((task) => task.id === id)[0];
for (const prop in taskData) { for (const prop in taskData) {
if ( if (
Object.prototype.hasOwnProperty.call(taskData, prop) && Object.prototype.hasOwnProperty.call(taskData, prop)
Object.prototype.hasOwnProperty.call(object, prop) && Object.prototype.hasOwnProperty.call(object, prop)
) { ) {
object[prop] = taskData[prop]; object[prop] = taskData[prop];
} }
@ -108,9 +167,13 @@ class ServerProxy {
id, id,
url: `http://localhost:7000/api/v1/tasks/${id}`, url: `http://localhost:7000/api/v1/tasks/${id}`,
name: taskData.name, name: taskData.name,
project_id: taskData.project_id || null,
size: 5000, size: 5000,
mode: 'interpolation', mode: 'interpolation',
owner: 2, owner: {
id: 2,
username: 'bsekache',
},
assignee: null, assignee: null,
bug_tracker: taskData.bug_tracker, bug_tracker: taskData.bug_tracker,
created_date: '2019-05-16T13:08:00.621747+03:00', created_date: '2019-05-16T13:08:00.621747+03:00',
@ -175,8 +238,8 @@ class ServerProxy {
for (const prop in jobData) { for (const prop in jobData) {
if ( if (
Object.prototype.hasOwnProperty.call(jobData, prop) && Object.prototype.hasOwnProperty.call(jobData, prop)
Object.prototype.hasOwnProperty.call(object, prop) && Object.prototype.hasOwnProperty.call(object, prop)
) { ) {
object[prop] = jobData[prop]; object[prop] = jobData[prop];
} }
@ -260,6 +323,16 @@ class ServerProxy {
writable: false, writable: false,
}, },
projects: {
value: Object.freeze({
get: getProjects,
save: saveProject,
create: createProject,
delete: deleteProject,
}),
writable: false,
},
tasks: { tasks: {
value: Object.freeze({ value: Object.freeze({
getTasks, getTasks,

@ -1 +1,2 @@
**/3rdparty/*.js **/3rdparty/*.js
webpack.config.js

@ -0,0 +1 @@
webpack.config.js

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.10.0", "version": "1.10.7",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -1179,6 +1179,11 @@
"integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=",
"dev": true "dev": true
}, },
"@types/lodash": {
"version": "4.14.165",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.165.tgz",
"integrity": "sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg=="
},
"@types/minimatch": { "@types/minimatch": {
"version": "3.0.3", "version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz",
@ -1208,9 +1213,9 @@
"dev": true "dev": true
}, },
"@types/react": { "@types/react": {
"version": "16.9.53", "version": "16.9.55",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.53.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-16.9.55.tgz",
"integrity": "sha512-4nW60Sd4L7+WMXH1D6jCdVftuW7j4Za6zdp6tJ33Rqv0nk1ZAmQKML9ZLD4H0dehA3FZxXR/GM8gXplf82oNGw==", "integrity": "sha512-6KLe6lkILeRwyyy7yG9rULKJ0sXplUsl98MGoCfpteXf9sPWFWWMknDcsvubcpaTdBuxtsLF6HDUwdApZL/xIg==",
"requires": { "requires": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.0.2" "csstype": "^3.0.2"
@ -1226,17 +1231,17 @@
} }
}, },
"@types/react-dom": { "@types/react-dom": {
"version": "16.9.8", "version": "16.9.9",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.8.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.9.tgz",
"integrity": "sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA==", "integrity": "sha512-jE16FNWO3Logq/Lf+yvEAjKzhpST/Eac8EMd1i4dgZdMczfgqC8EjpxwNgEe3SExHYLliabXDh9DEhhqnlXJhg==",
"requires": { "requires": {
"@types/react": "*" "@types/react": "*"
} }
}, },
"@types/react-redux": { "@types/react-redux": {
"version": "7.1.9", "version": "7.1.11",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.9.tgz", "resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.11.tgz",
"integrity": "sha512-mpC0jqxhP4mhmOl3P4ipRsgTgbNofMRXJb08Ms6gekViLj61v1hOZEKWDCyWsdONr6EjEA6ZHXC446wdywDe0w==", "integrity": "sha512-OjaFlmqy0CRbYKBoaWF84dub3impqnLJUrz4u8PRjDzaa4n1A2cVmjMV81shwXyAD5x767efhA8STFGJz/r1Zg==",
"requires": { "requires": {
"@types/hoist-non-react-statics": "^3.3.0", "@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*", "@types/react": "*",
@ -1849,9 +1854,9 @@
} }
}, },
"antd": { "antd": {
"version": "3.26.18", "version": "3.26.20",
"resolved": "https://registry.npmjs.org/antd/-/antd-3.26.18.tgz", "resolved": "https://registry.npmjs.org/antd/-/antd-3.26.20.tgz",
"integrity": "sha512-TPuacNJJNPji+LnapU46uWGqi+6JlyH75paMNs95IH0F7gGYtp4oSkua88gGsoAaUbDxTIF+cWI9mdIsr7ywlw==", "integrity": "sha512-VIous4ofZfxFtd9K1h9MpRX2sDDpj3QcOFi3YgIc9B/uyDli/GlLb8SWKfQfJaMkaxwatIv503dag2Tog+hiEg==",
"requires": { "requires": {
"@ant-design/create-react-context": "^0.2.4", "@ant-design/create-react-context": "^0.2.4",
"@ant-design/icons": "~2.1.1", "@ant-design/icons": "~2.1.1",
@ -3774,11 +3779,10 @@
} }
}, },
"create-react-class": { "create-react-class": {
"version": "15.6.3", "version": "15.7.0",
"resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz", "resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.7.0.tgz",
"integrity": "sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==", "integrity": "sha512-QZv4sFWG9S5RUvkTYWbflxeZX+JG7Cz0Tn33rQBJ+WFQTqTfUTjMjiv9tnfXazjsO5r0KhPs+AqCjyrQX6h2ng==",
"requires": { "requires": {
"fbjs": "^0.8.9",
"loose-envify": "^1.3.1", "loose-envify": "^1.3.1",
"object-assign": "^4.1.1" "object-assign": "^4.1.1"
} }
@ -3952,9 +3956,9 @@
} }
}, },
"csstype": { "csstype": {
"version": "3.0.4", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.4.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.5.tgz",
"integrity": "sha512-xc8DUsCLmjvCfoD7LTGE0ou2MIWLx0K9RCZwSHMOdynqRsP4MtUcLeqh1HcQ2dInwDTqn+3CE0/FZh1et+p4jA==" "integrity": "sha512-uVDi8LpBUKQj6sdxNaTetL6FpeCqTjOvAQuQUa/qAqq8oOd4ivkbhgnqayl0dnPal8Tb/yB1tF+gOvCBiicaiQ=="
}, },
"currently-unhandled": { "currently-unhandled": {
"version": "0.4.1", "version": "0.4.1",
@ -25949,9 +25953,14 @@
} }
}, },
"lodash": { "lodash": {
"version": "4.17.19", "version": "4.17.20",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA=="
},
"lodash-es": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.15.tgz",
"integrity": "sha512-rlrc3yU3+JNOpZ9zj5pQtxnx2THmvRykwL4Xlxoa8I9lHBlVbbyPhgyPMioxVZ4NqyxaVVtaJnzsyOidQIhyyQ=="
}, },
"lodash._reinterpolate": { "lodash._reinterpolate": {
"version": "3.0.0", "version": "3.0.0",
@ -28679,9 +28688,9 @@
} }
}, },
"rc-switch": { "rc-switch": {
"version": "1.9.0", "version": "1.9.2",
"resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-1.9.0.tgz", "resolved": "https://registry.npmjs.org/rc-switch/-/rc-switch-1.9.2.tgz",
"integrity": "sha512-Isas+egaK6qSk64jaEw4GgPStY4umYDbT7ZY93bZF1Af+b/JEsKsJdNOU2qG3WI0Z6tXo2DDq0kJCv8Yhu0zww==", "integrity": "sha512-qaK7mY4FLDKy99Hq3A1tf8CcqfzKtHp9LPX8WTnZ0MzdHCTneSARb1XD7Eqeu8BactasYGsi2bF9p18Q+/5JEw==",
"requires": { "requires": {
"classnames": "^2.2.1", "classnames": "^2.2.1",
"prop-types": "^15.5.6", "prop-types": "^15.5.6",
@ -28860,12 +28869,13 @@
} }
}, },
"react-color": { "react-color": {
"version": "2.18.1", "version": "2.19.3",
"resolved": "https://registry.npmjs.org/react-color/-/react-color-2.18.1.tgz", "resolved": "https://registry.npmjs.org/react-color/-/react-color-2.19.3.tgz",
"integrity": "sha512-X5XpyJS6ncplZs74ak0JJoqPi+33Nzpv5RYWWxn17bslih+X7OlgmfpmGC1fNvdkK7/SGWYf1JJdn7D2n5gSuQ==", "integrity": "sha512-LEeGE/ZzNLIsFWa1TMe8y5VYqr7bibneWmvJwm1pCn/eNmrabWDh659JSPn9BuaMpEfU83WTOJfnCcjDZwNQTA==",
"requires": { "requires": {
"@icons/material": "^0.2.4", "@icons/material": "^0.2.4",
"lodash": "^4.17.11", "lodash": "^4.17.15",
"lodash-es": "^4.17.15",
"material-colors": "^1.2.1", "material-colors": "^1.2.1",
"prop-types": "^15.5.10", "prop-types": "^15.5.10",
"reactcss": "^1.2.0", "reactcss": "^1.2.0",
@ -28923,15 +28933,43 @@
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
}, },
"react-redux": { "react-redux": {
"version": "7.2.1", "version": "7.2.2",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.1.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.2.tgz",
"integrity": "sha512-T+VfD/bvgGTUA74iW9d2i5THrDQWbweXP0AVNI8tNd1Rk5ch1rnMiJkDD67ejw7YBKM4+REvcvqRuWJb7BLuEg==", "integrity": "sha512-8+CQ1EvIVFkYL/vu6Olo7JFLWop1qRUeb46sGtIMDCSpgwPQq8fPLpirIB0iTqFe9XYEFPHssdX8/UwN6pAkEA==",
"requires": { "requires": {
"@babel/runtime": "^7.5.5", "@babel/runtime": "^7.12.1",
"hoist-non-react-statics": "^3.3.0", "hoist-non-react-statics": "^3.3.2",
"loose-envify": "^1.4.0", "loose-envify": "^1.4.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react-is": "^16.9.0" "react-is": "^16.13.1"
},
"dependencies": {
"@babel/runtime": {
"version": "7.12.5",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.12.5.tgz",
"integrity": "sha512-plcc+hbExy3McchJCEQG3knOsuh3HH+Prx1P6cLIkET/0dLuQDEnrT+s27Axgc9bqfsmNUNHfscgMUdBpC9xfg==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"requires": {
"react-is": "^16.7.0"
}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
},
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
}
} }
}, },
"react-router": { "react-router": {
@ -32249,9 +32287,9 @@
"dev": true "dev": true
}, },
"whatwg-fetch": { "whatwg-fetch": {
"version": "3.4.1", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.4.1.tgz", "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.5.0.tgz",
"integrity": "sha512-sofZVzE1wKwO+EYPbWfiwzaKovWiZXf4coEzjGP9b2GBVgQRLQUZ2QcuPpQExGDAW5GItpEm6Tl4OU5mywnAoQ==" "integrity": "sha512-jXkLtsR42xhXg7akoDKvKWE40eJeI+2KZqcp2h3NsOrRnDvtWX36KcKl30dy+hxECivdk2BVUHVNrPtoMBUx6A=="
}, },
"which": { "which": {
"version": "1.3.1", "version": "1.3.1",

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.10.0", "version": "1.10.7",
"description": "CVAT single-page application", "description": "CVAT single-page application",
"main": "src/index.tsx", "main": "src/index.tsx",
"scripts": { "scripts": {
@ -47,30 +47,32 @@
"worker-loader": "^2.0.0" "worker-loader": "^2.0.0"
}, },
"dependencies": { "dependencies": {
"@types/lodash": "^4.14.165",
"@types/platform": "^1.3.3", "@types/platform": "^1.3.3",
"@types/react": "^16.9.53", "@types/react": "^16.9.55",
"@types/react-color": "^3.0.4", "@types/react-color": "^3.0.4",
"@types/react-dom": "^16.9.0", "@types/react-dom": "^16.9.9",
"@types/react-redux": "^7.1.2", "@types/react-redux": "^7.1.11",
"@types/react-router": "^5.0.5", "@types/react-router": "^5.0.5",
"@types/react-router-dom": "^5.1.6", "@types/react-router-dom": "^5.1.6",
"@types/react-share": "^3.0.3", "@types/react-share": "^3.0.3",
"@types/redux-logger": "^3.0.8", "@types/redux-logger": "^3.0.8",
"antd": "^3.26.18", "antd": "^3.26.20",
"copy-to-clipboard": "^3.3.1", "copy-to-clipboard": "^3.3.1",
"cvat-canvas": "file:../cvat-canvas", "cvat-canvas": "file:../cvat-canvas",
"cvat-core": "file:../cvat-core", "cvat-core": "file:../cvat-core",
"dotenv-webpack": "^1.8.0", "dotenv-webpack": "^1.8.0",
"error-stack-parser": "^2.0.6", "error-stack-parser": "^2.0.6",
"lodash": "^4.17.20",
"moment": "^2.29.1", "moment": "^2.29.1",
"platform": "^1.3.6", "platform": "^1.3.6",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^16.14.0", "react": "^16.14.0",
"react-color": "^2.18.1", "react-color": "^2.19.3",
"react-cookie": "^4.0.3", "react-cookie": "^4.0.3",
"react-dom": "^16.14.0", "react-dom": "^16.14.0",
"react-hotkeys": "^2.0.0", "react-hotkeys": "^2.0.0",
"react-redux": "^7.1.1", "react-redux": "^7.2.2",
"react-router": "^5.1.0", "react-router": "^5.1.0",
"react-router-dom": "^5.1.0", "react-router-dom": "^5.1.0",
"react-share": "^3.0.1", "react-share": "^3.0.1",

@ -2,7 +2,9 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { AnyAction, Dispatch, ActionCreator, Store } from 'redux'; import {
AnyAction, Dispatch, ActionCreator, Store,
} from 'redux';
import { ThunkAction } from 'utils/redux'; import { ThunkAction } from 'utils/redux';
import { import {
@ -244,7 +246,9 @@ export function switchZLayer(cur: number): AnyAction {
export function fetchAnnotationsAsync(): ThunkAction { export function fetchAnnotationsAsync(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try { try {
const { filters, frame, showAllInterpolationTracks, jobInstance } = receiveAnnotationsParameters(); const {
filters, frame, showAllInterpolationTracks, jobInstance,
} = receiveAnnotationsParameters();
const states = await jobInstance.annotations.get(frame, showAllInterpolationTracks, filters); const states = await jobInstance.annotations.get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states); const [minZ, maxZ] = computeZRange(states);
@ -936,6 +940,10 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
throw new Error(`Task ${tid} doesn't contain the job ${jid}`); throw new Error(`Task ${tid} doesn't contain the job ${jid}`);
} }
if (!task.labels.length && task.projectId) {
throw new Error(`Project ${task.projectId} does not contain any label`);
}
const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame); const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame);
const frameData = await job.frames.get(frameNumber); const frameData = await job.frames.get(frameNumber);
// call first getting of frame data before rendering interface // call first getting of frame data before rendering interface
@ -1096,7 +1104,9 @@ export function splitTrack(enabled: boolean): AnyAction {
export function updateAnnotationsAsync(statesToUpdate: any[]): ThunkAction { export function updateAnnotationsAsync(statesToUpdate: any[]): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const { jobInstance, filters, frame, showAllInterpolationTracks } = receiveAnnotationsParameters(); const {
jobInstance, filters, frame, showAllInterpolationTracks,
} = receiveAnnotationsParameters();
try { try {
if (statesToUpdate.some((state: any): boolean => state.updateFlags.zOrder)) { if (statesToUpdate.some((state: any): boolean => state.updateFlags.zOrder)) {

@ -0,0 +1,171 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { Dispatch, ActionCreator } from 'redux';
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import { ProjectsQuery, CombinedState } from 'reducers/interfaces';
import { getTasksSuccess, updateTaskSuccess } from 'actions/tasks-actions';
import { getCVATStore } from 'cvat-store';
import getCore from 'cvat-core-wrapper';
const cvat = getCore();
export enum ProjectsActionTypes {
UPDATE_PROJECTS_GETTING_QUERY = 'UPDATE_PROJECTS_GETTING_QUERY',
GET_PROJECTS = 'GET_PROJECTS',
GET_PROJECTS_SUCCESS = 'GET_PROJECTS_SUCCESS',
GET_PROJECTS_FAILED = 'GET_PROJECTS_FAILED',
CREATE_PROJECT = 'CREATE_PROJECT',
CREATE_PROJECT_SUCCESS = 'CREATE_PROJECT_SUCCESS',
CREATE_PROJECT_FAILED = 'CREATE_PROJECT_FAILED',
UPDATE_PROJECT = 'UPDATE_PROJECT',
UPDATE_PROJECT_SUCCESS = 'UPDATE_PROJECT_SUCCESS',
UPDATE_PROJECT_FAILED = 'UPDATE_PROJECT_FAILED',
DELETE_PROJECT = 'DELETE_PROJECT',
DELETE_PROJECT_SUCCESS = 'DELETE_PROJECT_SUCCESS',
DELETE_PROJECT_FAILED = 'DELETE_PROJECT_FAILED',
}
// prettier-ignore
const projectActions = {
getProjects: () => createAction(ProjectsActionTypes.GET_PROJECTS),
getProjectsSuccess: (array: any[], count: number) => (
createAction(ProjectsActionTypes.GET_PROJECTS_SUCCESS, { array, count })
),
getProjectsFailed: (error: any) => createAction(ProjectsActionTypes.GET_PROJECTS_FAILED, { error }),
updateProjectsGettingQuery: (query: Partial<ProjectsQuery>) => (
createAction(ProjectsActionTypes.UPDATE_PROJECTS_GETTING_QUERY, { query })
),
createProject: () => createAction(ProjectsActionTypes.CREATE_PROJECT),
createProjectSuccess: (projectId: number) => (
createAction(ProjectsActionTypes.CREATE_PROJECT_SUCCESS, { projectId })
),
createProjectFailed: (error: any) => createAction(ProjectsActionTypes.CREATE_PROJECT_FAILED, { error }),
updateProject: () => createAction(ProjectsActionTypes.UPDATE_PROJECT),
updateProjectSuccess: (project: any) => createAction(ProjectsActionTypes.UPDATE_PROJECT_SUCCESS, { project }),
updateProjectFailed: (project: any, error: any) => (
createAction(ProjectsActionTypes.UPDATE_PROJECT_FAILED, { project, error })
),
deleteProject: (projectId: number) => createAction(ProjectsActionTypes.DELETE_PROJECT, { projectId }),
deleteProjectSuccess: (projectId: number) => (
createAction(ProjectsActionTypes.DELETE_PROJECT_SUCCESS, { projectId })
),
deleteProjectFailed: (projectId: number, error: any) => (
createAction(ProjectsActionTypes.DELETE_PROJECT_FAILED, { projectId, error })
),
};
export type ProjectActions = ActionUnion<typeof projectActions>;
export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(projectActions.getProjects());
dispatch(projectActions.updateProjectsGettingQuery(query));
// Clear query object from null fields
const filteredQuery: Partial<ProjectsQuery> = {
page: 1,
...query,
};
for (const key in filteredQuery) {
if (filteredQuery[key] === null || typeof filteredQuery[key] === 'undefined') {
delete filteredQuery[key];
}
}
let result = null;
try {
result = await cvat.projects.get(filteredQuery);
} catch (error) {
dispatch(projectActions.getProjectsFailed(error));
return;
}
const array = Array.from(result);
const tasks: any[] = [];
const taskPreviewPromises: Promise<any>[] = [];
for (const project of array) {
taskPreviewPromises.push(
...(project as any).tasks.map((task: any): string => {
tasks.push(task);
return (task as any).frames.preview().catch(() => '');
}),
);
}
const taskPreviews = await Promise.all(taskPreviewPromises);
dispatch(projectActions.getProjectsSuccess(array, result.count));
const store = getCVATStore();
const state: CombinedState = store.getState();
if (!state.tasks.fetching) {
dispatch(
getTasksSuccess(tasks, taskPreviews, tasks.length, {
page: 1,
assignee: null,
id: null,
mode: null,
name: null,
owner: null,
search: null,
status: null,
}),
);
}
};
}
export function createProjectAsync(data: any): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const projectInstance = new cvat.classes.Project(data);
dispatch(projectActions.createProject());
try {
const savedProject = await projectInstance.save();
dispatch(projectActions.createProjectSuccess(savedProject.id));
} catch (error) {
dispatch(projectActions.createProjectFailed(error));
}
};
}
export function updateProjectAsync(projectInstance: any): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
dispatch(projectActions.updateProject());
await projectInstance.save();
const [project] = await cvat.projects.get({ id: projectInstance.id });
dispatch(projectActions.updateProjectSuccess(project));
project.tasks.forEach((task: any) => {
dispatch(updateTaskSuccess(task));
});
} catch (error) {
let project = null;
try {
[project] = await cvat.projects.get({ id: projectInstance.id });
} catch (fetchError) {
dispatch(projectActions.updateProjectFailed(projectInstance, error));
return;
}
dispatch(projectActions.updateProjectFailed(project, error));
}
};
}
export function deleteProjectAsync(projectInstance: any): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(projectActions.deleteProject(projectInstance.id));
try {
await projectInstance.delete();
dispatch(projectActions.deleteProjectSuccess(projectInstance.id));
} catch (error) {
dispatch(projectActions.deleteProjectFailed(projectInstance.id, error));
}
};
}

@ -46,7 +46,7 @@ function getTasks(): AnyAction {
return action; return action;
} }
function getTasksSuccess(array: any[], previews: string[], count: number, query: TasksQuery): AnyAction { export function getTasksSuccess(array: any[], previews: string[], count: number, query: TasksQuery): AnyAction {
const action = { const action = {
type: TasksActionTypes.GET_TASKS_SUCCESS, type: TasksActionTypes.GET_TASKS_SUCCESS,
payload: { payload: {
@ -93,25 +93,11 @@ export function getTasksAsync(query: TasksQuery): ThunkAction<Promise<void>, {},
} }
const array = Array.from(result); const array = Array.from(result);
const previews = []; const promises = array.map((task): string => (task as any).frames.preview().catch(''));
const promises = array.map((task): string => (task as any).frames.preview());
dispatch(getInferenceStatusAsync()); dispatch(getInferenceStatusAsync());
for (const promise of promises) { dispatch(getTasksSuccess(array, await Promise.all(promises), result.count, query));
try {
// a tricky moment
// await is okay in loop in this case, there aren't any performance bottleneck
// because all server requests have been already sent in parallel
// eslint-disable-next-line no-await-in-loop
previews.push(await promise);
} catch (error) {
previews.push('');
}
}
dispatch(getTasksSuccess(array, previews, result.count, query));
}; };
} }
@ -381,6 +367,9 @@ export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, A
use_cache: data.advanced.useCache, use_cache: data.advanced.useCache,
}; };
if (data.projectId) {
description.project_id = data.projectId;
}
if (data.advanced.bugTracker) { if (data.advanced.bugTracker) {
description.bug_tracker = data.advanced.bugTracker; description.bug_tracker = data.advanced.bugTracker;
} }
@ -448,7 +437,7 @@ function updateTask(): AnyAction {
return action; return action;
} }
function updateTaskSuccess(task: any): AnyAction { export function updateTaskSuccess(task: any): AnyAction {
const action = { const action = {
type: TasksActionTypes.UPDATE_TASK_SUCCESS, type: TasksActionTypes.UPDATE_TASK_SUCCESS,
payload: { task }, payload: { task },

@ -1,36 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core-wrapper';
const core = getCore();
export enum UsersActionTypes {
GET_USERS = 'GET_USERS',
GET_USERS_SUCCESS = 'GET_USERS_SUCCESS',
GET_USERS_FAILED = 'GET_USERS_FAILED',
}
const usersActions = {
getUsers: () => createAction(UsersActionTypes.GET_USERS),
getUsersSuccess: (users: any[]) => createAction(UsersActionTypes.GET_USERS_SUCCESS, { users }),
getUsersFailed: (error: any) => createAction(UsersActionTypes.GET_USERS_FAILED, { error }),
};
export type UsersActions = ActionUnion<typeof usersActions>;
export function getUsersAsync(): ThunkAction {
return async (dispatch): Promise<void> => {
dispatch(usersActions.getUsers());
try {
const users = await core.users.get();
const wrappedUsers = users.map((userData: any): any => new core.classes.User(userData));
dispatch(usersActions.getUsersSuccess(wrappedUsers));
} catch (error) {
dispatch(usersActions.getUsersFailed(error));
}
};
}

@ -68,7 +68,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { const {
annotation: { appearanceCollapsed }, annotation: { appearanceCollapsed },
settings: { settings: {
shapes: { colorBy, opacity, selectedOpacity, outlined, outlineColor, showBitmap, showProjections }, shapes: {
colorBy, opacity, selectedOpacity, outlined, outlineColor, showBitmap, showProjections,
},
}, },
} = state; } = state;
@ -153,16 +155,33 @@ function AppearanceBlock(props: Props): JSX.Element {
<Collapse.Panel header={<Text strong>Appearance</Text>} key='appearance'> <Collapse.Panel header={<Text strong>Appearance</Text>} key='appearance'>
<div className='cvat-objects-appearance-content'> <div className='cvat-objects-appearance-content'>
<Text type='secondary'>Color by</Text> <Text type='secondary'>Color by</Text>
<Radio.Group value={colorBy} onChange={changeShapesColorBy}> <Radio.Group
className='cvat-appearance-color-by-radio-group'
value={colorBy}
onChange={changeShapesColorBy}
>
<Radio.Button value={ColorBy.LABEL}>{ColorBy.LABEL}</Radio.Button> <Radio.Button value={ColorBy.LABEL}>{ColorBy.LABEL}</Radio.Button>
<Radio.Button value={ColorBy.INSTANCE}>{ColorBy.INSTANCE}</Radio.Button> <Radio.Button value={ColorBy.INSTANCE}>{ColorBy.INSTANCE}</Radio.Button>
<Radio.Button value={ColorBy.GROUP}>{ColorBy.GROUP}</Radio.Button> <Radio.Button value={ColorBy.GROUP}>{ColorBy.GROUP}</Radio.Button>
</Radio.Group> </Radio.Group>
<Text type='secondary'>Opacity</Text> <Text type='secondary'>Opacity</Text>
<Slider onChange={changeShapesOpacity} value={opacity} min={0} max={100} /> <Slider
className='cvat-appearance-opacity-slider'
onChange={changeShapesOpacity}
value={opacity}
min={0}
max={100}
/>
<Text type='secondary'>Selected opacity</Text> <Text type='secondary'>Selected opacity</Text>
<Slider onChange={changeSelectedShapesOpacity} value={selectedOpacity} min={0} max={100} /> <Slider
className='cvat-appearance-selected-opacity-slider'
onChange={changeSelectedShapesOpacity}
value={selectedOpacity}
min={0}
max={100}
/>
<Checkbox <Checkbox
className='cvat-appearance-outlinded-borders-checkbox'
onChange={(event: CheckboxChangeEvent) => { onChange={(event: CheckboxChangeEvent) => {
changeShapesOutlinedBorders(event.target.checked, outlineColor); changeShapesOutlinedBorders(event.target.checked, outlineColor);
}} }}
@ -175,15 +194,23 @@ function AppearanceBlock(props: Props): JSX.Element {
placement='top' placement='top'
resetVisible={false} resetVisible={false}
> >
<Button type='link' shape='circle'> <Button className='cvat-appearance-outlined-borders-button' type='link' shape='circle'>
<ColorizeIcon /> <ColorizeIcon />
</Button> </Button>
</ColorPicker> </ColorPicker>
</Checkbox> </Checkbox>
<Checkbox onChange={changeShowBitmap} checked={showBitmap}> <Checkbox
className='cvat-appearance-bitmap-checkbox'
onChange={changeShowBitmap}
checked={showBitmap}
>
Show bitmap Show bitmap
</Checkbox> </Checkbox>
<Checkbox onChange={changeShowProjections} checked={showProjections}> <Checkbox
className='cvat-appearance-cuboid-projections-checkbox'
onChange={changeShowProjections}
checked={showProjections}
>
Show projections Show projections
</Checkbox> </Checkbox>
</div> </div>

@ -10,7 +10,9 @@ import Icon from 'antd/lib/icon';
import Layout from 'antd/lib/layout/layout'; import Layout from 'antd/lib/layout/layout';
import Slider, { SliderValue } from 'antd/lib/slider'; import Slider, { SliderValue } from 'antd/lib/slider';
import { ColorBy, GridColor, ObjectType, ContextMenuType, Workspace, ShapeType } from 'reducers/interfaces'; import {
ColorBy, GridColor, ObjectType, ContextMenuType, Workspace, ShapeType,
} from 'reducers/interfaces';
import { LogType } from 'cvat-logger'; import { LogType } from 'cvat-logger';
import { Canvas } from 'cvat-canvas-wrapper'; import { Canvas } from 'cvat-canvas-wrapper';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
@ -218,10 +220,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
this.updateCanvas(); this.updateCanvas();
} }
if ( if (prevProps.frame !== frameData.number && resetZoom && workspace !== Workspace.ATTRIBUTE_ANNOTATION) {
prevProps.frame !== frameData.number &&
((resetZoom && workspace !== Workspace.ATTRIBUTE_ANNOTATION) || workspace === Workspace.TAG_ANNOTATION)
) {
canvasInstance.html().addEventListener( canvasInstance.html().addEventListener(
'canvas.setup', 'canvas.setup',
() => { () => {
@ -312,7 +311,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
private onCanvasShapeDrawn = (event: any): void => { private onCanvasShapeDrawn = (event: any): void => {
const { jobInstance, activeLabelID, activeObjectType, frame, onShapeDrawn, onCreateAnnotations } = this.props; const {
jobInstance, activeLabelID, activeObjectType, frame, onShapeDrawn, onCreateAnnotations,
} = this.props;
if (!event.detail.continue) { if (!event.detail.continue) {
onShapeDrawn(); onShapeDrawn();
@ -335,7 +336,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}; };
private onCanvasObjectsMerged = (event: any): void => { private onCanvasObjectsMerged = (event: any): void => {
const { jobInstance, frame, onMergeAnnotations, onMergeObjects } = this.props; const {
jobInstance, frame, onMergeAnnotations, onMergeObjects,
} = this.props;
onMergeObjects(false); onMergeObjects(false);
@ -348,7 +351,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}; };
private onCanvasObjectsGroupped = (event: any): void => { private onCanvasObjectsGroupped = (event: any): void => {
const { jobInstance, frame, onGroupAnnotations, onGroupObjects } = this.props; const {
jobInstance, frame, onGroupAnnotations, onGroupObjects,
} = this.props;
onGroupObjects(false); onGroupObjects(false);
@ -357,7 +362,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}; };
private onCanvasTrackSplitted = (event: any): void => { private onCanvasTrackSplitted = (event: any): void => {
const { jobInstance, frame, onSplitAnnotations, onSplitTrack } = this.props; const {
jobInstance, frame, onSplitAnnotations, onSplitTrack,
} = this.props;
onSplitTrack(false); onSplitTrack(false);
@ -437,7 +444,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}; };
private onCanvasCursorMoved = async (event: any): Promise<void> => { private onCanvasCursorMoved = async (event: any): Promise<void> => {
const { jobInstance, activatedStateID, workspace, onActivateObject } = this.props; const {
jobInstance, activatedStateID, workspace, onActivateObject,
} = this.props;
if (workspace !== Workspace.STANDARD) { if (workspace !== Workspace.STANDARD) {
return; return;
@ -568,7 +577,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
private updateShapesView(): void { private updateShapesView(): void {
const { annotations, opacity, colorBy, outlined, outlineColor } = this.props; const {
annotations, opacity, colorBy, outlined, outlineColor,
} = this.props;
for (const state of annotations) { for (const state of annotations) {
let shapeColor = ''; let shapeColor = '';
@ -596,7 +607,9 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
private updateCanvas(): void { private updateCanvas(): void {
const { curZLayer, annotations, frameData, canvasInstance } = this.props; const {
curZLayer, annotations, frameData, canvasInstance,
} = this.props;
if (frameData !== null) { if (frameData !== null) {
canvasInstance.setup( canvasInstance.setup(

@ -22,23 +22,24 @@ interface Props {
onChange?: (value: string) => void; onChange?: (value: string) => void;
onVisibleChange?: (visible: boolean) => void; onVisibleChange?: (visible: boolean) => void;
placement?: placement?:
| 'left' | 'left'
| 'top' | 'top'
| 'right' | 'right'
| 'bottom' | 'bottom'
| 'topLeft' | 'topLeft'
| 'topRight' | 'topRight'
| 'bottomLeft' | 'bottomLeft'
| 'bottomRight' | 'bottomRight'
| 'leftTop' | 'leftTop'
| 'leftBottom' | 'leftBottom'
| 'rightTop' | 'rightTop'
| 'rightBottom' | 'rightBottom';
| undefined;
} }
function ColorPicker(props: Props, ref: React.Ref<any>): JSX.Element { function ColorPicker(props: Props, ref: React.Ref<any>): JSX.Element {
const { children, value, visible, resetVisible, onChange, onVisibleChange, placement } = props; const {
children, value, visible, resetVisible, onChange, onVisibleChange, placement,
} = props;
const [colorState, setColorState] = useState(value); const [colorState, setColorState] = useState(value);
const [pickerVisible, setPickerVisible] = useState(false); const [pickerVisible, setPickerVisible] = useState(false);
@ -55,7 +56,7 @@ function ColorPicker(props: Props, ref: React.Ref<any>): JSX.Element {
return ( return (
<Popover <Popover
content={ content={(
<> <>
<SketchPicker <SketchPicker
color={colorState} color={colorState}
@ -99,8 +100,8 @@ function ColorPicker(props: Props, ref: React.Ref<any>): JSX.Element {
</Col> </Col>
</Row> </Row>
</> </>
} )}
title={ title={(
<Row type='flex' justify='space-between' align='middle'> <Row type='flex' justify='space-between' align='middle'>
<Col span={12}> <Col span={12}>
<Text strong>Select color</Text> <Text strong>Select color</Text>
@ -118,7 +119,7 @@ function ColorPicker(props: Props, ref: React.Ref<any>): JSX.Element {
</Tooltip> </Tooltip>
</Col> </Col>
</Row> </Row>
} )}
placement={placement || 'left'} placement={placement || 'left'}
overlayClassName='cvat-label-color-picker' overlayClassName='cvat-label-color-picker'
trigger='click' trigger='click'

@ -33,6 +33,17 @@ function LabelItemComponent(props: Props): JSX.Element {
unlockStates, unlockStates,
} = props; } = props;
const classes = {
lock: {
enabled: { className: 'cvat-label-item-button-lock cvat-label-item-button-lock-enabled' },
disabled: { className: 'cvat-label-item-button-lock' },
},
hidden: {
enabled: { className: 'cvat-label-item-button-hidden cvat-label-item-button-hidden-enabled' },
disabled: { className: 'cvat-label-item-button-hidden' },
},
};
return ( return (
<Row <Row
type='flex' type='flex'
@ -51,16 +62,16 @@ function LabelItemComponent(props: Props): JSX.Element {
</Col> </Col>
<Col span={3}> <Col span={3}>
{statesLocked ? ( {statesLocked ? (
<Icon type='lock' onClick={unlockStates} /> <Icon {...classes.lock.enabled} type='lock' onClick={unlockStates} />
) : ( ) : (
<Icon type='unlock' onClick={lockStates} /> <Icon {...classes.lock.disabled} type='unlock' onClick={lockStates} />
)} )}
</Col> </Col>
<Col span={3}> <Col span={3}>
{statesHidden ? ( {statesHidden ? (
<Icon type='eye-invisible' onClick={showStates} /> <Icon {...classes.hidden.enabled} type='eye-invisible' onClick={showStates} />
) : ( ) : (
<Icon type='eye' onClick={hideStates} /> <Icon {...classes.hidden.disabled} type='eye' onClick={hideStates} />
)} )}
</Col> </Col>
</Row> </Row>

@ -23,7 +23,11 @@ function StatesOrderingSelectorComponent(props: StatesOrderingSelectorComponentP
return ( return (
<Col span={16}> <Col span={16}>
<Text strong>Sort by</Text> <Text strong>Sort by</Text>
<Select value={statesOrdering} onChange={changeStatesOrdering}> <Select
className='cvat-objects-sidebar-ordering-selector'
value={statesOrdering}
onChange={changeStatesOrdering}
>
<Select.Option key={StatesOrdering.ID_DESCENT} value={StatesOrdering.ID_DESCENT}> <Select.Option key={StatesOrdering.ID_DESCENT} value={StatesOrdering.ID_DESCENT}>
{StatesOrdering.ID_DESCENT} {StatesOrdering.ID_DESCENT}
</Select.Option> </Select.Option>

@ -109,7 +109,7 @@ function PlayerButtons(props: Props): JSX.Element {
<Popover <Popover
trigger='contextMenu' trigger='contextMenu'
placement='bottom' placement='bottom'
content={ content={(
<> <>
<Tooltip title={`${prevRegularText}`} mouseLeaveDelay={0}> <Tooltip title={`${prevRegularText}`} mouseLeaveDelay={0}>
<Icon <Icon
@ -139,7 +139,7 @@ function PlayerButtons(props: Props): JSX.Element {
/> />
</Tooltip> </Tooltip>
</> </>
} )}
> >
<Tooltip <Tooltip
placement='top' placement='top'
@ -163,7 +163,7 @@ function PlayerButtons(props: Props): JSX.Element {
<Popover <Popover
trigger='contextMenu' trigger='contextMenu'
placement='bottom' placement='bottom'
content={ content={(
<> <>
<Tooltip title={`${nextRegularText}`} mouseLeaveDelay={0}> <Tooltip title={`${nextRegularText}`} mouseLeaveDelay={0}>
<Icon <Icon
@ -193,7 +193,7 @@ function PlayerButtons(props: Props): JSX.Element {
/> />
</Tooltip> </Tooltip>
</> </>
} )}
> >
<Tooltip placement='top' mouseLeaveDelay={0} title={`${nextButtonTooltipMessage} ${nextFrameShortcut}`}> <Tooltip placement='top' mouseLeaveDelay={0} title={`${nextButtonTooltipMessage} ${nextFrameShortcut}`}>
{nextButton} {nextButton}

@ -0,0 +1,160 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, {
useState, useRef, useEffect, Component,
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router';
import { Col, Row } from 'antd/lib/grid';
import Text from 'antd/lib/typography/Text';
import Form, { FormComponentProps, WrappedFormUtils } from 'antd/lib/form/Form';
import Button from 'antd/lib/button';
import Input from 'antd/lib/input';
import notification from 'antd/lib/notification';
import patterns from 'utils/validation-patterns';
import { CombinedState } from 'reducers/interfaces';
import LabelsEditor from 'components/labels-editor/labels-editor';
import { createProjectAsync } from 'actions/projects-actions';
type FormRefType = Component<FormComponentProps<any>, any, any> & WrappedFormUtils;
const ProjectNameEditor = Form.create<FormComponentProps>()(
(props: FormComponentProps): JSX.Element => {
const { form } = props;
const { getFieldDecorator } = form;
return (
<Form onSubmit={(e): void => e.preventDefault()}>
<Form.Item hasFeedback label={<span>Name</span>}>
{getFieldDecorator('name', {
rules: [
{
required: true,
message: 'Please, specify a name',
},
],
})(<Input />)}
</Form.Item>
</Form>
);
},
);
const AdvanvedConfigurationForm = Form.create<FormComponentProps>()(
(props: FormComponentProps): JSX.Element => {
const { form } = props;
const { getFieldDecorator } = form;
return (
<Form onSubmit={(e): void => e.preventDefault()}>
<Form.Item
label={<span>Issue tracker</span>}
extra='Attach issue tracker where the project is described'
hasFeedback
>
{getFieldDecorator('bug_tracker', {
rules: [
{
validator: (_, value, callback): void => {
if (value && !patterns.validateURL.pattern.test(value)) {
callback('Issue tracker must be URL');
} else {
callback();
}
},
},
],
})(<Input />)}
</Form.Item>
</Form>
);
},
);
export default function CreateProjectContent(): JSX.Element {
const [projectLabels, setProjectLabels] = useState<any[]>([]);
const shouldShowNotification = useRef(false);
const nameFormRef = useRef<FormRefType>(null);
const advancedFormRef = useRef<FormRefType>(null);
const dispatch = useDispatch();
const history = useHistory();
const newProjectId = useSelector((state: CombinedState) => state.projects.activities.creates.id);
useEffect(() => {
if (Number.isInteger(newProjectId) && shouldShowNotification.current) {
const btn = <Button onClick={() => history.push(`/projects/${newProjectId}`)}>Open project</Button>;
// Clear new project forms
if (nameFormRef.current) nameFormRef.current.resetFields();
if (advancedFormRef.current) advancedFormRef.current.resetFields();
setProjectLabels([]);
notification.info({
message: 'The project has been created',
btn,
});
}
shouldShowNotification.current = true;
}, [newProjectId]);
const onSumbit = (): void => {
interface Project {
[key: string]: any;
}
const projectData: Project = {};
if (nameFormRef.current !== null) {
nameFormRef.current.validateFields((error, value) => {
if (!error) {
projectData.name = value.name;
}
});
}
if (advancedFormRef.current !== null) {
advancedFormRef.current.validateFields((error, values) => {
if (!error) {
for (const [field, value] of Object.entries(values)) {
projectData[field] = value;
}
}
});
}
projectData.labels = projectLabels;
if (!projectData.name) return;
dispatch(createProjectAsync(projectData));
};
return (
<Row type='flex' justify='start' align='middle' className='cvat-create-project-content'>
<Col span={24}>
<ProjectNameEditor ref={nameFormRef} />
</Col>
<Col span={24}>
<Text className='cvat-text-color'>Labels:</Text>
<LabelsEditor
labels={projectLabels}
onSubmit={(newLabels): void => {
setProjectLabels(newLabels);
}}
/>
</Col>
<Col span={24}>
<AdvanvedConfigurationForm ref={advancedFormRef} />
</Col>
<Col span={24}>
<Button type='primary' onClick={onSumbit}>
Submit
</Button>
</Col>
</Row>
);
}

@ -0,0 +1,21 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React from 'react';
import { Row, Col } from 'antd/lib/grid';
import Text from 'antd/lib/typography/Text';
import CreateProjectContent from './create-project-content';
export default function CreateProjectPageComponent(): JSX.Element {
return (
<Row type='flex' justify='center' align='top' className='cvat-create-task-form-wrapper'>
<Col md={20} lg={16} xl={14} xxl={9}>
<Text className='cvat-title'>Create a new project</Text>
<CreateProjectContent />
</Col>
</Row>
);
}

@ -0,0 +1,42 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
@import '../../base.scss';
.cvat-create-project-form-wrapper {
text-align: center;
padding-top: $grid-unit-size * 5;
overflow-y: auto;
height: 90%;
position: fixed;
width: 100%;
> div > span {
font-size: $grid-unit-size * 4;
}
}
.cvat-create-project-content {
margin-top: $grid-unit-size * 2;
width: 100%;
height: auto;
border: 1px solid $border-color-1;
border-radius: 3px;
padding: $grid-unit-size * 2;
background: $background-color-1;
text-align: initial;
> div:not(first-child) {
margin-top: $grid-unit-size;
}
> div:nth-child(4) {
display: flex;
justify-content: flex-end;
> button {
width: $grid-unit-size * 15;
}
}
}

@ -13,12 +13,14 @@ import notification from 'antd/lib/notification';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import ConnectedFileManager from 'containers/file-manager/file-manager'; import ConnectedFileManager from 'containers/file-manager/file-manager';
import LabelsEditor from 'components/labels-editor/labels-editor';
import { Files } from 'components/file-manager/file-manager';
import BasicConfigurationForm, { BaseConfiguration } from './basic-configuration-form'; import BasicConfigurationForm, { BaseConfiguration } from './basic-configuration-form';
import ProjectSearchField from './project-search-field';
import AdvancedConfigurationForm, { AdvancedConfiguration } from './advanced-configuration-form'; import AdvancedConfigurationForm, { AdvancedConfiguration } from './advanced-configuration-form';
import LabelsEditor from '../labels-editor/labels-editor';
import { Files } from '../file-manager/file-manager';
export interface CreateTaskData { export interface CreateTaskData {
projectId: number | null;
basic: BaseConfiguration; basic: BaseConfiguration;
advanced: AdvancedConfiguration; advanced: AdvancedConfiguration;
labels: any[]; labels: any[];
@ -30,12 +32,14 @@ interface Props {
onCreate: (data: CreateTaskData) => void; onCreate: (data: CreateTaskData) => void;
status: string; status: string;
taskId: number | null; taskId: number | null;
projectId: number | null;
installedGit: boolean; installedGit: boolean;
} }
type State = CreateTaskData; type State = CreateTaskData;
const defaultState = { const defaultState = {
projectId: null,
basic: { basic: {
name: '', name: '',
}, },
@ -65,6 +69,14 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
this.state = { ...defaultState }; this.state = { ...defaultState };
} }
public componentDidMount(): void {
const { projectId } = this.props;
if (projectId) {
this.handleProjectIdChange(projectId);
}
}
public componentDidUpdate(prevProps: Props): void { public componentDidUpdate(prevProps: Props): void {
const { status, history, taskId } = this.props; const { status, history, taskId } = this.props;
@ -89,9 +101,9 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
} }
} }
private validateLabels = (): boolean => { private validateLabelsOrProject = (): boolean => {
const { labels } = this.state; const { projectId, labels } = this.state;
return !!labels.length; return !!labels.length || !!projectId;
}; };
private validateFiles = (): boolean => { private validateFiles = (): boolean => {
@ -104,6 +116,12 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
return !!totalLen; return !!totalLen;
}; };
private handleProjectIdChange = (value: null | number): void => {
this.setState({
projectId: value,
});
};
private handleSubmitBasicConfiguration = (values: BaseConfiguration): void => { private handleSubmitBasicConfiguration = (values: BaseConfiguration): void => {
this.setState({ this.setState({
basic: { ...values }, basic: { ...values },
@ -125,10 +143,10 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
}; };
private handleSubmitClick = (): void => { private handleSubmitClick = (): void => {
if (!this.validateLabels()) { if (!this.validateLabelsOrProject()) {
notification.error({ notification.error({
message: 'Could not create a task', message: 'Could not create a task',
description: 'A task must contain at least one label', description: 'A task must contain at least one label or belong to some project',
}); });
return; return;
} }
@ -177,8 +195,36 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
); );
} }
private renderProjectBlock(): JSX.Element {
const { projectId } = this.state;
return (
<>
<Col span={24}>
<Text className='cvat-text-color'>Project:</Text>
</Col>
<Col span={24}>
<ProjectSearchField onSelect={this.handleProjectIdChange} value={projectId} />
</Col>
</>
);
}
private renderLabelsBlock(): JSX.Element { private renderLabelsBlock(): JSX.Element {
const { labels } = this.state; const { projectId, labels } = this.state;
if (projectId) {
return (
<>
<Col span={24}>
<Text className='cvat-text-color'>Labels:</Text>
</Col>
<Col span={24}>
<Text type='secondary'>Project labels will be used</Text>
</Col>
</>
);
}
return ( return (
<Col span={24}> <Col span={24}>
@ -243,12 +289,13 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
</Col> </Col>
{this.renderBasicBlock()} {this.renderBasicBlock()}
{this.renderProjectBlock()}
{this.renderLabelsBlock()} {this.renderLabelsBlock()}
{this.renderFilesBlock()} {this.renderFilesBlock()}
{this.renderAdvancedBlock()} {this.renderAdvancedBlock()}
<Col span={18}>{loading ? <Alert message={status} /> : null}</Col> <Col span={18}>{loading ? <Alert message={status} /> : null}</Col>
<Col span={6}> <Col span={6} className='cvat-create-task-submit-section'>
<Button loading={loading} disabled={loading} type='primary' onClick={this.handleSubmitClick}> <Button loading={loading} disabled={loading} type='primary' onClick={this.handleSubmitClick}>
Submit Submit
</Button> </Button>

@ -4,6 +4,7 @@
import './styles.scss'; import './styles.scss';
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { useLocation } from 'react-router';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import Modal from 'antd/lib/modal'; import Modal from 'antd/lib/modal';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
@ -21,7 +22,17 @@ interface Props {
} }
export default function CreateTaskPage(props: Props): JSX.Element { export default function CreateTaskPage(props: Props): JSX.Element {
const { error, status, taskId, onCreate, installedGit } = props; const {
error, status, taskId, onCreate, installedGit,
} = props;
const location = useLocation();
let projectId = null;
const params = new URLSearchParams(location.search);
if (params.get('projectId')?.match(/^[1-9]+[0-9]*$/)) {
projectId = +(params.get('projectId') as string);
}
useEffect(() => { useEffect(() => {
if (error) { if (error) {
@ -61,7 +72,13 @@ export default function CreateTaskPage(props: Props): JSX.Element {
<Row type='flex' justify='center' align='top' className='cvat-create-task-form-wrapper'> <Row type='flex' justify='center' align='top' className='cvat-create-task-form-wrapper'>
<Col md={20} lg={16} xl={14} xxl={9}> <Col md={20} lg={16} xl={14} xxl={9}>
<Text className='cvat-title'>Create a new task</Text> <Text className='cvat-title'>Create a new task</Text>
<CreateTaskContent taskId={taskId} status={status} onCreate={onCreate} installedGit={installedGit} /> <CreateTaskContent
taskId={taskId}
projectId={projectId}
status={status}
onCreate={onCreate}
installedGit={installedGit}
/>
</Col> </Col>
</Row> </Row>
); );

@ -0,0 +1,91 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useEffect, useState } from 'react';
import Autocomplete from 'antd/lib/auto-complete';
import getCore from 'cvat-core-wrapper';
import { SelectValue } from 'antd/lib/select';
const core = getCore();
type Props = {
value: number | null;
onSelect: (id: number | null) => void;
};
type Project = {
id: number;
name: string;
};
export default function ProjectSearchField(props: Props): JSX.Element {
const { value, onSelect } = props;
const [searchPhrase, setSearchPhrase] = useState('');
const [projects, setProjects] = useState<Project[]>([]);
const handleSearch = (searchValue: string): void => {
if (searchValue) {
core.projects.searchNames(searchValue).then((result: Project[]) => {
if (result) {
setProjects(result);
}
});
} else {
setProjects([]);
}
setSearchPhrase(searchValue);
onSelect(null);
};
const handleFocus = (open: boolean): void => {
if (!projects.length && open) {
core.projects.searchNames().then((result: Project[]) => {
if (result) {
setProjects(result);
}
});
}
if (!open && !value && searchPhrase) {
setSearchPhrase('');
}
};
const handleSelect = (_value: SelectValue): void => {
setSearchPhrase(projects.filter((proj) => proj.id === +_value)[0].name);
onSelect(_value ? +_value : null);
};
useEffect(() => {
if (value && !projects.filter((project) => project.id === value).length) {
core.projects.get({ id: value }).then((result: Project[]) => {
const [project] = result;
setProjects([...projects, {
id: project.id,
name: project.name,
}]);
setSearchPhrase(project.name);
onSelect(project.id);
});
}
}, [value]);
return (
<Autocomplete
value={searchPhrase}
placeholder='Select project'
onSearch={handleSearch}
onSelect={handleSelect}
className='cvat-project-search-field'
onDropdownVisibleChange={handleFocus}
dataSource={
projects.map((proj) => ({
value: proj.id.toString(),
text: proj.name,
}))
}
/>
);
}

@ -30,9 +30,13 @@
margin-top: 10px; margin-top: 10px;
} }
> div:nth-child(7) > button { .cvat-create-task-submit-section > button {
float: right; float: right;
width: 120px; width: 120px;
} }
.cvat-project-search-field {
width: 100%;
}
} }
} }

@ -14,14 +14,17 @@ import Header from 'components/header/header';
import ResetPasswordPageConfirmComponent from 'components/reset-password-confirm-page/reset-password-confirm-page'; import ResetPasswordPageConfirmComponent from 'components/reset-password-confirm-page/reset-password-confirm-page';
import ResetPasswordPageComponent from 'components/reset-password-page/reset-password-page'; import ResetPasswordPageComponent from 'components/reset-password-page/reset-password-page';
import ShorcutsDialog from 'components/shortcuts-dialog/shortcuts-dialog'; import ShorcutsDialog from 'components/shortcuts-dialog/shortcuts-dialog';
import ProjectsPageComponent from 'components/projects-page/projects-page';
import CreateProjectPageComponent from 'components/create-project-page/create-project-page';
import ProjectPageComponent from 'components/project-page/project-page';
import TasksPageContainer from 'containers/tasks-page/tasks-page';
import LoginWithTokenComponent from 'components/login-with-token/login-with-token'; import LoginWithTokenComponent from 'components/login-with-token/login-with-token';
import AnnotationPageContainer from 'containers/annotation-page/annotation-page';
import CreateTaskPageContainer from 'containers/create-task-page/create-task-page'; import CreateTaskPageContainer from 'containers/create-task-page/create-task-page';
import LoginPageContainer from 'containers/login-page/login-page'; import TaskPageContainer from 'containers/task-page/task-page';
import ModelsPageContainer from 'containers/models-page/models-page'; import ModelsPageContainer from 'containers/models-page/models-page';
import AnnotationPageContainer from 'containers/annotation-page/annotation-page';
import LoginPageContainer from 'containers/login-page/login-page';
import RegisterPageContainer from 'containers/register-page/register-page'; import RegisterPageContainer from 'containers/register-page/register-page';
import TaskPageContainer from 'containers/task-page/task-page';
import TasksPageContainer from 'containers/tasks-page/tasks-page';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
import React from 'react'; import React from 'react';
import { configure, ExtendedKeyMapOptions, GlobalHotKeys } from 'react-hotkeys'; import { configure, ExtendedKeyMapOptions, GlobalHotKeys } from 'react-hotkeys';
@ -34,7 +37,6 @@ import '../styles.scss';
interface CVATAppProps { interface CVATAppProps {
loadFormats: () => void; loadFormats: () => void;
loadUsers: () => void;
loadAbout: () => void; loadAbout: () => void;
verifyAuthorized: () => void; verifyAuthorized: () => void;
loadUserAgreements: () => void; loadUserAgreements: () => void;
@ -54,8 +56,6 @@ interface CVATAppProps {
modelsFetching: boolean; modelsFetching: boolean;
formatsInitialized: boolean; formatsInitialized: boolean;
formatsFetching: boolean; formatsFetching: boolean;
usersInitialized: boolean;
usersFetching: boolean;
aboutInitialized: boolean; aboutInitialized: boolean;
aboutFetching: boolean; aboutFetching: boolean;
userAgreementsFetching: boolean; userAgreementsFetching: boolean;
@ -92,7 +92,6 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
const { const {
verifyAuthorized, verifyAuthorized,
loadFormats, loadFormats,
loadUsers,
loadAbout, loadAbout,
loadUserAgreements, loadUserAgreements,
initPlugins, initPlugins,
@ -102,8 +101,6 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
userFetching, userFetching,
formatsInitialized, formatsInitialized,
formatsFetching, formatsFetching,
usersInitialized,
usersFetching,
aboutInitialized, aboutInitialized,
aboutFetching, aboutFetching,
pluginsInitialized, pluginsInitialized,
@ -143,10 +140,6 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
loadFormats(); loadFormats();
} }
if (!usersInitialized && !usersFetching) {
loadUsers();
}
if (!aboutInitialized && !aboutFetching) { if (!aboutInitialized && !aboutFetching) {
loadAbout(); loadAbout();
} }
@ -235,7 +228,6 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
public render(): JSX.Element { public render(): JSX.Element {
const { const {
userInitialized, userInitialized,
usersInitialized,
aboutInitialized, aboutInitialized,
pluginsInitialized, pluginsInitialized,
formatsInitialized, formatsInitialized,
@ -248,7 +240,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
const readyForRender = const readyForRender =
(userInitialized && (user == null || !user.isVerified)) || (userInitialized && (user == null || !user.isVerified)) ||
(userInitialized && formatsInitialized && pluginsInitialized && usersInitialized && aboutInitialized); (userInitialized && formatsInitialized && pluginsInitialized && aboutInitialized);
const subKeyMap = { const subKeyMap = {
SWITCH_SHORTCUTS: keyMap.SWITCH_SHORTCUTS, SWITCH_SHORTCUTS: keyMap.SWITCH_SHORTCUTS,
@ -270,7 +262,10 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
if (showPlatformNotification()) { if (showPlatformNotification()) {
stopNotifications(false); stopNotifications(false);
const info = platformInfo(); const {
name, version, engine, os,
} = platformInfo();
Modal.warning({ Modal.warning({
title: 'Unsupported platform detected', title: 'Unsupported platform detected',
content: ( content: (
@ -278,7 +273,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
<Row> <Row>
<Col> <Col>
<Text> <Text>
{`The browser you are using is ${info.name} ${info.version} based on ${info.engine} .` + {`The browser you are using is ${name} ${version} based on ${engine}.` +
' CVAT was tested in the latest versions of Chrome and Firefox.' + ' CVAT was tested in the latest versions of Chrome and Firefox.' +
' We recommend to use Chrome (or another Chromium based browser)'} ' We recommend to use Chrome (or another Chromium based browser)'}
</Text> </Text>
@ -286,7 +281,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
</Row> </Row>
<Row> <Row>
<Col> <Col>
<Text type='secondary'>{`The operating system is ${info.os}`}</Text> <Text type='secondary'>{`The operating system is ${os}`}</Text>
</Col> </Col>
</Row> </Row>
</> </>
@ -305,6 +300,9 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
<ShorcutsDialog /> <ShorcutsDialog />
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers}> <GlobalHotKeys keyMap={subKeyMap} handlers={handlers}>
<Switch> <Switch>
<Route exact path='/projects' component={ProjectsPageComponent} />
<Route exact path='/projects/create' component={CreateProjectPageComponent} />
<Route exact path='/projects/:id' component={ProjectPageComponent} />
<Route exact path='/tasks' component={TasksPageContainer} /> <Route exact path='/tasks' component={TasksPageContainer} />
<Route exact path='/tasks/create' component={CreateTaskPageContainer} /> <Route exact path='/tasks/create' component={CreateTaskPageContainer} />
<Route exact path='/tasks/:id' component={TaskPageContainer} /> <Route exact path='/tasks/:id' component={TaskPageContainer} />

@ -137,7 +137,9 @@ function HeaderContainer(props: Props): JSX.Element {
isModelsPluginActive, isModelsPluginActive,
} = props; } = props;
const { CHANGELOG_URL, LICENSE_URL, GITTER_URL, FORUM_URL, GITHUB_URL } = consts; const {
CHANGELOG_URL, LICENSE_URL, GITTER_URL, FORUM_URL, GITHUB_URL,
} = consts;
const history = useHistory(); const history = useHistory();
@ -238,6 +240,18 @@ function HeaderContainer(props: Props): JSX.Element {
<div className='cvat-left-header'> <div className='cvat-left-header'>
<Icon className='cvat-logo-icon' component={CVATLogo} /> <Icon className='cvat-logo-icon' component={CVATLogo} />
<Button
className='cvat-header-button'
type='link'
value='projects'
href='/projects'
onClick={(event: React.MouseEvent): void => {
event.preventDefault();
history.push('/projects');
}}
>
Projects
</Button>
<Button <Button
className='cvat-header-button' className='cvat-header-button'
type='link' type='link'
@ -288,8 +302,6 @@ function HeaderContainer(props: Props): JSX.Element {
href={GITHUB_URL} href={GITHUB_URL}
onClick={(event: React.MouseEvent): void => { onClick={(event: React.MouseEvent): void => {
event.preventDefault(); event.preventDefault();
// false positive
// eslint-disable-next-line security/detect-non-literal-fs-filename
window.open(GITHUB_URL, '_blank'); window.open(GITHUB_URL, '_blank');
}} }}
> >

@ -138,13 +138,13 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
<Row type='flex' className='cvat-player-settings-canvas-background'> <Row type='flex' className='cvat-player-settings-canvas-background'>
<Col> <Col>
<Popover <Popover
content={ content={(
<CompactPicker <CompactPicker
colors={consts.CANVAS_BACKGROUND_COLORS} colors={consts.CANVAS_BACKGROUND_COLORS}
color={canvasBackgroundColor} color={canvasBackgroundColor}
onChange={(e) => onChangeCanvasBackgroundColor(e.hex)} onChange={(e) => onChangeCanvasBackgroundColor(e.hex)}
/> />
} )}
overlayClassName='canvas-background-color-picker-popover' overlayClassName='canvas-background-color-picker-popover'
trigger='click' trigger='click'
> >
@ -169,6 +169,7 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
<Col span={8} className='cvat-player-settings-grid-size'> <Col span={8} className='cvat-player-settings-grid-size'>
<Text className='cvat-text-color'> Grid size </Text> <Text className='cvat-text-color'> Grid size </Text>
<InputNumber <InputNumber
className='cvat-player-settings-grid-size-input'
min={minGridSize} min={minGridSize}
max={maxGridSize} max={maxGridSize}
value={gridSize} value={gridSize}
@ -183,6 +184,7 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
<Col span={8} className='cvat-player-settings-grid-color'> <Col span={8} className='cvat-player-settings-grid-color'>
<Text className='cvat-text-color'> Grid color </Text> <Text className='cvat-text-color'> Grid color </Text>
<Select <Select
className='cvat-player-settings-grid-color-input'
value={gridColor} value={gridColor}
disabled={!grid} disabled={!grid}
onChange={(color: GridColor): void => { onChange={(color: GridColor): void => {
@ -209,6 +211,7 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
<Col span={8} className='cvat-player-settings-grid-opacity'> <Col span={8} className='cvat-player-settings-grid-opacity'>
<Text className='cvat-text-color'> Grid opacity </Text> <Text className='cvat-text-color'> Grid opacity </Text>
<Slider <Slider
className='cvat-player-settings-grid-opacity-input'
min={0} min={0}
max={100} max={100}
value={gridOpacity} value={gridOpacity}

@ -18,7 +18,9 @@ import ColorPicker from 'components/annotation-page/standard-workspace/objects-s
import { ColorizeIcon } from 'icons'; import { ColorizeIcon } from 'icons';
import patterns from 'utils/validation-patterns'; import patterns from 'utils/validation-patterns';
import consts from 'consts'; import consts from 'consts';
import { equalArrayHead, idGenerator, Label, Attribute } from './common'; import {
equalArrayHead, idGenerator, Label, Attribute,
} from './common';
export enum AttributeType { export enum AttributeType {
SELECT = 'SELECT', SELECT = 'SELECT',
@ -122,7 +124,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
message: patterns.validateAttributeName.message, message: patterns.validateAttributeName.message,
}, },
], ],
})(<Input disabled={locked} placeholder='Name' />)} })(<Input className='cvat-attribute-name-input' disabled={locked} placeholder='Name' />)}
</Form.Item> </Form.Item>
</Col> </Col>
); );
@ -140,7 +142,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
{form.getFieldDecorator(`type[${key}]`, { {form.getFieldDecorator(`type[${key}]`, {
initialValue: type, initialValue: type,
})( })(
<Select disabled={locked}> <Select className='cvat-attribute-type-input' disabled={locked}>
<Select.Option value={AttributeType.SELECT}>Select</Select.Option> <Select.Option value={AttributeType.SELECT}>Select</Select.Option>
<Select.Option value={AttributeType.RADIO}>Radio</Select.Option> <Select.Option value={AttributeType.RADIO}>Radio</Select.Option>
<Select.Option value={AttributeType.CHECKBOX}>Checkbox</Select.Option> <Select.Option value={AttributeType.CHECKBOX}>Checkbox</Select.Option>
@ -189,7 +191,14 @@ class LabelForm extends React.PureComponent<Props, {}> {
validator, validator,
}, },
], ],
})(<Select mode='tags' dropdownMenuStyle={{ display: 'none' }} placeholder='Attribute values' />)} })(
<Select
className='cvat-attribute-values-input'
mode='tags'
dropdownMenuStyle={{ display: 'none' }}
placeholder='Attribute values'
/>,
)}
</Form.Item> </Form.Item>
</Tooltip> </Tooltip>
); );
@ -205,7 +214,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
{form.getFieldDecorator(`values[${key}]`, { {form.getFieldDecorator(`values[${key}]`, {
initialValue: value, initialValue: value,
})( })(
<Select> <Select className='cvat-attribute-values-input'>
<Select.Option value='false'> False </Select.Option> <Select.Option value='false'> False </Select.Option>
<Select.Option value='true'> True </Select.Option> <Select.Option value='true'> True </Select.Option>
</Select>, </Select>,
@ -262,7 +271,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
validator, validator,
}, },
], ],
})(<Input disabled={locked} placeholder='min;max;step' />)} })(<Input className='cvat-attribute-values-input' disabled={locked} placeholder='min;max;step' />)}
</Form.Item> </Form.Item>
); );
} }
@ -275,7 +284,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
<Form.Item> <Form.Item>
{form.getFieldDecorator(`values[${key}]`, { {form.getFieldDecorator(`values[${key}]`, {
initialValue: value, initialValue: value,
})(<Input placeholder='Default value' />)} })(<Input className='cvat-attribute-values-input' placeholder='Default value' />)}
</Form.Item> </Form.Item>
); );
} }
@ -291,7 +300,13 @@ class LabelForm extends React.PureComponent<Props, {}> {
{form.getFieldDecorator(`mutable[${key}]`, { {form.getFieldDecorator(`mutable[${key}]`, {
initialValue: value, initialValue: value,
valuePropName: 'checked', valuePropName: 'checked',
})(<Checkbox disabled={locked}> Mutable </Checkbox>)} })(
<Checkbox className='cvat-attribute-mutable-checkbox' disabled={locked}>
{' '}
Mutable
{' '}
</Checkbox>,
)}
</Tooltip> </Tooltip>
</Form.Item> </Form.Item>
); );
@ -318,13 +333,19 @@ class LabelForm extends React.PureComponent<Props, {}> {
); );
} }
private renderAttribute = (key: number, index: number): JSX.Element => { private renderAttribute = (key: number): JSX.Element => {
const { label, form } = this.props; const { label, form } = this.props;
const attr = label && index < label.attributes.length ? label.attributes[index] : null; const attr = label ? label.attributes.filter((_attr: any): boolean => _attr.id === key)[0] : null;
return ( return (
<Form.Item key={key}> <Form.Item key={key}>
<Row type='flex' justify='space-between' align='middle'> <Row
type='flex'
justify='space-between'
align='middle'
cvat-attribute-id={key}
className='cvat-attribute-inputs-wrapper'
>
{this.renderAttributeNameInput(key, attr)} {this.renderAttributeNameInput(key, attr)}
{this.renderAttributeTypeInput(key, attr)} {this.renderAttributeTypeInput(key, attr)}
<Col span={6}> <Col span={6}>

@ -0,0 +1,85 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState } from 'react';
import { useDispatch } from 'react-redux';
import moment from 'moment';
import { Row, Col } from 'antd/lib/grid';
import Title from 'antd/lib/typography/Title';
import Text from 'antd/lib/typography/Text';
import getCore from 'cvat-core-wrapper';
import { Project } from 'reducers/interfaces';
import { updateProjectAsync } from 'actions/projects-actions';
import LabelsEditor from 'components/labels-editor/labels-editor';
import BugTrackerEditor from 'components/task-page/bug-tracker-editor';
import UserSelector from 'components/task-page/user-selector';
const core = getCore();
interface DetailsComponentProps {
project: Project;
}
export default function DetailsComponent(props: DetailsComponentProps): JSX.Element {
const { project } = props;
const dispatch = useDispatch();
const [projectName, setProjectName] = useState(project.name);
return (
<div className='cvat-project-details'>
<Row>
<Col>
<Title
level={4}
editable={{
onChange: (value: string): void => {
setProjectName(value);
project.name = value;
dispatch(updateProjectAsync(project));
},
}}
className='cvat-text-color'
>
{projectName}
</Title>
</Col>
</Row>
<Row type='flex' justify='space-between'>
<Col>
<Text type='secondary'>
{`Project #${project.id} created`}
{project.owner ? ` by ${project.owner.username}` : null}
{` on ${moment(project.createdDate).format('MMMM Do YYYY')}`}
</Text>
<BugTrackerEditor
instance={project}
onChange={(bugTracker): void => {
project.bugTracker = bugTracker;
dispatch(updateProjectAsync(project));
}}
/>
</Col>
<Col>
<Text type='secondary'>Assigned to</Text>
<UserSelector
value={project.assignee}
onSelect={(user) => {
project.assignee = user;
dispatch(updateProjectAsync(project));
}}
/>
</Col>
</Row>
<LabelsEditor
labels={project.labels.map((label: any): string => label.toJSON())}
onSubmit={(labels: any[]): void => {
project.labels = labels.map((labelData): any => new core.classes.Label(labelData));
dispatch(updateProjectAsync(project));
}}
/>
</div>
);
}

@ -0,0 +1,107 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useHistory, useParams } from 'react-router';
import Spin from 'antd/lib/spin';
import { Row, Col } from 'antd/lib/grid';
import Result from 'antd/lib/result';
import Button from 'antd/lib/button';
import Title from 'antd/lib/typography/Title';
import { CombinedState, Task } from 'reducers/interfaces';
import { getProjectsAsync } from 'actions/projects-actions';
import { cancelInferenceAsync } from 'actions/models-actions';
import TaskItem from 'components/tasks-page/task-item';
import DetailsComponent from './details';
import ProjectTopBar from './top-bar';
interface ParamType {
id: string;
}
export default function ProjectPageComponent(): JSX.Element {
const id = +useParams<ParamType>().id;
const dispatch = useDispatch();
const history = useHistory();
const projects = useSelector((state: CombinedState) => state.projects.current);
const projectsFetching = useSelector((state: CombinedState) => state.projects.fetching);
const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes);
const taskDeletes = useSelector((state: CombinedState) => state.tasks.activities.deletes);
const tasksActiveInferences = useSelector((state: CombinedState) => state.models.inferences);
const tasks = useSelector((state: CombinedState) => state.tasks.current);
const filteredProjects = projects.filter((project) => project.id === id);
const project = filteredProjects[0];
const deleteActivity = project && id in deletes ? deletes[id] : null;
useEffect(() => {
dispatch(
getProjectsAsync({
id,
}),
);
}, [id, dispatch]);
if (deleteActivity) {
history.push('/projects');
}
if (projectsFetching) {
return <Spin size='large' className='cvat-spinner' />;
}
if (!project) {
return (
<Result
className='cvat-not-found'
status='404'
title='Sorry, but this project was not found'
subTitle='Please, be sure information you tried to get exist and you have access'
/>
);
}
return (
<Row type='flex' justify='center' align='top' className='cvat-project-page'>
<Col md={22} lg={18} xl={16} xxl={14}>
<ProjectTopBar projectInstance={project} />
<DetailsComponent project={project} />
<Row type='flex' justify='space-between' align='middle' className='cvat-project-page-tasks-bar'>
<Col>
<Title level={4}>Tasks</Title>
</Col>
<Col>
<Button
size='large'
type='primary'
icon='plus'
id='cvat-create-task-button'
onClick={() => history.push(`/tasks/create?projectId=${id}`)}
>
Create new task
</Button>
</Col>
</Row>
{tasks
.filter((task) => task.instance.projectId === project.id)
.map((task: Task) => (
<TaskItem
key={task.instance.id}
deleted={task.instance.id in taskDeletes ? taskDeletes[task.instance.id] : false}
hidden={false}
activeInference={tasksActiveInferences[task.instance.id] || null}
cancelAutoAnnotation={() => {
dispatch(cancelInferenceAsync(task.instance.id));
}}
previewImage={task.preview}
taskInstance={task.instance}
/>
))}
</Col>
</Row>
);
}

@ -0,0 +1,56 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
@import '../../base.scss';
.cvat-project-details {
width: 100%;
height: auto;
border: 1px solid $border-color-1;
border-radius: 3px;
padding: $grid-unit-size * 2;
margin: $grid-unit-size * 2 0;
background: $background-color-1;
.ant-row-flex:nth-child(1) {
margin-bottom: $grid-unit-size * 2;
}
.ant-row-flex:nth-child(2) .ant-col:nth-child(2) > span {
margin-right: $grid-unit-size;
}
.cvat-project-details-actions {
display: flex;
align-items: center;
justify-content: flex-end;
}
.cvat-issue-tracker {
margin-top: $grid-unit-size * 2;
margin-bottom: $grid-unit-size * 2;
}
}
.cvat-project-page-tasks-bar {
margin: $grid-unit-size * 2 0;
}
.ant-menu.cvat-project-actions-menu {
box-shadow: 0 0 17px rgba(0, 0, 0, 0.2);
> li:hover {
background-color: $hover-menu-color;
}
.ant-menu-submenu-title {
margin: 0;
width: 13em;
}
}
.cvat-project-top-bar-actions > button {
display: flex;
align-items: center;
}

@ -0,0 +1,44 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { useHistory } from 'react-router';
import { Row, Col } from 'antd/lib/grid';
import Button from 'antd/lib/button';
import Dropdown from 'antd/lib/dropdown';
import Icon from 'antd/lib/icon';
import Text from 'antd/lib/typography/Text';
import { Project } from 'reducers/interfaces';
import ActionsMenu from 'components/projects-page/actions-menu';
import { MenuIcon } from 'icons';
interface DetailsComponentProps {
projectInstance: Project;
}
export default function ProjectTopBar(props: DetailsComponentProps): JSX.Element {
const { projectInstance } = props;
const history = useHistory();
return (
<Row className='cvat-task-top-bar' type='flex' justify='space-between' align='middle'>
<Col>
<Button onClick={() => history.push('/projects')} type='link' size='large'>
<Icon type='left' />
Back to projects
</Button>
</Col>
<Col className='cvat-project-top-bar-actions'>
<Dropdown overlay={<ActionsMenu projectInstance={projectInstance.instance} />}>
<Button size='large'>
<Text className='cvat-text-color'>Actions</Text>
<Icon className='cvat-menu-icon' component={MenuIcon} />
</Button>
</Dropdown>
</Col>
</Row>
);
}

@ -0,0 +1,40 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { useDispatch } from 'react-redux';
import Modal from 'antd/lib/modal';
import Menu from 'antd/lib/menu';
import { deleteProjectAsync } from 'actions/projects-actions';
interface Props {
projectInstance: any;
}
export default function ProjectActionsMenuComponent(props: Props): JSX.Element {
const { projectInstance } = props;
const dispatch = useDispatch();
const onDeleteProject = (): void => {
Modal.confirm({
title: `The project ${projectInstance.id} will be deleted`,
content: 'All related data (images, annotations) will be lost. Continue?',
onOk: () => {
dispatch(deleteProjectAsync(projectInstance));
},
okButtonProps: {
type: 'danger',
},
okText: 'Delete',
});
};
return (
<Menu className='cvat-project-actions-menu'>
<Menu.Item onClick={onDeleteProject}>Delete</Menu.Item>
</Menu>
);
}

@ -0,0 +1,53 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { Link } from 'react-router-dom';
import Text from 'antd/lib/typography/Text';
import { Row, Col } from 'antd/lib/grid';
import Icon from 'antd/lib/icon';
import { EmptyTasksIcon } from 'icons';
interface Props {
notFound?: boolean;
}
export default function EmptyListComponent(props: Props): JSX.Element {
const { notFound } = props;
return (
<div className='cvat-empty-projects-list'>
<Row type='flex' justify='center' align='middle'>
<Col>
<Icon className='cvat-empty-projects-icon' component={EmptyTasksIcon} />
</Col>
</Row>
{notFound ? (
<Row type='flex' justify='center' align='middle'>
<Col>
<Text strong>No results matched your search...</Text>
</Col>
</Row>
) : (
<>
<Row type='flex' justify='center' align='middle'>
<Col>
<Text strong>No projects created yet ...</Text>
</Col>
</Row>
<Row type='flex' justify='center' align='middle'>
<Col>
<Text type='secondary'>To get started with your annotation project</Text>
</Col>
</Row>
<Row type='flex' justify='center' align='middle'>
<Col>
<Link to='/projects/create'>create a new one</Link>
</Col>
</Row>
</>
)}
</div>
);
}

@ -0,0 +1,99 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import moment from 'moment';
import { useSelector } from 'react-redux';
import { useHistory } from 'react-router';
import Text from 'antd/lib/typography/Text';
import Empty from 'antd/lib/empty';
import Card from 'antd/lib/card';
import Meta from 'antd/lib/card/Meta';
import Dropdown from 'antd/lib/dropdown';
import Button from 'antd/lib/button';
import { CombinedState, Project } from 'reducers/interfaces';
import ProjectActionsMenuComponent from './actions-menu';
interface Props {
projectInstance: Project;
}
export default function ProjectItemComponent(props: Props): JSX.Element {
const { projectInstance } = props;
const history = useHistory();
const ownerName = projectInstance.owner ? projectInstance.owner.username : null;
const updated = moment(projectInstance.updatedDate).fromNow();
const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes);
const deleted = projectInstance.id in deletes ? deletes[projectInstance.id] : false;
let projectPreview = null;
if (projectInstance.tasks.length) {
// prettier-ignore
projectPreview = useSelector((state: CombinedState) => (
state.tasks.current.find((task) => task.instance.id === projectInstance.tasks[0].id)?.preview
));
}
const onOpenProject = (): void => {
history.push(`/projects/${projectInstance.id}`);
};
const style: React.CSSProperties = {};
if (deleted) {
style.pointerEvents = 'none';
style.opacity = 0.5;
}
return (
<Card
cover={
projectPreview ? (
<img
className='cvat-projects-project-item-card-preview'
src={projectPreview}
alt='Preview'
onClick={onOpenProject}
aria-hidden
/>
) : (
<div className='cvat-projects-project-item-card-preview' onClick={onOpenProject} aria-hidden>
<Empty description='No tasks' />
</div>
)
}
size='small'
style={style}
className='cvat-projects-project-item-card'
>
<Meta
title={(
<span onClick={onOpenProject} className='cvat-projects-project-item-title' aria-hidden>
{projectInstance.name}
</span>
)}
description={(
<div className='cvat-porjects-project-item-description'>
<div>
{ownerName && (
<>
<Text type='secondary'>{`Created ${ownerName ? `by ${ownerName}` : ''}`}</Text>
<br />
</>
)}
<Text type='secondary'>{`Last updated ${updated}`}</Text>
</div>
<div>
<Dropdown overlay={<ProjectActionsMenuComponent projectInstance={projectInstance} />}>
<Button type='link' size='large' icon='more' />
</Dropdown>
</div>
</div>
)}
/>
</Card>
);
}

@ -0,0 +1,59 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { Row, Col } from 'antd/lib/grid';
import Pagination from 'antd/lib/pagination';
import { getProjectsAsync } from 'actions/projects-actions';
import { CombinedState } from 'reducers/interfaces';
import ProjectItem from './project-item';
export default function ProjectListComponent(): JSX.Element {
const dispatch = useDispatch();
const projectsCount = useSelector((state: CombinedState) => state.projects.count);
const { page } = useSelector((state: CombinedState) => state.projects.gettingQuery);
const projectInstances = useSelector((state: CombinedState) => state.projects.current);
const gettingQuery = useSelector((state: CombinedState) => state.projects.gettingQuery);
function changePage(p: number): void {
dispatch(
getProjectsAsync({
...gettingQuery,
page: p,
}),
);
}
return (
<>
<Row type='flex' justify='center' align='middle'>
<Col className='cvat-projects-list' md={22} lg={18} xl={16} xxl={14}>
<Row gutter={[8, 8]}>
{projectInstances.map(
(instance: any): JSX.Element => (
<Col xs={8} sm={8} xl={6} key={instance.id}>
<ProjectItem projectInstance={instance} />
</Col>
),
)}
</Row>
</Col>
</Row>
<Row type='flex' justify='center' align='middle'>
<Col md={22} lg={18} xl={16} xxl={14}>
<Pagination
className='cvat-projects-pagination'
onChange={changePage}
total={projectsCount}
pageSize={12}
current={page}
showQuickJumper
/>
</Col>
</Row>
</>
);
}

@ -0,0 +1,67 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useLocation, useHistory } from 'react-router';
import Spin from 'antd/lib/spin';
import FeedbackComponent from 'components/feedback/feedback';
import { CombinedState, ProjectsQuery } from 'reducers/interfaces';
import { getProjectsAsync } from 'actions/projects-actions';
import EmptyListComponent from './empty-list';
import TopBarComponent from './top-bar';
import ProjectListComponent from './project-list';
export default function ProjectsPageComponent(): JSX.Element {
const { search } = useLocation();
const history = useHistory();
const dispatch = useDispatch();
const projectFetching = useSelector((state: CombinedState) => state.projects.fetching);
const projectsCount = useSelector((state: CombinedState) => state.projects.current.length);
const gettingQuery = useSelector((state: CombinedState) => state.projects.gettingQuery);
const anySearchQuery = !!Array.from(new URLSearchParams(search).keys()).filter((value) => value !== 'page').length;
useEffect(() => {
const searchParams: Partial<ProjectsQuery> = {};
for (const [param, value] of new URLSearchParams(search)) {
searchParams[param] = ['page', 'id'].includes(param) ? Number.parseInt(value, 10) : value;
}
dispatch(getProjectsAsync(searchParams));
}, []);
useEffect(() => {
const searchParams = new URLSearchParams();
for (const [name, value] of Object.entries(gettingQuery)) {
if (value !== null && typeof value !== 'undefined') {
searchParams.append(name, value.toString());
}
}
history.push({
pathname: '/projects',
search: `?${searchParams.toString()}`,
});
}, [gettingQuery]);
if (projectFetching) {
return (
<Spin size='large' className='cvat-spinner' />
);
}
return (
<div className='cvat-projects-page'>
<TopBarComponent />
{ projectsCount
? (
<ProjectListComponent />
) : (
<EmptyListComponent notFound={anySearchQuery} />
)}
<FeedbackComponent />
</div>
);
}

@ -0,0 +1,81 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import Search from 'antd/lib/input/Search';
import { CombinedState, ProjectsQuery } from 'reducers/interfaces';
import { getProjectsAsync } from 'actions/projects-actions';
function getSearchField(gettingQuery: ProjectsQuery): string {
let searchString = '';
for (const field of Object.keys(gettingQuery)) {
if (gettingQuery[field] !== null && field !== 'page') {
if (field === 'search') {
return (gettingQuery[field] as any) as string;
}
// not constant condition
// eslint-disable-next-line
if (typeof (gettingQuery[field] === 'number')) {
searchString += `${field}:${gettingQuery[field]} AND `;
} else {
searchString += `${field}:"${gettingQuery[field]}" AND `;
}
}
}
return searchString.slice(0, -5);
}
export default function ProjectSearchField(): JSX.Element {
const dispatch = useDispatch();
const gettingQuery = useSelector((state: CombinedState) => state.projects.gettingQuery);
const handleSearch = (value: string): void => {
const query = { ...gettingQuery };
const search = value.replace(/\s+/g, ' ').replace(/\s*:+\s*/g, ':').trim();
const fields = Object.keys(query).filter((key) => key !== 'page');
for (const field of fields) {
query[field] = null;
}
query.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)) {
query[field] = +fieldValue;
}
} else {
query[field] = fieldValue;
}
}
}
}
query.page = 1;
if (!specificRequest && value) {
query.search = value;
}
dispatch(getProjectsAsync(query));
};
return (
<Search
defaultValue={getSearchField(gettingQuery)}
onSearch={handleSearch}
size='large'
placeholder='Search'
/>
);
}

@ -0,0 +1,119 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
@import '../../base.scss';
.cvat-projects-page {
padding-top: $grid-unit-size * 2;
padding-bottom: $grid-unit-size * 5;
height: 100%;
position: fixed;
width: 100%;
> div:nth-child(1) {
padding-bottom: $grid-unit-size;
div > {
span {
color: $text-color;
}
}
}
}
/* empty-projects icon */
.cvat-empty-projects-list {
> div:nth-child(1) {
margin-top: $grid-unit-size * 6;
}
> div:nth-child(2) {
> div {
margin-top: $grid-unit-size * 3;
/* No projects created yet */
> span {
font-size: 20px;
color: $text-color;
}
}
}
/* To get started with your annotation project .. */
> div:nth-child(3) {
margin-top: $grid-unit-size;
}
}
.cvat-projects-top-bar {
> div:nth-child(1) {
display: flex;
> span:nth-child(2) {
width: $grid-unit-size * 25;
margin-left: $grid-unit-size;
}
}
> div:nth-child(2) {
display: flex;
justify-content: flex-end;
}
}
.cvat-create-project-button {
padding: 0 $grid-unit-size * 4;
}
.cvat-projects-pagination {
display: flex;
justify-content: center;
}
.cvat-projects-project-item-title,
.cvat-projects-project-item-card-preview {
cursor: pointer;
}
.cvat-porjects-project-item-description {
display: flex;
justify-content: space-between;
// actions button
> div:nth-child(2) {
display: flex;
align-self: flex-end;
justify-content: center;
> button {
color: $text-color;
width: inherit;
}
}
}
.ant-menu.cvat-project-actions-menu {
box-shadow: 0 0 17px rgba(0, 0, 0, 0.2);
> li:hover {
background-color: $hover-menu-color;
}
.ant-menu-submenu-title {
margin: 0;
width: 13em;
}
}
.cvat-projects-project-item-card {
.ant-empty {
margin: $grid-unit-size;
}
img {
height: 100%;
max-height: $grid-unit-size * 18;
object-fit: cover;
}
}

@ -0,0 +1,36 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { useHistory } from 'react-router';
import { Row, Col } from 'antd/lib/grid';
import Button from 'antd/lib/button';
import Text from 'antd/lib/typography/Text';
import SearchField from './search-field';
export default function TopBarComponent(): JSX.Element {
const history = useHistory();
return (
<Row type='flex' justify='center' align='middle' className='cvat-projects-top-bar'>
<Col md={11} lg={9} xl={8} xxl={7}>
<Text className='cvat-title'>Projects</Text>
<SearchField />
</Col>
<Col md={{ span: 11 }} lg={{ span: 9 }} xl={{ span: 8 }} xxl={{ span: 7 }}>
<Button
size='large'
id='cvat-create-project-button'
className='cvat-create-project-button'
type='primary'
onClick={(): void => history.push('/projects/create')}
icon='plus'
>
Create new project
</Button>
</Col>
</Row>
);
}

@ -21,8 +21,7 @@ export interface ResetPasswordConfirmData {
type ResetPasswordConfirmFormProps = { type ResetPasswordConfirmFormProps = {
fetching: boolean; fetching: boolean;
onSubmit(resetPasswordConfirmData: ResetPasswordConfirmData): void; onSubmit(resetPasswordConfirmData: ResetPasswordConfirmData): void;
} & FormComponentProps & } & FormComponentProps & RouteComponentProps;
RouteComponentProps;
class ResetPasswordConfirmFormComponent extends React.PureComponent<ResetPasswordConfirmFormProps> { class ResetPasswordConfirmFormComponent extends React.PureComponent<ResetPasswordConfirmFormProps> {
private validateConfirmation = (_: any, value: string, callback: Function): void => { private validateConfirmation = (_: any, value: string, callback: Function): void => {

@ -0,0 +1,92 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState } from 'react';
import Button from 'antd/lib/button';
import Modal from 'antd/lib/modal';
import Text from 'antd/lib/typography/Text';
import { Row, Col } from 'antd/lib/grid';
import patterns from 'utils/validation-patterns';
interface Props {
instance: any;
onChange: (bugTracker: string) => void;
}
export default function BugTrackerEditorComponent(props: Props): JSX.Element {
const { instance, onChange } = props;
const [bugTracker, setBugTracker] = useState(instance.bugTracker);
const [bugTrackerEditing, setBugTrackerEditing] = useState(false);
const instanceType = Array.isArray(instance.tasks) ? 'project' : 'task';
let shown = false;
const onStart = (): void => setBugTrackerEditing(true);
const onChangeValue = (value: string): void => {
if (value && !patterns.validateURL.pattern.test(value)) {
if (!shown) {
Modal.error({
title: `Could not update the ${instanceType} ${instance.id}`,
content: 'Issue tracker is expected to be URL',
onOk: () => {
shown = false;
},
});
shown = true;
}
} else {
setBugTracker(value);
setBugTrackerEditing(false);
onChange(value);
}
};
if (bugTracker) {
return (
<Row className='cvat-issue-tracker'>
<Col>
<Text strong className='cvat-text-color'>
Issue Tracker
</Text>
<br />
<Text editable={{ onChange: onChangeValue }}>{bugTracker}</Text>
<Button
type='ghost'
size='small'
onClick={(): void => {
// false positive
// eslint-disable-next-line
window.open(bugTracker, '_blank');
}}
className='cvat-open-bug-tracker-button'
>
Open the issue
</Button>
</Col>
</Row>
);
}
return (
<Row className='cvat-issue-tracker'>
<Col>
<Text strong className='cvat-text-color'>
Issue Tracker
</Text>
<br />
<Text
editable={{
editing: bugTrackerEditing,
onStart,
onChange: onChangeValue,
}}
>
{bugTrackerEditing ? '' : 'Not specified'}
</Text>
</Col>
</Row>
);
}

@ -7,18 +7,17 @@ import { Row, Col } from 'antd/lib/grid';
import Tag from 'antd/lib/tag'; import Tag from 'antd/lib/tag';
import Icon from 'antd/lib/icon'; import Icon from 'antd/lib/icon';
import Modal from 'antd/lib/modal'; import Modal from 'antd/lib/modal';
import Button from 'antd/lib/button';
import notification from 'antd/lib/notification'; import notification from 'antd/lib/notification';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import Title from 'antd/lib/typography/Title'; import Title from 'antd/lib/typography/Title';
import moment from 'moment'; import moment from 'moment';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
import patterns from 'utils/validation-patterns';
import { getReposData, syncRepos } from 'utils/git-utils'; import { getReposData, syncRepos } from 'utils/git-utils';
import { ActiveInference } from 'reducers/interfaces'; import { ActiveInference } from 'reducers/interfaces';
import AutomaticAnnotationProgress from 'components/tasks-page/automatic-annotation-progress'; import AutomaticAnnotationProgress from 'components/tasks-page/automatic-annotation-progress';
import UserSelector from './user-selector'; import UserSelector, { User } from './user-selector';
import BugTrackerEditor from './bug-tracker-editor';
import LabelsEditorComponent from '../labels-editor/labels-editor'; import LabelsEditorComponent from '../labels-editor/labels-editor';
const core = getCore(); const core = getCore();
@ -27,7 +26,6 @@ interface Props {
previewImage: string; previewImage: string;
taskInstance: any; taskInstance: any;
installedGit: boolean; // change to git repos url installedGit: boolean; // change to git repos url
registeredUsers: any[];
activeInference: ActiveInference | null; activeInference: ActiveInference | null;
cancelAutoAnnotation(): void; cancelAutoAnnotation(): void;
onTaskUpdate: (taskInstance: any) => void; onTaskUpdate: (taskInstance: any) => void;
@ -35,8 +33,6 @@ interface Props {
interface State { interface State {
name: string; name: string;
bugTracker: string;
bugTrackerEditing: boolean;
repository: string; repository: string;
repositoryStatus: string; repositoryStatus: string;
} }
@ -58,8 +54,6 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
this.previewWrapperRef = React.createRef<HTMLDivElement>(); this.previewWrapperRef = React.createRef<HTMLDivElement>();
this.state = { this.state = {
name: taskInstance.name, name: taskInstance.name,
bugTracker: taskInstance.bugTracker,
bugTrackerEditing: false,
repository: '', repository: '',
repositoryStatus: '', repositoryStatus: '',
}; };
@ -120,7 +114,6 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
if (prevProps !== this.props) { if (prevProps !== this.props) {
this.setState({ this.setState({
name: taskInstance.name, name: taskInstance.name,
bugTracker: taskInstance.bugTracker,
}); });
} }
} }
@ -195,36 +188,31 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
); );
} }
private renderUsers(): JSX.Element { private renderDescription(): JSX.Element {
const { taskInstance, registeredUsers, onTaskUpdate } = this.props; const { taskInstance, onTaskUpdate } = this.props;
const owner = taskInstance.owner ? taskInstance.owner.username : null; const owner = taskInstance.owner ? taskInstance.owner.username : null;
const assignee = taskInstance.assignee ? taskInstance.assignee.username : null; const assignee = taskInstance.assignee ? taskInstance.assignee : null;
const created = moment(taskInstance.createdDate).format('MMMM Do YYYY'); const created = moment(taskInstance.createdDate).format('MMMM Do YYYY');
const assigneeSelect = ( const assigneeSelect = (
<UserSelector <UserSelector
users={registeredUsers}
value={assignee} value={assignee}
onChange={(value: string): void => { onSelect={(value: User | null): void => {
let [userInstance] = registeredUsers.filter((user: any) => user.username === value); taskInstance.assignee = value;
if (userInstance === undefined) {
userInstance = null;
}
taskInstance.assignee = userInstance;
onTaskUpdate(taskInstance); onTaskUpdate(taskInstance);
}} }}
/> />
); );
return ( return (
<Row type='flex' justify='space-between' align='middle'> <Row className='cvat-task-details-user-block' type='flex' justify='space-between' align='middle'>
<Col span={12}>{owner && <Text type='secondary'>{`Created by ${owner} on ${created}`}</Text>}</Col> <Col span={12}>
{owner && (
<Text type='secondary'>{`Task #${taskInstance.id} Created by ${owner} on ${created}`}</Text>
)}
</Col>
<Col span={10}> <Col span={10}>
<Text type='secondary'> <Text type='secondary'>Assigned to</Text>
Assigned to {assigneeSelect}
{assigneeSelect}
</Text>
</Col> </Col>
</Row> </Row>
); );
@ -304,86 +292,6 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
); );
} }
private renderBugTracker(): JSX.Element {
const { taskInstance, onTaskUpdate } = this.props;
const { bugTracker, bugTrackerEditing } = this.state;
let shown = false;
const onStart = (): void => {
this.setState({
bugTrackerEditing: true,
});
};
const onChangeValue = (value: string): void => {
if (value && !patterns.validateURL.pattern.test(value)) {
if (!shown) {
Modal.error({
title: `Could not update the task ${taskInstance.id}`,
content: 'Issue tracker is expected to be URL',
onOk: () => {
shown = false;
},
});
shown = true;
}
} else {
this.setState({
bugTracker: value,
bugTrackerEditing: false,
});
taskInstance.bugTracker = value;
onTaskUpdate(taskInstance);
}
};
if (bugTracker) {
return (
<Row>
<Col>
<Text strong className='cvat-text-color'>
Issue Tracker
</Text>
<br />
<Text editable={{ onChange: onChangeValue }}>{bugTracker}</Text>
<Button
type='ghost'
size='small'
onClick={(): void => {
// false positive
// eslint-disable-next-line
window.open(bugTracker, '_blank');
}}
className='cvat-open-bug-tracker-button'
>
Open the issue
</Button>
</Col>
</Row>
);
}
return (
<Row>
<Col>
<Text strong className='cvat-text-color'>
Issue Tracker
</Text>
<br />
<Text
editable={{
editing: bugTrackerEditing,
onStart,
onChange: onChangeValue,
}}
>
{bugTrackerEditing ? '' : 'Not specified'}
</Text>
</Col>
</Row>
);
}
private renderLabelsEditor(): JSX.Element { private renderLabelsEditor(): JSX.Element {
const { taskInstance, onTaskUpdate } = this.props; const { taskInstance, onTaskUpdate } = this.props;
@ -403,7 +311,10 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
} }
public render(): JSX.Element { public render(): JSX.Element {
const { activeInference, cancelAutoAnnotation } = this.props; const {
activeInference, cancelAutoAnnotation, taskInstance, onTaskUpdate,
} = this.props;
return ( return (
<div className='cvat-task-details'> <div className='cvat-task-details'>
<Row type='flex' justify='start' align='middle'> <Row type='flex' justify='start' align='middle'>
@ -419,9 +330,17 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
</Row> </Row>
</Col> </Col>
<Col md={16} lg={17} xl={17} xxl={18}> <Col md={16} lg={17} xl={17} xxl={18}>
{this.renderUsers()} {this.renderDescription()}
<Row type='flex' justify='space-between' align='middle'> <Row type='flex' justify='space-between' align='middle'>
<Col span={12}>{this.renderBugTracker()}</Col> <Col span={12}>
<BugTrackerEditor
instance={taskInstance}
onChange={(bugTracker) => {
taskInstance.bugTracker = bugTracker;
onTaskUpdate(taskInstance);
}}
/>
</Col>
<Col span={10}> <Col span={10}>
<AutomaticAnnotationProgress <AutomaticAnnotationProgress
activeInference={activeInference} activeInference={activeInference}
@ -430,7 +349,7 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
</Col> </Col>
</Row> </Row>
{this.renderDatasetRepository()} {this.renderDatasetRepository()}
{this.renderLabelsEditor()} {!taskInstance.projectId && this.renderLabelsEditor()}
</Col> </Col>
</Row> </Row>
</div> </div>

@ -15,7 +15,7 @@ import moment from 'moment';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
import UserSelector from './user-selector'; import UserSelector, { User } from './user-selector';
const core = getCore(); const core = getCore();
@ -23,14 +23,12 @@ const baseURL = core.config.backendAPI.slice(0, -7);
interface Props { interface Props {
taskInstance: any; taskInstance: any;
registeredUsers: any[];
onJobUpdate(jobInstance: any): void; onJobUpdate(jobInstance: any): void;
} }
function JobListComponent(props: Props & RouteComponentProps): JSX.Element { function JobListComponent(props: Props & RouteComponentProps): JSX.Element {
const { const {
taskInstance, taskInstance,
registeredUsers,
onJobUpdate, onJobUpdate,
history: { push }, history: { push },
} = props; } = props;
@ -100,21 +98,14 @@ function JobListComponent(props: Props & RouteComponentProps): JSX.Element {
dataIndex: 'assignee', dataIndex: 'assignee',
key: 'assignee', key: 'assignee',
render: (jobInstance: any): JSX.Element => { render: (jobInstance: any): JSX.Element => {
const assignee = jobInstance.assignee ? jobInstance.assignee.username : null; const assignee = jobInstance.assignee ? jobInstance.assignee : null;
return ( return (
<UserSelector <UserSelector
users={registeredUsers}
value={assignee} value={assignee}
onChange={(value: string): void => { onSelect={(value: User | null): void => {
let [userInstance] = [...registeredUsers].filter((user: any) => user.username === value);
if (userInstance === undefined) {
userInstance = null;
}
// eslint-disable-next-line // eslint-disable-next-line
jobInstance.assignee = userInstance; jobInstance.assignee = value;
onJobUpdate(jobInstance); onJobUpdate(jobInstance);
}} }}
/> />

@ -17,6 +17,12 @@
padding: 20px; padding: 20px;
background: $background-color-1; background: $background-color-1;
.cvat-task-details-user-block {
> div:nth-child(2) > span {
margin-right: 8px;
}
}
> div:nth-child(2) { > div:nth-child(2) {
> div:nth-child(2) { > div:nth-child(2) {
padding-left: 20px; padding-left: 20px;
@ -72,8 +78,7 @@
} }
.cvat-task-top-bar { .cvat-task-top-bar {
margin-top: 20px; margin: $grid-unit-size * 2 0;
margin-bottom: 10px;
} }
.cvat-task-preview-wrapper { .cvat-task-preview-wrapper {
@ -87,11 +92,6 @@
background-color: $background-color-2; background-color: $background-color-2;
} }
.cvat-user-selector {
margin-left: 10px;
width: 150px;
}
.cvat-open-bug-tracker-button { .cvat-open-bug-tracker-button {
margin-left: 15px; margin-left: 15px;
} }

@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React from 'react';
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 Dropdown from 'antd/lib/dropdown'; import Dropdown from 'antd/lib/dropdown';
@ -18,12 +19,27 @@ interface DetailsComponentProps {
export default function DetailsComponent(props: DetailsComponentProps): JSX.Element { export default function DetailsComponent(props: DetailsComponentProps): JSX.Element {
const { taskInstance } = props; const { taskInstance } = props;
const { id } = taskInstance;
const history = useHistory();
return ( return (
<Row className='cvat-task-top-bar' type='flex' justify='space-between' align='middle'> <Row className='cvat-task-top-bar' type='flex' justify='space-between' align='middle'>
<Col> <Col>
<Text className='cvat-title'>{`Task details #${id}`}</Text> {taskInstance.projectId ? (
<Button
onClick={() => history.push(`/projects/${taskInstance.projectId}`)}
type='link'
size='large'
>
<Icon type='left' />
Back to project
</Button>
) : (
<Button onClick={() => history.push('/tasks')} type='link' size='large'>
<Icon type='left' />
Back to tasks
</Button>
)}
</Col> </Col>
<Col> <Col>
<Dropdown overlay={<ActionsMenuContainer taskInstance={taskInstance} />}> <Dropdown overlay={<ActionsMenuContainer taskInstance={taskInstance} />}>

@ -2,30 +2,108 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React, { useState, useEffect, useRef } from 'react';
import Select from 'antd/lib/select'; import Autocomplete from 'antd/lib/auto-complete';
import Input from 'antd/lib/input';
import getCore from 'cvat-core-wrapper';
import { SelectValue } from 'antd/lib/select';
import debounce from 'lodash/debounce';
const core = getCore();
export interface User {
id: number;
username: string;
}
interface Props { interface Props {
value: string | null; value: User | null;
users: any[]; onSelect: (user: User | null) => void;
onChange: (user: string) => void;
} }
const searchUsers = debounce(
(searchValue: string, setUsers: (users: User[]) => void): void => {
core.users
.get({
search: searchValue,
limit: 10,
})
.then((result: User[]) => {
if (result) {
setUsers(result);
}
});
},
250,
{
maxWait: 750,
},
);
export default function UserSelector(props: Props): JSX.Element { export default function UserSelector(props: Props): JSX.Element {
const { value, users, onChange } = props; const { value, onSelect } = props;
const [searchPhrase, setSearchPhrase] = useState('');
const [users, setUsers] = useState<User[]>([]);
const autocompleteRef = useRef<Autocomplete | null>(null);
const handleSearch = (searchValue: string): void => {
if (searchValue) {
searchUsers(searchValue, setUsers);
} else {
setUsers([]);
}
setSearchPhrase(searchValue);
};
const handleFocus = (open: boolean): void => {
if (!users.length && open) {
core.users.get({ limit: 10 }).then((result: User[]) => {
if (result) {
setUsers(result);
}
});
}
if (!open && searchPhrase !== value?.username) {
setSearchPhrase('');
if (value) {
onSelect(null);
}
}
};
const handleSelect = (_value: SelectValue): void => {
setSearchPhrase(users.filter((user) => user.id === +_value)[0].username);
onSelect(_value ? users.filter((user) => user.id === +_value)[0] : null);
};
useEffect(() => {
if (value && !users.filter((user) => user.id === value.id).length) {
core.users.get({ id: value.id }).then((result: User[]) => {
const [user] = result;
setUsers([...users, user]);
setSearchPhrase(user.username);
});
}
}, [value]);
return ( return (
<Select defaultValue={value || '—'} size='small' showSearch className='cvat-user-selector' onChange={onChange}> <Autocomplete
<Select.Option key='-1' value='—'> ref={autocompleteRef}
value={searchPhrase}
</Select.Option> placeholder='Select a user'
{users.map( onSearch={handleSearch}
(user): JSX.Element => ( onSelect={handleSelect}
<Select.Option key={user.id} value={user.username}> className='cvat-user-search-field'
{user.username} onDropdownVisibleChange={handleFocus}
</Select.Option> dataSource={users.map((user) => ({
), value: user.id.toString(),
)} text: user.username,
</Select> }))}
>
<Input onPressEnter={() => autocompleteRef.current?.blur()} />
</Autocomplete>
); );
} }

@ -31,6 +31,8 @@ export default function EmptyListComponent(): JSX.Element {
<Row type='flex' justify='center' align='middle'> <Row type='flex' justify='center' align='middle'>
<Col> <Col>
<Link to='/tasks/create'>create a new task</Link> <Link to='/tasks/create'>create a new task</Link>
<Text type='secondary'> or try to </Text>
<Link to='/projects/create'>create a new project</Link>
</Col> </Col>
</Row> </Row>
</div> </div>

@ -11,26 +11,16 @@
height: 100%; height: 100%;
width: 100%; width: 100%;
> div:nth-child(1) { > div:nth-child(2) {
padding-bottom: 10px;
div > {
span {
color: $text-color;
}
}
}
> div:nth-child(3) {
height: 83%; height: 83%;
padding-top: 10px; padding-top: 10px;
} }
> div:nth-child(4) { > div:nth-child(3) {
padding-top: 10px; padding-top: 10px;
} }
> div:nth-child(2) { > div:nth-child(1) {
> div:nth-child(1) { > div:nth-child(1) {
display: flex; display: flex;

@ -3,8 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React from 'react';
import { RouteComponentProps } from 'react-router'; import { useHistory } from 'react-router';
import { withRouter } from 'react-router-dom';
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 Input from 'antd/lib/input'; import Input from 'antd/lib/input';
@ -15,16 +14,13 @@ interface VisibleTopBarProps {
searchValue: string; searchValue: string;
} }
function TopBarComponent(props: VisibleTopBarProps & RouteComponentProps): JSX.Element { export default function TopBarComponent(props: VisibleTopBarProps): JSX.Element {
const { searchValue, history, onSearch } = props; const { searchValue, onSearch } = props;
const history = useHistory();
return ( return (
<> <>
<Row type='flex' justify='center' align='middle'>
<Col md={22} lg={18} xl={16} xxl={14}>
<Text strong>Default project</Text>
</Col>
</Row>
<Row type='flex' justify='center' align='middle'> <Row type='flex' justify='center' align='middle'>
<Col md={11} lg={9} xl={8} xxl={7}> <Col md={11} lg={9} xl={8} xxl={7}>
<Text className='cvat-title'>Tasks</Text> <Text className='cvat-title'>Tasks</Text>
@ -45,5 +41,3 @@ function TopBarComponent(props: VisibleTopBarProps & RouteComponentProps): JSX.E
</> </>
); );
} }
export default withRouter(TopBarComponent);

@ -24,7 +24,10 @@ function mapStateToProps(state: CombinedState): StateToProps {
annotation: { annotation: {
annotations: { activatedStateID, collapsed, states: objectStates }, annotations: { activatedStateID, collapsed, states: objectStates },
canvas: { canvas: {
contextMenu: { visible, top, left, type }, contextMenu: {
visible, top, left, type,
},
ready,
}, },
}, },
} = state; } = state;
@ -33,7 +36,11 @@ function mapStateToProps(state: CombinedState): StateToProps {
activatedStateID, activatedStateID,
collapsed: activatedStateID !== null ? collapsed[activatedStateID] : undefined, collapsed: activatedStateID !== null ? collapsed[activatedStateID] : undefined,
objectStates, objectStates,
visible, visible:
activatedStateID !== null &&
visible &&
ready &&
objectStates.map((_state: any): number => _state.clientID).includes(activatedStateID),
left, left,
top, top,
type, type,
@ -166,7 +173,9 @@ class CanvasContextMenuContainer extends React.PureComponent<Props, State> {
public render(): JSX.Element { public render(): JSX.Element {
const { left, top } = this.state; const { left, top } = this.state;
const { visible, activatedStateID, objectStates, type } = this.props; const {
visible, activatedStateID, objectStates, type,
} = this.props;
return ( return (
<> <>

@ -8,7 +8,7 @@ import { connect } from 'react-redux';
import { updateAnnotationsAsync } from 'actions/annotation-actions'; import { updateAnnotationsAsync } from 'actions/annotation-actions';
import LabelItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/label-item'; import LabelItemComponent from 'components/annotation-page/standard-workspace/objects-side-bar/label-item';
import { CombinedState } from 'reducers/interfaces'; import { CombinedState, ObjectType } from 'reducers/interfaces';
interface OwnProps { interface OwnProps {
labelID: number; labelID: number;
@ -92,8 +92,8 @@ class LabelItemContainer extends React.PureComponent<Props, State> {
let statesLocked = true; let statesLocked = true;
ownObjectStates.forEach((objectState: any) => { ownObjectStates.forEach((objectState: any) => {
const { lock } = objectState; const { lock, objectType } = objectState;
if (!lock) { if (!lock && objectType !== ObjectType.TAG) {
statesHidden = statesHidden && objectState.hidden; statesHidden = statesHidden && objectState.hidden;
statesLocked = statesLocked && objectState.lock; statesLocked = statesLocked && objectState.lock;
} }

@ -7,7 +7,9 @@ import copy from 'copy-to-clipboard';
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { LogType } from 'cvat-logger'; import { LogType } from 'cvat-logger';
import { ActiveControl, CombinedState, ColorBy, ShapeType } from 'reducers/interfaces'; import {
ActiveControl, CombinedState, ColorBy, ShapeType,
} from 'reducers/interfaces';
import { import {
collapseObjectItems, collapseObjectItems,
updateAnnotationsAsync, updateAnnotationsAsync,
@ -83,40 +85,25 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const stateIDs = states.map((_state: any): number => _state.clientID); const stateIDs = states.map((_state: any): number => _state.clientID);
const index = stateIDs.indexOf(clientID); const index = stateIDs.indexOf(clientID);
try { const collapsedState =
const collapsedState = typeof statesCollapsed[clientID] === 'undefined' ? initialCollapsed : statesCollapsed[clientID];
typeof statesCollapsed[clientID] === 'undefined' ? initialCollapsed : statesCollapsed[clientID];
return {
return { objectState: states[index],
objectState: states[index], collapsed: collapsedState,
collapsed: collapsedState, attributes: jobAttributes[states[index].label.id],
attributes: jobAttributes[states[index].label.id], labels,
labels, ready,
ready, activeControl,
activeControl, colorBy,
colorBy, jobInstance,
jobInstance, frameNumber,
frameNumber, activated: activatedStateID === clientID,
activated: activatedStateID === clientID, minZLayer,
minZLayer, maxZLayer,
maxZLayer, normalizedKeyMap,
normalizedKeyMap, aiToolsRef,
aiToolsRef, };
};
} catch (exception) {
// we have an exception here sometimes
// but I cannot understand when it happens and what is the root reason
// maybe this temporary hack helps us
const dataObject = {
index,
frameNumber,
clientID: own.clientID,
stateIDs,
};
throw new Error(
`${exception.toString()} in mapStateToProps of ObjectItemContainer. Data are ${JSON.stringify(dataObject)}`,
);
}
} }
function mapDispatchToProps(dispatch: any): DispatchToProps { function mapDispatchToProps(dispatch: any): DispatchToProps {
@ -219,7 +206,9 @@ class ObjectItemContainer extends React.PureComponent<Props> {
}; };
private activate = (): void => { private activate = (): void => {
const { activateObject, objectState, ready, activeControl } = this.props; const {
activateObject, objectState, ready, activeControl,
} = this.props;
if (ready && activeControl === ActiveControl.CURSOR) { if (ready && activeControl === ActiveControl.CURSOR) {
activateObject(objectState.clientID); activateObject(objectState.clientID);
@ -324,7 +313,9 @@ class ObjectItemContainer extends React.PureComponent<Props> {
} }
public render(): JSX.Element { public render(): JSX.Element {
const { objectState, collapsed, labels, attributes, activated, colorBy, normalizedKeyMap } = this.props; const {
objectState, collapsed, labels, attributes, activated, colorBy, normalizedKeyMap,
} = this.props;
let stateColor = ''; let stateColor = '';
if (colorBy === ColorBy.INSTANCE) { if (colorBy === ColorBy.INSTANCE) {

@ -231,7 +231,9 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
} }
private undo = (): void => { private undo = (): void => {
const { undo, jobInstance, frameNumber, canvasInstance } = this.props; const {
undo, jobInstance, frameNumber, canvasInstance,
} = this.props;
if (canvasInstance.isAbleToChangeFrame()) { if (canvasInstance.isAbleToChangeFrame()) {
undo(jobInstance, frameNumber); undo(jobInstance, frameNumber);
@ -239,7 +241,9 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
}; };
private redo = (): void => { private redo = (): void => {
const { redo, jobInstance, frameNumber, canvasInstance } = this.props; const {
redo, jobInstance, frameNumber, canvasInstance,
} = this.props;
if (canvasInstance.isAbleToChangeFrame()) { if (canvasInstance.isAbleToChangeFrame()) {
redo(jobInstance, frameNumber); redo(jobInstance, frameNumber);
@ -253,7 +257,9 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
}; };
private onSwitchPlay = (): void => { private onSwitchPlay = (): void => {
const { frameNumber, jobInstance, onSwitchPlay, playing } = this.props; const {
frameNumber, jobInstance, onSwitchPlay, playing,
} = this.props;
if (playing) { if (playing) {
onSwitchPlay(false); onSwitchPlay(false);
@ -263,7 +269,9 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
}; };
private onFirstFrame = (): void => { private onFirstFrame = (): void => {
const { frameNumber, jobInstance, playing, onSwitchPlay } = this.props; const {
frameNumber, jobInstance, playing, onSwitchPlay,
} = this.props;
const newFrame = jobInstance.startFrame; const newFrame = jobInstance.startFrame;
if (newFrame !== frameNumber) { if (newFrame !== frameNumber) {
@ -275,7 +283,9 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
}; };
private onBackward = (): void => { private onBackward = (): void => {
const { frameNumber, frameStep, jobInstance, playing, onSwitchPlay } = this.props; const {
frameNumber, frameStep, jobInstance, playing, onSwitchPlay,
} = this.props;
const newFrame = Math.max(jobInstance.startFrame, frameNumber - frameStep); const newFrame = Math.max(jobInstance.startFrame, frameNumber - frameStep);
if (newFrame !== frameNumber) { if (newFrame !== frameNumber) {
@ -288,7 +298,9 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
private onPrevFrame = (): void => { private onPrevFrame = (): void => {
const { prevButtonType } = this.state; const { prevButtonType } = this.state;
const { frameNumber, jobInstance, playing, onSwitchPlay, searchAnnotations, searchEmptyFrame } = this.props; const {
frameNumber, jobInstance, playing, onSwitchPlay,
} = this.props;
const { startFrame } = jobInstance; const { startFrame } = jobInstance;
const newFrame = Math.max(jobInstance.startFrame, frameNumber - 1); const newFrame = Math.max(jobInstance.startFrame, frameNumber - 1);
@ -296,19 +308,22 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
if (playing) { if (playing) {
onSwitchPlay(false); onSwitchPlay(false);
} }
if (prevButtonType === 'regular') { if (prevButtonType === 'regular') {
this.changeFrame(newFrame); this.changeFrame(newFrame);
} else if (prevButtonType === 'filtered') { } else if (prevButtonType === 'filtered') {
searchAnnotations(jobInstance, frameNumber - 1, startFrame); this.searchAnnotations(frameNumber - 1, startFrame);
} else { } else {
searchEmptyFrame(jobInstance, frameNumber - 1, startFrame); this.searchEmptyFrame(frameNumber - 1, startFrame);
} }
} }
}; };
private onNextFrame = (): void => { private onNextFrame = (): void => {
const { nextButtonType } = this.state; const { nextButtonType } = this.state;
const { frameNumber, jobInstance, playing, onSwitchPlay, searchAnnotations, searchEmptyFrame } = this.props; const {
frameNumber, jobInstance, playing, onSwitchPlay,
} = this.props;
const { stopFrame } = jobInstance; const { stopFrame } = jobInstance;
const newFrame = Math.min(jobInstance.stopFrame, frameNumber + 1); const newFrame = Math.min(jobInstance.stopFrame, frameNumber + 1);
@ -316,18 +331,21 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
if (playing) { if (playing) {
onSwitchPlay(false); onSwitchPlay(false);
} }
if (nextButtonType === 'regular') { if (nextButtonType === 'regular') {
this.changeFrame(newFrame); this.changeFrame(newFrame);
} else if (nextButtonType === 'filtered') { } else if (nextButtonType === 'filtered') {
searchAnnotations(jobInstance, frameNumber + 1, stopFrame); this.searchAnnotations(frameNumber + 1, stopFrame);
} else { } else {
searchEmptyFrame(jobInstance, frameNumber + 1, stopFrame); this.searchEmptyFrame(frameNumber + 1, stopFrame);
} }
} }
}; };
private onForward = (): void => { private onForward = (): void => {
const { frameNumber, frameStep, jobInstance, playing, onSwitchPlay } = this.props; const {
frameNumber, frameStep, jobInstance, playing, onSwitchPlay,
} = this.props;
const newFrame = Math.min(jobInstance.stopFrame, frameNumber + frameStep); const newFrame = Math.min(jobInstance.stopFrame, frameNumber + frameStep);
if (newFrame !== frameNumber) { if (newFrame !== frameNumber) {
@ -339,7 +357,9 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
}; };
private onLastFrame = (): void => { private onLastFrame = (): void => {
const { frameNumber, jobInstance, playing, onSwitchPlay } = this.props; const {
frameNumber, jobInstance, playing, onSwitchPlay,
} = this.props;
const newFrame = jobInstance.stopFrame; const newFrame = jobInstance.stopFrame;
if (newFrame !== frameNumber) { if (newFrame !== frameNumber) {
@ -418,6 +438,20 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
} }
} }
private searchAnnotations(start: number, stop: number): void {
const { canvasInstance, jobInstance, searchAnnotations } = this.props;
if (canvasInstance.isAbleToChangeFrame()) {
searchAnnotations(jobInstance, start, stop);
}
}
private searchEmptyFrame(start: number, stop: number): void {
const { canvasInstance, jobInstance, searchAnnotations } = this.props;
if (canvasInstance.isAbleToChangeFrame()) {
searchAnnotations(jobInstance, start, stop);
}
}
public render(): JSX.Element { public render(): JSX.Element {
const { nextButtonType, prevButtonType } = this.state; const { nextButtonType, prevButtonType } = this.state;
const { const {

@ -15,7 +15,6 @@ interface OwnProps {
} }
interface StateToProps { interface StateToProps {
registeredUsers: any[];
activeInference: ActiveInference | null; activeInference: ActiveInference | null;
installedGit: boolean; installedGit: boolean;
} }
@ -29,7 +28,6 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const { list } = state.plugins; const { list } = state.plugins;
return { return {
registeredUsers: state.users.users,
installedGit: list.GIT_INTEGRATION, installedGit: list.GIT_INTEGRATION,
activeInference: state.models.inferences[own.task.instance.id] || null, activeInference: state.models.inferences[own.task.instance.id] || null,
}; };
@ -47,14 +45,15 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
} }
function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps): JSX.Element { function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps): JSX.Element {
const { task, installedGit, activeInference, registeredUsers, cancelAutoAnnotation, onTaskUpdate } = props; const {
task, installedGit, activeInference, cancelAutoAnnotation, onTaskUpdate,
} = props;
return ( return (
<DetailsComponent <DetailsComponent
previewImage={task.preview} previewImage={task.preview}
taskInstance={task.instance} taskInstance={task.instance}
installedGit={installedGit} installedGit={installedGit}
registeredUsers={registeredUsers}
activeInference={activeInference} activeInference={activeInference}
onTaskUpdate={onTaskUpdate} onTaskUpdate={onTaskUpdate}
cancelAutoAnnotation={cancelAutoAnnotation} cancelAutoAnnotation={cancelAutoAnnotation}

@ -7,38 +7,26 @@ import { connect } from 'react-redux';
import JobListComponent from 'components/task-page/job-list'; import JobListComponent from 'components/task-page/job-list';
import { updateJobAsync } from 'actions/tasks-actions'; import { updateJobAsync } from 'actions/tasks-actions';
import { Task, CombinedState } from 'reducers/interfaces'; import { Task } from 'reducers/interfaces';
interface OwnProps { interface OwnProps {
task: Task; task: Task;
} }
interface StateToProps {
registeredUsers: any[];
}
interface DispatchToProps { interface DispatchToProps {
onJobUpdate(jobInstance: any): void; onJobUpdate(jobInstance: any): void;
} }
function mapStateToProps(state: CombinedState): StateToProps {
return {
registeredUsers: state.users.users,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps { function mapDispatchToProps(dispatch: any): DispatchToProps {
return { return {
onJobUpdate: (jobInstance: any): void => dispatch(updateJobAsync(jobInstance)), onJobUpdate: (jobInstance: any): void => dispatch(updateJobAsync(jobInstance)),
}; };
} }
function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps): JSX.Element { function TaskPageContainer(props: DispatchToProps & OwnProps): JSX.Element {
const { task, registeredUsers, onJobUpdate } = props; const { task, onJobUpdate } = props;
return ( return <JobListComponent taskInstance={task.instance} onJobUpdate={onJobUpdate} />;
<JobListComponent taskInstance={task.instance} registeredUsers={registeredUsers} onJobUpdate={onJobUpdate} />
);
} }
export default connect(mapStateToProps, mapDispatchToProps)(TaskPageContainer); export default connect(null, mapDispatchToProps)(TaskPageContainer);

@ -10,7 +10,6 @@ import { getPluginsAsync } from 'actions/plugins-actions';
import { switchSettingsDialog } from 'actions/settings-actions'; import { switchSettingsDialog } from 'actions/settings-actions';
import { shortcutsActions } from 'actions/shortcuts-actions'; import { shortcutsActions } from 'actions/shortcuts-actions';
import { getUserAgreementsAsync } from 'actions/useragreements-actions'; import { getUserAgreementsAsync } from 'actions/useragreements-actions';
import { getUsersAsync } from 'actions/users-actions';
import CVATApplication from 'components/cvat-app'; import CVATApplication from 'components/cvat-app';
import LayoutGrid from 'components/layout-grid/layout-grid'; import LayoutGrid from 'components/layout-grid/layout-grid';
import logger, { LogType } from 'cvat-logger'; import logger, { LogType } from 'cvat-logger';
@ -34,8 +33,6 @@ interface StateToProps {
modelsFetching: boolean; modelsFetching: boolean;
userInitialized: boolean; userInitialized: boolean;
userFetching: boolean; userFetching: boolean;
usersInitialized: boolean;
usersFetching: boolean;
aboutInitialized: boolean; aboutInitialized: boolean;
aboutFetching: boolean; aboutFetching: boolean;
formatsInitialized: boolean; formatsInitialized: boolean;
@ -55,7 +52,6 @@ interface StateToProps {
interface DispatchToProps { interface DispatchToProps {
loadFormats: () => void; loadFormats: () => void;
verifyAuthorized: () => void; verifyAuthorized: () => void;
loadUsers: () => void;
loadAbout: () => void; loadAbout: () => void;
initModels: () => void; initModels: () => void;
initPlugins: () => void; initPlugins: () => void;
@ -71,7 +67,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { plugins } = state; const { plugins } = state;
const { auth } = state; const { auth } = state;
const { formats } = state; const { formats } = state;
const { users } = state;
const { about } = state; const { about } = state;
const { shortcuts } = state; const { shortcuts } = state;
const { userAgreements } = state; const { userAgreements } = state;
@ -84,8 +79,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
pluginsFetching: plugins.fetching, pluginsFetching: plugins.fetching,
modelsInitialized: models.initialized, modelsInitialized: models.initialized,
modelsFetching: models.fetching, modelsFetching: models.fetching,
usersInitialized: users.initialized,
usersFetching: users.fetching,
aboutInitialized: about.initialized, aboutInitialized: about.initialized,
aboutFetching: about.fetching, aboutFetching: about.fetching,
formatsInitialized: formats.initialized, formatsInitialized: formats.initialized,
@ -110,7 +103,6 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
loadUserAgreements: (): void => dispatch(getUserAgreementsAsync()), loadUserAgreements: (): void => dispatch(getUserAgreementsAsync()),
initPlugins: (): void => dispatch(getPluginsAsync()), initPlugins: (): void => dispatch(getPluginsAsync()),
initModels: (): void => dispatch(getModelsAsync()), initModels: (): void => dispatch(getModelsAsync()),
loadUsers: (): void => dispatch(getUsersAsync()),
loadAbout: (): void => dispatch(getAboutAsync()), loadAbout: (): void => dispatch(getAboutAsync()),
resetErrors: (): void => dispatch(resetErrors()), resetErrors: (): void => dispatch(resetErrors()),
resetMessages: (): void => dispatch(resetMessages()), resetMessages: (): void => dispatch(resetMessages()),

@ -21,6 +21,35 @@ export interface AuthState {
allowResetPassword: boolean; allowResetPassword: boolean;
} }
export interface ProjectsQuery {
page: number;
id: number | null;
search: string | null;
owner: string | null;
name: string | null;
status: string | null;
[key: string]: string | number | null | undefined;
}
export type Project = any;
export interface ProjectsState {
initialized: boolean;
fetching: boolean;
count: number;
current: Project[];
gettingQuery: ProjectsQuery;
activities: {
creates: {
id: null | number;
error: string;
};
deletes: {
[projectId: number]: boolean; // deleted (deleting if in dictionary)
};
};
}
export interface TasksQuery { export interface TasksQuery {
page: number; page: number;
id: number | null; id: number | null;
@ -93,12 +122,6 @@ export interface PluginsState {
list: PluginsList; list: PluginsList;
} }
export interface UsersState {
users: any[];
fetching: boolean;
initialized: boolean;
}
export interface AboutState { export interface AboutState {
server: any; server: any;
packageVersion: { packageVersion: {
@ -198,6 +221,12 @@ export interface NotificationsState {
resetPassword: null | ErrorState; resetPassword: null | ErrorState;
loadAuthActions: null | ErrorState; loadAuthActions: null | ErrorState;
}; };
projects: {
fetching: null | ErrorState;
updating: null | ErrorState;
deleting: null | ErrorState;
creating: null | ErrorState;
};
tasks: { tasks: {
fetching: null | ErrorState; fetching: null | ErrorState;
updating: null | ErrorState; updating: null | ErrorState;
@ -493,8 +522,8 @@ export interface MetaState {
export interface CombinedState { export interface CombinedState {
auth: AuthState; auth: AuthState;
projects: ProjectsState;
tasks: TasksState; tasks: TasksState;
users: UsersState;
about: AboutState; about: AboutState;
share: ShareState; share: ShareState;
formats: FormatsState; formats: FormatsState;

@ -9,7 +9,7 @@ import { FormatsActionTypes } from 'actions/formats-actions';
import { ModelsActionTypes } from 'actions/models-actions'; import { ModelsActionTypes } from 'actions/models-actions';
import { ShareActionTypes } from 'actions/share-actions'; import { ShareActionTypes } from 'actions/share-actions';
import { TasksActionTypes } from 'actions/tasks-actions'; import { TasksActionTypes } from 'actions/tasks-actions';
import { UsersActionTypes } from 'actions/users-actions'; import { ProjectsActionTypes } from 'actions/projects-actions';
import { AboutActionTypes } from 'actions/about-actions'; import { AboutActionTypes } from 'actions/about-actions';
import { AnnotationActionTypes } from 'actions/annotation-actions'; import { AnnotationActionTypes } from 'actions/annotation-actions';
import { NotificationsActionType } from 'actions/notification-actions'; import { NotificationsActionType } from 'actions/notification-actions';
@ -30,6 +30,12 @@ const defaultState: NotificationsState = {
resetPassword: null, resetPassword: null,
loadAuthActions: null, loadAuthActions: null,
}, },
projects: {
fetching: null,
updating: null,
deleting: null,
creating: null,
},
tasks: { tasks: {
fetching: null, fetching: null,
updating: null, updating: null,
@ -357,8 +363,7 @@ export default function (state = defaultState, action: AnyAction): Notifications
tasks: { tasks: {
...state.errors.tasks, ...state.errors.tasks,
updating: { updating: {
message: message: `Could not update <a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
'Could not update ' + `<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
reason: action.payload.error.toString(), reason: action.payload.error.toString(),
}, },
}, },
@ -416,30 +421,81 @@ export default function (state = defaultState, action: AnyAction): Notifications
}, },
}; };
} }
case FormatsActionTypes.GET_FORMATS_FAILED: { case ProjectsActionTypes.GET_PROJECTS_FAILED: {
return { return {
...state, ...state,
errors: { errors: {
...state.errors, ...state.errors,
formats: { projects: {
...state.errors.formats, ...state.errors.projects,
fetching: { fetching: {
message: 'Could not get formats from the server', message: 'Could not fetch projects',
reason: action.payload.error.toString(), reason: action.payload.error.toString(),
}, },
}, },
}, },
}; };
} }
case UsersActionTypes.GET_USERS_FAILED: { case ProjectsActionTypes.CREATE_PROJECT_FAILED: {
return { return {
...state, ...state,
errors: { errors: {
...state.errors, ...state.errors,
users: { projects: {
...state.errors.users, ...state.errors.projects,
creating: {
message: 'Could not create the project',
reason: action.payload.error.toString(),
},
},
},
};
}
case ProjectsActionTypes.UPDATE_PROJECT_FAILED: {
const { id: projectId } = action.payload.project;
return {
...state,
errors: {
...state.errors,
projects: {
...state.errors.projects,
updating: {
message:
'Could not update ' +
`<a href="/project/${projectId}" target="_blank">project ${projectId}</a>`,
reason: action.payload.error.toString(),
},
},
},
};
}
case ProjectsActionTypes.DELETE_PROJECT_FAILED: {
const { projectId } = action.payload;
return {
...state,
errors: {
...state.errors,
projects: {
...state.errors.projects,
updating: {
message:
'Could not delete ' +
`<a href="/project/${projectId}" target="_blank">project ${projectId}</a>`,
reason: action.payload.error.toString(),
},
},
},
};
}
case FormatsActionTypes.GET_FORMATS_FAILED: {
return {
...state,
errors: {
...state.errors,
formats: {
...state.errors.formats,
fetching: { fetching: {
message: 'Could not get users from the server', message: 'Could not get formats from the server',
reason: action.payload.error.toString(), reason: action.payload.error.toString(),
}, },
}, },

@ -0,0 +1,192 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { AnyAction } from 'redux';
import { ProjectsActionTypes } from 'actions/projects-actions';
import { BoundariesActionTypes } from 'actions/boundaries-actions';
import { AuthActionTypes } from 'actions/auth-actions';
import { Project, ProjectsState } from './interfaces';
const defaultState: ProjectsState = {
initialized: false,
fetching: false,
count: 0,
current: [],
gettingQuery: {
page: 1,
id: null,
search: null,
owner: null,
name: null,
status: null,
},
activities: {
deletes: {},
creates: {
id: null,
error: '',
},
},
};
export default (state: ProjectsState = defaultState, action: AnyAction): ProjectsState => {
switch (action.type) {
case ProjectsActionTypes.UPDATE_PROJECTS_GETTING_QUERY:
return {
...state,
gettingQuery: {
...defaultState.gettingQuery,
...action.payload.query,
},
};
case ProjectsActionTypes.GET_PROJECTS:
return {
...state,
initialized: false,
fetching: true,
count: 0,
current: [],
};
case ProjectsActionTypes.GET_PROJECTS_SUCCESS: {
return {
...state,
initialized: true,
fetching: false,
count: action.payload.count,
current: action.payload.array,
};
}
case ProjectsActionTypes.GET_PROJECTS_FAILED: {
return {
...state,
initialized: true,
fetching: false,
};
}
case ProjectsActionTypes.CREATE_PROJECT: {
return {
...state,
activities: {
...state.activities,
creates: {
id: null,
error: '',
},
},
};
}
case ProjectsActionTypes.CREATE_PROJECT_FAILED: {
return {
...state,
activities: {
...state.activities,
creates: {
...state.activities.creates,
error: action.payload.error.toString(),
},
},
};
}
case ProjectsActionTypes.CREATE_PROJECT_SUCCESS: {
return {
...state,
activities: {
...state.activities,
creates: {
id: action.payload.projectId,
error: '',
},
},
};
}
case ProjectsActionTypes.UPDATE_PROJECT: {
return {
...state,
};
}
case ProjectsActionTypes.UPDATE_PROJECT_SUCCESS: {
return {
...state,
current: state.current.map(
(project): Project => {
if (project.id === action.payload.project.id) {
return action.payload.project;
}
return project;
},
),
};
}
case ProjectsActionTypes.UPDATE_PROJECT_FAILED: {
return {
...state,
current: state.current.map(
(project): Project => {
if (project.id === action.payload.project.id) {
return action.payload.project;
}
return project;
},
),
};
}
case ProjectsActionTypes.DELETE_PROJECT: {
const { projectId } = action.payload;
const { deletes } = state.activities;
deletes[projectId] = false;
return {
...state,
activities: {
...state.activities,
deletes: {
...deletes,
},
},
};
}
case ProjectsActionTypes.DELETE_PROJECT_SUCCESS: {
const { projectId } = action.payload;
const { deletes } = state.activities;
deletes[projectId] = true;
return {
...state,
activities: {
...state.activities,
deletes: {
...deletes,
},
},
};
}
case ProjectsActionTypes.DELETE_PROJECT_FAILED: {
const { projectId } = action.payload;
const { deletes } = state.activities;
delete deletes[projectId];
return {
...state,
activities: {
...state.activities,
deletes: {
...deletes,
},
},
};
}
case BoundariesActionTypes.RESET_AFTER_ERROR:
case AuthActionTypes.LOGOUT_SUCCESS: {
return { ...defaultState };
}
default:
return state;
}
};

@ -4,8 +4,8 @@
import { combineReducers, Reducer } from 'redux'; import { combineReducers, Reducer } from 'redux';
import authReducer from './auth-reducer'; import authReducer from './auth-reducer';
import projectsReducer from './projects-reducer';
import tasksReducer from './tasks-reducer'; import tasksReducer from './tasks-reducer';
import usersReducer from './users-reducer';
import aboutReducer from './about-reducer'; import aboutReducer from './about-reducer';
import shareReducer from './share-reducer'; import shareReducer from './share-reducer';
import formatsReducer from './formats-reducer'; import formatsReducer from './formats-reducer';
@ -20,8 +20,8 @@ import userAgreementsReducer from './useragreements-reducer';
export default function createRootReducer(): Reducer { export default function createRootReducer(): Reducer {
return combineReducers({ return combineReducers({
auth: authReducer, auth: authReducer,
projects: projectsReducer,
tasks: tasksReducer, tasks: tasksReducer,
users: usersReducer,
about: aboutReducer, about: aboutReducer,
share: shareReducer, share: shareReducer,
formats: formatsReducer, formats: formatsReducer,

@ -1,48 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { BoundariesActionTypes, BoundariesActions } from 'actions/boundaries-actions';
import { AuthActionTypes, AuthActions } from 'actions/auth-actions';
import { UsersActionTypes, UsersActions } from 'actions/users-actions';
import { UsersState } from './interfaces';
const defaultState: UsersState = {
users: [],
fetching: false,
initialized: false,
};
export default function (
state: UsersState = defaultState,
action: UsersActions | AuthActions | BoundariesActions,
): UsersState {
switch (action.type) {
case UsersActionTypes.GET_USERS: {
return {
...state,
fetching: true,
initialized: false,
};
}
case UsersActionTypes.GET_USERS_SUCCESS:
return {
...state,
fetching: false,
initialized: true,
users: action.payload.users,
};
case UsersActionTypes.GET_USERS_FAILED:
return {
...state,
fetching: false,
initialized: true,
};
case BoundariesActionTypes.RESET_AFTER_ERROR:
case AuthActionTypes.LOGOUT_SUCCESS: {
return { ...defaultState };
}
default:
return state;
}
}

@ -4,6 +4,6 @@
from cvat.utils.version import get_version from cvat.utils.version import get_version
VERSION = (1, 2, 0, 'alpha', 0) VERSION = (1, 2, 0, 'beta', 0)
__version__ = get_version(VERSION) __version__ = get_version(VERSION)

@ -1,5 +1,6 @@
{% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %}{% blocktrans with {% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %}
site_name=current_site.name site_domain=current_site.domain %}Hello from {{ site_name }}! {% blocktrans with site_name=current_site.name site_domain=current_site.domain %}
Hello from {{ site_name }}!
<p> <p>
You're receiving this e-mail because user <strong>{{ user_display }}</strong> has given yours as an e-mail address You're receiving this e-mail because user <strong>{{ user_display }}</strong> has given yours as an e-mail address
@ -7,6 +8,7 @@ site_name=current_site.name site_domain=current_site.domain %}Hello from {{ site
</p> </p>
<p>To confirm this is correct, go to <a href="{{ activate_url }}">{{ activate_url }}</a></p> <p>To confirm this is correct, go to <a href="{{ activate_url }}">{{ activate_url }}</a></p>
{% endblocktrans %} {% blocktrans with site_name=current_site.name site_domain=current_site.domain %} {% endblocktrans %}
{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}
<strong>{{ site_domain }}</strong> <strong>{{ site_domain }}</strong>
{% endblocktrans %} {% endautoescape %} {% endblocktrans %} {% endautoescape %}

@ -1,5 +1,16 @@
{% load i18n %}{% autoescape off %} {% blocktrans %}You're receiving this email because you requested a password reset {% load i18n %}{% autoescape off %}
for your user account at {{ site_name }}.{% endblocktrans %} {% trans "Please go to the following page and choose a new {% blocktrans %}
password:" %} {% block reset_link %} {{ protocol }}://{{ domain }}/auth/password/reset/confirm?uid={{ uid }}&token={{ You're receiving this email because you requested a password reset for your user account at {{ site_name }}.
token }} {% endblock %} {% trans "Your username, in case you've forgotten:" %} {{ user.get_username }} {% trans "Thanks {% endblocktrans %}
for using our site!" %} {% blocktrans %}The {{ site_name }} team{% endblocktrans %} {% endautoescape %}
{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}/auth/password/reset/confirm?uid={{ uid }}&token={{ token }}
{% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
{% trans "Thanks for using our site!" %}
{% blocktrans %}The {{ site_name }} team{% endblocktrans %}
{% endautoescape %}

@ -42,7 +42,7 @@ class TaskData:
self._frame_mapping = {} self._frame_mapping = {}
self._frame_step = db_task.data.get_frame_step() self._frame_step = db_task.data.get_frame_step()
db_labels = self._db_task.label_set.all().prefetch_related( db_labels = (self._db_task.project if self._db_task.project_id else self._db_task).label_set.all().prefetch_related(
'attributespec_set').order_by('pk') 'attributespec_set').order_by('pk')
self._label_mapping = OrderedDict( self._label_mapping = OrderedDict(

@ -93,7 +93,8 @@ class JobAnnotation:
self.ir_data = AnnotationIR() self.ir_data = AnnotationIR()
self.db_labels = {db_label.id:db_label self.db_labels = {db_label.id:db_label
for db_label in db_segment.task.label_set.all()} for db_label in (db_segment.task.project.label_set.all()
if db_segment.task.project_id else db_segment.task.label_set.all())}
self.db_attributes = {} self.db_attributes = {}
for db_label in self.db_labels.values(): for db_label in self.db_labels.values():

@ -218,8 +218,6 @@ class TaskExportTest(_DbTestBase):
def _generate_task(self, images): def _generate_task(self, images):
task = { task = {
"name": "my task #1", "name": "my task #1",
"owner": '',
"assignee": '',
"overlap": 0, "overlap": 0,
"segment_size": 100, "segment_size": 100,
"labels": [ "labels": [
@ -438,8 +436,6 @@ class FrameMatchingTest(_DbTestBase):
def _generate_task(self, images): def _generate_task(self, images):
task = { task = {
"name": "my task #1", "name": "my task #1",
"owner": '',
"assignee": '',
"overlap": 0, "overlap": 0,
"segment_size": 100, "segment_size": 100,
"labels": [ "labels": [

@ -1,5 +1,5 @@
<!-- <!--
Copyright (C) 2018 Intel Corporation Copyright (C) 2018-2020 Intel Corporation
SPDX-License-Identifier: MIT SPDX-License-Identifier: MIT
--> -->
@ -15,8 +15,7 @@
</head> </head>
<body> <body>
<xmp id="content" style="display: none" <xmp id="content" style="display: none">
>
{% autoescape off %} {% autoescape off %}
{% block content %} {% block content %}
{% endblock %} {% endblock %}

@ -1,7 +1,14 @@
<!-- <!--
Copyright (C) 2018 Intel Corporation Copyright (C) 2018-2020 Intel Corporation
SPDX-License-Identifier: MIT SPDX-License-Identifier: MIT
--> -->
{% extends 'documentation/base_page.html' %} {% block title %} CVAT User Guide {% endblock %} {% block content %} {{ {% extends 'documentation/base_page.html' %}
user_guide }} {% endblock %}
{% block title %}
CVAT User Guide
{% endblock %}
{% block content %}
{{ user_guide }}
{% endblock %}

@ -1,7 +1,8 @@
<!-- <!--
Copyright (C) 2018 Intel Corporation Copyright (C) 2018-2020 Intel Corporation
SPDX-License-Identifier: MIT SPDX-License-Identifier: MIT
--> -->
{% extends 'documentation/base_page.html' %} {% block title %} CVAT XML format {% endblock %} {% block content %} {{ {% extends 'documentation/base_page.html' %}
xml_format }} {% endblock %} {% block title %} CVAT XML format {% endblock %}
{% block content %} {{ xml_format }} {% endblock %}

@ -4,7 +4,7 @@
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
from django.contrib import admin from django.contrib import admin
from .models import Task, Segment, Job, Label, AttributeSpec from .models import Task, Segment, Job, Label, AttributeSpec, Project
class JobInline(admin.TabularInline): class JobInline(admin.TabularInline):
model = Job model = Job
@ -54,6 +54,20 @@ class SegmentAdmin(admin.ModelAdmin):
JobInline JobInline
] ]
class ProjectAdmin(admin.ModelAdmin):
date_hierarchy = 'updated_date'
readonly_fields = ('created_date', 'updated_date', 'status')
fields = ('name', 'owner', 'created_date', 'updated_date', 'status')
search_fields = ('name', 'owner__username', 'owner__first_name',
'owner__last_name', 'owner__email', 'assignee__username', 'assignee__first_name',
'assignee__last_name')
inlines = [
LabelInline
]
def has_add_permission(self, _request):
return False
class TaskAdmin(admin.ModelAdmin): class TaskAdmin(admin.ModelAdmin):
date_hierarchy = 'updated_date' date_hierarchy = 'updated_date'
readonly_fields = ('created_date', 'updated_date', 'overlap') readonly_fields = ('created_date', 'updated_date', 'overlap')
@ -74,3 +88,4 @@ class TaskAdmin(admin.ModelAdmin):
admin.site.register(Task, TaskAdmin) admin.site.register(Task, TaskAdmin)
admin.site.register(Segment, SegmentAdmin) admin.site.register(Segment, SegmentAdmin)
admin.site.register(Label, LabelAdmin) admin.site.register(Label, LabelAdmin)
admin.site.register(Project, ProjectAdmin)

@ -2,10 +2,15 @@
# #
# SPDX-License-Identifier: MIT # SPDX-License-Identifier: MIT
import os
import logging import logging
from cvat.settings.base import LOGGING from cvat.settings.base import LOGGING
from .models import Job, Task from .models import Job, Task, Project
def _get_project(pid):
try:
return Project.objects.get(pk=pid)
except Exception:
raise Exception('{} key must be a project identifier'.format(pid))
def _get_task(tid): def _get_task(tid):
try: try:
@ -19,6 +24,28 @@ def _get_job(jid):
except Exception: except Exception:
raise Exception('{} key must be a job identifier'.format(jid)) raise Exception('{} key must be a job identifier'.format(jid))
class ProjectLoggerStorage:
def __init__(self):
self._storage = dict()
def __getitem__(self, pid):
"""Get ceratain storage object for some project."""
if pid not in self._storage:
self._storage[pid] = self._create_project_logger(pid)
return self._storage[pid]
def _create_project_logger(self, pid):
project = _get_project(pid)
logger = logging.getLogger('cvat.server.project_{}'.format(pid))
server_file = logging.FileHandler(filename=project.get_log_path())
formatter = logging.Formatter(LOGGING['formatters']['standard']['format'])
server_file.setFormatter(formatter)
logger.addHandler(server_file)
return logger
class TaskLoggerStorage: class TaskLoggerStorage:
def __init__(self): def __init__(self):
self._storage = dict() self._storage = dict()
@ -52,6 +79,24 @@ class JobLoggerStorage:
job = _get_job(jid) job = _get_job(jid)
return slogger.task[job.segment.task.id] return slogger.task[job.segment.task.id]
class ProjectClientLoggerStorage:
def __init__(self):
self._storage = dict()
def __getitem__(self, pid):
"""Get logger for exact task by id."""
if pid not in self._storage:
self._storage[pid] = self._create_client_logger(pid)
return self._storage[pid]
def _create_client_logger(self, pid):
project = _get_project(pid)
logger = logging.getLogger('cvat.client.project_{}'.format(pid))
client_file = logging.FileHandler(filename=project.get_client_log_path())
logger.addHandler(client_file)
return logger
class TaskClientLoggerStorage: class TaskClientLoggerStorage:
def __init__(self): def __init__(self):
self._storage = dict() self._storage = dict()
@ -89,12 +134,14 @@ class dotdict(dict):
__delattr__ = dict.__delitem__ __delattr__ = dict.__delitem__
clogger = dotdict({ clogger = dotdict({
'project': ProjectClientLoggerStorage(),
'task': TaskClientLoggerStorage(), 'task': TaskClientLoggerStorage(),
'job': JobClientLoggerStorage(), 'job': JobClientLoggerStorage(),
'glob': logging.getLogger('cvat.client'), 'glob': logging.getLogger('cvat.client'),
}) })
slogger = dotdict({ slogger = dotdict({
'project': ProjectLoggerStorage(),
'task': TaskLoggerStorage(), 'task': TaskLoggerStorage(),
'job': JobLoggerStorage(), 'job': JobLoggerStorage(),
'glob': logging.getLogger('cvat.server'), 'glob': logging.getLogger('cvat.server'),

@ -0,0 +1,24 @@
# Generated by Django 3.1.1 on 2020-09-24 12:44
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('engine', '0032_remove_task_z_order'),
]
operations = [
migrations.AddField(
model_name='label',
name='project',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='engine.project'),
),
migrations.AlterField(
model_name='label',
name='task',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='engine.task'),
),
]

@ -164,10 +164,25 @@ class Project(models.Model):
status = models.CharField(max_length=32, choices=StatusChoice.choices(), status = models.CharField(max_length=32, choices=StatusChoice.choices(),
default=StatusChoice.ANNOTATION) default=StatusChoice.ANNOTATION)
def get_project_dirname(self):
return os.path.join(settings.PROJECTS_ROOT, str(self.id))
def get_project_logs_dirname(self):
return os.path.join(self.get_project_dirname(), 'logs')
def get_client_log_path(self):
return os.path.join(self.get_project_logs_dirname(), "client.log")
def get_log_path(self):
return os.path.join(self.get_project_logs_dirname(), "project.log")
# Extend default permission model # Extend default permission model
class Meta: class Meta:
default_permissions = () default_permissions = ()
def __str__(self):
return self.name
class Task(models.Model): class Task(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE, project = models.ForeignKey(Project, on_delete=models.CASCADE,
null=True, blank=True, related_name="tasks", null=True, blank=True, related_name="tasks",
@ -268,7 +283,8 @@ class Job(models.Model):
default_permissions = () default_permissions = ()
class Label(models.Model): class Label(models.Model):
task = models.ForeignKey(Task, on_delete=models.CASCADE) task = models.ForeignKey(Task, null=True, blank=True, on_delete=models.CASCADE)
project = models.ForeignKey(Project, null=True, blank=True, on_delete=models.CASCADE)
name = SafeCharField(max_length=64) name = SafeCharField(max_length=64)
color = models.CharField(default='', max_length=8) color = models.CharField(default='', max_length=8)
@ -324,7 +340,7 @@ class ShapeType(str, Enum):
POLYGON = 'polygon' # (x0, y0, ..., xn, yn) POLYGON = 'polygon' # (x0, y0, ..., xn, yn)
POLYLINE = 'polyline' # (x0, y0, ..., xn, yn) POLYLINE = 'polyline' # (x0, y0, ..., xn, yn)
POINTS = 'points' # (x0, y0, ..., xn, yn) POINTS = 'points' # (x0, y0, ..., xn, yn)
CUBOID = 'cuboid' CUBOID = 'cuboid' # (x0, y0, ..., x7, y7)
@classmethod @classmethod
def choices(cls): def choices(cls):

@ -13,6 +13,36 @@ from cvat.apps.engine import models
from cvat.apps.engine.log import slogger from cvat.apps.engine.log import slogger
from cvat.apps.dataset_manager.formats.utils import get_label_color from cvat.apps.dataset_manager.formats.utils import get_label_color
class BasicUserSerializer(serializers.ModelSerializer):
def validate(self, data):
if hasattr(self, 'initial_data'):
unknown_keys = set(self.initial_data.keys()) - set(self.fields.keys())
if unknown_keys:
if set(['is_staff', 'is_superuser', 'groups']) & unknown_keys:
message = 'You do not have permissions to access some of' + \
' these fields: {}'.format(unknown_keys)
else:
message = 'Got unknown fields: {}'.format(unknown_keys)
raise serializers.ValidationError(message)
return data
class Meta:
model = User
fields = ('url', 'id', 'username', 'first_name', 'last_name')
ordering = ['-id']
class UserSerializer(serializers.ModelSerializer):
groups = serializers.SlugRelatedField(many=True,
slug_field='name', queryset=Group.objects.all())
class Meta:
model = User
fields = ('url', 'id', 'username', 'first_name', 'last_name', 'email',
'groups', 'is_staff', 'is_superuser', 'is_active', 'last_login',
'date_joined')
read_only_fields = ('last_login', 'date_joined')
write_only_fields = ('password', )
ordering = ['-id']
class AttributeSerializer(serializers.ModelSerializer): class AttributeSerializer(serializers.ModelSerializer):
class Meta: class Meta:
@ -44,6 +74,47 @@ class LabelSerializer(serializers.ModelSerializer):
model = models.Label model = models.Label
fields = ('id', 'name', 'color', 'attributes') fields = ('id', 'name', 'color', 'attributes')
@staticmethod
def update_instance(validated_data, parent_instance):
attributes = validated_data.pop('attributespec_set', [])
instance = dict()
if isinstance(parent_instance, models.Project):
instance['project'] = parent_instance
logger = slogger.project[parent_instance.id]
else:
instance['task'] = parent_instance
logger = slogger.task[parent_instance.id]
(db_label, created) = models.Label.objects.get_or_create(name=validated_data['name'],
**instance)
if created:
logger.info("New {} label was created".format(db_label.name))
else:
logger.info("{} label was updated".format(db_label.name))
if not validated_data.get('color', None):
label_names = [l.name for l in
instance[tuple(instance.keys())[0]].label_set.exclude(id=db_label.id).order_by('id')
]
db_label.color = get_label_color(db_label.name, label_names)
else:
db_label.color = validated_data.get('color', db_label.color)
db_label.save()
for attr in attributes:
(db_attr, created) = models.AttributeSpec.objects.get_or_create(
label=db_label, name=attr['name'], defaults=attr)
if created:
logger.info("New {} attribute for {} label was created"
.format(db_attr.name, db_label.name))
else:
logger.info("{} attribute for {} label was updated"
.format(db_attr.name, db_label.name))
# FIXME: need to update only "safe" fields
db_attr.default_value = attr.get('default_value', db_attr.default_value)
db_attr.mutable = attr.get('mutable', db_attr.mutable)
db_attr.input_type = attr.get('input_type', db_attr.input_type)
db_attr.values = attr.get('values', db_attr.values)
db_attr.save()
class JobCommitSerializer(serializers.ModelSerializer): class JobCommitSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.JobCommit model = models.JobCommit
@ -53,16 +124,21 @@ class JobSerializer(serializers.ModelSerializer):
task_id = serializers.ReadOnlyField(source="segment.task.id") task_id = serializers.ReadOnlyField(source="segment.task.id")
start_frame = serializers.ReadOnlyField(source="segment.start_frame") start_frame = serializers.ReadOnlyField(source="segment.start_frame")
stop_frame = serializers.ReadOnlyField(source="segment.stop_frame") stop_frame = serializers.ReadOnlyField(source="segment.stop_frame")
assignee = BasicUserSerializer(allow_null=True, required=False)
assignee_id = serializers.IntegerField(write_only=True, allow_null=True, required=False)
class Meta: class Meta:
model = models.Job model = models.Job
fields = ('url', 'id', 'assignee', 'status', 'start_frame', fields = ('url', 'id', 'assignee', 'assignee_id', 'status', 'start_frame',
'stop_frame', 'task_id') 'stop_frame', 'task_id')
class SimpleJobSerializer(serializers.ModelSerializer): class SimpleJobSerializer(serializers.ModelSerializer):
assignee = BasicUserSerializer(allow_null=True)
assignee_id = serializers.IntegerField(write_only=True, allow_null=True)
class Meta: class Meta:
model = models.Job model = models.Job
fields = ('url', 'id', 'assignee', 'status') fields = ('url', 'id', 'assignee', 'assignee_id', 'status')
class SegmentSerializer(serializers.ModelSerializer): class SegmentSerializer(serializers.ModelSerializer):
jobs = SimpleJobSerializer(many=True, source='job_set') jobs = SimpleJobSerializer(many=True, source='job_set')
@ -120,6 +196,7 @@ class RqStatusSerializer(serializers.Serializer):
message = serializers.CharField(allow_blank=True, default="") message = serializers.CharField(allow_blank=True, default="")
class WriteOnceMixin: class WriteOnceMixin:
"""Adds support for write once fields to serializers. """Adds support for write once fields to serializers.
To use it, specify a list of fields as `write_once_fields` on the To use it, specify a list of fields as `write_once_fields` on the
@ -233,7 +310,7 @@ class DataSerializer(serializers.ModelSerializer):
return db_data return db_data
class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer): class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer):
labels = LabelSerializer(many=True, source='label_set', partial=True) labels = LabelSerializer(many=True, source='label_set', partial=True, required=False)
segments = SegmentSerializer(many=True, source='segment_set', read_only=True) segments = SegmentSerializer(many=True, source='segment_set', read_only=True)
data_chunk_size = serializers.ReadOnlyField(source='data.chunk_size') data_chunk_size = serializers.ReadOnlyField(source='data.chunk_size')
data_compressed_chunk_type = serializers.ReadOnlyField(source='data.compressed_chunk_type') data_compressed_chunk_type = serializers.ReadOnlyField(source='data.compressed_chunk_type')
@ -241,21 +318,31 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer):
size = serializers.ReadOnlyField(source='data.size') size = serializers.ReadOnlyField(source='data.size')
image_quality = serializers.ReadOnlyField(source='data.image_quality') image_quality = serializers.ReadOnlyField(source='data.image_quality')
data = serializers.ReadOnlyField(source='data.id') data = serializers.ReadOnlyField(source='data.id')
owner = BasicUserSerializer(required=False)
owner_id = serializers.IntegerField(write_only=True, allow_null=True, required=False)
assignee = BasicUserSerializer(allow_null=True, required=False)
assignee_id = serializers.IntegerField(write_only=True, allow_null=True, required=False)
project_id = serializers.IntegerField(required=False)
class Meta: class Meta:
model = models.Task model = models.Task
fields = ('url', 'id', 'name', 'mode', 'owner', 'assignee', fields = ('url', 'id', 'name', 'project_id', 'mode', 'owner', 'assignee', 'owner_id', 'assignee_id',
'bug_tracker', 'created_date', 'updated_date', 'overlap', 'bug_tracker', 'created_date', 'updated_date', 'overlap',
'segment_size', 'status', 'labels', 'segments', 'segment_size', 'status', 'labels', 'segments',
'project', 'data_chunk_size', 'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality', 'data') 'data_chunk_size', 'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality', 'data')
read_only_fields = ('mode', 'created_date', 'updated_date', 'status', 'data_chunk_size', read_only_fields = ('mode', 'created_date', 'updated_date', 'status', 'data_chunk_size', 'owner', 'asignee',
'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality', 'data') 'data_compressed_chunk_type', 'data_original_chunk_type', 'size', 'image_quality', 'data')
write_once_fields = ('overlap', 'segment_size') write_once_fields = ('overlap', 'segment_size', 'project_id')
ordering = ['-id'] ordering = ['-id']
# pylint: disable=no-self-use # pylint: disable=no-self-use
def create(self, validated_data): def create(self, validated_data):
labels = validated_data.pop('label_set') if not (validated_data.get("label_set") or validated_data.get("project_id")):
raise serializers.ValidationError('Label set or project_id must be present')
if validated_data.get("label_set") and validated_data.get("project_id"):
raise serializers.ValidationError('Project must have only one of Label set or project_id')
labels = validated_data.pop('label_set', [])
db_task = models.Task.objects.create(**validated_data) db_task = models.Task.objects.create(**validated_data)
label_names = list() label_names = list()
for label in labels: for label in labels:
@ -277,100 +364,96 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer):
db_task.save() db_task.save()
return db_task return db_task
def to_representation(self, instance):
response = super().to_representation(instance)
if instance.project_id:
response["labels"] = LabelSerializer(many=True).to_representation(instance.project.label_set)
return response
# pylint: disable=no-self-use # pylint: disable=no-self-use
def update(self, instance, validated_data): def update(self, instance, validated_data):
instance.name = validated_data.get('name', instance.name) instance.name = validated_data.get('name', instance.name)
instance.owner = validated_data.get('owner', instance.owner) instance.owner_id = validated_data.get('owner_id', instance.owner_id)
instance.assignee = validated_data.get('assignee', instance.assignee) instance.assignee_id = validated_data.get('assignee_id', instance.assignee_id)
instance.bug_tracker = validated_data.get('bug_tracker', instance.bug_tracker = validated_data.get('bug_tracker',
instance.bug_tracker) instance.bug_tracker)
instance.project = validated_data.get('project', instance.project)
labels = validated_data.get('label_set', []) labels = validated_data.get('label_set', [])
for label in labels: for label in labels:
attributes = label.pop('attributespec_set', []) LabelSerializer.update_instance(label, instance)
(db_label, created) = models.Label.objects.get_or_create(task=instance,
name=label['name'])
if created:
slogger.task[instance.id].info("New {} label was created"
.format(db_label.name))
else:
slogger.task[instance.id].info("{} label was updated"
.format(db_label.name))
if not label.get('color', None):
label_names = [l.name for l in
instance.label_set.all().exclude(id=db_label.id).order_by('id')
]
db_label.color = get_label_color(db_label.name, label_names)
else:
db_label.color = label.get('color', db_label.color)
db_label.save()
for attr in attributes:
(db_attr, created) = models.AttributeSpec.objects.get_or_create(
label=db_label, name=attr['name'], defaults=attr)
if created:
slogger.task[instance.id].info("New {} attribute for {} label was created"
.format(db_attr.name, db_label.name))
else:
slogger.task[instance.id].info("{} attribute for {} label was updated"
.format(db_attr.name, db_label.name))
# FIXME: need to update only "safe" fields
db_attr.default_value = attr.get('default_value', db_attr.default_value)
db_attr.mutable = attr.get('mutable', db_attr.mutable)
db_attr.input_type = attr.get('input_type', db_attr.input_type)
db_attr.values = attr.get('values', db_attr.values)
db_attr.save()
instance.save() instance.save()
return instance return instance
def validate_labels(self, value): def validate_labels(self, value):
if not value:
raise serializers.ValidationError('Label set must not be empty')
label_names = [label['name'] for label in value] label_names = [label['name'] for label in value]
if len(label_names) != len(set(label_names)): if len(label_names) != len(set(label_names)):
raise serializers.ValidationError('All label names must be unique for the task') raise serializers.ValidationError('All label names must be unique for the task')
return value return value
class ProjectSearchSerializer(serializers.ModelSerializer):
class ProjectSerializer(serializers.ModelSerializer):
class Meta: class Meta:
model = models.Project model = models.Project
fields = ('url', 'id', 'name', 'owner', 'assignee', 'bug_tracker', fields = ('id', 'name')
'created_date', 'updated_date', 'status') read_only_fields = ('name',)
read_only_fields = ('created_date', 'updated_date', 'status')
ordering = ['-id'] ordering = ['-id']
class BasicUserSerializer(serializers.ModelSerializer):
def validate(self, data):
if hasattr(self, 'initial_data'):
unknown_keys = set(self.initial_data.keys()) - set(self.fields.keys())
if unknown_keys:
if set(['is_staff', 'is_superuser', 'groups']) & unknown_keys:
message = 'You do not have permissions to access some of' + \
' these fields: {}'.format(unknown_keys)
else:
message = 'Got unknown fields: {}'.format(unknown_keys)
raise serializers.ValidationError(message)
return data
class ProjectSerializer(serializers.ModelSerializer):
labels = LabelSerializer(many=True, source='label_set', partial=True, default=[])
tasks = TaskSerializer(many=True, read_only=True)
owner = BasicUserSerializer(required=False)
owner_id = serializers.IntegerField(write_only=True, allow_null=True, required=False)
assignee = BasicUserSerializer(allow_null=True, required=False)
assignee_id = serializers.IntegerField(write_only=True, allow_null=True, required=False)
class Meta: class Meta:
model = User model = models.Project
fields = ('url', 'id', 'username', 'first_name', 'last_name') fields = ('url', 'id', 'name', 'labels', 'tasks', 'owner', 'assignee', 'owner_id', 'assignee_id',
'bug_tracker', 'created_date', 'updated_date', 'status')
read_only_fields = ('created_date', 'updated_date', 'status', 'owner', 'asignee')
ordering = ['-id'] ordering = ['-id']
class UserSerializer(serializers.ModelSerializer): # pylint: disable=no-self-use
groups = serializers.SlugRelatedField(many=True, def create(self, validated_data):
slug_field='name', queryset=Group.objects.all()) labels = validated_data.pop('label_set')
db_project = models.Project.objects.create(**validated_data)
label_names = list()
for label in labels:
attributes = label.pop('attributespec_set')
if not label.get('color', None):
label['color'] = get_label_color(label['name'], label_names)
label_names.append(label['name'])
db_label = models.Label.objects.create(project=db_project, **label)
for attr in attributes:
models.AttributeSpec.objects.create(label=db_label, **attr)
class Meta: project_path = db_project.get_project_dirname()
model = User if os.path.isdir(project_path):
fields = ('url', 'id', 'username', 'first_name', 'last_name', 'email', shutil.rmtree(project_path)
'groups', 'is_staff', 'is_superuser', 'is_active', 'last_login', os.makedirs(db_project.get_project_logs_dirname())
'date_joined')
read_only_fields = ('last_login', 'date_joined') db_project.save()
write_only_fields = ('password', ) return db_project
ordering = ['-id']
# pylint: disable=no-self-use
def update(self, instance, validated_data):
instance.name = validated_data.get('name', instance.name)
instance.owner_id = validated_data.get('owner_id', instance.owner_id)
instance.assignee_id = validated_data.get('assignee_id', instance.assignee_id)
instance.bug_tracker = validated_data.get('bug_tracker', instance.bug_tracker)
labels = validated_data.get('label_set', [])
for label in labels:
LabelSerializer.update_instance(label, instance)
instance.save()
return instance
def validate_labels(self, value):
if value:
label_names = [label['name'] for label in value]
if len(label_names) != len(set(label_names)):
raise serializers.ValidationError('All label names must be unique for the project')
return value
class ExceptionSerializer(serializers.Serializer): class ExceptionSerializer(serializers.Serializer):
system = serializers.CharField(max_length=255) system = serializers.CharField(max_length=255)

@ -358,7 +358,7 @@ def _create_thread(tid, data):
img_sizes = [] img_sizes = []
with open(db_data.get_dummy_chunk_path(chunk_number), 'w') as dummy_chunk: with open(db_data.get_dummy_chunk_path(chunk_number), 'w') as dummy_chunk:
for path, frame_id in chunk_paths: for path, frame_id in chunk_paths:
dummy_chunk.write(path + '\n') dummy_chunk.write(os.path.relpath(path, upload_dir) + '\n')
img_sizes.append(extractor.get_image_size(frame_id)) img_sizes.append(extractor.get_image_size(frame_id))
db_images.extend([ db_images.extend([

@ -29,7 +29,7 @@ from rest_framework import status
from rest_framework.test import APIClient, APITestCase from rest_framework.test import APIClient, APITestCase
from cvat.apps.engine.models import (AttributeType, Data, Job, Project, from cvat.apps.engine.models import (AttributeType, Data, Job, Project,
Segment, StatusChoice, Task, StorageMethodChoice, StorageChoice) Segment, StatusChoice, Task, Label, StorageMethodChoice, StorageChoice)
from cvat.apps.engine.prepare import prepare_meta, prepare_meta_for_upload from cvat.apps.engine.prepare import prepare_meta, prepare_meta_for_upload
def create_db_users(cls): def create_db_users(cls):
@ -297,47 +297,47 @@ class JobUpdateAPITestCase(APITestCase):
self.assertEqual(response.data["id"], self.job.id) self.assertEqual(response.data["id"], self.job.id)
self.assertEqual(response.data["status"], data.get('status', self.job.status)) self.assertEqual(response.data["status"], data.get('status', self.job.status))
assignee = self.job.assignee.id if self.job.assignee else None assignee = self.job.assignee.id if self.job.assignee else None
self.assertEqual(response.data["assignee"], data.get('assignee', assignee)) self.assertEqual(response.data["assignee"]["id"], data.get('assignee_id', assignee))
self.assertEqual(response.data["start_frame"], self.job.segment.start_frame) self.assertEqual(response.data["start_frame"], self.job.segment.start_frame)
self.assertEqual(response.data["stop_frame"], self.job.segment.stop_frame) self.assertEqual(response.data["stop_frame"], self.job.segment.stop_frame)
def test_api_v1_jobs_id_admin(self): def test_api_v1_jobs_id_admin(self):
data = {"status": StatusChoice.COMPLETED, "assignee": self.owner.id} data = {"status": StatusChoice.COMPLETED, "assignee_id": self.owner.id}
response = self._run_api_v1_jobs_id(self.job.id, self.admin, data) response = self._run_api_v1_jobs_id(self.job.id, self.admin, data)
self._check_request(response, data) self._check_request(response, data)
response = self._run_api_v1_jobs_id(self.job.id + 10, self.admin, data) response = self._run_api_v1_jobs_id(self.job.id + 10, self.admin, data)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_api_v1_jobs_id_owner(self): def test_api_v1_jobs_id_owner(self):
data = {"status": StatusChoice.VALIDATION, "assignee": self.annotator.id} data = {"status": StatusChoice.VALIDATION, "assignee_id": self.annotator.id}
response = self._run_api_v1_jobs_id(self.job.id, self.owner, data) response = self._run_api_v1_jobs_id(self.job.id, self.owner, data)
self._check_request(response, data) self._check_request(response, data)
response = self._run_api_v1_jobs_id(self.job.id + 10, self.owner, data) response = self._run_api_v1_jobs_id(self.job.id + 10, self.owner, data)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_api_v1_jobs_id_annotator(self): def test_api_v1_jobs_id_annotator(self):
data = {"status": StatusChoice.ANNOTATION, "assignee": self.user.id} data = {"status": StatusChoice.ANNOTATION, "assignee_id": self.user.id}
response = self._run_api_v1_jobs_id(self.job.id, self.annotator, data) response = self._run_api_v1_jobs_id(self.job.id, self.annotator, data)
self._check_request(response, data) self._check_request(response, data)
response = self._run_api_v1_jobs_id(self.job.id + 10, self.annotator, data) response = self._run_api_v1_jobs_id(self.job.id + 10, self.annotator, data)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_api_v1_jobs_id_observer(self): def test_api_v1_jobs_id_observer(self):
data = {"status": StatusChoice.ANNOTATION, "assignee": self.admin.id} data = {"status": StatusChoice.ANNOTATION, "assignee_id": self.admin.id}
response = self._run_api_v1_jobs_id(self.job.id, self.observer, data) response = self._run_api_v1_jobs_id(self.job.id, self.observer, data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
response = self._run_api_v1_jobs_id(self.job.id + 10, self.observer, data) response = self._run_api_v1_jobs_id(self.job.id + 10, self.observer, data)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_api_v1_jobs_id_user(self): def test_api_v1_jobs_id_user(self):
data = {"status": StatusChoice.ANNOTATION, "assignee": self.user.id} data = {"status": StatusChoice.ANNOTATION, "assignee_id": self.user.id}
response = self._run_api_v1_jobs_id(self.job.id, self.user, data) response = self._run_api_v1_jobs_id(self.job.id, self.user, data)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
response = self._run_api_v1_jobs_id(self.job.id + 10, self.user, data) response = self._run_api_v1_jobs_id(self.job.id + 10, self.user, data)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND) self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
def test_api_v1_jobs_id_no_auth(self): def test_api_v1_jobs_id_no_auth(self):
data = {"status": StatusChoice.ANNOTATION, "assignee": self.user.id} data = {"status": StatusChoice.ANNOTATION, "assignee_id": self.user.id}
response = self._run_api_v1_jobs_id(self.job.id, None, data) response = self._run_api_v1_jobs_id(self.job.id, None, data)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED) self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
response = self._run_api_v1_jobs_id(self.job.id + 10, None, data) response = self._run_api_v1_jobs_id(self.job.id + 10, None, data)
@ -356,7 +356,7 @@ class JobPartialUpdateAPITestCase(JobUpdateAPITestCase):
self._check_request(response, data) self._check_request(response, data)
def test_api_v1_jobs_id_admin_partial(self): def test_api_v1_jobs_id_admin_partial(self):
data = {"assignee": self.user.id} data = {"assignee_id": self.user.id}
response = self._run_api_v1_jobs_id(self.job.id, self.owner, data) response = self._run_api_v1_jobs_id(self.job.id, self.owner, data)
self._check_request(response, data) self._check_request(response, data)
@ -754,10 +754,13 @@ class ProjectGetAPITestCase(APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["name"], db_project.name) self.assertEqual(response.data["name"], db_project.name)
owner = db_project.owner.id if db_project.owner else None owner = db_project.owner.id if db_project.owner else None
self.assertEqual(response.data["owner"], owner) response_owner = response.data["owner"]["id"] if response.data["owner"] else None
self.assertEqual(response_owner, owner)
assignee = db_project.assignee.id if db_project.assignee else None assignee = db_project.assignee.id if db_project.assignee else None
self.assertEqual(response.data["assignee"], assignee) response_assignee = response.data["assignee"]["id"] if response.data["assignee"] else None
self.assertEqual(response_assignee, assignee)
self.assertEqual(response.data["status"], db_project.status) self.assertEqual(response.data["status"], db_project.status)
self.assertEqual(response.data["bug_tracker"], db_project.bug_tracker)
def _check_api_v1_projects_id(self, user): def _check_api_v1_projects_id(self, user):
for db_project in self.projects: for db_project in self.projects:
@ -835,10 +838,15 @@ class ProjectCreateAPITestCase(APITestCase):
def _check_response(self, response, user, data): def _check_response(self, response, user, data):
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["name"], data["name"]) self.assertEqual(response.data["name"], data["name"])
self.assertEqual(response.data["owner"], data.get("owner", user.id)) self.assertEqual(response.data["owner"]["id"], data.get("owner_id", user.id))
self.assertEqual(response.data["assignee"], data.get("assignee")) response_assignee = response.data["assignee"]["id"] if response.data["assignee"] else None
self.assertEqual(response_assignee, data.get('assignee_id', None))
self.assertEqual(response.data["bug_tracker"], data.get("bug_tracker", "")) self.assertEqual(response.data["bug_tracker"], data.get("bug_tracker", ""))
self.assertEqual(response.data["status"], StatusChoice.ANNOTATION) self.assertEqual(response.data["status"], StatusChoice.ANNOTATION)
self.assertListEqual(
[label["name"] for label in data.get("labels", [])],
[label["name"] for label in response.data["labels"]]
)
def _check_api_v1_projects(self, user, data): def _check_api_v1_projects(self, user, data):
response = self._run_api_v1_projects(user, data) response = self._run_api_v1_projects(user, data)
@ -857,18 +865,26 @@ class ProjectCreateAPITestCase(APITestCase):
self._check_api_v1_projects(self.admin, data) self._check_api_v1_projects(self.admin, data)
data = { data = {
"owner": self.owner.id, "owner_id": self.owner.id,
"assignee": self.assignee.id, "assignee_id": self.assignee.id,
"name": "new name for the project" "name": "new name for the project"
} }
self._check_api_v1_projects(self.admin, data) self._check_api_v1_projects(self.admin, data)
data = { data = {
"owner": self.admin.id, "owner_id": self.admin.id,
"name": "2" "name": "2"
} }
self._check_api_v1_projects(self.admin, data) self._check_api_v1_projects(self.admin, data)
data = {
"name": "Project with labels",
"labels": [{
"name": "car",
}]
}
self._check_api_v1_projects(self.admin, data)
def test_api_v1_projects_user(self): def test_api_v1_projects_user(self):
data = { data = {
@ -878,8 +894,8 @@ class ProjectCreateAPITestCase(APITestCase):
self._check_api_v1_projects(self.user, data) self._check_api_v1_projects(self.user, data)
data = { data = {
"owner": self.owner.id, "owner_id": self.owner.id,
"assignee": self.assignee.id, "assignee_id": self.assignee.id,
"name": "My import project with data" "name": "My import project with data"
} }
self._check_api_v1_projects(self.user, data) self._check_api_v1_projects(self.user, data)
@ -888,15 +904,15 @@ class ProjectCreateAPITestCase(APITestCase):
def test_api_v1_projects_observer(self): def test_api_v1_projects_observer(self):
data = { data = {
"name": "My Project #1", "name": "My Project #1",
"owner": self.owner.id, "owner_id": self.owner.id,
"assignee": self.assignee.id "assignee_id": self.assignee.id
} }
self._check_api_v1_projects(self.observer, data) self._check_api_v1_projects(self.observer, data)
def test_api_v1_projects_no_auth(self): def test_api_v1_projects_no_auth(self):
data = { data = {
"name": "My Project #2", "name": "My Project #2",
"owner": self.admin.id, "owner_id": self.admin.id,
} }
self._check_api_v1_projects(None, data) self._check_api_v1_projects(None, data)
@ -918,15 +934,16 @@ class ProjectPartialUpdateAPITestCase(APITestCase):
def _check_response(self, response, db_project, data): def _check_response(self, response, db_project, data):
self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.status_code, status.HTTP_200_OK)
name = data.get("name", db_project.name) name = data.get("name", data.get("name", db_project.name))
self.assertEqual(response.data["name"], name) self.assertEqual(response.data["name"], name)
owner = db_project.owner.id if db_project.owner else None response_owner = response.data["owner"]["id"] if response.data["owner"] else None
owner = data.get("owner", owner) db_owner = db_project.owner.id if db_project.owner else None
self.assertEqual(response.data["owner"], owner) self.assertEqual(response_owner, data.get("owner_id", db_owner))
assignee = db_project.assignee.id if db_project.assignee else None response_assignee = response.data["assignee"]["id"] if response.data["assignee"] else None
assignee = data.get("assignee", assignee) db_assignee = db_project.assignee.id if db_project.assignee else None
self.assertEqual(response.data["assignee"], assignee) self.assertEqual(response_assignee, data.get("assignee_id", db_assignee))
self.assertEqual(response.data["status"], db_project.status) self.assertEqual(response.data["status"], data.get("status", db_project.status))
self.assertEqual(response.data["bug_tracker"], data.get("bug_tracker", db_project.bug_tracker))
def _check_api_v1_projects_id(self, user, data): def _check_api_v1_projects_id(self, user, data):
for db_project in self.projects: for db_project in self.projects:
@ -941,14 +958,15 @@ class ProjectPartialUpdateAPITestCase(APITestCase):
def test_api_v1_projects_id_admin(self): def test_api_v1_projects_id_admin(self):
data = { data = {
"name": "new name for the project", "name": "new name for the project",
"owner": self.owner.id, "owner_id": self.owner.id,
"bug_tracker": "https://new.bug.tracker",
} }
self._check_api_v1_projects_id(self.admin, data) self._check_api_v1_projects_id(self.admin, data)
def test_api_v1_projects_id_user(self): def test_api_v1_projects_id_user(self):
data = { data = {
"name": "new name for the project", "name": "new name for the project",
"owner": self.assignee.id, "owner_id": self.assignee.id,
} }
self._check_api_v1_projects_id(self.user, data) self._check_api_v1_projects_id(self.user, data)
@ -1073,9 +1091,11 @@ class TaskGetAPITestCase(APITestCase):
self.assertEqual(response.data["size"], db_task.data.size) self.assertEqual(response.data["size"], db_task.data.size)
self.assertEqual(response.data["mode"], db_task.mode) self.assertEqual(response.data["mode"], db_task.mode)
owner = db_task.owner.id if db_task.owner else None owner = db_task.owner.id if db_task.owner else None
self.assertEqual(response.data["owner"], owner) response_owner = response.data["owner"]["id"] if response.data["owner"] else None
self.assertEqual(response_owner, owner)
assignee = db_task.assignee.id if db_task.assignee else None assignee = db_task.assignee.id if db_task.assignee else None
self.assertEqual(response.data["assignee"], assignee) response_assignee = response.data["assignee"]["id"] if response.data["assignee"] else None
self.assertEqual(response_assignee, assignee)
self.assertEqual(response.data["overlap"], db_task.overlap) self.assertEqual(response.data["overlap"], db_task.overlap)
self.assertEqual(response.data["segment_size"], db_task.segment_size) self.assertEqual(response.data["segment_size"], db_task.segment_size)
self.assertEqual(response.data["image_quality"], db_task.data.image_quality) self.assertEqual(response.data["image_quality"], db_task.data.image_quality)
@ -1179,11 +1199,13 @@ class TaskUpdateAPITestCase(APITestCase):
mode = data.get("mode", db_task.mode) mode = data.get("mode", db_task.mode)
self.assertEqual(response.data["mode"], mode) self.assertEqual(response.data["mode"], mode)
owner = db_task.owner.id if db_task.owner else None owner = db_task.owner.id if db_task.owner else None
owner = data.get("owner", owner) owner = data.get("owner_id", owner)
self.assertEqual(response.data["owner"], owner) response_owner = response.data["owner"]["id"] if response.data["owner"] else None
self.assertEqual(response_owner, owner)
assignee = db_task.assignee.id if db_task.assignee else None assignee = db_task.assignee.id if db_task.assignee else None
assignee = data.get("assignee", assignee) assignee = data.get("assignee_id", assignee)
self.assertEqual(response.data["assignee"], assignee) response_assignee = response.data["assignee"]["id"] if response.data["assignee"] else None
self.assertEqual(response_assignee, assignee)
self.assertEqual(response.data["overlap"], db_task.overlap) self.assertEqual(response.data["overlap"], db_task.overlap)
self.assertEqual(response.data["segment_size"], db_task.segment_size) self.assertEqual(response.data["segment_size"], db_task.segment_size)
image_quality = data.get("image_quality", db_task.data.image_quality) image_quality = data.get("image_quality", db_task.data.image_quality)
@ -1213,7 +1235,7 @@ class TaskUpdateAPITestCase(APITestCase):
def test_api_v1_tasks_id_admin(self): def test_api_v1_tasks_id_admin(self):
data = { data = {
"name": "new name for the task", "name": "new name for the task",
"owner": self.owner.id, "owner_id": self.owner.id,
"labels": [{ "labels": [{
"name": "non-vehicle", "name": "non-vehicle",
"attributes": [{ "attributes": [{
@ -1229,7 +1251,7 @@ class TaskUpdateAPITestCase(APITestCase):
def test_api_v1_tasks_id_user(self): def test_api_v1_tasks_id_user(self):
data = { data = {
"name": "new name for the task", "name": "new name for the task",
"owner": self.assignee.id, "owner_id": self.assignee.id,
"labels": [{ "labels": [{
"name": "car", "name": "car",
"attributes": [{ "attributes": [{
@ -1277,7 +1299,7 @@ class TaskPartialUpdateAPITestCase(TaskUpdateAPITestCase):
data = { data = {
"name": "new name for the task", "name": "new name for the task",
"owner": self.owner.id "owner_id": self.owner.id
} }
self._check_api_v1_tasks_id(self.admin, data) self._check_api_v1_tasks_id(self.admin, data)
# Now owner is updated, but self.db_tasks are obsolete # Now owner is updated, but self.db_tasks are obsolete
@ -1300,8 +1322,8 @@ class TaskPartialUpdateAPITestCase(TaskUpdateAPITestCase):
self._check_api_v1_tasks_id(self.user, data) self._check_api_v1_tasks_id(self.user, data)
data = { data = {
"owner": self.observer.id, "owner_id": self.observer.id,
"assignee": self.annotator.id "assignee_id": self.annotator.id
} }
self._check_api_v1_tasks_id(self.user, data) self._check_api_v1_tasks_id(self.user, data)
@ -1324,6 +1346,16 @@ class TaskPartialUpdateAPITestCase(TaskUpdateAPITestCase):
class TaskCreateAPITestCase(APITestCase): class TaskCreateAPITestCase(APITestCase):
def setUp(self): def setUp(self):
self.client = APIClient() self.client = APIClient()
project = {
"name": "Project for task creation",
"owner": self.user,
}
self.project = Project.objects.create(**project)
label = {
"name": "car",
"project": self.project
}
Label.objects.create(**label)
@classmethod @classmethod
def setUpTestData(cls): def setUpTestData(cls):
@ -1339,8 +1371,10 @@ class TaskCreateAPITestCase(APITestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED) self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["name"], data["name"]) self.assertEqual(response.data["name"], data["name"])
self.assertEqual(response.data["mode"], "") self.assertEqual(response.data["mode"], "")
self.assertEqual(response.data["owner"], data.get("owner", user.id)) self.assertEqual(response.data["project_id"], data.get("project_id", None))
self.assertEqual(response.data["assignee"], data.get("assignee")) self.assertEqual(response.data["owner"]["id"], data.get("owner_id", user.id))
assignee = response.data["assignee"]["id"] if response.data["assignee"] else None
self.assertEqual(assignee, data.get("assignee_id", None))
self.assertEqual(response.data["bug_tracker"], data.get("bug_tracker", "")) self.assertEqual(response.data["bug_tracker"], data.get("bug_tracker", ""))
self.assertEqual(response.data["overlap"], data.get("overlap", None)) self.assertEqual(response.data["overlap"], data.get("overlap", None))
self.assertEqual(response.data["segment_size"], data.get("segment_size", 0)) self.assertEqual(response.data["segment_size"], data.get("segment_size", 0))
@ -1377,7 +1411,7 @@ class TaskCreateAPITestCase(APITestCase):
def test_api_v1_tasks_user(self): def test_api_v1_tasks_user(self):
data = { data = {
"name": "new name for the task", "name": "new name for the task",
"owner": self.assignee.id, "owner_id": self.assignee.id,
"labels": [{ "labels": [{
"name": "car", "name": "car",
"attributes": [{ "attributes": [{
@ -1391,6 +1425,17 @@ class TaskCreateAPITestCase(APITestCase):
} }
self._check_api_v1_tasks(self.user, data) self._check_api_v1_tasks(self.user, data)
def test_api_vi_tasks_user_project(self):
data = {
"name": "new name for the task",
"project_id": self.project.id,
}
response = self._run_api_v1_tasks(self.user, data)
data["labels"] = [{
"name": "car"
}]
self._check_response(response, self.user, data)
def test_api_v1_tasks_observer(self): def test_api_v1_tasks_observer(self):
data = { data = {
"name": "new name for the task", "name": "new name for the task",
@ -1664,8 +1709,8 @@ class TaskDataAPITestCase(APITestCase):
response = self._get_task(user, task_id) response = self._get_task(user, task_id)
expected_status_code = status.HTTP_200_OK expected_status_code = status.HTTP_200_OK
if user == self.user and "owner" in spec and spec["owner"] != user.id and \ if user == self.user and "owner_id" in spec and spec["owner_id"] != user.id and \
"assignee" in spec and spec["assignee"] != user.id: "assignee_id" in spec and spec["assignee_id"] != user.id:
expected_status_code = status.HTTP_403_FORBIDDEN expected_status_code = status.HTTP_403_FORBIDDEN
self.assertEqual(response.status_code, expected_status_code) self.assertEqual(response.status_code, expected_status_code)
@ -1753,8 +1798,8 @@ class TaskDataAPITestCase(APITestCase):
def _test_api_v1_tasks_id_data(self, user): def _test_api_v1_tasks_id_data(self, user):
task_spec = { task_spec = {
"name": "my task #1", "name": "my task #1",
"owner": self.owner.id, "owner_id": self.owner.id,
"assignee": self.assignee.id, "assignee_id": self.assignee.id,
"overlap": 0, "overlap": 0,
"segment_size": 100, "segment_size": 100,
"labels": [ "labels": [
@ -2143,8 +2188,8 @@ class TaskDataAPITestCase(APITestCase):
def test_api_v1_tasks_id_data_no_auth(self): def test_api_v1_tasks_id_data_no_auth(self):
data = { data = {
"name": "my task #3", "name": "my task #3",
"owner": self.owner.id, "owner_id": self.owner.id,
"assignee": self.assignee.id, "assignee_id": self.assignee.id,
"overlap": 0, "overlap": 0,
"segment_size": 100, "segment_size": 100,
"labels": [ "labels": [
@ -2189,8 +2234,8 @@ class JobAnnotationAPITestCase(APITestCase):
def _create_task(self, owner, assignee): def _create_task(self, owner, assignee):
data = { data = {
"name": "my task #1", "name": "my task #1",
"owner": owner.id, "owner_id": owner.id,
"assignee": assignee.id, "assignee_id": assignee.id,
"overlap": 0, "overlap": 0,
"segment_size": 100, "segment_size": 100,
"labels": [ "labels": [

@ -37,14 +37,15 @@ from cvat.apps.authentication import auth
from cvat.apps.dataset_manager.serializers import DatasetFormatsSerializer from cvat.apps.dataset_manager.serializers import DatasetFormatsSerializer
from cvat.apps.engine.frame_provider import FrameProvider from cvat.apps.engine.frame_provider import FrameProvider
from cvat.apps.engine.models import ( from cvat.apps.engine.models import (
Job, StatusChoice, Task, StorageMethodChoice, StorageChoice Job, StatusChoice, Task, Project, StorageMethodChoice, StorageChoice
) )
from cvat.apps.engine.serializers import ( from cvat.apps.engine.serializers import (
AboutSerializer, AnnotationFileSerializer, BasicUserSerializer, AboutSerializer, AnnotationFileSerializer, BasicUserSerializer,
DataMetaSerializer, DataSerializer, ExceptionSerializer, DataMetaSerializer, DataSerializer, ExceptionSerializer,
FileInfoSerializer, JobSerializer, LabeledDataSerializer, FileInfoSerializer, JobSerializer, LabeledDataSerializer,
LogEventSerializer, ProjectSerializer, RqStatusSerializer, LogEventSerializer, ProjectSerializer, ProjectSearchSerializer,
TaskSerializer, UserSerializer, PluginsSerializer, RqStatusSerializer, TaskSerializer, UserSerializer,
PluginsSerializer,
) )
from cvat.apps.engine.utils import av_scan_paths from cvat.apps.engine.utils import av_scan_paths
@ -194,14 +195,13 @@ class ProjectFilter(filters.FilterSet):
name = filters.CharFilter(field_name="name", lookup_expr="icontains") name = filters.CharFilter(field_name="name", lookup_expr="icontains")
owner = filters.CharFilter(field_name="owner__username", lookup_expr="icontains") owner = filters.CharFilter(field_name="owner__username", lookup_expr="icontains")
status = filters.CharFilter(field_name="status", lookup_expr="icontains") status = filters.CharFilter(field_name="status", lookup_expr="icontains")
assignee = filters.CharFilter(field_name="assignee__username", lookup_expr="icontains")
class Meta: class Meta:
model = models.Project model = models.Project
fields = ("id", "name", "owner", "status", "assignee") fields = ("id", "name", "owner", "status")
@method_decorator(name='list', decorator=swagger_auto_schema( @method_decorator(name='list', decorator=swagger_auto_schema(
operation_summary='Returns a paginated list of projects according to query parameters (10 projects per page)', operation_summary='Returns a paginated list of projects according to query parameters (12 projects per page)',
manual_parameters=[ manual_parameters=[
openapi.Parameter('id', openapi.IN_QUERY, description="A unique number value identifying this project", openapi.Parameter('id', openapi.IN_QUERY, description="A unique number value identifying this project",
type=openapi.TYPE_NUMBER), type=openapi.TYPE_NUMBER),
@ -210,21 +210,24 @@ class ProjectFilter(filters.FilterSet):
openapi.Parameter('owner', openapi.IN_QUERY, description="Find all project where owner name contains a parameter value", openapi.Parameter('owner', openapi.IN_QUERY, description="Find all project where owner name contains a parameter value",
type=openapi.TYPE_STRING), type=openapi.TYPE_STRING),
openapi.Parameter('status', openapi.IN_QUERY, description="Find all projects with a specific status", openapi.Parameter('status', openapi.IN_QUERY, description="Find all projects with a specific status",
type=openapi.TYPE_STRING, enum=[str(i) for i in StatusChoice]), type=openapi.TYPE_STRING, enum=[str(i) for i in StatusChoice])]))
openapi.Parameter('assignee', openapi.IN_QUERY, description="Find all projects where assignee name contains a parameter value",
type=openapi.TYPE_STRING)]))
@method_decorator(name='create', decorator=swagger_auto_schema(operation_summary='Method creates a new project')) @method_decorator(name='create', decorator=swagger_auto_schema(operation_summary='Method creates a new project'))
@method_decorator(name='retrieve', decorator=swagger_auto_schema(operation_summary='Method returns details of a specific project')) @method_decorator(name='retrieve', decorator=swagger_auto_schema(operation_summary='Method returns details of a specific project'))
@method_decorator(name='destroy', decorator=swagger_auto_schema(operation_summary='Method deletes a specific project')) @method_decorator(name='destroy', decorator=swagger_auto_schema(operation_summary='Method deletes a specific project'))
@method_decorator(name='partial_update', decorator=swagger_auto_schema(operation_summary='Methods does a partial update of chosen fields in a project')) @method_decorator(name='partial_update', decorator=swagger_auto_schema(operation_summary='Methods does a partial update of chosen fields in a project'))
class ProjectViewSet(auth.ProjectGetQuerySetMixin, viewsets.ModelViewSet): class ProjectViewSet(auth.ProjectGetQuerySetMixin, viewsets.ModelViewSet):
queryset = models.Project.objects.all().order_by('-id') queryset = models.Project.objects.all().order_by('-id')
serializer_class = ProjectSerializer search_fields = ("name", "owner__username", "status")
search_fields = ("name", "owner__username", "assignee__username", "status")
filterset_class = ProjectFilter filterset_class = ProjectFilter
ordering_fields = ("id", "name", "owner", "status", "assignee") ordering_fields = ("id", "name", "owner", "status", "assignee")
http_method_names = ['get', 'post', 'head', 'patch', 'delete'] http_method_names = ['get', 'post', 'head', 'patch', 'delete']
def get_serializer_class(self):
if self.request.query_params and self.request.query_params.get("names_only") == "true":
return ProjectSearchSerializer
else:
return ProjectSerializer
def get_permissions(self): def get_permissions(self):
http_method = self.request.method http_method = self.request.method
permissions = [IsAuthenticated] permissions = [IsAuthenticated]
@ -243,9 +246,19 @@ class ProjectViewSet(auth.ProjectGetQuerySetMixin, viewsets.ModelViewSet):
return [perm() for perm in permissions] return [perm() for perm in permissions]
def perform_create(self, serializer): def perform_create(self, serializer):
if self.request.data.get('owner', None): def validate_project_limit(owner):
admin_perm = auth.AdminRolePermission()
is_admin = admin_perm.has_permission(self.request, self)
if not is_admin and settings.RESTRICTIONS['project_limit'] is not None and \
Project.objects.filter(owner=owner).count() >= settings.RESTRICTIONS['project_limit']:
raise serializers.ValidationError('The user has the maximum number of projects')
owner = self.request.data.get('owner', None)
if owner:
validate_project_limit(owner)
serializer.save() serializer.save()
else: else:
validate_project_limit(self.request.user)
serializer.save(owner=self.request.user) serializer.save(owner=self.request.user)
@swagger_auto_schema(method='get', operation_summary='Returns information of the tasks of the project with the selected id', @swagger_auto_schema(method='get', operation_summary='Returns information of the tasks of the project with the selected id',
@ -713,7 +726,16 @@ class JobViewSet(viewsets.GenericViewSet,
return Response(data=str(e), status=status.HTTP_400_BAD_REQUEST) return Response(data=str(e), status=status.HTTP_400_BAD_REQUEST)
return Response(data) return Response(data)
class UserFilter(filters.FilterSet):
class Meta:
model = User
fields = ("id",)
@method_decorator(name='list', decorator=swagger_auto_schema( @method_decorator(name='list', decorator=swagger_auto_schema(
manual_parameters=[
openapi.Parameter('id',openapi.IN_QUERY,description="A unique number value identifying this user",type=openapi.TYPE_NUMBER),
],
operation_summary='Method provides a paginated list of users registered on the server')) operation_summary='Method provides a paginated list of users registered on the server'))
@method_decorator(name='retrieve', decorator=swagger_auto_schema( @method_decorator(name='retrieve', decorator=swagger_auto_schema(
operation_summary='Method provides information of a specific user')) operation_summary='Method provides information of a specific user'))
@ -725,6 +747,8 @@ class UserViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin): mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin):
queryset = User.objects.prefetch_related('groups').all().order_by('id') queryset = User.objects.prefetch_related('groups').all().order_by('id')
http_method_names = ['get', 'post', 'head', 'patch', 'delete'] http_method_names = ['get', 'post', 'head', 'patch', 'delete']
search_fields = ('username', 'first_name', 'last_name')
filterset_class = UserFilter
def get_serializer_class(self): def get_serializer_class(self):
user = self.request.user user = self.request.user

@ -346,6 +346,9 @@ os.makedirs(CACHE_ROOT, exist_ok=True)
TASKS_ROOT = os.path.join(DATA_ROOT, 'tasks') TASKS_ROOT = os.path.join(DATA_ROOT, 'tasks')
os.makedirs(TASKS_ROOT, exist_ok=True) os.makedirs(TASKS_ROOT, exist_ok=True)
PROJECTS_ROOT = os.path.join(DATA_ROOT, 'projects')
os.makedirs(PROJECTS_ROOT, exist_ok=True)
SHARE_ROOT = os.path.join(BASE_DIR, 'share') SHARE_ROOT = os.path.join(BASE_DIR, 'share')
os.makedirs(SHARE_ROOT, exist_ok=True) os.makedirs(SHARE_ROOT, exist_ok=True)
@ -427,6 +430,9 @@ RESTRICTIONS = {
# this setting limits the number of tasks for the user # this setting limits the number of tasks for the user
'task_limit': None, 'task_limit': None,
# this setting limits the number of projects for the user
'project_limit': None,
# this setting reduse task visibility to owner and assignee only # this setting reduse task visibility to owner and assignee only
'reduce_task_visibility': False, 'reduce_task_visibility': False,

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save