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
steps:
- uses: actions/checkout@v2
- uses: actions/setup-python@v2
with:
python-version: '3.8'
- name: Getting SHA from the default branch
id: get-sha
run: |
@ -63,6 +66,22 @@ jobs:
cache-from: type=local,src=/tmp/cvat_cache_server
tags: openvino/cvat_server:latest
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
env:
HOST_COVERAGE_DATA_DIR: ${{ github.workspace }}

1
.gitignore vendored

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

@ -45,7 +45,7 @@
"python": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"env": {
"CVAT_SERVERLESS": "1"
"CVAT_SERVERLESS": "1"
},
"args": [
"runserver",
@ -62,10 +62,10 @@
"type": "pwa-chrome",
"request": "launch",
"url": "http://localhost:7000/",
"disableNetworkCache":true,
"disableNetworkCache": true,
"trace": true,
"showAsyncStacks": true,
"pathMapping":{
"pathMapping": {
"/static/engine/": "${workspaceFolder}/cvat/apps/engine/static/engine/",
"/static/dashboard/": "${workspaceFolder}/cvat/apps/dashboard/static/dashboard/",
}
@ -111,7 +111,7 @@
"request": "launch",
"justMyCode": false,
"stopOnEntry": false,
"python":"${command:python.interpreterPath}",
"python": "${command:python.interpreterPath}",
"program": "${workspaceRoot}/manage.py",
"args": [
"rqworker",
@ -195,8 +195,8 @@
"name": "jest debug",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": [
"--config",
"${workspaceFolder}/cvat-core/jest.config.js"
"--config",
"${workspaceFolder}/cvat-core/jest.config.js"
],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen",

@ -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>)
- 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>)
- 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>)
- 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>)
### 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
- 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
- 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 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>)
- 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>)
- 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>)
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>)
@ -63,6 +67,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- 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>)
- 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>)

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

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

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

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

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

@ -10,30 +10,28 @@ export enum Orientation {
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 {
const L1 = line(p1, p2);
const L2 = line(p3, p4);
const D = L1[0] * L2[1] - L1[1] * L2[0];
const Dx = L1[2] * L2[1] - L1[1] * L2[2];
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 };
// Check if none of the lines are of length 0
const { x: x1, y: y1 } = p1;
const { x: x2, y: y2 } = p2;
const { x: x3, y: y3 } = p3;
const { x: x4, y: y4 } = p4;
if ((x1 === x2 && y1 === y2) || (x3 === x4 && y3 === y4)) {
return null;
}
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 {

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

@ -1,6 +1,6 @@
{
"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",
"main": "babel.config.js",
"scripts": {

@ -276,7 +276,7 @@
if (instance instanceof Task) {
result = await serverProxy.tasks.exportDataset(instance.id, format, name, saveImages);
} 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 {
result = await serverProxy.projects.exportDataset(instance.id, format, name, saveImages);
}

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

@ -15,7 +15,6 @@ function build() {
const Statistics = require('./statistics');
const Comment = require('./comment');
const Issue = require('./issue');
const Review = require('./review');
const { Job, Task } = require('./session');
const { Project } = require('./project');
const implementProject = require('./project-implementation');
@ -23,6 +22,7 @@ function build() {
const MLModel = require('./ml-model');
const { FrameData } = require('./frames');
const { CloudStorage } = require('./cloud-storage');
const Organization = require('./organization');
const enums = require('./enums');
@ -792,6 +792,50 @@ function build() {
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 classes
@ -810,9 +854,9 @@ function build() {
MLModel,
Comment,
Issue,
Review,
FrameData,
CloudStorage,
Organization,
},
};
@ -826,6 +870,7 @@ function build() {
cvat.client = Object.freeze(cvat.client);
cvat.enums = Object.freeze(cvat.enums);
cvat.cloudStorages = Object.freeze(cvat.cloudStorages);
cvat.organizations = Object.freeze(cvat.organizations);
const implementAPI = require('./api-implementation');

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

@ -97,37 +97,30 @@
);
}
function negativeIDGenerator() {
const value = negativeIDGenerator.start;
negativeIDGenerator.start -= 1;
return value;
}
negativeIDGenerator.start = -1;
class FieldUpdateTrigger {
constructor(initialFields) {
const data = { ...initialFields };
constructor() {
let updatedFlags = {};
Object.defineProperties(
this,
Object.freeze({
...Object.assign(
{},
...Array.from(Object.keys(data), (key) => ({
[key]: {
get: () => data[key],
set: (value) => {
data[key] = value;
},
enumerable: true,
},
})),
),
reset: {
value: () => {
Object.keys(data).forEach((key) => {
data[key] = false;
});
updatedFlags = {};
},
},
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,
checkFilter,
checkObjectType,
negativeIDGenerator,
checkExclusiveFields,
camelToSnake,
FieldUpdateTrigger,

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

@ -34,33 +34,51 @@
});
/**
* Task dimension
* @enum
* @name DimensionType
* Job stages
* @enum {string}
* @name JobStage
* @memberof module:API.cvat.enums
* @property {string} DIMENSION_2D '2d'
* @property {string} DIMENSION_3D '3d'
* @property {string} ANNOTATION 'annotation'
* @property {string} VALIDATION 'validation'
* @property {string} ACCEPTANCE 'acceptance'
* @readonly
*/
const DimensionType = Object.freeze({
DIMENSION_2D: '2d',
DIMENSION_3D: '3d',
const JobStage = Object.freeze({
ANNOTATION: 'annotation',
VALIDATION: 'validation',
ACCEPTANCE: 'acceptance',
});
/**
* Review statuses
* Job states
* @enum {string}
* @name ReviewStatus
* @name JobState
* @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} REVIEW_FURTHER 'review_further'
* @readonly
*/
const ReviewStatus = Object.freeze({
ACCEPTED: 'accepted',
const JobState = Object.freeze({
NEW: 'new',
IN_PROGRESS: 'in progress',
COMPLETED: 'completed',
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',
});
/**
* 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
* @enum {string}
@ -388,7 +424,8 @@
module.exports = {
ShareFileType,
TaskStatus,
ReviewStatus,
JobStage,
JobState,
TaskMode,
AttributeType,
ObjectType,
@ -402,6 +439,7 @@
DimensionType,
CloudStorageProviderType,
CloudStorageCredentialsType,
MembershipRole,
SortingMethod,
};
})();

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

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

@ -1,10 +1,9 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
const serverProxy = require('./server-proxy');
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
const MLModel = require('./ml-model');
const { RQStatus } = require('./enums');
@ -35,11 +34,9 @@ class LambdaManager {
return models;
}
async run(task, model, args) {
if (!(task instanceof Task)) {
throw new ArgumentError(
`Argument task is expected to be an instance of Task class, but got ${typeof task}`,
);
async run(taskID, model, args) {
if (!Number.isInteger(taskID) || taskID < 0) {
throw new ArgumentError(`Argument taskID must be a positive integer. Got "${taskID}"`);
}
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}`);
}
const body = args;
body.task = task.id;
body.function = model.id;
const body = {
...args,
task: taskID,
function: model.id,
};
const result = await serverProxy.lambda.run(body);
return result.id;
}
async call(task, model, args) {
const body = args;
body.task = task.id;
async call(taskID, model, args) {
if (!Number.isInteger(taskID) || taskID < 0) {
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);
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) {
projectClass.prototype.save.implementation = async function () {
const trainingProjectCopy = this.trainingProject;
if (typeof this.id !== 'undefined') {
// project has been already created, need to update some data
const projectData = {
name: this.name,
assignee_id: this.assignee ? this.assignee.id : null,
bug_tracker: this.bugTracker,
labels: [...this._internalData.labels.map((el) => el.toJSON())],
};
if (trainingProjectCopy) {
projectData.training_project = trainingProjectCopy;
const projectData = this._updateTrigger.getUpdated(this, {
bugTracker: 'bug_tracker',
trainingProject: 'training_project',
assignee: 'assignee_id',
});
if (projectData.assignee_id) {
projectData.assignee_id = projectData.assignee_id.id;
}
if (projectData.labels) {
projectData.labels = projectData.labels.map((el) => el.toJSON());
}
await serverProxy.projects.save(this.id, projectData);
this._updateTrigger.reset();
return this;
}
// initial creating
const projectSpec = {
name: this.name,
labels: [...this.labels.map((el) => el.toJSON())],
labels: this.labels.map((el) => el.toJSON()),
};
if (this.bugTracker) {
projectSpec.bug_tracker = this.bugTracker;
}
if (trainingProjectCopy) {
projectSpec.training_project = trainingProjectCopy;
if (this.trainingProject) {
projectSpec.training_project = this.trainingProject;
}
const project = await serverProxy.projects.create(projectSpec);

@ -7,6 +7,7 @@
const { ArgumentError } = require('./exceptions');
const { Label } = require('./labels');
const User = require('./user');
const { FieldUpdateTrigger } = require('./common');
/**
* Class representing a project
@ -36,6 +37,8 @@
dimension: undefined,
};
const updateTrigger = new FieldUpdateTrigger();
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property];
@ -82,6 +85,7 @@
throw new ArgumentError('Value must not be empty');
}
data.name = value;
updateTrigger.update('name');
},
},
@ -110,6 +114,7 @@
throw new ArgumentError('Value must be a user instance');
}
data.assignee = assignee;
updateTrigger.update('assignee');
},
},
/**
@ -134,6 +139,7 @@
get: () => data.bug_tracker,
set: (tracker) => {
data.bug_tracker = tracker;
updateTrigger.update('bugTracker');
},
},
/**
@ -195,6 +201,7 @@
});
data.labels = [...deletedLabels, ...labels];
updateTrigger.update('labels');
},
},
/**
@ -231,11 +238,15 @@
} else {
data.training_project = updatedProject;
}
updateTrigger.update('trainingProject');
},
},
_internalData: {
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 config = require('./config');
const DownloadWorker = require('./download.worker');
const Axios = require('axios');
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) {
return new Promise((resolve, reject) => {
if (typeof predicate !== 'function') {
@ -61,8 +71,8 @@
}
class WorkerWrappedAxios {
constructor() {
const worker = new DownloadWorker();
constructor(requestInterseptor) {
const worker = new DownloadWorker(requestInterseptor);
const requests = {};
let requestId = 0;
@ -123,11 +133,18 @@
class ServerProxy {
constructor() {
const Axios = require('axios');
Axios.defaults.withCredentials = true;
Axios.defaults.xsrfHeaderName = 'X-CSRFTOKEN';
Axios.defaults.xsrfCookieName = 'csrftoken';
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');
if (token) {
@ -151,12 +168,13 @@
async function share(directoryArg) {
const { backendAPI } = config;
const directory = encodeURIComponent(directoryArg);
const directory = encodeURI(directoryArg);
let response = null;
try {
response = await Axios.get(`${backendAPI}/server/share?directory=${directory}`, {
response = await Axios.get(`${backendAPI}/server/share`, {
proxy: config.proxy,
params: { directory },
});
} catch (errorData) {
throw generateError(errorData);
@ -242,7 +260,7 @@
.join('&')
.replace(/%20/g, '+');
Axios.defaults.headers.common.Authorization = '';
removeToken();
let authenticationResponse = null;
try {
authenticationResponse = await Axios.post(`${config.backendAPI}/auth/login`, authenticationData, {
@ -269,12 +287,10 @@
await Axios.post(`${config.backendAPI}/auth/logout`, {
proxy: config.proxy,
});
removeToken();
} catch (errorData) {
throw generateError(errorData);
}
store.remove('token');
Axios.defaults.headers.common.Authorization = '';
}
async function changePassword(oldPassword, newPassword1, newPassword2) {
@ -335,6 +351,7 @@
await module.exports.users.self();
} catch (serverError) {
if (serverError.code === 401) {
removeToken();
return false;
}
@ -362,12 +379,15 @@
let response = null;
try {
response = await Axios.get(
`${backendAPI}/projects?names_only=true&page=1&page_size=${limit}&search=${search}`,
{
proxy,
response = await Axios.get(`${backendAPI}/projects`, {
proxy,
params: {
names_only: true,
page: 1,
page_size: limit,
search,
},
);
});
} catch (errorData) {
throw generateError(errorData);
}
@ -376,12 +396,25 @@
return response.data.results;
}
async function getProjects(filter = '') {
async function getProjects(filter = {}) {
const { backendAPI, proxy } = config;
let response = null;
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,
});
} catch (errorData) {
@ -411,7 +444,9 @@
const { backendAPI } = config;
try {
await Axios.delete(`${backendAPI}/projects/${id}`);
await Axios.delete(`${backendAPI}/projects/${id}`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
@ -433,12 +468,25 @@
}
}
async function getTasks(filter = '') {
async function getTasks(filter = {}) {
const { backendAPI } = config;
let response = null;
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,
});
} catch (errorData) {
@ -452,8 +500,9 @@
async function saveTask(id, taskData) {
const { backendAPI } = config;
let response = null;
try {
await Axios.patch(`${backendAPI}/tasks/${id}`, JSON.stringify(taskData), {
response = await Axios.patch(`${backendAPI}/tasks/${id}`, JSON.stringify(taskData), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
@ -462,13 +511,16 @@
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function deleteTask(id) {
async function deleteTask(id, organizationID = null) {
const { backendAPI } = config;
try {
await Axios.delete(`${backendAPI}/tasks/${id}`, {
...(organizationID ? { org: organizationID } : {}),
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
@ -483,25 +535,27 @@
return async function (id, format, name, saveImages) {
const { backendAPI } = config;
const baseURL = `${backendAPI}/${instanceType}/${id}/${saveImages ? 'dataset' : 'annotations'}`;
let query = `format=${encodeURIComponent(format)}`;
const params = {
...enableOrganization(),
format,
};
if (name) {
const filename = name.replace(/\//g, '_');
query += `&filename=${encodeURIComponent(filename)}`;
params.filename = name.replace(/\//g, '_');
}
let url = `${baseURL}?${query}`;
return new Promise((resolve, reject) => {
async function request() {
Axios.get(`${url}`, {
Axios.get(baseURL, {
proxy: config.proxy,
params,
})
.then((response) => {
if (response.status === 202) {
setTimeout(request, 3000);
} else {
query = `${query}&action=download`;
url = `${baseURL}?${query}`;
resolve(url);
params.action = 'download';
resolve(`${baseURL}?${new URLSearchParams(params).toString()}`);
}
})
.catch((errorData) => {
@ -554,6 +608,9 @@
async function exportTask(id) {
const { backendAPI } = config;
const params = {
...enableOrganization(),
};
const url = `${backendAPI}/tasks/${id}/backup`;
return new Promise((resolve, reject) => {
@ -561,11 +618,13 @@
try {
const response = await Axios.get(url, {
proxy: config.proxy,
params,
});
if (response.status === 202) {
setTimeout(request, 3000);
} else {
resolve(`${url}?action=download`);
params.action = 'download';
resolve(`${url}?${new URLSearchParams(params).toString()}`);
}
} catch (errorData) {
reject(generateError(errorData));
@ -578,6 +637,8 @@
async function importTask(file) {
const { backendAPI } = config;
// keep current default params to 'freeze" them during this request
const params = enableOrganization();
let taskData = new FormData();
taskData.append('task_file', file);
@ -587,13 +648,15 @@
try {
const response = await Axios.post(`${backendAPI}/tasks/backup`, taskData, {
proxy: config.proxy,
params,
});
if (response.status === 202) {
taskData = new FormData();
taskData.append('rq_id', response.data.rq_id);
setTimeout(request, 3000);
} 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]);
}
} catch (errorData) {
@ -607,6 +670,8 @@
async function backupProject(id) {
const { backendAPI } = config;
// keep current default params to 'freeze" them during this request
const params = enableOrganization();
const url = `${backendAPI}/projects/${id}/backup`;
return new Promise((resolve, reject) => {
@ -614,11 +679,13 @@
try {
const response = await Axios.get(url, {
proxy: config.proxy,
params,
});
if (response.status === 202) {
setTimeout(request, 3000);
} else {
resolve(`${url}?action=download`);
params.action = 'download';
resolve(`${url}?${new URLSearchParams(params).toString()}`);
}
} catch (errorData) {
reject(generateError(errorData));
@ -631,6 +698,8 @@
async function restoreProject(file) {
const { backendAPI } = config;
// keep current default params to 'freeze" them during this request
const params = enableOrganization();
let data = new FormData();
data.append('project_file', file);
@ -640,13 +709,15 @@
try {
const response = await Axios.post(`${backendAPI}/projects/backup`, data, {
proxy: config.proxy,
params,
});
if (response.status === 202) {
data = new FormData();
data.append('rq_id', response.data.rq_id);
setTimeout(request, 3000);
} 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]);
}
} catch (errorData) {
@ -660,12 +731,14 @@
async function createTask(taskSpec, taskDataSpec, onUpdate) {
const { backendAPI, origin } = config;
// keep current default params to 'freeze" them during this request
const params = enableOrganization();
async function wait(id) {
return new Promise((resolve, reject) => {
async function checkStatus() {
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 (response.data.message !== '') {
onUpdate(response.data.message, response.data.progress || 0);
@ -732,6 +805,7 @@
try {
response = await Axios.post(`${backendAPI}/tasks`, JSON.stringify(taskSpec), {
proxy: config.proxy,
params,
headers: {
'Content-Type': 'application/json',
},
@ -760,6 +834,8 @@
},
onBeforeRequest(req) {
const xhr = req.getUnderlyingObject();
const { org } = params;
req.setHeader('X-Organization', org);
xhr.withCredentials = true;
},
onProgress(bytesUploaded) {
@ -796,6 +872,7 @@
onUpdate(`The data are being uploaded to the server
${((totalSentSize / totalSize) * 100).toFixed(2)}%`);
await Axios.post(`${backendAPI}/tasks/${taskId}/data`, taskData, {
...params,
proxy: config.proxy,
headers: { 'Upload-Multiple': true },
});
@ -810,6 +887,7 @@
try {
await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`,
taskData, {
...params,
proxy: config.proxy,
headers: { 'Upload-Start': true },
});
@ -821,12 +899,13 @@
}
await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`,
taskData, {
...params,
proxy: config.proxy,
headers: { 'Upload-Finish': true },
});
} catch (errorData) {
try {
await deleteTask(response.data.id);
await deleteTask(response.data.id, params.org || null);
} catch (_) {
// ignore
}
@ -836,11 +915,12 @@
try {
await wait(response.data.id);
} catch (createException) {
await deleteTask(response.data.id);
await deleteTask(response.data.id, params.org || null);
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];
}
@ -859,12 +939,12 @@
return response.data;
}
async function getJobReviews(jobID) {
async function getJobIssues(jobID) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/jobs/${jobID}/reviews`, {
response = await Axios.get(`${backendAPI}/jobs/${jobID}/issues`, {
proxy: config.proxy,
});
} catch (errorData) {
@ -874,12 +954,12 @@
return response.data;
}
async function createReview(data) {
async function createComment(data) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.post(`${backendAPI}/reviews`, JSON.stringify(data), {
response = await Axios.post(`${backendAPI}/comments`, JSON.stringify(data), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
@ -892,27 +972,12 @@
return response.data;
}
async function getJobIssues(jobID) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/jobs/${jobID}/issues`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function createComment(data) {
async function createIssue(data) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.post(`${backendAPI}/comments`, JSON.stringify(data), {
response = await Axios.post(`${backendAPI}/issues`, JSON.stringify(data), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
@ -956,8 +1021,9 @@
async function saveJob(id, jobData) {
const { backendAPI } = config;
let response = null;
try {
await Axios.patch(`${backendAPI}/jobs/${id}`, JSON.stringify(jobData), {
response = await Axios.patch(`${backendAPI}/jobs/${id}`, JSON.stringify(jobData), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
@ -966,15 +1032,20 @@
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function getUsers(filter = 'page_size=all') {
async function getUsers(filter = { page_size: 'all' }) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/users?${filter}`, {
response = await Axios.get(`${backendAPI}/users`, {
proxy: config.proxy,
params: {
...filter,
},
});
} catch (errorData) {
throw generateError(errorData);
@ -1003,7 +1074,10 @@
let response = null;
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,
responseType: 'blob',
});
@ -1015,18 +1089,20 @@
return response.data;
}
async function getImageContext(tid, frame) {
async function getImageContext(jid, frame) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(
`${backendAPI}/tasks/${tid}/data?quality=original&type=context_image&number=${frame}`,
{
proxy: config.proxy,
responseType: 'blob',
response = await Axios.get(`${backendAPI}/jobs/${jid}/data`, {
params: {
quality: 'original',
type: 'context_image',
number: frame,
},
);
proxy: config.proxy,
responseType: 'blob',
});
} catch (errorData) {
throw generateError(errorData);
}
@ -1034,18 +1110,23 @@
return response.data;
}
async function getData(tid, chunk) {
async function getData(tid, jid, chunk) {
const { backendAPI } = config;
const url = jid === null ? `tasks/${tid}/data` : `jobs/${jid}/data`;
let response = null;
try {
response = await workerAxios.get(
`${backendAPI}/tasks/${tid}/data?type=chunk&number=${chunk}&quality=compressed`,
{
proxy: config.proxy,
responseType: 'arraybuffer',
response = await workerAxios.get(`${backendAPI}/${url}`, {
params: {
...enableOrganization(),
quality: 'compressed',
type: 'chunk',
number: chunk,
},
);
proxy: config.proxy,
responseType: 'arraybuffer',
});
} catch (errorData) {
throw generateError({
message: '',
@ -1093,20 +1174,22 @@
// Session is 'task' or 'job'
async function updateAnnotations(session, id, data, action) {
const { backendAPI } = config;
const url = `${backendAPI}/${session}s/${id}/annotations`;
const params = {};
let requestFunc = null;
let url = null;
if (action.toUpperCase() === 'PUT') {
requestFunc = Axios.put.bind(Axios);
url = `${backendAPI}/${session}s/${id}/annotations`;
} else {
requestFunc = Axios.patch.bind(Axios);
url = `${backendAPI}/${session}s/${id}/annotations?action=${action}`;
params.action = action;
}
let response = null;
try {
response = await requestFunc(url, JSON.stringify(data), {
proxy: config.proxy,
params,
headers: {
'Content-Type': 'application/json',
},
@ -1121,7 +1204,10 @@
// Session is 'task' or 'job'
async function uploadAnnotations(session, id, file, format) {
const { backendAPI } = config;
const params = {
...enableOrganization(),
format,
};
let annotationData = new FormData();
annotationData.append('annotation_file', file);
@ -1129,9 +1215,10 @@
async function request() {
try {
const response = await Axios.put(
`${backendAPI}/${session}s/${id}/annotations?format=${format}`,
`${backendAPI}/${session}s/${id}/annotations`,
annotationData,
{
params,
proxy: config.proxy,
},
);
@ -1154,25 +1241,25 @@
async function dumpAnnotations(id, name, format) {
const { backendAPI } = config;
const baseURL = `${backendAPI}/tasks/${id}/annotations`;
let query = `format=${encodeURIComponent(format)}`;
const params = enableOrganization();
params.format = encodeURIComponent(format);
if (name) {
const filename = name.replace(/\//g, '_');
query += `&filename=${encodeURIComponent(filename)}`;
params.filename = encodeURIComponent(filename);
}
let url = `${baseURL}?${query}`;
return new Promise((resolve, reject) => {
async function request() {
Axios.get(`${url}`, {
Axios.get(baseURL, {
proxy: config.proxy,
params,
})
.then((response) => {
if (response.status === 202) {
setTimeout(request, 3000);
} else {
query = `${query}&action=download`;
url = `${baseURL}?${query}`;
resolve(url);
params.action = 'download';
resolve(`${baseURL}?${new URLSearchParams(params).toString()}`);
}
})
.catch((errorData) => {
@ -1291,7 +1378,11 @@
return new Promise((resolve, reject) => {
async function request() {
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;
} catch (errorData) {
throw generateError(errorData);
@ -1324,9 +1415,12 @@
async function request() {
try {
const response = await Axios.get(
`${backendAPI}/predict/frame?task=${taskId}&frame=${frame}`,
);
const response = await Axios.get(`${backendAPI}/predict/frame`, {
params: {
task: taskId,
frame,
},
});
return response.data;
} catch (errorData) {
throw generateError(errorData);
@ -1456,6 +1550,7 @@
try {
const url = `${backendAPI}/cloudstorages/${id}/preview`;
response = await workerAxios.get(url, {
params: enableOrganization(),
proxy: config.proxy,
responseType: 'arraybuffer',
});
@ -1492,10 +1587,164 @@
const { backendAPI } = config;
try {
await Axios.delete(`${backendAPI}/cloudstorages/${id}`);
await Axios.delete(`${backendAPI}/cloudstorages/${id}`, {
proxy: config.proxy,
});
} catch (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(
@ -1538,13 +1787,13 @@
tasks: {
value: Object.freeze({
getTasks,
saveTask,
createTask,
deleteTask,
get: getTasks,
save: saveTask,
create: createTask,
delete: deleteTask,
exportDataset: exportDataset('tasks'),
exportTask,
importTask,
export: exportTask,
import: importTask,
}),
writable: false,
},
@ -1553,11 +1802,6 @@
value: Object.freeze({
get: getJob,
save: saveJob,
issues: getJobIssues,
reviews: {
get: getJobReviews,
create: createReview,
},
}),
writable: false,
},
@ -1611,7 +1855,9 @@
issues: {
value: Object.freeze({
create: createIssue,
update: updateIssue,
get: getJobIssues,
delete: deleteIssue,
}),
writable: false,
@ -1644,6 +1890,21 @@
}),
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
(() => {
const store = require('store');
const PluginRegistry = require('./plugins');
const loggerStorage = require('./logger-storage');
const serverProxy = require('./server-proxy');
@ -11,12 +10,11 @@
getFrame, getRanges, getPreview, clear: clearFrames, getContextImage,
} = require('./frames');
const { ArgumentError, DataError } = require('./exceptions');
const { TaskStatus } = require('./enums');
const { JobStage, JobState } = require('./enums');
const { Label } = require('./labels');
const User = require('./user');
const Issue = require('./issue');
const Review = require('./review');
const { FieldUpdateTrigger } = require('./common');
const { FieldUpdateTrigger, checkObjectType } = require('./common');
function buildDuplicatedAPI(prototype) {
Object.defineProperties(prototype, {
@ -180,11 +178,10 @@
const result = await PluginRegistry.apiWrapper.call(this, prototype.frames.preview);
return result;
},
async contextImage(taskId, frameId) {
async contextImage(frameId) {
const result = await PluginRegistry.apiWrapper.call(
this,
prototype.frames.contextImage,
taskId,
frameId,
);
return result;
@ -709,18 +706,21 @@
const data = {
id: undefined,
assignee: null,
reviewer: null,
status: undefined,
stage: undefined,
state: undefined,
start_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({
assignee: false,
reviewer: false,
status: false,
});
const updateTrigger = new FieldUpdateTrigger();
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property)) {
@ -735,7 +735,19 @@
}
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(
this,
@ -764,42 +776,53 @@
if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError('Value must be a user instance');
}
updatedFields.assignee = true;
updateTrigger.update('assignee');
data.assignee = assignee;
},
},
/**
* Instance of a user who is responsible for review
* @name reviewer
* @type {module:API.cvat.classes.User}
* @name stage
* @type {module:API.cvat.enums.JobStage}
* @memberof module:API.cvat.classes.Job
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
reviewer: {
get: () => data.reviewer,
set: (reviewer) => {
if (reviewer !== null && !(reviewer instanceof User)) {
throw new ArgumentError('Value must be a user instance');
stage: {
get: () => data.stage,
set: (stage) => {
const type = JobStage;
let valueInEnum = false;
for (const value in type) {
if (type[value] === stage) {
valueInEnum = true;
break;
}
}
if (!valueInEnum) {
throw new ArgumentError(
'Value must be a value from the enumeration cvat.enums.JobStage',
);
}
updatedFields.reviewer = true;
data.reviewer = reviewer;
updateTrigger.update('stage');
data.stage = stage;
},
},
/**
* @name status
* @type {module:API.cvat.enums.TaskStatus}
* @name state
* @type {module:API.cvat.enums.JobState}
* @memberof module:API.cvat.classes.Job
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
status: {
get: () => data.status,
set: (status) => {
const type = TaskStatus;
state: {
get: () => data.state,
set: (state) => {
const type = JobState;
let valueInEnum = false;
for (const value in type) {
if (type[value] === status) {
if (type[value] === state) {
valueInEnum = true;
break;
}
@ -807,12 +830,12 @@
if (!valueInEnum) {
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;
data.status = status;
updateTrigger.update('state');
data.state = state;
},
},
/**
@ -836,17 +859,96 @@
get: () => data.stop_frame,
},
/**
* @name task
* @type {module:API.cvat.classes.Task}
* @name projectId
* @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
* @readonly
* @instance
*/
dataChunkSize: {
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
*/
task: {
get: () => data.task,
mode: {
get: () => data.mode,
},
/**
* @name bugTracker
* @type {string|null}
* @memberof module:API.cvat.classes.Job
* @instance
* @readonly
*/
bugTracker: {
get: () => data.bug_tracker,
},
__updatedFields: {
get: () => updatedFields,
_updateTrigger: {
get: () => updateTrigger,
},
}),
);
@ -870,6 +972,7 @@
export: Object.getPrototypeOf(this).annotations.export.bind(this),
statistics: Object.getPrototypeOf(this).annotations.statistics.bind(this),
hasUnsavedChanges: Object.getPrototypeOf(this).annotations.hasUnsavedChanges.bind(this),
exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this),
};
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
* @memberof module:API.cvat.classes.Job
* @readonly
@ -916,7 +1019,7 @@
* Method returns a list of issues for a job
* @method issues
* @memberof module:API.cvat.classes.Job
* @type {module:API.cvat.classes.Issue[]}
* @returns {module:API.cvat.classes.Issue[]}
* @readonly
* @instance
* @async
@ -929,44 +1032,36 @@
}
/**
* Method returns a list of reviews for a job
* @method reviews
* @type {module:API.cvat.classes.Review[]}
* Method adds a new issue to a job
* @method openIssue
* @memberof module:API.cvat.classes.Job
* @returns {module:API.cvat.classes.Issue}
* @param {module:API.cvat.classes.Issue} issue
* @param {string} message
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ArgumentError}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async reviews() {
const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.reviews);
async openIssue(issue, message) {
const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.openIssue, issue, message);
return result;
}
/**
* /**
* @typedef {Object} ReviewSummary
* @property {number} reviews Number of done reviews
* @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}
* Method removes all job related data from the client (annotations, history, etc.)
* @method close
* @returns {module:API.cvat.classes.Job}
* @memberof module:API.cvat.classes.Job
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @instance
* @throws {module:API.cvat.exceptions.PluginError}
*/
async reviewsSummary() {
const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.reviewsSummary);
async close() {
const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.close);
return result;
}
}
@ -993,7 +1088,7 @@
const data = {
id: undefined,
name: undefined,
project_id: undefined,
project_id: null,
status: undefined,
size: undefined,
mode: undefined,
@ -1020,14 +1115,7 @@
sorting_method: undefined,
};
const updatedFields = new FieldUpdateTrigger({
name: false,
assignee: false,
bug_tracker: false,
subset: false,
labels: false,
project_id: false,
});
const updateTrigger = new FieldUpdateTrigger();
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
@ -1046,6 +1134,13 @@
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)) {
for (const segment of initialData.segments) {
if (Array.isArray(segment.jobs)) {
@ -1054,25 +1149,28 @@
url: job.url,
id: job.id,
assignee: job.assignee,
reviewer: job.reviewer,
status: job.status,
state: job.state,
stage: job.stage,
start_frame: segment.start_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);
}
}
}
}
if (Array.isArray(initialData.labels)) {
for (const label of initialData.labels) {
const classInstance = new Label(label);
data.labels.push(classInstance);
}
}
Object.defineProperties(
this,
Object.freeze({
@ -1099,7 +1197,7 @@
if (!value.trim().length) {
throw new ArgumentError('Value must not be empty');
}
updatedFields.name = true;
updateTrigger.update('name');
data.name = value;
},
},
@ -1116,7 +1214,7 @@
throw new ArgumentError('Value must be a positive integer');
}
updatedFields.project_id = true;
updateTrigger.update('projectId');
data.project_id = projectId;
},
},
@ -1175,7 +1273,7 @@
if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError('Value must be a user instance');
}
updatedFields.assignee = true;
updateTrigger.update('assignee');
data.assignee = assignee;
},
},
@ -1215,7 +1313,7 @@
);
}
updatedFields.bug_tracker = true;
updateTrigger.update('bugTracker');
data.bug_tracker = tracker;
},
},
@ -1235,7 +1333,7 @@
);
}
updatedFields.subset = true;
updateTrigger.update('subset');
data.subset = subset;
},
},
@ -1364,7 +1462,7 @@
_label.deleted = true;
});
updatedFields.labels = true;
updateTrigger.update('labels');
data.labels = [...deletedLabels, ...labels];
},
},
@ -1531,14 +1629,14 @@
dataChunkType: {
get: () => data.data_compressed_chunk_type,
},
/**
* @name dimension
* @type {module:API.cvat.enums.DimensionType}
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
*/
dimension: {
/**
* @name enabled
* @type {string}
* @memberof module:API.cvat.enums.DimensionType
* @readonly
* @instance
*/
get: () => data.dimension,
},
/**
@ -1563,8 +1661,8 @@
_internalData: {
get: () => data,
},
__updatedFields: {
get: () => updatedFields,
_updateTrigger: {
get: () => updateTrigger,
},
}),
);
@ -1731,70 +1829,32 @@
Job.prototype.save.implementation = async function () {
if (this.id) {
const jobData = {};
for (const [field, isUpdated] of Object.entries(this.__updatedFields)) {
if (isUpdated) {
switch (field) {
case 'status':
jobData.status = this.status;
break;
case 'assignee':
jobData.assignee_id = this.assignee ? this.assignee.id : null;
break;
case 'reviewer':
jobData.reviewer_id = this.reviewer ? this.reviewer.id : null;
break;
default:
break;
}
}
const jobData = this._updateTrigger.getUpdated(this);
if (jobData.assignee) {
jobData.assignee = jobData.assignee.id;
}
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 () {
const result = await serverProxy.jobs.issues(this.id);
const result = await serverProxy.issues.get(this.id);
return result.map((issue) => new Issue(issue));
};
Job.prototype.reviews.implementation = async function () {
const result = await serverProxy.jobs.reviews.get(this.id);
const reviews = result.map((review) => new Review(review));
// try to get not finished review from the local storage
const data = store.get(`job-${this.id}-review`);
if (data) {
reviews.push(new Review(JSON.parse(data)));
}
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.openIssue.implementation = async function (issue, message) {
checkObjectType('issue', issue, null, Issue);
checkObjectType('message', message, 'string');
const result = await serverProxy.issues.create({
...issue.serialize(),
message,
});
return new Issue(result);
};
Job.prototype.frames.get.implementation = async function (frame, isPlaying, step) {
@ -1807,27 +1867,28 @@
}
const frameData = await getFrame(
this.task.id,
this.task.dataChunkSize,
this.task.dataChunkType,
this.task.mode,
this.taskId,
this.id,
this.dataChunkSize,
this.dataChunkType,
this.mode,
frame,
this.startFrame,
this.stopFrame,
isPlaying,
step,
this.task.dimension,
this.dimension,
);
return frameData;
};
Job.prototype.frames.ranges.implementation = async function () {
const rangesData = await getRanges(this.task.id);
const rangesData = await getRanges(this.taskId);
return rangesData;
};
Job.prototype.frames.preview.implementation = async function () {
const frameData = await getPreview(this.task.id);
const frameData = await getPreview(this.taskId);
return frameData;
};
@ -1950,7 +2011,7 @@
};
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;
};
@ -1980,20 +2041,54 @@
};
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;
};
Job.prototype.predictor.status.implementation = async function () {
const result = await this.task.predictor.status();
return result;
if (!Number.isInteger(this.projectId)) {
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) {
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;
};
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() {
clearFrames(this.id);
for (const job of this.jobs) {
@ -2008,40 +2103,22 @@
// TODO: Add ability to change an owner and an assignee
if (typeof this.id !== 'undefined') {
// If the task has been already created, we update it
const taskData = {};
for (const [field, isUpdated] of Object.entries(this.__updatedFields)) {
if (isUpdated) {
switch (field) {
case 'assignee':
taskData.assignee_id = this.assignee ? this.assignee.id : null;
break;
case 'name':
taskData.name = this.name;
break;
case 'bug_tracker':
taskData.bug_tracker = this.bugTracker;
break;
case '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;
}
}
const taskData = this._updateTrigger.getUpdated(this, {
bugTracker: 'bug_tracker',
projectId: 'project_id',
assignee: 'assignee_id',
});
if (taskData.assignee_id) {
taskData.assignee_id = taskData.assignee_id.id;
}
if (taskData.labels) {
taskData.labels = this._internalData.labels;
taskData.labels = taskData.labels.map((el) => el.toJSON());
}
await serverProxy.tasks.saveTask(this.id, taskData);
this.__updatedFields.reset();
return this;
const data = await serverProxy.tasks.save(this.id, taskData);
this._updateTrigger.reset();
return new Task(data);
}
const taskSpec = {
@ -2094,22 +2171,23 @@
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);
};
Task.prototype.delete.implementation = async function () {
const result = await serverProxy.tasks.deleteTask(this.id);
const result = await serverProxy.tasks.delete(this.id);
return result;
};
Task.prototype.export.implementation = async function () {
const result = await serverProxy.tasks.exportTask(this.id);
const result = await serverProxy.tasks.export(this.id);
return result;
};
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;
};
@ -2124,6 +2202,7 @@
const result = await getFrame(
this.id,
null,
this.dataChunkSize,
this.dataChunkType,
this.mode,
@ -2329,9 +2408,4 @@
const result = await serverProxy.predictor.predict(this.id, frame);
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
@ -174,10 +174,6 @@
email_verification_required: this.isVerified,
};
}
toJSON() {
return this.serialize();
}
}
module.exports = User;

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

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

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

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

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

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

@ -24,6 +24,7 @@ import {
Task,
Workspace,
} from 'reducers/interfaces';
import { updateJobAsync } from './tasks-actions';
interface AnnotationsParameters {
filters: string[];
@ -183,8 +184,6 @@ export enum AnnotationActionTypes {
SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED',
INTERACT_WITH_CANVAS = 'INTERACT_WITH_CANVAS',
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',
UPDATE_PREDICTOR_STATE = 'UPDATE_PREDICTOR_STATE',
GET_PREDICTIONS = 'GET_PREDICTIONS',
@ -343,7 +342,7 @@ export function uploadJobAnnotationsAsync(job: any, loader: any, file: File): Th
const state: CombinedState = getStore().getState();
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');
}
if (state.annotation.activities.loads[job.id]) {
@ -639,7 +638,7 @@ export function getPredictionsAsync(): ThunkAction {
annotations = annotations.map(
(data: any): any => new cvat.classes.ObjectState({
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,
objectType: ObjectType.SHAPE,
frame,
@ -950,7 +949,7 @@ export function closeJob(): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const { jobInstance } = receiveAnnotationsParameters();
if (jobInstance) {
await jobInstance.task.close();
await jobInstance.close();
}
dispatch({
@ -960,9 +959,9 @@ export function closeJob(): 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 {
const state: CombinedState = getStore().getState();
const state = getState();
const filters = initialFilters;
const {
settings: {
@ -986,20 +985,18 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
true,
);
// Check state if the task is already there
let task = state.tasks.current
// Check if the task was already downloaded to the state
let job: any | null = null;
const [task] = state.tasks.current
.filter((_task: Task) => _task.instance.id === tid)
.map((_task: Task) => _task.instance)[0];
// If there aren't the task, get it from the server
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) {
throw new Error(`Task ${tid} doesn't contain the job ${jid}`);
.map((_task: Task) => _task.instance);
if (task) {
[job] = task.jobs.filter((_job: any) => _job.id === jid);
if (!job) {
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);
@ -1018,7 +1015,6 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
}
const states = await job.annotations.get(frameNumber, showAllInterpolationTracks, filters);
const issues = await job.issues();
const reviews = await job.reviews();
const [minZ, maxZ] = computeZRange(states);
const colors = [...cvat.enums.colors];
@ -1031,7 +1027,6 @@ export function getJobAsync(tid: number, jid: number, initialFrame: number, init
openTime,
job,
issues,
reviews,
states,
frameNumber,
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;
dispatch(changeWorkspace(workspace));
}
const updatePredictorStatus = async (): Promise<void> => {
// get current job
const currentState: CombinedState = getStore().getState();
const currentState: CombinedState = getState();
const { openTime: currentOpenTime, instance: currentJob } = currentState.annotation.job;
if (currentJob === null || currentJob.id !== job.id || currentOpenTime !== openTime) {
// 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();
}
@ -1120,6 +1115,11 @@ export function saveAnnotationsAsync(sessionInstance: any, afterSave?: () => voi
afterSave();
}
if (sessionInstance instanceof cvat.classes.Job && sessionInstance.state === cvat.enums.JobState.NEW) {
sessionInstance.state = cvat.enums.JobState.IN_PROGRESS;
updateJobAsync(sessionInstance);
}
dispatch({
type: AnnotationActionTypes.SAVE_ANNOTATIONS_SUCCESS,
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 {
return {
type: AnnotationActionTypes.SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG,
@ -1645,7 +1627,7 @@ export function getContextImageAsync(): ThunkAction {
payload: {},
});
const contextImageData = await job.frames.contextImage(job.task.id, frameNumber);
const contextImageData = await job.frames.contextImage(frameNumber);
dispatch({
type: AnnotationActionTypes.GET_CONTEXT_IMAGE_SUCCESS,
payload: { contextImageData },

@ -106,7 +106,6 @@ export const loginAsync = (username: string, password: string): ThunkAction => a
try {
await cvat.server.login(username, password);
const users = await cvat.users.get({ self: true });
dispatch(authActions.loginSuccess(users[0]));
} catch (error) {
dispatch(authActions.loginFailed(error));
@ -117,6 +116,7 @@ export const logoutAsync = (): ThunkAction => async (dispatch) => {
dispatch(authActions.logout());
try {
await cvat.organizations.deactivate();
await cvat.server.logout();
dispatch(authActions.logoutSuccess());
} 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> => {
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 => {
dispatch(action);
};
listen(
{
taskID: taskInstance.id,
taskID: taskId,
requestID,
},
dispatchCallback,
);
} 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 getCore from 'cvat-core-wrapper';
import { updateTaskSuccess } from './tasks-actions';
const cvat = getCore();
export enum ReviewActionTypes {
INITIALIZE_REVIEW_SUCCESS = 'INITIALIZE_REVIEW_SUCCESS',
INITIALIZE_REVIEW_FAILED = 'INITIALIZE_REVIEW_FAILED',
CREATE_ISSUE = 'CREATE_ISSUE',
START_ISSUE = 'START_ISSUE',
FINISH_ISSUE_SUCCESS = 'FINISH_ISSUE_SUCCESS',
@ -35,10 +32,6 @@ export enum ReviewActionTypes {
}
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, {}),
startIssue: (position: number[]) => (
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 }),
reopenIssueSuccess: () => createAction(ReviewActionTypes.REOPEN_ISSUE_SUCCESS),
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),
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) => (
createAction(ReviewActionTypes.REMOVE_ISSUE_SUCCESS, { issueId, frame })
),
@ -72,59 +67,29 @@ export const 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) => {
const state = getState();
const {
auth: { user },
annotation: {
player: {
frame: { number: frameNumber },
},
job: {
instance: jobInstance,
},
},
review: { activeReview, newIssuePosition },
review: { newIssuePosition },
} = state;
try {
const issue = await activeReview.openIssue({
const issue = new cvat.classes.Issue({
job: jobInstance.id,
frame: frameNumber,
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) {
dispatch(reviewActions.finishIssueFailed(error));
}
@ -134,7 +99,7 @@ export const commentIssueAsync = (id: number, message: string): ThunkAction => a
const state = getState();
const {
auth: { user },
review: { frameIssues, activeReview },
review: { frameIssues },
} = state;
try {
@ -142,11 +107,9 @@ export const commentIssueAsync = (id: number, message: string): ThunkAction => a
const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id);
await issue.comment({
message,
author: user,
owner: user,
});
if (activeReview && activeReview.issues.includes(issue)) {
await activeReview.toLocalStorage();
}
dispatch(reviewActions.commentIssueSuccess());
} catch (error) {
dispatch(reviewActions.commentIssueFailed(error));
@ -157,17 +120,13 @@ export const resolveIssueAsync = (id: number): ThunkAction => async (dispatch, g
const state = getState();
const {
auth: { user },
review: { frameIssues, activeReview },
review: { frameIssues },
} = state;
try {
dispatch(reviewActions.resolveIssue(id));
const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id);
await issue.resolve(user);
if (activeReview && activeReview.issues.includes(issue)) {
await activeReview.toLocalStorage();
}
dispatch(reviewActions.resolveIssueSuccess());
} catch (error) {
dispatch(reviewActions.resolveIssueFailed(error));
@ -178,47 +137,23 @@ export const reopenIssueAsync = (id: number): ThunkAction => async (dispatch, ge
const state = getState();
const {
auth: { user },
review: { frameIssues, activeReview },
review: { frameIssues },
} = state;
try {
dispatch(reviewActions.reopenIssue(id));
const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id);
await issue.reopen(user);
if (activeReview && activeReview.issues.includes(issue)) {
await activeReview.toLocalStorage();
}
dispatch(reviewActions.reopenIssueSuccess());
} catch (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) => {
const state = getState();
const {
review: { frameIssues, activeReview },
review: { frameIssues },
annotation: {
player: {
frame: { number: frameNumber },
@ -229,10 +164,6 @@ export const deleteIssueAsync = (id: number): ThunkAction => async (dispatch, ge
try {
const [issue] = frameIssues.filter((_issue: any): boolean => _issue.id === id);
await issue.delete();
if (activeReview !== null) {
await activeReview.deleteIssue(id);
await activeReview.toLocalStorage();
}
dispatch(reviewActions.removeIssueSuccess(id, frameNumber));
} catch (error) {
dispatch(reviewActions.removeIssueFailed(error));

@ -28,6 +28,9 @@ export enum TasksActionTypes {
UPDATE_TASK = 'UPDATE_TASK',
UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS',
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',
EXPORT_TASK = 'EXPORT_TASK',
EXPORT_TASK_SUCCESS = 'EXPORT_TASK_SUCCESS',
@ -38,46 +41,42 @@ export enum TasksActionTypes {
SWITCH_MOVE_TASK_MODAL_VISIBLE = 'SWITCH_MOVE_TASK_MODAL_VISIBLE',
}
function getTasks(): AnyAction {
function getTasks(query: TasksQuery): AnyAction {
const action = {
type: TasksActionTypes.GET_TASKS,
payload: {},
payload: {
query,
},
};
return action;
}
export function getTasksSuccess(
array: any[], previews: string[], count: number, query: Partial<TasksQuery>,
): AnyAction {
export function getTasksSuccess(array: any[], previews: string[], count: number): AnyAction {
const action = {
type: TasksActionTypes.GET_TASKS_SUCCESS,
payload: {
previews,
array,
count,
query,
},
};
return action;
}
function getTasksFailed(error: any, query: Partial<TasksQuery>): AnyAction {
function getTasksFailed(error: any): AnyAction {
const action = {
type: TasksActionTypes.GET_TASKS_FAILED,
payload: {
error,
query,
},
payload: { error },
};
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> => {
dispatch(getTasks());
dispatch(getTasks(query));
// We need remove all keys with null values from query
const filteredQuery = { ...query };
@ -91,7 +90,7 @@ export function getTasksAsync(query: Partial<TasksQuery>): ThunkAction<Promise<v
try {
result = await cvat.tasks.get(filteredQuery);
} catch (error) {
dispatch(getTasksFailed(error, query));
dispatch(getTasksFailed(error));
return;
}
@ -100,7 +99,7 @@ export function getTasksAsync(query: Partial<TasksQuery>): ThunkAction<Promise<v
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;
}
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 {
const action = {
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> {
return async (dispatch: ActionCreator<Dispatch>, getState: () => CombinedState): Promise<void> => {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
dispatch(updateTask());
const currentUser = getState().auth.user;
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));
}
const task = await taskInstance.save();
dispatch(updateTaskSuccess(task, taskInstance.id));
} catch (error) {
// try abort all changes
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> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
dispatch(updateTask());
await jobInstance.save();
const [task] = await cvat.tasks.get({ id: jobInstance.task.id });
dispatch(updateTaskSuccess(task, jobInstance.task.id));
dispatch(updateJob());
const newJob = await jobInstance.save();
dispatch(updateJobSuccess(newJob));
} catch (error) {
// try abort all changes
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));
dispatch(updateJobFailed(jobInstance.id, error));
}
};
}

@ -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;
$header-height: $grid-unit-size * 7;
$header-height: $grid-unit-size * 6;
$layout-sm-grid-size: $grid-unit-size * 0.5;
$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 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 SubmitReviewModal from 'components/annotation-page/review/submit-review-modal';
import StandardWorkspaceComponent from 'components/annotation-page/standard-workspace/standard-workspace';
import StandardWorkspace3DComponent from 'components/annotation-page/standard3D-workspace/standard3D-workspace';
import TagAnnotationWorkspace from 'components/annotation-page/tag-annotation-workspace/tag-annotation-workspace';
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 { Workspace } from 'reducers/interfaces';
import { usePrevious } from 'utils/hooks';
@ -66,15 +64,15 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
}, [job, fetching]);
useEffect(() => {
if (prevFetching && !fetching && !prevJob && job && !job.task.labels.length) {
if (prevFetching && !fetching && !prevJob && job && !job.labels.length) {
notification.warning({
message: 'No labels',
description: (
<span>
{`${job.task.projectId ? 'Project' : 'Task'} ${
job.task.projectId || job.task.id
{`${job.projectId ? 'Project' : 'Task'} ${
job.projectId || job.taskId
} 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
</a>
{' the first one for editing annotation.'}
@ -132,9 +130,7 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
</Layout.Content>
)}
<FiltersModalComponent />
<StatisticsModalContainer />
<SubmitAnnotationsModal />
<SubmitReviewModal />
<StatisticsModalComponent />
</Layout>
);
}

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

@ -389,7 +389,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
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.frame = frame;
const objectState = new cvat.classes.ObjectState(state);

@ -237,7 +237,7 @@ const CanvasWrapperComponent = (props: Props): ReactElement => {
}
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.frame = frame;
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
import './styles.scss';
import React, { useEffect } from 'react';
import React from 'react';
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 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';
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 (
<Layout hasSider className='cvat-review-workspace'>
<ControlsSideBarContainer />

@ -89,7 +89,7 @@ export default function IssueDialog(props: Props): JSX.Element {
<Comment
avatar={null}
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>}
datetime={(
<CVATTooltip title={created.format('MMMM Do YYYY')}>

@ -84,8 +84,8 @@ export default function IssueAggregatorComponent(): JSX.Element | null {
const { geometry } = canvasInstance;
for (const issue of frameIssues) {
if (issuesHidden) break;
if (issuesResolvedHidden && !!issue.resolvedDate) continue;
const issueResolved = !!issue.resolver;
const issueResolved = issue.resolved;
if (issuesResolvedHidden && issueResolved) continue;
const offset = 15;
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;

@ -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,
} = props;
const is2D = jobInstance.task.dimension === DimensionType.DIM_2D;
const is2D = jobInstance.dimension === DimensionType.DIM_2D;
return (
<div className='cvat-draw-shape-popover-content'>

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

@ -340,7 +340,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
try {
// run server request
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
const approximated = await this.approximateResponsePoints(response);
@ -430,7 +430,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, 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;
if (!isDone || !shapesUpdated) {
@ -535,8 +535,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
<EnvironmentFilled
onClick={() => {
const filteredStates = trackedShapes.filter(
(trackedShape: TrackedShape) =>
trackedShape.clientID !== clientID,
(trackedShape: TrackedShape) => trackedShape.clientID !== clientID,
);
/* eslint no-param-reassign: ["error", { "props": false }] */
objectState.descriptions = [];
@ -690,8 +689,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
0,
);
// eslint-disable-next-line no-await-in-loop
const response = await core.lambda.call(jobInstance.task, tracker, {
task: jobInstance.task,
const response = await core.lambda.call(jobInstance.taskId, tracker, {
frame: frame - 1,
shapes: trackableObjects.shapes,
});
@ -736,8 +734,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
0,
);
// eslint-disable-next-line no-await-in-loop
const response = await core.lambda.call(jobInstance.task, tracker, {
task: jobInstance.task,
const response = await core.lambda.call(jobInstance.taskId, tracker, {
frame: frame - 1,
shapes: trackableObjects.shapes,
states: trackableObjects.states,
@ -1022,24 +1019,24 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
<DetectorRunner
withCleanup={false}
models={detectors}
task={jobInstance.task}
runInference={async (task: any, model: Model, body: object) => {
labels={jobInstance.labels}
dimension={jobInstance.dimension}
runInference={async (model: Model, body: object) => {
try {
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(
(data: any): any =>
new core.classes.ObjectState({
shapeType: data.type,
label: task.labels.filter((label: any): boolean => label.name === data.label)[0],
points: data.points,
objectType: ObjectType.SHAPE,
frame,
occluded: false,
source: 'auto',
attributes: {},
zOrder: curZOrder,
}),
(data: any): any => new core.classes.ObjectState({
shapeType: data.type,
label: jobInstance.labels.filter((label: any): boolean => label.name === data.label)[0],
points: data.points,
objectType: ObjectType.SHAPE,
frame,
occluded: false,
source: 'auto',
attributes: {},
zOrder: curZOrder,
}),
);
createAnnotations(jobInstance, frame, states);

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

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

@ -81,11 +81,7 @@ function ObjectsSideBar(props: StateToProps & DispatchToProps & OwnProps): JSX.E
collapseSidebar();
};
let is2D = true;
if (jobInstance) {
is2D = jobInstance.task.dimension === DimensionType.DIM_2D;
}
const is2D = jobInstance ? jobInstance.dimension === DimensionType.DIM_2D : true;
return (
<Layout.Sider
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 {
> .ant-modal-content > .ant-modal-body {
> div:nth-child(2) > div:nth-child(2) {
.ant-col {
.ant-modal-body {
> div.ant-row:nth-child(2) {
> .ant-col {
width: 100%;
> div:nth-child(2) {
margin-top: $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
import React from 'react';
import { withRouter, RouteComponentProps } from 'react-router';
import Menu from 'antd/lib/menu';
import Modal from 'antd/lib/modal';
import Text from 'antd/lib/typography/Text';
import {
InputNumber, Tooltip, Checkbox, Collapse,
} from 'antd';
import InputNumber from 'antd/lib/input-number';
import Checkbox from 'antd/lib/checkbox';
import Collapse from 'antd/lib/collapse';
// eslint-disable-next-line import/no-extraneous-dependencies
import { MenuInfo } from 'rc-menu/lib/interface';
import CVATTooltip from 'components/common/cvat-tooltip';
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 {
taskMode: string;
loaders: any[];
dumpers: any[];
loadActivity: string | null;
isReviewer: boolean;
jobInstance: any;
onClickMenu(params: MenuInfo): void;
onUploadAnnotations(format: string, file: File): void;
@ -36,19 +41,17 @@ export enum Actions {
EXPORT_TASK_DATASET = 'export_task_dataset',
REMOVE_ANNO = 'remove_anno',
OPEN_TASK = 'open_task',
REQUEST_REVIEW = 'request_review',
SUBMIT_REVIEW = 'submit_review',
FINISH_JOB = 'finish_job',
RENEW_JOB = 'renew_job',
}
export default function AnnotationMenuComponent(props: Props): JSX.Element {
function AnnotationMenuComponent(props: Props & RouteComponentProps): JSX.Element {
const {
loaders,
loadActivity,
isReviewer,
jobInstance,
stopFrame,
history,
onClickMenu,
onUploadAnnotations,
removeAnnotations,
@ -56,8 +59,10 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element {
saveAnnotations,
} = props;
const jobStatus = jobInstance.status;
const taskID = jobInstance.task.id;
const jobStage = jobInstance.stage;
const jobState = jobInstance.state;
const taskID = jobInstance.taskId;
const { JobState } = core.enums;
function onClickMenuWrapper(params: MenuInfo): void {
function checkUnsavedChanges(_params: MenuInfo): void {
@ -117,7 +122,7 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element {
max={stopFrame}
onChange={(value) => { removeUpTo = value; }}
/>
<Tooltip title='Applicable only for annotations in range'>
<CVATTooltip title='Applicable only for annotations in range'>
<br />
<br />
<Checkbox
@ -127,7 +132,7 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element {
>
Delete only keyframes for tracks
</Checkbox>
</Tooltip>
</CVATTooltip>
</Panel>
</Collapse>
</div>
@ -142,12 +147,21 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element {
},
okText: 'Delete',
});
} else if (params.key === Actions.REQUEST_REVIEW) {
checkUnsavedChanges(params);
} 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);
},
});
} else if (params.key === Actions.FINISH_JOB) {
Modal.confirm({
title: 'The job status is going to be switched',
content: 'Status will be changed to "completed". Would you like to continue?',
title: 'The job stage is going to be switched',
content: 'Stage will be changed to "acceptance". Would you like to continue?',
okText: 'Continue',
cancelText: 'Cancel',
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) {
Modal.confirm({
title: 'The job status is going to be switched',
content: 'Status will be changed to "annotations". Would you like to continue?',
title: 'Do you want to renew the job?',
content: 'Stage will be set to "in progress", state will be set to "annotation". Would you like to continue?',
okText: 'Continue',
cancelText: 'Cancel',
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 (
<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,
taskDimension: jobInstance.task.dimension,
taskDimension: jobInstance.dimension,
})}
<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.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
</a>
</Menu.Item>
{jobStatus === 'annotation' && is2d && <Menu.Item key={Actions.REQUEST_REVIEW}>Request a review</Menu.Item>}
{jobStatus === 'annotation' && <Menu.Item key={Actions.FINISH_JOB}>Finish the job</Menu.Item>}
{jobStatus === 'validation' && isReviewer && (
<Menu.Item key={Actions.SUBMIT_REVIEW}>Submit the review</Menu.Item>
)}
{jobStatus === 'completed' && <Menu.Item key={Actions.RENEW_JOB}>Renew the job</Menu.Item>}
{jobStage !== JobStage.ACCEPTANCE ? (
<Menu.SubMenu popupClassName='cvat-annotation-menu-job-state-submenu' key='job-state-submenu' title='Change job state'>
<Menu.Item key={`state:${JobState.NEW}`}>
<Text className={computeClassName(JobState.NEW)}>{JobState.NEW}</Text>
</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>
);
}
export default withRouter(AnnotationMenuComponent);

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

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

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

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

@ -75,3 +75,9 @@
padding-left: $grid-unit-size * 0.5;
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'
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>
</Form.Item>

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

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

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

@ -17,6 +17,9 @@ import Icon, {
QuestionCircleOutlined,
CaretDownOutlined,
ControlOutlined,
UserOutlined,
TeamOutlined,
PlusOutlined,
} from '@ant-design/icons';
import Layout from 'antd/lib/layout';
import Button from 'antd/lib/button';
@ -28,11 +31,13 @@ import Text from 'antd/lib/typography/Text';
import getCore from 'cvat-core-wrapper';
import consts from 'consts';
import { CVATLogo, AccountIcon } from 'icons';
import { CVATLogo } from 'icons';
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 { logoutAsync, authActions } from 'actions/auth-actions';
import { CombinedState } from 'reducers/interfaces';
import { Select } from 'antd';
import SettingsModal from './settings-modal/settings-modal';
const core = getCore();
@ -67,6 +72,9 @@ interface StateToProps {
isAnalyticsPluginActive: boolean;
isModelsPluginActive: boolean;
isGitPluginActive: boolean;
organizationsFetching: boolean;
organizationsList: any[];
currentOrganization: any | null;
}
interface DispatchToProps {
@ -88,6 +96,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
about: { server, packageVersion },
shortcuts: { normalizedKeyMap },
settings: { showDialog: settingsDialogShown },
organizations: { fetching: organizationsFetching, current: currentOrganization, list: organizationsList },
} = state;
return {
@ -118,6 +127,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
isAnalyticsPluginActive: list.ANALYTICS,
isModelsPluginActive: list.MODELS,
isGitPluginActive: list.GIT_INTEGRATION,
organizationsFetching,
currentOrganization,
organizationsList,
};
}
@ -145,10 +157,13 @@ function HeaderContainer(props: Props): JSX.Element {
renderChangePasswordItem,
isAnalyticsPluginActive,
isModelsPluginActive,
organizationsFetching,
currentOrganization,
organizationsList,
} = props;
const {
CHANGELOG_URL, LICENSE_URL, GITTER_URL, FORUM_URL, GITHUB_URL,
CHANGELOG_URL, LICENSE_URL, GITTER_URL, FORUM_URL, GITHUB_URL, GUIDE_URL,
} = consts;
const history = useHistory();
@ -208,10 +223,32 @@ function HeaderContainer(props: Props): JSX.Element {
});
}
const menu = (
<Menu className='cvat-header-menu' mode='vertical'>
const resetOrganization = (): void => {
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 && (
<Menu.Item
icon={<ControlOutlined />}
key='admin_page'
onClick={(): void => {
// false positive
@ -219,37 +256,114 @@ function HeaderContainer(props: Props): JSX.Element {
window.open(`${tool.server.host}/admin`, '_blank');
}}
>
<ControlOutlined />
Admin page
</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
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'
title={`Press ${switchSettingsShortcut} to switch`}
onClick={() => switchSettingsDialog(true)}
>
<SettingOutlined />
Settings
</Menu.Item>
<Menu.Item key='about' onClick={showAboutModal}>
<InfoCircleOutlined />
<Menu.Item icon={<InfoCircleOutlined />} key='about' onClick={() => showAboutModal()}>
About
</Menu.Item>
{renderChangePasswordItem && (
<Menu.Item
key='change_password'
icon={changePasswordFetching ? <LoadingOutlined /> : <EditOutlined />}
className='cvat-header-menu-change-password'
onClick={(): void => switchChangePasswordDialog(true)}
disabled={changePasswordFetching}
>
{changePasswordFetching ? <LoadingOutlined /> : <EditOutlined />}
Change password
</Menu.Item>
)}
<Menu.Item key='logout' onClick={onLogout} disabled={logoutFetching}>
{logoutFetching ? <LoadingOutlined /> : <LogoutOutlined />}
<Menu.Item
key='logout'
icon={logoutFetching ? <LoadingOutlined /> : <LogoutOutlined />}
onClick={onLogout}
disabled={logoutFetching}
>
Logout
</Menu.Item>
</Menu>
@ -278,7 +392,7 @@ function HeaderContainer(props: Props): JSX.Element {
href='/tasks?page=1'
onClick={(event: React.MouseEvent): void => {
event.preventDefault();
history.push('/tasks?page=1');
history.push('/tasks');
}}
>
Tasks
@ -290,7 +404,7 @@ function HeaderContainer(props: Props): JSX.Element {
href='/cloudstorages?page=1'
onClick={(event: React.MouseEvent): void => {
event.preventDefault();
history.push('/cloudstorages?page=1');
history.push('/cloudstorages');
}}
>
Cloud Storages
@ -326,39 +440,52 @@ function HeaderContainer(props: Props): JSX.Element {
)}
</div>
<div className='cvat-right-header'>
<Button
className='cvat-header-button'
type='link'
href={GITHUB_URL}
onClick={(event: React.MouseEvent): void => {
event.preventDefault();
window.open(GITHUB_URL, '_blank');
}}
>
<GithubOutlined />
<Text className='cvat-text-color'>GitHub</Text>
</Button>
<Button
className='cvat-header-button'
type='link'
href='https://openvinotoolkit.github.io/cvat/docs'
onClick={(event: React.MouseEvent): void => {
event.preventDefault();
// false positive
// eslint-disable-next-line
window.open('https://openvinotoolkit.github.io/cvat/docs');
}}
>
<QuestionCircleOutlined />
Help
</Button>
<Dropdown overlay={menu} className='cvat-header-menu-dropdown'>
<CVATTooltip overlay='Click to open repository'>
<Button
icon={<GithubOutlined />}
size='large'
className='cvat-header-button'
type='link'
href={GITHUB_URL}
onClick={(event: React.MouseEvent): void => {
event.preventDefault();
// false alarm
// eslint-disable-next-line security/detect-non-literal-fs-filename
window.open(GITHUB_URL, '_blank');
}}
/>
</CVATTooltip>
<CVATTooltip overlay='Click to open guide'>
<Button
icon={<QuestionCircleOutlined />}
size='large'
className='cvat-header-button'
type='link'
href={GUIDE_URL}
onClick={(event: React.MouseEvent): void => {
event.preventDefault();
// false alarm
// eslint-disable-next-line security/detect-non-literal-fs-filename
window.open(GUIDE_URL, '_blank');
}}
/>
</CVATTooltip>
<Dropdown placement='bottomRight' overlay={userMenu} className='cvat-header-menu-user-dropdown'>
<span>
<Icon className='cvat-header-account-icon' component={AccountIcon} />
<Text strong>
{user.username.length > 14 ? `${user.username.slice(0, 10)} ...` : user.username}
</Text>
<CaretDownOutlined className='cvat-header-menu-icon' />
<UserOutlined className='cvat-header-dropdown-icon' />
<Row>
<Col span={24}>
<Text strong>
{user.username.length > 14 ? `${user.username.slice(0, 10)} ...` : user.username}
</Text>
</Col>
{ currentOrganization ? (
<Col span={24}>
<Text>{currentOrganization.slug}</Text>
</Col>
) : null }
</Row>
<CaretDownOutlined className='cvat-header-dropdown-icon' />
</span>
</Dropdown>
</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-player-settings {
.cvat-player-settings,
.cvat-organizations-settings {
width: 100%;
height: max-content;
background: $background-color-1;
@ -96,3 +97,14 @@
.cvat-settings-modal .ant-modal-body {
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
@ -25,6 +25,15 @@
display: flex;
justify-content: flex-end;
align-items: center;
> a.ant-btn {
height: 24px;
span[role='img'] {
font-size: 24px;
line-height: 24px;
}
}
}
.anticon.cvat-logo-icon {
@ -35,32 +44,48 @@
.ant-btn.cvat-header-button {
color: $text-color;
padding: 0 10px;
margin-right: 10px;
padding: 0 $grid-unit-size;
margin-right: $grid-unit-size;
}
.ant-dropdown-trigger.cvat-header-menu-dropdown {
.cvat-header-menu-user-dropdown {
display: flex;
align-items: center;
border-left: 1px solid $border-color-1;
padding: 0 20px;
}
.anticon.cvat-header-account-icon {
> svg {
transform: scale(0.4);
.anticon.cvat-header-dropdown-icon {
&.anticon-caret-down {
font-size: 12px;
}
font-size: 20px;
padding: $grid-unit-size;
}
}
.anticon.cvat-header-menu-icon {
margin-left: 16px;
margin-right: 0;
> div:nth-child(2) {
> div:nth-child(2) { /* org slug */
font-size: 10px;
}
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 {
> li {
span[role='img'] {
margin-right: $grid-unit-size;
}
.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 {
withCleanup: boolean;
models: Model[];
task: any;
runInference(task: any, model: Model, body: object): void;
labels: any[];
dimension: DimensionType;
runInference(model: Model, body: object): void;
}
function DetectorRunner(props: Props): JSX.Element {
const {
task, models, withCleanup, runInference,
models, withCleanup, labels, dimension, runInference,
} = props;
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));
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) {
notification.warning({
@ -90,7 +91,7 @@ function DetectorRunner(props: Props): JSX.Element {
function renderSelector(
value: string,
tooltip: string,
labels: string[],
labelsToRender: string[],
onChange: (label: string) => void,
): JSX.Element {
return (
@ -111,7 +112,7 @@ function DetectorRunner(props: Props): JSX.Element {
return false;
}}
>
{labels.map(
{labelsToRender.map(
(label: string): JSX.Element => (
<Select.Option value={label} key={label}>
{label}
@ -129,12 +130,12 @@ function DetectorRunner(props: Props): JSX.Element {
<Col span={4}>Model:</Col>
<Col span={20}>
<Select
placeholder={task.dimension === DimensionType.DIM_2D ? 'Select a model' : 'No models available'}
disabled={task.dimension !== DimensionType.DIM_2D}
placeholder={dimension === DimensionType.DIM_2D ? 'Select a model' : 'No models available'}
disabled={dimension !== DimensionType.DIM_2D}
style={{ width: '100%' }}
onChange={(_modelID: string): void => {
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)) {
acc[label.name] = label.name;
}
@ -159,7 +160,7 @@ function DetectorRunner(props: Props): JSX.Element {
{isDetector &&
!!Object.keys(mapping).length &&
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;
return (
<Row key={modelLabel} justify='start' align='middle'>
@ -188,12 +189,14 @@ function DetectorRunner(props: Props): JSX.Element {
<>
<Row justify='start' align='middle'>
<Col span={10}>
{renderSelector(match.model || '', 'Model labels', modelLabels, (modelLabel: string) =>
updateMatch(modelLabel, null))}
{renderSelector(
match.model || '', 'Model labels', modelLabels, (modelLabel: string) => updateMatch(modelLabel, null),
)}
</Col>
<Col span={10} offset={1}>
{renderSelector(match.task || '', 'Task labels', taskLabels, (taskLabel: string) =>
updateMatch(null, taskLabel))}
{renderSelector(
match.task || '', 'Task labels', taskLabels, (taskLabel: string) => updateMatch(null, taskLabel),
)}
</Col>
<Col span={1} offset={1}>
<CVATTooltip title='Specify a label mapping between model labels and task labels'>
@ -262,16 +265,11 @@ function DetectorRunner(props: Props): JSX.Element {
disabled={!buttonEnabled}
type='primary'
onClick={() => {
runInference(
task,
model,
model.type === 'detector' ?
{ mapping, cleanup } :
{
threshold,
max_distance: distance,
},
);
runInference(model, model.type === 'detector' ?
{ mapping, cleanup } : {
threshold,
max_distance: distance,
});
}}
>
Annotate
@ -282,14 +280,4 @@ function DetectorRunner(props: Props): JSX.Element {
);
}
export default React.memo(
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,
),
);
export default React.memo(DetectorRunner);

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -29,8 +29,8 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { detectors, reid } = models;
return {
visible: models.visibleRunWindows,
task: models.activeRunTask,
visible: models.modelRunnerIsVisible,
task: models.modelRunnerTask,
reid,
detectors,
};
@ -38,8 +38,8 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: ThunkDispatch): DispatchToProps {
return {
runInference(task: any, model: Model, body: object) {
dispatch(startInferenceAsync(task, model, body));
runInference(taskID: number, model: Model, body: object) {
dispatch(startInferenceAsync(taskID, model, body));
},
closeDialog() {
dispatch(modelsActions.closeRunModelDialog());
@ -63,15 +63,18 @@ function ModelRunnerDialog(props: StateToProps & DispatchToProps): JSX.Element {
maskClosable
title='Automatic annotation'
>
<DetectorRunner
withCleanup
models={models}
task={task}
runInference={(...args) => {
closeDialog();
runInference(...args);
}}
/>
{ task ? (
<DetectorRunner
withCleanup
models={models}
labels={task.labels}
dimension={task.dimension}
runInference={(...args) => {
closeDialog();
runInference(task.id, ...args);
}}
/>
) : null }
</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,16 +45,15 @@ function ShortcutsDialog(props: StateToProps & DispatchToProps): JSX.Element | n
const { visible, switchShortcutsDialog, jobInstance } = props;
const keyMap = getApplicationKeyMap();
const splitToRows = (data: string[]): JSX.Element[] =>
data.map(
(item: string, id: number): JSX.Element => (
// eslint-disable-next-line react/no-array-index-key
<span key={id}>
{item}
<br />
</span>
),
);
const splitToRows = (data: string[]): JSX.Element[] => data.map(
(item: string, id: number): JSX.Element => (
// eslint-disable-next-line react/no-array-index-key
<span key={id}>
{item}
<br />
</span>
),
);
const columns = [
{
@ -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)
.filter((key: string) => !dimensionType || keyMap[key].applicable.includes(dimensionType))
.map((key: string, id: number) => ({

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

@ -117,13 +117,9 @@
}
}
.cvat-job-item-status {
.cvat-job-annotation-color,
.cvat-job-validation-color,
.cvat-job-completed-color {
span {
margin-left: $grid-unit-size;
}
.cvat-job-item-stage {
.ant-select {
margin-right: $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 {
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' />;
}
@ -71,6 +71,7 @@ class TaskPageComponent extends React.PureComponent<Props> {
return (
<>
{ updating ? <Spin size='large' className='cvat-spinner' /> : null }
<Row
style={{ display: updating ? 'none' : undefined }}
justify='center'
@ -85,7 +86,6 @@ class TaskPageComponent extends React.PureComponent<Props> {
</Row>
<ModelRunnerModal />
<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 => {
setSearchPhrase(users.filter((user) => user.id === +_value)[0].username);
const user = _value ? users.filter((_user) => _user.id === +_value)[0] : null;
if ((user?.id || null) !== (value?.id || null)) {
onSelect(user);
@ -101,7 +100,9 @@ export default function UserSelector(props: Props): JSX.Element {
if (!users.filter((user) => user.id === value.id).length) {
core.users.get({ id: value.id }).then((result: User[]) => {
const [user] = result;
setUsers([...users, user]);
if (user) {
setUsers([...users, user]);
}
});
}

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

@ -72,7 +72,7 @@ class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteCompone
const { taskInstance, activeInference, cancelAutoAnnotation } = this.props;
// Count number of jobs and performed jobs
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
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 FORUM_URL = 'https://software.intel.com/en-us/forums/intel-distribution-of-openvino-toolkit';
const GITHUB_URL = 'https://github.com/openvinotoolkit/cvat';
const GITHUB_IMAGE_URL =
'https://github.com/openvinotoolkit/cvat/raw/develop/site/content/en/images/cvat.jpg';
const GITHUB_IMAGE_URL = '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 =
'https://openvinotoolkit.github.io/cvat/docs/administration/basics/installation/#share-path';
const NUCLIO_GUIDE =
@ -94,6 +94,7 @@ export default {
FORUM_URL,
GITHUB_URL,
GITHUB_IMAGE_URL,
GUIDE_URL,
SHARE_MOUNT_GUIDE_URL,
CANVAS_BACKGROUND_COLORS,
NEW_LABEL_COLOR,

@ -8,33 +8,31 @@ import { connect } from 'react-redux';
// eslint-disable-next-line import/no-extraneous-dependencies
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 { updateJobAsync } from 'actions/tasks-actions';
import {
uploadJobAnnotationsAsync,
saveAnnotationsAsync,
switchRequestReviewDialog as switchRequestReviewDialogAction,
switchSubmitReviewDialog as switchSubmitReviewDialogAction,
setForceExitAnnotationFlag as setForceExitAnnotationFlagAction,
removeAnnotationsAsync as removeAnnotationsAsyncAction,
} from 'actions/annotation-actions';
import { exportActions } from 'actions/export-actions';
import getCore from 'cvat-core-wrapper';
const core = getCore();
interface StateToProps {
annotationFormats: any;
jobInstance: any;
stopFrame: number;
loadActivity: string | null;
user: any;
}
interface DispatchToProps {
loadAnnotations(job: any, loader: any, file: File): void;
showExportModal(task: any): void;
removeAnnotations(startnumber:number, endnumber:number, delTrackKeyframesOnly:boolean): void;
switchRequestReviewDialog(visible: boolean): void;
switchSubmitReviewDialog(visible: boolean): void;
showExportModal(jobInstance: any): void;
removeAnnotations(startnumber: number, endnumber: number, delTrackKeyframesOnly: boolean): void;
setForceExitAnnotationFlag(forceExit: boolean): void;
saveAnnotations(jobInstance: any, afterSave?: () => void): void;
updateJob(jobInstance: any): void;
@ -53,10 +51,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
tasks: {
activities: { loads },
},
auth: { user },
} = state;
const taskID = jobInstance.task.id;
const taskID = jobInstance.taskId;
const jobID = jobInstance.id;
return {
@ -64,7 +61,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
jobInstance,
stopFrame,
annotationFormats,
user,
};
}
@ -73,18 +69,12 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
loadAnnotations(job: any, loader: any, file: File): void {
dispatch(uploadJobAnnotationsAsync(job, loader, file));
},
showExportModal(task: any): void {
dispatch(exportActions.openExportModal(task));
showExportModal(jobInstance: any): void {
dispatch(exportActions.openExportModal(jobInstance));
},
removeAnnotations(startnumber: number, endnumber: number, delTrackKeyframesOnly:boolean) {
dispatch(removeAnnotationsAsyncAction(startnumber, endnumber, delTrackKeyframesOnly));
},
switchRequestReviewDialog(visible: boolean): void {
dispatch(switchRequestReviewDialogAction(visible));
},
switchSubmitReviewDialog(visible: boolean): void {
dispatch(switchSubmitReviewDialogAction(visible));
},
setForceExitAnnotationFlag(forceExit: boolean): void {
dispatch(setForceExitAnnotationFlagAction(forceExit));
},
@ -103,15 +93,12 @@ function AnnotationMenuContainer(props: Props): JSX.Element {
const {
jobInstance,
stopFrame,
user,
annotationFormats: { loaders, dumpers },
history,
loadActivity,
loadAnnotations,
showExportModal,
removeAnnotations,
switchRequestReviewDialog,
switchSubmitReviewDialog,
setForceExitAnnotationFlag,
saveAnnotations,
updateJob,
@ -127,29 +114,29 @@ function AnnotationMenuContainer(props: Props): JSX.Element {
const onClickMenu = (params: MenuInfo): void => {
const [action] = params.keyPath;
if (action === Actions.EXPORT_TASK_DATASET) {
showExportModal(jobInstance.task);
} else if (action === Actions.REQUEST_REVIEW) {
switchRequestReviewDialog(true);
} else if (action === Actions.SUBMIT_REVIEW) {
switchSubmitReviewDialog(true);
showExportModal(jobInstance);
} else if (action === Actions.RENEW_JOB) {
jobInstance.status = TaskStatus.ANNOTATION;
jobInstance.state = core.enums.JobState.NEW;
jobInstance.stage = JobStage.ANNOTATION;
updateJob(jobInstance);
history.push(`/tasks/${jobInstance.task.id}`);
window.location.reload();
} else if (action === Actions.FINISH_JOB) {
jobInstance.status = TaskStatus.COMPLETED;
jobInstance.stage = JobStage.ACCEPTANCE;
jobInstance.state = core.enums.JobState.COMPLETED;
updateJob(jobInstance);
history.push(`/tasks/${jobInstance.task.id}`);
history.push(`/tasks/${jobInstance.taskId}`);
} 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 (
<AnnotationMenuComponent
taskMode={jobInstance.task.mode}
taskMode={jobInstance.mode}
loaders={loaders}
dumpers={dumpers}
loadActivity={loadActivity}
@ -159,7 +146,6 @@ function AnnotationMenuContainer(props: Props): JSX.Element {
setForceExitAnnotationFlag={setForceExitAnnotationFlag}
saveAnnotations={saveAnnotations}
jobInstance={jobInstance}
isReviewer={isReviewer}
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;
this.unblock = history.block((location: any) => {
const { forceExit } = self.props;
const { task, id: jobID } = jobInstance;
const { id: taskID } = task;
const { id: jobID, taskId: taskID } = jobInstance;
if (
jobInstance.annotations.hasUnsavedChanges() &&
@ -506,7 +505,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
if (stillPlaying) {
if (isAbleToChangeFrame()) {
onChangeFrame(frameNumber + 1 + framesSkipped, stillPlaying, framesSkipped + 1);
} else if (jobInstance.task.dimension === DimensionType.DIM_2D) {
} else if (jobInstance.dimension === DimensionType.DIM_2D) {
onSwitchPlay(false);
} else {
setTimeout(() => this.play(), frameDelay);

@ -5,7 +5,6 @@
import React from 'react';
import SVGCVATLogo from './assets/cvat-logo.svg';
import SVGAccountIcon from './assets/account-icon.svg';
import SVGEmptyTasksIcon from './assets/empty-tasks-icon.svg';
import SVGMenuIcon from './assets/menu-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';
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 MenuIcon = React.memo((): JSX.Element => <SVGMenuIcon />);
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 { KeyMap } from 'utils/mousetrap-react';
import createRootReducer from 'reducers/root-reducer';
import { getOrganizationsAsync } from 'actions/organization-actions';
import { resetErrors, resetMessages } from './actions/notification-actions';
import { CombinedState, NotificationsState } from './reducers/interfaces';
@ -34,6 +35,8 @@ interface StateToProps {
modelsFetching: boolean;
userInitialized: boolean;
userFetching: boolean;
organizationsFetching: boolean;
organizationsInitialized: boolean;
aboutInitialized: boolean;
aboutFetching: boolean;
formatsInitialized: boolean;
@ -62,6 +65,7 @@ interface DispatchToProps {
loadUserAgreements: () => void;
switchSettingsDialog: () => void;
loadAuthActions: () => void;
loadOrganizations: () => void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -72,10 +76,13 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { shortcuts } = state;
const { userAgreements } = state;
const { models } = state;
const { organizations } = state;
return {
userInitialized: auth.initialized,
userFetching: auth.fetching,
organizationsFetching: organizations.fetching,
organizationsInitialized: organizations.initialized,
pluginsInitialized: plugins.initialized,
pluginsFetching: plugins.fetching,
modelsInitialized: models.initialized,
@ -110,6 +117,7 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
switchShortcutsDialog: (): void => dispatch(shortcutsActions.switchShortcutsDialog()),
switchSettingsDialog: (): void => dispatch(switchSettingsDialog()),
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 { Canvas, CanvasMode } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import {
ActiveControl,
AnnotationState,
ContextMenuType,
DimensionType,
JobStage,
ObjectType,
ShapeType,
TaskStatus,
Workspace,
} from './interfaces';
@ -111,8 +112,6 @@ const defaultState: AnnotationState = {
sidebarCollapsed: false,
appearanceCollapsed: false,
filtersPanelVisible: false,
requestReviewDialogVisible: false,
submitReviewDialogVisible: false,
predictor: {
enabled: false,
error: null,
@ -157,11 +156,11 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
maxZ,
} = action.payload;
const isReview = job.status === TaskStatus.REVIEW;
const isReview = job.stage === JobStage.REVIEW;
let workspaceSelected = Workspace.STANDARD;
let activeShapeType = ShapeType.RECTANGLE;
if (job.task.dimension === DimensionType.DIM_3D) {
if (job.dimension === DimensionType.DIM_3D) {
workspaceSelected = Workspace.STANDARD3D;
activeShapeType = ShapeType.CUBOID;
}
@ -177,14 +176,12 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
openTime,
fetching: false,
instance: job,
labels: job.task.labels,
attributes: job.task.labels.reduce((acc: Record<number, any[]>, label: any): Record<
number,
any[]
> => {
acc[label.id] = label.attributes;
return acc;
}, {}),
labels: job.labels,
attributes: job.labels
.reduce((acc: Record<number, any[]>, label: any): Record<number, any[]> => {
acc[label.id] = label.attributes;
return acc;
}, {}),
},
annotations: {
...state.annotations,
@ -209,13 +206,13 @@ export default (state = defaultState, action: AnyAction): AnnotationState => {
},
drawing: {
...state.drawing,
activeLabelID: job.task.labels.length ? job.task.labels[0].id : null,
activeObjectType: job.task.mode === 'interpolation' ? ObjectType.TRACK : ObjectType.SHAPE,
activeLabelID: job.labels.length ? job.labels[0].id : null,
activeObjectType: job.mode === 'interpolation' ? ObjectType.TRACK : ObjectType.SHAPE,
activeShapeType,
},
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,
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: {
const { forceExit } = action.payload;
return {

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

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

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