Merge branch 'develop' into mk/share_without_copying_

main
Maya 5 years ago
commit c1da108f6c

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

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

4
.gitignore vendored

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

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

@ -21,6 +21,21 @@
},
"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",
"type": "python",

@ -1,27 +1,22 @@
{
"python.pythonPath": ".env/bin/python",
"eslint.enable": true,
"eslint.validate": [
"eslint.probe": [
"javascript",
"typescript",
"typescriptreact",
"typescriptreact"
],
"eslint.onIgnoredFiles": "warn",
"eslint.workingDirectories": [
{
"directory": "./cvat-core",
"changeProcessCWD": true
"directory": "${cwd}",
},
{
"directory": "./cvat-canvas",
"changeProcessCWD": true
"pattern": "cvat-*"
},
{
"directory": "./cvat-ui",
"changeProcessCWD": true
},
{
"directory": ".",
"changeProcessCWD": true
"directory": "tests",
"!cwd": 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/),
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
- 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>)
- Added layout grids toggling ('ctrl + alt + Enter')
- 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>)
- 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>)
- User search field for assignee fields (<https://github.com/openvinotoolkit/cvat/pull/2370>)
### 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>)
- UI packages installation with `npm ci` instead of `npm install` (<https://github.com/openvinotoolkit/cvat/pull/2350>)
### Deprecated
-
### Removed
-
- Removed Z-Order flag from task creation process
### 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>)
- 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
### Added

@ -21,8 +21,6 @@ COPY cvat-canvas/package*.json /tmp/cvat-canvas/
COPY cvat-ui/package*.json /tmp/cvat-ui/
COPY cvat-data/package*.json /tmp/cvat-data/
RUN npm config set loglevel info
# Install cvat-data dependencies
WORKDIR /tmp/cvat-data/
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/)
## 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.

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

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

@ -4,11 +4,9 @@
module.exports = {
env: {
node: false,
node: true,
browser: true,
es6: true,
jquery: true,
qunit: true,
'jest/globals': true,
},
parserOptions: {
@ -16,7 +14,7 @@ module.exports = {
sourceType: 'module',
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'],
rules: {
'no-await-in-loop': [0],

File diff suppressed because it is too large Load Diff

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

@ -16,26 +16,7 @@
const { AnnotationFormats } = require('./annotation-formats');
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
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;
}
const { Project } = require('./project');
function implementAPI(cvat) {
cvat.plugins.list.implementation = PluginRegistry.list;
@ -120,9 +101,17 @@
return result;
};
cvat.server.installedApps.implementation = async () => {
const result = await serverProxy.server.installedApps();
return result;
};
cvat.users.get.implementation = async (filter) => {
checkFilter(filter, {
id: isInteger,
self: isBoolean,
search: isString,
limit: isInteger,
});
let users = null;
@ -130,7 +119,13 @@
users = await serverProxy.users.getSelf();
users = [users];
} 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));
@ -163,8 +158,7 @@
// If task was found by its id, then create task instance and get Job instance from it
if (tasks !== null && tasks.length) {
const users = (await serverProxy.users.getUsers()).map((userData) => new User(userData));
const task = new Task(attachUsers(tasks[0], users));
const task = new Task(tasks[0]);
return filter.jobID ? task.jobs.filter((job) => job.id === filter.jobID) : task.jobs;
}
@ -175,6 +169,7 @@
cvat.tasks.get.implementation = async (filter) => {
checkFilter(filter, {
page: isInteger,
projectId: isInteger,
name: isString,
id: isInteger,
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();
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)) {
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 tasks = tasksData.map((task) => attachUsers(task, users)).map((task) => new Task(task));
const tasks = tasksData.map((task) => new Task(task));
tasks.count = tasksData.count;
return tasks;
};
cvat.server.installedApps.implementation = async () => {
const result = await serverProxy.server.installedApps();
return result;
cvat.projects.get.implementation = async (filter) => {
checkFilter(filter, {
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;
}

@ -14,6 +14,7 @@ function build() {
const ObjectState = require('./object-state');
const Statistics = require('./statistics');
const { Job, Task } = require('./session');
const { Project } = require('./project');
const { Attribute, Label } = require('./labels');
const MLModel = require('./ml-model');
@ -274,6 +275,60 @@ function build() {
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 tasks
@ -291,6 +346,7 @@ function build() {
* @property {integer} page Get specific page
* (default REST API returns 20 tasks per request.
* 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} assignee Check if assigneed contains this value
* @property {string} search Combined search of contains among all fields
@ -717,8 +773,9 @@ function build() {
* @memberof module:API.cvat
*/
classes: {
Task,
User,
Project,
Task,
Job,
Log,
Attribute,
@ -730,6 +787,7 @@ function build() {
};
cvat.server = Object.freeze(cvat.server);
cvat.projects = Object.freeze(cvat.projects);
cvat.tasks = Object.freeze(cvat.tasks);
cvat.jobs = Object.freeze(cvat.jobs);
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 = '') {
const { backendAPI } = config;
@ -347,7 +423,12 @@
const { backendAPI } = config;
try {
await Axios.delete(`${backendAPI}/tasks/${id}`);
await Axios.delete(`${backendAPI}/tasks/${id}`, {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
@ -500,20 +581,14 @@
}
}
async function getUsers(id = null) {
async function getUsers(filter = 'page_size=all') {
const { backendAPI } = config;
let response = null;
try {
if (id === null) {
response = await Axios.get(`${backendAPI}/users?page_size=all`, {
proxy: config.proxy,
});
} else {
response = await Axios.get(`${backendAPI}/users/${id}`, {
proxy: config.proxy,
});
}
response = await Axios.get(`${backendAPI}/users?${filter}`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
@ -835,6 +910,17 @@
writable: false,
},
projects: {
value: Object.freeze({
get: getProjects,
searchNames: searchProjectNames,
save: saveProject,
create: createProject,
delete: deleteProject,
}),
writable: false,
},
tasks: {
value: Object.freeze({
getTasks,

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

@ -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 = {
count: 5,
next: null,
@ -154,7 +322,11 @@ const tasksDummyData = {
name: 'Test',
size: 1,
mode: 'annotation',
owner: 1,
owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'admin',
},
assignee: null,
bug_tracker: '',
created_date: '2019-09-05T11:59:22.987942Z',
@ -194,7 +366,11 @@ const tasksDummyData = {
name: 'Image Task',
size: 9,
mode: 'annotation',
owner: 1,
owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'admin',
},
assignee: null,
bug_tracker: '',
created_date: '2019-06-18T13:05:08.941304+03:00',
@ -239,7 +415,11 @@ const tasksDummyData = {
name: 'Video Task',
size: 5002,
mode: 'interpolation',
owner: 1,
owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'admin',
},
assignee: null,
bug_tracker: '',
created_date: '2019-06-21T16:34:49.199691+03:00',
@ -558,7 +738,11 @@ const tasksDummyData = {
name: 'Test Task',
size: 5002,
mode: 'interpolation',
owner: 2,
owner: {
url: 'http://localhost:7000/api/v1/users/2',
id: 2,
username: 'bsekache',
},
assignee: null,
bug_tracker: '',
created_date: '2019-05-16T13:08:00.621747+03:00',
@ -767,7 +951,11 @@ const tasksDummyData = {
name: 'Video',
size: 75,
mode: 'interpolation',
owner: 1,
owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'admin',
},
assignee: null,
bug_tracker: '',
created_date: '2019-05-15T11:40:19.487999+03:00',
@ -964,7 +1152,11 @@ const tasksDummyData = {
name: 'Labels Set',
size: 9,
mode: 'annotation',
owner: 1,
owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'admin',
},
assignee: null,
bug_tracker: 'http://bugtracker.com/issue12345',
created_date: '2019-05-13T15:35:29.871003+03:00',
@ -2328,6 +2520,7 @@ const frameMetaDummyData = {
module.exports = {
tasksDummyData,
projectsDummyData,
aboutDummyData,
shareDummyData,
usersDummyData,

@ -4,6 +4,7 @@
const {
tasksDummyData,
projectsDummyData,
aboutDummyData,
formatsDummyData,
shareDummyData,
@ -13,6 +14,22 @@ const {
frameMetaDummyData,
} = 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 {
constructor() {
async function about() {
@ -55,23 +72,65 @@ class ServerProxy {
return null;
}
async function getTasks(filter = '') {
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;
async function getProjects(filter = '') {
const queries = QueryStringToJSON(filter);
const result = projectsDummyData.results.filter((x) => {
for (const key in queries) {
if (Object.prototype.hasOwnProperty.call(queries, key)) {
// TODO: Particular match for some fields is not checked
if (queries[key] !== x[key]) {
return false;
}
}
}
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
const queries = QueryStringToJSON(filter);
const result = tasksDummyData.results.filter((x) => {
@ -94,8 +153,8 @@ class ServerProxy {
const object = tasksDummyData.results.filter((task) => task.id === id)[0];
for (const prop in taskData) {
if (
Object.prototype.hasOwnProperty.call(taskData, prop) &&
Object.prototype.hasOwnProperty.call(object, prop)
Object.prototype.hasOwnProperty.call(taskData, prop)
&& Object.prototype.hasOwnProperty.call(object, prop)
) {
object[prop] = taskData[prop];
}
@ -108,9 +167,13 @@ class ServerProxy {
id,
url: `http://localhost:7000/api/v1/tasks/${id}`,
name: taskData.name,
project_id: taskData.project_id || null,
size: 5000,
mode: 'interpolation',
owner: 2,
owner: {
id: 2,
username: 'bsekache',
},
assignee: null,
bug_tracker: taskData.bug_tracker,
created_date: '2019-05-16T13:08:00.621747+03:00',
@ -175,8 +238,8 @@ class ServerProxy {
for (const prop in jobData) {
if (
Object.prototype.hasOwnProperty.call(jobData, prop) &&
Object.prototype.hasOwnProperty.call(object, prop)
Object.prototype.hasOwnProperty.call(jobData, prop)
&& Object.prototype.hasOwnProperty.call(object, prop)
) {
object[prop] = jobData[prop];
}
@ -260,6 +323,16 @@ class ServerProxy {
writable: false,
},
projects: {
value: Object.freeze({
get: getProjects,
save: saveProject,
create: createProject,
delete: deleteProject,
}),
writable: false,
},
tasks: {
value: Object.freeze({
getTasks,

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

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

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

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

@ -2,7 +2,9 @@
//
// SPDX-License-Identifier: MIT
import { AnyAction, Dispatch, ActionCreator, Store } from 'redux';
import {
AnyAction, Dispatch, ActionCreator, Store,
} from 'redux';
import { ThunkAction } from 'utils/redux';
import {
@ -244,7 +246,9 @@ export function switchZLayer(cur: number): AnyAction {
export function fetchAnnotationsAsync(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const { filters, frame, showAllInterpolationTracks, jobInstance } = receiveAnnotationsParameters();
const {
filters, frame, showAllInterpolationTracks, jobInstance,
} = receiveAnnotationsParameters();
const states = await jobInstance.annotations.get(frame, showAllInterpolationTracks, filters);
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}`);
}
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 frameData = await job.frames.get(frameNumber);
// 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 {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const { jobInstance, filters, frame, showAllInterpolationTracks } = receiveAnnotationsParameters();
const {
jobInstance, filters, frame, showAllInterpolationTracks,
} = receiveAnnotationsParameters();
try {
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;
}
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 = {
type: TasksActionTypes.GET_TASKS_SUCCESS,
payload: {
@ -93,25 +93,11 @@ export function getTasksAsync(query: TasksQuery): ThunkAction<Promise<void>, {},
}
const array = Array.from(result);
const previews = [];
const promises = array.map((task): string => (task as any).frames.preview());
const promises = array.map((task): string => (task as any).frames.preview().catch(''));
dispatch(getInferenceStatusAsync());
for (const promise of promises) {
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));
dispatch(getTasksSuccess(array, await Promise.all(promises), result.count, query));
};
}
@ -381,6 +367,9 @@ export function createTaskAsync(data: any): ThunkAction<Promise<void>, {}, {}, A
use_cache: data.advanced.useCache,
};
if (data.projectId) {
description.project_id = data.projectId;
}
if (data.advanced.bugTracker) {
description.bug_tracker = data.advanced.bugTracker;
}
@ -448,7 +437,7 @@ function updateTask(): AnyAction {
return action;
}
function updateTaskSuccess(task: any): AnyAction {
export function updateTaskSuccess(task: any): AnyAction {
const action = {
type: TasksActionTypes.UPDATE_TASK_SUCCESS,
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 {
annotation: { appearanceCollapsed },
settings: {
shapes: { colorBy, opacity, selectedOpacity, outlined, outlineColor, showBitmap, showProjections },
shapes: {
colorBy, opacity, selectedOpacity, outlined, outlineColor, showBitmap, showProjections,
},
},
} = state;
@ -153,16 +155,33 @@ function AppearanceBlock(props: Props): JSX.Element {
<Collapse.Panel header={<Text strong>Appearance</Text>} key='appearance'>
<div className='cvat-objects-appearance-content'>
<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.INSTANCE}>{ColorBy.INSTANCE}</Radio.Button>
<Radio.Button value={ColorBy.GROUP}>{ColorBy.GROUP}</Radio.Button>
</Radio.Group>
<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>
<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
className='cvat-appearance-outlinded-borders-checkbox'
onChange={(event: CheckboxChangeEvent) => {
changeShapesOutlinedBorders(event.target.checked, outlineColor);
}}
@ -175,15 +194,23 @@ function AppearanceBlock(props: Props): JSX.Element {
placement='top'
resetVisible={false}
>
<Button type='link' shape='circle'>
<Button className='cvat-appearance-outlined-borders-button' type='link' shape='circle'>
<ColorizeIcon />
</Button>
</ColorPicker>
</Checkbox>
<Checkbox onChange={changeShowBitmap} checked={showBitmap}>
<Checkbox
className='cvat-appearance-bitmap-checkbox'
onChange={changeShowBitmap}
checked={showBitmap}
>
Show bitmap
</Checkbox>
<Checkbox onChange={changeShowProjections} checked={showProjections}>
<Checkbox
className='cvat-appearance-cuboid-projections-checkbox'
onChange={changeShowProjections}
checked={showProjections}
>
Show projections
</Checkbox>
</div>

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

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

@ -33,6 +33,17 @@ function LabelItemComponent(props: Props): JSX.Element {
unlockStates,
} = 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 (
<Row
type='flex'
@ -51,16 +62,16 @@ function LabelItemComponent(props: Props): JSX.Element {
</Col>
<Col span={3}>
{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 span={3}>
{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>
</Row>

@ -23,7 +23,11 @@ function StatesOrderingSelectorComponent(props: StatesOrderingSelectorComponentP
return (
<Col span={16}>
<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}>
{StatesOrdering.ID_DESCENT}
</Select.Option>

@ -109,7 +109,7 @@ function PlayerButtons(props: Props): JSX.Element {
<Popover
trigger='contextMenu'
placement='bottom'
content={
content={(
<>
<Tooltip title={`${prevRegularText}`} mouseLeaveDelay={0}>
<Icon
@ -139,7 +139,7 @@ function PlayerButtons(props: Props): JSX.Element {
/>
</Tooltip>
</>
}
)}
>
<Tooltip
placement='top'
@ -163,7 +163,7 @@ function PlayerButtons(props: Props): JSX.Element {
<Popover
trigger='contextMenu'
placement='bottom'
content={
content={(
<>
<Tooltip title={`${nextRegularText}`} mouseLeaveDelay={0}>
<Icon
@ -193,7 +193,7 @@ function PlayerButtons(props: Props): JSX.Element {
/>
</Tooltip>
</>
}
)}
>
<Tooltip placement='top' mouseLeaveDelay={0} title={`${nextButtonTooltipMessage} ${nextFrameShortcut}`}>
{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 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 ProjectSearchField from './project-search-field';
import AdvancedConfigurationForm, { AdvancedConfiguration } from './advanced-configuration-form';
import LabelsEditor from '../labels-editor/labels-editor';
import { Files } from '../file-manager/file-manager';
export interface CreateTaskData {
projectId: number | null;
basic: BaseConfiguration;
advanced: AdvancedConfiguration;
labels: any[];
@ -30,12 +32,14 @@ interface Props {
onCreate: (data: CreateTaskData) => void;
status: string;
taskId: number | null;
projectId: number | null;
installedGit: boolean;
}
type State = CreateTaskData;
const defaultState = {
projectId: null,
basic: {
name: '',
},
@ -65,6 +69,14 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
this.state = { ...defaultState };
}
public componentDidMount(): void {
const { projectId } = this.props;
if (projectId) {
this.handleProjectIdChange(projectId);
}
}
public componentDidUpdate(prevProps: Props): void {
const { status, history, taskId } = this.props;
@ -89,9 +101,9 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
}
}
private validateLabels = (): boolean => {
const { labels } = this.state;
return !!labels.length;
private validateLabelsOrProject = (): boolean => {
const { projectId, labels } = this.state;
return !!labels.length || !!projectId;
};
private validateFiles = (): boolean => {
@ -104,6 +116,12 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
return !!totalLen;
};
private handleProjectIdChange = (value: null | number): void => {
this.setState({
projectId: value,
});
};
private handleSubmitBasicConfiguration = (values: BaseConfiguration): void => {
this.setState({
basic: { ...values },
@ -125,10 +143,10 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
};
private handleSubmitClick = (): void => {
if (!this.validateLabels()) {
if (!this.validateLabelsOrProject()) {
notification.error({
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;
}
@ -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 {
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 (
<Col span={24}>
@ -243,12 +289,13 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
</Col>
{this.renderBasicBlock()}
{this.renderProjectBlock()}
{this.renderLabelsBlock()}
{this.renderFilesBlock()}
{this.renderAdvancedBlock()}
<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}>
Submit
</Button>

@ -4,6 +4,7 @@
import './styles.scss';
import React, { useEffect } from 'react';
import { useLocation } from 'react-router';
import { Row, Col } from 'antd/lib/grid';
import Modal from 'antd/lib/modal';
import Text from 'antd/lib/typography/Text';
@ -21,7 +22,17 @@ interface Props {
}
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(() => {
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'>
<Col md={20} lg={16} xl={14} xxl={9}>
<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>
</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;
}
> div:nth-child(7) > button {
.cvat-create-task-submit-section > button {
float: right;
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 ResetPasswordPageComponent from 'components/reset-password-page/reset-password-page';
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 AnnotationPageContainer from 'containers/annotation-page/annotation-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 AnnotationPageContainer from 'containers/annotation-page/annotation-page';
import LoginPageContainer from 'containers/login-page/login-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 React from 'react';
import { configure, ExtendedKeyMapOptions, GlobalHotKeys } from 'react-hotkeys';
@ -34,7 +37,6 @@ import '../styles.scss';
interface CVATAppProps {
loadFormats: () => void;
loadUsers: () => void;
loadAbout: () => void;
verifyAuthorized: () => void;
loadUserAgreements: () => void;
@ -54,8 +56,6 @@ interface CVATAppProps {
modelsFetching: boolean;
formatsInitialized: boolean;
formatsFetching: boolean;
usersInitialized: boolean;
usersFetching: boolean;
aboutInitialized: boolean;
aboutFetching: boolean;
userAgreementsFetching: boolean;
@ -92,7 +92,6 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
const {
verifyAuthorized,
loadFormats,
loadUsers,
loadAbout,
loadUserAgreements,
initPlugins,
@ -102,8 +101,6 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
userFetching,
formatsInitialized,
formatsFetching,
usersInitialized,
usersFetching,
aboutInitialized,
aboutFetching,
pluginsInitialized,
@ -143,10 +140,6 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
loadFormats();
}
if (!usersInitialized && !usersFetching) {
loadUsers();
}
if (!aboutInitialized && !aboutFetching) {
loadAbout();
}
@ -235,7 +228,6 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
public render(): JSX.Element {
const {
userInitialized,
usersInitialized,
aboutInitialized,
pluginsInitialized,
formatsInitialized,
@ -248,7 +240,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
const readyForRender =
(userInitialized && (user == null || !user.isVerified)) ||
(userInitialized && formatsInitialized && pluginsInitialized && usersInitialized && aboutInitialized);
(userInitialized && formatsInitialized && pluginsInitialized && aboutInitialized);
const subKeyMap = {
SWITCH_SHORTCUTS: keyMap.SWITCH_SHORTCUTS,
@ -270,7 +262,10 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
if (showPlatformNotification()) {
stopNotifications(false);
const info = platformInfo();
const {
name, version, engine, os,
} = platformInfo();
Modal.warning({
title: 'Unsupported platform detected',
content: (
@ -278,7 +273,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
<Row>
<Col>
<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.' +
' We recommend to use Chrome (or another Chromium based browser)'}
</Text>
@ -286,7 +281,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
</Row>
<Row>
<Col>
<Text type='secondary'>{`The operating system is ${info.os}`}</Text>
<Text type='secondary'>{`The operating system is ${os}`}</Text>
</Col>
</Row>
</>
@ -305,6 +300,9 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
<ShorcutsDialog />
<GlobalHotKeys keyMap={subKeyMap} handlers={handlers}>
<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/create' component={CreateTaskPageContainer} />
<Route exact path='/tasks/:id' component={TaskPageContainer} />

@ -137,7 +137,9 @@ function HeaderContainer(props: Props): JSX.Element {
isModelsPluginActive,
} = 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();
@ -238,6 +240,18 @@ function HeaderContainer(props: Props): JSX.Element {
<div className='cvat-left-header'>
<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
className='cvat-header-button'
type='link'
@ -288,8 +302,6 @@ function HeaderContainer(props: Props): JSX.Element {
href={GITHUB_URL}
onClick={(event: React.MouseEvent): void => {
event.preventDefault();
// false positive
// eslint-disable-next-line security/detect-non-literal-fs-filename
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'>
<Col>
<Popover
content={
content={(
<CompactPicker
colors={consts.CANVAS_BACKGROUND_COLORS}
color={canvasBackgroundColor}
onChange={(e) => onChangeCanvasBackgroundColor(e.hex)}
/>
}
)}
overlayClassName='canvas-background-color-picker-popover'
trigger='click'
>
@ -169,6 +169,7 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
<Col span={8} className='cvat-player-settings-grid-size'>
<Text className='cvat-text-color'> Grid size </Text>
<InputNumber
className='cvat-player-settings-grid-size-input'
min={minGridSize}
max={maxGridSize}
value={gridSize}
@ -183,6 +184,7 @@ export default function PlayerSettingsComponent(props: Props): JSX.Element {
<Col span={8} className='cvat-player-settings-grid-color'>
<Text className='cvat-text-color'> Grid color </Text>
<Select
className='cvat-player-settings-grid-color-input'
value={gridColor}
disabled={!grid}
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'>
<Text className='cvat-text-color'> Grid opacity </Text>
<Slider
className='cvat-player-settings-grid-opacity-input'
min={0}
max={100}
value={gridOpacity}

@ -18,7 +18,9 @@ import ColorPicker from 'components/annotation-page/standard-workspace/objects-s
import { ColorizeIcon } from 'icons';
import patterns from 'utils/validation-patterns';
import consts from 'consts';
import { equalArrayHead, idGenerator, Label, Attribute } from './common';
import {
equalArrayHead, idGenerator, Label, Attribute,
} from './common';
export enum AttributeType {
SELECT = 'SELECT',
@ -122,7 +124,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
message: patterns.validateAttributeName.message,
},
],
})(<Input disabled={locked} placeholder='Name' />)}
})(<Input className='cvat-attribute-name-input' disabled={locked} placeholder='Name' />)}
</Form.Item>
</Col>
);
@ -140,7 +142,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
{form.getFieldDecorator(`type[${key}]`, {
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.RADIO}>Radio</Select.Option>
<Select.Option value={AttributeType.CHECKBOX}>Checkbox</Select.Option>
@ -189,7 +191,14 @@ class LabelForm extends React.PureComponent<Props, {}> {
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>
</Tooltip>
);
@ -205,7 +214,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
{form.getFieldDecorator(`values[${key}]`, {
initialValue: value,
})(
<Select>
<Select className='cvat-attribute-values-input'>
<Select.Option value='false'> False </Select.Option>
<Select.Option value='true'> True </Select.Option>
</Select>,
@ -262,7 +271,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
validator,
},
],
})(<Input disabled={locked} placeholder='min;max;step' />)}
})(<Input className='cvat-attribute-values-input' disabled={locked} placeholder='min;max;step' />)}
</Form.Item>
);
}
@ -275,7 +284,7 @@ class LabelForm extends React.PureComponent<Props, {}> {
<Form.Item>
{form.getFieldDecorator(`values[${key}]`, {
initialValue: value,
})(<Input placeholder='Default value' />)}
})(<Input className='cvat-attribute-values-input' placeholder='Default value' />)}
</Form.Item>
);
}
@ -291,7 +300,13 @@ class LabelForm extends React.PureComponent<Props, {}> {
{form.getFieldDecorator(`mutable[${key}]`, {
initialValue: value,
valuePropName: 'checked',
})(<Checkbox disabled={locked}> Mutable </Checkbox>)}
})(
<Checkbox className='cvat-attribute-mutable-checkbox' disabled={locked}>
{' '}
Mutable
{' '}
</Checkbox>,
)}
</Tooltip>
</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 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 (
<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.renderAttributeTypeInput(key, attr)}
<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 = {
fetching: boolean;
onSubmit(resetPasswordConfirmData: ResetPasswordConfirmData): void;
} & FormComponentProps &
RouteComponentProps;
} & FormComponentProps & RouteComponentProps;
class ResetPasswordConfirmFormComponent extends React.PureComponent<ResetPasswordConfirmFormProps> {
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 Icon from 'antd/lib/icon';
import Modal from 'antd/lib/modal';
import Button from 'antd/lib/button';
import notification from 'antd/lib/notification';
import Text from 'antd/lib/typography/Text';
import Title from 'antd/lib/typography/Title';
import moment from 'moment';
import getCore from 'cvat-core-wrapper';
import patterns from 'utils/validation-patterns';
import { getReposData, syncRepos } from 'utils/git-utils';
import { ActiveInference } from 'reducers/interfaces';
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';
const core = getCore();
@ -27,7 +26,6 @@ interface Props {
previewImage: string;
taskInstance: any;
installedGit: boolean; // change to git repos url
registeredUsers: any[];
activeInference: ActiveInference | null;
cancelAutoAnnotation(): void;
onTaskUpdate: (taskInstance: any) => void;
@ -35,8 +33,6 @@ interface Props {
interface State {
name: string;
bugTracker: string;
bugTrackerEditing: boolean;
repository: string;
repositoryStatus: string;
}
@ -58,8 +54,6 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
this.previewWrapperRef = React.createRef<HTMLDivElement>();
this.state = {
name: taskInstance.name,
bugTracker: taskInstance.bugTracker,
bugTrackerEditing: false,
repository: '',
repositoryStatus: '',
};
@ -120,7 +114,6 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
if (prevProps !== this.props) {
this.setState({
name: taskInstance.name,
bugTracker: taskInstance.bugTracker,
});
}
}
@ -195,36 +188,31 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
);
}
private renderUsers(): JSX.Element {
const { taskInstance, registeredUsers, onTaskUpdate } = this.props;
private renderDescription(): JSX.Element {
const { taskInstance, onTaskUpdate } = this.props;
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 assigneeSelect = (
<UserSelector
users={registeredUsers}
value={assignee}
onChange={(value: string): void => {
let [userInstance] = registeredUsers.filter((user: any) => user.username === value);
if (userInstance === undefined) {
userInstance = null;
}
taskInstance.assignee = userInstance;
onSelect={(value: User | null): void => {
taskInstance.assignee = value;
onTaskUpdate(taskInstance);
}}
/>
);
return (
<Row type='flex' justify='space-between' align='middle'>
<Col span={12}>{owner && <Text type='secondary'>{`Created by ${owner} on ${created}`}</Text>}</Col>
<Row className='cvat-task-details-user-block' type='flex' justify='space-between' align='middle'>
<Col span={12}>
{owner && (
<Text type='secondary'>{`Task #${taskInstance.id} Created by ${owner} on ${created}`}</Text>
)}
</Col>
<Col span={10}>
<Text type='secondary'>
Assigned to
{assigneeSelect}
</Text>
<Text type='secondary'>Assigned to</Text>
{assigneeSelect}
</Col>
</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 {
const { taskInstance, onTaskUpdate } = this.props;
@ -403,7 +311,10 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
}
public render(): JSX.Element {
const { activeInference, cancelAutoAnnotation } = this.props;
const {
activeInference, cancelAutoAnnotation, taskInstance, onTaskUpdate,
} = this.props;
return (
<div className='cvat-task-details'>
<Row type='flex' justify='start' align='middle'>
@ -419,9 +330,17 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
</Row>
</Col>
<Col md={16} lg={17} xl={17} xxl={18}>
{this.renderUsers()}
{this.renderDescription()}
<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}>
<AutomaticAnnotationProgress
activeInference={activeInference}
@ -430,7 +349,7 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
</Col>
</Row>
{this.renderDatasetRepository()}
{this.renderLabelsEditor()}
{!taskInstance.projectId && this.renderLabelsEditor()}
</Col>
</Row>
</div>

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

@ -17,6 +17,12 @@
padding: 20px;
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) {
padding-left: 20px;
@ -72,8 +78,7 @@
}
.cvat-task-top-bar {
margin-top: 20px;
margin-bottom: 10px;
margin: $grid-unit-size * 2 0;
}
.cvat-task-preview-wrapper {
@ -87,11 +92,6 @@
background-color: $background-color-2;
}
.cvat-user-selector {
margin-left: 10px;
width: 150px;
}
.cvat-open-bug-tracker-button {
margin-left: 15px;
}

@ -3,6 +3,7 @@
// 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';
@ -18,12 +19,27 @@ interface DetailsComponentProps {
export default function DetailsComponent(props: DetailsComponentProps): JSX.Element {
const { taskInstance } = props;
const { id } = taskInstance;
const history = useHistory();
return (
<Row className='cvat-task-top-bar' type='flex' justify='space-between' align='middle'>
<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>
<Dropdown overlay={<ActionsMenuContainer taskInstance={taskInstance} />}>

@ -2,30 +2,108 @@
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Select from 'antd/lib/select';
import React, { useState, useEffect, useRef } from 'react';
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 {
value: string | null;
users: any[];
onChange: (user: string) => void;
value: User | null;
onSelect: (user: User | null) => 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 {
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 (
<Select defaultValue={value || '—'} size='small' showSearch className='cvat-user-selector' onChange={onChange}>
<Select.Option key='-1' value='—'>
</Select.Option>
{users.map(
(user): JSX.Element => (
<Select.Option key={user.id} value={user.username}>
{user.username}
</Select.Option>
),
)}
</Select>
<Autocomplete
ref={autocompleteRef}
value={searchPhrase}
placeholder='Select a user'
onSearch={handleSearch}
onSelect={handleSelect}
className='cvat-user-search-field'
onDropdownVisibleChange={handleFocus}
dataSource={users.map((user) => ({
value: user.id.toString(),
text: user.username,
}))}
>
<Input onPressEnter={() => autocompleteRef.current?.blur()} />
</Autocomplete>
);
}

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

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

@ -3,8 +3,7 @@
// SPDX-License-Identifier: MIT
import React from 'react';
import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom';
import { useHistory } from 'react-router';
import { Row, Col } from 'antd/lib/grid';
import Button from 'antd/lib/button';
import Input from 'antd/lib/input';
@ -15,16 +14,13 @@ interface VisibleTopBarProps {
searchValue: string;
}
function TopBarComponent(props: VisibleTopBarProps & RouteComponentProps): JSX.Element {
const { searchValue, history, onSearch } = props;
export default function TopBarComponent(props: VisibleTopBarProps): JSX.Element {
const { searchValue, onSearch } = props;
const history = useHistory();
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'>
<Col md={11} lg={9} xl={8} xxl={7}>
<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: {
annotations: { activatedStateID, collapsed, states: objectStates },
canvas: {
contextMenu: { visible, top, left, type },
contextMenu: {
visible, top, left, type,
},
ready,
},
},
} = state;
@ -33,7 +36,11 @@ function mapStateToProps(state: CombinedState): StateToProps {
activatedStateID,
collapsed: activatedStateID !== null ? collapsed[activatedStateID] : undefined,
objectStates,
visible,
visible:
activatedStateID !== null &&
visible &&
ready &&
objectStates.map((_state: any): number => _state.clientID).includes(activatedStateID),
left,
top,
type,
@ -166,7 +173,9 @@ class CanvasContextMenuContainer extends React.PureComponent<Props, State> {
public render(): JSX.Element {
const { left, top } = this.state;
const { visible, activatedStateID, objectStates, type } = this.props;
const {
visible, activatedStateID, objectStates, type,
} = this.props;
return (
<>

@ -8,7 +8,7 @@ import { connect } from 'react-redux';
import { updateAnnotationsAsync } from 'actions/annotation-actions';
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 {
labelID: number;
@ -92,8 +92,8 @@ class LabelItemContainer extends React.PureComponent<Props, State> {
let statesLocked = true;
ownObjectStates.forEach((objectState: any) => {
const { lock } = objectState;
if (!lock) {
const { lock, objectType } = objectState;
if (!lock && objectType !== ObjectType.TAG) {
statesHidden = statesHidden && objectState.hidden;
statesLocked = statesLocked && objectState.lock;
}

@ -7,7 +7,9 @@ import copy from 'copy-to-clipboard';
import { connect } from 'react-redux';
import { LogType } from 'cvat-logger';
import { ActiveControl, CombinedState, ColorBy, ShapeType } from 'reducers/interfaces';
import {
ActiveControl, CombinedState, ColorBy, ShapeType,
} from 'reducers/interfaces';
import {
collapseObjectItems,
updateAnnotationsAsync,
@ -83,40 +85,25 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const stateIDs = states.map((_state: any): number => _state.clientID);
const index = stateIDs.indexOf(clientID);
try {
const collapsedState =
typeof statesCollapsed[clientID] === 'undefined' ? initialCollapsed : statesCollapsed[clientID];
return {
objectState: states[index],
collapsed: collapsedState,
attributes: jobAttributes[states[index].label.id],
labels,
ready,
activeControl,
colorBy,
jobInstance,
frameNumber,
activated: activatedStateID === clientID,
minZLayer,
maxZLayer,
normalizedKeyMap,
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)}`,
);
}
const collapsedState =
typeof statesCollapsed[clientID] === 'undefined' ? initialCollapsed : statesCollapsed[clientID];
return {
objectState: states[index],
collapsed: collapsedState,
attributes: jobAttributes[states[index].label.id],
labels,
ready,
activeControl,
colorBy,
jobInstance,
frameNumber,
activated: activatedStateID === clientID,
minZLayer,
maxZLayer,
normalizedKeyMap,
aiToolsRef,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
@ -219,7 +206,9 @@ class ObjectItemContainer extends React.PureComponent<Props> {
};
private activate = (): void => {
const { activateObject, objectState, ready, activeControl } = this.props;
const {
activateObject, objectState, ready, activeControl,
} = this.props;
if (ready && activeControl === ActiveControl.CURSOR) {
activateObject(objectState.clientID);
@ -324,7 +313,9 @@ class ObjectItemContainer extends React.PureComponent<Props> {
}
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 = '';
if (colorBy === ColorBy.INSTANCE) {

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

@ -15,7 +15,6 @@ interface OwnProps {
}
interface StateToProps {
registeredUsers: any[];
activeInference: ActiveInference | null;
installedGit: boolean;
}
@ -29,7 +28,6 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const { list } = state.plugins;
return {
registeredUsers: state.users.users,
installedGit: list.GIT_INTEGRATION,
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 {
const { task, installedGit, activeInference, registeredUsers, cancelAutoAnnotation, onTaskUpdate } = props;
const {
task, installedGit, activeInference, cancelAutoAnnotation, onTaskUpdate,
} = props;
return (
<DetailsComponent
previewImage={task.preview}
taskInstance={task.instance}
installedGit={installedGit}
registeredUsers={registeredUsers}
activeInference={activeInference}
onTaskUpdate={onTaskUpdate}
cancelAutoAnnotation={cancelAutoAnnotation}

@ -7,38 +7,26 @@ import { connect } from 'react-redux';
import JobListComponent from 'components/task-page/job-list';
import { updateJobAsync } from 'actions/tasks-actions';
import { Task, CombinedState } from 'reducers/interfaces';
import { Task } from 'reducers/interfaces';
interface OwnProps {
task: Task;
}
interface StateToProps {
registeredUsers: any[];
}
interface DispatchToProps {
onJobUpdate(jobInstance: any): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
return {
registeredUsers: state.users.users,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onJobUpdate: (jobInstance: any): void => dispatch(updateJobAsync(jobInstance)),
};
}
function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps): JSX.Element {
const { task, registeredUsers, onJobUpdate } = props;
function TaskPageContainer(props: DispatchToProps & OwnProps): JSX.Element {
const { task, onJobUpdate } = props;
return (
<JobListComponent taskInstance={task.instance} registeredUsers={registeredUsers} onJobUpdate={onJobUpdate} />
);
return <JobListComponent taskInstance={task.instance} 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 { shortcutsActions } from 'actions/shortcuts-actions';
import { getUserAgreementsAsync } from 'actions/useragreements-actions';
import { getUsersAsync } from 'actions/users-actions';
import CVATApplication from 'components/cvat-app';
import LayoutGrid from 'components/layout-grid/layout-grid';
import logger, { LogType } from 'cvat-logger';
@ -34,8 +33,6 @@ interface StateToProps {
modelsFetching: boolean;
userInitialized: boolean;
userFetching: boolean;
usersInitialized: boolean;
usersFetching: boolean;
aboutInitialized: boolean;
aboutFetching: boolean;
formatsInitialized: boolean;
@ -55,7 +52,6 @@ interface StateToProps {
interface DispatchToProps {
loadFormats: () => void;
verifyAuthorized: () => void;
loadUsers: () => void;
loadAbout: () => void;
initModels: () => void;
initPlugins: () => void;
@ -71,7 +67,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { plugins } = state;
const { auth } = state;
const { formats } = state;
const { users } = state;
const { about } = state;
const { shortcuts } = state;
const { userAgreements } = state;
@ -84,8 +79,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
pluginsFetching: plugins.fetching,
modelsInitialized: models.initialized,
modelsFetching: models.fetching,
usersInitialized: users.initialized,
usersFetching: users.fetching,
aboutInitialized: about.initialized,
aboutFetching: about.fetching,
formatsInitialized: formats.initialized,
@ -110,7 +103,6 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
loadUserAgreements: (): void => dispatch(getUserAgreementsAsync()),
initPlugins: (): void => dispatch(getPluginsAsync()),
initModels: (): void => dispatch(getModelsAsync()),
loadUsers: (): void => dispatch(getUsersAsync()),
loadAbout: (): void => dispatch(getAboutAsync()),
resetErrors: (): void => dispatch(resetErrors()),
resetMessages: (): void => dispatch(resetMessages()),

@ -21,6 +21,35 @@ export interface AuthState {
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 {
page: number;
id: number | null;
@ -93,12 +122,6 @@ export interface PluginsState {
list: PluginsList;
}
export interface UsersState {
users: any[];
fetching: boolean;
initialized: boolean;
}
export interface AboutState {
server: any;
packageVersion: {
@ -198,6 +221,12 @@ export interface NotificationsState {
resetPassword: null | ErrorState;
loadAuthActions: null | ErrorState;
};
projects: {
fetching: null | ErrorState;
updating: null | ErrorState;
deleting: null | ErrorState;
creating: null | ErrorState;
};
tasks: {
fetching: null | ErrorState;
updating: null | ErrorState;
@ -493,8 +522,8 @@ export interface MetaState {
export interface CombinedState {
auth: AuthState;
projects: ProjectsState;
tasks: TasksState;
users: UsersState;
about: AboutState;
share: ShareState;
formats: FormatsState;

@ -9,7 +9,7 @@ import { FormatsActionTypes } from 'actions/formats-actions';
import { ModelsActionTypes } from 'actions/models-actions';
import { ShareActionTypes } from 'actions/share-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 { AnnotationActionTypes } from 'actions/annotation-actions';
import { NotificationsActionType } from 'actions/notification-actions';
@ -30,6 +30,12 @@ const defaultState: NotificationsState = {
resetPassword: null,
loadAuthActions: null,
},
projects: {
fetching: null,
updating: null,
deleting: null,
creating: null,
},
tasks: {
fetching: null,
updating: null,
@ -357,8 +363,7 @@ export default function (state = defaultState, action: AnyAction): Notifications
tasks: {
...state.errors.tasks,
updating: {
message:
'Could not update ' + `<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
message: `Could not update <a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
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 {
...state,
errors: {
...state.errors,
formats: {
...state.errors.formats,
projects: {
...state.errors.projects,
fetching: {
message: 'Could not get formats from the server',
message: 'Could not fetch projects',
reason: action.payload.error.toString(),
},
},
},
};
}
case UsersActionTypes.GET_USERS_FAILED: {
case ProjectsActionTypes.CREATE_PROJECT_FAILED: {
return {
...state,
errors: {
...state.errors,
users: {
...state.errors.users,
projects: {
...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: {
message: 'Could not get users from the server',
message: 'Could not get formats from the server',
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 authReducer from './auth-reducer';
import projectsReducer from './projects-reducer';
import tasksReducer from './tasks-reducer';
import usersReducer from './users-reducer';
import aboutReducer from './about-reducer';
import shareReducer from './share-reducer';
import formatsReducer from './formats-reducer';
@ -20,8 +20,8 @@ import userAgreementsReducer from './useragreements-reducer';
export default function createRootReducer(): Reducer {
return combineReducers({
auth: authReducer,
projects: projectsReducer,
tasks: tasksReducer,
users: usersReducer,
about: aboutReducer,
share: shareReducer,
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
VERSION = (1, 2, 0, 'alpha', 0)
VERSION = (1, 2, 0, 'beta', 0)
__version__ = get_version(VERSION)

@ -1,5 +1,6 @@
{% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %}{% blocktrans with
site_name=current_site.name site_domain=current_site.domain %}Hello from {{ site_name }}!
{% load account %}{% user_display user as user_display %}{% load i18n %}{% autoescape off %}
{% blocktrans with site_name=current_site.name site_domain=current_site.domain %}
Hello from {{ site_name }}!
<p>
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>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>
{% endblocktrans %} {% endautoescape %}

@ -1,5 +1,16 @@
{% load i18n %}{% autoescape off %} {% blocktrans %}You're receiving this email because you requested a password reset
for your user account at {{ site_name }}.{% endblocktrans %} {% 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 %}
{% load i18n %}{% autoescape off %}
{% blocktrans %}
You're receiving this email because you requested a password reset for your user account at {{ site_name }}.
{% endblocktrans %}
{% 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_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')
self._label_mapping = OrderedDict(

@ -93,7 +93,8 @@ class JobAnnotation:
self.ir_data = AnnotationIR()
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 = {}
for db_label in self.db_labels.values():

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

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

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

@ -4,7 +4,7 @@
# SPDX-License-Identifier: MIT
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):
model = Job
@ -54,6 +54,20 @@ class SegmentAdmin(admin.ModelAdmin):
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):
date_hierarchy = 'updated_date'
readonly_fields = ('created_date', 'updated_date', 'overlap')
@ -74,3 +88,4 @@ class TaskAdmin(admin.ModelAdmin):
admin.site.register(Task, TaskAdmin)
admin.site.register(Segment, SegmentAdmin)
admin.site.register(Label, LabelAdmin)
admin.site.register(Project, ProjectAdmin)

@ -2,10 +2,15 @@
#
# SPDX-License-Identifier: MIT
import os
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):
try:
@ -19,6 +24,28 @@ def _get_job(jid):
except Exception:
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:
def __init__(self):
self._storage = dict()
@ -52,6 +79,24 @@ class JobLoggerStorage:
job = _get_job(jid)
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:
def __init__(self):
self._storage = dict()
@ -89,12 +134,14 @@ class dotdict(dict):
__delattr__ = dict.__delitem__
clogger = dotdict({
'project': ProjectClientLoggerStorage(),
'task': TaskClientLoggerStorage(),
'job': JobClientLoggerStorage(),
'glob': logging.getLogger('cvat.client'),
})
slogger = dotdict({
'project': ProjectLoggerStorage(),
'task': TaskLoggerStorage(),
'job': JobLoggerStorage(),
'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(),
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
class Meta:
default_permissions = ()
def __str__(self):
return self.name
class Task(models.Model):
project = models.ForeignKey(Project, on_delete=models.CASCADE,
null=True, blank=True, related_name="tasks",
@ -268,7 +283,8 @@ class Job(models.Model):
default_permissions = ()
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)
color = models.CharField(default='', max_length=8)
@ -324,7 +340,7 @@ class ShapeType(str, Enum):
POLYGON = 'polygon' # (x0, y0, ..., xn, yn)
POLYLINE = 'polyline' # (x0, y0, ..., xn, yn)
POINTS = 'points' # (x0, y0, ..., xn, yn)
CUBOID = 'cuboid'
CUBOID = 'cuboid' # (x0, y0, ..., x7, y7)
@classmethod
def choices(cls):

@ -13,6 +13,36 @@ from cvat.apps.engine import models
from cvat.apps.engine.log import slogger
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 Meta:
@ -44,6 +74,47 @@ class LabelSerializer(serializers.ModelSerializer):
model = models.Label
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 Meta:
model = models.JobCommit
@ -53,16 +124,21 @@ class JobSerializer(serializers.ModelSerializer):
task_id = serializers.ReadOnlyField(source="segment.task.id")
start_frame = serializers.ReadOnlyField(source="segment.start_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:
model = models.Job
fields = ('url', 'id', 'assignee', 'status', 'start_frame',
fields = ('url', 'id', 'assignee', 'assignee_id', 'status', 'start_frame',
'stop_frame', 'task_id')
class SimpleJobSerializer(serializers.ModelSerializer):
assignee = BasicUserSerializer(allow_null=True)
assignee_id = serializers.IntegerField(write_only=True, allow_null=True)
class Meta:
model = models.Job
fields = ('url', 'id', 'assignee', 'status')
fields = ('url', 'id', 'assignee', 'assignee_id', 'status')
class SegmentSerializer(serializers.ModelSerializer):
jobs = SimpleJobSerializer(many=True, source='job_set')
@ -120,6 +196,7 @@ class RqStatusSerializer(serializers.Serializer):
message = serializers.CharField(allow_blank=True, default="")
class WriteOnceMixin:
"""Adds support for write once fields to serializers.
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
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)
data_chunk_size = serializers.ReadOnlyField(source='data.chunk_size')
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')
image_quality = serializers.ReadOnlyField(source='data.image_quality')
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:
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',
'segment_size', 'status', 'labels', 'segments',
'project', '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',
'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', 'owner', 'asignee',
'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']
# pylint: disable=no-self-use
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)
label_names = list()
for label in labels:
@ -277,100 +364,96 @@ class TaskSerializer(WriteOnceMixin, serializers.ModelSerializer):
db_task.save()
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
def update(self, instance, validated_data):
instance.name = validated_data.get('name', instance.name)
instance.owner = validated_data.get('owner', instance.owner)
instance.assignee = validated_data.get('assignee', instance.assignee)
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)
instance.project = validated_data.get('project', instance.project)
labels = validated_data.get('label_set', [])
for label in labels:
attributes = label.pop('attributespec_set', [])
(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()
LabelSerializer.update_instance(label, instance)
instance.save()
return instance
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]
if len(label_names) != len(set(label_names)):
raise serializers.ValidationError('All label names must be unique for the task')
return value
class ProjectSerializer(serializers.ModelSerializer):
class ProjectSearchSerializer(serializers.ModelSerializer):
class Meta:
model = models.Project
fields = ('url', 'id', 'name', 'owner', 'assignee', 'bug_tracker',
'created_date', 'updated_date', 'status')
read_only_fields = ('created_date', 'updated_date', 'status')
fields = ('id', 'name')
read_only_fields = ('name',)
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:
model = User
fields = ('url', 'id', 'username', 'first_name', 'last_name')
model = models.Project
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']
class UserSerializer(serializers.ModelSerializer):
groups = serializers.SlugRelatedField(many=True,
slug_field='name', queryset=Group.objects.all())
# pylint: disable=no-self-use
def create(self, validated_data):
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:
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']
project_path = db_project.get_project_dirname()
if os.path.isdir(project_path):
shutil.rmtree(project_path)
os.makedirs(db_project.get_project_logs_dirname())
db_project.save()
return db_project
# 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):
system = serializers.CharField(max_length=255)

@ -358,7 +358,7 @@ def _create_thread(tid, data):
img_sizes = []
with open(db_data.get_dummy_chunk_path(chunk_number), 'w') as dummy_chunk:
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))
db_images.extend([

@ -29,7 +29,7 @@ from rest_framework import status
from rest_framework.test import APIClient, APITestCase
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
def create_db_users(cls):
@ -297,47 +297,47 @@ class JobUpdateAPITestCase(APITestCase):
self.assertEqual(response.data["id"], self.job.id)
self.assertEqual(response.data["status"], data.get('status', self.job.status))
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["stop_frame"], self.job.segment.stop_frame)
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)
self._check_request(response, 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)
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)
self._check_request(response, 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)
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)
self._check_request(response, 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)
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)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
response = self._run_api_v1_jobs_id(self.job.id + 10, self.observer, data)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
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)
self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
response = self._run_api_v1_jobs_id(self.job.id + 10, self.user, data)
self.assertEqual(response.status_code, status.HTTP_404_NOT_FOUND)
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)
self.assertEqual(response.status_code, status.HTTP_401_UNAUTHORIZED)
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)
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)
self._check_request(response, data)
@ -754,10 +754,13 @@ class ProjectGetAPITestCase(APITestCase):
self.assertEqual(response.status_code, status.HTTP_200_OK)
self.assertEqual(response.data["name"], db_project.name)
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
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["bug_tracker"], db_project.bug_tracker)
def _check_api_v1_projects_id(self, user):
for db_project in self.projects:
@ -835,10 +838,15 @@ class ProjectCreateAPITestCase(APITestCase):
def _check_response(self, response, user, data):
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["name"], data["name"])
self.assertEqual(response.data["owner"], data.get("owner", user.id))
self.assertEqual(response.data["assignee"], data.get("assignee"))
self.assertEqual(response.data["owner"]["id"], data.get("owner_id", user.id))
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["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):
response = self._run_api_v1_projects(user, data)
@ -857,18 +865,26 @@ class ProjectCreateAPITestCase(APITestCase):
self._check_api_v1_projects(self.admin, data)
data = {
"owner": self.owner.id,
"assignee": self.assignee.id,
"owner_id": self.owner.id,
"assignee_id": self.assignee.id,
"name": "new name for the project"
}
self._check_api_v1_projects(self.admin, data)
data = {
"owner": self.admin.id,
"owner_id": self.admin.id,
"name": "2"
}
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):
data = {
@ -878,8 +894,8 @@ class ProjectCreateAPITestCase(APITestCase):
self._check_api_v1_projects(self.user, data)
data = {
"owner": self.owner.id,
"assignee": self.assignee.id,
"owner_id": self.owner.id,
"assignee_id": self.assignee.id,
"name": "My import project with data"
}
self._check_api_v1_projects(self.user, data)
@ -888,15 +904,15 @@ class ProjectCreateAPITestCase(APITestCase):
def test_api_v1_projects_observer(self):
data = {
"name": "My Project #1",
"owner": self.owner.id,
"assignee": self.assignee.id
"owner_id": self.owner.id,
"assignee_id": self.assignee.id
}
self._check_api_v1_projects(self.observer, data)
def test_api_v1_projects_no_auth(self):
data = {
"name": "My Project #2",
"owner": self.admin.id,
"owner_id": self.admin.id,
}
self._check_api_v1_projects(None, data)
@ -918,15 +934,16 @@ class ProjectPartialUpdateAPITestCase(APITestCase):
def _check_response(self, response, db_project, data):
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)
owner = db_project.owner.id if db_project.owner else None
owner = data.get("owner", owner)
self.assertEqual(response.data["owner"], owner)
assignee = db_project.assignee.id if db_project.assignee else None
assignee = data.get("assignee", assignee)
self.assertEqual(response.data["assignee"], assignee)
self.assertEqual(response.data["status"], db_project.status)
response_owner = response.data["owner"]["id"] if response.data["owner"] else None
db_owner = db_project.owner.id if db_project.owner else None
self.assertEqual(response_owner, data.get("owner_id", db_owner))
response_assignee = response.data["assignee"]["id"] if response.data["assignee"] else None
db_assignee = db_project.assignee.id if db_project.assignee else None
self.assertEqual(response_assignee, data.get("assignee_id", db_assignee))
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):
for db_project in self.projects:
@ -941,14 +958,15 @@ class ProjectPartialUpdateAPITestCase(APITestCase):
def test_api_v1_projects_id_admin(self):
data = {
"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)
def test_api_v1_projects_id_user(self):
data = {
"name": "new name for the project",
"owner": self.assignee.id,
"owner_id": self.assignee.id,
}
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["mode"], db_task.mode)
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
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["segment_size"], db_task.segment_size)
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)
self.assertEqual(response.data["mode"], mode)
owner = db_task.owner.id if db_task.owner else None
owner = data.get("owner", owner)
self.assertEqual(response.data["owner"], owner)
owner = data.get("owner_id", 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 = data.get("assignee", assignee)
self.assertEqual(response.data["assignee"], assignee)
assignee = data.get("assignee_id", 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["segment_size"], db_task.segment_size)
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):
data = {
"name": "new name for the task",
"owner": self.owner.id,
"owner_id": self.owner.id,
"labels": [{
"name": "non-vehicle",
"attributes": [{
@ -1229,7 +1251,7 @@ class TaskUpdateAPITestCase(APITestCase):
def test_api_v1_tasks_id_user(self):
data = {
"name": "new name for the task",
"owner": self.assignee.id,
"owner_id": self.assignee.id,
"labels": [{
"name": "car",
"attributes": [{
@ -1277,7 +1299,7 @@ class TaskPartialUpdateAPITestCase(TaskUpdateAPITestCase):
data = {
"name": "new name for the task",
"owner": self.owner.id
"owner_id": self.owner.id
}
self._check_api_v1_tasks_id(self.admin, data)
# 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)
data = {
"owner": self.observer.id,
"assignee": self.annotator.id
"owner_id": self.observer.id,
"assignee_id": self.annotator.id
}
self._check_api_v1_tasks_id(self.user, data)
@ -1324,6 +1346,16 @@ class TaskPartialUpdateAPITestCase(TaskUpdateAPITestCase):
class TaskCreateAPITestCase(APITestCase):
def setUp(self):
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
def setUpTestData(cls):
@ -1339,8 +1371,10 @@ class TaskCreateAPITestCase(APITestCase):
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
self.assertEqual(response.data["name"], data["name"])
self.assertEqual(response.data["mode"], "")
self.assertEqual(response.data["owner"], data.get("owner", user.id))
self.assertEqual(response.data["assignee"], data.get("assignee"))
self.assertEqual(response.data["project_id"], data.get("project_id", None))
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["overlap"], data.get("overlap", None))
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):
data = {
"name": "new name for the task",
"owner": self.assignee.id,
"owner_id": self.assignee.id,
"labels": [{
"name": "car",
"attributes": [{
@ -1391,6 +1425,17 @@ class TaskCreateAPITestCase(APITestCase):
}
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):
data = {
"name": "new name for the task",
@ -1664,8 +1709,8 @@ class TaskDataAPITestCase(APITestCase):
response = self._get_task(user, task_id)
expected_status_code = status.HTTP_200_OK
if user == self.user and "owner" in spec and spec["owner"] != user.id and \
"assignee" in spec and spec["assignee"] != user.id:
if user == self.user and "owner_id" in spec and spec["owner_id"] != user.id and \
"assignee_id" in spec and spec["assignee_id"] != user.id:
expected_status_code = status.HTTP_403_FORBIDDEN
self.assertEqual(response.status_code, expected_status_code)
@ -1753,8 +1798,8 @@ class TaskDataAPITestCase(APITestCase):
def _test_api_v1_tasks_id_data(self, user):
task_spec = {
"name": "my task #1",
"owner": self.owner.id,
"assignee": self.assignee.id,
"owner_id": self.owner.id,
"assignee_id": self.assignee.id,
"overlap": 0,
"segment_size": 100,
"labels": [
@ -2143,8 +2188,8 @@ class TaskDataAPITestCase(APITestCase):
def test_api_v1_tasks_id_data_no_auth(self):
data = {
"name": "my task #3",
"owner": self.owner.id,
"assignee": self.assignee.id,
"owner_id": self.owner.id,
"assignee_id": self.assignee.id,
"overlap": 0,
"segment_size": 100,
"labels": [
@ -2189,8 +2234,8 @@ class JobAnnotationAPITestCase(APITestCase):
def _create_task(self, owner, assignee):
data = {
"name": "my task #1",
"owner": owner.id,
"assignee": assignee.id,
"owner_id": owner.id,
"assignee_id": assignee.id,
"overlap": 0,
"segment_size": 100,
"labels": [

@ -37,14 +37,15 @@ from cvat.apps.authentication import auth
from cvat.apps.dataset_manager.serializers import DatasetFormatsSerializer
from cvat.apps.engine.frame_provider import FrameProvider
from cvat.apps.engine.models import (
Job, StatusChoice, Task, StorageMethodChoice, StorageChoice
Job, StatusChoice, Task, Project, StorageMethodChoice, StorageChoice
)
from cvat.apps.engine.serializers import (
AboutSerializer, AnnotationFileSerializer, BasicUserSerializer,
DataMetaSerializer, DataSerializer, ExceptionSerializer,
FileInfoSerializer, JobSerializer, LabeledDataSerializer,
LogEventSerializer, ProjectSerializer, RqStatusSerializer,
TaskSerializer, UserSerializer, PluginsSerializer,
LogEventSerializer, ProjectSerializer, ProjectSearchSerializer,
RqStatusSerializer, TaskSerializer, UserSerializer,
PluginsSerializer,
)
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")
owner = filters.CharFilter(field_name="owner__username", lookup_expr="icontains")
status = filters.CharFilter(field_name="status", lookup_expr="icontains")
assignee = filters.CharFilter(field_name="assignee__username", lookup_expr="icontains")
class Meta:
model = models.Project
fields = ("id", "name", "owner", "status", "assignee")
fields = ("id", "name", "owner", "status")
@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=[
openapi.Parameter('id', openapi.IN_QUERY, description="A unique number value identifying this project",
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",
type=openapi.TYPE_STRING),
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]),
openapi.Parameter('assignee', openapi.IN_QUERY, description="Find all projects where assignee name contains a parameter value",
type=openapi.TYPE_STRING)]))
type=openapi.TYPE_STRING, enum=[str(i) for i in StatusChoice])]))
@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='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'))
class ProjectViewSet(auth.ProjectGetQuerySetMixin, viewsets.ModelViewSet):
queryset = models.Project.objects.all().order_by('-id')
serializer_class = ProjectSerializer
search_fields = ("name", "owner__username", "assignee__username", "status")
search_fields = ("name", "owner__username", "status")
filterset_class = ProjectFilter
ordering_fields = ("id", "name", "owner", "status", "assignee")
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):
http_method = self.request.method
permissions = [IsAuthenticated]
@ -243,9 +246,19 @@ class ProjectViewSet(auth.ProjectGetQuerySetMixin, viewsets.ModelViewSet):
return [perm() for perm in permissions]
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()
else:
validate_project_limit(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',
@ -713,7 +726,16 @@ class JobViewSet(viewsets.GenericViewSet,
return Response(data=str(e), status=status.HTTP_400_BAD_REQUEST)
return Response(data)
class UserFilter(filters.FilterSet):
class Meta:
model = User
fields = ("id",)
@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'))
@method_decorator(name='retrieve', decorator=swagger_auto_schema(
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):
queryset = User.objects.prefetch_related('groups').all().order_by('id')
http_method_names = ['get', 'post', 'head', 'patch', 'delete']
search_fields = ('username', 'first_name', 'last_name')
filterset_class = UserFilter
def get_serializer_class(self):
user = self.request.user

@ -346,6 +346,9 @@ os.makedirs(CACHE_ROOT, exist_ok=True)
TASKS_ROOT = os.path.join(DATA_ROOT, 'tasks')
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')
os.makedirs(SHARE_ROOT, exist_ok=True)
@ -427,6 +430,9 @@ RESTRICTIONS = {
# this setting limits the number of tasks for the user
'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
'reduce_task_visibility': False,

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

Loading…
Cancel
Save