IAM: Open Policy Agent integration (#3788)

Co-authored-by: Boris Sekachev <boris.sekachev@intel.com>
Co-authored-by: Dmitry Kruchinin <dmitryx.kruchinin@intel.com>
main
Nikita Manovich 4 years ago committed by GitHub
parent 5281e7938c
commit 4708b5ecf8
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -16,6 +16,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Getting SHA from the default branch - name: Getting SHA from the default branch
id: get-sha id: get-sha
run: | run: |
@ -63,6 +66,22 @@ jobs:
cache-from: type=local,src=/tmp/cvat_cache_server cache-from: type=local,src=/tmp/cvat_cache_server
tags: openvino/cvat_server:latest tags: openvino/cvat_server:latest
load: true load: true
- name: Running OPA tests
run: |
curl -L -o opa https://openpolicyagent.org/downloads/v0.34.2/opa_linux_amd64_static
chmod +x ./opa
./opa test cvat/apps/iam/rules
- name: Running REST API tests
env:
API_ABOUT_PAGE: "localhost:8080/api/v1/server/about"
run: |
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml up -d
/bin/bash -c 'while [[ "$(curl -s -o /dev/null -w ''%{http_code}'' ${API_ABOUT_PAGE})" != "401" ]]; do sleep 5; done'
cat tests/rest_api/assets/cvat_db.sql | docker exec -i cvat_db psql -q -U root -d cvat
cat tests/rest_api/assets/cvat_data.tar.bz2 | docker run --rm -i --volumes-from cvat ubuntu tar -xj --strip 3 -C /home/django/data
pip3 install --user -r tests/rest_api/requirements.txt
pytest tests/rest_api/
docker-compose -f docker-compose.yml -f docker-compose.dev.yml -f components/serverless/docker-compose.serverless.yml down -v
- name: Running unit tests - name: Running unit tests
env: env:
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }} HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}

1
.gitignore vendored

@ -19,6 +19,7 @@ __pycache__
*.pyc *.pyc
._* ._*
.coverage .coverage
.husky/
# Ignore npm logs file # Ignore npm logs file
npm-debug.log* npm-debug.log*

@ -62,10 +62,10 @@
"type": "pwa-chrome", "type": "pwa-chrome",
"request": "launch", "request": "launch",
"url": "http://localhost:7000/", "url": "http://localhost:7000/",
"disableNetworkCache":true, "disableNetworkCache": true,
"trace": true, "trace": true,
"showAsyncStacks": true, "showAsyncStacks": true,
"pathMapping":{ "pathMapping": {
"/static/engine/": "${workspaceFolder}/cvat/apps/engine/static/engine/", "/static/engine/": "${workspaceFolder}/cvat/apps/engine/static/engine/",
"/static/dashboard/": "${workspaceFolder}/cvat/apps/dashboard/static/dashboard/", "/static/dashboard/": "${workspaceFolder}/cvat/apps/dashboard/static/dashboard/",
} }
@ -111,7 +111,7 @@
"request": "launch", "request": "launch",
"justMyCode": false, "justMyCode": false,
"stopOnEntry": false, "stopOnEntry": false,
"python":"${command:python.interpreterPath}", "python": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py", "program": "${workspaceRoot}/manage.py",
"args": [ "args": [
"rqworker", "rqworker",

@ -21,18 +21,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Data sorting option (<https://github.com/openvinotoolkit/cvat/pull/3937>) - Data sorting option (<https://github.com/openvinotoolkit/cvat/pull/3937>)
- Options to change font size & position of text labels on the canvas (<https://github.com/openvinotoolkit/cvat/pull/3972>) - Options to change font size & position of text labels on the canvas (<https://github.com/openvinotoolkit/cvat/pull/3972>)
- Add "tag" return type for automatic annotation in Nuclio (<https://github.com/openvinotoolkit/cvat/pull/3896>) - Add "tag" return type for automatic annotation in Nuclio (<https://github.com/openvinotoolkit/cvat/pull/3896>)
- Advanced identity access management system, using open policy agent (<https://github.com/openvinotoolkit/cvat/pull/3788>)
- Organizations to create "shared space" for different groups of users (<https://github.com/openvinotoolkit/cvat/pull/3788>)
- Dataset importing to a project (<https://github.com/openvinotoolkit/cvat/pull/3790>) - Dataset importing to a project (<https://github.com/openvinotoolkit/cvat/pull/3790>)
- User is able to customize information that text labels show (<https://github.com/openvinotoolkit/cvat/pull/4029>) - User is able to customize information that text labels show (<https://github.com/openvinotoolkit/cvat/pull/4029>)
- Support for uploading manifest with any name (<https://github.com/openvinotoolkit/cvat/pull/4041>) - Support for uploading manifest with any name (<https://github.com/openvinotoolkit/cvat/pull/4041>)
### Changed ### Changed
- TDB - Users don't have access to a task object anymore if they are assigneed only on some jobs of the task (<https://github.com/openvinotoolkit/cvat/pull/3788>)
- Different resources (tasks, projects) are not visible anymore for all CVAT instance users by default (<https://github.com/openvinotoolkit/cvat/pull/3788>)
### Deprecated ### Deprecated
- TDB - Job field "status" is not used in UI anymore, but it has not been removed from the database yet (<https://github.com/openvinotoolkit/cvat/pull/3788>)
### Removed ### Removed
- TDB - Review rating, reviewer field from the job instance (use assignee field together with stage field instead) (<https://github.com/openvinotoolkit/cvat/pull/3788>)
### Fixed ### Fixed
- Fixed Interaction handler keyboard handlers (<https://github.com/openvinotoolkit/cvat/pull/3881>) - Fixed Interaction handler keyboard handlers (<https://github.com/openvinotoolkit/cvat/pull/3881>)
@ -55,6 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- interactor: add HRNet interactive segmentation serverless function (<https://github.com/openvinotoolkit/cvat/pull/3740>) - interactor: add HRNet interactive segmentation serverless function (<https://github.com/openvinotoolkit/cvat/pull/3740>)
- Added GPU implementation for SiamMask, reworked tracking approach (<https://github.com/openvinotoolkit/cvat/pull/3571>) - Added GPU implementation for SiamMask, reworked tracking approach (<https://github.com/openvinotoolkit/cvat/pull/3571>)
- Progress bar for manifest creating (<https://github.com/openvinotoolkit/cvat/pull/3712>) - Progress bar for manifest creating (<https://github.com/openvinotoolkit/cvat/pull/3712>)
- IAM: Open Policy Agent integration (<https://github.com/openvinotoolkit/cvat/pull/3788>)
- Add a tutorial on attaching cloud storage AWS-S3 (<https://github.com/openvinotoolkit/cvat/pull/3745>) - Add a tutorial on attaching cloud storage AWS-S3 (<https://github.com/openvinotoolkit/cvat/pull/3745>)
and Azure Blob Container (<https://github.com/openvinotoolkit/cvat/pull/3778>) and Azure Blob Container (<https://github.com/openvinotoolkit/cvat/pull/3778>)
- The feature to remove annotations in a specified range of frames (<https://github.com/openvinotoolkit/cvat/pull/3617>) - The feature to remove annotations in a specified range of frames (<https://github.com/openvinotoolkit/cvat/pull/3617>)
@ -63,6 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- UI tracking has been reworked (<https://github.com/openvinotoolkit/cvat/pull/3571>) - UI tracking has been reworked (<https://github.com/openvinotoolkit/cvat/pull/3571>)
- Updated Django till 3.2.7 (automatic AppConfig discovery)
- Manifest generation: Reduce creating time (<https://github.com/openvinotoolkit/cvat/pull/3712>) - Manifest generation: Reduce creating time (<https://github.com/openvinotoolkit/cvat/pull/3712>)
- Migration from NPM 6 to NPM 7 (<https://github.com/openvinotoolkit/cvat/pull/3773>) - Migration from NPM 6 to NPM 7 (<https://github.com/openvinotoolkit/cvat/pull/3773>)
- Update Datumaro dependency to 0.2.0 (<https://github.com/openvinotoolkit/cvat/pull/3813>) - Update Datumaro dependency to 0.2.0 (<https://github.com/openvinotoolkit/cvat/pull/3813>)

@ -152,7 +152,6 @@ USER ${USER}
WORKDIR ${HOME} WORKDIR ${HOME}
RUN mkdir data share media keys logs /tmp/supervisord RUN mkdir data share media keys logs /tmp/supervisord
RUN python3 manage.py collectstatic
EXPOSE 8080 EXPOSE 8080
ENTRYPOINT ["/usr/bin/supervisord"] ENTRYPOINT ["/usr/bin/supervisord"]

@ -79,7 +79,6 @@ services:
DJANGO_LOG_VIEWER_HOST: kibana DJANGO_LOG_VIEWER_HOST: kibana
DJANGO_LOG_VIEWER_PORT: 5601 DJANGO_LOG_VIEWER_PORT: 5601
CVAT_ANALYTICS: 1 CVAT_ANALYTICS: 1
no_proxy: kibana,logstash,nuclio,${no_proxy}
volumes: volumes:
cvat_events: cvat_events:

@ -21,7 +21,6 @@ services:
cvat: cvat:
environment: environment:
CVAT_SERVERLESS: 1 CVAT_SERVERLESS: 1
no_proxy: kibana,logstash,nuclio,${no_proxy}
volumes: volumes:
cvat_events: cvat_events:

@ -1,12 +1,12 @@
{ {
"name": "cvat-canvas", "name": "cvat-canvas",
"version": "2.11.0", "version": "2.11.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cvat-canvas", "name": "cvat-canvas",
"version": "2.11.0", "version": "2.11.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/polylabel": "^1.0.5", "@types/polylabel": "^1.0.5",

@ -1,6 +1,6 @@
{ {
"name": "cvat-canvas", "name": "cvat-canvas",
"version": "2.11.0", "version": "2.11.1",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library", "description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts", "main": "src/canvas.ts",
"scripts": { "scripts": {

@ -10,30 +10,28 @@ export enum Orientation {
RIGHT = 'right', RIGHT = 'right',
} }
function line(p1: Point, p2: Point): number[] {
const a = p1.y - p2.y;
const b = p2.x - p1.x;
const c = b * p1.y + a * p1.x;
return [a, b, c];
}
export function intersection(p1: Point, p2: Point, p3: Point, p4: Point): Point | null { export function intersection(p1: Point, p2: Point, p3: Point, p4: Point): Point | null {
const L1 = line(p1, p2); // Check if none of the lines are of length 0
const L2 = line(p3, p4); const { x: x1, y: y1 } = p1;
const { x: x2, y: y2 } = p2;
const { x: x3, y: y3 } = p3;
const { x: x4, y: y4 } = p4;
const D = L1[0] * L2[1] - L1[1] * L2[0]; if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) {
const Dx = L1[2] * L2[1] - L1[1] * L2[2]; return null;
const Dy = L1[0] * L2[2] - L1[2] * L2[0];
let x = null;
let y = null;
if (Math.abs(D) > Number.EPSILON) {
x = Dx / D;
y = Dy / D;
return { x, y };
} }
const denominator = ((y4 - y3) * (x2 - x1) - (x4 - x3) * (y2 - y1));
// Lines are parallel
if (Math.abs(denominator) < Number.EPSILON) {
return null; return null;
}
const ua = ((x4 - x3) * (y1 - y3) - (y4 - y3) * (x1 - x3)) / denominator;
// Return a object with the x and y coordinates of the intersection
return { x: x1 + ua * (x2 - x1), y: y1 + ua * (y2 - y1) };
} }
export class Equation { export class Equation {

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

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

@ -276,7 +276,7 @@
if (instance instanceof Task) { if (instance instanceof Task) {
result = await serverProxy.tasks.exportDataset(instance.id, format, name, saveImages); result = await serverProxy.tasks.exportDataset(instance.id, format, name, saveImages);
} else if (instance instanceof Job) { } else if (instance instanceof Job) {
result = await serverProxy.tasks.exportDataset(instance.task.id, format, name, saveImages); result = await serverProxy.tasks.exportDataset(instance.taskId, format, name, saveImages);
} else { } else {
result = await serverProxy.projects.exportDataset(instance.id, format, name, saveImages); result = await serverProxy.projects.exportDataset(instance.id, format, name, saveImages);
} }

@ -2,6 +2,8 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
const config = require('./config');
(() => { (() => {
const PluginRegistry = require('./plugins'); const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy');
@ -14,6 +16,7 @@
checkFilter, checkFilter,
checkExclusiveFields, checkExclusiveFields,
camelToSnake, camelToSnake,
checkObjectType,
} = require('./common'); } = require('./common');
const { const {
@ -27,9 +30,10 @@
const User = require('./user'); const User = require('./user');
const { AnnotationFormats } = require('./annotation-formats'); const { AnnotationFormats } = require('./annotation-formats');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { Task } = require('./session'); const { Task, Job } = require('./session');
const { Project } = require('./project'); const { Project } = require('./project');
const { CloudStorage } = require('./cloud-storage'); const { CloudStorage } = require('./cloud-storage');
const Organization = require('./organization');
function implementAPI(cvat) { function implementAPI(cvat) {
cvat.plugins.list.implementation = PluginRegistry.list; cvat.plugins.list.implementation = PluginRegistry.list;
@ -139,7 +143,7 @@
searchParams[key] = filter[key]; searchParams[key] = filter[key];
} }
} }
users = await serverProxy.users.get(new URLSearchParams(searchParams).toString()); users = await serverProxy.users.get(searchParams);
} }
users = users.map((user) => new User(user)); users = users.map((user) => new User(user));
@ -160,23 +164,21 @@
throw new ArgumentError('Job filter must not be empty'); throw new ArgumentError('Job filter must not be empty');
} }
let tasks = [];
if ('taskID' in filter) { if ('taskID' in filter) {
tasks = await serverProxy.tasks.getTasks(`id=${filter.taskID}`); const [task] = await serverProxy.tasks.get({ id: filter.taskID });
} else { if (task) {
const job = await serverProxy.jobs.get(filter.jobID); return new Task(task).jobs;
if (typeof job.task_id !== 'undefined') {
tasks = await serverProxy.tasks.getTasks(`id=${job.task_id}`);
} }
return [];
} }
// If task was found by its id, then create task instance and get Job instance from it const job = await serverProxy.jobs.get(filter.jobID);
if (tasks.length) { if (job) {
const task = new Task(tasks[0]); return [new Job(job)];
return filter.jobID ? task.jobs.filter((job) => job.id === filter.jobID) : task.jobs;
} }
return tasks; return [];
}; };
cvat.tasks.get.implementation = async (filter) => { cvat.tasks.get.implementation = async (filter) => {
@ -196,8 +198,7 @@
checkExclusiveFields(filter, ['id', 'search', 'projectId'], ['page']); checkExclusiveFields(filter, ['id', 'search', 'projectId'], ['page']);
const searchParams = new URLSearchParams(); const searchParams = {};
for (const field of [ for (const field of [
'name', 'name',
'owner', 'owner',
@ -212,11 +213,11 @@
'dimension', 'dimension',
]) { ]) {
if (Object.prototype.hasOwnProperty.call(filter, field)) { if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(camelToSnake(field), filter[field]); searchParams[camelToSnake(field)] = filter[field];
} }
} }
const tasksData = await serverProxy.tasks.getTasks(searchParams.toString()); const tasksData = await serverProxy.tasks.get(searchParams);
const tasks = tasksData.map((task) => new Task(task)); const tasks = tasksData.map((task) => new Task(task));
tasks.count = tasksData.count; tasks.count = tasksData.count;
@ -237,14 +238,14 @@
checkExclusiveFields(filter, ['id', 'search'], ['page']); checkExclusiveFields(filter, ['id', 'search'], ['page']);
const searchParams = new URLSearchParams(); const searchParams = {};
for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page']) { for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page']) {
if (Object.prototype.hasOwnProperty.call(filter, field)) { if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(camelToSnake(field), filter[field]); searchParams[camelToSnake(field)] = filter[field];
} }
} }
const projectsData = await serverProxy.projects.get(searchParams.toString()); const projectsData = await serverProxy.projects.get(searchParams);
const projects = projectsData.map((project) => { const projects = projectsData.map((project) => {
project.task_ids = project.tasks; project.task_ids = project.tasks;
return project; return project;
@ -295,12 +296,25 @@
const cloudStoragesData = await serverProxy.cloudStorages.get(searchParams.toString()); const cloudStoragesData = await serverProxy.cloudStorages.get(searchParams.toString());
const cloudStorages = cloudStoragesData.map((cloudStorage) => new CloudStorage(cloudStorage)); const cloudStorages = cloudStoragesData.map((cloudStorage) => new CloudStorage(cloudStorage));
cloudStorages.count = cloudStoragesData.count; cloudStorages.count = cloudStoragesData.count;
return cloudStorages; return cloudStorages;
}; };
cvat.organizations.get.implementation = async () => {
const organizationsData = await serverProxy.organizations.get();
const organizations = organizationsData.map((organizationData) => new Organization(organizationData));
return organizations;
};
cvat.organizations.activate.implementation = (organization) => {
checkObjectType('organization', organization, null, Organization);
config.organizationID = organization.slug;
};
cvat.organizations.deactivate.implementation = async () => {
config.organizationID = null;
};
return cvat; return cvat;
} }

@ -15,7 +15,6 @@ function build() {
const Statistics = require('./statistics'); const Statistics = require('./statistics');
const Comment = require('./comment'); const Comment = require('./comment');
const Issue = require('./issue'); const Issue = require('./issue');
const Review = require('./review');
const { Job, Task } = require('./session'); const { Job, Task } = require('./session');
const { Project } = require('./project'); const { Project } = require('./project');
const implementProject = require('./project-implementation'); const implementProject = require('./project-implementation');
@ -23,6 +22,7 @@ function build() {
const MLModel = require('./ml-model'); const MLModel = require('./ml-model');
const { FrameData } = require('./frames'); const { FrameData } = require('./frames');
const { CloudStorage } = require('./cloud-storage'); const { CloudStorage } = require('./cloud-storage');
const Organization = require('./organization');
const enums = require('./enums'); const enums = require('./enums');
@ -792,6 +792,50 @@ function build() {
return result; return result;
}, },
}, },
/**
* This namespace could be used to get organizations list from the server
* @namespace organizations
* @memberof module:API.cvat
*/
organizations: {
/**
* Method returns a list of organizations
* @method get
* @async
* @memberof module:API.cvat.organizations
* @returns {module:API.cvat.classes.Organization[]}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async get() {
const result = await PluginRegistry.apiWrapper(cvat.organizations.get);
return result;
},
/**
* Method activates organization context
* @method activate
* @async
* @param {module:API.cvat.classes.Organization}
* @memberof module:API.cvat.organizations
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async activate(organization) {
const result = await PluginRegistry.apiWrapper(cvat.organizations.activate, organization);
return result;
},
/**
* Method deactivates organization context
* @method deactivate
* @async
* @memberof module:API.cvat.organizations
* @throws {module:API.cvat.exceptions.PluginError}
*/
async deactivate() {
const result = await PluginRegistry.apiWrapper(cvat.organizations.deactivate);
return result;
},
},
/** /**
* Namespace is used for access to classes * Namespace is used for access to classes
* @namespace classes * @namespace classes
@ -810,9 +854,9 @@ function build() {
MLModel, MLModel,
Comment, Comment,
Issue, Issue,
Review,
FrameData, FrameData,
CloudStorage, CloudStorage,
Organization,
}, },
}; };
@ -826,6 +870,7 @@ function build() {
cvat.client = Object.freeze(cvat.client); cvat.client = Object.freeze(cvat.client);
cvat.enums = Object.freeze(cvat.enums); cvat.enums = Object.freeze(cvat.enums);
cvat.cloudStorages = Object.freeze(cvat.cloudStorages); cvat.cloudStorages = Object.freeze(cvat.cloudStorages);
cvat.organizations = Object.freeze(cvat.organizations);
const implementAPI = require('./api-implementation'); const implementAPI = require('./api-implementation');

@ -1,10 +1,9 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
const User = require('./user'); const User = require('./user');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { negativeIDGenerator } = require('./common');
/** /**
* Class representing a single comment * Class representing a single comment
@ -18,8 +17,7 @@ class Comment {
message: undefined, message: undefined,
created_date: undefined, created_date: undefined,
updated_date: undefined, updated_date: undefined,
removed: false, owner: undefined,
author: undefined,
}; };
for (const property in data) { for (const property in data) {
@ -28,11 +26,7 @@ class Comment {
} }
} }
if (data.author && !(data.author instanceof User)) data.author = new User(data.author); if (data.owner && !(data.owner instanceof User)) data.owner = new User(data.owner);
if (typeof id === 'undefined') {
data.id = negativeIDGenerator();
}
if (typeof data.created_date === 'undefined') { if (typeof data.created_date === 'undefined') {
data.created_date = new Date().toISOString(); data.created_date = new Date().toISOString();
} }
@ -88,29 +82,14 @@ class Comment {
}, },
/** /**
* Instance of a user who has created the comment * Instance of a user who has created the comment
* @name author * @name owner
* @type {module:API.cvat.classes.User} * @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Comment * @memberof module:API.cvat.classes.Comment
* @readonly * @readonly
* @instance * @instance
*/ */
author: { owner: {
get: () => data.author, get: () => data.owner,
},
/**
* @name removed
* @type {boolean}
* @memberof module:API.cvat.classes.Comment
* @instance
*/
removed: {
get: () => data.removed,
set: (value) => {
if (typeof value !== 'boolean') {
throw new ArgumentError('Value must be a boolean value');
}
data.removed = value;
},
}, },
__internal: { __internal: {
get: () => data, get: () => data,
@ -124,7 +103,7 @@ class Comment {
message: this.message, message: this.message,
}; };
if (this.id > 0) { if (typeof this.id === 'number') {
data.id = this.id; data.id = this.id;
} }
if (this.createdDate) { if (this.createdDate) {
@ -133,21 +112,12 @@ class Comment {
if (this.updatedDate) { if (this.updatedDate) {
data.updated_date = this.updatedDate; data.updated_date = this.updatedDate;
} }
if (this.author) { if (this.owner) {
data.author = this.author.serialize(); data.owner_id = this.owner.serialize().id;
} }
return data; return data;
} }
toJSON() {
const data = this.serialize();
const { author, ...updated } = data;
return {
...updated,
author_id: author ? author.id : undefined,
};
}
} }
module.exports = Comment; module.exports = Comment;

@ -97,37 +97,30 @@
); );
} }
function negativeIDGenerator() {
const value = negativeIDGenerator.start;
negativeIDGenerator.start -= 1;
return value;
}
negativeIDGenerator.start = -1;
class FieldUpdateTrigger { class FieldUpdateTrigger {
constructor(initialFields) { constructor() {
const data = { ...initialFields }; let updatedFlags = {};
Object.defineProperties( Object.defineProperties(
this, this,
Object.freeze({ Object.freeze({
...Object.assign(
{},
...Array.from(Object.keys(data), (key) => ({
[key]: {
get: () => data[key],
set: (value) => {
data[key] = value;
},
enumerable: true,
},
})),
),
reset: { reset: {
value: () => { value: () => {
Object.keys(data).forEach((key) => { updatedFlags = {};
data[key] = false; },
}); },
update: {
value: (name) => {
updatedFlags[name] = true;
},
},
getUpdated: {
value: (data, propMap = {}) => {
const result = {};
for (const updatedField of Object.keys(updatedFlags)) {
result[propMap[updatedField] || updatedField] = data[updatedField];
}
return result;
}, },
}, },
}), }),
@ -142,7 +135,6 @@
isString, isString,
checkFilter, checkFilter,
checkObjectType, checkObjectType,
negativeIDGenerator,
checkExclusiveFields, checkExclusiveFields,
camelToSnake, camelToSnake,
FieldUpdateTrigger, FieldUpdateTrigger,

@ -5,5 +5,6 @@
module.exports = { module.exports = {
backendAPI: '/api/v1', backendAPI: '/api/v1',
proxy: false, proxy: false,
organizationID: null,
origin: '', origin: '',
}; };

@ -34,33 +34,51 @@
}); });
/** /**
* Task dimension * Job stages
* @enum * @enum {string}
* @name DimensionType * @name JobStage
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums
* @property {string} DIMENSION_2D '2d' * @property {string} ANNOTATION 'annotation'
* @property {string} DIMENSION_3D '3d' * @property {string} VALIDATION 'validation'
* @property {string} ACCEPTANCE 'acceptance'
* @readonly * @readonly
*/ */
const DimensionType = Object.freeze({ const JobStage = Object.freeze({
DIMENSION_2D: '2d', ANNOTATION: 'annotation',
DIMENSION_3D: '3d', VALIDATION: 'validation',
ACCEPTANCE: 'acceptance',
}); });
/** /**
* Review statuses * Job states
* @enum {string} * @enum {string}
* @name ReviewStatus * @name JobState
* @memberof module:API.cvat.enums * @memberof module:API.cvat.enums
* @property {string} ACCEPTED 'accepted' * @property {string} NEW 'new'
* @property {string} IN_PROGRESS 'in progress'
* @property {string} COMPLETED 'completed'
* @property {string} REJECTED 'rejected' * @property {string} REJECTED 'rejected'
* @property {string} REVIEW_FURTHER 'review_further'
* @readonly * @readonly
*/ */
const ReviewStatus = Object.freeze({ const JobState = Object.freeze({
ACCEPTED: 'accepted', NEW: 'new',
IN_PROGRESS: 'in progress',
COMPLETED: 'completed',
REJECTED: 'rejected', REJECTED: 'rejected',
REVIEW_FURTHER: 'review_further', });
/**
* Task dimension
* @enum
* @name DimensionType
* @memberof module:API.cvat.enums
* @property {string} DIMENSION_2D '2d'
* @property {string} DIMENSION_3D '3d'
* @readonly
*/
const DimensionType = Object.freeze({
DIMENSION_2D: '2d',
DIMENSION_3D: '3d',
}); });
/** /**
@ -367,6 +385,24 @@
KEY_FILE_PATH: 'KEY_FILE_PATH', KEY_FILE_PATH: 'KEY_FILE_PATH',
}); });
/**
* Task statuses
* @enum {string}
* @name MembershipRole
* @memberof module:API.cvat.enums
* @property {string} WORKER 'worker'
* @property {string} SUPERVISOR 'supervisor'
* @property {string} MAINTAINER 'maintainer'
* @property {string} OWNER 'owner'
* @readonly
*/
const MembershipRole = Object.freeze({
WORKER: 'worker',
SUPERVISOR: 'supervisor',
MAINTAINER: 'maintainer',
OWNER: 'owner',
});
/** /**
* Sorting methods * Sorting methods
* @enum {string} * @enum {string}
@ -388,7 +424,8 @@
module.exports = { module.exports = {
ShareFileType, ShareFileType,
TaskStatus, TaskStatus,
ReviewStatus, JobStage,
JobState,
TaskMode, TaskMode,
AttributeType, AttributeType,
ObjectType, ObjectType,
@ -402,6 +439,7 @@
DimensionType, DimensionType,
CloudStorageProviderType, CloudStorageProviderType,
CloudStorageCredentialsType, CloudStorageCredentialsType,
MembershipRole,
SortingMethod, SortingMethod,
}; };
})(); })();

@ -23,6 +23,7 @@
height, height,
name, name,
taskID, taskID,
jobID,
frameNumber, frameNumber,
startFrame, startFrame,
stopFrame, stopFrame,
@ -69,6 +70,17 @@
value: taskID, value: taskID,
writable: false, writable: false,
}, },
/**
* @name jid
* @type {integer}
* @memberof module:API.cvat.classes.FrameData
* @readonly
* @instance
*/
jid: {
value: jobID,
writable: false,
},
/** /**
* @name number * @name number
* @type {integer} * @type {integer}
@ -191,7 +203,7 @@
const taskDataCache = frameDataCache[this.tid]; const taskDataCache = frameDataCache[this.tid];
const activeChunk = taskDataCache.activeChunkRequest; const activeChunk = taskDataCache.activeChunkRequest;
activeChunk.request = serverProxy.frames activeChunk.request = serverProxy.frames
.getData(this.tid, activeChunk.chunkNumber) .getData(this.tid, this.jid, activeChunk.chunkNumber)
.then((chunk) => { .then((chunk) => {
frameDataCache[this.tid].activeChunkRequest.completed = true; frameDataCache[this.tid].activeChunkRequest.completed = true;
if (!taskDataCache.nextChunkRequest) { if (!taskDataCache.nextChunkRequest) {
@ -366,7 +378,7 @@
} }
class FrameBuffer { class FrameBuffer {
constructor(size, chunkSize, stopFrame, taskID) { constructor(size, chunkSize, stopFrame, taskID, jobID) {
this._size = size; this._size = size;
this._buffer = {}; this._buffer = {};
this._contextImage = {}; this._contextImage = {};
@ -375,6 +387,7 @@
this._stopFrame = stopFrame; this._stopFrame = stopFrame;
this._activeFillBufferRequest = false; this._activeFillBufferRequest = false;
this._taskID = taskID; this._taskID = taskID;
this._jobID = jobID;
} }
isContextImageAvailable(frame) { isContextImageAvailable(frame) {
@ -411,6 +424,7 @@
const frameData = new FrameData({ const frameData = new FrameData({
...frameMeta, ...frameMeta,
taskID: this._taskID, taskID: this._taskID,
jobID: this._jobID,
frameNumber: requestedFrame, frameNumber: requestedFrame,
startFrame: frameDataCache[this._taskID].startFrame, startFrame: frameDataCache[this._taskID].startFrame,
stopFrame: frameDataCache[this._taskID].stopFrame, stopFrame: frameDataCache[this._taskID].stopFrame,
@ -508,7 +522,7 @@
} }
} }
async require(frameNumber, taskID, fillBuffer, frameStep) { async require(frameNumber, taskID, jobID, fillBuffer, frameStep) {
for (const frame in this._buffer) { for (const frame in this._buffer) {
if (frame < frameNumber || frame >= frameNumber + this._size * frameStep) { if (frame < frameNumber || frame >= frameNumber + this._size * frameStep) {
delete this._buffer[frame]; delete this._buffer[frame];
@ -520,6 +534,7 @@
let frame = new FrameData({ let frame = new FrameData({
...frameMeta, ...frameMeta,
taskID, taskID,
jobID,
frameNumber, frameNumber,
startFrame: frameDataCache[taskID].startFrame, startFrame: frameDataCache[taskID].startFrame,
stopFrame: frameDataCache[taskID].stopFrame, stopFrame: frameDataCache[taskID].stopFrame,
@ -576,10 +591,10 @@
} }
} }
async function getImageContext(taskID, frame) { async function getImageContext(jobID, frame) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
serverProxy.frames serverProxy.frames
.getImageContext(taskID, frame) .getImageContext(jobID, frame)
.then((result) => { .then((result) => {
if (isNode) { if (isNode) {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
@ -598,11 +613,11 @@
}); });
} }
async function getContextImage(taskID, frame) { async function getContextImage(taskID, jobID, frame) {
if (frameDataCache[taskID].frameBuffer.isContextImageAvailable(frame)) { if (frameDataCache[taskID].frameBuffer.isContextImageAvailable(frame)) {
return frameDataCache[taskID].frameBuffer.getContextImage(frame); return frameDataCache[taskID].frameBuffer.getContextImage(frame);
} }
const response = getImageContext(taskID, frame); const response = getImageContext(jobID, frame);
frameDataCache[taskID].frameBuffer.addContextImage(frame, response); frameDataCache[taskID].frameBuffer.addContextImage(frame, response);
return frameDataCache[taskID].frameBuffer.getContextImage(frame); return frameDataCache[taskID].frameBuffer.getContextImage(frame);
} }
@ -632,6 +647,7 @@
async function getFrame( async function getFrame(
taskID, taskID,
jobID,
chunkSize, chunkSize,
chunkType, chunkType,
mode, mode,
@ -674,6 +690,7 @@
chunkSize, chunkSize,
stopFrame, stopFrame,
taskID, taskID,
jobID,
), ),
decodedBlocksCacheSize, decodedBlocksCacheSize,
activeChunkRequest: null, activeChunkRequest: null,
@ -684,7 +701,7 @@
frameDataCache[taskID].provider.setRenderSize(frameMeta.width, frameMeta.height); frameDataCache[taskID].provider.setRenderSize(frameMeta.width, frameMeta.height);
} }
return frameDataCache[taskID].frameBuffer.require(frame, taskID, isPlaying, step); return frameDataCache[taskID].frameBuffer.require(frame, taskID, jobID, isPlaying, step);
} }
function getRanges(taskID) { function getRanges(taskID) {

@ -8,7 +8,6 @@ const PluginRegistry = require('./plugins');
const Comment = require('./comment'); const Comment = require('./comment');
const User = require('./user'); const User = require('./user');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { negativeIDGenerator } = require('./common');
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy');
/** /**
@ -20,14 +19,13 @@ class Issue {
constructor(initialData) { constructor(initialData) {
const data = { const data = {
id: undefined, id: undefined,
job: undefined,
position: undefined, position: undefined,
comment_set: [], comments: [],
frame: undefined, frame: undefined,
created_date: undefined, created_date: undefined,
resolved_date: undefined,
owner: undefined, owner: undefined,
resolver: undefined, resolved: undefined,
removed: false,
}; };
for (const property in data) { for (const property in data) {
@ -37,15 +35,11 @@ class Issue {
} }
if (data.owner && !(data.owner instanceof User)) data.owner = new User(data.owner); if (data.owner && !(data.owner instanceof User)) data.owner = new User(data.owner);
if (data.resolver && !(data.resolver instanceof User)) data.resolver = new User(data.resolver);
if (data.comment_set) { if (data.comments) {
data.comment_set = data.comment_set.map((comment) => new Comment(comment)); data.comments = data.comments.map((comment) => new Comment(comment));
} }
if (typeof data.id === 'undefined') {
data.id = negativeIDGenerator();
}
if (typeof data.created_date === 'undefined') { if (typeof data.created_date === 'undefined') {
data.created_date = new Date().toISOString(); data.created_date = new Date().toISOString();
} }
@ -81,6 +75,18 @@ class Issue {
data.position = value; data.position = value;
}, },
}, },
/**
* ID of a job, the issue is linked with
* @name job
* @type {number}
* @memberof module:API.cvat.classes.Issue
* @instance
* @readonly
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
job: {
get: () => data.job,
},
/** /**
* List of comments attached to the issue * List of comments attached to the issue
* @name comments * @name comments
@ -91,7 +97,7 @@ class Issue {
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
*/ */
comments: { comments: {
get: () => data.comment_set.filter((comment) => !comment.removed), get: () => [...data.comments],
}, },
/** /**
* @name frame * @name frame
@ -113,16 +119,6 @@ class Issue {
createdDate: { createdDate: {
get: () => data.created_date, get: () => data.created_date,
}, },
/**
* @name resolvedDate
* @type {string}
* @memberof module:API.cvat.classes.Issue
* @readonly
* @instance
*/
resolvedDate: {
get: () => data.resolved_date,
},
/** /**
* An instance of a user who has raised the issue * An instance of a user who has raised the issue
* @name owner * @name owner
@ -135,30 +131,15 @@ class Issue {
get: () => data.owner, get: () => data.owner,
}, },
/** /**
* An instance of a user who has resolved the issue * The flag defines issue status
* @name resolver * @name resolved
* @type {module:API.cvat.classes.User} * @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Issue * @memberof module:API.cvat.classes.Issue
* @readonly * @readonly
* @instance * @instance
*/ */
resolver: { resolved: {
get: () => data.resolver, get: () => data.resolved,
},
/**
* @name removed
* @type {boolean}
* @memberof module:API.cvat.classes.Comment
* @instance
*/
removed: {
get: () => data.removed,
set: (value) => {
if (typeof value !== 'boolean') {
throw new ArgumentError('Value must be a boolean value');
}
data.removed = value;
},
}, },
__internal: { __internal: {
get: () => data, get: () => data,
@ -184,7 +165,6 @@ class Issue {
/** /**
* @typedef {Object} CommentData * @typedef {Object} CommentData
* @property {number} [author] an ID of a user who has created the comment
* @property {string} message a comment message * @property {string} message a comment message
* @global * @global
*/ */
@ -261,89 +241,68 @@ class Issue {
const data = { const data = {
position: this.position, position: this.position,
frame: this.frame, frame: this.frame,
comment_set: comments.map((comment) => comment.serialize()), comments: comments.map((comment) => comment.serialize()),
}; };
if (this.id > 0) { if (typeof this.id === 'number') {
data.id = this.id; data.id = this.id;
} }
if (this.createdDate) { if (typeof this.job === 'number') {
data.created_date = this.createdDate; data.job = this.job;
} }
if (this.resolvedDate) { if (typeof this.createdDate === 'string') {
data.resolved_date = this.resolvedDate; data.created_date = this.createdDate;
} }
if (this.owner) { if (typeof this.resolved === 'boolean') {
data.owner = this.owner.toJSON(); data.resolved = this.resolved;
} }
if (this.resolver) { if (this.owner instanceof User) {
data.resolver = this.resolver.toJSON(); data.owner = this.owner.serialize().id;
} }
return data; return data;
} }
toJSON() {
const data = this.serialize();
const { owner, resolver, ...updated } = data;
return {
...updated,
comment_set: this.comments.map((comment) => comment.toJSON()),
owner_id: owner ? owner.id : undefined,
resolver_id: resolver ? resolver.id : undefined,
};
}
} }
Issue.prototype.comment.implementation = async function (data) { Issue.prototype.comment.implementation = async function (data) {
if (typeof data !== 'object' || data === null) { if (typeof data !== 'object' || data === null) {
throw new ArgumentError(`The argument "data" must be a not null object. Got ${data}`); throw new ArgumentError(`The argument "data" must be an object. Got "${data}"`);
} }
if (typeof data.message !== 'string' || data.message.length < 1) { if (typeof data.message !== 'string' || data.message.length < 1) {
throw new ArgumentError(`Comment message must be a not empty string. Got ${data.message}`); throw new ArgumentError(`Comment message must be a not empty string. Got "${data.message}"`);
}
if (!(data.author instanceof User)) {
throw new ArgumentError(`Author of the comment must a User instance. Got ${data.author}`);
} }
const comment = new Comment(data); const comment = new Comment(data);
const { id } = this; if (typeof this.id === 'number') {
if (id >= 0) { const serialized = comment.serialize();
const jsonified = comment.toJSON(); serialized.issue = this.id;
jsonified.issue = id; const response = await serverProxy.comments.create(serialized);
const response = await serverProxy.comments.create(jsonified);
const savedComment = new Comment(response); const savedComment = new Comment(response);
this.__internal.comment_set.push(savedComment); this.__internal.comments.push(savedComment);
} else { } else {
this.__internal.comment_set.push(comment); this.__internal.comments.push(comment);
} }
}; };
Issue.prototype.resolve.implementation = async function (user) { Issue.prototype.resolve.implementation = async function (user) {
if (!(user instanceof User)) { if (!(user instanceof User)) {
throw new ArgumentError(`The argument "user" must be an instance of a User class. Got ${typeof user}`); throw new ArgumentError(`The argument "user" must be an instance of a User class. Got "${typeof user}"`);
} }
const { id } = this; if (typeof this.id === 'number') {
if (id >= 0) { const response = await serverProxy.issues.update(this.id, { resolved: true });
const response = await serverProxy.issues.update(id, { resolver_id: user.id }); this.__internal.resolved = response.resolved;
this.__internal.resolved_date = response.resolved_date;
this.__internal.resolver = new User(response.resolver);
} else { } else {
this.__internal.resolved_date = new Date().toISOString(); this.__internal.resolved = true;
this.__internal.resolver = user;
} }
}; };
Issue.prototype.reopen.implementation = async function () { Issue.prototype.reopen.implementation = async function () {
const { id } = this; if (typeof this.id === 'number') {
if (id >= 0) { const response = await serverProxy.issues.update(this.id, { resolved: false });
const response = await serverProxy.issues.update(id, { resolver_id: null }); this.__internal.resolved = response.resolved;
this.__internal.resolved_date = response.resolved_date;
this.__internal.resolver = response.resolver;
} else { } else {
this.__internal.resolved_date = null; this.__internal.resolved = false;
this.__internal.resolver = null;
} }
}; };

@ -1,10 +1,9 @@
// Copyright (C) 2019-2020 Intel Corporation // Copyright (C) 2019-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
const MLModel = require('./ml-model'); const MLModel = require('./ml-model');
const { RQStatus } = require('./enums'); const { RQStatus } = require('./enums');
@ -35,11 +34,9 @@ class LambdaManager {
return models; return models;
} }
async run(task, model, args) { async run(taskID, model, args) {
if (!(task instanceof Task)) { if (!Number.isInteger(taskID) || taskID < 0) {
throw new ArgumentError( throw new ArgumentError(`Argument taskID must be a positive integer. Got "${taskID}"`);
`Argument task is expected to be an instance of Task class, but got ${typeof task}`,
);
} }
if (!(model instanceof MLModel)) { if (!(model instanceof MLModel)) {
@ -52,17 +49,26 @@ class LambdaManager {
throw new ArgumentError(`Argument args is expected to be an object, but got ${typeof model}`); throw new ArgumentError(`Argument args is expected to be an object, but got ${typeof model}`);
} }
const body = args; const body = {
body.task = task.id; ...args,
body.function = model.id; task: taskID,
function: model.id,
};
const result = await serverProxy.lambda.run(body); const result = await serverProxy.lambda.run(body);
return result.id; return result.id;
} }
async call(task, model, args) { async call(taskID, model, args) {
const body = args; if (!Number.isInteger(taskID) || taskID < 0) {
body.task = task.id; throw new ArgumentError(`Argument taskID must be a positive integer. Got "${taskID}"`);
}
const body = {
...args,
task: taskID,
};
const result = await serverProxy.lambda.call(model.id, body); const result = await serverProxy.lambda.call(model.id, body);
return result; return result;
} }

@ -0,0 +1,372 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
const { checkObjectType, isEnum } = require('./common');
const config = require('./config');
const { MembershipRole } = require('./enums');
const { ArgumentError, ServerError } = require('./exceptions');
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
const User = require('./user');
/**
* Class representing an organization
* @memberof module:API.cvat.classes
*/
class Organization {
/**
* @param {object} initialData - Object which is used for initialization
* <br> It must contains keys:
* <br> <li style="margin-left: 10px;"> slug
* <br> It can contains keys:
* <br> <li style="margin-left: 10px;"> name
* <br> <li style="margin-left: 10px;"> description
* <br> <li style="margin-left: 10px;"> owner
* <br> <li style="margin-left: 10px;"> created_date
* <br> <li style="margin-left: 10px;"> updated_date
* <br> <li style="margin-left: 10px;"> contact
*/
constructor(initialData) {
const data = {
id: undefined,
slug: undefined,
name: undefined,
description: undefined,
created_date: undefined,
updated_date: undefined,
owner: undefined,
contact: undefined,
};
for (const prop of Object.keys(data)) {
if (prop in initialData) {
data[prop] = initialData[prop];
}
}
if (data.owner) data.owner = new User(data.owner);
checkObjectType('slug', data.slug, 'string');
if (typeof data.name !== 'undefined') {
checkObjectType('name', data.name, 'string');
}
if (typeof data.description !== 'undefined') {
checkObjectType('description', data.description, 'string');
}
if (typeof data.id !== 'undefined') {
checkObjectType('id', data.id, 'number');
}
if (typeof data.contact !== 'undefined') {
checkObjectType('contact', data.contact, 'object');
for (const prop in data.contact) {
if (typeof data.contact[prop] !== 'string') {
throw ArgumentError(`Contact fields must be strings, tried to set ${typeof data.contact[prop]}`);
}
}
}
if (typeof data.owner !== 'undefined' && data.owner !== null) {
checkObjectType('owner', data.owner, null, User);
}
Object.defineProperties(this, {
id: {
get: () => data.id,
},
slug: {
get: () => data.slug,
},
name: {
get: () => data.name,
set: (name) => {
if (typeof name !== 'string') {
throw ArgumentError(`Name property must be a string, tried to set ${typeof description}`);
}
data.name = name;
},
},
description: {
get: () => data.description,
set: (description) => {
if (typeof description !== 'string') {
throw ArgumentError(
`Description property must be a string, tried to set ${typeof description}`,
);
}
data.description = description;
},
},
contact: {
get: () => ({ ...data.contact }),
set: (contact) => {
if (typeof contact !== 'object') {
throw ArgumentError(`Contact property must be an object, tried to set ${typeof contact}`);
}
for (const prop in contact) {
if (typeof contact[prop] !== 'string') {
throw ArgumentError(`Contact fields must be strings, tried to set ${typeof contact[prop]}`);
}
}
data.contact = { ...contact };
},
},
owner: {
get: () => data.owner,
},
createdDate: {
get: () => data.created_date,
},
updatedDate: {
get: () => data.updated_date,
},
});
}
/**
* Method updates organization data if it was created before, or creates a new organization
* @method save
* @returns {module:API.cvat.classes.Organization}
* @memberof module:API.cvat.classes.Organization
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async save() {
const result = await PluginRegistry.apiWrapper.call(this, Organization.prototype.save);
return result;
}
/**
* Method returns paginatable list of organization members
* @method save
* @returns {module:API.cvat.classes.Organization}
* @param page page number
* @param page_size number of results per page
* @memberof module:API.cvat.classes.Organization
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async members(page = 1, page_size = 10) {
const result = await PluginRegistry.apiWrapper.call(
this,
Organization.prototype.members,
this.slug,
page,
page_size,
);
return result;
}
/**
* Method removes the organization
* @method remove
* @returns {module:API.cvat.classes.Organization}
* @memberof module:API.cvat.classes.Organization
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async remove() {
const result = await PluginRegistry.apiWrapper.call(this, Organization.prototype.remove);
return result;
}
/**
* Method invites new members by email
* @method invite
* @returns {module:API.cvat.classes.Organization}
* @param {string} email
* @param {string} role
* @memberof module:API.cvat.classes.Organization
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async invite(email, role) {
const result = await PluginRegistry.apiWrapper.call(this, Organization.prototype.invite, email, role);
return result;
}
/**
* Method allows a user to get out from an organization
* The difference between deleteMembership is that membershipId is unknown in this case
* @method leave
* @returns {module:API.cvat.classes.Organization}
* @memberof module:API.cvat.classes.Organization
* @param {module:API.cvat.classes.User} user
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async leave(user) {
const result = await PluginRegistry.apiWrapper.call(this, Organization.prototype.leave, user);
return result;
}
/**
* Method allows to change a membership role
* @method updateMembership
* @returns {module:API.cvat.classes.Organization}
* @param {number} membershipId
* @param {string} role
* @memberof module:API.cvat.classes.Organization
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async updateMembership(membershipId, role) {
const result = await PluginRegistry.apiWrapper.call(
this,
Organization.prototype.updateMembership,
membershipId,
role,
);
return result;
}
/**
* Method allows to kick a user from an organization
* @method deleteMembership
* @returns {module:API.cvat.classes.Organization}
* @param {number} membershipId
* @memberof module:API.cvat.classes.Organization
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async deleteMembership(membershipId) {
const result = await PluginRegistry.apiWrapper.call(
this,
Organization.prototype.deleteMembership,
membershipId,
);
return result;
}
}
Organization.prototype.save.implementation = async function () {
if (typeof this.id === 'number') {
const organizationData = {
name: this.name || this.slug,
description: this.description,
contact: this.contact,
};
const result = await serverProxy.organizations.update(this.id, organizationData);
return new Organization(result);
}
const organizationData = {
slug: this.slug,
name: this.name || this.slug,
description: this.description,
contact: this.contact,
};
const result = await serverProxy.organizations.create(organizationData);
return new Organization(result);
};
Organization.prototype.members.implementation = async function (orgSlug, page, pageSize) {
checkObjectType('orgSlug', orgSlug, 'string');
checkObjectType('page', page, 'number');
checkObjectType('pageSize', pageSize, 'number');
const result = await serverProxy.organizations.members(orgSlug, page, pageSize);
await Promise.all(
result.results.map((membership) => {
const { invitation } = membership;
membership.user = new User(membership.user);
if (invitation) {
return serverProxy.organizations
.invitation(invitation)
.then((invitationData) => {
membership.invitation = invitationData;
})
.catch(() => {
membership.invitation = null;
});
}
return Promise.resolve();
}),
);
result.results.count = result.count;
return result.results;
};
Organization.prototype.remove.implementation = async function () {
if (typeof this.id === 'number') {
await serverProxy.organizations.delete(this.id);
config.organizationID = null;
}
};
Organization.prototype.invite.implementation = async function (email, role) {
checkObjectType('email', email, 'string');
if (!isEnum.bind(MembershipRole)(role)) {
throw new ArgumentError(`Role must be one of: ${Object.values(MembershipRole).toString()}`);
}
if (typeof this.id === 'number') {
await serverProxy.organizations.invite(this.id, { email, role });
}
};
Organization.prototype.updateMembership.implementation = async function (membershipId, role) {
checkObjectType('membershipId', membershipId, 'number');
if (!isEnum.bind(MembershipRole)(role)) {
throw new ArgumentError(`Role must be one of: ${Object.values(MembershipRole).toString()}`);
}
if (typeof this.id === 'number') {
await serverProxy.organizations.updateMembership(membershipId, { role });
}
};
Organization.prototype.deleteMembership.implementation = async function (membershipId) {
checkObjectType('membershipId', membershipId, 'number');
if (typeof this.id === 'number') {
await serverProxy.organizations.deleteMembership(membershipId);
}
};
Organization.prototype.leave.implementation = async function (user) {
checkObjectType('user', user, null, User);
if (typeof this.id === 'number') {
const result = await serverProxy.organizations.members(this.slug, 1, 10, { user: user.id });
const [membership] = result.results;
if (!membership) {
throw new ServerError(`Could not find membership for user ${user.username} in organization ${this.slug}`);
}
await serverProxy.organizations.deleteMembership(membership.id);
}
};
module.exports = Organization;

@ -11,36 +11,36 @@
function implementProject(projectClass) { function implementProject(projectClass) {
projectClass.prototype.save.implementation = async function () { projectClass.prototype.save.implementation = async function () {
const trainingProjectCopy = this.trainingProject;
if (typeof this.id !== 'undefined') { if (typeof this.id !== 'undefined') {
// project has been already created, need to update some data const projectData = this._updateTrigger.getUpdated(this, {
const projectData = { bugTracker: 'bug_tracker',
name: this.name, trainingProject: 'training_project',
assignee_id: this.assignee ? this.assignee.id : null, assignee: 'assignee_id',
bug_tracker: this.bugTracker, });
labels: [...this._internalData.labels.map((el) => el.toJSON())], if (projectData.assignee_id) {
}; projectData.assignee_id = projectData.assignee_id.id;
}
if (trainingProjectCopy) { if (projectData.labels) {
projectData.training_project = trainingProjectCopy; projectData.labels = projectData.labels.map((el) => el.toJSON());
} }
await serverProxy.projects.save(this.id, projectData); await serverProxy.projects.save(this.id, projectData);
this._updateTrigger.reset();
return this; return this;
} }
// initial creating // initial creating
const projectSpec = { const projectSpec = {
name: this.name, name: this.name,
labels: [...this.labels.map((el) => el.toJSON())], labels: this.labels.map((el) => el.toJSON()),
}; };
if (this.bugTracker) { if (this.bugTracker) {
projectSpec.bug_tracker = this.bugTracker; projectSpec.bug_tracker = this.bugTracker;
} }
if (trainingProjectCopy) { if (this.trainingProject) {
projectSpec.training_project = trainingProjectCopy; projectSpec.training_project = this.trainingProject;
} }
const project = await serverProxy.projects.create(projectSpec); const project = await serverProxy.projects.create(projectSpec);

@ -7,6 +7,7 @@
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { Label } = require('./labels'); const { Label } = require('./labels');
const User = require('./user'); const User = require('./user');
const { FieldUpdateTrigger } = require('./common');
/** /**
* Class representing a project * Class representing a project
@ -36,6 +37,8 @@
dimension: undefined, dimension: undefined,
}; };
const updateTrigger = new FieldUpdateTrigger();
for (const property in data) { for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) { if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property]; data[property] = initialData[property];
@ -82,6 +85,7 @@
throw new ArgumentError('Value must not be empty'); throw new ArgumentError('Value must not be empty');
} }
data.name = value; data.name = value;
updateTrigger.update('name');
}, },
}, },
@ -110,6 +114,7 @@
throw new ArgumentError('Value must be a user instance'); throw new ArgumentError('Value must be a user instance');
} }
data.assignee = assignee; data.assignee = assignee;
updateTrigger.update('assignee');
}, },
}, },
/** /**
@ -134,6 +139,7 @@
get: () => data.bug_tracker, get: () => data.bug_tracker,
set: (tracker) => { set: (tracker) => {
data.bug_tracker = tracker; data.bug_tracker = tracker;
updateTrigger.update('bugTracker');
}, },
}, },
/** /**
@ -195,6 +201,7 @@
}); });
data.labels = [...deletedLabels, ...labels]; data.labels = [...deletedLabels, ...labels];
updateTrigger.update('labels');
}, },
}, },
/** /**
@ -231,11 +238,15 @@
} else { } else {
data.training_project = updatedProject; data.training_project = updatedProject;
} }
updateTrigger.update('trainingProject');
}, },
}, },
_internalData: { _internalData: {
get: () => data, get: () => data,
}, },
_updateTrigger: {
get: () => updateTrigger,
},
}), }),
); );

@ -1,405 +0,0 @@
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
const store = require('store');
const PluginRegistry = require('./plugins');
const Issue = require('./issue');
const User = require('./user');
const { ArgumentError, DataError } = require('./exceptions');
const { ReviewStatus } = require('./enums');
const { negativeIDGenerator } = require('./common');
const serverProxy = require('./server-proxy');
/**
* Class representing a single review
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Review {
constructor(initialData) {
const data = {
id: undefined,
job: undefined,
issue_set: [],
estimated_quality: undefined,
status: undefined,
reviewer: undefined,
assignee: undefined,
reviewed_frames: undefined,
reviewed_states: undefined,
};
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property];
}
}
if (data.reviewer && !(data.reviewer instanceof User)) data.reviewer = new User(data.reviewer);
if (data.assignee && !(data.assignee instanceof User)) data.assignee = new User(data.assignee);
data.reviewed_frames = Array.isArray(data.reviewed_frames) ? new Set(data.reviewed_frames) : new Set();
data.reviewed_states = Array.isArray(data.reviewed_states) ? new Set(data.reviewed_states) : new Set();
if (data.issue_set) {
data.issue_set = data.issue_set.map((issue) => new Issue(issue));
}
if (typeof data.id === 'undefined') {
data.id = negativeIDGenerator();
}
Object.defineProperties(
this,
Object.freeze({
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
*/
id: {
get: () => data.id,
},
/**
* An identifier of a job the review is attached to
* @name job
* @type {integer}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
*/
job: {
get: () => data.job,
},
/**
* List of attached issues
* @name issues
* @type {number[]}
* @memberof module:API.cvat.classes.Review
* @instance
* @readonly
*/
issues: {
get: () => data.issue_set.filter((issue) => !issue.removed),
},
/**
* Estimated quality of the review
* @name estimatedQuality
* @type {number}
* @memberof module:API.cvat.classes.Review
* @instance
* @readonly
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
estimatedQuality: {
get: () => data.estimated_quality,
set: (value) => {
if (typeof value !== 'number' || value < 0 || value > 5) {
throw new ArgumentError(`Value must be a number in range [0, 5]. Got ${value}`);
}
data.estimated_quality = value;
},
},
/**
* @name status
* @type {module:API.cvat.enums.ReviewStatus}
* @memberof module:API.cvat.classes.Review
* @instance
* @readonly
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
status: {
get: () => data.status,
set: (status) => {
const type = ReviewStatus;
let valueInEnum = false;
for (const value in type) {
if (type[value] === status) {
valueInEnum = true;
break;
}
}
if (!valueInEnum) {
throw new ArgumentError(
'Value must be a value from the enumeration cvat.enums.ReviewStatus',
);
}
data.status = status;
},
},
/**
* An instance of a user who has done the review
* @name reviewer
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
reviewer: {
get: () => data.reviewer,
set: (reviewer) => {
if (!(reviewer instanceof User)) {
throw new ArgumentError(`Reviewer must be an instance of the User class. Got ${reviewer}`);
}
data.reviewer = reviewer;
},
},
/**
* An instance of a user who was assigned for annotation before the review
* @name assignee
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
*/
assignee: {
get: () => data.assignee,
},
/**
* A set of frames that have been visited during review
* @name reviewedFrames
* @type {number[]}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
*/
reviewedFrames: {
get: () => Array.from(data.reviewed_frames),
},
/**
* A set of reviewed states (server IDs combined with frames)
* @name reviewedFrames
* @type {string[]}
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
*/
reviewedStates: {
get: () => Array.from(data.reviewed_states),
},
__internal: {
get: () => data,
},
}),
);
}
/**
* Method appends a frame to a set of reviewed frames
* Reviewed frames are saved only in local storage
* @method reviewFrame
* @memberof module:API.cvat.classes.Review
* @param {number} frame
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async reviewFrame(frame) {
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.reviewFrame, frame);
return result;
}
/**
* Method appends a frame to a set of reviewed frames
* Reviewed states are saved only in local storage. They are used to automatic annotations quality assessment
* @method reviewStates
* @memberof module:API.cvat.classes.Review
* @param {string[]} stateIDs
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async reviewStates(stateIDs) {
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.reviewStates, stateIDs);
return result;
}
/**
* @typedef {Object} IssueData
* @property {number} frame
* @property {number[]} position
* @property {number} owner
* @property {CommentData[]} comment_set
* @global
*/
/**
* Method adds a new issue to the review
* @method openIssue
* @memberof module:API.cvat.classes.Review
* @param {IssueData} data
* @returns {module:API.cvat.classes.Issue}
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async openIssue(data) {
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.openIssue, data);
return result;
}
async deleteIssue(issueId) {
await PluginRegistry.apiWrapper.call(this, Review.prototype.deleteIssue, issueId);
}
/**
* Method submits local review to the server
* @method submit
* @memberof module:API.cvat.classes.Review
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.DataError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async submit() {
const result = await PluginRegistry.apiWrapper.call(this, Review.prototype.submit);
return result;
}
serialize() {
const { issues, reviewedFrames, reviewedStates } = this;
const data = {
job: this.job,
issue_set: issues.map((issue) => issue.serialize()),
reviewed_frames: Array.from(reviewedFrames),
reviewed_states: Array.from(reviewedStates),
};
if (this.id > 0) {
data.id = this.id;
}
if (typeof this.estimatedQuality !== 'undefined') {
data.estimated_quality = this.estimatedQuality;
}
if (typeof this.status !== 'undefined') {
data.status = this.status;
}
if (this.reviewer) {
data.reviewer = this.reviewer.toJSON();
}
if (this.assignee) {
data.reviewer = this.assignee.toJSON();
}
return data;
}
toJSON() {
const data = this.serialize();
const {
reviewer,
assignee,
reviewed_frames: reviewedFrames,
reviewed_states: reviewedStates,
...updated
} = data;
return {
...updated,
issue_set: this.issues.map((issue) => issue.toJSON()),
reviewer_id: reviewer ? reviewer.id : undefined,
assignee_id: assignee ? assignee.id : undefined,
};
}
async toLocalStorage() {
const data = this.serialize();
store.set(`job-${this.job}-review`, JSON.stringify(data));
}
}
Review.prototype.reviewFrame.implementation = function (frame) {
if (!Number.isInteger(frame)) {
throw new ArgumentError(`The argument "frame" is expected to be an integer. Got ${frame}`);
}
this.__internal.reviewed_frames.add(frame);
};
Review.prototype.reviewStates.implementation = function (stateIDs) {
if (!Array.isArray(stateIDs) || stateIDs.some((stateID) => typeof stateID !== 'string')) {
throw new ArgumentError(`The argument "stateIDs" is expected to be an array of string. Got ${stateIDs}`);
}
stateIDs.forEach((stateID) => this.__internal.reviewed_states.add(stateID));
};
Review.prototype.openIssue.implementation = async function (data) {
if (typeof data !== 'object' || data === null) {
throw new ArgumentError(`The argument "data" must be a not null object. Got ${data}`);
}
if (typeof data.frame !== 'number') {
throw new ArgumentError(`Issue frame must be a number. Got ${data.frame}`);
}
if (!(data.owner instanceof User)) {
throw new ArgumentError(`Issue owner must be a User instance. Got ${data.owner}`);
}
if (!Array.isArray(data.position) || data.position.some((coord) => typeof coord !== 'number')) {
throw new ArgumentError(`Issue position must be an array of numbers. Got ${data.position}`);
}
if (!Array.isArray(data.comment_set)) {
throw new ArgumentError(`Issue comment set must be an array. Got ${data.comment_set}`);
}
const copied = {
frame: data.frame,
position: Issue.hull(data.position),
owner: data.owner,
comment_set: [],
};
const issue = new Issue(copied);
for (const comment of data.comment_set) {
await issue.comment.implementation.call(issue, comment);
}
this.__internal.issue_set.push(issue);
return issue;
};
Review.prototype.submit.implementation = async function () {
if (typeof this.estimatedQuality === 'undefined') {
throw new DataError('Estimated quality is expected to be a number. Got "undefined"');
}
if (typeof this.status === 'undefined') {
throw new DataError('Review status is expected to be a string. Got "undefined"');
}
if (this.id < 0) {
const data = this.toJSON();
const response = await serverProxy.jobs.reviews.create(data);
store.remove(`job-${this.job}-review`);
this.__internal.id = response.id;
this.__internal.issue_set = response.issue_set.map((issue) => new Issue(issue));
this.__internal.estimated_quality = response.estimated_quality;
this.__internal.status = response.status;
if (response.reviewer) this.__internal.reviewer = new User(response.reviewer);
if (response.assignee) this.__internal.assignee = new User(response.assignee);
}
};
Review.prototype.deleteIssue.implementation = function (issueId) {
this.__internal.issue_set = this.__internal.issue_set.filter((issue) => issue.id !== issueId);
};
module.exports = Review;

@ -8,8 +8,18 @@
const store = require('store'); const store = require('store');
const config = require('./config'); const config = require('./config');
const DownloadWorker = require('./download.worker'); const DownloadWorker = require('./download.worker');
const Axios = require('axios');
const tus = require('tus-js-client'); const tus = require('tus-js-client');
function enableOrganization() {
return { org: config.organizationID || '' };
}
function removeToken() {
Axios.defaults.headers.common.Authorization = '';
store.remove('token');
}
function waitFor(frequencyHz, predicate) { function waitFor(frequencyHz, predicate) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
if (typeof predicate !== 'function') { if (typeof predicate !== 'function') {
@ -61,8 +71,8 @@
} }
class WorkerWrappedAxios { class WorkerWrappedAxios {
constructor() { constructor(requestInterseptor) {
const worker = new DownloadWorker(); const worker = new DownloadWorker(requestInterseptor);
const requests = {}; const requests = {};
let requestId = 0; let requestId = 0;
@ -123,11 +133,18 @@
class ServerProxy { class ServerProxy {
constructor() { constructor() {
const Axios = require('axios');
Axios.defaults.withCredentials = true; Axios.defaults.withCredentials = true;
Axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN'; Axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN';
Axios.defaults.xsrfCookieName = 'csrftoken'; Axios.defaults.xsrfCookieName = 'csrftoken';
const workerAxios = new WorkerWrappedAxios(); const workerAxios = new WorkerWrappedAxios();
Axios.interceptors.request.use((reqConfig) => {
if ('params' in reqConfig && 'org' in reqConfig.params) {
return reqConfig;
}
reqConfig.params = { ...enableOrganization(), ...(reqConfig.params || {}) };
return reqConfig;
});
let token = store.get('token'); let token = store.get('token');
if (token) { if (token) {
@ -151,12 +168,13 @@
async function share(directoryArg) { async function share(directoryArg) {
const { backendAPI } = config; const { backendAPI } = config;
const directory = encodeURIComponent(directoryArg); const directory = encodeURI(directoryArg);
let response = null; let response = null;
try { try {
response = await Axios.get(`${backendAPI}/server/share?directory=${directory}`, { response = await Axios.get(`${backendAPI}/server/share`, {
proxy: config.proxy, proxy: config.proxy,
params: { directory },
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
@ -242,7 +260,7 @@
.join('&') .join('&')
.replace(/%20/g, '+'); .replace(/%20/g, '+');
Axios.defaults.headers.common.Authorization = ''; removeToken();
let authenticationResponse = null; let authenticationResponse = null;
try { try {
authenticationResponse = await Axios.post(`${config.backendAPI}/auth/login`, authenticationData, { authenticationResponse = await Axios.post(`${config.backendAPI}/auth/login`, authenticationData, {
@ -269,12 +287,10 @@
await Axios.post(`${config.backendAPI}/auth/logout`, { await Axios.post(`${config.backendAPI}/auth/logout`, {
proxy: config.proxy, proxy: config.proxy,
}); });
removeToken();
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
} }
store.remove('token');
Axios.defaults.headers.common.Authorization = '';
} }
async function changePassword(oldPassword, newPassword1, newPassword2) { async function changePassword(oldPassword, newPassword1, newPassword2) {
@ -335,6 +351,7 @@
await module.exports.users.self(); await module.exports.users.self();
} catch (serverError) { } catch (serverError) {
if (serverError.code === 401) { if (serverError.code === 401) {
removeToken();
return false; return false;
} }
@ -362,12 +379,15 @@
let response = null; let response = null;
try { try {
response = await Axios.get( response = await Axios.get(`${backendAPI}/projects`, {
`${backendAPI}/projects?names_only=true&page=1&page_size=${limit}&search=${search}`,
{
proxy, proxy,
params: {
names_only: true,
page: 1,
page_size: limit,
search,
}, },
); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
} }
@ -376,12 +396,25 @@
return response.data.results; return response.data.results;
} }
async function getProjects(filter = '') { async function getProjects(filter = {}) {
const { backendAPI, proxy } = config; const { backendAPI, proxy } = config;
let response = null; let response = null;
try { try {
response = await Axios.get(`${backendAPI}/projects?page_size=12&${filter}`, { if ('id' in filter) {
response = await Axios.get(`${backendAPI}/projects/${filter.id}`, {
proxy,
});
const results = [response.data];
results.count = 1;
return results;
}
response = await Axios.get(`${backendAPI}/projects`, {
params: {
...filter,
page_size: 12,
},
proxy, proxy,
}); });
} catch (errorData) { } catch (errorData) {
@ -411,7 +444,9 @@
const { backendAPI } = config; const { backendAPI } = config;
try { try {
await Axios.delete(`${backendAPI}/projects/${id}`); await Axios.delete(`${backendAPI}/projects/${id}`, {
proxy: config.proxy,
});
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
} }
@ -433,12 +468,25 @@
} }
} }
async function getTasks(filter = '') { async function getTasks(filter = {}) {
const { backendAPI } = config; const { backendAPI } = config;
let response = null; let response = null;
try { try {
response = await Axios.get(`${backendAPI}/tasks?page_size=10&${filter}`, { if ('id' in filter) {
response = await Axios.get(`${backendAPI}/tasks/${filter.id}`, {
proxy: config.proxy,
});
const results = [response.data];
results.count = 1;
return results;
}
response = await Axios.get(`${backendAPI}/tasks`, {
params: {
...filter,
page_size: 10,
},
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
@ -452,8 +500,9 @@
async function saveTask(id, taskData) { async function saveTask(id, taskData) {
const { backendAPI } = config; const { backendAPI } = config;
let response = null;
try { try {
await Axios.patch(`${backendAPI}/tasks/${id}`, JSON.stringify(taskData), { response = await Axios.patch(`${backendAPI}/tasks/${id}`, JSON.stringify(taskData), {
proxy: config.proxy, proxy: config.proxy,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -462,13 +511,16 @@
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
} }
return response.data;
} }
async function deleteTask(id) { async function deleteTask(id, organizationID = null) {
const { backendAPI } = config; const { backendAPI } = config;
try { try {
await Axios.delete(`${backendAPI}/tasks/${id}`, { await Axios.delete(`${backendAPI}/tasks/${id}`, {
...(organizationID ? { org: organizationID } : {}),
proxy: config.proxy, proxy: config.proxy,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -483,25 +535,27 @@
return async function (id, format, name, saveImages) { return async function (id, format, name, saveImages) {
const { backendAPI } = config; const { backendAPI } = config;
const baseURL = `${backendAPI}/${instanceType}/${id}/${saveImages ? 'dataset' : 'annotations'}`; const baseURL = `${backendAPI}/${instanceType}/${id}/${saveImages ? 'dataset' : 'annotations'}`;
let query = `format=${encodeURIComponent(format)}`; const params = {
...enableOrganization(),
format,
};
if (name) { if (name) {
const filename = name.replace(/\//g, '_'); params.filename = name.replace(/\//g, '_');
query += `&filename=${encodeURIComponent(filename)}`;
} }
let url = `${baseURL}?${query}`;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
async function request() { async function request() {
Axios.get(`${url}`, { Axios.get(baseURL, {
proxy: config.proxy, proxy: config.proxy,
params,
}) })
.then((response) => { .then((response) => {
if (response.status === 202) { if (response.status === 202) {
setTimeout(request, 3000); setTimeout(request, 3000);
} else { } else {
query = `${query}&action=download`; params.action = 'download';
url = `${baseURL}?${query}`; resolve(`${baseURL}?${new URLSearchParams(params).toString()}`);
resolve(url);
} }
}) })
.catch((errorData) => { .catch((errorData) => {
@ -554,6 +608,9 @@
async function exportTask(id) { async function exportTask(id) {
const { backendAPI } = config; const { backendAPI } = config;
const params = {
...enableOrganization(),
};
const url = `${backendAPI}/tasks/${id}/backup`; const url = `${backendAPI}/tasks/${id}/backup`;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -561,11 +618,13 @@
try { try {
const response = await Axios.get(url, { const response = await Axios.get(url, {
proxy: config.proxy, proxy: config.proxy,
params,
}); });
if (response.status === 202) { if (response.status === 202) {
setTimeout(request, 3000); setTimeout(request, 3000);
} else { } else {
resolve(`${url}?action=download`); params.action = 'download';
resolve(`${url}?${new URLSearchParams(params).toString()}`);
} }
} catch (errorData) { } catch (errorData) {
reject(generateError(errorData)); reject(generateError(errorData));
@ -578,6 +637,8 @@
async function importTask(file) { async function importTask(file) {
const { backendAPI } = config; const { backendAPI } = config;
// keep current default params to 'freeze" them during this request
const params = enableOrganization();
let taskData = new FormData(); let taskData = new FormData();
taskData.append('task_file', file); taskData.append('task_file', file);
@ -587,13 +648,15 @@
try { try {
const response = await Axios.post(`${backendAPI}/tasks/backup`, taskData, { const response = await Axios.post(`${backendAPI}/tasks/backup`, taskData, {
proxy: config.proxy, proxy: config.proxy,
params,
}); });
if (response.status === 202) { if (response.status === 202) {
taskData = new FormData(); taskData = new FormData();
taskData.append('rq_id', response.data.rq_id); taskData.append('rq_id', response.data.rq_id);
setTimeout(request, 3000); setTimeout(request, 3000);
} else { } else {
const importedTask = await getTasks(`?id=${response.data.id}`); // to be able to get the task after it was created, pass frozen params
const importedTask = await getTasks({ id: response.data.id, ...params });
resolve(importedTask[0]); resolve(importedTask[0]);
} }
} catch (errorData) { } catch (errorData) {
@ -607,6 +670,8 @@
async function backupProject(id) { async function backupProject(id) {
const { backendAPI } = config; const { backendAPI } = config;
// keep current default params to 'freeze" them during this request
const params = enableOrganization();
const url = `${backendAPI}/projects/${id}/backup`; const url = `${backendAPI}/projects/${id}/backup`;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
@ -614,11 +679,13 @@
try { try {
const response = await Axios.get(url, { const response = await Axios.get(url, {
proxy: config.proxy, proxy: config.proxy,
params,
}); });
if (response.status === 202) { if (response.status === 202) {
setTimeout(request, 3000); setTimeout(request, 3000);
} else { } else {
resolve(`${url}?action=download`); params.action = 'download';
resolve(`${url}?${new URLSearchParams(params).toString()}`);
} }
} catch (errorData) { } catch (errorData) {
reject(generateError(errorData)); reject(generateError(errorData));
@ -631,6 +698,8 @@
async function restoreProject(file) { async function restoreProject(file) {
const { backendAPI } = config; const { backendAPI } = config;
// keep current default params to 'freeze" them during this request
const params = enableOrganization();
let data = new FormData(); let data = new FormData();
data.append('project_file', file); data.append('project_file', file);
@ -640,13 +709,15 @@
try { try {
const response = await Axios.post(`${backendAPI}/projects/backup`, data, { const response = await Axios.post(`${backendAPI}/projects/backup`, data, {
proxy: config.proxy, proxy: config.proxy,
params,
}); });
if (response.status === 202) { if (response.status === 202) {
data = new FormData(); data = new FormData();
data.append('rq_id', response.data.rq_id); data.append('rq_id', response.data.rq_id);
setTimeout(request, 3000); setTimeout(request, 3000);
} else { } else {
const restoredProject = await getProjects(`?id=${response.data.id}`); // to be able to get the task after it was created, pass frozen params
const restoredProject = await getProjects({ id: response.data.id, ...params });
resolve(restoredProject[0]); resolve(restoredProject[0]);
} }
} catch (errorData) { } catch (errorData) {
@ -660,12 +731,14 @@
async function createTask(taskSpec, taskDataSpec, onUpdate) { async function createTask(taskSpec, taskDataSpec, onUpdate) {
const { backendAPI, origin } = config; const { backendAPI, origin } = config;
// keep current default params to 'freeze" them during this request
const params = enableOrganization();
async function wait(id) { async function wait(id) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
async function checkStatus() { async function checkStatus() {
try { try {
const response = await Axios.get(`${backendAPI}/tasks/${id}/status`); const response = await Axios.get(`${backendAPI}/tasks/${id}/status`, { params });
if (['Queued', 'Started'].includes(response.data.state)) { if (['Queued', 'Started'].includes(response.data.state)) {
if (response.data.message !== '') { if (response.data.message !== '') {
onUpdate(response.data.message, response.data.progress || 0); onUpdate(response.data.message, response.data.progress || 0);
@ -732,6 +805,7 @@
try { try {
response = await Axios.post(`${backendAPI}/tasks`, JSON.stringify(taskSpec), { response = await Axios.post(`${backendAPI}/tasks`, JSON.stringify(taskSpec), {
proxy: config.proxy, proxy: config.proxy,
params,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -760,6 +834,8 @@
}, },
onBeforeRequest(req) { onBeforeRequest(req) {
const xhr = req.getUnderlyingObject(); const xhr = req.getUnderlyingObject();
const { org } = params;
req.setHeader('X-Organization', org);
xhr.withCredentials = true; xhr.withCredentials = true;
}, },
onProgress(bytesUploaded) { onProgress(bytesUploaded) {
@ -796,6 +872,7 @@
onUpdate(`The data are being uploaded to the server onUpdate(`The data are being uploaded to the server
${((totalSentSize / totalSize) * 100).toFixed(2)}%`); ${((totalSentSize / totalSize) * 100).toFixed(2)}%`);
await Axios.post(`${backendAPI}/tasks/${taskId}/data`, taskData, { await Axios.post(`${backendAPI}/tasks/${taskId}/data`, taskData, {
...params,
proxy: config.proxy, proxy: config.proxy,
headers: { 'Upload-Multiple': true }, headers: { 'Upload-Multiple': true },
}); });
@ -810,6 +887,7 @@
try { try {
await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`, await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`,
taskData, { taskData, {
...params,
proxy: config.proxy, proxy: config.proxy,
headers: { 'Upload-Start': true }, headers: { 'Upload-Start': true },
}); });
@ -821,12 +899,13 @@
} }
await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`, await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`,
taskData, { taskData, {
...params,
proxy: config.proxy, proxy: config.proxy,
headers: { 'Upload-Finish': true }, headers: { 'Upload-Finish': true },
}); });
} catch (errorData) { } catch (errorData) {
try { try {
await deleteTask(response.data.id); await deleteTask(response.data.id, params.org || null);
} catch (_) { } catch (_) {
// ignore // ignore
} }
@ -836,11 +915,12 @@
try { try {
await wait(response.data.id); await wait(response.data.id);
} catch (createException) { } catch (createException) {
await deleteTask(response.data.id); await deleteTask(response.data.id, params.org || null);
throw createException; throw createException;
} }
const createdTask = await getTasks(`?id=${response.id}`); // to be able to get the task after it was created, pass frozen params
const createdTask = await getTasks({ id: response.data.id, ...params });
return createdTask[0]; return createdTask[0];
} }
@ -859,12 +939,12 @@
return response.data; return response.data;
} }
async function getJobReviews(jobID) { async function getJobIssues(jobID) {
const { backendAPI } = config; const { backendAPI } = config;
let response = null; let response = null;
try { try {
response = await Axios.get(`${backendAPI}/jobs/${jobID}/reviews`, { response = await Axios.get(`${backendAPI}/jobs/${jobID}/issues`, {
proxy: config.proxy, proxy: config.proxy,
}); });
} catch (errorData) { } catch (errorData) {
@ -874,12 +954,12 @@
return response.data; return response.data;
} }
async function createReview(data) { async function createComment(data) {
const { backendAPI } = config; const { backendAPI } = config;
let response = null; let response = null;
try { try {
response = await Axios.post(`${backendAPI}/reviews`, JSON.stringify(data), { response = await Axios.post(`${backendAPI}/comments`, JSON.stringify(data), {
proxy: config.proxy, proxy: config.proxy,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -892,27 +972,12 @@
return response.data; return response.data;
} }
async function getJobIssues(jobID) { async function createIssue(data) {
const { backendAPI } = config; const { backendAPI } = config;
let response = null; let response = null;
try { try {
response = await Axios.get(`${backendAPI}/jobs/${jobID}/issues`, { response = await Axios.post(`${backendAPI}/issues`, JSON.stringify(data), {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function createComment(data) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.post(`${backendAPI}/comments`, JSON.stringify(data), {
proxy: config.proxy, proxy: config.proxy,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -956,8 +1021,9 @@
async function saveJob(id, jobData) { async function saveJob(id, jobData) {
const { backendAPI } = config; const { backendAPI } = config;
let response = null;
try { try {
await Axios.patch(`${backendAPI}/jobs/${id}`, JSON.stringify(jobData), { response = await Axios.patch(`${backendAPI}/jobs/${id}`, JSON.stringify(jobData), {
proxy: config.proxy, proxy: config.proxy,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
@ -966,15 +1032,20 @@
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
} }
return response.data;
} }
async function getUsers(filter = 'page_size=all') { async function getUsers(filter = { page_size: 'all' }) {
const { backendAPI } = config; const { backendAPI } = config;
let response = null; let response = null;
try { try {
response = await Axios.get(`${backendAPI}/users?${filter}`, { response = await Axios.get(`${backendAPI}/users`, {
proxy: config.proxy, proxy: config.proxy,
params: {
...filter,
},
}); });
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
@ -1003,7 +1074,10 @@
let response = null; let response = null;
try { try {
response = await Axios.get(`${backendAPI}/tasks/${tid}/data?type=preview`, { response = await Axios.get(`${backendAPI}/tasks/${tid}/data`, {
params: {
type: 'preview',
},
proxy: config.proxy, proxy: config.proxy,
responseType: 'blob', responseType: 'blob',
}); });
@ -1015,18 +1089,20 @@
return response.data; return response.data;
} }
async function getImageContext(tid, frame) { async function getImageContext(jid, frame) {
const { backendAPI } = config; const { backendAPI } = config;
let response = null; let response = null;
try { try {
response = await Axios.get( response = await Axios.get(`${backendAPI}/jobs/${jid}/data`, {
`${backendAPI}/tasks/${tid}/data?quality=original&type=context_image&number=${frame}`, params: {
{ quality: 'original',
type: 'context_image',
number: frame,
},
proxy: config.proxy, proxy: config.proxy,
responseType: 'blob', responseType: 'blob',
}, });
);
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
} }
@ -1034,18 +1110,23 @@
return response.data; return response.data;
} }
async function getData(tid, chunk) { async function getData(tid, jid, chunk) {
const { backendAPI } = config; const { backendAPI } = config;
const url = jid === null ? `tasks/${tid}/data` : `jobs/${jid}/data`;
let response = null; let response = null;
try { try {
response = await workerAxios.get( response = await workerAxios.get(`${backendAPI}/${url}`, {
`${backendAPI}/tasks/${tid}/data?type=chunk&number=${chunk}&quality=compressed`, params: {
{ ...enableOrganization(),
quality: 'compressed',
type: 'chunk',
number: chunk,
},
proxy: config.proxy, proxy: config.proxy,
responseType: 'arraybuffer', responseType: 'arraybuffer',
}, });
);
} catch (errorData) { } catch (errorData) {
throw generateError({ throw generateError({
message: '', message: '',
@ -1093,20 +1174,22 @@
// Session is 'task' or 'job' // Session is 'task' or 'job'
async function updateAnnotations(session, id, data, action) { async function updateAnnotations(session, id, data, action) {
const { backendAPI } = config; const { backendAPI } = config;
const url = `${backendAPI}/${session}s/${id}/annotations`;
const params = {};
let requestFunc = null; let requestFunc = null;
let url = null;
if (action.toUpperCase() === 'PUT') { if (action.toUpperCase() === 'PUT') {
requestFunc = Axios.put.bind(Axios); requestFunc = Axios.put.bind(Axios);
url = `${backendAPI}/${session}s/${id}/annotations`;
} else { } else {
requestFunc = Axios.patch.bind(Axios); requestFunc = Axios.patch.bind(Axios);
url = `${backendAPI}/${session}s/${id}/annotations?action=${action}`; params.action = action;
} }
let response = null; let response = null;
try { try {
response = await requestFunc(url, JSON.stringify(data), { response = await requestFunc(url, JSON.stringify(data), {
proxy: config.proxy, proxy: config.proxy,
params,
headers: { headers: {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
}, },
@ -1121,7 +1204,10 @@
// Session is 'task' or 'job' // Session is 'task' or 'job'
async function uploadAnnotations(session, id, file, format) { async function uploadAnnotations(session, id, file, format) {
const { backendAPI } = config; const { backendAPI } = config;
const params = {
...enableOrganization(),
format,
};
let annotationData = new FormData(); let annotationData = new FormData();
annotationData.append('annotation_file', file); annotationData.append('annotation_file', file);
@ -1129,9 +1215,10 @@
async function request() { async function request() {
try { try {
const response = await Axios.put( const response = await Axios.put(
`${backendAPI}/${session}s/${id}/annotations?format=${format}`, `${backendAPI}/${session}s/${id}/annotations`,
annotationData, annotationData,
{ {
params,
proxy: config.proxy, proxy: config.proxy,
}, },
); );
@ -1154,25 +1241,25 @@
async function dumpAnnotations(id, name, format) { async function dumpAnnotations(id, name, format) {
const { backendAPI } = config; const { backendAPI } = config;
const baseURL = `${backendAPI}/tasks/${id}/annotations`; const baseURL = `${backendAPI}/tasks/${id}/annotations`;
let query = `format=${encodeURIComponent(format)}`; const params = enableOrganization();
params.format = encodeURIComponent(format);
if (name) { if (name) {
const filename = name.replace(/\//g, '_'); const filename = name.replace(/\//g, '_');
query += `&filename=${encodeURIComponent(filename)}`; params.filename = encodeURIComponent(filename);
} }
let url = `${baseURL}?${query}`;
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
async function request() { async function request() {
Axios.get(`${url}`, { Axios.get(baseURL, {
proxy: config.proxy, proxy: config.proxy,
params,
}) })
.then((response) => { .then((response) => {
if (response.status === 202) { if (response.status === 202) {
setTimeout(request, 3000); setTimeout(request, 3000);
} else { } else {
query = `${query}&action=download`; params.action = 'download';
url = `${baseURL}?${query}`; resolve(`${baseURL}?${new URLSearchParams(params).toString()}`);
resolve(url);
} }
}) })
.catch((errorData) => { .catch((errorData) => {
@ -1291,7 +1378,11 @@
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
async function request() { async function request() {
try { try {
const response = await Axios.get(`${backendAPI}/predict/status?project=${projectId}`); const response = await Axios.get(`${backendAPI}/predict/status`, {
params: {
project: projectId,
},
});
return response.data; return response.data;
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
@ -1324,9 +1415,12 @@
async function request() { async function request() {
try { try {
const response = await Axios.get( const response = await Axios.get(`${backendAPI}/predict/frame`, {
`${backendAPI}/predict/frame?task=${taskId}&frame=${frame}`, params: {
); task: taskId,
frame,
},
});
return response.data; return response.data;
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
@ -1456,6 +1550,7 @@
try { try {
const url = `${backendAPI}/cloudstorages/${id}/preview`; const url = `${backendAPI}/cloudstorages/${id}/preview`;
response = await workerAxios.get(url, { response = await workerAxios.get(url, {
params: enableOrganization(),
proxy: config.proxy, proxy: config.proxy,
responseType: 'arraybuffer', responseType: 'arraybuffer',
}); });
@ -1492,12 +1587,166 @@
const { backendAPI } = config; const { backendAPI } = config;
try { try {
await Axios.delete(`${backendAPI}/cloudstorages/${id}`); await Axios.delete(`${backendAPI}/cloudstorages/${id}`, {
proxy: config.proxy,
});
} catch (errorData) { } catch (errorData) {
throw generateError(errorData); throw generateError(errorData);
} }
} }
async function getOrganizations() {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/organizations`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function createOrganization(data) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.post(`${backendAPI}/organizations`, JSON.stringify(data), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function updateOrganization(id, data) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.patch(`${backendAPI}/organizations/${id}`, JSON.stringify(data), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function deleteOrganization(id) {
const { backendAPI } = config;
try {
await Axios.delete(`${backendAPI}/organizations/${id}`, {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
}
async function getOrganizationMembers(orgSlug, page, pageSize, filters = {}) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/memberships`, {
proxy: config.proxy,
params: {
...filters,
org: orgSlug,
page,
page_size: pageSize,
},
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function inviteOrganizationMembers(orgId, data) {
const { backendAPI } = config;
try {
await Axios.post(
`${backendAPI}/invitations`,
{
...data,
organization: orgId,
},
{
proxy: config.proxy,
},
);
} catch (errorData) {
throw generateError(errorData);
}
}
async function updateOrganizationMembership(membershipId, data) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.patch(
`${backendAPI}/memberships/${membershipId}`,
{
...data,
},
{
proxy: config.proxy,
},
);
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function deleteOrganizationMembership(membershipId) {
const { backendAPI } = config;
try {
await Axios.delete(`${backendAPI}/memberships/${membershipId}`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
}
async function getMembershipInvitation(id) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/invitations/${id}`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
Object.defineProperties( Object.defineProperties(
this, this,
Object.freeze({ Object.freeze({
@ -1538,13 +1787,13 @@
tasks: { tasks: {
value: Object.freeze({ value: Object.freeze({
getTasks, get: getTasks,
saveTask, save: saveTask,
createTask, create: createTask,
deleteTask, delete: deleteTask,
exportDataset: exportDataset('tasks'), exportDataset: exportDataset('tasks'),
exportTask, export: exportTask,
importTask, import: importTask,
}), }),
writable: false, writable: false,
}, },
@ -1553,11 +1802,6 @@
value: Object.freeze({ value: Object.freeze({
get: getJob, get: getJob,
save: saveJob, save: saveJob,
issues: getJobIssues,
reviews: {
get: getJobReviews,
create: createReview,
},
}), }),
writable: false, writable: false,
}, },
@ -1611,7 +1855,9 @@
issues: { issues: {
value: Object.freeze({ value: Object.freeze({
create: createIssue,
update: updateIssue, update: updateIssue,
get: getJobIssues,
delete: deleteIssue, delete: deleteIssue,
}), }),
writable: false, writable: false,
@ -1644,6 +1890,21 @@
}), }),
writable: false, writable: false,
}, },
organizations: {
value: Object.freeze({
get: getOrganizations,
create: createOrganization,
update: updateOrganization,
members: getOrganizationMembers,
invitation: getMembershipInvitation,
delete: deleteOrganization,
invite: inviteOrganizationMembers,
updateMembership: updateOrganizationMembership,
deleteMembership: deleteOrganizationMembership,
}),
writable: false,
},
}), }),
); );
} }

@ -3,7 +3,6 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
(() => { (() => {
const store = require('store');
const PluginRegistry = require('./plugins'); const PluginRegistry = require('./plugins');
const loggerStorage = require('./logger-storage'); const loggerStorage = require('./logger-storage');
const serverProxy = require('./server-proxy'); const serverProxy = require('./server-proxy');
@ -11,12 +10,11 @@
getFrame, getRanges, getPreview, clear: clearFrames, getContextImage, getFrame, getRanges, getPreview, clear: clearFrames, getContextImage,
} = require('./frames'); } = require('./frames');
const { ArgumentError, DataError } = require('./exceptions'); const { ArgumentError, DataError } = require('./exceptions');
const { TaskStatus } = require('./enums'); const { JobStage, JobState } = require('./enums');
const { Label } = require('./labels'); const { Label } = require('./labels');
const User = require('./user'); const User = require('./user');
const Issue = require('./issue'); const Issue = require('./issue');
const Review = require('./review'); const { FieldUpdateTrigger, checkObjectType } = require('./common');
const { FieldUpdateTrigger } = require('./common');
function buildDuplicatedAPI(prototype) { function buildDuplicatedAPI(prototype) {
Object.defineProperties(prototype, { Object.defineProperties(prototype, {
@ -180,11 +178,10 @@
const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.preview); const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.preview);
return result; return result;
}, },
async contextImage(taskId, frameId) { async contextImage(frameId) {
const result = await PluginRegistry.apiWrapper.call( const result = await PluginRegistry.apiWrapper.call(
this, this,
prototype.frames.contextImage, prototype.frames.contextImage,
taskId,
frameId, frameId,
); );
return result; return result;
@ -709,18 +706,21 @@
const data = { const data = {
id: undefined, id: undefined,
assignee: null, assignee: null,
reviewer: null, stage: undefined,
status: undefined, state: undefined,
start_frame: undefined, start_frame: undefined,
stop_frame: undefined, stop_frame: undefined,
task: undefined, project_id: null,
task_id: undefined,
labels: undefined,
dimension: undefined,
data_compressed_chunk_type: undefined,
data_chunk_size: undefined,
bug_tracker: null,
mode: undefined,
}; };
const updatedFields = new FieldUpdateTrigger({ const updateTrigger = new FieldUpdateTrigger();
assignee: false,
reviewer: false,
status: false,
});
for (const property in data) { for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property)) { if (Object.prototype.hasOwnProperty.call(data, property)) {
@ -735,7 +735,19 @@
} }
if (data.assignee) data.assignee = new User(data.assignee); if (data.assignee) data.assignee = new User(data.assignee);
if (data.reviewer) data.reviewer = new User(data.reviewer); if (Array.isArray(initialData.labels)) {
data.labels = initialData.labels.map((labelData) => {
// can be already wrapped to the class
// when create this job from Task constructor
if (labelData instanceof Label) {
return labelData;
}
return new Label(labelData);
});
} else {
throw new Error('Job labels must be an array');
}
Object.defineProperties( Object.defineProperties(
this, this,
@ -764,42 +776,53 @@
if (assignee !== null && !(assignee instanceof User)) { if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError('Value must be a user instance'); throw new ArgumentError('Value must be a user instance');
} }
updatedFields.assignee = true; updateTrigger.update('assignee');
data.assignee = assignee; data.assignee = assignee;
}, },
}, },
/** /**
* Instance of a user who is responsible for review * @name stage
* @name reviewer * @type {module:API.cvat.enums.JobStage}
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Job * @memberof module:API.cvat.classes.Job
* @instance * @instance
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
*/ */
reviewer: { stage: {
get: () => data.reviewer, get: () => data.stage,
set: (reviewer) => { set: (stage) => {
if (reviewer !== null && !(reviewer instanceof User)) { const type = JobStage;
throw new ArgumentError('Value must be a user instance'); let valueInEnum = false;
for (const value in type) {
if (type[value] === stage) {
valueInEnum = true;
break;
} }
updatedFields.reviewer = true; }
data.reviewer = reviewer;
if (!valueInEnum) {
throw new ArgumentError(
'Value must be a value from the enumeration cvat.enums.JobStage',
);
}
updateTrigger.update('stage');
data.stage = stage;
}, },
}, },
/** /**
* @name status * @name state
* @type {module:API.cvat.enums.TaskStatus} * @type {module:API.cvat.enums.JobState}
* @memberof module:API.cvat.classes.Job * @memberof module:API.cvat.classes.Job
* @instance * @instance
* @throws {module:API.cvat.exceptions.ArgumentError} * @throws {module:API.cvat.exceptions.ArgumentError}
*/ */
status: { state: {
get: () => data.status, get: () => data.state,
set: (status) => { set: (state) => {
const type = TaskStatus; const type = JobState;
let valueInEnum = false; let valueInEnum = false;
for (const value in type) { for (const value in type) {
if (type[value] === status) { if (type[value] === state) {
valueInEnum = true; valueInEnum = true;
break; break;
} }
@ -807,12 +830,12 @@
if (!valueInEnum) { if (!valueInEnum) {
throw new ArgumentError( throw new ArgumentError(
'Value must be a value from the enumeration cvat.enums.TaskStatus', 'Value must be a value from the enumeration cvat.enums.JobState',
); );
} }
updatedFields.status = true; updateTrigger.update('state');
data.status = status; data.state = state;
}, },
}, },
/** /**
@ -836,17 +859,96 @@
get: () => data.stop_frame, get: () => data.stop_frame,
}, },
/** /**
* @name task * @name projectId
* @type {module:API.cvat.classes.Task} * @type {integer|null}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
*/
projectId: {
get: () => data.project_id,
},
/**
* @name taskId
* @type {integer}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
*/
taskId: {
get: () => data.task_id,
},
/**
* @name labels
* @type {module:API.cvat.classes.Label[]}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
*/
labels: {
get: () => data.labels.filter((_label) => !_label.deleted),
},
/**
* @name dimension
* @type {module:API.cvat.enums.DimensionType}
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
*/
dimension: {
get: () => data.dimension,
},
/**
* @name dataChunkSize
* @type {integer}
* @memberof module:API.cvat.classes.Job * @memberof module:API.cvat.classes.Job
* @readonly * @readonly
* @instance * @instance
*/ */
task: { dataChunkSize: {
get: () => data.task, get: () => data.data_chunk_size,
set: (chunkSize) => {
if (typeof chunkSize !== 'number' || chunkSize < 1) {
throw new ArgumentError(
`Chunk size value must be a positive number. But value ${chunkSize} has been got.`,
);
}
data.data_chunk_size = chunkSize;
},
},
/**
* @name dataChunkSize
* @type {string}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
*/
dataChunkType: {
get: () => data.data_compressed_chunk_type,
},
/**
* @name mode
* @type {string}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
*/
mode: {
get: () => data.mode,
},
/**
* @name bugTracker
* @type {string|null}
* @memberof module:API.cvat.classes.Job
* @instance
* @readonly
*/
bugTracker: {
get: () => data.bug_tracker,
}, },
__updatedFields: { _updateTrigger: {
get: () => updatedFields, get: () => updateTrigger,
}, },
}), }),
); );
@ -870,6 +972,7 @@
export: Object.getPrototypeOf(this).annotations.export.bind(this), export: Object.getPrototypeOf(this).annotations.export.bind(this),
statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this), statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this),
hasUnsavedChanges: Object.getPrototypeOf(this).annotations.hasUnsavedChanges.bind(this), hasUnsavedChanges: Object.getPrototypeOf(this).annotations.hasUnsavedChanges.bind(this),
exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this),
}; };
this.actions = { this.actions = {
@ -898,7 +1001,7 @@
} }
/** /**
* Method updates job data like status or assignee * Method updates job data like state, stage or assignee
* @method save * @method save
* @memberof module:API.cvat.classes.Job * @memberof module:API.cvat.classes.Job
* @readonly * @readonly
@ -916,7 +1019,7 @@
* Method returns a list of issues for a job * Method returns a list of issues for a job
* @method issues * @method issues
* @memberof module:API.cvat.classes.Job * @memberof module:API.cvat.classes.Job
* @type {module:API.cvat.classes.Issue[]} * @returns {module:API.cvat.classes.Issue[]}
* @readonly * @readonly
* @instance * @instance
* @async * @async
@ -929,44 +1032,36 @@
} }
/** /**
* Method returns a list of reviews for a job * Method adds a new issue to a job
* @method reviews * @method openIssue
* @type {module:API.cvat.classes.Review[]}
* @memberof module:API.cvat.classes.Job * @memberof module:API.cvat.classes.Job
* @returns {module:API.cvat.classes.Issue}
* @param {module:API.cvat.classes.Issue} issue
* @param {string} message
* @readonly * @readonly
* @instance * @instance
* @async * @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.ServerError} * @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
*/ */
async reviews() { async openIssue(issue, message) {
const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.reviews); const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.openIssue, issue, message);
return result; return result;
} }
/** /**
* /** * Method removes all job related data from the client (annotations, history, etc.)
* @typedef {Object} ReviewSummary * @method close
* @property {number} reviews Number of done reviews * @returns {module:API.cvat.classes.Job}
* @property {number} average_estimated_quality
* @property {number} issues_unsolved
* @property {number} issues_resolved
* @property {string[]} assignees
* @property {string[]} reviewers
*/
/**
* Method returns brief summary of within all reviews
* @method reviewsSummary
* @type {ReviewSummary}
* @memberof module:API.cvat.classes.Job * @memberof module:API.cvat.classes.Job
* @readonly * @readonly
* @instance
* @async * @async
* @throws {module:API.cvat.exceptions.ServerError} * @instance
* @throws {module:API.cvat.exceptions.PluginError} * @throws {module:API.cvat.exceptions.PluginError}
*/ */
async reviewsSummary() { async close() {
const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.reviewsSummary); const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.close);
return result; return result;
} }
} }
@ -993,7 +1088,7 @@
const data = { const data = {
id: undefined, id: undefined,
name: undefined, name: undefined,
project_id: undefined, project_id: null,
status: undefined, status: undefined,
size: undefined, size: undefined,
mode: undefined, mode: undefined,
@ -1020,14 +1115,7 @@
sorting_method: undefined, sorting_method: undefined,
}; };
const updatedFields = new FieldUpdateTrigger({ const updateTrigger = new FieldUpdateTrigger();
name: false,
assignee: false,
bug_tracker: false,
subset: false,
labels: false,
project_id: false,
});
for (const property in data) { for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) { if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
@ -1046,6 +1134,13 @@
remote_files: [], remote_files: [],
}); });
if (Array.isArray(initialData.labels)) {
for (const label of initialData.labels) {
const classInstance = new Label(label);
data.labels.push(classInstance);
}
}
if (Array.isArray(initialData.segments)) { if (Array.isArray(initialData.segments)) {
for (const segment of initialData.segments) { for (const segment of initialData.segments) {
if (Array.isArray(segment.jobs)) { if (Array.isArray(segment.jobs)) {
@ -1054,25 +1149,28 @@
url: job.url, url: job.url,
id: job.id, id: job.id,
assignee: job.assignee, assignee: job.assignee,
reviewer: job.reviewer, state: job.state,
status: job.status, stage: job.stage,
start_frame: segment.start_frame, start_frame: segment.start_frame,
stop_frame: segment.stop_frame, stop_frame: segment.stop_frame,
task: this, // following fields also returned when doing API request /jobs/<id>
// here we know them from task and append to constructor
task_id: data.id,
project_id: data.project_id,
labels: data.labels,
bug_tracker: data.bug_tracker,
mode: data.mode,
dimension: data.dimension,
data_compressed_chunk_type: data.data_compressed_chunk_type,
data_chunk_size: data.data_chunk_size,
}); });
data.jobs.push(jobInstance); data.jobs.push(jobInstance);
} }
} }
} }
} }
if (Array.isArray(initialData.labels)) {
for (const label of initialData.labels) {
const classInstance = new Label(label);
data.labels.push(classInstance);
}
}
Object.defineProperties( Object.defineProperties(
this, this,
Object.freeze({ Object.freeze({
@ -1099,7 +1197,7 @@
if (!value.trim().length) { if (!value.trim().length) {
throw new ArgumentError('Value must not be empty'); throw new ArgumentError('Value must not be empty');
} }
updatedFields.name = true; updateTrigger.update('name');
data.name = value; data.name = value;
}, },
}, },
@ -1116,7 +1214,7 @@
throw new ArgumentError('Value must be a positive integer'); throw new ArgumentError('Value must be a positive integer');
} }
updatedFields.project_id = true; updateTrigger.update('projectId');
data.project_id = projectId; data.project_id = projectId;
}, },
}, },
@ -1175,7 +1273,7 @@
if (assignee !== null && !(assignee instanceof User)) { if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError('Value must be a user instance'); throw new ArgumentError('Value must be a user instance');
} }
updatedFields.assignee = true; updateTrigger.update('assignee');
data.assignee = assignee; data.assignee = assignee;
}, },
}, },
@ -1215,7 +1313,7 @@
); );
} }
updatedFields.bug_tracker = true; updateTrigger.update('bugTracker');
data.bug_tracker = tracker; data.bug_tracker = tracker;
}, },
}, },
@ -1235,7 +1333,7 @@
); );
} }
updatedFields.subset = true; updateTrigger.update('subset');
data.subset = subset; data.subset = subset;
}, },
}, },
@ -1364,7 +1462,7 @@
_label.deleted = true; _label.deleted = true;
}); });
updatedFields.labels = true; updateTrigger.update('labels');
data.labels = [...deletedLabels, ...labels]; data.labels = [...deletedLabels, ...labels];
}, },
}, },
@ -1531,14 +1629,14 @@
dataChunkType: { dataChunkType: {
get: () => data.data_compressed_chunk_type, get: () => data.data_compressed_chunk_type,
}, },
dimension: {
/** /**
* @name enabled * @name dimension
* @type {string} * @type {module:API.cvat.enums.DimensionType}
* @memberof module:API.cvat.enums.DimensionType * @memberof module:API.cvat.classes.Task
* @readonly * @readonly
* @instance * @instance
*/ */
dimension: {
get: () => data.dimension, get: () => data.dimension,
}, },
/** /**
@ -1563,8 +1661,8 @@
_internalData: { _internalData: {
get: () => data, get: () => data,
}, },
__updatedFields: { _updateTrigger: {
get: () => updatedFields, get: () => updateTrigger,
}, },
}), }),
); );
@ -1731,70 +1829,32 @@
Job.prototype.save.implementation = async function () { Job.prototype.save.implementation = async function () {
if (this.id) { if (this.id) {
const jobData = {}; const jobData = this._updateTrigger.getUpdated(this);
if (jobData.assignee) {
for (const [field, isUpdated] of Object.entries(this.__updatedFields)) { jobData.assignee = jobData.assignee.id;
if (isUpdated) {
switch (field) {
case 'status':
jobData.status = this.status;
break;
case 'assignee':
jobData.assignee_id = this.assignee ? this.assignee.id : null;
break;
case 'reviewer':
jobData.reviewer_id = this.reviewer ? this.reviewer.id : null;
break;
default:
break;
}
} }
}
await serverProxy.jobs.save(this.id, jobData);
this.__updatedFields.reset();
return this; const data = await serverProxy.jobs.save(this.id, jobData);
this._updateTrigger.reset();
return new Job(data);
} }
throw new ArgumentError('Can not save job without and id'); throw new ArgumentError('Could not save job without id');
}; };
Job.prototype.issues.implementation = async function () { Job.prototype.issues.implementation = async function () {
const result = await serverProxy.jobs.issues(this.id); const result = await serverProxy.issues.get(this.id);
return result.map((issue) => new Issue(issue)); return result.map((issue) => new Issue(issue));
}; };
Job.prototype.reviews.implementation = async function () { Job.prototype.openIssue.implementation = async function (issue, message) {
const result = await serverProxy.jobs.reviews.get(this.id); checkObjectType('issue', issue, null, Issue);
const reviews = result.map((review) => new Review(review)); checkObjectType('message', message, 'string');
const result = await serverProxy.issues.create({
// try to get not finished review from the local storage ...issue.serialize(),
const data = store.get(`job-${this.id}-review`); message,
if (data) { });
reviews.push(new Review(JSON.parse(data))); return new Issue(result);
}
return reviews;
};
Job.prototype.reviewsSummary.implementation = async function () {
const reviews = await serverProxy.jobs.reviews.get(this.id);
const issues = await serverProxy.jobs.issues(this.id);
const qualities = reviews.map((review) => review.estimated_quality);
const reviewers = reviews.filter((review) => review.reviewer).map((review) => review.reviewer.username);
const assignees = reviews.filter((review) => review.assignee).map((review) => review.assignee.username);
return {
reviews: reviews.length,
average_estimated_quality: qualities.reduce((acc, quality) => acc + quality, 0) / (qualities.length || 1),
issues_unsolved: issues.filter((issue) => !issue.resolved_date).length,
issues_resolved: issues.filter((issue) => issue.resolved_date).length,
assignees: Array.from(new Set(assignees.filter((assignee) => assignee !== null))),
reviewers: Array.from(new Set(reviewers.filter((reviewer) => reviewer !== null))),
};
}; };
Job.prototype.frames.get.implementation = async function (frame, isPlaying, step) { Job.prototype.frames.get.implementation = async function (frame, isPlaying, step) {
@ -1807,27 +1867,28 @@
} }
const frameData = await getFrame( const frameData = await getFrame(
this.task.id, this.taskId,
this.task.dataChunkSize, this.id,
this.task.dataChunkType, this.dataChunkSize,
this.task.mode, this.dataChunkType,
this.mode,
frame, frame,
this.startFrame, this.startFrame,
this.stopFrame, this.stopFrame,
isPlaying, isPlaying,
step, step,
this.task.dimension, this.dimension,
); );
return frameData; return frameData;
}; };
Job.prototype.frames.ranges.implementation = async function () { Job.prototype.frames.ranges.implementation = async function () {
const rangesData = await getRanges(this.task.id); const rangesData = await getRanges(this.taskId);
return rangesData; return rangesData;
}; };
Job.prototype.frames.preview.implementation = async function () { Job.prototype.frames.preview.implementation = async function () {
const frameData = await getPreview(this.task.id); const frameData = await getPreview(this.taskId);
return frameData; return frameData;
}; };
@ -1950,7 +2011,7 @@
}; };
Job.prototype.annotations.exportDataset.implementation = async function (format, saveImages, customName) { Job.prototype.annotations.exportDataset.implementation = async function (format, saveImages, customName) {
const result = await exportDataset(this.task, format, customName, saveImages); const result = await exportDataset(this, format, customName, saveImages);
return result; return result;
}; };
@ -1980,20 +2041,54 @@
}; };
Job.prototype.logger.log.implementation = async function (logType, payload, wait) { Job.prototype.logger.log.implementation = async function (logType, payload, wait) {
const result = await this.task.logger.log(logType, { ...payload, job_id: this.id }, wait); const result = await loggerStorage.log(logType, { ...payload, task_id: this.taskId, job_id: this.id }, wait);
return result; return result;
}; };
Job.prototype.predictor.status.implementation = async function () { Job.prototype.predictor.status.implementation = async function () {
const result = await this.task.predictor.status(); if (!Number.isInteger(this.projectId)) {
return result; throw new DataError('The job must belong to a project to use the feature');
}
const result = await serverProxy.predictor.status(this.projectId);
return {
message: result.message,
progress: result.progress,
projectScore: result.score,
timeRemaining: result.time_remaining,
mediaAmount: result.media_amount,
annotationAmount: result.annotation_amount,
};
}; };
Job.prototype.predictor.predict.implementation = async function (frame) { Job.prototype.predictor.predict.implementation = async function (frame) {
const result = await this.task.predictor.predict(frame); if (!Number.isInteger(frame) || frame < 0) {
throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`);
}
if (frame < this.startFrame || frame > this.stopFrame) {
throw new ArgumentError(`The frame with number ${frame} is out of the job`);
}
if (!Number.isInteger(this.projectId)) {
throw new DataError('The job must belong to a project to use the feature');
}
const result = await serverProxy.predictor.predict(this.taskId, frame);
return result; return result;
}; };
Job.prototype.frames.contextImage.implementation = async function (frameId) {
const result = await getContextImage(this.taskId, this.id, frameId);
return result;
};
Job.prototype.close.implementation = function closeTask() {
clearFrames(this.taskId);
closeSession(this);
return this;
};
Task.prototype.close.implementation = function closeTask() { Task.prototype.close.implementation = function closeTask() {
clearFrames(this.id); clearFrames(this.id);
for (const job of this.jobs) { for (const job of this.jobs) {
@ -2008,40 +2103,22 @@
// TODO: Add ability to change an owner and an assignee // TODO: Add ability to change an owner and an assignee
if (typeof this.id !== 'undefined') { if (typeof this.id !== 'undefined') {
// If the task has been already created, we update it // If the task has been already created, we update it
const taskData = {}; const taskData = this._updateTrigger.getUpdated(this, {
bugTracker: 'bug_tracker',
for (const [field, isUpdated] of Object.entries(this.__updatedFields)) { projectId: 'project_id',
if (isUpdated) { assignee: 'assignee_id',
switch (field) { });
case 'assignee': if (taskData.assignee_id) {
taskData.assignee_id = this.assignee ? this.assignee.id : null; taskData.assignee_id = taskData.assignee_id.id;
break;
case 'name':
taskData.name = this.name;
break;
case 'bug_tracker':
taskData.bug_tracker = this.bugTracker;
break;
case 'subset':
taskData.subset = this.subset;
break;
case 'project_id':
taskData.project_id = this.projectId;
break;
case 'labels':
taskData.labels = [...this._internalData.labels.map((el) => el.toJSON())];
break;
default:
break;
}
} }
if (taskData.labels) {
taskData.labels = this._internalData.labels;
taskData.labels = taskData.labels.map((el) => el.toJSON());
} }
await serverProxy.tasks.saveTask(this.id, taskData); const data = await serverProxy.tasks.save(this.id, taskData);
this._updateTrigger.reset();
this.__updatedFields.reset(); return new Task(data);
return this;
} }
const taskSpec = { const taskSpec = {
@ -2094,22 +2171,23 @@
taskDataSpec.cloud_storage_id = this.cloudStorageId; taskDataSpec.cloud_storage_id = this.cloudStorageId;
} }
const task = await serverProxy.tasks.createTask(taskSpec, taskDataSpec, onUpdate); const task = await serverProxy.tasks.create(taskSpec, taskDataSpec, onUpdate);
return new Task(task); return new Task(task);
}; };
Task.prototype.delete.implementation = async function () { Task.prototype.delete.implementation = async function () {
const result = await serverProxy.tasks.deleteTask(this.id); const result = await serverProxy.tasks.delete(this.id);
return result; return result;
}; };
Task.prototype.export.implementation = async function () { Task.prototype.export.implementation = async function () {
const result = await serverProxy.tasks.exportTask(this.id); const result = await serverProxy.tasks.export(this.id);
return result; return result;
}; };
Task.import.implementation = async function (file) { Task.import.implementation = async function (file) {
const result = await serverProxy.tasks.importTask(file); // eslint-disable-next-line no-unsanitized/method
const result = await serverProxy.tasks.import(file);
return result; return result;
}; };
@ -2124,6 +2202,7 @@
const result = await getFrame( const result = await getFrame(
this.id, this.id,
null,
this.dataChunkSize, this.dataChunkSize,
this.dataChunkType, this.dataChunkType,
this.mode, this.mode,
@ -2329,9 +2408,4 @@
const result = await serverProxy.predictor.predict(this.id, frame); const result = await serverProxy.predictor.predict(this.id, frame);
return result; return result;
}; };
Job.prototype.frames.contextImage.implementation = async function (taskId, frameId) {
const result = await getContextImage(taskId, frameId);
return result;
};
})(); })();

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation // Copyright (C) 2019-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -174,10 +174,6 @@
email_verification_required: this.isVerified, email_verification_required: this.isVerified,
}; };
} }
toJSON() {
return this.serialize();
}
} }
module.exports = User; module.exports = User;

@ -125,7 +125,7 @@ describe('Feature: put annotations', () => {
shapeType: window.cvat.enums.ObjectShape.RECTANGLE, shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
points: [0, 0, 100, 100], points: [0, 0, 100, 100],
occluded: false, occluded: false,
label: job.task.labels[0], label: job.labels[0],
zOrder: 0, zOrder: 0,
}); });
@ -169,7 +169,7 @@ describe('Feature: put annotations', () => {
shapeType: window.cvat.enums.ObjectShape.RECTANGLE, shapeType: window.cvat.enums.ObjectShape.RECTANGLE,
points: [0, 0, 100, 100], points: [0, 0, 100, 100],
occluded: false, occluded: false,
label: job.task.labels[0], label: job.labels[0],
zOrder: 0, zOrder: 0,
}); });
@ -419,7 +419,7 @@ describe('Feature: save annotations', () => {
shapeType: window.cvat.enums.ObjectShape.POLYGON, shapeType: window.cvat.enums.ObjectShape.POLYGON,
points: [0, 0, 100, 0, 100, 50], points: [0, 0, 100, 0, 100, 50],
occluded: true, occluded: true,
label: job.task.labels[0], label: job.labels[0],
zOrder: 0, zOrder: 0,
}); });

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -25,8 +25,8 @@ describe('Feature: get a list of jobs', () => {
expect(el).toBeInstanceOf(Job); expect(el).toBeInstanceOf(Job);
} }
expect(result[0].task.id).toBe(3); expect(result[0].taskId).toBe(3);
expect(result[0].task).toBe(result[1].task); expect(result[0].taskId).toBe(result[1].taskId);
}); });
test('get jobs by an unknown task id', async () => { test('get jobs by an unknown task id', async () => {
@ -89,18 +89,17 @@ describe('Feature: get a list of jobs', () => {
}); });
describe('Feature: save job', () => { describe('Feature: save job', () => {
test('save status of a job', async () => { test('save stage and state of a job', async () => {
let result = await window.cvat.jobs.get({ const result = await window.cvat.jobs.get({
jobID: 1, jobID: 1,
}); });
result[0].status = 'validation'; result[0].stage = 'validation';
await result[0].save(); result[0].state = 'new';
const newJob = await result[0].save();
result = await window.cvat.jobs.get({ expect(newJob.stage).toBe('validation');
jobID: 1, expect(newJob.state).toBe('new');
});
expect(result[0].status).toBe('validation');
}); });
test('save invalid status of a job', async () => { test('save invalid status of a job', async () => {
@ -108,9 +107,11 @@ describe('Feature: save job', () => {
jobID: 1, jobID: 1,
}); });
await result[0].save();
expect(() => { expect(() => {
result[0].status = 'invalid'; result[0].state = 'invalid';
}).toThrow(window.cvat.exceptions.ArgumentError);
expect(() => {
result[0].stage = 'invalid';
}).toThrow(window.cvat.exceptions.ArgumentError); }).toThrow(window.cvat.exceptions.ArgumentError);
}); });
}); });

@ -242,8 +242,9 @@ const projectsDummyData = {
url: 'http://192.168.0.139:7000/api/v1/jobs/1', url: 'http://192.168.0.139:7000/api/v1/jobs/1',
id: 1, id: 1,
assignee: null, assignee: null,
reviewer: null,
status: 'completed', status: 'completed',
stage: 'acceptance',
state: 'completed',
}, },
], ],
}, },
@ -255,8 +256,9 @@ const projectsDummyData = {
url: 'http://192.168.0.139:7000/api/v1/jobs/2', url: 'http://192.168.0.139:7000/api/v1/jobs/2',
id: 2, id: 2,
assignee: null, assignee: null,
reviewer: null,
status: 'completed', status: 'completed',
stage: 'acceptance',
state: 'completed',
}, },
], ],
}, },
@ -268,8 +270,9 @@ const projectsDummyData = {
url: 'http://192.168.0.139:7000/api/v1/jobs/3', url: 'http://192.168.0.139:7000/api/v1/jobs/3',
id: 3, id: 3,
assignee: null, assignee: null,
reviewer: null,
status: 'completed', status: 'completed',
stage: 'acceptance',
state: 'completed',
}, },
], ],
}, },
@ -281,8 +284,9 @@ const projectsDummyData = {
url: 'http://192.168.0.139:7000/api/v1/jobs/4', url: 'http://192.168.0.139:7000/api/v1/jobs/4',
id: 4, id: 4,
assignee: null, assignee: null,
reviewer: null,
status: 'completed', status: 'completed',
stage: 'acceptance',
state: 'completed',
}, },
], ],
}, },
@ -294,8 +298,9 @@ const projectsDummyData = {
url: 'http://192.168.0.139:7000/api/v1/jobs/5', url: 'http://192.168.0.139:7000/api/v1/jobs/5',
id: 5, id: 5,
assignee: null, assignee: null,
reviewer: null,
status: 'completed', status: 'completed',
stage: 'acceptance',
state: 'completed',
}, },
], ],
}, },
@ -344,6 +349,9 @@ const tasksDummyData = {
updated_date: '2019-09-05T14:04:07.569344Z', updated_date: '2019-09-05T14:04:07.569344Z',
overlap: 0, overlap: 0,
segment_size: 0, segment_size: 0,
dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation', status: 'annotation',
labels: [ labels: [
{ {
@ -361,8 +369,9 @@ const tasksDummyData = {
url: 'http://localhost:7000/api/v1/jobs/112', url: 'http://localhost:7000/api/v1/jobs/112',
id: 112, id: 112,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -389,6 +398,9 @@ const tasksDummyData = {
updated_date: '2019-07-16T15:51:29.142871+03:00', updated_date: '2019-07-16T15:51:29.142871+03:00',
overlap: 0, overlap: 0,
segment_size: 0, segment_size: 0,
dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation', status: 'annotation',
labels: [ labels: [
{ {
@ -411,8 +423,9 @@ const tasksDummyData = {
url: 'http://localhost:7000/api/v1/jobs/100', url: 'http://localhost:7000/api/v1/jobs/100',
id: 100, id: 100,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -439,6 +452,9 @@ const tasksDummyData = {
updated_date: '2019-07-12T16:43:58.904892+03:00', updated_date: '2019-07-12T16:43:58.904892+03:00',
overlap: 5, overlap: 5,
segment_size: 500, segment_size: 500,
dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation', status: 'annotation',
labels: [ labels: [
{ {
@ -615,8 +631,9 @@ const tasksDummyData = {
url: 'http://localhost:7000/api/v1/jobs/10', url: 'http://localhost:7000/api/v1/jobs/10',
id: 101, id: 101,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -628,8 +645,9 @@ const tasksDummyData = {
url: 'http://localhost:7000/api/v1/jobs/11', url: 'http://localhost:7000/api/v1/jobs/11',
id: 102, id: 102,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -641,8 +659,9 @@ const tasksDummyData = {
url: 'http://localhost:7000/api/v1/jobs/12', url: 'http://localhost:7000/api/v1/jobs/12',
id: 103, id: 103,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -654,8 +673,9 @@ const tasksDummyData = {
url: 'http://localhost:7000/api/v1/jobs/13', url: 'http://localhost:7000/api/v1/jobs/13',
id: 104, id: 104,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -667,8 +687,9 @@ const tasksDummyData = {
url: 'http://localhost:7000/api/v1/jobs/14', url: 'http://localhost:7000/api/v1/jobs/14',
id: 105, id: 105,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -680,8 +701,9 @@ const tasksDummyData = {
url: 'http://localhost:7000/api/v1/jobs/15', url: 'http://localhost:7000/api/v1/jobs/15',
id: 106, id: 106,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -693,8 +715,9 @@ const tasksDummyData = {
url: 'http://localhost:7000/api/v1/jobs/16', url: 'http://localhost:7000/api/v1/jobs/16',
id: 107, id: 107,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -706,8 +729,9 @@ const tasksDummyData = {
url: 'http://localhost:7000/api/v1/jobs/17', url: 'http://localhost:7000/api/v1/jobs/17',
id: 108, id: 108,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -719,8 +743,9 @@ const tasksDummyData = {
url: 'http://localhost:7000/api/v1/jobs/18', url: 'http://localhost:7000/api/v1/jobs/18',
id: 109, id: 109,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -732,8 +757,9 @@ const tasksDummyData = {
url: 'http://localhost:7000/api/v1/jobs/19', url: 'http://localhost:7000/api/v1/jobs/19',
id: 110, id: 110,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -745,8 +771,9 @@ const tasksDummyData = {
url: 'http://localhost:7000/api/v1/jobs/20', url: 'http://localhost:7000/api/v1/jobs/20',
id: 111, id: 111,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -773,7 +800,9 @@ const tasksDummyData = {
updated_date: '2019-05-16T13:08:00.621797+03:00', updated_date: '2019-05-16T13:08:00.621797+03:00',
overlap: 5, overlap: 5,
segment_size: 5000, segment_size: 5000,
flipped: false, dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation', status: 'annotation',
labels: [ labels: [
{ {
@ -950,8 +979,9 @@ const tasksDummyData = {
url: 'http://localhost:7000/api/v1/jobs/3', url: 'http://localhost:7000/api/v1/jobs/3',
id: 3, id: 3,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -963,8 +993,9 @@ const tasksDummyData = {
url: 'http://localhost:7000/api/v1/jobs/4', url: 'http://localhost:7000/api/v1/jobs/4',
id: 4, id: 4,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -989,7 +1020,9 @@ const tasksDummyData = {
updated_date: '2019-05-15T16:58:27.992785+03:00', updated_date: '2019-05-15T16:58:27.992785+03:00',
overlap: 5, overlap: 5,
segment_size: 0, segment_size: 0,
flipped: false, dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation', status: 'annotation',
labels: [ labels: [
{ {
@ -1166,8 +1199,9 @@ const tasksDummyData = {
url: 'http://localhost:7000/api/v1/jobs/2', url: 'http://localhost:7000/api/v1/jobs/2',
id: 2, id: 2,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: 'annotation',
state: 'new',
}, },
], ],
}, },
@ -1191,7 +1225,9 @@ const tasksDummyData = {
updated_date: '2019-05-15T11:20:55.770587+03:00', updated_date: '2019-05-15T11:20:55.770587+03:00',
overlap: 0, overlap: 0,
segment_size: 0, segment_size: 0,
flipped: false, dimension: '2d',
data_compressed_chunk_type: 'imageset',
data_chunk_size: 1,
status: 'annotation', status: 'annotation',
labels: [ labels: [
{ {
@ -1368,8 +1404,9 @@ const tasksDummyData = {
url: 'http://localhost:7000/api/v1/jobs/1', url: 'http://localhost:7000/api/v1/jobs/1',
id: 1, id: 1,
assignee: null, assignee: null,
reviewer: null,
status: 'annotation', status: 'annotation',
stage: "annotation",
state: "new",
}, },
], ],
}, },

@ -76,7 +76,7 @@ class ServerProxy {
} }
async function getProjects(filter = '') { async function getProjects(filter = '') {
const queries = QueryStringToJSON(filter, ['without_tasks']); const queries = QueryStringToJSON(filter);
const result = projectsDummyData.results.filter((x) => { const result = projectsDummyData.results.filter((x) => {
for (const key in queries) { for (const key in queries) {
if (Object.prototype.hasOwnProperty.call(queries, key)) { if (Object.prototype.hasOwnProperty.call(queries, key)) {
@ -170,6 +170,9 @@ class ServerProxy {
} }
} }
} }
const [updatedTask] = await getTasks({ id });
return updatedTask;
} }
async function createTask(taskData) { async function createTask(taskData) {
@ -218,6 +221,12 @@ class ServerProxy {
copy.start_frame = segment.start_frame; copy.start_frame = segment.start_frame;
copy.stop_frame = segment.stop_frame; copy.stop_frame = segment.stop_frame;
copy.task_id = task.id; copy.task_id = task.id;
copy.dimension = task.dimension;
copy.data_compressed_chunk_type = task.data_compressed_chunk_type;
copy.data_chunk_size = task.data_chunk_size;
copy.bug_tracker = task.bug_tracker;
copy.mode = task.mode;
copy.labels = task.labels;
acc.push(copy); acc.push(copy);
} }
@ -255,6 +264,8 @@ class ServerProxy {
object[prop] = jobData[prop]; object[prop] = jobData[prop];
} }
} }
return getJob(id);
} }
async function getUsers() { async function getUsers() {
@ -402,10 +413,10 @@ class ServerProxy {
tasks: { tasks: {
value: Object.freeze({ value: Object.freeze({
getTasks, get: getTasks,
saveTask, save: saveTask,
createTask, create: createTask,
deleteTask, delete: deleteTask,
}), }),
writable: false, writable: false,
}, },

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

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

@ -24,6 +24,7 @@ import {
Task, Task,
Workspace, Workspace,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { updateJobAsync } from './tasks-actions';
interface AnnotationsParameters { interface AnnotationsParameters {
filters: string[]; filters: string[];
@ -183,8 +184,6 @@ export enum AnnotationActionTypes {
SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED', SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED',
INTERACT_WITH_CANVAS = 'INTERACT_WITH_CANVAS', INTERACT_WITH_CANVAS = 'INTERACT_WITH_CANVAS',
GET_DATA_FAILED = 'GET_DATA_FAILED', GET_DATA_FAILED = 'GET_DATA_FAILED',
SWITCH_REQUEST_REVIEW_DIALOG = 'SWITCH_REQUEST_REVIEW_DIALOG',
SWITCH_SUBMIT_REVIEW_DIALOG = 'SWITCH_SUBMIT_REVIEW_DIALOG',
SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG = 'SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG', SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG = 'SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG',
UPDATE_PREDICTOR_STATE = 'UPDATE_PREDICTOR_STATE', UPDATE_PREDICTOR_STATE = 'UPDATE_PREDICTOR_STATE',
GET_PREDICTIONS = 'GET_PREDICTIONS', GET_PREDICTIONS = 'GET_PREDICTIONS',
@ -343,7 +342,7 @@ export function uploadJobAnnotationsAsync(job: any, loader: any, file: File): Th
const state: CombinedState = getStore().getState(); const state: CombinedState = getStore().getState();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters(); const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
if (state.tasks.activities.loads[job.task.id]) { if (state.tasks.activities.loads[job.taskId]) {
throw Error('Annotations is being uploaded for the task'); throw Error('Annotations is being uploaded for the task');
} }
if (state.annotation.activities.loads[job.id]) { if (state.annotation.activities.loads[job.id]) {
@ -639,7 +638,7 @@ export function getPredictionsAsync(): ThunkAction {
annotations = annotations.map( annotations = annotations.map(
(data: any): any => new cvat.classes.ObjectState({ (data: any): any => new cvat.classes.ObjectState({
shapeType: data.type, shapeType: data.type,
label: job.task.labels.filter((label: any): boolean => label.id === data.label)[0], label: job.labels.filter((label: any): boolean => label.id === data.label)[0],
points: data.points, points: data.points,
objectType: ObjectType.SHAPE, objectType: ObjectType.SHAPE,
frame, frame,
@ -950,7 +949,7 @@ export function closeJob(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const { jobInstance } = receiveAnnotationsParameters(); const { jobInstance } = receiveAnnotationsParameters();
if (jobInstance) { if (jobInstance) {
await jobInstance.task.close(); await jobInstance.close();
} }
dispatch({ dispatch({
@ -960,9 +959,9 @@ export function closeJob(): ThunkAction {
} }
export function getJobAsync(tid: number, jid: number, initialFrame: number, initialFilters: object[]): ThunkAction { export function getJobAsync(tid: number, jid: number, initialFrame: number, initialFilters: object[]): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>, getState): Promise<void> => {
try { try {
const state: CombinedState = getStore().getState(); const state = getState();
const filters = initialFilters; const filters = initialFilters;
const { const {
settings: { settings: {
@ -986,21 +985,19 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
true, true,
); );
// Check state if the task is already there // Check if the task was already downloaded to the state
let task = state.tasks.current let job: any | null = null;
const [task] = state.tasks.current
.filter((_task: Task) => _task.instance.id === tid) .filter((_task: Task) => _task.instance.id === tid)
.map((_task: Task) => _task.instance)[0]; .map((_task: Task) => _task.instance);
if (task) {
// If there aren't the task, get it from the server [job] = task.jobs.filter((_job: any) => _job.id === jid);
if (!task) {
[task] = await cvat.tasks.get({ id: tid });
}
// Finally get the job from the task
const job = task.jobs.filter((_job: any) => _job.id === jid)[0];
if (!job) { if (!job) {
throw new Error(`Task ${tid} doesn't contain the job ${jid}`); throw new Error(`Task ${tid} doesn't contain the job ${jid}`);
} }
} else {
[job] = await cvat.jobs.get({ jobID: jid });
}
const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame); const frameNumber = Math.max(Math.min(job.stopFrame, initialFrame), job.startFrame);
const frameData = await job.frames.get(frameNumber); const frameData = await job.frames.get(frameNumber);
@ -1018,7 +1015,6 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
} }
const states = await job.annotations.get(frameNumber, showAllInterpolationTracks, filters); const states = await job.annotations.get(frameNumber, showAllInterpolationTracks, filters);
const issues = await job.issues(); const issues = await job.issues();
const reviews = await job.reviews();
const [minZ, maxZ] = computeZRange(states); const [minZ, maxZ] = computeZRange(states);
const colors = [...cvat.enums.colors]; const colors = [...cvat.enums.colors];
@ -1031,7 +1027,6 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
openTime, openTime,
job, job,
issues, issues,
reviews,
states, states,
frameNumber, frameNumber,
frameFilename: frameData.filename, frameFilename: frameData.filename,
@ -1044,14 +1039,14 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
}, },
}); });
if (job.task.dimension === DimensionType.DIM_3D) { if (job.dimension === DimensionType.DIM_3D) {
const workspace = Workspace.STANDARD3D; const workspace = Workspace.STANDARD3D;
dispatch(changeWorkspace(workspace)); dispatch(changeWorkspace(workspace));
} }
const updatePredictorStatus = async (): Promise<void> => { const updatePredictorStatus = async (): Promise<void> => {
// get current job // get current job
const currentState: CombinedState = getStore().getState(); const currentState: CombinedState = getState();
const { openTime: currentOpenTime, instance: currentJob } = currentState.annotation.job; const { openTime: currentOpenTime, instance: currentJob } = currentState.annotation.job;
if (currentJob === null || currentJob.id !== job.id || currentOpenTime !== openTime) { if (currentJob === null || currentJob.id !== job.id || currentOpenTime !== openTime) {
// the job was closed, changed or reopened // the job was closed, changed or reopened
@ -1074,7 +1069,7 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
} }
}; };
if (state.plugins.list.PREDICT && job.task.projectId !== null) { if (state.plugins.list.PREDICT && job.projectId !== null) {
updatePredictorStatus(); updatePredictorStatus();
} }
@ -1120,6 +1115,11 @@ export function saveAnnotationsAsync(sessionInstance: any, afterSave?: () => voi
afterSave(); afterSave();
} }
if (sessionInstance instanceof cvat.classes.Job && sessionInstance.state === cvat.enums.JobState.NEW) {
sessionInstance.state = cvat.enums.JobState.IN_PROGRESS;
updateJobAsync(sessionInstance);
}
dispatch({ dispatch({
type: AnnotationActionTypes.SAVE_ANNOTATIONS_SUCCESS, type: AnnotationActionTypes.SAVE_ANNOTATIONS_SUCCESS,
payload: { payload: {
@ -1589,24 +1589,6 @@ export function redrawShapeAsync(): ThunkAction {
}; };
} }
export function switchRequestReviewDialog(visible: boolean): AnyAction {
return {
type: AnnotationActionTypes.SWITCH_REQUEST_REVIEW_DIALOG,
payload: {
visible,
},
};
}
export function switchSubmitReviewDialog(visible: boolean): AnyAction {
return {
type: AnnotationActionTypes.SWITCH_SUBMIT_REVIEW_DIALOG,
payload: {
visible,
},
};
}
export function setForceExitAnnotationFlag(forceExit: boolean): AnyAction { export function setForceExitAnnotationFlag(forceExit: boolean): AnyAction {
return { return {
type: AnnotationActionTypes.SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG, type: AnnotationActionTypes.SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG,
@ -1645,7 +1627,7 @@ export function getContextImageAsync(): ThunkAction {
payload: {}, payload: {},
}); });
const contextImageData = await job.frames.contextImage(job.task.id, frameNumber); const contextImageData = await job.frames.contextImage(frameNumber);
dispatch({ dispatch({
type: AnnotationActionTypes.GET_CONTEXT_IMAGE_SUCCESS, type: AnnotationActionTypes.GET_CONTEXT_IMAGE_SUCCESS,
payload: { contextImageData }, payload: { contextImageData },

@ -106,7 +106,6 @@ export const loginAsync = (username: string, password: string): ThunkAction => a
try { try {
await cvat.server.login(username, password); await cvat.server.login(username, password);
const users = await cvat.users.get({ self: true }); const users = await cvat.users.get({ self: true });
dispatch(authActions.loginSuccess(users[0])); dispatch(authActions.loginSuccess(users[0]));
} catch (error) { } catch (error) {
dispatch(authActions.loginFailed(error)); dispatch(authActions.loginFailed(error));
@ -117,6 +116,7 @@ export const logoutAsync = (): ThunkAction => async (dispatch) => {
dispatch(authActions.logout()); dispatch(authActions.logout());
try { try {
await cvat.organizations.deactivate();
await cvat.server.logout(); await cvat.server.logout();
dispatch(authActions.logoutSuccess()); dispatch(authActions.logoutSuccess());
} catch (error) { } catch (error) {

@ -146,23 +146,23 @@ export function getInferenceStatusAsync(): ThunkAction {
}; };
} }
export function startInferenceAsync(taskInstance: any, model: Model, body: object): ThunkAction { export function startInferenceAsync(taskId: number, model: Model, body: object): ThunkAction {
return async (dispatch): Promise<void> => { return async (dispatch): Promise<void> => {
try { try {
const requestID: string = await core.lambda.run(taskInstance, model, body); const requestID: string = await core.lambda.run(taskId, model, body);
const dispatchCallback = (action: ModelsActions): void => { const dispatchCallback = (action: ModelsActions): void => {
dispatch(action); dispatch(action);
}; };
listen( listen(
{ {
taskID: taskInstance.id, taskID: taskId,
requestID, requestID,
}, },
dispatchCallback, dispatchCallback,
); );
} catch (error) { } catch (error) {
dispatch(modelsActions.startInferenceFailed(taskInstance.id, error)); dispatch(modelsActions.startInferenceFailed(taskId, error));
} }
}; };
} }

@ -0,0 +1,266 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { Store } from 'antd/lib/form/interface';
import { User } from 'components/task-page/user-selector';
import getCore from 'cvat-core-wrapper';
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
const core = getCore();
export enum OrganizationActionsTypes {
GET_ORGANIZATIONS = 'GET_ORGANIZATIONS',
GET_ORGANIZATIONS_SUCCESS = 'GET_ORGANIZATIONS_SUCCESS',
GET_ORGANIZATIONS_FAILED = 'GET_ORGANIZATIONS_FAILED',
ACTIVATE_ORGANIZATION_SUCCESS = 'ACTIVATE_ORGANIZATION_SUCCESS',
ACTIVATE_ORGANIZATION_FAILED = 'ACTIVATE_ORGANIZATION_FAILED',
CREATE_ORGANIZATION = 'CREATE_ORGANIZATION',
CREATE_ORGANIZATION_SUCCESS = 'CREATE_ORGANIZATION_SUCCESS',
CREATE_ORGANIZATION_FAILED = 'CREATE_ORGANIZATION_FAILED',
UPDATE_ORGANIZATION = 'UPDATE_ORGANIZATION',
UPDATE_ORGANIZATION_SUCCESS = 'UPDATE_ORGANIZATION_SUCCESS',
UPDATE_ORGANIZATION_FAILED = 'UPDATE_ORGANIZATION_FAILED',
REMOVE_ORGANIZATION = 'REMOVE_ORGANIZATION',
REMOVE_ORGANIZATION_SUCCESS = 'REMOVE_ORGANIZATION_SUCCESS',
REMOVE_ORGANIZATION_FAILED = 'REMOVE_ORGANIZATION_FAILED',
INVITE_ORGANIZATION_MEMBERS = 'INVITE_ORGANIZATION_MEMBERS',
INVITE_ORGANIZATION_MEMBERS_FAILED = 'INVITE_ORGANIZATION_MEMBERS_FAILED',
INVITE_ORGANIZATION_MEMBERS_DONE = 'INVITE_ORGANIZATION_MEMBERS_DONE',
INVITE_ORGANIZATION_MEMBER_SUCCESS = 'INVITE_ORGANIZATION_MEMBER_SUCCESS',
INVITE_ORGANIZATION_MEMBER_FAILED = 'INVITE_ORGANIZATION_MEMBER_FAILED',
LEAVE_ORGANIZATION = 'LEAVE_ORGANIZATION',
LEAVE_ORGANIZATION_SUCCESS = 'LEAVE_ORGANIZATION_SUCCESS',
LEAVE_ORGANIZATION_FAILED = 'LEAVE_ORGANIZATION_FAILED',
REMOVE_ORGANIZATION_MEMBER = 'REMOVE_ORGANIZATION_MEMBERS',
REMOVE_ORGANIZATION_MEMBER_SUCCESS = 'REMOVE_ORGANIZATION_MEMBER_SUCCESS',
REMOVE_ORGANIZATION_MEMBER_FAILED = 'REMOVE_ORGANIZATION_MEMBER_FAILED',
UPDATE_ORGANIZATION_MEMBER = 'UPDATE_ORGANIZATION_MEMBER',
UPDATE_ORGANIZATION_MEMBER_SUCCESS = 'UPDATE_ORGANIZATION_MEMBER_SUCCESS',
UPDATE_ORGANIZATION_MEMBER_FAILED = 'UPDATE_ORGANIZATION_MEMBER_FAILED',
}
const organizationActions = {
getOrganizations: () => createAction(OrganizationActionsTypes.GET_ORGANIZATIONS),
getOrganizationsSuccess: (list: any[]) => createAction(
OrganizationActionsTypes.GET_ORGANIZATIONS_SUCCESS, { list },
),
getOrganizationsFailed: (error: any) => createAction(OrganizationActionsTypes.GET_ORGANIZATIONS_FAILED, { error }),
createOrganization: () => createAction(OrganizationActionsTypes.CREATE_ORGANIZATION),
createOrganizationSuccess: (organization: any) => createAction(
OrganizationActionsTypes.CREATE_ORGANIZATION_SUCCESS, { organization },
),
createOrganizationFailed: (slug: string, error: any) => createAction(
OrganizationActionsTypes.CREATE_ORGANIZATION_FAILED, { slug, error },
),
updateOrganization: () => createAction(OrganizationActionsTypes.UPDATE_ORGANIZATION),
updateOrganizationSuccess: (organization: any) => createAction(
OrganizationActionsTypes.UPDATE_ORGANIZATION_SUCCESS, { organization },
),
updateOrganizationFailed: (slug: string, error: any) => createAction(
OrganizationActionsTypes.UPDATE_ORGANIZATION_FAILED, { slug, error },
),
activateOrganizationSuccess: (organization: any | null) => createAction(
OrganizationActionsTypes.ACTIVATE_ORGANIZATION_SUCCESS, { organization },
),
activateOrganizationFailed: (error: any, slug: string | null) => createAction(
OrganizationActionsTypes.ACTIVATE_ORGANIZATION_FAILED, { slug, error },
),
removeOrganization: () => createAction(OrganizationActionsTypes.REMOVE_ORGANIZATION),
removeOrganizationSuccess: (slug: string) => createAction(
OrganizationActionsTypes.REMOVE_ORGANIZATION_SUCCESS, { slug },
),
removeOrganizationFailed: (error: any, slug: string) => createAction(
OrganizationActionsTypes.REMOVE_ORGANIZATION_FAILED, { error, slug },
),
inviteOrganizationMembers: () => createAction(OrganizationActionsTypes.INVITE_ORGANIZATION_MEMBERS),
inviteOrganizationMembersFailed: (error: any) => createAction(
OrganizationActionsTypes.INVITE_ORGANIZATION_MEMBERS_FAILED, { error },
),
inviteOrganizationMembersDone: () => createAction(OrganizationActionsTypes.INVITE_ORGANIZATION_MEMBERS_DONE),
inviteOrganizationMemberSuccess: (email: string) => createAction(
OrganizationActionsTypes.INVITE_ORGANIZATION_MEMBER_SUCCESS, { email },
),
inviteOrganizationMemberFailed: (email: string, error: any) => createAction(
OrganizationActionsTypes.INVITE_ORGANIZATION_MEMBER_FAILED, { email, error },
),
leaveOrganization: () => createAction(OrganizationActionsTypes.LEAVE_ORGANIZATION),
leaveOrganizationSuccess: () => createAction(OrganizationActionsTypes.LEAVE_ORGANIZATION_SUCCESS),
leaveOrganizationFailed: (error: any) => createAction(
OrganizationActionsTypes.LEAVE_ORGANIZATION_FAILED, { error },
),
removeOrganizationMember: () => createAction(OrganizationActionsTypes.REMOVE_ORGANIZATION_MEMBER),
removeOrganizationMemberSuccess: () => createAction(OrganizationActionsTypes.REMOVE_ORGANIZATION_MEMBER_SUCCESS),
removeOrganizationMemberFailed: (username: string, error: any) => createAction(
OrganizationActionsTypes.REMOVE_ORGANIZATION_MEMBER_FAILED, { username, error },
),
updateOrganizationMember: () => createAction(OrganizationActionsTypes.UPDATE_ORGANIZATION_MEMBER),
updateOrganizationMemberSuccess: () => createAction(OrganizationActionsTypes.UPDATE_ORGANIZATION_MEMBER_SUCCESS),
updateOrganizationMemberFailed: (username: string, role: string, error: any) => createAction(
OrganizationActionsTypes.UPDATE_ORGANIZATION_MEMBER_FAILED, { username, role, error },
),
};
export function getOrganizationsAsync(): ThunkAction {
return async function (dispatch) {
dispatch(organizationActions.getOrganizations());
try {
const organizations = await core.organizations.get();
let currentOrganization = null;
try {
// this action is dispatched after user is authentificated
// need to configure organization at cvat-core immediately to get relevant data
const curSlug = localStorage.getItem('currentOrganization');
if (curSlug) {
currentOrganization =
organizations.find((organization: any) => organization.slug === curSlug) || null;
if (currentOrganization) {
await core.organizations.activate(currentOrganization);
} else {
// not valid anymore (for example when organization
// does not exist anymore, or the user has been kicked from it)
localStorage.removeItem('currentOrganization');
}
}
dispatch(organizationActions.activateOrganizationSuccess(currentOrganization));
} catch (error) {
dispatch(
organizationActions.activateOrganizationFailed(error, localStorage.getItem('currentOrganization')),
);
} finally {
dispatch(organizationActions.getOrganizationsSuccess(organizations));
}
} catch (error) {
dispatch(organizationActions.getOrganizationsFailed(error));
}
};
}
export function createOrganizationAsync(
organizationData: Store,
onCreateSuccess?: (createdSlug: string) => void,
): ThunkAction {
return async function (dispatch) {
const { slug } = organizationData;
const organization = new core.classes.Organization(organizationData);
dispatch(organizationActions.createOrganization());
try {
const createdOrganization = await organization.save();
dispatch(organizationActions.createOrganizationSuccess(createdOrganization));
if (onCreateSuccess) onCreateSuccess(createdOrganization.slug);
} catch (error) {
dispatch(organizationActions.createOrganizationFailed(slug, error));
}
};
}
export function updateOrganizationAsync(organization: any): ThunkAction {
return async function (dispatch) {
dispatch(organizationActions.updateOrganization());
try {
const updatedOrganization = await organization.save();
dispatch(organizationActions.updateOrganizationSuccess(updatedOrganization));
} catch (error) {
dispatch(organizationActions.updateOrganizationFailed(organization.slug, error));
}
};
}
export function removeOrganizationAsync(organization: any): ThunkAction {
return async function (dispatch) {
try {
await organization.remove();
localStorage.removeItem('currentOrganization');
dispatch(organizationActions.removeOrganizationSuccess(organization.slug));
} catch (error) {
dispatch(organizationActions.removeOrganizationFailed(error, organization.slug));
}
};
}
export function inviteOrganizationMembersAsync(
organization: any,
members: { email: string; role: string }[],
onFinish: () => void,
): ThunkAction {
return async function (dispatch) {
dispatch(organizationActions.inviteOrganizationMembers());
try {
for (let i = 0; i < members.length; i++) {
const { email, role } = members[i];
organization
.invite(email, role)
.then(() => {
dispatch(organizationActions.inviteOrganizationMemberSuccess(email));
})
.catch((error: any) => {
dispatch(organizationActions.inviteOrganizationMemberFailed(email, error));
})
.finally(() => {
if (i === members.length - 1) {
dispatch(organizationActions.inviteOrganizationMembersDone());
onFinish();
}
});
}
} catch (error) {
dispatch(organizationActions.inviteOrganizationMembersFailed(error));
}
};
}
export function leaveOrganizationAsync(organization: any): ThunkAction {
return async function (dispatch, getState) {
const { user } = getState().auth;
dispatch(organizationActions.leaveOrganization());
try {
await organization.leave(user);
dispatch(organizationActions.leaveOrganizationSuccess());
localStorage.removeItem('currentOrganization');
} catch (error) {
dispatch(organizationActions.leaveOrganizationFailed(error));
}
};
}
export function removeOrganizationMemberAsync(
organization: any,
{ user, id }: { user: User; id: number },
onFinish: () => void,
): ThunkAction {
return async function (dispatch) {
dispatch(organizationActions.removeOrganizationMember());
try {
await organization.deleteMembership(id);
dispatch(organizationActions.removeOrganizationMemberSuccess());
onFinish();
} catch (error) {
dispatch(organizationActions.removeOrganizationMemberFailed(user.username, error));
}
};
}
export function updateOrganizationMemberAsync(
organization: any,
{ user, id }: { user: User; id: number },
role: string,
onFinish: () => void,
): ThunkAction {
return async function (dispatch) {
dispatch(organizationActions.updateOrganizationMember());
try {
await organization.updateMembership(id, role);
dispatch(organizationActions.updateOrganizationMemberSuccess());
onFinish();
} catch (error) {
dispatch(organizationActions.updateOrganizationMemberFailed(user.username, role, error));
}
};
}
export type OrganizationActions = ActionUnion<typeof organizationActions>;

@ -4,13 +4,10 @@
import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
import { updateTaskSuccess } from './tasks-actions';
const cvat = getCore(); const cvat = getCore();
export enum ReviewActionTypes { export enum ReviewActionTypes {
INITIALIZE_REVIEW_SUCCESS = 'INITIALIZE_REVIEW_SUCCESS',
INITIALIZE_REVIEW_FAILED = 'INITIALIZE_REVIEW_FAILED',
CREATE_ISSUE = 'CREATE_ISSUE', CREATE_ISSUE = 'CREATE_ISSUE',
START_ISSUE = 'START_ISSUE', START_ISSUE = 'START_ISSUE',
FINISH_ISSUE_SUCCESS = 'FINISH_ISSUE_SUCCESS', FINISH_ISSUE_SUCCESS = 'FINISH_ISSUE_SUCCESS',
@ -35,10 +32,6 @@ export enum ReviewActionTypes {
} }
export const reviewActions = { export const reviewActions = {
initializeReviewSuccess: (reviewInstance: any, frame: number) => (
createAction(ReviewActionTypes.INITIALIZE_REVIEW_SUCCESS, { reviewInstance, frame })
),
initializeReviewFailed: (error: any) => createAction(ReviewActionTypes.INITIALIZE_REVIEW_FAILED, { error }),
createIssue: () => createAction(ReviewActionTypes.CREATE_ISSUE, {}), createIssue: () => createAction(ReviewActionTypes.CREATE_ISSUE, {}),
startIssue: (position: number[]) => ( startIssue: (position: number[]) => (
createAction(ReviewActionTypes.START_ISSUE, { position: cvat.classes.Issue.hull(position) }) createAction(ReviewActionTypes.START_ISSUE, { position: cvat.classes.Issue.hull(position) })
@ -57,9 +50,11 @@ export const reviewActions = {
reopenIssue: (issueId: number) => createAction(ReviewActionTypes.REOPEN_ISSUE, { issueId }), reopenIssue: (issueId: number) => createAction(ReviewActionTypes.REOPEN_ISSUE, { issueId }),
reopenIssueSuccess: () => createAction(ReviewActionTypes.REOPEN_ISSUE_SUCCESS), reopenIssueSuccess: () => createAction(ReviewActionTypes.REOPEN_ISSUE_SUCCESS),
reopenIssueFailed: (error: any) => createAction(ReviewActionTypes.REOPEN_ISSUE_FAILED, { error }), reopenIssueFailed: (error: any) => createAction(ReviewActionTypes.REOPEN_ISSUE_FAILED, { error }),
submitReview: (reviewId: number) => createAction(ReviewActionTypes.SUBMIT_REVIEW, { reviewId }), submitReview: (jobId: number) => createAction(ReviewActionTypes.SUBMIT_REVIEW, { jobId }),
submitReviewSuccess: () => createAction(ReviewActionTypes.SUBMIT_REVIEW_SUCCESS), submitReviewSuccess: () => createAction(ReviewActionTypes.SUBMIT_REVIEW_SUCCESS),
submitReviewFailed: (error: any) => createAction(ReviewActionTypes.SUBMIT_REVIEW_FAILED, { error }), submitReviewFailed: (error: any, jobId: number) => (
createAction(ReviewActionTypes.SUBMIT_REVIEW_FAILED, { error, jobId })
),
removeIssueSuccess: (issueId: number, frame: number) => ( removeIssueSuccess: (issueId: number, frame: number) => (
createAction(ReviewActionTypes.REMOVE_ISSUE_SUCCESS, { issueId, frame }) createAction(ReviewActionTypes.REMOVE_ISSUE_SUCCESS, { issueId, frame })
), ),
@ -72,59 +67,29 @@ export const reviewActions = {
export type ReviewActions = ActionUnion<typeof reviewActions>; export type ReviewActions = ActionUnion<typeof reviewActions>;
export const initializeReviewAsync = (): ThunkAction => async (dispatch, getState) => {
try {
const state = getState();
const {
annotation: {
job: { instance: jobInstance },
player: {
frame: { number: frame },
},
},
} = state;
const reviews = await jobInstance.reviews();
const count = reviews.length;
let reviewInstance = null;
if (count && reviews[count - 1].id < 0) {
reviewInstance = reviews[count - 1];
} else {
reviewInstance = new cvat.classes.Review({ job: jobInstance.id });
}
dispatch(reviewActions.initializeReviewSuccess(reviewInstance, frame));
} catch (error) {
dispatch(reviewActions.initializeReviewFailed(error));
}
};
export const finishIssueAsync = (message: string): ThunkAction => async (dispatch, getState) => { export const finishIssueAsync = (message: string): ThunkAction => async (dispatch, getState) => {
const state = getState(); const state = getState();
const { const {
auth: { user },
annotation: { annotation: {
player: { player: {
frame: { number: frameNumber }, frame: { number: frameNumber },
}, },
job: {
instance: jobInstance,
},
}, },
review: { activeReview, newIssuePosition }, review: { newIssuePosition },
} = state; } = state;
try { try {
const issue = await activeReview.openIssue({ const issue = new cvat.classes.Issue({
job: jobInstance.id,
frame: frameNumber, frame: frameNumber,
position: newIssuePosition, position: newIssuePosition,
owner: user,
comment_set: [
{
message,
author: user,
},
],
}); });
await activeReview.toLocalStorage();
dispatch(reviewActions.finishIssueSuccess(frameNumber, issue)); const savedIssue = await jobInstance.openIssue(issue, message);
dispatch(reviewActions.finishIssueSuccess(frameNumber, savedIssue));
} catch (error) { } catch (error) {
dispatch(reviewActions.finishIssueFailed(error)); dispatch(reviewActions.finishIssueFailed(error));
} }
@ -134,7 +99,7 @@ export const commentIssueAsync = (id: number, message: string): ThunkAction => a
const state = getState(); const state = getState();
const { const {
auth: { user }, auth: { user },
review: { frameIssues, activeReview }, review: { frameIssues },
} = state; } = state;
try { try {
@ -142,11 +107,9 @@ export const commentIssueAsync = (id: number, message: string): ThunkAction => a
const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id); const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id);
await issue.comment({ await issue.comment({
message, message,
author: user, owner: user,
}); });
if (activeReview && activeReview.issues.includes(issue)) {
await activeReview.toLocalStorage();
}
dispatch(reviewActions.commentIssueSuccess()); dispatch(reviewActions.commentIssueSuccess());
} catch (error) { } catch (error) {
dispatch(reviewActions.commentIssueFailed(error)); dispatch(reviewActions.commentIssueFailed(error));
@ -157,17 +120,13 @@ export const resolveIssueAsync = (id: number): ThunkAction => async (dispatch, g
const state = getState(); const state = getState();
const { const {
auth: { user }, auth: { user },
review: { frameIssues, activeReview }, review: { frameIssues },
} = state; } = state;
try { try {
dispatch(reviewActions.resolveIssue(id)); dispatch(reviewActions.resolveIssue(id));
const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id); const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id);
await issue.resolve(user); await issue.resolve(user);
if (activeReview && activeReview.issues.includes(issue)) {
await activeReview.toLocalStorage();
}
dispatch(reviewActions.resolveIssueSuccess()); dispatch(reviewActions.resolveIssueSuccess());
} catch (error) { } catch (error) {
dispatch(reviewActions.resolveIssueFailed(error)); dispatch(reviewActions.resolveIssueFailed(error));
@ -178,47 +137,23 @@ export const reopenIssueAsync = (id: number): ThunkAction => async (dispatch, ge
const state = getState(); const state = getState();
const { const {
auth: { user }, auth: { user },
review: { frameIssues, activeReview }, review: { frameIssues },
} = state; } = state;
try { try {
dispatch(reviewActions.reopenIssue(id)); dispatch(reviewActions.reopenIssue(id));
const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id); const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id);
await issue.reopen(user); await issue.reopen(user);
if (activeReview && activeReview.issues.includes(issue)) {
await activeReview.toLocalStorage();
}
dispatch(reviewActions.reopenIssueSuccess()); dispatch(reviewActions.reopenIssueSuccess());
} catch (error) { } catch (error) {
dispatch(reviewActions.reopenIssueFailed(error)); dispatch(reviewActions.reopenIssueFailed(error));
} }
}; };
export const submitReviewAsync = (review: any): ThunkAction => async (dispatch, getState) => {
const state = getState();
const {
annotation: {
job: { instance: jobInstance },
},
} = state;
try {
dispatch(reviewActions.submitReview(review.id));
await review.submit(jobInstance.id);
const [task] = await cvat.tasks.get({ id: jobInstance.task.id });
dispatch(updateTaskSuccess(task, jobInstance.task.id));
dispatch(reviewActions.submitReviewSuccess());
} catch (error) {
dispatch(reviewActions.submitReviewFailed(error));
}
};
export const deleteIssueAsync = (id: number): ThunkAction => async (dispatch, getState) => { export const deleteIssueAsync = (id: number): ThunkAction => async (dispatch, getState) => {
const state = getState(); const state = getState();
const { const {
review: { frameIssues, activeReview }, review: { frameIssues },
annotation: { annotation: {
player: { player: {
frame: { number: frameNumber }, frame: { number: frameNumber },
@ -229,10 +164,6 @@ export const deleteIssueAsync = (id: number): ThunkAction => async (dispatch, ge
try { try {
const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id); const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id);
await issue.delete(); await issue.delete();
if (activeReview !== null) {
await activeReview.deleteIssue(id);
await activeReview.toLocalStorage();
}
dispatch(reviewActions.removeIssueSuccess(id, frameNumber)); dispatch(reviewActions.removeIssueSuccess(id, frameNumber));
} catch (error) { } catch (error) {
dispatch(reviewActions.removeIssueFailed(error)); dispatch(reviewActions.removeIssueFailed(error));

@ -28,6 +28,9 @@ export enum TasksActionTypes {
UPDATE_TASK = 'UPDATE_TASK', UPDATE_TASK = 'UPDATE_TASK',
UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS', UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS',
UPDATE_TASK_FAILED = 'UPDATE_TASK_FAILED', UPDATE_TASK_FAILED = 'UPDATE_TASK_FAILED',
UPDATE_JOB = 'UPDATE_JOB',
UPDATE_JOB_SUCCESS = 'UPDATE_JOB_SUCCESS',
UPDATE_JOB_FAILED = 'UPDATE_JOB_FAILED',
HIDE_EMPTY_TASKS = 'HIDE_EMPTY_TASKS', HIDE_EMPTY_TASKS = 'HIDE_EMPTY_TASKS',
EXPORT_TASK = 'EXPORT_TASK', EXPORT_TASK = 'EXPORT_TASK',
EXPORT_TASK_SUCCESS = 'EXPORT_TASK_SUCCESS', EXPORT_TASK_SUCCESS = 'EXPORT_TASK_SUCCESS',
@ -38,46 +41,42 @@ export enum TasksActionTypes {
SWITCH_MOVE_TASK_MODAL_VISIBLE = 'SWITCH_MOVE_TASK_MODAL_VISIBLE', SWITCH_MOVE_TASK_MODAL_VISIBLE = 'SWITCH_MOVE_TASK_MODAL_VISIBLE',
} }
function getTasks(): AnyAction { function getTasks(query: TasksQuery): AnyAction {
const action = { const action = {
type: TasksActionTypes.GET_TASKS, type: TasksActionTypes.GET_TASKS,
payload: {}, payload: {
query,
},
}; };
return action; return action;
} }
export function getTasksSuccess( export function getTasksSuccess(array: any[], previews: string[], count: number): AnyAction {
array: any[], previews: string[], count: number, query: Partial<TasksQuery>,
): AnyAction {
const action = { const action = {
type: TasksActionTypes.GET_TASKS_SUCCESS, type: TasksActionTypes.GET_TASKS_SUCCESS,
payload: { payload: {
previews, previews,
array, array,
count, count,
query,
}, },
}; };
return action; return action;
} }
function getTasksFailed(error: any, query: Partial<TasksQuery>): AnyAction { function getTasksFailed(error: any): AnyAction {
const action = { const action = {
type: TasksActionTypes.GET_TASKS_FAILED, type: TasksActionTypes.GET_TASKS_FAILED,
payload: { payload: { error },
error,
query,
},
}; };
return action; return action;
} }
export function getTasksAsync(query: Partial<TasksQuery>): ThunkAction<Promise<void>, {}, {}, AnyAction> { export function getTasksAsync(query: TasksQuery): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(getTasks()); dispatch(getTasks(query));
// We need remove all keys with null values from query // We need remove all keys with null values from query
const filteredQuery = { ...query }; const filteredQuery = { ...query };
@ -91,7 +90,7 @@ export function getTasksAsync(query: Partial<TasksQuery>): ThunkAction<Promise<v
try { try {
result = await cvat.tasks.get(filteredQuery); result = await cvat.tasks.get(filteredQuery);
} catch (error) { } catch (error) {
dispatch(getTasksFailed(error, query)); dispatch(getTasksFailed(error));
return; return;
} }
@ -100,7 +99,7 @@ export function getTasksAsync(query: Partial<TasksQuery>): ThunkAction<Promise<v
dispatch(getInferenceStatusAsync()); dispatch(getInferenceStatusAsync());
dispatch(getTasksSuccess(array, await Promise.all(promises), result.count, query)); dispatch(getTasksSuccess(array, await Promise.all(promises), result.count));
}; };
} }
@ -442,6 +441,33 @@ export function updateTaskSuccess(task: any, taskID: number): AnyAction {
return action; return action;
} }
function updateJob(): AnyAction {
const action = {
type: TasksActionTypes.UPDATE_JOB,
payload: { },
};
return action;
}
function updateJobSuccess(jobInstance: any): AnyAction {
const action = {
type: TasksActionTypes.UPDATE_JOB_SUCCESS,
payload: { jobInstance },
};
return action;
}
function updateJobFailed(jobID: number, error: any): AnyAction {
const action = {
type: TasksActionTypes.UPDATE_JOB_FAILED,
payload: { jobID, error },
};
return action;
}
function updateTaskFailed(error: any, task: any): AnyAction { function updateTaskFailed(error: any, task: any): AnyAction {
const action = { const action = {
type: TasksActionTypes.UPDATE_TASK_FAILED, type: TasksActionTypes.UPDATE_TASK_FAILED,
@ -452,17 +478,11 @@ function updateTaskFailed(error: any, task: any): AnyAction {
} }
export function updateTaskAsync(taskInstance: any): ThunkAction<Promise<void>, CombinedState, {}, AnyAction> { export function updateTaskAsync(taskInstance: any): ThunkAction<Promise<void>, CombinedState, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>, getState: () => CombinedState): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try { try {
dispatch(updateTask()); dispatch(updateTask());
const currentUser = getState().auth.user; const task = await taskInstance.save();
await taskInstance.save();
const nextUser = getState().auth.user;
const userFetching = getState().auth.fetching;
if (!userFetching && nextUser && currentUser.username === nextUser.username) {
const [task] = await cvat.tasks.get({ id: taskInstance.id });
dispatch(updateTaskSuccess(task, taskInstance.id)); dispatch(updateTaskSuccess(task, taskInstance.id));
}
} catch (error) { } catch (error) {
// try abort all changes // try abort all changes
let task = null; let task = null;
@ -483,21 +503,11 @@ export function updateTaskAsync(taskInstance: any): ThunkAction<Promise<void>, C
export function updateJobAsync(jobInstance: any): ThunkAction<Promise<void>, {}, {}, AnyAction> { export function updateJobAsync(jobInstance: any): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try { try {
dispatch(updateTask()); dispatch(updateJob());
await jobInstance.save(); const newJob = await jobInstance.save();
const [task] = await cvat.tasks.get({ id: jobInstance.task.id }); dispatch(updateJobSuccess(newJob));
dispatch(updateTaskSuccess(task, jobInstance.task.id));
} catch (error) { } catch (error) {
// try abort all changes dispatch(updateJobFailed(jobInstance.id, error));
let task = null;
try {
[task] = await cvat.tasks.get({ id: jobInstance.task.id });
} catch (fetchError) {
dispatch(updateTaskFailed(error, jobInstance.task));
return;
}
dispatch(updateTaskFailed(error, task));
} }
}; };
} }

@ -1 +0,0 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M25.9 23c-1.73 0-2.561 1-5.4 1-2.839 0-3.664-1-5.4-1-4.472 0-8.1 3.762-8.1 8.4V33c0 1.656 1.296 3 2.893 3h21.214C32.704 36 34 34.656 34 33v-1.6c0-4.637-3.628-8.4-8.1-8.4zm5.207 10H9.893v-1.6c0-2.975 2.338-5.4 5.207-5.4.88 0 2.308 1 5.4 1 3.116 0 4.514-1 5.4-1 2.869 0 5.207 2.425 5.207 5.4V33zM20.5 22c4.791 0 8.679-4.031 8.679-9S25.29 4 20.5 4s-8.679 4.031-8.679 9 3.888 9 8.679 9zm0-15c3.188 0 5.786 2.694 5.786 6s-2.598 6-5.786 6c-3.188 0-5.786-2.694-5.786-6s2.598-6 5.786-6z" fill="#000" fill-rule="nonzero"/></svg>

Before

Width:  |  Height:  |  Size: 591 B

@ -4,7 +4,7 @@
$grid-unit-size: 8px; $grid-unit-size: 8px;
$header-height: $grid-unit-size * 7; $header-height: $grid-unit-size * 6;
$layout-sm-grid-size: $grid-unit-size * 0.5; $layout-sm-grid-size: $grid-unit-size * 0.5;
$layout-lg-grid-size: $grid-unit-size * 2; $layout-lg-grid-size: $grid-unit-size * 2;

@ -10,14 +10,12 @@ import Spin from 'antd/lib/spin';
import notification from 'antd/lib/notification'; import notification from 'antd/lib/notification';
import AttributeAnnotationWorkspace from 'components/annotation-page/attribute-annotation-workspace/attribute-annotation-workspace'; import AttributeAnnotationWorkspace from 'components/annotation-page/attribute-annotation-workspace/attribute-annotation-workspace';
import SubmitAnnotationsModal from 'components/annotation-page/request-review-modal';
import ReviewAnnotationsWorkspace from 'components/annotation-page/review-workspace/review-workspace'; import ReviewAnnotationsWorkspace from 'components/annotation-page/review-workspace/review-workspace';
import SubmitReviewModal from 'components/annotation-page/review/submit-review-modal';
import StandardWorkspaceComponent from 'components/annotation-page/standard-workspace/standard-workspace'; import StandardWorkspaceComponent from 'components/annotation-page/standard-workspace/standard-workspace';
import StandardWorkspace3DComponent from 'components/annotation-page/standard3D-workspace/standard3D-workspace'; import StandardWorkspace3DComponent from 'components/annotation-page/standard3D-workspace/standard3D-workspace';
import TagAnnotationWorkspace from 'components/annotation-page/tag-annotation-workspace/tag-annotation-workspace'; import TagAnnotationWorkspace from 'components/annotation-page/tag-annotation-workspace/tag-annotation-workspace';
import FiltersModalComponent from 'components/annotation-page/top-bar/filters-modal'; import FiltersModalComponent from 'components/annotation-page/top-bar/filters-modal';
import StatisticsModalContainer from 'containers/annotation-page/top-bar/statistics-modal'; import StatisticsModalComponent from 'components/annotation-page/top-bar/statistics-modal';
import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar'; import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-bar';
import { Workspace } from 'reducers/interfaces'; import { Workspace } from 'reducers/interfaces';
import { usePrevious } from 'utils/hooks'; import { usePrevious } from 'utils/hooks';
@ -66,15 +64,15 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
}, [job, fetching]); }, [job, fetching]);
useEffect(() => { useEffect(() => {
if (prevFetching && !fetching && !prevJob && job && !job.task.labels.length) { if (prevFetching && !fetching && !prevJob && job && !job.labels.length) {
notification.warning({ notification.warning({
message: 'No labels', message: 'No labels',
description: ( description: (
<span> <span>
{`${job.task.projectId ? 'Project' : 'Task'} ${ {`${job.projectId ? 'Project' : 'Task'} ${
job.task.projectId || job.task.id job.projectId || job.taskId
} does not contain any label. `} } does not contain any label. `}
<a href={`/${job.task.projectId ? 'projects' : 'tasks'}/${job.task.projectId || job.task.id}/`}> <a href={`/${job.projectId ? 'projects' : 'tasks'}/${job.projectId || job.id}/`}>
Add Add
</a> </a>
{' the first one for editing annotation.'} {' the first one for editing annotation.'}
@ -132,9 +130,7 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
</Layout.Content> </Layout.Content>
)} )}
<FiltersModalComponent /> <FiltersModalComponent />
<StatisticsModalContainer /> <StatisticsModalComponent />
<SubmitAnnotationsModal />
<SubmitReviewModal />
</Layout> </Layout>
); );
} }

@ -121,7 +121,7 @@ function AppearanceBlock(props: Props): JSX.Element {
jobInstance, jobInstance,
} = props; } = props;
const is2D = jobInstance.task.dimension === DimensionType.DIM_2D; const is2D = jobInstance.dimension === DimensionType.DIM_2D;
return ( return (
<Collapse <Collapse

@ -389,7 +389,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
state.objectType = state.objectType || activeObjectType; state.objectType = state.objectType || activeObjectType;
state.label = state.label || jobInstance.task.labels.filter((label: any) => label.id === activeLabelID)[0]; state.label = state.label || jobInstance.labels.filter((label: any) => label.id === activeLabelID)[0];
state.occluded = state.occluded || false; state.occluded = state.occluded || false;
state.frame = frame; state.frame = frame;
const objectState = new cvat.classes.ObjectState(state); const objectState = new cvat.classes.ObjectState(state);

@ -237,7 +237,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
} }
state.objectType = state.objectType || activeObjectType; state.objectType = state.objectType || activeObjectType;
state.label = state.label || jobInstance.task.labels.filter((label: any) => label.id === activeLabelID)[0]; state.label = state.label || jobInstance.labels.filter((label: any) => label.id === activeLabelID)[0];
state.occluded = state.occluded || false; state.occluded = state.occluded || false;
state.frame = frame; state.frame = frame;
state.zOrder = 0; state.zOrder = 0;

@ -1,64 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState } from 'react';
import { AnyAction } from 'redux';
import { useSelector, useDispatch } from 'react-redux';
import { useHistory } from 'react-router';
import Text from 'antd/lib/typography/Text';
import Title from 'antd/lib/typography/Title';
import Modal from 'antd/lib/modal';
import { Row, Col } from 'antd/lib/grid';
import UserSelector, { User } from 'components/task-page/user-selector';
import { CombinedState, TaskStatus } from 'reducers/interfaces';
import { switchRequestReviewDialog } from 'actions/annotation-actions';
import { updateJobAsync } from 'actions/tasks-actions';
export default function RequestReviewModal(): JSX.Element | null {
const dispatch = useDispatch();
const history = useHistory();
const isVisible = useSelector((state: CombinedState): boolean => state.annotation.requestReviewDialogVisible);
const job = useSelector((state: CombinedState): any => state.annotation.job.instance);
const [reviewer, setReviewer] = useState<User | null>(job.reviewer ? job.reviewer : null);
const close = (): AnyAction => dispatch(switchRequestReviewDialog(false));
const submitAnnotations = (): void => {
job.reviewer = reviewer;
job.status = TaskStatus.REVIEW;
dispatch(updateJobAsync(job));
history.push(`/tasks/${job.task.id}`);
};
if (!isVisible) {
return null;
}
return (
<Modal
className='cvat-request-review-dialog'
visible={isVisible}
destroyOnClose
onCancel={close}
onOk={submitAnnotations}
okText='Submit'
>
<Row justify='start'>
<Col>
<Title level={4}>Assign a user who is responsible for review</Title>
</Col>
</Row>
<Row align='middle' justify='start'>
<Col>
<Text type='secondary'>Reviewer: </Text>
</Col>
<Col offset={1}>
<UserSelector value={reviewer} onSelect={setReviewer} />
</Col>
</Row>
<Row justify='start'>
<Text type='secondary'>You might not be able to change the job after this action. Continue?</Text>
</Row>
</Modal>
);
}

@ -1,14 +1,10 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import './styles.scss'; import './styles.scss';
import React, { useEffect } from 'react'; import React from 'react';
import Layout from 'antd/lib/layout'; import Layout from 'antd/lib/layout';
import { useDispatch, useSelector } from 'react-redux';
import { CombinedState } from 'reducers/interfaces';
import { initializeReviewAsync } from 'actions/review-actions';
import CanvasWrapperContainer from 'containers/annotation-page/canvas/canvas-wrapper'; import CanvasWrapperContainer from 'containers/annotation-page/canvas/canvas-wrapper';
import ControlsSideBarContainer from 'containers/annotation-page/review-workspace/controls-side-bar/controls-side-bar'; import ControlsSideBarContainer from 'containers/annotation-page/review-workspace/controls-side-bar/controls-side-bar';
@ -18,26 +14,6 @@ import CanvasContextMenuContainer from 'containers/annotation-page/canvas/canvas
import IssueAggregatorComponent from 'components/annotation-page/review/issues-aggregator'; import IssueAggregatorComponent from 'components/annotation-page/review/issues-aggregator';
export default function ReviewWorkspaceComponent(): JSX.Element { export default function ReviewWorkspaceComponent(): JSX.Element {
const dispatch = useDispatch();
const frame = useSelector((state: CombinedState): number => state.annotation.player.frame.number);
const states = useSelector((state: CombinedState): any[] => state.annotation.annotations.states);
const review = useSelector((state: CombinedState): any => state.review.activeReview);
useEffect(() => {
if (review) {
review.reviewFrame(frame);
review.reviewStates(
states
.map((state: any): number | undefined => state.serverID)
.filter((serverID: number | undefined): boolean => typeof serverID !== 'undefined')
.map((serverID: number | undefined): string => `${frame}_${serverID}`),
);
}
}, [frame, states, review]);
useEffect(() => {
dispatch(initializeReviewAsync());
}, []);
return ( return (
<Layout hasSider className='cvat-review-workspace'> <Layout hasSider className='cvat-review-workspace'>
<ControlsSideBarContainer /> <ControlsSideBarContainer />

@ -89,7 +89,7 @@ export default function IssueDialog(props: Props): JSX.Element {
<Comment <Comment
avatar={null} avatar={null}
key={_comment.id} key={_comment.id}
author={<Text strong>{_comment.author ? _comment.author.username : 'Unknown'}</Text>} author={<Text strong>{_comment.owner ? _comment.owner.username : 'Unknown'}</Text>}
content={<p>{_comment.message}</p>} content={<p>{_comment.message}</p>}
datetime={( datetime={(
<CVATTooltip title={created.format('MMMM Do YYYY')}> <CVATTooltip title={created.format('MMMM Do YYYY')}>

@ -84,8 +84,8 @@ export default function IssueAggregatorComponent(): JSX.Element | null {
const { geometry } = canvasInstance; const { geometry } = canvasInstance;
for (const issue of frameIssues) { for (const issue of frameIssues) {
if (issuesHidden) break; if (issuesHidden) break;
if (issuesResolvedHidden && !!issue.resolvedDate) continue; const issueResolved = issue.resolved;
const issueResolved = !!issue.resolver; if (issuesResolvedHidden && issueResolved) continue;
const offset = 15; const offset = 15;
const translated = issue.position.map((coord: number): number => coord + geometry.offset); const translated = issue.position.map((coord: number): number => coord + geometry.offset);
const minX = Math.min(...translated.filter((_: number, idx: number): boolean => idx % 2 === 0)) + offset; const minX = Math.min(...translated.filter((_: number, idx: number): boolean => idx % 2 === 0)) + offset;

@ -1,149 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState, useEffect } from 'react';
import { AnyAction } from 'redux';
import { useSelector, useDispatch } from 'react-redux';
import Text from 'antd/lib/typography/Text';
import Title from 'antd/lib/typography/Title';
import Modal from 'antd/lib/modal';
import Radio, { RadioChangeEvent } from 'antd/lib/radio';
import RadioButton from 'antd/lib/radio/radioButton';
import Description from 'antd/lib/descriptions';
import Rate from 'antd/lib/rate';
import { Row, Col } from 'antd/lib/grid';
import UserSelector, { User } from 'components/task-page/user-selector';
import { CombinedState, ReviewStatus } from 'reducers/interfaces';
import { switchSubmitReviewDialog } from 'actions/annotation-actions';
import { submitReviewAsync } from 'actions/review-actions';
import { clamp } from 'utils/math';
import { useHistory } from 'react-router';
function computeEstimatedQuality(reviewedStates: number, openedIssues: number): number {
if (reviewedStates === 0 && openedIssues === 0) {
return 5; // corner case
}
const K = 2; // means how many reviewed states are equivalent to one issue
const quality = reviewedStates / (reviewedStates + K * openedIssues);
return clamp(+(5 * quality).toPrecision(2), 0, 5);
}
export default function SubmitReviewModal(): JSX.Element | null {
const dispatch = useDispatch();
const history = useHistory();
const isVisible = useSelector((state: CombinedState): boolean => state.annotation.submitReviewDialogVisible);
const job = useSelector((state: CombinedState): any => state.annotation.job.instance);
const activeReview = useSelector((state: CombinedState): any => state.review.activeReview);
const reviewIsBeingSubmitted = useSelector((state: CombinedState): any => state.review.fetching.reviewId);
const numberOfIssues = useSelector((state: CombinedState): any => state.review.issues.length);
const [isSubmitting, setIsSubmitting] = useState<boolean>(false);
const numberOfNewIssues = activeReview ? activeReview.issues.length : 0;
const reviewedFrames = activeReview ? activeReview.reviewedFrames.length : 0;
const reviewedStates = activeReview ? activeReview.reviewedStates.length : 0;
const [reviewer, setReviewer] = useState<User | null>(job.reviewer ? job.reviewer : null);
const [reviewStatus, setReviewStatus] = useState<string>(ReviewStatus.ACCEPTED);
const [estimatedQuality, setEstimatedQuality] = useState<number>(0);
const close = (): AnyAction => dispatch(switchSubmitReviewDialog(false));
const submitReview = (): void => {
activeReview.estimatedQuality = estimatedQuality;
activeReview.status = reviewStatus;
if (reviewStatus === ReviewStatus.REVIEW_FURTHER) {
activeReview.reviewer = reviewer;
}
dispatch(submitReviewAsync(activeReview));
};
useEffect(() => {
setEstimatedQuality(computeEstimatedQuality(reviewedStates, numberOfNewIssues));
}, [reviewedStates, numberOfNewIssues]);
useEffect(() => {
if (!isSubmitting && activeReview && activeReview.id === reviewIsBeingSubmitted) {
setIsSubmitting(true);
} else if (isSubmitting && reviewIsBeingSubmitted === null) {
setIsSubmitting(false);
close();
history.push(`/tasks/${job.task.id}`);
}
}, [reviewIsBeingSubmitted, activeReview]);
if (!isVisible) {
return null;
}
return (
<Modal
className='cvat-submit-review-dialog'
visible={isVisible}
destroyOnClose
confirmLoading={isSubmitting}
onOk={submitReview}
onCancel={close}
okText='Submit'
width={650}
>
<Row justify='start'>
<Col>
<Title level={4}>Submitting your review</Title>
</Col>
</Row>
<Row justify='start'>
<Col span={12}>
<Description title='Review summary' layout='horizontal' column={1} size='small' bordered>
<Description.Item label='Estimated quality: '>{estimatedQuality}</Description.Item>
<Description.Item label='Issues: '>
<Text>{numberOfIssues}</Text>
{!!numberOfNewIssues && <Text strong>{` (+${numberOfNewIssues})`}</Text>}
</Description.Item>
<Description.Item label='Reviewed frames '>{reviewedFrames}</Description.Item>
<Description.Item label='Reviewed objects: '>{reviewedStates}</Description.Item>
</Description>
</Col>
<Col span={11} offset={1}>
<Row>
<Col>
<Radio.Group
value={reviewStatus}
onChange={(event: RadioChangeEvent) => {
if (typeof event.target.value !== 'undefined') {
setReviewStatus(event.target.value);
}
}}
>
<RadioButton value={ReviewStatus.ACCEPTED}>Accept</RadioButton>
<RadioButton value={ReviewStatus.REVIEW_FURTHER}>Review next</RadioButton>
<RadioButton value={ReviewStatus.REJECTED}>Reject</RadioButton>
</Radio.Group>
{reviewStatus === ReviewStatus.REVIEW_FURTHER && (
<Row align='middle' justify='start'>
<Col span={7}>
<Text type='secondary'>Reviewer: </Text>
</Col>
<Col span={16} offset={1}>
<UserSelector value={reviewer} onSelect={setReviewer} />
</Col>
</Row>
)}
<Row justify='center' align='middle'>
<Col>
<Rate
value={Math.round(estimatedQuality)}
onChange={(value: number | undefined) => {
if (typeof value !== 'undefined') {
setEstimatedQuality(value);
}
}}
/>
</Col>
</Row>
</Col>
</Row>
</Col>
</Row>
</Modal>
);
}

@ -52,7 +52,7 @@ function DrawShapePopoverComponent(props: Props): JSX.Element {
jobInstance, jobInstance,
} = props; } = props;
const is2D = jobInstance.task.dimension === DimensionType.DIM_2D; const is2D = jobInstance.dimension === DimensionType.DIM_2D;
return ( return (
<div className='cvat-draw-shape-popover-content'> <div className='cvat-draw-shape-popover-content'>

@ -52,7 +52,7 @@ function GroupControl(props: Props): JSX.Element {
const title = [ const title = [
`Group shapes${ `Group shapes${
jobInstance && jobInstance.task.dimension === DimensionType.DIM_3D ? '' : '/tracks' jobInstance && jobInstance.dimension === DimensionType.DIM_3D ? '' : '/tracks'
} ${switchGroupShortcut}. `, } ${switchGroupShortcut}. `,
`Select and press ${resetGroupShortcut} to reset a group.`, `Select and press ${resetGroupShortcut} to reset a group.`,
].join(' '); ].join(' ');

@ -340,7 +340,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
try { try {
// run server request // run server request
this.setState({ fetching: true }); this.setState({ fetching: true });
const response = await core.lambda.call(jobInstance.task, interactor, data); const response = await core.lambda.call(jobInstance.taskId, interactor, data);
// approximation with cv.approxPolyDP // approximation with cv.approxPolyDP
const approximated = await this.approximateResponsePoints(response); const approximated = await this.approximateResponsePoints(response);
@ -430,7 +430,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
} }
const { activeLabelID } = this.state; const { activeLabelID } = this.state;
const [label] = jobInstance.task.labels.filter((_label: any): boolean => _label.id === activeLabelID); const [label] = jobInstance.labels.filter((_label: any): boolean => _label.id === activeLabelID);
const { isDone, shapesUpdated } = (e as CustomEvent).detail; const { isDone, shapesUpdated } = (e as CustomEvent).detail;
if (!isDone || !shapesUpdated) { if (!isDone || !shapesUpdated) {
@ -535,8 +535,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
<EnvironmentFilled <EnvironmentFilled
onClick={() => { onClick={() => {
const filteredStates = trackedShapes.filter( const filteredStates = trackedShapes.filter(
(trackedShape: TrackedShape) => (trackedShape: TrackedShape) => trackedShape.clientID !== clientID,
trackedShape.clientID !== clientID,
); );
/* eslint no-param-reassign: ["error", { "props": false }] */ /* eslint no-param-reassign: ["error", { "props": false }] */
objectState.descriptions = []; objectState.descriptions = [];
@ -690,8 +689,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
0, 0,
); );
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const response = await core.lambda.call(jobInstance.task, tracker, { const response = await core.lambda.call(jobInstance.taskId, tracker, {
task: jobInstance.task,
frame: frame - 1, frame: frame - 1,
shapes: trackableObjects.shapes, shapes: trackableObjects.shapes,
}); });
@ -736,8 +734,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
0, 0,
); );
// eslint-disable-next-line no-await-in-loop // eslint-disable-next-line no-await-in-loop
const response = await core.lambda.call(jobInstance.task, tracker, { const response = await core.lambda.call(jobInstance.taskId, tracker, {
task: jobInstance.task,
frame: frame - 1, frame: frame - 1,
shapes: trackableObjects.shapes, shapes: trackableObjects.shapes,
states: trackableObjects.states, states: trackableObjects.states,
@ -1022,16 +1019,16 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
<DetectorRunner <DetectorRunner
withCleanup={false} withCleanup={false}
models={detectors} models={detectors}
task={jobInstance.task} labels={jobInstance.labels}
runInference={async (task: any, model: Model, body: object) => { dimension={jobInstance.dimension}
runInference={async (model: Model, body: object) => {
try { try {
this.setState({ mode: 'detection', fetching: true }); this.setState({ mode: 'detection', fetching: true });
const result = await core.lambda.call(task, model, { ...body, frame }); const result = await core.lambda.call(jobInstance.taskId, model, { ...body, frame });
const states = result.map( const states = result.map(
(data: any): any => (data: any): any => new core.classes.ObjectState({
new core.classes.ObjectState({
shapeType: data.type, shapeType: data.type,
label: task.labels.filter((label: any): boolean => label.name === data.label)[0], label: jobInstance.labels.filter((label: any): boolean => label.name === data.label)[0],
points: data.points, points: data.points,
objectType: ObjectType.SHAPE, objectType: ObjectType.SHAPE,
frame, frame,

@ -21,11 +21,9 @@ export default function LabelsListComponent(): JSX.Element {
const frame = useSelector((state: CombinedState): number => state.annotation.player.frame.number); const frame = useSelector((state: CombinedState): number => state.annotation.player.frame.number);
const frameIssues = useSelector((state: CombinedState): any[] => state.review.frameIssues); const frameIssues = useSelector((state: CombinedState): any[] => state.review.frameIssues);
const issues = useSelector((state: CombinedState): any[] => state.review.issues); const issues = useSelector((state: CombinedState): any[] => state.review.issues);
const activeReview = useSelector((state: CombinedState): any => state.review.activeReview);
const issuesHidden = useSelector((state: CombinedState): any => state.review.issuesHidden); const issuesHidden = useSelector((state: CombinedState): any => state.review.issuesHidden);
const issuesResolvedHidden = useSelector((state: CombinedState): any => state.review.issuesResolvedHidden); const issuesResolvedHidden = useSelector((state: CombinedState): any => state.review.issuesResolvedHidden);
const combinedIssues = activeReview ? issues.concat(activeReview.issues) : issues; const frames = issues.map((issue: any): number => issue.frame).sort((a: number, b: number) => +a - +b);
const frames = combinedIssues.map((issue: any): number => issue.frame).sort((a: number, b: number) => +a - +b);
const nearestLeft = frames.filter((_frame: number): boolean => _frame < frame).reverse()[0]; const nearestLeft = frames.filter((_frame: number): boolean => _frame < frame).reverse()[0];
const dinamicLeftProps: any = Number.isInteger(nearestLeft) ? const dinamicLeftProps: any = Number.isInteger(nearestLeft) ?
{ {
@ -101,6 +99,7 @@ export default function LabelsListComponent(): JSX.Element {
{frameIssues.map( {frameIssues.map(
(frameIssue: any): JSX.Element => ( (frameIssue: any): JSX.Element => (
<div <div
key={frameIssue.id}
id={`cvat-objects-sidebar-issue-item-${frameIssue.id}`} id={`cvat-objects-sidebar-issue-item-${frameIssue.id}`}
className='cvat-objects-sidebar-issue-item' className='cvat-objects-sidebar-issue-item'
onMouseEnter={() => { onMouseEnter={() => {
@ -120,20 +119,10 @@ export default function LabelsListComponent(): JSX.Element {
} }
}} }}
> >
{frameIssue.resolver ? ( {frameIssue.resolved ? (
<Alert <Alert message='Resolved' type='success' showIcon />
description={<span>{`By ${frameIssue.resolver.username}`}</span>}
message='Resolved'
type='success'
showIcon
/>
) : ( ) : (
<Alert <Alert message='Opened' type='warning' showIcon />
description={<span>{`By ${frameIssue.owner.username}`}</span>}
message='Opened'
type='warning'
showIcon
/>
)} )}
</div> </div>
), ),

@ -225,7 +225,7 @@ export default function ItemMenu(props: Props): JSX.Element {
REMOVE_ITEM = 'remove_item', REMOVE_ITEM = 'remove_item',
} }
const is2D = jobInstance.task.dimension === DimensionType.DIM_2D; const is2D = jobInstance.dimension === DimensionType.DIM_2D;
return ( return (
<Menu className='cvat-object-item-menu' selectable={false}> <Menu className='cvat-object-item-menu' selectable={false}>

@ -81,11 +81,7 @@ function ObjectsSideBar(props: StateToProps & DispatchToProps & OwnProps): JSX.E
collapseSidebar(); collapseSidebar();
}; };
let is2D = true; const is2D = jobInstance ? jobInstance.dimension === DimensionType.DIM_2D : true;
if (jobInstance) {
is2D = jobInstance.task.dimension === DimensionType.DIM_2D;
}
return ( return (
<Layout.Sider <Layout.Sider
className='cvat-objects-sidebar' className='cvat-objects-sidebar'

@ -447,22 +447,12 @@ button.cvat-predictor-button {
} }
} }
.cvat-request-review-dialog {
> .ant-modal-content > .ant-modal-body {
> div:nth-child(2) {
margin-top: $grid-unit-size * 2;
}
> div:nth-child(3) {
margin-top: $grid-unit-size * 2;
}
}
}
.cvat-submit-review-dialog { .cvat-submit-review-dialog {
> .ant-modal-content > .ant-modal-body { .ant-modal-body {
> div:nth-child(2) > div:nth-child(2) { > div.ant-row:nth-child(2) {
.ant-col { > .ant-col {
width: 100%;
> div:nth-child(2) { > div:nth-child(2) {
margin-top: $grid-unit-size * 2; margin-top: $grid-unit-size * 2;
margin-bottom: $grid-unit-size * 2; margin-bottom: $grid-unit-size * 2;
@ -496,3 +486,12 @@ button.cvat-predictor-button {
} }
} }
} }
.cvat-submenu-current-job-state-item {
&::after {
content: ' \2713';
float: right;
}
font-weight: bold;
}

@ -3,25 +3,30 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React from 'react';
import { withRouter, RouteComponentProps } from 'react-router';
import Menu from 'antd/lib/menu'; import Menu from 'antd/lib/menu';
import Modal from 'antd/lib/modal'; import Modal from 'antd/lib/modal';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { import InputNumber from 'antd/lib/input-number';
InputNumber, Tooltip, Checkbox, Collapse, import Checkbox from 'antd/lib/checkbox';
} from 'antd'; import Collapse from 'antd/lib/collapse';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import { MenuInfo } from 'rc-menu/lib/interface'; import { MenuInfo } from 'rc-menu/lib/interface';
import CVATTooltip from 'components/common/cvat-tooltip';
import LoadSubmenu from 'components/actions-menu/load-submenu'; import LoadSubmenu from 'components/actions-menu/load-submenu';
import { DimensionType } from '../../../reducers/interfaces'; import getCore from 'cvat-core-wrapper';
import { JobStage } from 'reducers/interfaces';
const core = getCore();
interface Props { interface Props {
taskMode: string; taskMode: string;
loaders: any[]; loaders: any[];
dumpers: any[]; dumpers: any[];
loadActivity: string | null; loadActivity: string | null;
isReviewer: boolean;
jobInstance: any; jobInstance: any;
onClickMenu(params: MenuInfo): void; onClickMenu(params: MenuInfo): void;
onUploadAnnotations(format: string, file: File): void; onUploadAnnotations(format: string, file: File): void;
@ -36,19 +41,17 @@ export enum Actions {
EXPORT_TASK_DATASET = 'export_task_dataset', EXPORT_TASK_DATASET = 'export_task_dataset',
REMOVE_ANNO = 'remove_anno', REMOVE_ANNO = 'remove_anno',
OPEN_TASK = 'open_task', OPEN_TASK = 'open_task',
REQUEST_REVIEW = 'request_review',
SUBMIT_REVIEW = 'submit_review',
FINISH_JOB = 'finish_job', FINISH_JOB = 'finish_job',
RENEW_JOB = 'renew_job', RENEW_JOB = 'renew_job',
} }
export default function AnnotationMenuComponent(props: Props): JSX.Element { function AnnotationMenuComponent(props: Props & RouteComponentProps): JSX.Element {
const { const {
loaders, loaders,
loadActivity, loadActivity,
isReviewer,
jobInstance, jobInstance,
stopFrame, stopFrame,
history,
onClickMenu, onClickMenu,
onUploadAnnotations, onUploadAnnotations,
removeAnnotations, removeAnnotations,
@ -56,8 +59,10 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element {
saveAnnotations, saveAnnotations,
} = props; } = props;
const jobStatus = jobInstance.status; const jobStage = jobInstance.stage;
const taskID = jobInstance.task.id; const jobState = jobInstance.state;
const taskID = jobInstance.taskId;
const { JobState } = core.enums;
function onClickMenuWrapper(params: MenuInfo): void { function onClickMenuWrapper(params: MenuInfo): void {
function checkUnsavedChanges(_params: MenuInfo): void { function checkUnsavedChanges(_params: MenuInfo): void {
@ -117,7 +122,7 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element {
max={stopFrame} max={stopFrame}
onChange={(value) => { removeUpTo = value; }} onChange={(value) => { removeUpTo = value; }}
/> />
<Tooltip title='Applicable only for annotations in range'> <CVATTooltip title='Applicable only for annotations in range'>
<br /> <br />
<br /> <br />
<Checkbox <Checkbox
@ -127,7 +132,7 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element {
> >
Delete only keyframes for tracks Delete only keyframes for tracks
</Checkbox> </Checkbox>
</Tooltip> </CVATTooltip>
</Panel> </Panel>
</Collapse> </Collapse>
</div> </div>
@ -142,12 +147,21 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element {
}, },
okText: 'Delete', okText: 'Delete',
}); });
} else if (params.key === Actions.REQUEST_REVIEW) { } else if (params.key.startsWith('state:')) {
Modal.confirm({
title: 'Do you want to change current job state?',
content: `Job state will be switched to "${params.key.split(':')[1]}". Continue?`,
okText: 'Continue',
cancelText: 'Cancel',
className: 'cvat-modal-content-change-job-state',
onOk: () => {
checkUnsavedChanges(params); checkUnsavedChanges(params);
},
});
} else if (params.key === Actions.FINISH_JOB) { } else if (params.key === Actions.FINISH_JOB) {
Modal.confirm({ Modal.confirm({
title: 'The job status is going to be switched', title: 'The job stage is going to be switched',
content: 'Status will be changed to "completed". Would you like to continue?', content: 'Stage will be changed to "acceptance". Would you like to continue?',
okText: 'Continue', okText: 'Continue',
cancelText: 'Cancel', cancelText: 'Cancel',
className: 'cvat-modal-content-finish-job', className: 'cvat-modal-content-finish-job',
@ -157,8 +171,8 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element {
}); });
} else if (params.key === Actions.RENEW_JOB) { } else if (params.key === Actions.RENEW_JOB) {
Modal.confirm({ Modal.confirm({
title: 'The job status is going to be switched', title: 'Do you want to renew the job?',
content: 'Status will be changed to "annotations". Would you like to continue?', content: 'Stage will be set to "in progress", state will be set to "annotation". Would you like to continue?',
okText: 'Continue', okText: 'Continue',
cancelText: 'Cancel', cancelText: 'Cancel',
className: 'cvat-modal-content-renew-job', className: 'cvat-modal-content-renew-job',
@ -171,7 +185,10 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element {
} }
} }
const is2d = jobInstance.task.dimension === DimensionType.DIM_2D; const computeClassName = (menuItemState: string): string => {
if (menuItemState === jobState) return 'cvat-submenu-current-job-state-item';
return '';
};
return ( return (
<Menu onClick={(params: MenuInfo) => onClickMenuWrapper(params)} className='cvat-annotation-menu' selectable={false}> <Menu onClick={(params: MenuInfo) => onClickMenuWrapper(params)} className='cvat-annotation-menu' selectable={false}>
@ -196,21 +213,44 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element {
} }
}, },
menuKey: Actions.LOAD_JOB_ANNO, menuKey: Actions.LOAD_JOB_ANNO,
taskDimension: jobInstance.task.dimension, taskDimension: jobInstance.dimension,
})} })}
<Menu.Item key={Actions.EXPORT_TASK_DATASET}>Export task dataset</Menu.Item> <Menu.Item key={Actions.EXPORT_TASK_DATASET}>Export task dataset</Menu.Item>
<Menu.Item key={Actions.REMOVE_ANNO}>Remove annotations</Menu.Item> <Menu.Item key={Actions.REMOVE_ANNO}>Remove annotations</Menu.Item>
<Menu.Item key={Actions.OPEN_TASK}> <Menu.Item key={Actions.OPEN_TASK}>
<a href={`/tasks/${taskID}`} onClick={(e: React.MouseEvent) => e.preventDefault()}> <a
href={`/tasks/${taskID}`}
onClick={(e: React.MouseEvent) => {
e.preventDefault();
history.push(`/tasks/${taskID}`);
return false;
}}
>
Open the task Open the task
</a> </a>
</Menu.Item> </Menu.Item>
{jobStatus === 'annotation' && is2d && <Menu.Item key={Actions.REQUEST_REVIEW}>Request a review</Menu.Item>} {jobStage !== JobStage.ACCEPTANCE ? (
{jobStatus === 'annotation' && <Menu.Item key={Actions.FINISH_JOB}>Finish the job</Menu.Item>} <Menu.SubMenu popupClassName='cvat-annotation-menu-job-state-submenu' key='job-state-submenu' title='Change job state'>
{jobStatus === 'validation' && isReviewer && ( <Menu.Item key={`state:${JobState.NEW}`}>
<Menu.Item key={Actions.SUBMIT_REVIEW}>Submit the review</Menu.Item> <Text className={computeClassName(JobState.NEW)}>{JobState.NEW}</Text>
)} </Menu.Item>
{jobStatus === 'completed' && <Menu.Item key={Actions.RENEW_JOB}>Renew the job</Menu.Item>} <Menu.Item key={`state:${JobState.IN_PROGRESS}`}>
<Text className={computeClassName(JobState.IN_PROGRESS)}>{JobState.IN_PROGRESS}</Text>
</Menu.Item>
<Menu.Item key={`state:${JobState.REJECTED}`}>
<Text className={computeClassName(JobState.REJECTED)}>{JobState.REJECTED}</Text>
</Menu.Item>
<Menu.Item key={`state:${JobState.COMPLETED}`}>
<Text className={computeClassName(JobState.COMPLETED)}>{JobState.COMPLETED}</Text>
</Menu.Item>
</Menu.SubMenu>
) : null }
{[JobStage.ANNOTATION, JobStage.REVIEW].includes(jobStage) ?
<Menu.Item key={Actions.FINISH_JOB}>Finish the job</Menu.Item> : null}
{jobStage === JobStage.ACCEPTANCE ?
<Menu.Item key={Actions.RENEW_JOB}>Renew the job</Menu.Item> : null}
</Menu> </Menu>
); );
} }
export default withRouter(AnnotationMenuComponent);

@ -175,7 +175,7 @@ function RightGroup(props: Props): JSX.Element {
value={workspace} value={workspace}
> >
{Object.values(Workspace).map((ws) => { {Object.values(Workspace).map((ws) => {
if (jobInstance.task.dimension === DimensionType.DIM_3D) { if (jobInstance.dimension === DimensionType.DIM_3D) {
if (ws === Workspace.STANDARD) { if (ws === Workspace.STANDARD) {
return null; return null;
} }

@ -3,6 +3,7 @@
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React from 'react'; import React from 'react';
import { connect } from 'react-redux';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import { QuestionCircleOutlined } from '@ant-design/icons'; import { QuestionCircleOutlined } from '@ant-design/icons';
import Table from 'antd/lib/table'; import Table from 'antd/lib/table';
@ -11,38 +12,80 @@ import Spin from 'antd/lib/spin';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
import { DimensionType } from 'reducers/interfaces'; import { CombinedState, DimensionType } from 'reducers/interfaces';
import { showStatistics } from 'actions/annotation-actions';
interface Props { interface StateToProps {
visible: boolean;
collecting: boolean; collecting: boolean;
data: any; data: any;
visible: boolean;
assignee: string;
reviewer: string;
startFrame: number;
stopFrame: number;
bugTracker: string;
jobStatus: string; jobStatus: string;
savingJobStatus: boolean; savingJobStatus: boolean;
bugTracker: string | null;
startFrame: number;
stopFrame: number;
dimension: DimensionType;
assignee: any | null;
}
interface DispatchToProps {
closeStatistics(): void; closeStatistics(): void;
jobInstance: any;
} }
export default function StatisticsModalComponent(props: Props): JSX.Element { function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
statistics: { visible, collecting, data },
job: {
saving: savingJobStatus,
instance: {
bugTracker,
startFrame,
stopFrame,
assignee,
dimension,
status: jobStatus,
},
},
},
} = state;
return {
visible,
collecting,
data,
jobStatus,
savingJobStatus,
bugTracker,
startFrame,
stopFrame,
dimension,
assignee: assignee || 'Nobody',
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
closeStatistics(): void {
dispatch(showStatistics(false));
},
};
}
function StatisticsModalComponent(props: StateToProps & DispatchToProps): JSX.Element {
const { const {
collecting, collecting,
data, data,
visible, visible,
assignee, assignee,
reviewer,
startFrame, startFrame,
stopFrame, stopFrame,
bugTracker, bugTracker,
closeStatistics, closeStatistics,
jobInstance, dimension,
} = props; } = props;
const is2D = jobInstance.task.dimension === DimensionType.DIM_2D; const is2D = dimension === DimensionType.DIM_2D;
const baseProps = { const baseProps = {
cancelButtonProps: { style: { display: 'none' } }, cancelButtonProps: { style: { display: 'none' } },
@ -184,12 +227,6 @@ export default function StatisticsModalComponent(props: Props): JSX.Element {
</Text> </Text>
<Text className='cvat-text'>{assignee}</Text> <Text className='cvat-text'>{assignee}</Text>
</Col> </Col>
<Col span={4}>
<Text strong className='cvat-text'>
Reviewer
</Text>
<Text className='cvat-text'>{reviewer}</Text>
</Col>
<Col span={4}> <Col span={4}>
<Text strong className='cvat-text'> <Text strong className='cvat-text'>
Start frame Start frame
@ -235,3 +272,5 @@ export default function StatisticsModalComponent(props: Props): JSX.Element {
</Modal> </Modal>
); );
} }
export default connect(mapStateToProps, mapDispatchToProps)(StatisticsModalComponent);

@ -2,9 +2,7 @@
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import React, { import React, { useState, useEffect, useRef } from 'react';
useState, useEffect, useRef,
} from 'react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useHistory } from 'react-router'; import { useHistory } from 'react-router';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
@ -238,9 +236,7 @@ export default function CreateCloudStorageForm(props: Props): JSX.Element {
if (cloudStorageData.manifests && cloudStorageData.manifests.length) { if (cloudStorageData.manifests && cloudStorageData.manifests.length) {
delete cloudStorageData.manifests; delete cloudStorageData.manifests;
cloudStorageData.manifests = form cloudStorageData.manifests = form.getFieldValue('manifests').map((manifest: any): string => manifest.name);
.getFieldValue('manifests')
.map((manifest: any): string => manifest.name);
} }
if (cloudStorage) { if (cloudStorage) {
@ -308,14 +304,13 @@ export default function CreateCloudStorageForm(props: Props): JSX.Element {
const commonProps = { const commonProps = {
className: 'cvat-cloud-storage-form-item', className: 'cvat-cloud-storage-form-item',
labelCol: { span: 5 },
wrapperCol: { offset: 1 },
}; };
const credentialsBlok = (): JSX.Element => { const credentialsBlok = (): JSX.Element => {
const internalCommonProps = { const internalCommonProps = {
...commonProps, ...commonProps,
labelCol: { span: 8, offset: 2 }, labelCol: { span: 8, offset: 2 },
wrapperCol: { offset: 2 },
}; };
if (providerType === ProviderType.AWS_S3_BUCKET && credentialsType === CredentialsType.KEY_SECRET_KEY_PAIR) { if (providerType === ProviderType.AWS_S3_BUCKET && credentialsType === CredentialsType.KEY_SECRET_KEY_PAIR) {
@ -466,7 +461,7 @@ export default function CreateCloudStorageForm(props: Props): JSX.Element {
const AWSS3Configuration = (): JSX.Element => { const AWSS3Configuration = (): JSX.Element => {
const internalCommonProps = { const internalCommonProps = {
...commonProps, ...commonProps,
labelCol: { span: 6, offset: 1 }, labelCol: { offset: 1 },
wrapperCol: { offset: 1 }, wrapperCol: { offset: 1 },
}; };
@ -506,7 +501,7 @@ export default function CreateCloudStorageForm(props: Props): JSX.Element {
const AzureBlobStorageConfiguration = (): JSX.Element => { const AzureBlobStorageConfiguration = (): JSX.Element => {
const internalCommonProps = { const internalCommonProps = {
...commonProps, ...commonProps,
labelCol: { span: 6, offset: 1 }, labelCol: { offset: 1 },
wrapperCol: { offset: 1 }, wrapperCol: { offset: 1 },
}; };
@ -595,7 +590,7 @@ export default function CreateCloudStorageForm(props: Props): JSX.Element {
}; };
return ( return (
<Form className='cvat-cloud-storage-form' layout='horizontal' form={form}> <Form className='cvat-cloud-storage-form' layout='vertical' form={form}>
<Form.Item <Form.Item
{...commonProps} {...commonProps}
label='Display name' label='Display name'

@ -71,6 +71,7 @@ export default function ManifestsManager(props: Props): JSX.Element {
<> <>
<Form.Item <Form.Item
name='manifests' name='manifests'
className='cvat-manifests-manager-form-item'
label={( label={(
<> <>
Manifests Manifests

@ -75,3 +75,9 @@
padding-left: $grid-unit-size * 0.5; padding-left: $grid-unit-size * 0.5;
padding-right: 0; padding-right: 0;
} }
.cvat-manifests-manager-form-item {
> .ant-form-item-control {
display: none;
}
}

@ -0,0 +1,99 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { useHistory } from 'react-router';
import { useDispatch, useSelector } from 'react-redux';
import Form from 'antd/lib/form';
import Input from 'antd/lib/input';
import Button from 'antd/lib/button';
import Space from 'antd/lib/space';
import { Store } from 'antd/lib/form/interface';
import { useForm } from 'antd/lib/form/Form';
import notification from 'antd/lib/notification';
import { createOrganizationAsync } from 'actions/organization-actions';
import validationPatterns from 'utils/validation-patterns';
import { CombinedState } from 'reducers/interfaces';
function CreateOrganizationForm(): JSX.Element {
const [form] = useForm<Store>();
const dispatch = useDispatch();
const history = useHistory();
const creating = useSelector((state: CombinedState) => state.organizations.creating);
const MAX_SLUG_LEN = 16;
const MAX_NAME_LEN = 64;
const onFinish = (values: Store): void => {
const {
phoneNumber, location, email, ...rest
} = values;
rest.contact = {
...(phoneNumber ? { phoneNumber } : {}),
...(email ? { email } : {}),
...(location ? { location } : {}),
};
dispatch(
createOrganizationAsync(rest, (createdSlug: string): void => {
form.resetFields();
notification.info({ message: `Organization ${createdSlug} has been successfully created` });
}),
);
};
return (
<Form
form={form}
autoComplete='off'
onFinish={onFinish}
className='cvat-create-organization-form'
layout='vertical'
>
<Form.Item
hasFeedback
name='slug'
label='Short name'
rules={[
{ required: true, message: 'Short name is a required field' },
{ max: MAX_SLUG_LEN, message: `Short name must not exceed ${MAX_SLUG_LEN} characters` },
{ ...validationPatterns.validateOrganizationSlug },
]}
>
<Input />
</Form.Item>
<Form.Item
hasFeedback
name='name'
label='Full name'
rules={[{ max: MAX_NAME_LEN, message: `Full name must not exceed ${MAX_NAME_LEN} characters` }]}
>
<Input />
</Form.Item>
<Form.Item hasFeedback name='description' label='Description'>
<Input.TextArea rows={3} />
</Form.Item>
<Form.Item hasFeedback name='email' label='Email' rules={[{ type: 'email', message: 'The input is not a valid E-mail' }]}>
<Input autoComplete='email' placeholder='support@organization.com' />
</Form.Item>
<Form.Item hasFeedback name='phoneNumber' label='Phone number' rules={[{ ...validationPatterns.validatePhoneNumber }]}>
<Input autoComplete='phoneNumber' placeholder='+44 5555 555555' />
</Form.Item>
<Form.Item hasFeedback name='location' label='Location'>
<Input autoComplete='location' placeholder='Country, State/Province, Address, Postal code' />
</Form.Item>
<Form.Item>
<Space className='cvat-create-organization-form-buttons-block' align='end'>
<Button onClick={() => history.goBack()}>Cancel</Button>
<Button loading={creating} disabled={creating} htmlType='submit' type='primary'>
Submit
</Button>
</Space>
</Form.Item>
</Form>
);
}
export default React.memo(CreateOrganizationForm);

@ -0,0 +1,23 @@
// Copyright (C) 2021 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 CreateOrganizationForm from './create-organization-form';
function CreateOrganizationComponent(): JSX.Element {
return (
<Row justify='center' align='top' className='cvat-create-organization-page'>
<Col md={20} lg={16} xl={14} xxl={9}>
<Text className='cvat-title'>Create a new organization</Text>
<CreateOrganizationForm />
</Col>
</Row>
);
}
export default React.memo(CreateOrganizationComponent);

@ -0,0 +1,52 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@import '../../base.scss';
.cvat-create-organization-page {
text-align: center;
padding-top: $grid-unit-size * 5;
overflow-y: auto;
height: 90%;
width: 100%;
> div:first-child {
> span {
font-size: 36px;
}
}
}
.cvat-create-organization-form {
text-align: initial;
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;
> div:not(first-child) {
margin-top: $grid-unit-size;
}
.cvat-create-organization-form-buttons-block {
display: flex;
justify-content: flex-end;
}
.cvat-create-organization-form-contact-block {
display: flex;
align-items: flex-end;
> .ant-space-item:first-child {
width: 100%;
}
}
.cvat-create-organization-form-add-contact-block {
align-items: baseline;
}
}

@ -325,9 +325,16 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
name='format' name='format'
label='Choose format' label='Choose format'
> >
<Select style={{ width: '100%' }} initialValue='CVAT for video 1.1'> <Select style={{ width: '100%' }}>
{ {
dumpers.map((dumper: any) => <Option value={dumper.name}>{dumper.name}</Option>) dumpers.map((dumper: any) => (
<Option
key={dumper.name}
value={dumper.name}
>
{dumper.name}
</Option>
))
} }
</Select> </Select>
</Form.Item> </Form.Item>

@ -233,7 +233,7 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
return ( return (
<> <>
<Col span={24}> <Col span={24}>
<Text className='cvat-text-color'>Project:</Text> <Text className='cvat-text-color'>Project</Text>
</Col> </Col>
<Col span={24}> <Col span={24}>
<ProjectSearchField onSelect={this.handleProjectIdChange} value={projectId} /> <ProjectSearchField onSelect={this.handleProjectIdChange} value={projectId} />
@ -249,7 +249,7 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
return ( return (
<> <>
<Col span={24}> <Col span={24}>
<Text className='cvat-text-color'>Subset:</Text> <Text className='cvat-text-color'>Subset</Text>
</Col> </Col>
<Col span={24}> <Col span={24}>
<ProjectSubsetField <ProjectSubsetField
@ -272,7 +272,7 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
return ( return (
<> <>
<Col span={24}> <Col span={24}>
<Text className='cvat-text-color'>Labels:</Text> <Text className='cvat-text-color'>Labels</Text>
</Col> </Col>
<Col span={24}> <Col span={24}>
<Text type='secondary'>Project labels will be used</Text> <Text type='secondary'>Project labels will be used</Text>
@ -284,7 +284,7 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
return ( return (
<Col span={24}> <Col span={24}>
<Text type='danger'>* </Text> <Text type='danger'>* </Text>
<Text className='cvat-text-color'>Labels:</Text> <Text className='cvat-text-color'>Labels</Text>
<LabelsEditor <LabelsEditor
labels={labels} labels={labels}
onSubmit={(newLabels): void => { onSubmit={(newLabels): void => {
@ -301,7 +301,7 @@ class CreateTaskContent extends React.PureComponent<Props & RouteComponentProps,
return ( return (
<Col span={24}> <Col span={24}>
<Text type='danger'>* </Text> <Text type='danger'>* </Text>
<Text className='cvat-text-color'>Select files:</Text> <Text className='cvat-text-color'>Select files</Text>
<ConnectedFileManager <ConnectedFileManager
onChangeActiveKey={this.changeFileManagerTab} onChangeActiveKey={this.changeFileManagerTab}
ref={(container: any): void => { ref={(container: any): void => {

@ -13,26 +13,35 @@ import Spin from 'antd/lib/spin';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import 'antd/dist/antd.css'; import 'antd/dist/antd.css';
import GlobalErrorBoundary from 'components/global-error-boundary/global-error-boundary'; import LoginPageContainer from 'containers/login-page/login-page';
import Header from 'components/header/header'; import LoginWithTokenComponent from 'components/login-with-token/login-with-token';
import RegisterPageContainer from 'containers/register-page/register-page';
import ResetPasswordPageConfirmComponent from 'components/reset-password-confirm-page/reset-password-confirm-page'; import ResetPasswordPageConfirmComponent from 'components/reset-password-confirm-page/reset-password-confirm-page';
import ResetPasswordPageComponent from 'components/reset-password-page/reset-password-page'; import ResetPasswordPageComponent from 'components/reset-password-page/reset-password-page';
import Header from 'components/header/header';
import GlobalErrorBoundary from 'components/global-error-boundary/global-error-boundary';
import ShortcutsDialog from 'components/shortcuts-dialog/shortcuts-dialog'; import ShortcutsDialog 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 ExportDatasetModal from 'components/export-dataset/export-dataset-modal'; import ExportDatasetModal from 'components/export-dataset/export-dataset-modal';
import ModelsPageContainer from 'containers/models-page/models-page';
import TasksPageContainer from 'containers/tasks-page/tasks-page';
import CreateTaskPageContainer from 'containers/create-task-page/create-task-page'; import CreateTaskPageContainer from 'containers/create-task-page/create-task-page';
import TaskPageContainer from 'containers/task-page/task-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 ProjectsPageComponent from 'components/projects-page/projects-page';
import LoginPageContainer from 'containers/login-page/login-page'; import CreateProjectPageComponent from 'components/create-project-page/create-project-page';
import RegisterPageContainer from 'containers/register-page/register-page'; import ProjectPageComponent from 'components/project-page/project-page';
import CloudStoragesPageComponent from 'components/cloud-storages-page/cloud-storages-page'; import CloudStoragesPageComponent from 'components/cloud-storages-page/cloud-storages-page';
import CreateCloudStoragePageComponent from 'components/create-cloud-storage-page/create-cloud-storage-page'; import CreateCloudStoragePageComponent from 'components/create-cloud-storage-page/create-cloud-storage-page';
import UpdateCloudStoragePageComponent from 'components/update-cloud-storage-page/update-cloud-storage-page'; import UpdateCloudStoragePageComponent from 'components/update-cloud-storage-page/update-cloud-storage-page';
import OrganizationPage from 'components/organization-page/organization-page';
import CreateOrganizationComponent from 'components/create-organization-page/create-organization-page';
import AnnotationPageContainer from 'containers/annotation-page/annotation-page';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import { NotificationsState } from 'reducers/interfaces'; import { NotificationsState } from 'reducers/interfaces';
@ -57,9 +66,12 @@ interface CVATAppProps {
switchShortcutsDialog: () => void; switchShortcutsDialog: () => void;
switchSettingsDialog: () => void; switchSettingsDialog: () => void;
loadAuthActions: () => void; loadAuthActions: () => void;
loadOrganizations: () => void;
keyMap: KeyMap; keyMap: KeyMap;
userInitialized: boolean; userInitialized: boolean;
userFetching: boolean; userFetching: boolean;
organizationsFetching: boolean;
organizationsInitialized: boolean;
pluginsInitialized: boolean; pluginsInitialized: boolean;
pluginsFetching: boolean; pluginsFetching: boolean;
modelsInitialized: boolean; modelsInitialized: boolean;
@ -150,9 +162,12 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
loadUserAgreements, loadUserAgreements,
initPlugins, initPlugins,
initModels, initModels,
loadOrganizations,
loadAuthActions, loadAuthActions,
userInitialized, userInitialized,
userFetching, userFetching,
organizationsFetching,
organizationsInitialized,
formatsInitialized, formatsInitialized,
formatsFetching, formatsFetching,
aboutInitialized, aboutInitialized,
@ -190,6 +205,10 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
return; return;
} }
if (!organizationsInitialized && !organizationsFetching) {
loadOrganizations();
}
if (!formatsInitialized && !formatsFetching) { if (!formatsInitialized && !formatsFetching) {
loadFormats(); loadFormats();
} }
@ -288,6 +307,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
pluginsInitialized, pluginsInitialized,
formatsInitialized, formatsInitialized,
modelsInitialized, modelsInitialized,
organizationsInitialized,
switchShortcutsDialog, switchShortcutsDialog,
switchSettingsDialog, switchSettingsDialog,
user, user,
@ -302,6 +322,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
formatsInitialized && formatsInitialized &&
pluginsInitialized && pluginsInitialized &&
aboutInitialized && aboutInitialized &&
organizationsInitialized &&
(!isModelPluginActive || modelsInitialized)); (!isModelPluginActive || modelsInitialized));
const subKeyMap = { const subKeyMap = {
@ -350,6 +371,12 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
path='/cloudstorages/update/:id' path='/cloudstorages/update/:id'
component={UpdateCloudStoragePageComponent} component={UpdateCloudStoragePageComponent}
/> />
<Route
exact
path='/organizations/create'
component={CreateOrganizationComponent}
/>
<Route exact path='/organization' component={OrganizationPage} />
{isModelPluginActive && ( {isModelPluginActive && (
<Route exact path='/models' component={ModelsPageContainer} /> <Route exact path='/models' component={ModelsPageContainer} />
)} )}

@ -40,11 +40,12 @@ function ExportDatasetModal(): JSX.Element {
const initActivities = (): void => { const initActivities = (): void => {
if (instance instanceof core.classes.Project) { if (instance instanceof core.classes.Project) {
setInstanceType('project'); setInstanceType(`project #${instance.id}`);
setActivities(projectExportActivities[instance.id] || []); setActivities(projectExportActivities[instance.id] || []);
} else if (instance instanceof core.classes.Task) { } else if (instance) {
setInstanceType('task'); const taskID = instance instanceof core.classes.Task ? instance.id : instance.taskId;
setActivities(taskExportActivities[instance.id] || []); setInstanceType(`task #${taskID}`);
setActivities(taskExportActivities[taskID] || []);
if (instance.mode === 'interpolation' && instance.dimension === '2d') { if (instance.mode === 'interpolation' && instance.dimension === '2d') {
form.setFieldsValue({ selectedFormat: 'CVAT for video 1.1' }); form.setFieldsValue({ selectedFormat: 'CVAT for video 1.1' });
} else if (instance.mode === 'annotation' && instance.dimension === '2d') { } else if (instance.mode === 'annotation' && instance.dimension === '2d') {
@ -77,21 +78,21 @@ function ExportDatasetModal(): JSX.Element {
Notification.info({ Notification.info({
message: 'Dataset export started', message: 'Dataset export started',
description: description:
`Dataset export was started for ${instanceType} #${instance?.id}. ` + `Dataset export was started for ${instanceType}. ` +
'Download will start automaticly as soon as the dataset is ready.', 'Download will start automaticly as soon as the dataset is ready.',
className: `cvat-notification-notice-export-${instanceType}-start`, className: `cvat-notification-notice-export-${instanceType.split(' ')[0]}-start`,
}); });
}, },
[instance?.id, instance instanceof core.classes.Project, instanceType], [instance, instanceType],
); );
return ( return (
<Modal <Modal
title={`Export ${instanceType} #${instance?.id} as a dataset`} title={`Export ${instanceType} as a dataset`}
visible={modalVisible} visible={modalVisible}
onCancel={closeModal} onCancel={closeModal}
onOk={() => form.submit()} onOk={() => form.submit()}
className={`cvat-modal-export-${instanceType}`} className={`cvat-modal-export-${instanceType.split(' ')[0]}`}
destroyOnClose destroyOnClose
> >
<Form <Form

@ -17,6 +17,9 @@ import Icon, {
QuestionCircleOutlined, QuestionCircleOutlined,
CaretDownOutlined, CaretDownOutlined,
ControlOutlined, ControlOutlined,
UserOutlined,
TeamOutlined,
PlusOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import Layout from 'antd/lib/layout'; import Layout from 'antd/lib/layout';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
@ -28,11 +31,13 @@ import Text from 'antd/lib/typography/Text';
import getCore from 'cvat-core-wrapper'; import getCore from 'cvat-core-wrapper';
import consts from 'consts'; import consts from 'consts';
import { CVATLogo, AccountIcon } from 'icons'; import { CVATLogo } from 'icons';
import ChangePasswordDialog from 'components/change-password-modal/change-password-modal'; import ChangePasswordDialog from 'components/change-password-modal/change-password-modal';
import CVATTooltip from 'components/common/cvat-tooltip';
import { switchSettingsDialog as switchSettingsDialogAction } from 'actions/settings-actions'; import { switchSettingsDialog as switchSettingsDialogAction } from 'actions/settings-actions';
import { logoutAsync, authActions } from 'actions/auth-actions'; import { logoutAsync, authActions } from 'actions/auth-actions';
import { CombinedState } from 'reducers/interfaces'; import { CombinedState } from 'reducers/interfaces';
import { Select } from 'antd';
import SettingsModal from './settings-modal/settings-modal'; import SettingsModal from './settings-modal/settings-modal';
const core = getCore(); const core = getCore();
@ -67,6 +72,9 @@ interface StateToProps {
isAnalyticsPluginActive: boolean; isAnalyticsPluginActive: boolean;
isModelsPluginActive: boolean; isModelsPluginActive: boolean;
isGitPluginActive: boolean; isGitPluginActive: boolean;
organizationsFetching: boolean;
organizationsList: any[];
currentOrganization: any | null;
} }
interface DispatchToProps { interface DispatchToProps {
@ -88,6 +96,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
about: { server, packageVersion }, about: { server, packageVersion },
shortcuts: { normalizedKeyMap }, shortcuts: { normalizedKeyMap },
settings: { showDialog: settingsDialogShown }, settings: { showDialog: settingsDialogShown },
organizations: { fetching: organizationsFetching, current: currentOrganization, list: organizationsList },
} = state; } = state;
return { return {
@ -118,6 +127,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
isAnalyticsPluginActive: list.ANALYTICS, isAnalyticsPluginActive: list.ANALYTICS,
isModelsPluginActive: list.MODELS, isModelsPluginActive: list.MODELS,
isGitPluginActive: list.GIT_INTEGRATION, isGitPluginActive: list.GIT_INTEGRATION,
organizationsFetching,
currentOrganization,
organizationsList,
}; };
} }
@ -145,10 +157,13 @@ function HeaderContainer(props: Props): JSX.Element {
renderChangePasswordItem, renderChangePasswordItem,
isAnalyticsPluginActive, isAnalyticsPluginActive,
isModelsPluginActive, isModelsPluginActive,
organizationsFetching,
currentOrganization,
organizationsList,
} = props; } = props;
const { const {
CHANGELOG_URL, LICENSE_URL, GITTER_URL, FORUM_URL, GITHUB_URL, CHANGELOG_URL, LICENSE_URL, GITTER_URL, FORUM_URL, GITHUB_URL, GUIDE_URL,
} = consts; } = consts;
const history = useHistory(); const history = useHistory();
@ -208,10 +223,32 @@ function HeaderContainer(props: Props): JSX.Element {
}); });
} }
const menu = ( const resetOrganization = (): void => {
<Menu className='cvat-header-menu' mode='vertical'> localStorage.removeItem('currentOrganization');
if (/\d+$/.test(window.location.pathname)) {
window.location.pathname = '/';
} else {
window.location.reload();
}
};
const setNewOrganization = (organization: any): void => {
if (!currentOrganization || currentOrganization.slug !== organization.slug) {
localStorage.setItem('currentOrganization', organization.slug);
if (/\d+$/.test(window.location.pathname)) {
// a resource is opened (task/job/etc.)
window.location.pathname = '/';
} else {
window.location.reload();
}
}
};
const userMenu = (
<Menu className='cvat-header-menu'>
{user.isStaff && ( {user.isStaff && (
<Menu.Item <Menu.Item
icon={<ControlOutlined />}
key='admin_page' key='admin_page'
onClick={(): void => { onClick={(): void => {
// false positive // false positive
@ -219,37 +256,114 @@ function HeaderContainer(props: Props): JSX.Element {
window.open(`${tool.server.host}/admin`, '_blank'); window.open(`${tool.server.host}/admin`, '_blank');
}} }}
> >
<ControlOutlined />
Admin page Admin page
</Menu.Item> </Menu.Item>
)} )}
<Menu.SubMenu
disabled={organizationsFetching}
key='organization'
title='Organization'
icon={organizationsFetching ? <LoadingOutlined /> : <TeamOutlined />}
>
{currentOrganization ? (
<Menu.Item icon={<SettingOutlined />} key='open_organization' onClick={() => history.push('/organization')}>
Settings
</Menu.Item>
) : null}
<Menu.Item icon={<PlusOutlined />} key='create_organization' onClick={() => history.push('/organizations/create')}>Create</Menu.Item>
{ organizationsList.length > 5 ? (
<Menu.Item
key='switch_organization'
onClick={() => {
Modal.confirm({
title: 'Select an organization',
okButtonProps: {
style: { display: 'none' },
},
content: (
<Select
showSearch
className='cvat-modal-organization-selector'
value={currentOrganization?.slug}
onChange={(value: string) => {
if (value === '$personal') {
resetOrganization();
return;
}
const [organization] = organizationsList
.filter((_organization): boolean => _organization.slug === value);
if (organization) {
setNewOrganization(organization);
}
}}
>
<Select.Option value='$personal'>Personal workspace</Select.Option>
{organizationsList.map((organization: any): JSX.Element => {
const { slug } = organization;
return <Select.Option key={slug} value={slug}>{slug}</Select.Option>;
})}
</Select>
),
});
}}
>
Switch organization
</Menu.Item>
) : (
<>
<Menu.Divider />
<Menu.ItemGroup>
<Menu.Item
className={!currentOrganization ? 'cvat-header-menu-active-organization-item' : ''}
key='$personal'
onClick={resetOrganization}
>
Personal workspace
</Menu.Item>
{organizationsList.map((organization: any): JSX.Element => (
<Menu.Item <Menu.Item
className={currentOrganization?.slug === organization.slug ?
'cvat-header-menu-active-organization-item' : ''}
key={organization.slug}
onClick={() => setNewOrganization(organization)}
>
{organization.slug}
</Menu.Item>
))}
</Menu.ItemGroup>
</>
)}
</Menu.SubMenu>
<Menu.Item
icon={<SettingOutlined />}
key='settings' key='settings'
title={`Press ${switchSettingsShortcut} to switch`} title={`Press ${switchSettingsShortcut} to switch`}
onClick={() => switchSettingsDialog(true)} onClick={() => switchSettingsDialog(true)}
> >
<SettingOutlined />
Settings Settings
</Menu.Item> </Menu.Item>
<Menu.Item key='about' onClick={showAboutModal}> <Menu.Item icon={<InfoCircleOutlined />} key='about' onClick={() => showAboutModal()}>
<InfoCircleOutlined />
About About
</Menu.Item> </Menu.Item>
{renderChangePasswordItem && ( {renderChangePasswordItem && (
<Menu.Item <Menu.Item
key='change_password' key='change_password'
icon={changePasswordFetching ? <LoadingOutlined /> : <EditOutlined />}
className='cvat-header-menu-change-password' className='cvat-header-menu-change-password'
onClick={(): void => switchChangePasswordDialog(true)} onClick={(): void => switchChangePasswordDialog(true)}
disabled={changePasswordFetching} disabled={changePasswordFetching}
> >
{changePasswordFetching ? <LoadingOutlined /> : <EditOutlined />}
Change password Change password
</Menu.Item> </Menu.Item>
)} )}
<Menu.Item key='logout' onClick={onLogout} disabled={logoutFetching}> <Menu.Item
{logoutFetching ? <LoadingOutlined /> : <LogoutOutlined />} key='logout'
icon={logoutFetching ? <LoadingOutlined /> : <LogoutOutlined />}
onClick={onLogout}
disabled={logoutFetching}
>
Logout Logout
</Menu.Item> </Menu.Item>
</Menu> </Menu>
@ -278,7 +392,7 @@ function HeaderContainer(props: Props): JSX.Element {
href='/tasks?page=1' href='/tasks?page=1'
onClick={(event: React.MouseEvent): void => { onClick={(event: React.MouseEvent): void => {
event.preventDefault(); event.preventDefault();
history.push('/tasks?page=1'); history.push('/tasks');
}} }}
> >
Tasks Tasks
@ -290,7 +404,7 @@ function HeaderContainer(props: Props): JSX.Element {
href='/cloudstorages?page=1' href='/cloudstorages?page=1'
onClick={(event: React.MouseEvent): void => { onClick={(event: React.MouseEvent): void => {
event.preventDefault(); event.preventDefault();
history.push('/cloudstorages?page=1'); history.push('/cloudstorages');
}} }}
> >
Cloud Storages Cloud Storages
@ -326,39 +440,52 @@ function HeaderContainer(props: Props): JSX.Element {
)} )}
</div> </div>
<div className='cvat-right-header'> <div className='cvat-right-header'>
<CVATTooltip overlay='Click to open repository'>
<Button <Button
icon={<GithubOutlined />}
size='large'
className='cvat-header-button' className='cvat-header-button'
type='link' type='link'
href={GITHUB_URL} href={GITHUB_URL}
onClick={(event: React.MouseEvent): void => { onClick={(event: React.MouseEvent): void => {
event.preventDefault(); event.preventDefault();
// false alarm
// eslint-disable-next-line security/detect-non-literal-fs-filename
window.open(GITHUB_URL, '_blank'); window.open(GITHUB_URL, '_blank');
}} }}
> />
<GithubOutlined /> </CVATTooltip>
<Text className='cvat-text-color'>GitHub</Text> <CVATTooltip overlay='Click to open guide'>
</Button>
<Button <Button
icon={<QuestionCircleOutlined />}
size='large'
className='cvat-header-button' className='cvat-header-button'
type='link' type='link'
href='https://openvinotoolkit.github.io/cvat/docs' href={GUIDE_URL}
onClick={(event: React.MouseEvent): void => { onClick={(event: React.MouseEvent): void => {
event.preventDefault(); event.preventDefault();
// false positive // false alarm
// eslint-disable-next-line // eslint-disable-next-line security/detect-non-literal-fs-filename
window.open('https://openvinotoolkit.github.io/cvat/docs'); window.open(GUIDE_URL, '_blank');
}} }}
> />
<QuestionCircleOutlined /> </CVATTooltip>
Help <Dropdown placement='bottomRight' overlay={userMenu} className='cvat-header-menu-user-dropdown'>
</Button>
<Dropdown overlay={menu} className='cvat-header-menu-dropdown'>
<span> <span>
<Icon className='cvat-header-account-icon' component={AccountIcon} /> <UserOutlined className='cvat-header-dropdown-icon' />
<Row>
<Col span={24}>
<Text strong> <Text strong>
{user.username.length > 14 ? `${user.username.slice(0, 10)} ...` : user.username} {user.username.length > 14 ? `${user.username.slice(0, 10)} ...` : user.username}
</Text> </Text>
<CaretDownOutlined className='cvat-header-menu-icon' /> </Col>
{ currentOrganization ? (
<Col span={24}>
<Text>{currentOrganization.slug}</Text>
</Col>
) : null }
</Row>
<CaretDownOutlined className='cvat-header-dropdown-icon' />
</span> </span>
</Dropdown> </Dropdown>
</div> </div>

@ -118,4 +118,4 @@ const SettingsModal = (props: SettingsModalProps): JSX.Element => {
); );
}; };
export default SettingsModal; export default React.memo(SettingsModal);

@ -16,7 +16,8 @@
} }
.cvat-workspace-settings, .cvat-workspace-settings,
.cvat-player-settings { .cvat-player-settings,
.cvat-organizations-settings {
width: 100%; width: 100%;
height: max-content; height: max-content;
background: $background-color-1; background: $background-color-1;
@ -96,3 +97,14 @@
.cvat-settings-modal .ant-modal-body { .cvat-settings-modal .ant-modal-body {
padding-top: 0; padding-top: 0;
} }
.cvat-organizations-settings-list {
width: $grid-unit-size * 24;
margin-bottom: $grid-unit-size * 3;
}
.cvat-organizations-settings-list-item {
div {
width: 100%;
}
}

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -25,6 +25,15 @@
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: center; align-items: center;
> a.ant-btn {
height: 24px;
span[role='img'] {
font-size: 24px;
line-height: 24px;
}
}
} }
.anticon.cvat-logo-icon { .anticon.cvat-logo-icon {
@ -35,32 +44,48 @@
.ant-btn.cvat-header-button { .ant-btn.cvat-header-button {
color: $text-color; color: $text-color;
padding: 0 10px; padding: 0 $grid-unit-size;
margin-right: 10px; margin-right: $grid-unit-size;
} }
.ant-dropdown-trigger.cvat-header-menu-dropdown { .cvat-header-menu-user-dropdown {
display: flex; display: flex;
align-items: center; align-items: center;
border-left: 1px solid $border-color-1; border-left: 1px solid $border-color-1;
padding: 0 20px;
}
.anticon.cvat-header-account-icon { .anticon.cvat-header-dropdown-icon {
> svg { &.anticon-caret-down {
transform: scale(0.4); font-size: 12px;
} }
}
.anticon.cvat-header-menu-icon { font-size: 20px;
margin-left: 16px; padding: $grid-unit-size;
margin-right: 0; }
}
.cvat-header-menu { > div:nth-child(2) {
> li { > div:nth-child(2) { /* org slug */
span[role='img'] { font-size: 10px;
margin-right: $grid-unit-size; }
max-width: $grid-unit-size * 15;
height: $grid-unit-size * 5;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
align-items: center;
} }
}
.cvat-header-menu-active-organization-item {
::after {
content: ' \2713';
float: right;
margin-left: $grid-unit-size;
} }
font-weight: bold;
}
.cvat-modal-organization-selector {
width: 100%;
} }

@ -27,13 +27,14 @@ import { DimensionType } from '../../reducers/interfaces';
interface Props { interface Props {
withCleanup: boolean; withCleanup: boolean;
models: Model[]; models: Model[];
task: any; labels: any[];
runInference(task: any, model: Model, body: object): void; dimension: DimensionType;
runInference(model: Model, body: object): void;
} }
function DetectorRunner(props: Props): JSX.Element { function DetectorRunner(props: Props): JSX.Element {
const { const {
task, models, withCleanup, runInference, models, withCleanup, labels, dimension, runInference,
} = props; } = props;
const [modelID, setModelID] = useState<string | null>(null); const [modelID, setModelID] = useState<string | null>(null);
@ -56,7 +57,7 @@ function DetectorRunner(props: Props): JSX.Element {
model && (model.type === 'reid' || (model.type === 'detector' && !!Object.keys(mapping).length)); model && (model.type === 'reid' || (model.type === 'detector' && !!Object.keys(mapping).length));
const modelLabels = (isDetector ? model.labels : []).filter((_label: string): boolean => !(_label in mapping)); const modelLabels = (isDetector ? model.labels : []).filter((_label: string): boolean => !(_label in mapping));
const taskLabels = isDetector && !!task ? task.labels.map((label: any): string => label.name) : []; const taskLabels = isDetector ? labels.map((label: any): string => label.name) : [];
if (model && model.type !== 'reid' && !model.labels.length) { if (model && model.type !== 'reid' && !model.labels.length) {
notification.warning({ notification.warning({
@ -90,7 +91,7 @@ function DetectorRunner(props: Props): JSX.Element {
function renderSelector( function renderSelector(
value: string, value: string,
tooltip: string, tooltip: string,
labels: string[], labelsToRender: string[],
onChange: (label: string) => void, onChange: (label: string) => void,
): JSX.Element { ): JSX.Element {
return ( return (
@ -111,7 +112,7 @@ function DetectorRunner(props: Props): JSX.Element {
return false; return false;
}} }}
> >
{labels.map( {labelsToRender.map(
(label: string): JSX.Element => ( (label: string): JSX.Element => (
<Select.Option value={label} key={label}> <Select.Option value={label} key={label}>
{label} {label}
@ -129,12 +130,12 @@ function DetectorRunner(props: Props): JSX.Element {
<Col span={4}>Model:</Col> <Col span={4}>Model:</Col>
<Col span={20}> <Col span={20}>
<Select <Select
placeholder={task.dimension === DimensionType.DIM_2D ? 'Select a model' : 'No models available'} placeholder={dimension === DimensionType.DIM_2D ? 'Select a model' : 'No models available'}
disabled={task.dimension !== DimensionType.DIM_2D} disabled={dimension !== DimensionType.DIM_2D}
style={{ width: '100%' }} style={{ width: '100%' }}
onChange={(_modelID: string): void => { onChange={(_modelID: string): void => {
const newmodel = models.filter((_model): boolean => _model.id === _modelID)[0]; const newmodel = models.filter((_model): boolean => _model.id === _modelID)[0];
const newmapping = task.labels.reduce((acc: StringObject, label: any): StringObject => { const newmapping = labels.reduce((acc: StringObject, label: any): StringObject => {
if (newmodel.labels.includes(label.name)) { if (newmodel.labels.includes(label.name)) {
acc[label.name] = label.name; acc[label.name] = label.name;
} }
@ -159,7 +160,7 @@ function DetectorRunner(props: Props): JSX.Element {
{isDetector && {isDetector &&
!!Object.keys(mapping).length && !!Object.keys(mapping).length &&
Object.keys(mapping).map((modelLabel: string) => { Object.keys(mapping).map((modelLabel: string) => {
const label = task.labels.filter((_label: any): boolean => _label.name === mapping[modelLabel])[0]; const label = labels.filter((_label: any): boolean => _label.name === mapping[modelLabel])[0];
const color = label ? label.color : consts.NEW_LABEL_COLOR; const color = label ? label.color : consts.NEW_LABEL_COLOR;
return ( return (
<Row key={modelLabel} justify='start' align='middle'> <Row key={modelLabel} justify='start' align='middle'>
@ -188,12 +189,14 @@ function DetectorRunner(props: Props): JSX.Element {
<> <>
<Row justify='start' align='middle'> <Row justify='start' align='middle'>
<Col span={10}> <Col span={10}>
{renderSelector(match.model || '', 'Model labels', modelLabels, (modelLabel: string) => {renderSelector(
updateMatch(modelLabel, null))} match.model || '', 'Model labels', modelLabels, (modelLabel: string) => updateMatch(modelLabel, null),
)}
</Col> </Col>
<Col span={10} offset={1}> <Col span={10} offset={1}>
{renderSelector(match.task || '', 'Task labels', taskLabels, (taskLabel: string) => {renderSelector(
updateMatch(null, taskLabel))} match.task || '', 'Task labels', taskLabels, (taskLabel: string) => updateMatch(null, taskLabel),
)}
</Col> </Col>
<Col span={1} offset={1}> <Col span={1} offset={1}>
<CVATTooltip title='Specify a label mapping between model labels and task labels'> <CVATTooltip title='Specify a label mapping between model labels and task labels'>
@ -262,16 +265,11 @@ function DetectorRunner(props: Props): JSX.Element {
disabled={!buttonEnabled} disabled={!buttonEnabled}
type='primary' type='primary'
onClick={() => { onClick={() => {
runInference( runInference(model, model.type === 'detector' ?
task, { mapping, cleanup } : {
model,
model.type === 'detector' ?
{ mapping, cleanup } :
{
threshold, threshold,
max_distance: distance, max_distance: distance,
}, });
);
}} }}
> >
Annotate Annotate
@ -282,14 +280,4 @@ function DetectorRunner(props: Props): JSX.Element {
); );
} }
export default React.memo( export default React.memo(DetectorRunner);
DetectorRunner,
(prevProps: Props, nextProps: Props): boolean =>
prevProps.task === nextProps.task &&
prevProps.runInference === nextProps.runInference &&
prevProps.models.length === nextProps.models.length &&
nextProps.models.reduce(
(acc: boolean, model: Model, index: number): boolean => acc && model.id === prevProps.models[index].id,
true,
),
);

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -29,8 +29,8 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { detectors, reid } = models; const { detectors, reid } = models;
return { return {
visible: models.visibleRunWindows, visible: models.modelRunnerIsVisible,
task: models.activeRunTask, task: models.modelRunnerTask,
reid, reid,
detectors, detectors,
}; };
@ -38,8 +38,8 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps { function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps {
return { return {
runInference(task: any, model: Model, body: object) { runInference(taskID: number, model: Model, body: object) {
dispatch(startInferenceAsync(task, model, body)); dispatch(startInferenceAsync(taskID, model, body));
}, },
closeDialog() { closeDialog() {
dispatch(modelsActions.closeRunModelDialog()); dispatch(modelsActions.closeRunModelDialog());
@ -63,15 +63,18 @@ function ModelRunnerDialog(props: StateToProps & DispatchToProps): JSX.Element {
maskClosable maskClosable
title='Automatic annotation' title='Automatic annotation'
> >
{ task ? (
<DetectorRunner <DetectorRunner
withCleanup withCleanup
models={models} models={models}
task={task} labels={task.labels}
dimension={task.dimension}
runInference={(...args) => { runInference={(...args) => {
closeDialog(); closeDialog();
runInference(...args); runInference(task.id, ...args);
}} }}
/> />
) : null }
</Modal> </Modal>
); );
} }

@ -0,0 +1,86 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Select from 'antd/lib/select';
import Text from 'antd/lib/typography/Text';
import { Row, Col } from 'antd/lib/grid';
import moment from 'moment';
import { CloseOutlined } from '@ant-design/icons';
import Modal from 'antd/lib/modal';
export interface Props {
membershipInstance: any;
onRemoveMembership(): void;
onUpdateMembershipRole(role: string): void;
}
function MemberItem(props: Props): JSX.Element {
const {
membershipInstance, onRemoveMembership, onUpdateMembershipRole,
} = props;
const {
user, joined_date: joinedDate, role, invitation,
} = membershipInstance;
const { username, firstName, lastName } = user;
return (
<Row className='cvat-organization-member-item' justify='space-between'>
<Col span={5} className='cvat-organization-member-item-username'>
<Text strong>{username}</Text>
</Col>
<Col span={6} className='cvat-organization-member-item-name'>
<Text strong>{`${firstName || ''} ${lastName || ''}`}</Text>
</Col>
<Col span={8} className='cvat-organization-member-item-dates'>
{invitation ? (
<Text type='secondary'>
{`Invited ${moment(invitation.created_date).fromNow()} ${invitation.owner ? `by ${invitation.owner.username}` : ''}`}
</Text>
) : null}
{joinedDate ? <Text type='secondary'>{`Joined ${moment(joinedDate).fromNow()}`}</Text> : null}
</Col>
<Col span={3} className='cvat-organization-member-item-role'>
<Select
onChange={(_role: string) => {
onUpdateMembershipRole(_role);
}}
value={role}
disabled={role === 'owner'}
>
{role === 'owner' ? (
<Select.Option value='owner'>Owner</Select.Option>
) : (
<>
<Select.Option value='worker'>Worker</Select.Option>
<Select.Option value='supervisor'>Supervisor</Select.Option>
<Select.Option value='maintainer'>Maintainer</Select.Option>
</>
)}
</Select>
</Col>
<Col span={1} className='cvat-organization-member-item-remove'>
{role !== 'owner' ? (
<CloseOutlined
onClick={() => {
Modal.confirm({
title: `You are removing "${username}" from this organization`,
content: 'The person will not have access to the organization data anymore. Continue?',
okText: 'Yes, remove',
okButtonProps: {
danger: true,
},
onOk: () => {
onRemoveMembership();
},
});
}}
/>
) : null}
</Col>
</Row>
);
}
export default React.memo(MemberItem);

@ -0,0 +1,83 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Pagination from 'antd/lib/pagination';
import Spin from 'antd/lib/spin';
import { useDispatch, useSelector } from 'react-redux';
import { CombinedState } from 'reducers/interfaces';
import { removeOrganizationMemberAsync, updateOrganizationMemberAsync } from 'actions/organization-actions';
import MemberItem from './member-item';
export interface Props {
organizationInstance: any;
userInstance: any;
fetching: boolean;
pageSize: number;
pageNumber: number;
members: any[];
setPageNumber: (pageNumber: number) => void;
setPageSize: (pageSize: number) => void;
fetchMembers: () => void;
}
function MembersList(props: Props): JSX.Element {
const {
organizationInstance, fetching, members, pageSize, pageNumber, fetchMembers, setPageNumber, setPageSize,
} = props;
const dispatch = useDispatch();
const inviting = useSelector((state: CombinedState) => state.organizations.inviting);
const updatingMember = useSelector((state: CombinedState) => state.organizations.updatingMember);
const removingMember = useSelector((state: CombinedState) => state.organizations.removingMember);
return fetching || inviting || updatingMember || removingMember ? (
<Spin className='cvat-spinner' />
) : (
<>
<div>
{members.map(
(member: any): JSX.Element => (
<MemberItem
key={member.user.id}
membershipInstance={member}
onRemoveMembership={() => {
dispatch(
removeOrganizationMemberAsync(organizationInstance, member, () => {
fetchMembers();
}),
);
}}
onUpdateMembershipRole={(role: string) => {
dispatch(
updateOrganizationMemberAsync(organizationInstance, member, role, () => {
fetchMembers();
}),
);
}}
/>
),
)}
</div>
<div className='cvat-organization-members-pagination-block'>
<Pagination
total={members.length ? (members as any).count : 0}
onShowSizeChange={(current: number, newShowSize: number) => {
setPageNumber(current);
setPageSize(newShowSize);
}}
onChange={(current: number) => {
setPageNumber(current);
}}
current={pageNumber}
pageSize={pageSize}
showSizeChanger
showQuickJumper
/>
</div>
</>
);
}
export default React.memo(MembersList);

@ -0,0 +1,86 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, { useEffect, useState } from 'react';
import { useSelector } from 'react-redux';
import Empty from 'antd/lib/empty';
import Spin from 'antd/lib/spin';
import { CombinedState } from 'reducers/interfaces';
import TopBarComponent from './top-bar';
import MembersList from './members-list';
function fetchMembers(
organizationInstance: any,
page: number,
pageSize: number,
setMembers: (members: any[]) => void,
setFetching: (fetching: boolean) => void,
): void {
setFetching(true);
organizationInstance
.members(page, pageSize)
.then((_members: any[]) => {
setMembers(_members);
})
.catch(() => {})
.finally(() => {
setFetching(false);
});
}
function OrganizationPage(): JSX.Element | null {
const organization = useSelector((state: CombinedState) => state.organizations.current);
const fetching = useSelector((state: CombinedState) => state.organizations.fetching);
const updating = useSelector((state: CombinedState) => state.organizations.updating);
const user = useSelector((state: CombinedState) => state.auth.user);
const [membersFetching, setMembersFetching] = useState<boolean>(true);
const [members, setMembers] = useState<any[]>([]);
const [pageNumber, setPageNumber] = useState<number>(1);
const [pageSize, setPageSize] = useState<number>(10);
useEffect(() => {
if (organization) {
fetchMembers(organization, pageNumber, pageSize, setMembers, setMembersFetching);
}
}, [pageSize, pageNumber, organization]);
if (fetching || updating) {
return <Spin className='cvat-spinner' />;
}
return (
<div className='cvat-organization-page'>
{!organization ? (
<Empty description='You are not in an organization' />
) : (
<>
<TopBarComponent
organizationInstance={organization}
userInstance={user}
fetchMembers={() => fetchMembers(
organization, pageNumber, pageSize, setMembers, setMembersFetching,
)}
/>
<MembersList
fetching={membersFetching}
members={members}
organizationInstance={organization}
userInstance={user}
pageSize={pageSize}
pageNumber={pageNumber}
setPageNumber={setPageNumber}
setPageSize={setPageSize}
fetchMembers={() => fetchMembers(
organization, pageNumber, pageSize, setMembers, setMembersFetching,
)}
/>
</>
)}
</div>
);
}
export default React.memo(OrganizationPage);

@ -0,0 +1,150 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@import 'base.scss';
.cvat-organization-page {
height: 100%;
padding-top: $grid-unit-size * 2;
width: $grid-unit-size * 120;
margin: 0 auto;
.ant-empty {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
> div:nth-child(1) {
align-items: flex-end;
.cvat-organization-top-bar-buttons-block {
text-align: right;
}
.cvat-organization-top-bar-descriptions {
> div {
> button {
margin-top: $grid-unit-size;
}
}
> span {
display: block;
}
> span:nth-child(3) {
max-height: 7em;
display: grid;
overflow: auto;
margin-bottom: $grid-unit-size;
}
> span:not(.cvat-title),
div {
font-size: 12px;
span.anticon {
margin-right: $grid-unit-size;
}
span.anticon[aria-label=edit] {
margin-left: $grid-unit-size;
}
}
}
.cvat-organization-top-bar-contacts {
button {
margin-top: $grid-unit-size;
float: right;
}
> span {
display: block;
}
> span,
div {
font-size: 12px;
span.anticon {
margin-right: $grid-unit-size;
}
span.anticon[aria-label=edit] {
margin-left: $grid-unit-size;
}
}
}
}
> div:nth-child(2) {
overflow: auto;
height: auto;
max-height: 60%;
margin-top: $grid-unit-size;
}
@media screen and (min-height: 900px) {
> div:nth-child(2) {
max-height: 65%;
}
}
@media screen and (min-height: 1080px) {
> div:nth-child(2) {
max-height: 70%;
}
}
}
.cvat-organization-member-item {
border: 1px solid $border-color-1;
border-radius: 3px;
padding: $grid-unit-size;
background: $background-color-1;
margin-top: $grid-unit-size;
align-items: center;
> .cvat-organization-member-item-username,
.cvat-organization-member-item-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
> .cvat-organization-member-item-dates {
font-size: 12px;
display: grid;
}
> .cvat-organization-member-item-remove {
text-align: center;
}
> .cvat-organization-member-item-role {
> .ant-select {
width: 100%;
}
}
}
.cvat-organization-members-pagination-block {
display: flex;
justify-content: center;
margin-top: 8px;
margin-bottom: 8px;
}
.cvat-remove-organization-submit {
> input {
margin-top: $grid-unit-size;
}
}
.cvat-organization-invitation-field {
align-items: baseline;
}

@ -0,0 +1,341 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState, useRef, useEffect } from 'react';
import { useDispatch } from 'react-redux';
import moment from 'moment';
import { Row, Col } from 'antd/lib/grid';
import Text from 'antd/lib/typography/Text';
import Modal from 'antd/lib/modal';
import Button from 'antd/lib/button';
import Space from 'antd/lib/space';
import Input from 'antd/lib/input';
import Form from 'antd/lib/form';
import Select from 'antd/lib/select';
import { useForm } from 'antd/lib/form/Form';
import { Store } from 'antd/lib/form/interface';
import {
CloseOutlined, EditTwoTone, EnvironmentOutlined, MailOutlined, PhoneOutlined, PlusCircleOutlined,
} from '@ant-design/icons';
import {
inviteOrganizationMembersAsync,
leaveOrganizationAsync,
removeOrganizationAsync,
updateOrganizationAsync,
} from 'actions/organization-actions';
export interface Props {
organizationInstance: any;
userInstance: any;
fetchMembers: () => void;
}
function OrganizationTopBar(props: Props): JSX.Element {
const { organizationInstance, userInstance, fetchMembers } = props;
const {
owner, createdDate, description, updatedDate, slug, name, contact,
} = organizationInstance;
const { id: userID } = userInstance;
const [form] = useForm();
const descriptionEditingRef = useRef<HTMLDivElement>(null);
const [visibleInviteModal, setVisibleInviteModal] = useState<boolean>(false);
const [editingDescription, setEditingDescription] = useState<boolean>(false);
const dispatch = useDispatch();
useEffect(() => {
const listener = (event: MouseEvent): void => {
const divElement = descriptionEditingRef.current;
if (editingDescription && divElement && !event.composedPath().includes(divElement)) {
setEditingDescription(false);
}
};
window.addEventListener('mousedown', listener);
return () => {
window.removeEventListener('mousedown', listener);
};
});
let organizationName = name;
let organizationDescription = description;
let organizationContacts = contact;
return (
<>
<Row justify='space-between'>
<Col span={24}>
<div className='cvat-organization-top-bar-descriptions'>
<Text>
<Text className='cvat-title'>{`Organization: ${slug} `}</Text>
</Text>
<Text
editable={{
onChange: (value: string) => {
organizationName = value;
},
onEnd: () => {
organizationInstance.name = organizationName;
dispatch(updateOrganizationAsync(organizationInstance));
},
}}
type='secondary'
>
{name}
</Text>
{!editingDescription ? (
<span style={{ display: 'grid' }}>
{(description || 'Add description').split('\n').map((val: string, idx: number) => (
<Text key={idx} type='secondary'>
{val}
{idx === 0 ? <EditTwoTone onClick={() => setEditingDescription(true)} /> : null}
</Text>
))}
</span>
) : (
<div ref={descriptionEditingRef}>
<Input.TextArea
defaultValue={description}
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => {
organizationDescription = event.target.value;
}}
/>
<Button
size='small'
type='primary'
onClick={() => {
if (organizationDescription !== description) {
organizationInstance.description = organizationDescription;
dispatch(updateOrganizationAsync(organizationInstance));
}
setEditingDescription(false);
}}
>
Submit
</Button>
</div>
)}
</div>
</Col>
<Col span={12}>
<div className='cvat-organization-top-bar-contacts'>
<div>
<PhoneOutlined />
{ !contact.phoneNumber ? <Text type='secondary'>Add phone number</Text> : null }
<Text
type='secondary'
editable={{
onChange: (value: string) => {
organizationContacts = {
...organizationInstance.contact, phoneNumber: value,
};
},
onEnd: () => {
organizationInstance.contact = organizationContacts;
dispatch(updateOrganizationAsync(organizationInstance));
},
}}
>
{contact.phoneNumber}
</Text>
</div>
<div>
<MailOutlined />
{ !contact.email ? <Text type='secondary'>Add email</Text> : null }
<Text
type='secondary'
editable={{
onChange: (value: string) => {
organizationContacts = {
...organizationInstance.contact, email: value,
};
},
onEnd: () => {
organizationInstance.contact = organizationContacts;
dispatch(updateOrganizationAsync(organizationInstance));
},
}}
>
{contact.email}
</Text>
</div>
<div>
<EnvironmentOutlined />
{ !contact.location ? <Text type='secondary'>Add location</Text> : null }
<Text
type='secondary'
editable={{
onChange: (value: string) => {
organizationContacts = {
...organizationInstance.contact, location: value,
};
},
onEnd: () => {
organizationInstance.contact = organizationContacts;
dispatch(updateOrganizationAsync(organizationInstance));
},
}}
>
{contact.location}
</Text>
</div>
<Text type='secondary'>{`Created ${moment(createdDate).format('MMMM Do YYYY')}`}</Text>
<Text type='secondary'>{`Updated ${moment(updatedDate).fromNow()}`}</Text>
</div>
</Col>
<Col span={12} className='cvat-organization-top-bar-buttons-block'>
<Space align='end'>
{!(owner && userID === owner.id) ? (
<Button
type='primary'
danger
onClick={() => {
Modal.confirm({
onOk: () => {
dispatch(leaveOrganizationAsync(organizationInstance));
},
content: (
<>
<Text>Please, confirm leaving the organization</Text>
<Text strong>{` ${organizationInstance.slug}`}</Text>
<Text>. You will not have access to the organization data anymore</Text>
</>
),
okText: 'Leave',
okButtonProps: {
danger: true,
},
});
}}
>
Leave organization
</Button>
) : null}
{owner && userID === owner.id ? (
<Button
type='primary'
danger
onClick={() => {
const modal = Modal.confirm({
onOk: () => {
dispatch(removeOrganizationAsync(organizationInstance));
},
content: (
<div className='cvat-remove-organization-submit'>
<Text type='warning'>
To remove the organization, enter its short name below
</Text>
<Input
onChange={(event: React.ChangeEvent<HTMLInputElement>) => {
modal.update({
okButtonProps: {
disabled:
event.target.value !== organizationInstance.slug,
danger: true,
},
});
}}
/>
</div>
),
okButtonProps: {
disabled: true,
danger: true,
},
okText: 'Remove',
});
}}
>
Remove organization
</Button>
) : null}
<Button
type='primary'
onClick={() => setVisibleInviteModal(true)}
icon={<PlusCircleOutlined />}
>
Invite members
</Button>
</Space>
</Col>
</Row>
<Modal
visible={visibleInviteModal}
onCancel={() => {
setVisibleInviteModal(false);
form.resetFields(['users']);
}}
destroyOnClose
onOk={() => {
form.submit();
}}
>
<Form
initialValues={{
users: [{ email: '', role: 'worker' }],
}}
onFinish={(values: Store) => {
dispatch(
inviteOrganizationMembersAsync(organizationInstance, values.users, () => {
fetchMembers();
}),
);
setVisibleInviteModal(false);
form.resetFields(['users']);
}}
layout='vertical'
form={form}
>
<Text>Invitation list: </Text>
<Form.List name='users'>
{(fields, { add, remove }) => (
<>
{fields.map((field: any, index: number) => (
<Row className='cvat-organization-invitation-field' key={field.key}>
<Col span={10}>
<Form.Item
hasFeedback
name={[field.name, 'email']}
fieldKey={[field.fieldKey, 'email']}
rules={[
{ required: true, message: 'This field is required' },
{ type: 'email', message: 'The input is not a valid email' },
]}
>
<Input placeholder='Enter an email address' />
</Form.Item>
</Col>
<Col span={10} offset={1}>
<Form.Item
name={[field.name, 'role']}
fieldKey={[field.fieldKey, 'role']}
initialValue='worker'
rules={[{ required: true, message: 'This field is required' }]}
>
<Select>
<Select.Option value='worker'>Worker</Select.Option>
<Select.Option value='supervisor'>Supervisor</Select.Option>
<Select.Option value='maintainer'>Maintainer</Select.Option>
</Select>
</Form.Item>
</Col>
<Col span={1} offset={1}>
{index > 0 ? <CloseOutlined onClick={() => remove(field.name)} /> : null}
</Col>
</Row>
))}
<Form.Item>
<Button icon={<PlusCircleOutlined />} onClick={() => add()}>
Invite more
</Button>
</Form.Item>
</>
)}
</Form.List>
</Form>
</Modal>
</>
);
}
export default React.memo(OrganizationTopBar);

@ -45,8 +45,7 @@ function ShortcutsDialog(props: StateToProps & DispatchToProps): JSX.Element | n
const { visible, switchShortcutsDialog, jobInstance } = props; const { visible, switchShortcutsDialog, jobInstance } = props;
const keyMap = getApplicationKeyMap(); const keyMap = getApplicationKeyMap();
const splitToRows = (data: string[]): JSX.Element[] => const splitToRows = (data: string[]): JSX.Element[] => data.map(
data.map(
(item: string, id: number): JSX.Element => ( (item: string, id: number): JSX.Element => (
// eslint-disable-next-line react/no-array-index-key // eslint-disable-next-line react/no-array-index-key
<span key={id}> <span key={id}>
@ -81,8 +80,7 @@ function ShortcutsDialog(props: StateToProps & DispatchToProps): JSX.Element | n
}, },
]; ];
const dimensionType = jobInstance ? jobInstance.task.dimension : undefined; const dimensionType = jobInstance?.dimension;
const dataSource = Object.keys(keyMap) const dataSource = Object.keys(keyMap)
.filter((key: string) => !dimensionType || keyMap[key].applicable.includes(dimensionType)) .filter((key: string) => !dimensionType || keyMap[key].applicable.includes(dimensionType))
.map((key: string, id: number) => ({ .map((key: string, id: number) => ({

@ -10,10 +10,12 @@ import { LoadingOutlined, QuestionCircleOutlined, CopyOutlined } from '@ant-desi
import { ColumnFilterItem } from 'antd/lib/table/interface'; import { ColumnFilterItem } from 'antd/lib/table/interface';
import Table from 'antd/lib/table'; import Table from 'antd/lib/table';
import Button from 'antd/lib/button'; import Button from 'antd/lib/button';
import Select from 'antd/lib/select';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import moment from 'moment'; import moment from 'moment';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import { JobStage } from 'reducers/interfaces';
import CVATTooltip from 'components/common/cvat-tooltip'; import CVATTooltip from 'components/common/cvat-tooltip';
import UserSelector, { User } from './user-selector'; import UserSelector, { User } from './user-selector';
@ -28,9 +30,12 @@ function ReviewSummaryComponent({ jobInstance }: { jobInstance: any }): JSX.Elem
useEffect(() => { useEffect(() => {
setError(null); setError(null);
jobInstance jobInstance
.reviewsSummary() .issues(jobInstance.id)
.then((_summary: Record<string, any>) => { .then((issues: any[]) => {
setSummary(_summary); setSummary({
issues_unsolved: issues.filter((issue) => !issue.resolved_date).length,
issues_resolved: issues.filter((issue) => issue.resolved_date).length,
});
}) })
.catch((_error: any) => { .catch((_error: any) => {
// eslint-disable-next-line // eslint-disable-next-line
@ -59,18 +64,6 @@ function ReviewSummaryComponent({ jobInstance }: { jobInstance: any }): JSX.Elem
return ( return (
<table className='cvat-review-summary-description'> <table className='cvat-review-summary-description'>
<tbody> <tbody>
<tr>
<td>
<Text strong>Reviews</Text>
</td>
<td>{summary.reviews}</td>
</tr>
<tr>
<td>
<Text strong>Average quality</Text>
</td>
<td>{Number.parseFloat(summary.average_estimated_quality).toFixed(2)}</td>
</tr>
<tr> <tr>
<td> <td>
<Text strong>Unsolved issues</Text> <Text strong>Unsolved issues</Text>
@ -163,37 +156,61 @@ function JobListComponent(props: Props & RouteComponentProps): JSX.Element {
className: 'cvat-text-color cvat-job-item-frames', className: 'cvat-text-color cvat-job-item-frames',
}, },
{ {
title: 'Status', title: 'Stage',
dataIndex: 'status', dataIndex: 'stage',
key: 'status', key: 'stage',
className: 'cvat-job-item-status', className: 'cvat-job-item-stage',
render: (jobInstance: any): JSX.Element => { render: (jobInstance: any): JSX.Element => {
const { status } = jobInstance; const { stage } = jobInstance;
let progressColor = null;
if (status === 'completed') {
progressColor = 'cvat-job-completed-color';
} else if (status === 'validation') {
progressColor = 'cvat-job-validation-color';
} else {
progressColor = 'cvat-job-annotation-color';
}
return ( return (
<Text strong className={progressColor}> <div>
{status} <Select
value={stage}
onChange={(newValue: string) => {
jobInstance.stage = newValue;
onJobUpdate(jobInstance);
}}
>
<Select.Option value={JobStage.ANNOTATION}>{JobStage.ANNOTATION}</Select.Option>
<Select.Option value={JobStage.REVIEW}>{JobStage.REVIEW}</Select.Option>
<Select.Option value={JobStage.ACCEPTANCE}>{JobStage.ACCEPTANCE}</Select.Option>
</Select>
<CVATTooltip title={<ReviewSummaryComponent jobInstance={jobInstance} />}> <CVATTooltip title={<ReviewSummaryComponent jobInstance={jobInstance} />}>
<QuestionCircleOutlined /> <QuestionCircleOutlined />
</CVATTooltip> </CVATTooltip>
</Text> </div>
); );
}, },
sorter: sorter('status.status'), sorter: sorter('stage.stage'),
filters: [ filters: [
{ text: 'annotation', value: 'annotation' }, { text: 'annotation', value: 'annotation' },
{ text: 'validation', value: 'validation' }, { text: 'validation', value: 'validation' },
{ text: 'acceptance', value: 'acceptance' },
],
onFilter: (value: string | number | boolean, record: any) => record.stage.stage === value,
},
{
title: 'State',
dataIndex: 'state',
key: 'state',
className: 'cvat-job-item-state',
render: (jobInstance: any): JSX.Element => {
const { state } = jobInstance;
return (
<Text type='secondary'>
{state}
</Text>
);
},
sorter: sorter('state.state'),
filters: [
{ text: 'new', value: 'new' },
{ text: 'in progress', value: 'in progress' },
{ text: 'completed', value: 'completed' }, { text: 'completed', value: 'completed' },
{ text: 'rejected', value: 'rejected' },
], ],
onFilter: (value: string | number | boolean, record: any) => record.status.status === value, onFilter: (value: string | number | boolean, record: any) => record.state.state === value,
}, },
{ {
title: 'Started on', title: 'Started on',
@ -217,7 +234,6 @@ function JobListComponent(props: Props & RouteComponentProps): JSX.Element {
className='cvat-job-assignee-selector' className='cvat-job-assignee-selector'
value={jobInstance.assignee} value={jobInstance.assignee}
onSelect={(value: User | null): void => { onSelect={(value: User | null): void => {
// eslint-disable-next-line
jobInstance.assignee = value; jobInstance.assignee = value;
onJobUpdate(jobInstance); onJobUpdate(jobInstance);
}} }}
@ -225,35 +241,15 @@ function JobListComponent(props: Props & RouteComponentProps): JSX.Element {
), ),
sorter: sorter('assignee.assignee.username'), sorter: sorter('assignee.assignee.username'),
filters: collectUsers('assignee'), filters: collectUsers('assignee'),
onFilter: (value: string | number | boolean, record: any) => onFilter: (value: string | number | boolean, record: any) => (
(record.assignee.assignee?.username || false) === value, record.assignee.assignee?.username || false
}, ) === value,
{
title: 'Reviewer',
dataIndex: 'reviewer',
key: 'reviewer',
className: 'cvat-job-item-reviewer',
render: (jobInstance: any): JSX.Element => (
<UserSelector
className='cvat-job-reviewer-selector'
value={jobInstance.reviewer}
onSelect={(value: User | null): void => {
// eslint-disable-next-line
jobInstance.reviewer = value;
onJobUpdate(jobInstance);
}}
/>
),
sorter: sorter('reviewer.reviewer.username'),
filters: collectUsers('reviewer'),
onFilter: (value: string | number | boolean, record: any) =>
(record.reviewer.reviewer?.username || false) === value,
}, },
]; ];
let completed = 0; let completed = 0;
const data = jobs.reduce((acc: any[], job: any) => { const data = jobs.reduce((acc: any[], job: any) => {
if (job.status === 'completed') { if (job.stage === 'acceptance') {
completed++; completed++;
} }
@ -264,11 +260,11 @@ function JobListComponent(props: Props & RouteComponentProps): JSX.Element {
key: job.id, key: job.id,
job: job.id, job: job.id,
frames: `${job.startFrame}-${job.stopFrame}`, frames: `${job.startFrame}-${job.stopFrame}`,
status: job, state: job,
stage: job,
started: `${created.format('MMMM Do YYYY HH:MM')}`, started: `${created.format('MMMM Do YYYY HH:MM')}`,
duration: `${moment.duration(now.diff(created)).humanize()}`, duration: `${moment.duration(now.diff(created)).humanize()}`,
assignee: job, assignee: job,
reviewer: job,
}); });
return acc; return acc;
@ -301,10 +297,6 @@ function JobListComponent(props: Props & RouteComponentProps): JSX.Element {
serialized += `\t assigned to "${job.assignee.username}"`; serialized += `\t assigned to "${job.assignee.username}"`;
} }
if (job.reviewer) {
serialized += `\t reviewed by "${job.reviewer.username}"`;
}
serialized += '\n'; serialized += '\n';
} }
copy(serialized); copy(serialized);

@ -117,13 +117,9 @@
} }
} }
.cvat-job-item-status { .cvat-job-item-stage {
.cvat-job-annotation-color, .ant-select {
.cvat-job-validation-color, margin-right: $grid-unit-size;
.cvat-job-completed-color {
span {
margin-left: $grid-unit-size;
}
} }
} }
@ -140,15 +136,3 @@
} }
} }
} }
.cvat-job-completed-color {
color: $completed-progress-color;
}
.cvat-job-validation-color {
color: $inprogress-progress-color;
}
.cvat-job-annotation-color {
color: $pending-progress-color;
}

@ -52,9 +52,9 @@ class TaskPageComponent extends React.PureComponent<Props> {
} }
public render(): JSX.Element { public render(): JSX.Element {
const { task, updating } = this.props; const { task, updating, fetching } = this.props;
if (task === null) { if (task === null || fetching) {
return <Spin size='large' className='cvat-spinner' />; return <Spin size='large' className='cvat-spinner' />;
} }
@ -71,6 +71,7 @@ class TaskPageComponent extends React.PureComponent<Props> {
return ( return (
<> <>
{ updating ? <Spin size='large' className='cvat-spinner' /> : null }
<Row <Row
style={{ display: updating ? 'none' : undefined }} style={{ display: updating ? 'none' : undefined }}
justify='center' justify='center'
@ -85,7 +86,6 @@ class TaskPageComponent extends React.PureComponent<Props> {
</Row> </Row>
<ModelRunnerModal /> <ModelRunnerModal />
<MoveTaskModal /> <MoveTaskModal />
{updating && <Spin size='large' className='cvat-spinner' />}
</> </>
); );
} }

@ -89,7 +89,6 @@ export default function UserSelector(props: Props): JSX.Element {
}; };
const handleSelect = (_value: SelectValue): void => { const handleSelect = (_value: SelectValue): void => {
setSearchPhrase(users.filter((user) => user.id === +_value)[0].username);
const user = _value ? users.filter((_user) => _user.id === +_value)[0] : null; const user = _value ? users.filter((_user) => _user.id === +_value)[0] : null;
if ((user?.id || null) !== (value?.id || null)) { if ((user?.id || null) !== (value?.id || null)) {
onSelect(user); onSelect(user);
@ -101,7 +100,9 @@ export default function UserSelector(props: Props): JSX.Element {
if (!users.filter((user) => user.id === value.id).length) { if (!users.filter((user) => user.id === value.id).length) {
core.users.get({ id: value.id }).then((result: User[]) => { core.users.get({ id: value.id }).then((result: User[]) => {
const [user] = result; const [user] = result;
if (user) {
setUsers([...users, user]); setUsers([...users, user]);
}
}); });
} }

@ -14,12 +14,10 @@
.cvat-tasks-page-top-bar { .cvat-tasks-page-top-bar {
> div:nth-child(1) { > div:nth-child(1) {
> div:nth-child(1) { > div:nth-child(1) {
width: 100%;
> div:nth-child(1) { > div:nth-child(1) {
display: flex; display: flex;
span { > .cvat-title {
margin-right: $grid-unit-size; margin-right: $grid-unit-size;
} }
} }

@ -72,7 +72,7 @@ class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteCompone
const { taskInstance, activeInference, cancelAutoAnnotation } = this.props; const { taskInstance, activeInference, cancelAutoAnnotation } = this.props;
// Count number of jobs and performed jobs // Count number of jobs and performed jobs
const numOfJobs = taskInstance.jobs.length; const numOfJobs = taskInstance.jobs.length;
const numOfCompleted = taskInstance.jobs.filter((job: any): boolean => job.status === 'completed').length; const numOfCompleted = taskInstance.jobs.filter((job: any): boolean => job.stage === 'acceptance').length;
// Progress appearance depends on number of jobs // Progress appearance depends on number of jobs
let progressColor = null; let progressColor = null;

@ -10,8 +10,8 @@ const GITTER_URL = 'https://gitter.im/opencv-cvat';
const GITTER_PUBLIC_URL = 'https://gitter.im/opencv-cvat/public'; const GITTER_PUBLIC_URL = 'https://gitter.im/opencv-cvat/public';
const FORUM_URL = 'https://software.intel.com/en-us/forums/intel-distribution-of-openvino-toolkit'; const FORUM_URL = 'https://software.intel.com/en-us/forums/intel-distribution-of-openvino-toolkit';
const GITHUB_URL = 'https://github.com/openvinotoolkit/cvat'; const GITHUB_URL = 'https://github.com/openvinotoolkit/cvat';
const GITHUB_IMAGE_URL = const GITHUB_IMAGE_URL = 'https://github.com/openvinotoolkit/cvat/raw/develop/site/content/en/images/cvat.jpg';
'https://github.com/openvinotoolkit/cvat/raw/develop/site/content/en/images/cvat.jpg'; const GUIDE_URL = 'https://openvinotoolkit.github.io/cvat/docs';
const SHARE_MOUNT_GUIDE_URL = const SHARE_MOUNT_GUIDE_URL =
'https://openvinotoolkit.github.io/cvat/docs/administration/basics/installation/#share-path'; 'https://openvinotoolkit.github.io/cvat/docs/administration/basics/installation/#share-path';
const NUCLIO_GUIDE = const NUCLIO_GUIDE =
@ -94,6 +94,7 @@ export default {
FORUM_URL, FORUM_URL,
GITHUB_URL, GITHUB_URL,
GITHUB_IMAGE_URL, GITHUB_IMAGE_URL,
GUIDE_URL,
SHARE_MOUNT_GUIDE_URL, SHARE_MOUNT_GUIDE_URL,
CANVAS_BACKGROUND_COLORS, CANVAS_BACKGROUND_COLORS,
NEW_LABEL_COLOR, NEW_LABEL_COLOR,

@ -8,33 +8,31 @@ import { connect } from 'react-redux';
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
import { MenuInfo } from 'rc-menu/lib/interface'; import { MenuInfo } from 'rc-menu/lib/interface';
import { CombinedState, TaskStatus } from 'reducers/interfaces'; import { CombinedState, JobStage } from 'reducers/interfaces';
import AnnotationMenuComponent, { Actions } from 'components/annotation-page/top-bar/annotation-menu'; import AnnotationMenuComponent, { Actions } from 'components/annotation-page/top-bar/annotation-menu';
import { updateJobAsync } from 'actions/tasks-actions'; import { updateJobAsync } from 'actions/tasks-actions';
import { import {
uploadJobAnnotationsAsync, uploadJobAnnotationsAsync,
saveAnnotationsAsync, saveAnnotationsAsync,
switchRequestReviewDialog as switchRequestReviewDialogAction,
switchSubmitReviewDialog as switchSubmitReviewDialogAction,
setForceExitAnnotationFlag as setForceExitAnnotationFlagAction, setForceExitAnnotationFlag as setForceExitAnnotationFlagAction,
removeAnnotationsAsync as removeAnnotationsAsyncAction, removeAnnotationsAsync as removeAnnotationsAsyncAction,
} from 'actions/annotation-actions'; } from 'actions/annotation-actions';
import { exportActions } from 'actions/export-actions'; import { exportActions } from 'actions/export-actions';
import getCore from 'cvat-core-wrapper';
const core = getCore();
interface StateToProps { interface StateToProps {
annotationFormats: any; annotationFormats: any;
jobInstance: any; jobInstance: any;
stopFrame: number; stopFrame: number;
loadActivity: string | null; loadActivity: string | null;
user: any;
} }
interface DispatchToProps { interface DispatchToProps {
loadAnnotations(job: any, loader: any, file: File): void; loadAnnotations(job: any, loader: any, file: File): void;
showExportModal(task: any): void; showExportModal(jobInstance: any): void;
removeAnnotations(startnumber:number, endnumber:number, delTrackKeyframesOnly:boolean): void; removeAnnotations(startnumber: number, endnumber: number, delTrackKeyframesOnly: boolean): void;
switchRequestReviewDialog(visible: boolean): void;
switchSubmitReviewDialog(visible: boolean): void;
setForceExitAnnotationFlag(forceExit: boolean): void; setForceExitAnnotationFlag(forceExit: boolean): void;
saveAnnotations(jobInstance: any, afterSave?: () => void): void; saveAnnotations(jobInstance: any, afterSave?: () => void): void;
updateJob(jobInstance: any): void; updateJob(jobInstance: any): void;
@ -53,10 +51,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
tasks: { tasks: {
activities: { loads }, activities: { loads },
}, },
auth: { user },
} = state; } = state;
const taskID = jobInstance.task.id; const taskID = jobInstance.taskId;
const jobID = jobInstance.id; const jobID = jobInstance.id;
return { return {
@ -64,7 +61,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
jobInstance, jobInstance,
stopFrame, stopFrame,
annotationFormats, annotationFormats,
user,
}; };
} }
@ -73,18 +69,12 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
loadAnnotations(job: any, loader: any, file: File): void { loadAnnotations(job: any, loader: any, file: File): void {
dispatch(uploadJobAnnotationsAsync(job, loader, file)); dispatch(uploadJobAnnotationsAsync(job, loader, file));
}, },
showExportModal(task: any): void { showExportModal(jobInstance: any): void {
dispatch(exportActions.openExportModal(task)); dispatch(exportActions.openExportModal(jobInstance));
}, },
removeAnnotations(startnumber: number, endnumber: number, delTrackKeyframesOnly:boolean) { removeAnnotations(startnumber: number, endnumber: number, delTrackKeyframesOnly:boolean) {
dispatch(removeAnnotationsAsyncAction(startnumber, endnumber, delTrackKeyframesOnly)); dispatch(removeAnnotationsAsyncAction(startnumber, endnumber, delTrackKeyframesOnly));
}, },
switchRequestReviewDialog(visible: boolean): void {
dispatch(switchRequestReviewDialogAction(visible));
},
switchSubmitReviewDialog(visible: boolean): void {
dispatch(switchSubmitReviewDialogAction(visible));
},
setForceExitAnnotationFlag(forceExit: boolean): void { setForceExitAnnotationFlag(forceExit: boolean): void {
dispatch(setForceExitAnnotationFlagAction(forceExit)); dispatch(setForceExitAnnotationFlagAction(forceExit));
}, },
@ -103,15 +93,12 @@ function AnnotationMenuContainer(props: Props): JSX.Element {
const { const {
jobInstance, jobInstance,
stopFrame, stopFrame,
user,
annotationFormats: { loaders, dumpers }, annotationFormats: { loaders, dumpers },
history, history,
loadActivity, loadActivity,
loadAnnotations, loadAnnotations,
showExportModal, showExportModal,
removeAnnotations, removeAnnotations,
switchRequestReviewDialog,
switchSubmitReviewDialog,
setForceExitAnnotationFlag, setForceExitAnnotationFlag,
saveAnnotations, saveAnnotations,
updateJob, updateJob,
@ -127,29 +114,29 @@ function AnnotationMenuContainer(props: Props): JSX.Element {
const onClickMenu = (params: MenuInfo): void => { const onClickMenu = (params: MenuInfo): void => {
const [action] = params.keyPath; const [action] = params.keyPath;
if (action === Actions.EXPORT_TASK_DATASET) { if (action === Actions.EXPORT_TASK_DATASET) {
showExportModal(jobInstance.task); showExportModal(jobInstance);
} else if (action === Actions.REQUEST_REVIEW) {
switchRequestReviewDialog(true);
} else if (action === Actions.SUBMIT_REVIEW) {
switchSubmitReviewDialog(true);
} else if (action === Actions.RENEW_JOB) { } else if (action === Actions.RENEW_JOB) {
jobInstance.status = TaskStatus.ANNOTATION; jobInstance.state = core.enums.JobState.NEW;
jobInstance.stage = JobStage.ANNOTATION;
updateJob(jobInstance); updateJob(jobInstance);
history.push(`/tasks/${jobInstance.task.id}`); window.location.reload();
} else if (action === Actions.FINISH_JOB) { } else if (action === Actions.FINISH_JOB) {
jobInstance.status = TaskStatus.COMPLETED; jobInstance.stage = JobStage.ACCEPTANCE;
jobInstance.state = core.enums.JobState.COMPLETED;
updateJob(jobInstance); updateJob(jobInstance);
history.push(`/tasks/${jobInstance.task.id}`); history.push(`/tasks/${jobInstance.taskId}`);
} else if (action === Actions.OPEN_TASK) { } else if (action === Actions.OPEN_TASK) {
history.push(`/tasks/${jobInstance.task.id}`); history.push(`/tasks/${jobInstance.taskId}`);
} else if (action.startsWith('state:')) {
[, jobInstance.state] = action.split(':');
updateJob(jobInstance);
window.location.reload();
} }
}; };
const isReviewer = jobInstance.reviewer?.id === user.id || user.isSuperuser;
return ( return (
<AnnotationMenuComponent <AnnotationMenuComponent
taskMode={jobInstance.task.mode} taskMode={jobInstance.mode}
loaders={loaders} loaders={loaders}
dumpers={dumpers} dumpers={dumpers}
loadActivity={loadActivity} loadActivity={loadActivity}
@ -159,7 +146,6 @@ function AnnotationMenuContainer(props: Props): JSX.Element {
setForceExitAnnotationFlag={setForceExitAnnotationFlag} setForceExitAnnotationFlag={setForceExitAnnotationFlag}
saveAnnotations={saveAnnotations} saveAnnotations={saveAnnotations}
jobInstance={jobInstance} jobInstance={jobInstance}
isReviewer={isReviewer}
stopFrame={stopFrame} stopFrame={stopFrame}
/> />
); );

@ -1,81 +0,0 @@
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { connect } from 'react-redux';
import { CombinedState } from 'reducers/interfaces';
import { showStatistics } from 'actions/annotation-actions';
import StatisticsModalComponent from 'components/annotation-page/top-bar/statistics-modal';
interface StateToProps {
visible: boolean;
collecting: boolean;
data: any;
jobInstance: any;
jobStatus: string;
savingJobStatus: boolean;
}
interface DispatchToProps {
closeStatistics(): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const {
annotation: {
statistics: { visible, collecting, data },
job: {
saving: savingJobStatus,
instance: { status: jobStatus },
instance: jobInstance,
},
},
} = state;
return {
visible,
collecting,
data,
jobInstance,
jobStatus,
savingJobStatus,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
closeStatistics(): void {
dispatch(showStatistics(false));
},
};
}
type Props = StateToProps & DispatchToProps;
class StatisticsModalContainer extends React.PureComponent<Props> {
public render(): JSX.Element {
const {
jobInstance, visible, collecting, data, closeStatistics, jobStatus, savingJobStatus,
} = this.props;
return (
<StatisticsModalComponent
jobInstance={jobInstance}
collecting={collecting}
data={data}
visible={visible}
jobStatus={jobStatus}
bugTracker={jobInstance.task.bugTracker}
startFrame={jobInstance.startFrame}
stopFrame={jobInstance.stopFrame}
assignee={jobInstance.assignee ? jobInstance.assignee.username : 'Nobody'}
reviewer={jobInstance.reviewer ? jobInstance.reviewer.username : 'Nobody'}
savingJobStatus={savingJobStatus}
closeStatistics={closeStatistics}
/>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(StatisticsModalContainer);

@ -214,8 +214,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
const self = this; const self = this;
this.unblock = history.block((location: any) => { this.unblock = history.block((location: any) => {
const { forceExit } = self.props; const { forceExit } = self.props;
const { task, id: jobID } = jobInstance; const { id: jobID, taskId: taskID } = jobInstance;
const { id: taskID } = task;
if ( if (
jobInstance.annotations.hasUnsavedChanges() && jobInstance.annotations.hasUnsavedChanges() &&
@ -506,7 +505,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
if (stillPlaying) { if (stillPlaying) {
if (isAbleToChangeFrame()) { if (isAbleToChangeFrame()) {
onChangeFrame(frameNumber + 1 + framesSkipped, stillPlaying, framesSkipped + 1); onChangeFrame(frameNumber + 1 + framesSkipped, stillPlaying, framesSkipped + 1);
} else if (jobInstance.task.dimension === DimensionType.DIM_2D) { } else if (jobInstance.dimension === DimensionType.DIM_2D) {
onSwitchPlay(false); onSwitchPlay(false);
} else { } else {
setTimeout(() => this.play(), frameDelay); setTimeout(() => this.play(), frameDelay);

@ -5,7 +5,6 @@
import React from 'react'; import React from 'react';
import SVGCVATLogo from './assets/cvat-logo.svg'; import SVGCVATLogo from './assets/cvat-logo.svg';
import SVGAccountIcon from './assets/account-icon.svg';
import SVGEmptyTasksIcon from './assets/empty-tasks-icon.svg'; import SVGEmptyTasksIcon from './assets/empty-tasks-icon.svg';
import SVGMenuIcon from './assets/menu-icon.svg'; import SVGMenuIcon from './assets/menu-icon.svg';
import SVGCursorIcon from './assets/cursor-icon.svg'; import SVGCursorIcon from './assets/cursor-icon.svg';
@ -55,7 +54,6 @@ import SVGCVATS3Provider from './assets/S3.svg';
import SVGCVATGoogleCloudProvider from './assets/google-cloud.svg'; import SVGCVATGoogleCloudProvider from './assets/google-cloud.svg';
export const CVATLogo = React.memo((): JSX.Element => <SVGCVATLogo />); export const CVATLogo = React.memo((): JSX.Element => <SVGCVATLogo />);
export const AccountIcon = React.memo((): JSX.Element => <SVGAccountIcon />);
export const EmptyTasksIcon = React.memo((): JSX.Element => <SVGEmptyTasksIcon />); export const EmptyTasksIcon = React.memo((): JSX.Element => <SVGEmptyTasksIcon />);
export const MenuIcon = React.memo((): JSX.Element => <SVGMenuIcon />); export const MenuIcon = React.memo((): JSX.Element => <SVGMenuIcon />);
export const CursorIcon = React.memo((): JSX.Element => <SVGCursorIcon />); export const CursorIcon = React.memo((): JSX.Element => <SVGCursorIcon />);

@ -21,6 +21,7 @@ import logger, { LogType } from 'cvat-logger';
import createCVATStore, { getCVATStore } from 'cvat-store'; import createCVATStore, { getCVATStore } from 'cvat-store';
import { KeyMap } from 'utils/mousetrap-react'; import { KeyMap } from 'utils/mousetrap-react';
import createRootReducer from 'reducers/root-reducer'; import createRootReducer from 'reducers/root-reducer';
import { getOrganizationsAsync } from 'actions/organization-actions';
import { resetErrors, resetMessages } from './actions/notification-actions'; import { resetErrors, resetMessages } from './actions/notification-actions';
import { CombinedState, NotificationsState } from './reducers/interfaces'; import { CombinedState, NotificationsState } from './reducers/interfaces';
@ -34,6 +35,8 @@ interface StateToProps {
modelsFetching: boolean; modelsFetching: boolean;
userInitialized: boolean; userInitialized: boolean;
userFetching: boolean; userFetching: boolean;
organizationsFetching: boolean;
organizationsInitialized: boolean;
aboutInitialized: boolean; aboutInitialized: boolean;
aboutFetching: boolean; aboutFetching: boolean;
formatsInitialized: boolean; formatsInitialized: boolean;
@ -62,6 +65,7 @@ interface DispatchToProps {
loadUserAgreements: () => void; loadUserAgreements: () => void;
switchSettingsDialog: () => void; switchSettingsDialog: () => void;
loadAuthActions: () => void; loadAuthActions: () => void;
loadOrganizations: () => void;
} }
function mapStateToProps(state: CombinedState): StateToProps { function mapStateToProps(state: CombinedState): StateToProps {
@ -72,10 +76,13 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { shortcuts } = state; const { shortcuts } = state;
const { userAgreements } = state; const { userAgreements } = state;
const { models } = state; const { models } = state;
const { organizations } = state;
return { return {
userInitialized: auth.initialized, userInitialized: auth.initialized,
userFetching: auth.fetching, userFetching: auth.fetching,
organizationsFetching: organizations.fetching,
organizationsInitialized: organizations.initialized,
pluginsInitialized: plugins.initialized, pluginsInitialized: plugins.initialized,
pluginsFetching: plugins.fetching, pluginsFetching: plugins.fetching,
modelsInitialized: models.initialized, modelsInitialized: models.initialized,
@ -110,6 +117,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
switchShortcutsDialog: (): void => dispatch(shortcutsActions.switchShortcutsDialog()), switchShortcutsDialog: (): void => dispatch(shortcutsActions.switchShortcutsDialog()),
switchSettingsDialog: (): void => dispatch(switchSettingsDialog()), switchSettingsDialog: (): void => dispatch(switchSettingsDialog()),
loadAuthActions: (): void => dispatch(loadAuthActionsAsync()), loadAuthActions: (): void => dispatch(loadAuthActionsAsync()),
loadOrganizations: (): void => dispatch(getOrganizationsAsync()),
}; };
} }

@ -8,14 +8,15 @@ import { AuthActionTypes } from 'actions/auth-actions';
import { BoundariesActionTypes } from 'actions/boundaries-actions'; import { BoundariesActionTypes } from 'actions/boundaries-actions';
import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; import { Canvas, CanvasMode } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper'; import { Canvas3d } from 'cvat-canvas3d-wrapper';
import { import {
ActiveControl, ActiveControl,
AnnotationState, AnnotationState,
ContextMenuType, ContextMenuType,
DimensionType, DimensionType,
JobStage,
ObjectType, ObjectType,
ShapeType, ShapeType,
TaskStatus,
Workspace, Workspace,
} from './interfaces'; } from './interfaces';
@ -111,8 +112,6 @@ const defaultState: AnnotationState = {
sidebarCollapsed: false, sidebarCollapsed: false,
appearanceCollapsed: false, appearanceCollapsed: false,
filtersPanelVisible: false, filtersPanelVisible: false,
requestReviewDialogVisible: false,
submitReviewDialogVisible: false,
predictor: { predictor: {
enabled: false, enabled: false,
error: null, error: null,
@ -157,11 +156,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
maxZ, maxZ,
} = action.payload; } = action.payload;
const isReview = job.status === TaskStatus.REVIEW; const isReview = job.stage === JobStage.REVIEW;
let workspaceSelected = Workspace.STANDARD; let workspaceSelected = Workspace.STANDARD;
let activeShapeType = ShapeType.RECTANGLE; let activeShapeType = ShapeType.RECTANGLE;
if (job.task.dimension === DimensionType.DIM_3D) { if (job.dimension === DimensionType.DIM_3D) {
workspaceSelected = Workspace.STANDARD3D; workspaceSelected = Workspace.STANDARD3D;
activeShapeType = ShapeType.CUBOID; activeShapeType = ShapeType.CUBOID;
} }
@ -177,11 +176,9 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
openTime, openTime,
fetching: false, fetching: false,
instance: job, instance: job,
labels: job.task.labels, labels: job.labels,
attributes: job.task.labels.reduce((acc: Record<number, any[]>, label: any): Record< attributes: job.labels
number, .reduce((acc: Record<number, any[]>, label: any): Record<number, any[]> => {
any[]
> => {
acc[label.id] = label.attributes; acc[label.id] = label.attributes;
return acc; return acc;
}, {}), }, {}),
@ -209,13 +206,13 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}, },
drawing: { drawing: {
...state.drawing, ...state.drawing,
activeLabelID: job.task.labels.length ? job.task.labels[0].id : null, activeLabelID: job.labels.length ? job.labels[0].id : null,
activeObjectType: job.task.mode === 'interpolation' ? ObjectType.TRACK : ObjectType.SHAPE, activeObjectType: job.mode === 'interpolation' ? ObjectType.TRACK : ObjectType.SHAPE,
activeShapeType, activeShapeType,
}, },
canvas: { canvas: {
...state.canvas, ...state.canvas,
instance: job.task.dimension === DimensionType.DIM_2D ? new Canvas() : new Canvas3d(), instance: job.dimension === DimensionType.DIM_2D ? new Canvas() : new Canvas3d(),
}, },
colors, colors,
workspace: isReview ? Workspace.REVIEW_WORKSPACE : workspaceSelected, workspace: isReview ? Workspace.REVIEW_WORKSPACE : workspaceSelected,
@ -1073,20 +1070,6 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
}, },
}; };
} }
case AnnotationActionTypes.SWITCH_REQUEST_REVIEW_DIALOG: {
const { visible } = action.payload;
return {
...state,
requestReviewDialogVisible: visible,
};
}
case AnnotationActionTypes.SWITCH_SUBMIT_REVIEW_DIALOG: {
const { visible } = action.payload;
return {
...state,
submitReviewDialogVisible: visible,
};
}
case AnnotationActionTypes.SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG: { case AnnotationActionTypes.SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG: {
const { forceExit } = action.payload; const { forceExit } = action.payload;
return { return {

@ -34,31 +34,33 @@ export default (state: ExportState = defaultState, action: ExportActions): Expor
case ExportActionTypes.EXPORT_DATASET: { case ExportActionTypes.EXPORT_DATASET: {
const { instance, format } = action.payload; const { instance, format } = action.payload;
const activities = deepCopy(instance instanceof core.classes.Project ? state.projects : state.tasks); const activities = deepCopy(instance instanceof core.classes.Project ? state.projects : state.tasks);
const instanceId = instance instanceof core.classes.Project ||
instance instanceof core.classes.Task ? instance.id : instance.taskId;
activities[instance.id] = activities[instanceId] =
instance.id in activities && !activities[instance.id].includes(format) ? instanceId in activities && !activities[instanceId].includes(format) ?
[...activities[instance.id], format] : [...activities[instanceId], format] :
activities[instance.id] || [format]; activities[instanceId] || [format];
return { return {
...state, ...state,
tasks: instance instanceof core.classes.Task ? activities : state.tasks, ...(instance instanceof core.classes.Project ? { projects: activities } : { tasks: activities }),
projects: instance instanceof core.classes.Project ? activities : state.projects,
}; };
} }
case ExportActionTypes.EXPORT_DATASET_FAILED: case ExportActionTypes.EXPORT_DATASET_FAILED:
case ExportActionTypes.EXPORT_DATASET_SUCCESS: { case ExportActionTypes.EXPORT_DATASET_SUCCESS: {
const { instance, format } = action.payload; const { instance, format } = action.payload;
const activities = deepCopy(instance instanceof core.classes.Project ? state.projects : state.tasks); const activities = deepCopy(instance instanceof core.classes.Project ? state.projects : state.tasks);
const instanceId = instance instanceof core.classes.Project ||
instance instanceof core.classes.Task ? instance.id : instance.taskId;
activities[instance.id] = activities[instance.id].filter( activities[instanceId] = activities[instanceId].filter(
(exporterName: string): boolean => exporterName !== format, (exporterName: string): boolean => exporterName !== format,
); );
return { return {
...state, ...state,
tasks: instance instanceof core.classes.Task ? activities : state.tasks, ...(instance instanceof core.classes.Project ? { projects: activities } : { tasks: activities }),
projects: instance instanceof core.classes.Project ? activities : state.projects,
}; };
} }
default: default:

@ -281,6 +281,12 @@ export enum TaskStatus {
COMPLETED = 'completed', COMPLETED = 'completed',
} }
export enum JobStage {
ANNOTATION = 'annotation',
REVIEW = 'validation',
ACCEPTANCE = 'acceptance',
}
export enum RQStatus { export enum RQStatus {
unknown = 'unknown', unknown = 'unknown',
queued = 'queued', queued = 'queued',
@ -307,8 +313,8 @@ export interface ModelsState {
inferences: { inferences: {
[index: number]: ActiveInference; [index: number]: ActiveInference;
}; };
visibleRunWindows: boolean; modelRunnerIsVisible: boolean;
activeRunTask: any; modelRunnerTask: any;
} }
export interface ErrorState { export interface ErrorState {
@ -349,6 +355,9 @@ export interface NotificationsState {
importing: null | ErrorState; importing: null | ErrorState;
moving: null | ErrorState; moving: null | ErrorState;
}; };
jobs: {
updating: null | ErrorState;
};
formats: { formats: {
fetching: null | ErrorState; fetching: null | ErrorState;
}; };
@ -399,7 +408,6 @@ export interface NotificationsState {
fetching: null | ErrorState; fetching: null | ErrorState;
}; };
review: { review: {
initialization: null | ErrorState;
finishingIssue: null | ErrorState; finishingIssue: null | ErrorState;
resolvingIssue: null | ErrorState; resolvingIssue: null | ErrorState;
reopeningIssue: null | ErrorState; reopeningIssue: null | ErrorState;
@ -424,6 +432,17 @@ export interface NotificationsState {
updating: null | ErrorState; updating: null | ErrorState;
deleting: null | ErrorState; deleting: null | ErrorState;
}; };
organizations: {
fetching: null | ErrorState;
creating: null | ErrorState;
updating: null | ErrorState;
activation: null | ErrorState;
deleting: null | ErrorState;
leaving: null | ErrorState;
inviting: null | ErrorState;
updatingMembership: null | ErrorState;
removingMembership: null | ErrorState;
};
}; };
messages: { messages: {
tasks: { tasks: {
@ -600,8 +619,6 @@ export interface AnnotationState {
}; };
colors: any[]; colors: any[];
filtersPanelVisible: boolean; filtersPanelVisible: boolean;
requestReviewDialogVisible: boolean;
submitReviewDialogVisible: boolean;
sidebarCollapsed: boolean; sidebarCollapsed: boolean;
appearanceCollapsed: boolean; appearanceCollapsed: boolean;
workspace: Workspace; workspace: Workspace;
@ -700,20 +717,31 @@ export enum ReviewStatus {
} }
export interface ReviewState { export interface ReviewState {
reviews: any[];
issues: any[]; issues: any[];
frameIssues: any[]; frameIssues: any[];
latestComments: string[]; latestComments: string[];
activeReview: any | null;
newIssuePosition: number[] | null; newIssuePosition: number[] | null;
issuesHidden: boolean; issuesHidden: boolean;
issuesResolvedHidden: boolean; issuesResolvedHidden: boolean;
fetching: { fetching: {
reviewId: number | null; jobId: number | null;
issueId: number | null; issueId: number | null;
}; };
} }
export interface OrganizationState {
list: any[];
current: any | null;
initialized: boolean;
fetching: boolean;
creating: boolean;
updating: boolean;
inviting: boolean;
leaving: boolean;
removingMember: boolean;
updatingMember: boolean;
}
export interface CombinedState { export interface CombinedState {
auth: AuthState; auth: AuthState;
projects: ProjectsState; projects: ProjectsState;
@ -732,6 +760,7 @@ export interface CombinedState {
export: ExportState; export: ExportState;
import: ImportState; import: ImportState;
cloudStorages: CloudStoragesState; cloudStorages: CloudStoragesState;
organizations: OrganizationState;
} }
export enum DimensionType { export enum DimensionType {

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -15,8 +15,8 @@ const defaultState: ModelsState = {
detectors: [], detectors: [],
trackers: [], trackers: [],
reid: [], reid: [],
visibleRunWindows: false, modelRunnerIsVisible: false,
activeRunTask: null, modelRunnerTask: null,
inferences: {}, inferences: {},
}; };
@ -50,15 +50,15 @@ export default function (state = defaultState, action: ModelsActions | AuthActio
case ModelsActionTypes.SHOW_RUN_MODEL_DIALOG: { case ModelsActionTypes.SHOW_RUN_MODEL_DIALOG: {
return { return {
...state, ...state,
visibleRunWindows: true, modelRunnerIsVisible: true,
activeRunTask: action.payload.taskInstance, modelRunnerTask: action.payload.taskInstance,
}; };
} }
case ModelsActionTypes.CLOSE_RUN_MODEL_DIALOG: { case ModelsActionTypes.CLOSE_RUN_MODEL_DIALOG: {
return { return {
...state, ...state,
visibleRunWindows: false, modelRunnerIsVisible: false,
activeRunTask: null, modelRunnerTask: null,
}; };
} }
case ModelsActionTypes.GET_INFERENCE_STATUS_SUCCESS: { case ModelsActionTypes.GET_INFERENCE_STATUS_SUCCESS: {

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

Loading…
Cancel
Save