diff --git a/cvat-core/package.json b/cvat-core/package.json index a6f3ffc5a9ce..32680da3e4cc 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -25,6 +25,7 @@ "ts-jest": "26" }, "dependencies": { + "@types/lodash": "^4.14.191", "axios": "^0.27.2", "browser-or-node": "^2.0.0", "cvat-data": "link:./../cvat-data", @@ -34,6 +35,7 @@ "jest-config": "^29.0.3", "js-cookie": "^3.0.1", "json-logic-js": "^2.0.1", + "lodash": "^4.17.21", "platform": "^1.3.5", "quickhull": "^1.0.3", "store": "^2.0.12", diff --git a/cvat-core/src/annotation-formats.ts b/cvat-core/src/annotation-formats.ts index d976d63922bb..584d1f89bca8 100644 --- a/cvat-core/src/annotation-formats.ts +++ b/cvat-core/src/annotation-formats.ts @@ -5,9 +5,9 @@ import { DimensionType } from 'enums'; import { - AnnotationExporterResponseBody, - AnnotationFormatsResponseBody, - AnnotationImporterResponseBody, + SerializedAnnotationExporter, + SerializedAnnotationFormats, + SerializedAnnotationImporter, } from 'server-response-types'; export class Loader { @@ -17,7 +17,7 @@ export class Loader { public enabled: boolean; public dimension: DimensionType; - constructor(initialData: AnnotationImporterResponseBody) { + constructor(initialData: SerializedAnnotationImporter) { const data = { name: initialData.name, format: initialData.ext, @@ -53,7 +53,7 @@ export class Dumper { public enabled: boolean; public dimension: DimensionType; - constructor(initialData: AnnotationExporterResponseBody) { + constructor(initialData: SerializedAnnotationExporter) { const data = { name: initialData.name, format: initialData.ext, @@ -86,7 +86,7 @@ export class AnnotationFormats { public loaders: Loader[]; public dumpers: Dumper[]; - constructor(initialData: AnnotationFormatsResponseBody) { + constructor(initialData: SerializedAnnotationFormats) { const data = { exporters: initialData.exporters.map((el) => new Dumper(el)), importers: initialData.importers.map((el) => new Loader(el)), diff --git a/cvat-core/src/annotations-history.ts b/cvat-core/src/annotations-history.ts index 5d7fecf26be7..748d55bcf93d 100644 --- a/cvat-core/src/annotations-history.ts +++ b/cvat-core/src/annotations-history.ts @@ -1,4 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT diff --git a/cvat-core/src/api-implementation.ts b/cvat-core/src/api-implementation.ts index 7a77b3f6247c..e5fcd7962a62 100644 --- a/cvat-core/src/api-implementation.ts +++ b/cvat-core/src/api-implementation.ts @@ -172,27 +172,25 @@ export default function implementAPI(cvat) { filter: isString, sort: isString, search: isString, - taskID: isInteger, jobID: isInteger, }); - checkExclusiveFields(query, ['jobID', 'taskID', 'filter', 'search'], ['page', 'sort']); + checkExclusiveFields(query, ['jobID', 'filter', 'search'], ['page', 'sort']); if ('jobID' in query) { - const job = await serverProxy.jobs.get({ id: query.jobID }); + const { results } = await serverProxy.jobs.get({ id: query.jobID }); + const [job] = results; if (job) { - return [new Job(job)]; + // When request job by ID we also need to add labels to work with them + const labels = await serverProxy.labels.get({ job_id: job.id }); + return [new Job({ ...job, labels: labels.results })]; } return []; } - if ('taskID' in query) { - query.filter = JSON.stringify({ and: [{ '==': [{ var: 'task_id' }, query.taskID] }] }); - } - const searchParams = {}; for (const key of Object.keys(query)) { - if (['page', 'sort', 'search', 'filter'].includes(key)) { + if (['page', 'sort', 'search', 'filter', 'task_id'].includes(key)) { searchParams[key] = query[key]; } } @@ -214,7 +212,7 @@ export default function implementAPI(cvat) { ordering: isString, }); - checkExclusiveFields(filter, ['id', 'projectId'], ['page']); + checkExclusiveFields(filter, ['id'], ['page']); const searchParams = {}; for (const key of Object.keys(filter)) { if (['page', 'id', 'sort', 'search', 'filter', 'ordering'].includes(key)) { @@ -233,15 +231,19 @@ export default function implementAPI(cvat) { const tasksData = await serverProxy.tasks.get(searchParams); const tasks = await Promise.all(tasksData.map(async (taskItem) => { - // Temporary workaround for UI - // Fixme: too much requests on tasks page - let jobs = { results: [] }; if ('id' in filter) { - jobs = await serverProxy.jobs.get({ - filter: JSON.stringify({ and: [{ '==': [{ var: 'task_id' }, taskItem.id] }] }), - }, true); + // When request task by ID we also need to add labels and jobs to work with them + const labels = await serverProxy.labels.get({ task_id: taskItem.id }); + const jobs = await serverProxy.jobs.get({ task_id: taskItem.id }, true); + return new Task({ + ...taskItem, progress: taskItem.jobs, jobs: jobs.results, labels: labels.results, + }); } - return new Task({ ...taskItem, jobs: jobs.results }); + + return new Task({ + ...taskItem, + progress: taskItem.jobs, + }); })); tasks.count = tasksData.count; @@ -260,15 +262,25 @@ export default function implementAPI(cvat) { checkExclusiveFields(filter, ['id'], ['page']); const searchParams = {}; for (const key of Object.keys(filter)) { - if (['id', 'page', 'search', 'sort', 'page', 'filter'].includes(key)) { + if (['page', 'id', 'sort', 'search', 'filter'].includes(key)) { searchParams[key] = filter[key]; } } const projectsData = await serverProxy.projects.get(searchParams); - const projects = projectsData.map((project) => new Project(project)); - projects.count = projectsData.count; + const projects = await Promise.all(projectsData.map(async (projectItem) => { + if ('id' in filter) { + // When request a project by ID we also need to add labels to work with them + const labels = await serverProxy.labels.get({ project_id: projectItem.id }); + return new Project({ ...projectItem, labels: labels.results }); + } + return new Project({ + ...projectItem, + }); + })); + + projects.count = projectsData.count; return projects; }; diff --git a/cvat-core/src/common.ts b/cvat-core/src/common.ts index ab07c5b96ca5..1aa67589cfdc 100644 --- a/cvat-core/src/common.ts +++ b/cvat-core/src/common.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023s CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -92,33 +92,22 @@ export function checkObjectType(name, value, type, instance?): boolean { } export class FieldUpdateTrigger { - constructor() { - let updatedFlags = {}; - - Object.defineProperties( - this, - Object.freeze({ - reset: { - value: () => { - 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; - }, - }, - }), - ); + #updatedFlags: Record = {}; + + reset(): void { + this.#updatedFlags = {}; + } + + update(name: string): void { + this.#updatedFlags[name] = true; + } + + getUpdated(data: object, propMap: Record = {}): Record { + const result = {}; + for (const updatedField of Object.keys(this.#updatedFlags)) { + result[propMap[updatedField] || updatedField] = data[updatedField]; + } + return result; } } diff --git a/cvat-core/src/config.ts b/cvat-core/src/config.ts index b44d136295b8..063c693a8907 100644 --- a/cvat-core/src/config.ts +++ b/cvat-core/src/config.ts @@ -5,7 +5,6 @@ const config = { backendAPI: '/api', - proxy: false, organizationID: null, origin: '', uploadChunkSize: 100, diff --git a/cvat-core/src/enums.ts b/cvat-core/src/enums.ts index 78b451cbe6cb..6ad3364f1a25 100644 --- a/cvat-core/src/enums.ts +++ b/cvat-core/src/enums.ts @@ -8,12 +8,19 @@ export enum ShareFileType { REG = 'REG', } +export enum ChunkType { + IMAGESET = 'imageset', + VIDEO = 'video', +} + export enum TaskStatus { ANNOTATION = 'annotation', VALIDATION = 'validation', COMPLETED = 'completed', } +export type ProjectStatus = TaskStatus; + export enum JobStage { ANNOTATION = 'annotation', VALIDATION = 'validation', diff --git a/cvat-core/src/labels.ts b/cvat-core/src/labels.ts index 86b41422e534..5e88e9336b7c 100644 --- a/cvat-core/src/labels.ts +++ b/cvat-core/src/labels.ts @@ -1,21 +1,14 @@ // Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT +import { + AttrInputType, LabelType, SerializedAttribute, SerializedLabel, +} from 'server-response-types'; import { ShapeType, AttributeType } from './enums'; import { ArgumentError } from './exceptions'; -type AttrInputType = 'select' | 'radio' | 'checkbox' | 'number' | 'text'; - -export interface RawAttribute { - name: string; - mutable: boolean; - input_type: AttrInputType; - default_value: string; - values: string[]; - id?: number; -} - export class Attribute { public id?: number; public defaultValue: string; @@ -24,7 +17,7 @@ export class Attribute { public name: string; public values: string[]; - constructor(initialData: RawAttribute) { + constructor(initialData: SerializedAttribute) { const data = { id: undefined, default_value: undefined, @@ -75,8 +68,8 @@ export class Attribute { ); } - toJSON(): RawAttribute { - const object: RawAttribute = { + toJSON(): SerializedAttribute { + const object: SerializedAttribute = { name: this.name, mutable: this.mutable, input_type: this.inputType, @@ -92,19 +85,6 @@ export class Attribute { } } -type LabelType = 'rectangle' | 'polygon' | 'polyline' | 'points' | 'ellipse' | 'cuboid' | 'skeleton' | 'mask' | 'tag' | 'any'; -export interface RawLabel { - id?: number; - name: string; - color?: string; - type: LabelType; - svg?: string; - sublabels?: RawLabel[]; - has_parent?: boolean; - deleted?: boolean; - attributes: RawAttribute[]; -} - export class Label { public name: string; public readonly id?: number; @@ -116,9 +96,10 @@ export class Label { svg: string; } | null; public deleted: boolean; + public patched: boolean; public readonly hasParent?: boolean; - constructor(initialData: RawLabel) { + constructor(initialData: SerializedLabel) { const data = { id: undefined, name: undefined, @@ -127,6 +108,7 @@ export class Label { structure: undefined, has_parent: false, deleted: false, + patched: false, svg: undefined, elements: undefined, sublabels: undefined, @@ -167,6 +149,9 @@ export class Label { throw new ArgumentError(`Name must be a string, but ${typeof name} was given`); } data.name = name; + if (Number.isInteger(data.id)) { + data.patched = true; + } }, }, color: { @@ -174,6 +159,9 @@ export class Label { set: (color) => { if (typeof color === 'string' && color.match(/^#[0-9a-f]{6}$|^$/)) { data.color = color; + if (Number.isInteger(data.id)) { + data.patched = true; + } } else { throw new ArgumentError('Trying to set wrong color format'); } @@ -203,6 +191,12 @@ export class Label { data.deleted = value; }, }, + patched: { + get: () => data.patched, + set: (value) => { + data.patched = value; + }, + }, hasParent: { get: () => data.has_parent, }, @@ -210,8 +204,8 @@ export class Label { ); } - toJSON(): RawLabel { - const object: RawLabel = { + toJSON(): SerializedLabel { + const object: SerializedLabel = { name: this.name, attributes: [...this.attributes.map((el) => el.toJSON())], type: this.type, @@ -225,10 +219,6 @@ export class Label { object.id = this.id; } - if (this.deleted) { - object.deleted = this.deleted; - } - if (this.type) { object.type = this.type; } diff --git a/cvat-core/src/project-implementation.ts b/cvat-core/src/project-implementation.ts index cac2ce9232b9..202f56ab7014 100644 --- a/cvat-core/src/project-implementation.ts +++ b/cvat-core/src/project-implementation.ts @@ -1,15 +1,15 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import { Storage } from './storage'; - import serverProxy from './server-proxy'; import { decodePreview } from './frames'; - import Project from './project'; import { exportDataset, importDataset } from './annotations'; +import { SerializedLabel } from './server-response-types'; +import { Label } from './labels'; export default function implementProject(projectClass) { projectClass.prototype.save.implementation = async function () { @@ -18,16 +18,40 @@ export default function implementProject(projectClass) { bugTracker: 'bug_tracker', 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 Promise.all((projectData.labels || []).map((label: Label): Promise => { + if (label.deleted) { + return serverProxy.labels.delete(label.id); + } + + if (label.patched) { + return serverProxy.labels.update(label.id, label.toJSON()); + } + + return Promise.resolve(); + })); + + // leave only new labels to create them via project PATCH request + projectData.labels = (projectData.labels || []) + .filter((label: SerializedLabel) => !Number.isInteger(label.id)).map((el) => el.toJSON()); + if (!projectData.labels.length) { + delete projectData.labels; } - await serverProxy.projects.save(this.id, projectData); this._updateTrigger.reset(); - return this; + let serializedProject = null; + if (Object.keys(projectData).length) { + serializedProject = await serverProxy.projects.save(this.id, projectData); + } else { + [serializedProject] = (await serverProxy.projects.get({ id: this.id })); + } + + const labels = await serverProxy.labels.get({ project_id: serializedProject.id }); + return new Project({ ...serializedProject, labels: labels.results }); } // initial creating @@ -49,7 +73,8 @@ export default function implementProject(projectClass) { } const project = await serverProxy.projects.create(projectSpec); - return new Project(project); + const labels = await serverProxy.labels.get({ project_id: project.id }); + return new Project({ ...project, labels: labels.results }); }; projectClass.prototype.delete.implementation = async function () { diff --git a/cvat-core/src/project.ts b/cvat-core/src/project.ts index 6e7252ece29b..92be144179aa 100644 --- a/cvat-core/src/project.ts +++ b/cvat-core/src/project.ts @@ -1,11 +1,12 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -import { StorageLocation } from './enums'; +import _ from 'lodash'; +import { DimensionType, ProjectStatus, StorageLocation } from './enums'; import { Storage } from './storage'; - +import { SerializedLabel, SerializedProject } from './server-response-types'; import PluginRegistry from './plugins'; import { ArgumentError } from './exceptions'; import { Label } from './labels'; @@ -13,7 +14,26 @@ import User from './user'; import { FieldUpdateTrigger } from './common'; export default class Project { - constructor(initialData) { + public readonly id: number; + public name: string; + public assignee: User; + public bugTracker: string; + public readonly status: ProjectStatus; + public readonly organization: string | null; + public readonly owner: User; + public readonly createdDate: string; + public readonly updatedDate: string; + public readonly taskSubsets: string[]; + public readonly dimension: DimensionType; + public readonly sourceStorage: Storage; + public readonly targetStorage: Storage; + public labels: Label[]; + public annotations: { + exportDataset: CallableFunction; + importDataset: CallableFunction; + } + + constructor(initialData: SerializedProject & { labels?: SerializedLabel[] }) { const data = { id: undefined, name: undefined, @@ -99,24 +119,47 @@ export default class Project { }, labels: { get: () => [...data.labels], - set: (labels) => { + set: (labels: Label[]) => { if (!Array.isArray(labels)) { throw new ArgumentError('Value must be an array of Labels'); } if (!Array.isArray(labels) || labels.some((label) => !(label instanceof Label))) { throw new ArgumentError( - `Each array value must be an instance of Label. ${typeof label} was found`, + 'Each array value must be an instance of Label', ); } - const IDs = labels.map((_label) => _label.id); - const deletedLabels = data.labels.filter((_label) => !IDs.includes(_label.id)); - deletedLabels.forEach((_label) => { - _label.deleted = true; + const oldIDs = data.labels.map((_label) => _label.id); + const newIDs = labels.map((_label) => _label.id); + + // find any deleted labels and mark them + data.labels.filter((_label) => !newIDs.includes(_label.id)) + .forEach((_label) => { + // for deleted labels let's specify that they are deleted + _label.deleted = true; + }); + + // find any patched labels and mark them + labels.forEach((_label) => { + const { id } = _label; + if (oldIDs.includes(id)) { + const oldLabelIndex = data.labels.findIndex((__label) => __label.id === id); + if (oldLabelIndex !== -1) { + // replace current label by the patched one + const oldLabel = data.labels[oldLabelIndex]; + data.labels.splice(oldLabelIndex, 1, _label); + if (!_.isEqual(_label.toJSON(), oldLabel.toJSON())) { + _label.patched = true; + } + } + } }); - data.labels = [...deletedLabels, ...labels]; + // find new labels to append them to the end + const newLabels = labels.filter((_label) => !Number.isInteger(_label.id)); + data.labels = [...data.labels, ...newLabels]; + updateTrigger.update('labels'); }, }, @@ -150,7 +193,7 @@ export default class Project { // When we call a function, for example: project.annotations.get() // In the method get we lose the project context - // So, we need return it + // So, we need to bind it this.annotations = { exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this), importDataset: Object.getPrototypeOf(this).annotations.importDataset.bind(this), diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index bde9a3ae5ef4..6a67286bbf41 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -5,9 +5,14 @@ import FormData from 'form-data'; import store from 'store'; -import Axios, { AxiosResponse } from 'axios'; +import Axios, { AxiosError, AxiosResponse } from 'axios'; import * as tus from 'tus-js-client'; -import { AnnotationFormatsResponseBody } from 'server-response-types'; +import { + SerializedLabel, SerializedAnnotationFormats, ProjectsFilter, + SerializedProject, SerializedTask, TasksFilter, SerializedUser, + SerializedAbout, SerializedShare, SerializedUserAgreement, + SerializedRegister, JobsFilter, SerializedJob, +} from 'server-response-types'; import { Storage } from './storage'; import { StorageLocation, WebhookSourceType } from './enums'; import { isEmail } from './common'; @@ -26,7 +31,7 @@ type Params = { action?: string, }; -function enableOrganization() { +function enableOrganization(): { org: string } { return { org: config.organizationID || '' }; } @@ -42,32 +47,65 @@ function configureStorage(storage: Storage, useDefaultLocation = false): Partial }; } -function waitFor(frequencyHz, predicate) { - return new Promise((resolve, reject) => { - if (typeof predicate !== 'function') { - reject(new Error(`Predicate must be a function, got ${typeof predicate}`)); - } - - const internalWait = () => { - let result = false; - try { - result = predicate(); - } catch (error) { - reject(error); +function fetchAll(url, filter = {}): Promise { + const pageSize = 500; + const result = { + count: 0, + results: [], + }; + return new Promise((resolve, reject) => { + Axios.get(url, { + params: { + ...filter, + page_size: pageSize, + page: 1, + }, + }).then((initialData) => { + const { count, results } = initialData.data; + result.results = result.results.concat(results); + if (count <= pageSize) { + resolve(result); + return; } - if (result) { - resolve(); - } else { - setTimeout(internalWait, 1000 / frequencyHz); - } - }; + const pages = Math.ceil(count / pageSize); + const promises = Array(pages).fill(0).map((_: number, i: number) => { + if (i) { + return Axios.get(url, { + params: { + ...filter, + page_size: pageSize, + page: i + 1, + }, + }); + } + + return Promise.resolve(null); + }); - setTimeout(internalWait); + Promise.all(promises).then((responses: AxiosResponse[]) => { + responses.forEach((resp) => { + if (resp) { + result.results = result.results.concat(resp.data.results); + } + }); + + // removing possible dublicates + const obj = result.results.reduce((acc: Record, item: any) => { + acc[item.id] = item; + return acc; + }, {}); + + result.results = Object.values(obj); + result.count = result.results.length; + + resolve(result); + }).catch((error) => reject(error)); + }).catch((error) => reject(error)); }); } -async function chunkUpload(file, uploadConfig) { +async function chunkUpload(file: File, uploadConfig) { const params = enableOrganization(); const { endpoint, chunkSize, totalSize, onUpdate, metadata, @@ -116,7 +154,7 @@ async function chunkUpload(file, uploadConfig) { }); } -function generateError(errorData) { +function generateError(errorData: AxiosError<{ message?: string }>): ServerError { if (errorData.response) { if (errorData.response.data?.message) { return new ServerError(errorData.response.data?.message, errorData.response.status); @@ -174,11 +212,11 @@ class WorkerWrappedAxios { } }; - function getRequestId() { + function getRequestId(): number { return requestId++; } - async function get(url, requestConfig) { + async function get(url: string, requestConfig) { return new Promise((resolve, reject) => { const newRequestId = getRequestId(); requests[newRequestId] = { @@ -231,7 +269,7 @@ if (token) { Axios.defaults.headers.common.Authorization = `Token ${token}`; } -function setAuthData(response) { +function setAuthData(response: AxiosResponse): void { if (response.headers['set-cookie']) { // Browser itself setup cookie and header is none // In NodeJS we need do it manually @@ -246,20 +284,18 @@ function setAuthData(response) { } } -function removeAuthData() { +function removeAuthData(): void { Axios.defaults.headers.common.Authorization = ''; store.remove('token'); token = null; } -async function about() { +async function about(): Promise { const { backendAPI } = config; let response = null; try { - response = await Axios.get(`${backendAPI}/server/about`, { - proxy: config.proxy, - }); + response = await Axios.get(`${backendAPI}/server/about`); } catch (errorData) { throw generateError(errorData); } @@ -267,13 +303,12 @@ async function about() { return response.data; } -async function share(directoryArg) { +async function share(directoryArg: string): Promise { const { backendAPI } = config; let response = null; try { response = await Axios.get(`${backendAPI}/server/share`, { - proxy: config.proxy, params: { directory: directoryArg }, }); } catch (errorData) { @@ -283,14 +318,12 @@ async function share(directoryArg) { return response.data; } -async function formats(): Promise { +async function formats(): Promise { const { backendAPI } = config; let response = null; try { - response = await Axios.get(`${backendAPI}/server/annotation/formats`, { - proxy: config.proxy, - }); + response = await Axios.get(`${backendAPI}/server/annotation/formats`); } catch (errorData) { throw generateError(errorData); } @@ -298,12 +331,11 @@ async function formats(): Promise { return response.data; } -async function userAgreements() { +async function userAgreements(): Promise { const { backendAPI } = config; let response = null; try { response = await Axios.get(`${backendAPI}/user-agreements`, { - proxy: config.proxy, validateStatus: (status) => status === 200 || status === 404, }); @@ -317,7 +349,14 @@ async function userAgreements() { } } -async function register(username, firstName, lastName, email, password, confirmations) { +async function register( + username: string, + firstName: string, + lastName: string, + email: string, + password: string, + confirmations: Record, +): Promise { let response = null; try { const data = JSON.stringify({ @@ -330,7 +369,6 @@ async function register(username, firstName, lastName, email, password, confirma confirmations, }); response = await Axios.post(`${config.backendAPI}/auth/register`, data, { - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -343,7 +381,7 @@ async function register(username, firstName, lastName, email, password, confirma return response.data; } -async function login(credential, password) { +async function login(credential: string, password: string): Promise { const authenticationData = [ `${encodeURIComponent(isEmail(credential) ? 'email' : 'username')}=${encodeURIComponent(credential)}`, `${encodeURIComponent('password')}=${encodeURIComponent(password)}`, @@ -354,9 +392,7 @@ async function login(credential, password) { removeAuthData(); let authenticationResponse = null; try { - authenticationResponse = await Axios.post(`${config.backendAPI}/auth/login`, authenticationData, { - proxy: config.proxy, - }); + authenticationResponse = await Axios.post(`${config.backendAPI}/auth/login`, authenticationData); } catch (errorData) { throw generateError(errorData); } @@ -370,7 +406,7 @@ async function loginWithSocialAccount( authParams?: string, process?: string, scope?: string, -) { +): Promise { removeAuthData(); const data = { code, @@ -380,10 +416,7 @@ async function loginWithSocialAccount( }; let authenticationResponse = null; try { - authenticationResponse = await Axios.post(`${config.backendAPI}/auth/${provider}/login/token`, data, - { - proxy: config.proxy, - }); + authenticationResponse = await Axios.post(`${config.backendAPI}/auth/${provider}/login/token`, data); } catch (errorData) { throw generateError(errorData); } @@ -391,18 +424,16 @@ async function loginWithSocialAccount( setAuthData(authenticationResponse); } -async function logout() { +async function logout(): Promise { try { - await Axios.post(`${config.backendAPI}/auth/logout`, { - proxy: config.proxy, - }); + await Axios.post(`${config.backendAPI}/auth/logout`); removeAuthData(); } catch (errorData) { throw generateError(errorData); } } -async function changePassword(oldPassword, newPassword1, newPassword2) { +async function changePassword(oldPassword: string, newPassword1: string, newPassword2: string): Promise { try { const data = JSON.stringify({ old_password: oldPassword, @@ -410,7 +441,6 @@ async function changePassword(oldPassword, newPassword1, newPassword2) { new_password2: newPassword2, }); await Axios.post(`${config.backendAPI}/auth/password/change`, data, { - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -420,13 +450,12 @@ async function changePassword(oldPassword, newPassword1, newPassword2) { } } -async function requestPasswordReset(email) { +async function requestPasswordReset(email: string): Promise { try { const data = JSON.stringify({ email, }); await Axios.post(`${config.backendAPI}/auth/password/reset`, data, { - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -436,7 +465,7 @@ async function requestPasswordReset(email) { } } -async function resetPassword(newPassword1, newPassword2, uid, _token) { +async function resetPassword(newPassword1: string, newPassword2: string, uid: string, _token: string): Promise { try { const data = JSON.stringify({ new_password1: newPassword1, @@ -445,7 +474,6 @@ async function resetPassword(newPassword1, newPassword2, uid, _token) { token: _token, }); await Axios.post(`${config.backendAPI}/auth/password/reset/confirm`, data, { - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -455,14 +483,12 @@ async function resetPassword(newPassword1, newPassword2, uid, _token) { } } -async function getSelf() { +async function getSelf(): Promise { const { backendAPI } = config; let response = null; try { - response = await Axios.get(`${backendAPI}/users/self`, { - proxy: config.proxy, - }); + response = await Axios.get(`${backendAPI}/users/self`); } catch (errorData) { throw generateError(errorData); } @@ -470,7 +496,7 @@ async function getSelf() { return response.data; } -async function authorized() { +async function authorized(): Promise { try { // In CVAT app we use two types of authentication // At first we check if authentication token is present @@ -492,7 +518,13 @@ async function authorized() { return true; } -async function healthCheck(maxRetries, checkPeriod, requestTimeout, progressCallback, attempt = 0) { +async function healthCheck( + maxRetries: number, + checkPeriod: number, + requestTimeout: number, + progressCallback: (status: string) => void, + attempt = 0, +): Promise { const { backendAPI } = config; const url = `${backendAPI}/server/health/?format=json`; @@ -501,7 +533,6 @@ async function healthCheck(maxRetries, checkPeriod, requestTimeout, progressCall } return Axios.get(url, { - proxy: config.proxy, timeout: requestTimeout, }) .then((response) => response.data) @@ -534,7 +565,7 @@ async function healthCheck(maxRetries, checkPeriod, requestTimeout, progressCall }); } -async function serverRequest(url, data) { +async function serverRequest(url: string, data: object): Promise { try { return ( await Axios({ @@ -547,13 +578,12 @@ async function serverRequest(url, data) { } } -async function searchProjectNames(search, limit) { - const { backendAPI, proxy } = config; +async function searchProjectNames(search: string, limit: number): Promise { + const { backendAPI } = config; let response = null; try { response = await Axios.get(`${backendAPI}/projects`, { - proxy, params: { names_only: true, page: 1, @@ -569,18 +599,18 @@ async function searchProjectNames(search, limit) { return response.data.results; } -async function getProjects(filter = {}) { - const { backendAPI, proxy } = config; +async function getProjects(filter: ProjectsFilter = {}): Promise { + const { backendAPI } = config; let response = null; try { if ('id' in filter) { - response = await Axios.get(`${backendAPI}/projects/${filter.id}`, { - proxy, - }); + response = await Axios.get(`${backendAPI}/projects/${filter.id}`); const results = [response.data]; - results.count = 1; - return results; + Object.defineProperty(results, 'count', { + value: 1, + }); + return results as SerializedProject[] & { count: number }; } response = await Axios.get(`${backendAPI}/projects`, { @@ -588,7 +618,6 @@ async function getProjects(filter = {}) { ...filter, page_size: 12, }, - proxy, }); } catch (errorData) { throw generateError(errorData); @@ -598,12 +627,12 @@ async function getProjects(filter = {}) { return response.data.results; } -async function saveProject(id, projectData) { +async function saveProject(id: number, projectData: Partial): Promise { const { backendAPI } = config; + let response = null; try { - await Axios.patch(`${backendAPI}/projects/${id}`, JSON.stringify(projectData), { - proxy: config.proxy, + response = await Axios.patch(`${backendAPI}/projects/${id}`, JSON.stringify(projectData), { headers: { 'Content-Type': 'application/json', }, @@ -611,26 +640,25 @@ async function saveProject(id, projectData) { } catch (errorData) { throw generateError(errorData); } + + return response.data; } -async function deleteProject(id) { +async function deleteProject(id: number): Promise { const { backendAPI } = config; try { - await Axios.delete(`${backendAPI}/projects/${id}`, { - proxy: config.proxy, - }); + await Axios.delete(`${backendAPI}/projects/${id}`); } catch (errorData) { throw generateError(errorData); } } -async function createProject(projectSpec) { +async function createProject(projectSpec: SerializedProject): Promise { const { backendAPI } = config; try { const response = await Axios.post(`${backendAPI}/projects`, JSON.stringify(projectSpec), { - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -641,18 +669,17 @@ async function createProject(projectSpec) { } } -async function getTasks(filter = {}) { +async function getTasks(filter: TasksFilter = {}): Promise { const { backendAPI } = config; - let response = null; try { if ('id' in filter) { - response = await Axios.get(`${backendAPI}/tasks/${filter.id}`, { - proxy: config.proxy, - }); + response = await Axios.get(`${backendAPI}/tasks/${filter.id}`); const results = [response.data]; - results.count = 1; - return results; + Object.defineProperty(results, 'count', { + value: 1, + }); + return results as SerializedTask[] & { count: number }; } response = await Axios.get(`${backendAPI}/tasks`, { @@ -660,7 +687,6 @@ async function getTasks(filter = {}) { ...filter, page_size: 10, }, - proxy: config.proxy, }); } catch (errorData) { throw generateError(errorData); @@ -670,13 +696,12 @@ async function getTasks(filter = {}) { return response.data.results; } -async function saveTask(id, taskData) { +async function saveTask(id: number, taskData: Partial): Promise { const { backendAPI } = config; let response = null; try { response = await Axios.patch(`${backendAPI}/tasks/${id}`, JSON.stringify(taskData), { - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -688,13 +713,12 @@ async function saveTask(id, taskData) { return response.data; } -async function deleteTask(id, organizationID = null) { +async function deleteTask(id: number, organizationID: string | null = null): Promise { const { backendAPI } = config; try { await Axios.delete(`${backendAPI}/tasks/${id}`, { ...(organizationID ? { org: organizationID } : {}), - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -704,7 +728,40 @@ async function deleteTask(id, organizationID = null) { } } -function exportDataset(instanceType) { +async function getLabels(filter: { + job_id?: number, + task_id?: number, + project_id?: number, +}): Promise<{ results: SerializedLabel[] }> { + const { backendAPI } = config; + return fetchAll(`${backendAPI}/labels`, { + ...filter, + ...enableOrganization(), + }); +} + +async function deleteLabel(id: number): Promise { + const { backendAPI } = config; + try { + await Axios.delete(`${backendAPI}/labels/${id}`, { method: 'DELETE' }); + } catch (errorData) { + throw generateError(errorData); + } +} + +async function updateLabel(id: number, body: SerializedLabel): Promise { + const { backendAPI } = config; + let response = null; + try { + response = await Axios.patch(`${backendAPI}/labels/${id}`, body, { method: 'PATCH' }); + } catch (errorData) { + throw generateError(errorData); + } + + return response.data; +} + +function exportDataset(instanceType: 'projects' | 'jobs' | 'tasks') { return async function ( id: number, format: string, @@ -725,7 +782,6 @@ function exportDataset(instanceType) { return new Promise((resolve, reject) => { async function request() { Axios.get(baseURL, { - proxy: config.proxy, params, }) .then((response) => { @@ -778,7 +834,6 @@ async function importDataset( try { const response = await Axios.get(url, { params: { ...params, action: 'import_status' }, - proxy: config.proxy, }); if (response.status === 202) { if (response.data.message) { @@ -804,7 +859,6 @@ async function importDataset( await Axios.post(url, new FormData(), { params, - proxy: config.proxy, }); } catch (errorData) { throw generateError(errorData); @@ -824,14 +878,12 @@ async function importDataset( await Axios.post(url, new FormData(), { params, - proxy: config.proxy, headers: { 'Upload-Start': true }, }); await chunkUpload(file, uploadConfig); await Axios.post(url, new FormData(), { params, - proxy: config.proxy, headers: { 'Upload-Finish': true }, }); } catch (errorData) { @@ -858,7 +910,6 @@ async function backupTask(id: number, targetStorage: Storage, useDefaultSettings async function request() { try { const response = await Axios.get(url, { - proxy: config.proxy, params, }); const isCloudStorage = targetStorage.location === StorageLocation.CLOUD_STORAGE; @@ -898,7 +949,6 @@ async function restoreTask(storage: Storage, file: File | string) { try { taskData.set('rq_id', response.data.rq_id); response = await Axios.post(url, taskData, { - proxy: config.proxy, params, }); if (response.status === 202) { @@ -922,7 +972,6 @@ async function restoreTask(storage: Storage, file: File | string) { response = await Axios.post(url, new FormData(), { params, - proxy: config.proxy, }); } else { const uploadConfig = { @@ -934,14 +983,12 @@ async function restoreTask(storage: Storage, file: File | string) { await Axios.post(url, new FormData(), { params, - proxy: config.proxy, headers: { 'Upload-Start': true }, }); const { filename } = await chunkUpload(file, uploadConfig); response = await Axios.post(url, new FormData(), { params: { ...params, filename }, - proxy: config.proxy, headers: { 'Upload-Finish': true }, }); } @@ -968,7 +1015,6 @@ async function backupProject( async function request() { try { const response = await Axios.get(url, { - proxy: config.proxy, params, }); const isCloudStorage = targetStorage.location === StorageLocation.CLOUD_STORAGE; @@ -1008,7 +1054,6 @@ async function restoreProject(storage: Storage, file: File | string) { try { projectData.set('rq_id', response.data.rq_id); response = await Axios.post(`${backendAPI}/projects/backup`, projectData, { - proxy: config.proxy, params, }); if (response.status === 202) { @@ -1034,7 +1079,6 @@ async function restoreProject(storage: Storage, file: File | string) { response = await Axios.post(url, new FormData(), { params, - proxy: config.proxy, }); } else { const uploadConfig = { @@ -1046,14 +1090,12 @@ async function restoreProject(storage: Storage, file: File | string) { await Axios.post(url, new FormData(), { params, - proxy: config.proxy, headers: { 'Upload-Start': true }, }); const { filename } = await chunkUpload(file, uploadConfig); response = await Axios.post(url, new FormData(), { params: { ...params, filename }, - proxy: config.proxy, headers: { 'Upload-Finish': true }, }); } @@ -1135,7 +1177,6 @@ async function createTask(taskSpec, taskDataSpec, onUpdate) { onUpdate('The task is being created on the server..', null); try { response = await Axios.post(`${backendAPI}/tasks`, JSON.stringify(taskSpec), { - proxy: config.proxy, params, headers: { 'Content-Type': 'application/json', @@ -1168,7 +1209,6 @@ async function createTask(taskSpec, taskDataSpec, onUpdate) { onUpdate('The data are being uploaded to the server', percentage); await Axios.post(`${backendAPI}/tasks/${taskId}/data`, taskData, { ...params, - proxy: config.proxy, headers: { 'Upload-Multiple': true }, }); for (let i = 0; i < fileBulks[currentChunkNumber].files.length; i++) { @@ -1183,7 +1223,6 @@ async function createTask(taskSpec, taskDataSpec, onUpdate) { await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`, taskData, { ...params, - proxy: config.proxy, headers: { 'Upload-Start': true }, }); const uploadConfig = { @@ -1204,7 +1243,6 @@ async function createTask(taskSpec, taskDataSpec, onUpdate) { await Axios.post(`${backendAPI}/tasks/${response.data.id}/data`, taskData, { ...params, - proxy: config.proxy, headers: { 'Upload-Finish': true }, }); } catch (errorData) { @@ -1228,92 +1266,36 @@ async function createTask(taskSpec, taskDataSpec, onUpdate) { return createdTask[0]; } -function fetchAll(url, filter = {}): Promise { - const pageSize = 500; - const result = { - count: 0, - results: [], - }; - return new Promise((resolve, reject) => { - Axios.get(url, { - params: { - ...filter, - page_size: pageSize, - page: 1, - }, - proxy: config.proxy, - }).then((initialData) => { - const { count, results } = initialData.data; - result.results = result.results.concat(results); - if (count <= pageSize) { - resolve(result); - return; - } - - const pages = Math.ceil(count / pageSize); - const promises = Array(pages).fill(0).map((_: number, i: number) => { - if (i) { - return Axios.get(url, { - params: { - ...filter, - page_size: pageSize, - page: i + 1, - }, - proxy: config.proxy, - }); - } - - return Promise.resolve(null); - }); - - Promise.all(promises).then((responses: AxiosResponse[]) => { - responses.forEach((resp) => { - if (resp) { - result.results = result.results.concat(resp.data.results); - } - }); - - // removing possible dublicates - const obj = result.results.reduce((acc: Record, item: any) => { - acc[item.id] = item; - return acc; - }, {}); - - result.results = Object.values(obj); - result.count = result.results.length; - - resolve(result); - }).catch((error) => reject(error)); - }).catch((error) => reject(error)); - }); -} - -async function getJobs(filter = {}, aggregate = false) { +async function getJobs( + filter: JobsFilter = {}, + aggregate = false, +): Promise<{ results: SerializedJob[], count: number }> { const { backendAPI } = config; const id = filter.id || null; let response = null; try { if (id !== null) { - response = await Axios.get(`${backendAPI}/jobs/${id}`, { - proxy: config.proxy, + response = await Axios.get(`${backendAPI}/jobs/${id}`); + return ({ + results: [response.data], + count: 1, }); - } else { - if (aggregate) { - return await fetchAll(`${backendAPI}/jobs`, { - ...filter, - ...enableOrganization(), - }); - } + } - response = await Axios.get(`${backendAPI}/jobs`, { - proxy: config.proxy, - params: { - ...filter, - page_size: 12, - }, + if (aggregate) { + return await fetchAll(`${backendAPI}/jobs`, { + ...filter, + ...enableOrganization(), }); } + + response = await Axios.get(`${backendAPI}/jobs`, { + params: { + ...filter, + page_size: 12, + }, + }); } catch (errorData) { throw generateError(errorData); } @@ -1365,7 +1347,6 @@ async function createComment(data) { let response = null; try { response = await Axios.post(`${backendAPI}/comments`, JSON.stringify(data), { - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -1384,7 +1365,6 @@ async function createIssue(data) { try { const organization = enableOrganization(); response = await Axios.post(`${backendAPI}/issues`, JSON.stringify(data), { - proxy: config.proxy, params: { ...organization }, headers: { 'Content-Type': 'application/json', @@ -1410,7 +1390,6 @@ async function updateIssue(issueID, data) { let response = null; try { response = await Axios.patch(`${backendAPI}/issues/${issueID}`, JSON.stringify(data), { - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -1422,7 +1401,7 @@ async function updateIssue(issueID, data) { return response.data; } -async function deleteIssue(issueID) { +async function deleteIssue(issueID: number): Promise { const { backendAPI } = config; try { @@ -1432,13 +1411,12 @@ async function deleteIssue(issueID) { } } -async function saveJob(id, jobData) { +async function saveJob(id: number, jobData: Partial): Promise { const { backendAPI } = config; let response = null; try { response = await Axios.patch(`${backendAPI}/jobs/${id}`, JSON.stringify(jobData), { - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -1456,7 +1434,6 @@ async function getUsers(filter = { page_size: 'all' }) { let response = null; try { response = await Axios.get(`${backendAPI}/users`, { - proxy: config.proxy, params: { ...filter, }, @@ -1476,7 +1453,6 @@ function getPreview(instance) { try { const url = `${backendAPI}/${instance}/${id}/preview`; response = await Axios.get(url, { - proxy: config.proxy, responseType: 'blob', }); } catch (errorData) { @@ -1499,7 +1475,6 @@ async function getImageContext(jid, frame) { type: 'context_image', number: frame, }, - proxy: config.proxy, responseType: 'arraybuffer', }); } catch (errorData) { @@ -1523,7 +1498,6 @@ async function getData(tid, jid, chunk) { type: 'chunk', number: chunk, }, - proxy: config.proxy, responseType: 'arraybuffer', }); } catch (errorData) { @@ -1561,7 +1535,6 @@ async function getMeta(session, jid): Promise { let response = null; try { response = await Axios.get(`${backendAPI}/${session}s/${jid}/data/meta`, { - proxy: config.proxy, }); } catch (errorData) { throw generateError(errorData); @@ -1575,9 +1548,7 @@ async function saveMeta(session, jid, meta) { let response = null; try { - response = await Axios.patch(`${backendAPI}/${session}s/${jid}/data/meta`, meta, { - proxy: config.proxy, - }); + response = await Axios.patch(`${backendAPI}/${session}s/${jid}/data/meta`, meta); } catch (errorData) { throw generateError(errorData); } @@ -1591,9 +1562,7 @@ async function getAnnotations(session, id) { let response = null; try { - response = await Axios.get(`${backendAPI}/${session}s/${id}/annotations`, { - proxy: config.proxy, - }); + response = await Axios.get(`${backendAPI}/${session}s/${id}/annotations`); } catch (errorData) { throw generateError(errorData); } @@ -1604,9 +1573,7 @@ async function getFunctions(): Promise { const { backendAPI } = config; try { - const response = await Axios.get(`${backendAPI}/functions`, { - proxy: config.proxy, - }); + const response = await Axios.get(`${backendAPI}/functions`); return response.data; } catch (errorData) { if (errorData.response.status === 404) { @@ -1626,7 +1593,6 @@ async function getFunctionPreview(modelID) { try { const url = `${backendAPI}/functions/${modelID}/preview`; response = await Axios.get(url, { - proxy: config.proxy, responseType: 'blob', }); } catch (errorData) { @@ -1642,7 +1608,6 @@ async function getFunctionProviders() { try { const response = await Axios.get(`${backendAPI}/functions/info`, { - proxy: config.proxy, }); return response.data; } catch (errorData) { @@ -1658,7 +1623,6 @@ async function deleteFunction(functionId: number) { try { await Axios.delete(`${backendAPI}/functions/${functionId}`, { - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -1685,7 +1649,6 @@ async function updateAnnotations(session, id, data, action) { let response = null; try { response = await requestFunc(url, JSON.stringify(data), { - proxy: config.proxy, params, headers: { 'Content-Type': 'application/json', @@ -1702,7 +1665,6 @@ async function runFunctionRequest(body) { try { const response = await Axios.post(`${backendAPI}/functions/requests/`, JSON.stringify(body), { - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -1743,7 +1705,6 @@ async function uploadAnnotations( new FormData(), { params, - proxy: config.proxy, }, ); if (response.status === 202) { @@ -1765,7 +1726,6 @@ async function uploadAnnotations( await Axios.post(url, new FormData(), { params, - proxy: config.proxy, }); } catch (errorData) { throw generateError(errorData); @@ -1781,14 +1741,12 @@ async function uploadAnnotations( await Axios.post(url, new FormData(), { params, - proxy: config.proxy, headers: { 'Upload-Start': true }, }); await chunkUpload(file, uploadConfig); await Axios.post(url, new FormData(), { params, - proxy: config.proxy, headers: { 'Upload-Finish': true }, }); } catch (errorData) { @@ -1806,9 +1764,7 @@ async function getFunctionRequestStatus(requestID) { const { backendAPI } = config; try { - const response = await Axios.get(`${backendAPI}/functions/requests/${requestID}`, { - proxy: config.proxy, - }); + const response = await Axios.get(`${backendAPI}/functions/requests/${requestID}`); return response.data; } catch (errorData) { throw generateError(errorData); @@ -1829,7 +1785,6 @@ async function dumpAnnotations(id, name, format) { return new Promise((resolve, reject) => { async function request() { Axios.get(baseURL, { - proxy: config.proxy, params, }) .then((response) => { @@ -1848,7 +1803,7 @@ async function dumpAnnotations(id, name, format) { }); } -async function cancelFunctionRequest(requestId) { +async function cancelFunctionRequest(requestId: string): Promise { const { backendAPI } = config; try { @@ -1866,7 +1821,6 @@ async function createFunction(functionData: any) { try { const response = await Axios.post(`${backendAPI}/functions`, JSON.stringify(functionData), { - proxy: config.proxy, params, headers: { 'Content-Type': 'application/json', @@ -1898,7 +1852,6 @@ async function callFunction(funId, body) { try { const response = await Axios.post(`${backendAPI}/functions/${funId}/run`, JSON.stringify(body), { - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -1914,10 +1867,7 @@ async function getFunctionsRequests() { const { backendAPI } = config; try { - const response = await Axios.get(`${backendAPI}/functions/requests/`, { - proxy: config.proxy, - }); - + const response = await Axios.get(`${backendAPI}/functions/requests/`); return response.data; } catch (errorData) { if (errorData.response.status === 404) { @@ -1931,9 +1881,7 @@ async function getLambdaFunctions() { const { backendAPI } = config; try { - const response = await Axios.get(`${backendAPI}/lambda/functions`, { - proxy: config.proxy, - }); + const response = await Axios.get(`${backendAPI}/lambda/functions`); return response.data; } catch (errorData) { if (errorData.response.status === 503) { @@ -1948,7 +1896,6 @@ async function runLambdaRequest(body) { try { const response = await Axios.post(`${backendAPI}/lambda/requests`, JSON.stringify(body), { - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -1965,7 +1912,6 @@ async function callLambdaFunction(funId, body) { try { const response = await Axios.post(`${backendAPI}/lambda/functions/${funId}`, JSON.stringify(body), { - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -1981,10 +1927,7 @@ async function getLambdaRequests() { const { backendAPI } = config; try { - const response = await Axios.get(`${backendAPI}/lambda/requests`, { - proxy: config.proxy, - }); - + const response = await Axios.get(`${backendAPI}/lambda/requests`); return response.data; } catch (errorData) { throw generateError(errorData); @@ -1995,9 +1938,7 @@ async function getRequestStatus(requestID) { const { backendAPI } = config; try { - const response = await Axios.get(`${backendAPI}/lambda/requests/${requestID}`, { - proxy: config.proxy, - }); + const response = await Axios.get(`${backendAPI}/lambda/requests/${requestID}`); return response.data; } catch (errorData) { throw generateError(errorData); @@ -2016,110 +1957,10 @@ async function cancelLambdaRequest(requestId) { } } -function predictorStatus(projectId) { - const { backendAPI } = config; - - return new Promise((resolve, reject) => { - async function request() { - try { - const response = await Axios.get(`${backendAPI}/predict/status`, { - params: { - project: projectId, - }, - }); - return response.data; - } catch (errorData) { - throw generateError(errorData); - } - } - - const timeoutCallback = async () => { - let data = null; - try { - data = await request(); - if (data.status === 'queued') { - setTimeout(timeoutCallback, 1000); - } else if (data.status === 'done') { - resolve(data); - } else { - throw new Error(`Unknown status was received "${data.status}"`); - } - } catch (error) { - reject(error); - } - }; - - setTimeout(timeoutCallback); - }); -} - -function predictAnnotations(taskId, frame) { - return new Promise((resolve, reject) => { - const { backendAPI } = config; - - async function request() { - try { - const response = await Axios.get(`${backendAPI}/predict/frame`, { - params: { - task: taskId, - frame, - }, - }); - return response.data; - } catch (errorData) { - throw generateError(errorData); - } - } - - const timeoutCallback = async () => { - let data = null; - try { - data = await request(); - if (data.status === 'queued') { - setTimeout(timeoutCallback, 1000); - } else if (data.status === 'done') { - predictAnnotations.latestRequest.fetching = false; - resolve(data.annotation); - } else { - throw new Error(`Unknown status was received "${data.status}"`); - } - } catch (error) { - predictAnnotations.latestRequest.fetching = false; - reject(error); - } - }; - - const closureId = Date.now(); - predictAnnotations.latestRequest.id = closureId; - const predicate = () => !predictAnnotations.latestRequest.fetching || - predictAnnotations.latestRequest.id !== closureId; - if (predictAnnotations.latestRequest.fetching) { - waitFor(5, predicate).then(() => { - if (predictAnnotations.latestRequest.id !== closureId) { - resolve(null); - } else { - predictAnnotations.latestRequest.fetching = true; - setTimeout(timeoutCallback); - } - }); - } else { - predictAnnotations.latestRequest.fetching = true; - setTimeout(timeoutCallback); - } - }); -} - -predictAnnotations.latestRequest = { - fetching: false, - id: null, -}; - async function installedApps() { const { backendAPI } = config; try { - const response = await Axios.get(`${backendAPI}/server/plugins`, { - proxy: config.proxy, - }); + const response = await Axios.get(`${backendAPI}/server/plugins`); return response.data; } catch (errorData) { throw generateError(errorData); @@ -2131,9 +1972,7 @@ async function createCloudStorage(storageDetail) { const storageDetailData = prepareData(storageDetail); try { - const response = await Axios.post(`${backendAPI}/cloudstorages`, storageDetailData, { - proxy: config.proxy, - }); + const response = await Axios.post(`${backendAPI}/cloudstorages`, storageDetailData); return response.data; } catch (errorData) { throw generateError(errorData); @@ -2145,9 +1984,7 @@ async function updateCloudStorage(id, storageDetail) { const storageDetailData = prepareData(storageDetail); try { - await Axios.patch(`${backendAPI}/cloudstorages/${id}`, storageDetailData, { - proxy: config.proxy, - }); + await Axios.patch(`${backendAPI}/cloudstorages/${id}`, storageDetailData); } catch (errorData) { throw generateError(errorData); } @@ -2159,7 +1996,6 @@ async function getCloudStorages(filter = {}) { let response = null; try { response = await Axios.get(`${backendAPI}/cloudstorages`, { - proxy: config.proxy, params: filter, page_size: 12, }); @@ -2179,9 +2015,7 @@ async function getCloudStorageContent(id, manifestPath) { const url = `${backendAPI}/cloudstorages/${id}/content${ manifestPath ? `?manifest_path=${manifestPath}` : '' }`; - response = await Axios.get(url, { - proxy: config.proxy, - }); + response = await Axios.get(url); } catch (errorData) { throw generateError(errorData); } @@ -2195,9 +2029,7 @@ async function getCloudStorageStatus(id) { let response = null; try { const url = `${backendAPI}/cloudstorages/${id}/status`; - response = await Axios.get(url, { - proxy: config.proxy, - }); + response = await Axios.get(url); } catch (errorData) { throw generateError(errorData); } @@ -2209,9 +2041,7 @@ async function deleteCloudStorage(id) { const { backendAPI } = config; try { - await Axios.delete(`${backendAPI}/cloudstorages/${id}`, { - proxy: config.proxy, - }); + await Axios.delete(`${backendAPI}/cloudstorages/${id}`); } catch (errorData) { throw generateError(errorData); } @@ -2236,7 +2066,6 @@ async function createOrganization(data) { let response = null; try { response = await Axios.post(`${backendAPI}/organizations`, JSON.stringify(data), { - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -2254,7 +2083,6 @@ async function updateOrganization(id, data) { let response = null; try { response = await Axios.patch(`${backendAPI}/organizations/${id}`, JSON.stringify(data), { - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -2271,7 +2099,6 @@ async function deleteOrganization(id) { try { await Axios.delete(`${backendAPI}/organizations/${id}`, { - proxy: config.proxy, headers: { 'Content-Type': 'application/json', }, @@ -2287,7 +2114,6 @@ async function getOrganizationMembers(orgSlug, page, pageSize, filters = {}) { let response = null; try { response = await Axios.get(`${backendAPI}/memberships`, { - proxy: config.proxy, params: { ...filters, org: orgSlug, @@ -2311,9 +2137,6 @@ async function inviteOrganizationMembers(orgId, data) { ...data, organization: orgId, }, - { - proxy: config.proxy, - }, ); } catch (errorData) { throw generateError(errorData); @@ -2329,9 +2152,6 @@ async function updateOrganizationMembership(membershipId, data) { { ...data, }, - { - proxy: config.proxy, - }, ); } catch (errorData) { throw generateError(errorData); @@ -2344,9 +2164,7 @@ async function deleteOrganizationMembership(membershipId) { const { backendAPI } = config; try { - await Axios.delete(`${backendAPI}/memberships/${membershipId}`, { - proxy: config.proxy, - }); + await Axios.delete(`${backendAPI}/memberships/${membershipId}`); } catch (errorData) { throw generateError(errorData); } @@ -2357,9 +2175,7 @@ async function getMembershipInvitation(id) { let response = null; try { - response = await Axios.get(`${backendAPI}/invitations/${id}`, { - proxy: config.proxy, - }); + response = await Axios.get(`${backendAPI}/invitations/${id}`); return response.data; } catch (errorData) { throw generateError(errorData); @@ -2372,7 +2188,6 @@ async function getWebhookDelivery(webhookID: number, deliveryID: number): Promis try { const response = await Axios.get(`${backendAPI}/webhooks/${webhookID}/deliveries/${deliveryID}`, { - proxy: config.proxy, params, headers: { 'Content-Type': 'application/json', @@ -2390,7 +2205,6 @@ async function getWebhooks(filter, pageSize = 10): Promise { try { const response = await Axios.get(`${backendAPI}/webhooks`, { - proxy: config.proxy, params: { ...params, ...filter, @@ -2414,7 +2228,6 @@ async function createWebhook(webhookData: any): Promise { try { const response = await Axios.post(`${backendAPI}/webhooks`, JSON.stringify(webhookData), { - proxy: config.proxy, params, headers: { 'Content-Type': 'application/json', @@ -2433,7 +2246,6 @@ async function updateWebhook(webhookID: number, webhookData: any): Promise try { const response = await Axios .patch(`${backendAPI}/webhooks/${webhookID}`, JSON.stringify(webhookData), { - proxy: config.proxy, params, headers: { 'Content-Type': 'application/json', @@ -2451,7 +2263,6 @@ async function deleteWebhook(webhookID: number): Promise { try { await Axios.delete(`${backendAPI}/webhooks/${webhookID}`, { - proxy: config.proxy, params, headers: { 'Content-Type': 'application/json', @@ -2482,7 +2293,6 @@ async function pingWebhook(webhookID: number): Promise { try { const response = await Axios.post(`${backendAPI}/webhooks/${webhookID}/ping`, { - proxy: config.proxy, params, headers: { 'Content-Type': 'application/json', @@ -2502,7 +2312,6 @@ async function receiveWebhookEvents(type: WebhookSourceType): Promise try { const response = await Axios.get(`${backendAPI}/webhooks/events`, { - proxy: config.proxy, params: { type, }, @@ -2519,9 +2328,7 @@ async function receiveWebhookEvents(type: WebhookSourceType): Promise async function socialAuthentication(): Promise { const { backendAPI } = config; try { - const response = await Axios.get(`${backendAPI}/auth/social/methods`, { - proxy: config.proxy, - }); + const response = await Axios.get(`${backendAPI}/auth/social/methods`); return response.data; } catch (errorData) { throw generateError(errorData); @@ -2572,6 +2379,12 @@ export default Object.freeze({ restore: restoreTask, }), + labels: Object.freeze({ + get: getLabels, + delete: deleteLabel, + update: updateLabel, + }), + jobs: Object.freeze({ get: getJobs, getPreview: getPreview('jobs'), @@ -2636,11 +2449,6 @@ export default Object.freeze({ create: createComment, }), - predictor: Object.freeze({ - status: predictorStatus, - predict: predictAnnotations, - }), - cloudStorages: Object.freeze({ get: getCloudStorages, getContent: getCloudStorageContent, diff --git a/cvat-core/src/server-response-types.ts b/cvat-core/src/server-response-types.ts index 88835113d4b9..d29b8d0630f7 100644 --- a/cvat-core/src/server-response-types.ts +++ b/cvat-core/src/server-response-types.ts @@ -2,10 +2,14 @@ // // SPDX-License-Identifier: MIT -import { DimensionType } from 'enums'; +import { + ChunkType, + DimensionType, JobStage, JobState, ProjectStatus, + ShareFileType, TaskMode, TaskStatus, +} from 'enums'; import { SerializedModel } from 'core-types'; -export interface AnnotationImporterResponseBody { +export interface SerializedAnnotationImporter { name: string; ext: string; version: string; @@ -13,14 +17,160 @@ export interface AnnotationImporterResponseBody { dimension: DimensionType; } -export type AnnotationExporterResponseBody = AnnotationImporterResponseBody; +export type SerializedAnnotationExporter = SerializedAnnotationImporter; -export interface AnnotationFormatsResponseBody { - importers: AnnotationImporterResponseBody[]; - exporters: AnnotationExporterResponseBody[]; +export interface SerializedAnnotationFormats { + importers: SerializedAnnotationImporter[]; + exporters: SerializedAnnotationExporter[]; } export interface FunctionsResponseBody { results: SerializedModel[]; count: number; } + +export interface ProjectsFilter { + page?: number; + id?: number; + sort?: string; + search?: string; + filter?: string; +} + +export interface SerializedUser { + url: string; + id: number; + username: string; + first_name: string; + last_name: string; + email?: string; + groups?: ('user' | 'business' | 'admin')[]; + is_staff?: boolean; + is_superuser?: boolean; + is_active?: boolean; + last_login?: string; + date_joined?: string; +} + +export interface SerializedProject { + assignee: SerializedUser | null; + id: number; + bug_tracker: string; + created_date: string; + updated_date: string; + dimension: DimensionType; + name: string; + organization: number | null; + owner: SerializedUser; + source_storage: { id: number; location: 'local' | 'cloud'; cloud_storage_id: null }; + target_storage: { id: number; location: 'local' | 'cloud'; cloud_storage_id: null }; + url: string; + tasks: { count: number; url: string; }; + task_subsets: string[]; + status: ProjectStatus; +} + +export type TasksFilter = ProjectsFilter & { ordering?: string; }; // TODO: Need to clarify how "ordering" is used +export type JobsFilter = ProjectsFilter & { + task_id?: number; +}; + +export interface SerializedTask { + assignee: SerializedUser | null; + bug_tracker: string; + created_date: string; + data: number; + data_chunk_size: number | null; + data_compressed_chunk_type: ChunkType + data_original_chunk_type: ChunkType; + dimension: DimensionType; + id: number; + image_quality: number; + jobs: { count: 1; completed: 0; url: string; }; + labels: { count: number; url: string; }; + mode: TaskMode | ''; + name: string; + organization: number | null; + overlap: number | null; + owner: SerializedUser; + project_id: number | null; + segment_size: number; + size: number; + source_storage: { id: number; location: 'local' | 'cloud'; cloud_storage_id: null }; + target_storage: { id: number; location: 'local' | 'cloud'; cloud_storage_id: null }; + status: TaskStatus; + subset: string; + updated_date: string; + url: string; +} + +export interface SerializedJob { + assignee: SerializedUser | null; + bug_tracker: string; + data_chunk_size: number | null; + data_compressed_chunk_type: ChunkType + dimension: DimensionType; + id: number; + issues: { count: number; url: string }; + labels: { count: number; url: string }; + mode: TaskMode; + project_id: number | null; + stage: JobStage; + state: JobState; + startFrame: number; + stopFrame: number; + task_id: number; + updated_date: string; + url: string; +} + +export type AttrInputType = 'select' | 'radio' | 'checkbox' | 'number' | 'text'; +export interface SerializedAttribute { + name: string; + mutable: boolean; + input_type: AttrInputType; + default_value: string; + values: string[]; + id?: number; +} + +export type LabelType = 'rectangle' | 'polygon' | 'polyline' | 'points' | 'ellipse' | 'cuboid' | 'skeleton' | 'mask' | 'tag' | 'any'; +export interface SerializedLabel { + id?: number; + name: string; + color?: string; + type: LabelType; + svg?: string; + sublabels?: SerializedLabel[]; + has_parent?: boolean; + attributes: SerializedAttribute[]; +} + +export interface SerializedAbout { + description: string; + name: string; + version: string; +} + +export interface SerializedShare { + name: string; + type: ShareFileType; + mime_type: string; +} + +export interface SerializedUserAgreement { + name: string; + required: boolean; + textPrefix: string; + url: string; + urlDisplayText: string; + value: boolean; +} + +export interface SerializedRegister { + email: string; + email_verification_required: boolean; + first_name: string; + last_name: string; + username: string; +} diff --git a/cvat-core/src/session-implementation.ts b/cvat-core/src/session-implementation.ts index 99ec80b9a929..bdde1c7c21de 100644 --- a/cvat-core/src/session-implementation.ts +++ b/cvat-core/src/session-implementation.ts @@ -1,5 +1,11 @@ -import { ArgumentError, DataError } from './exceptions'; +// Copyright (C) 2019-2022 Intel Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import { ArgumentError } from './exceptions'; import { HistoryActions } from './enums'; +import { Storage } from './storage'; import loggerStorage from './logger-storage'; import serverProxy from './server-proxy'; import { @@ -15,6 +21,8 @@ import { decodePreview, } from './frames'; import Issue from './issue'; +import { Label } from './labels'; +import { SerializedLabel } from './server-response-types'; import { checkObjectType } from './common'; import { getAnnotations, putAnnotations, saveAnnotations, @@ -357,39 +365,6 @@ export function implementJob(Job) { return result; }; - Job.prototype.predictor.status.implementation = async function () { - 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) { - 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.close.implementation = function closeTask() { clearFrames(this.id); clearCache(this); @@ -411,7 +386,6 @@ export function implementTask(Task) { }; Task.prototype.save.implementation = async function (onUpdate) { - // TODO: Add ability to change an owner and an assignee if (typeof this.id !== 'undefined') { // If the task has been already created, we update it const taskData = this._updateTrigger.getUpdated(this, { @@ -419,21 +393,47 @@ export function implementTask(Task) { 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 Promise.all((taskData.labels || []).map((label: Label): Promise => { + if (label.deleted) { + return serverProxy.labels.delete(label.id); + } + + if (label.patched) { + return serverProxy.labels.update(label.id, label.toJSON()); + } + + return Promise.resolve(); + })); + + // leave only new labels to create them via project PATCH request + taskData.labels = (taskData.labels || []) + .filter((label: SerializedLabel) => !Number.isInteger(label.id)).map((el) => el.toJSON()); + if (!taskData.labels.length) { + delete taskData.labels; } - const data = await serverProxy.tasks.save(this.id, taskData); - // Temporary workaround for UI - const jobs = await serverProxy.jobs.get({ - filter: JSON.stringify({ and: [{ '==': [{ var: 'task_id' }, data.id] }] }), - }, true); this._updateTrigger.reset(); - return new Task({ ...data, jobs: jobs.results }); + + let serializedTask = null; + if (Object.keys(taskData).length) { + serializedTask = await serverProxy.tasks.save(this.id, taskData); + } else { + [serializedTask] = (await serverProxy.tasks.get({ id: this.id })); + } + const labels = await serverProxy.labels.get({ task_id: this.id }); + const jobs = await serverProxy.jobs.get({ task_id: this.id }, true); + + return new Task({ + ...serializedTask, + progress: serializedTask.jobs, + jobs: jobs.results, + labels: labels.results, + }); } const taskSpec: any = { @@ -473,33 +473,26 @@ export function implementTask(Task) { use_zip_chunks: this.useZipChunks, use_cache: this.useCache, sorting_method: this.sortingMethod, + ...(typeof this.startFrame !== 'undefined' ? { start_frame: this.startFrame } : {}), + ...(typeof this.stopFrame !== 'undefined' ? { stop_frame: this.stopFrame } : {}), + ...(typeof this.frameFilter !== 'undefined' ? { frame_filter: this.frameFilter } : {}), + ...(typeof this.dataChunkSize !== 'undefined' ? { chunk_size: this.dataChunkSize } : {}), + ...(typeof this.copyData !== 'undefined' ? { copy_data: this.copyData } : {}), + ...(typeof this.cloudStorageId !== 'undefined' ? { cloud_storage_id: this.cloudStorageId } : {}), }; - if (typeof this.startFrame !== 'undefined') { - taskDataSpec.start_frame = this.startFrame; - } - if (typeof this.stopFrame !== 'undefined') { - taskDataSpec.stop_frame = this.stopFrame; - } - if (typeof this.frameFilter !== 'undefined') { - taskDataSpec.frame_filter = this.frameFilter; - } - if (typeof this.dataChunkSize !== 'undefined') { - taskDataSpec.chunk_size = this.dataChunkSize; - } - if (typeof this.copyData !== 'undefined') { - taskDataSpec.copy_data = this.copyData; - } - if (typeof this.cloudStorageId !== 'undefined') { - taskDataSpec.cloud_storage_id = this.cloudStorageId; - } - const task = await serverProxy.tasks.create(taskSpec, taskDataSpec, onUpdate); - // Temporary workaround for UI + const labels = await serverProxy.labels.get({ task_id: task.id }); const jobs = await serverProxy.jobs.get({ filter: JSON.stringify({ and: [{ '==': [{ var: 'task_id' }, task.id] }] }), }, true); - return new Task({ ...task, jobs: jobs.results }); + + return new Task({ + ...task, + progress: task.jobs, + jobs: jobs.results, + labels: labels.results, + }); }; Task.prototype.delete.implementation = async function () { @@ -543,6 +536,7 @@ export function implementTask(Task) { job.stopFrame, isPlaying, step, + this.dimension, ); return result; }; @@ -816,38 +810,5 @@ export function implementTask(Task) { return result; }; - Task.prototype.predictor.status.implementation = async function () { - if (!Number.isInteger(this.projectId)) { - throw new DataError('The task 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, - }; - }; - - Task.prototype.predictor.predict.implementation = async function (frame) { - if (!Number.isInteger(frame) || frame < 0) { - throw new ArgumentError(`Frame must be a positive integer. Got: "${frame}"`); - } - - if (frame >= this.size) { - throw new ArgumentError(`The frame with number ${frame} is out of the task`); - } - - if (!Number.isInteger(this.projectId)) { - throw new DataError('The task must belong to a project to use the feature'); - } - - const result = await serverProxy.predictor.predict(this.id, frame); - return result; - }; - return Task; } diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts index e776c9c2a207..4a01202de8ca 100644 --- a/cvat-core/src/session.ts +++ b/cvat-core/src/session.ts @@ -1,9 +1,13 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -import { JobStage, JobState, StorageLocation } from './enums'; +import _ from 'lodash'; +import { + ChunkType, DimensionType, JobStage, + JobState, StorageLocation, TaskMode, TaskStatus, +} from './enums'; import { Storage } from './storage'; import PluginRegistry from './plugins'; @@ -295,25 +299,68 @@ function buildDuplicatedAPI(prototype) { }, writable: true, }), - predictor: Object.freeze({ - value: { - async status() { - const result = await PluginRegistry.apiWrapper.call(this, prototype.predictor.status); - return result; - }, - async predict(frame) { - const result = await PluginRegistry.apiWrapper.call(this, prototype.predictor.predict, frame); - return result; - }, - }, - writable: true, - }), }); } export class Session {} export class Job extends Session { + public assignee: User; + public stage: JobStage; + public state: JobState; + public readonly id: number; + public readonly startFrame: number; + public readonly stopFrame: number; + public readonly projectId: number | null; + public readonly taskId: number; + public readonly dimension: DimensionType; + public readonly dataCompressedChunkType: ChunkType; + public readonly bugTracker: string | null; + public readonly mode: TaskMode; + public readonly labels: Label[]; + + public annotations: { + get: CallableFunction; + put: CallableFunction; + save: CallableFunction; + merge: CallableFunction; + split: CallableFunction; + group: CallableFunction; + clear: CallableFunction; + search: CallableFunction; + searchEmpty: CallableFunction; + upload: CallableFunction; + select: CallableFunction; + import: CallableFunction; + export: CallableFunction; + statistics: CallableFunction; + hasUnsavedChanges: CallableFunction; + exportDataset: CallableFunction; + }; + + public actions: { + undo: CallableFunction; + redo: CallableFunction; + freeze: CallableFunction; + clear: CallableFunction; + get: CallableFunction; + }; + + public frames: { + get: CallableFunction; + delete: CallableFunction; + restore: CallableFunction; + save: CallableFunction; + ranges: CallableFunction; + preview: CallableFunction; + contextImage: CallableFunction; + search: CallableFunction; + }; + + public logger: { + log: CallableFunction; + }; + constructor(initialData) { super(); const data = { @@ -325,7 +372,7 @@ export class Job extends Session { stop_frame: undefined, project_id: null, task_id: undefined, - labels: undefined, + labels: [], dimension: undefined, data_compressed_chunk_type: undefined, data_chunk_size: undefined, @@ -358,8 +405,6 @@ export class Job extends Session { return new Label(labelData); }).filter((label) => !label.hasParent); - } else { - throw new Error('Job labels must be an array'); } Object.defineProperties( @@ -435,22 +480,13 @@ export class Job extends Session { get: () => data.task_id, }, labels: { - get: () => data.labels.filter((_label) => !_label.deleted), + get: () => [...data.labels], }, dimension: { get: () => data.dimension, }, 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; - }, }, dataChunkType: { get: () => data.data_compressed_chunk_type, @@ -511,11 +547,6 @@ export class Job extends Session { this.logger = { log: Object.getPrototypeOf(this).logger.log.bind(this), }; - - this.predictor = { - status: Object.getPrototypeOf(this).predictor.status.bind(this), - predict: Object.getPrototypeOf(this).predictor.predict.bind(this), - }; } async save() { @@ -540,8 +571,86 @@ export class Job extends Session { } export class Task extends Session { + public name: string; + public projectId: number | null; + public assignee: User | null; + public bugTracker: string; + public subset: string; + public labels: Label[]; + public readonly id: number; + public readonly status: TaskStatus; + public readonly size: number; + public readonly mode: TaskMode; + public readonly owner: User; + public readonly createdDate: string; + public readonly updatedDate: string; + public readonly overlap: number | null; + public readonly segmentSize: number; + public readonly imageQuality: number; + public readonly dataChunkSize: number; + public readonly dataCompressedChunkType: ChunkType; + public readonly dataOriginalChunkType: ChunkType; + public readonly dimension: DimensionType; + public readonly sourceStorage: Storage; + public readonly targetStorage: Storage; + public readonly organization: number | null; + public readonly progress: { count: number; completed: number }; + public readonly jobs: Job[]; + + public readonly startFrame: number; + public readonly stopFrame: number; + public readonly frameFilter: string; + public readonly useZipChunks: boolean; + public readonly useCache: boolean; + public readonly copyData: boolean; + public readonly cloudStorageID: number; + public readonly sortingMethod: string; + + public annotations: { + get: CallableFunction; + put: CallableFunction; + save: CallableFunction; + merge: CallableFunction; + split: CallableFunction; + group: CallableFunction; + clear: CallableFunction; + search: CallableFunction; + searchEmpty: CallableFunction; + upload: CallableFunction; + select: CallableFunction; + import: CallableFunction; + export: CallableFunction; + statistics: CallableFunction; + hasUnsavedChanges: CallableFunction; + exportDataset: CallableFunction; + }; + + public actions: { + undo: CallableFunction; + redo: CallableFunction; + freeze: CallableFunction; + clear: CallableFunction; + get: CallableFunction; + }; + + public frames: { + get: CallableFunction; + delete: CallableFunction; + restore: CallableFunction; + save: CallableFunction; + ranges: CallableFunction; + preview: CallableFunction; + contextImage: CallableFunction; + search: CallableFunction; + }; + + public logger: { + log: CallableFunction; + }; + constructor(initialData) { super(); + const data = { id: undefined, name: undefined, @@ -558,22 +667,26 @@ export class Task extends Session { overlap: undefined, segment_size: undefined, image_quality: undefined, - start_frame: undefined, - stop_frame: undefined, - frame_filter: undefined, data_chunk_size: undefined, data_compressed_chunk_type: undefined, data_original_chunk_type: undefined, - deleted_frames: undefined, + dimension: undefined, + source_storage: undefined, + target_storage: undefined, + organization: undefined, + progress: undefined, + labels: undefined, + jobs: undefined, + + start_frame: undefined, + stop_frame: undefined, + frame_filter: undefined, use_zip_chunks: undefined, use_cache: undefined, copy_data: undefined, - dimension: undefined, cloud_storage_id: undefined, sorting_method: undefined, - source_storage: undefined, - target_storage: undefined, - progress: undefined, + files: undefined, }; const updateTrigger = new FieldUpdateTrigger(); @@ -590,20 +703,11 @@ export class Task extends Session { data.labels = []; data.jobs = []; - // FIX ME: progress shoud come from server, not from segments - const progress = { - completedJobs: 0, - totalJobs: 0, + data.progress = { + completedJobs: initialData?.jobs?.completed || 0, + totalJobs: initialData?.jobs?.count || 0, }; - if (Array.isArray(initialData.segments)) { - for (const segment of initialData.segments) { - for (const job of segment.jobs) { - progress.totalJobs += 1; - if (job.stage === 'acceptance') progress.completedJobs += 1; - } - } - } - data.progress = progress; + data.files = Object.freeze({ server_files: [], client_files: [], @@ -625,6 +729,7 @@ export class Task extends Session { stage: job.stage, start_frame: job.start_frame, stop_frame: job.stop_frame, + // following fields also returned when doing API request /jobs/ // here we know them from task and append to constructor task_id: data.id, @@ -724,85 +829,70 @@ export class Task extends Session { }, overlap: { get: () => data.overlap, - set: (overlap) => { - if (!Number.isInteger(overlap) || overlap < 0) { - throw new ArgumentError('Value must be a non negative integer'); - } - data.overlap = overlap; - }, }, segmentSize: { get: () => data.segment_size, - set: (segment) => { - if (!Number.isInteger(segment) || segment < 0) { - throw new ArgumentError('Value must be a positive integer'); - } - data.segment_size = segment; - }, }, imageQuality: { get: () => data.image_quality, - set: (quality) => { - if (!Number.isInteger(quality) || quality < 0) { - throw new ArgumentError('Value must be a positive integer'); - } - data.image_quality = quality; - }, }, useZipChunks: { get: () => data.use_zip_chunks, - set: (useZipChunks) => { - if (typeof useZipChunks !== 'boolean') { - throw new ArgumentError('Value must be a boolean'); - } - data.use_zip_chunks = useZipChunks; - }, }, useCache: { get: () => data.use_cache, - set: (useCache) => { - if (typeof useCache !== 'boolean') { - throw new ArgumentError('Value must be a boolean'); - } - data.use_cache = useCache; - }, }, copyData: { get: () => data.copy_data, - set: (copyData) => { - if (typeof copyData !== 'boolean') { - throw new ArgumentError('Value must be a boolean'); - } - data.copy_data = copyData; - }, }, labels: { - get: () => data.labels.filter((_label) => !_label.deleted), - set: (labels) => { + get: () => [...data.labels], + set: (labels: Label[]) => { if (!Array.isArray(labels)) { throw new ArgumentError('Value must be an array of Labels'); } - for (const label of labels) { - if (!(label instanceof Label)) { - throw new ArgumentError( - `Each array value must be an instance of Label. ${typeof label} was found`, - ); - } + if (!Array.isArray(labels) || labels.some((label) => !(label instanceof Label))) { + throw new ArgumentError( + 'Each array value must be an instance of Label', + ); } - const IDs = labels.map((_label) => _label.id); - const deletedLabels = data.labels.filter((_label) => !IDs.includes(_label.id)); - deletedLabels.forEach((_label) => { - _label.deleted = true; + const oldIDs = data.labels.map((_label) => _label.id); + const newIDs = labels.map((_label) => _label.id); + + // find any deleted labels and mark them + data.labels.filter((_label) => !newIDs.includes(_label.id)) + .forEach((_label) => { + // for deleted labels let's specify that they are deleted + _label.deleted = true; + }); + + // find any patched labels and mark them + labels.forEach((_label) => { + const { id } = _label; + if (oldIDs.includes(id)) { + const oldLabelIndex = data.labels.findIndex((__label) => __label.id === id); + if (oldLabelIndex !== -1) { + // replace current label by the patched one + const oldLabel = data.labels[oldLabelIndex]; + data.labels.splice(oldLabelIndex, 1, _label); + if (!_.isEqual(_label.toJSON(), oldLabel.toJSON())) { + _label.patched = true; + } + } + } }); + // find new labels to append them to the end + const newLabels = labels.filter((_label) => !Number.isInteger(_label.id)); + data.labels = [...data.labels, ...newLabels]; + updateTrigger.update('labels'); - data.labels = [...deletedLabels, ...labels]; }, }, jobs: { - get: () => [...data.jobs], + get: () => [...(data.jobs || [])], }, serverFiles: { get: () => [...data.files.server_files], @@ -864,47 +954,17 @@ export class Task extends Session { Array.prototype.push.apply(data.files.remote_files, remoteFiles); }, }, + frameFilter: { + get: () => data.frame_filter, + }, startFrame: { get: () => data.start_frame, - set: (frame) => { - if (!Number.isInteger(frame) || frame < 0) { - throw new ArgumentError('Value must be a not negative integer'); - } - data.start_frame = frame; - }, }, stopFrame: { get: () => data.stop_frame, - set: (frame) => { - if (!Number.isInteger(frame) || frame < 0) { - throw new ArgumentError('Value must be a not negative integer'); - } - data.stop_frame = frame; - }, - }, - frameFilter: { - get: () => data.frame_filter, - set: (filter) => { - if (typeof filter !== 'string') { - throw new ArgumentError( - `Filter value must be a string. But ${typeof filter} has been got.`, - ); - } - - data.frame_filter = filter; - }, }, 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; - }, }, dataChunkType: { get: () => data.data_compressed_chunk_type, @@ -918,6 +978,9 @@ export class Task extends Session { sortingMethod: { get: () => data.sorting_method, }, + organization: { + get: () => data.organization, + }, sourceStorage: { get: () => ( new Storage({ @@ -990,24 +1053,19 @@ export class Task extends Session { this.logger = { log: Object.getPrototypeOf(this).logger.log.bind(this), }; - - this.predictor = { - status: Object.getPrototypeOf(this).predictor.status.bind(this), - predict: Object.getPrototypeOf(this).predictor.predict.bind(this), - }; } - async close() { + async close(): Promise { const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.close); return result; } - async save(onUpdate = () => {}) { + async save(onUpdate = () => {}): Promise { const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.save, onUpdate); return result; } - async delete() { + async delete(): Promise { const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.delete); return result; } diff --git a/cvat-core/tests/api/jobs.js b/cvat-core/tests/api/jobs.js index b8a9197f55b6..0faf0a3bcf21 100644 --- a/cvat-core/tests/api/jobs.js +++ b/cvat-core/tests/api/jobs.js @@ -20,7 +20,7 @@ const { Job } = require('../../src/session'); describe('Feature: get a list of jobs', () => { test('get jobs by a task id', async () => { const result = await window.cvat.jobs.get({ - taskID: 3, + filter: JSON.stringify({ and: [{ '==': [{ var: 'task_id' }, 3] }] }), }); expect(Array.isArray(result)).toBeTruthy(); expect(result).toHaveLength(2); @@ -34,7 +34,7 @@ describe('Feature: get a list of jobs', () => { test('get jobs by an unknown task id', async () => { const result = await window.cvat.jobs.get({ - taskID: 50, + filter: JSON.stringify({ and: [{ '==': [{ var: 'task_id' }, 50] }] }), }); expect(Array.isArray(result)).toBeTruthy(); expect(result).toHaveLength(0); @@ -93,9 +93,7 @@ describe('Feature: get a list of jobs', () => { describe('Feature: save job', () => { test('save stage and state of a job', async () => { - const result = await window.cvat.jobs.get({ - jobID: 1, - }); + const result = await window.cvat.jobs.get({ jobID: 1 }); result[0].stage = 'validation'; result[0].state = 'new'; diff --git a/cvat-core/tests/api/projects.js b/cvat-core/tests/api/projects.js index 95fa5692baaf..33473d9032ae 100644 --- a/cvat-core/tests/api/projects.js +++ b/cvat-core/tests/api/projects.js @@ -71,16 +71,16 @@ describe('Feature: get projects', () => { describe('Feature: save a project', () => { test('save some changed fields in a project', async () => { - let result = await window.cvat.tasks.get({ + let result = await window.cvat.projects.get({ id: 2, }); result[0].bugTracker = 'newBugTracker'; result[0].name = 'New Project Name'; - result[0].save(); + await result[0].save(); - result = await window.cvat.tasks.get({ + result = await window.cvat.projects.get({ id: 2, }); @@ -108,7 +108,7 @@ describe('Feature: save a project', () => { }); result[0].labels = [...result[0].labels, newLabel]; - result[0].save(); + await result[0].save(); result = await window.cvat.projects.get({ id: 6, diff --git a/cvat-core/tests/api/tasks.js b/cvat-core/tests/api/tasks.js index 9b4d343df77c..02957f1078ec 100644 --- a/cvat-core/tests/api/tasks.js +++ b/cvat-core/tests/api/tasks.js @@ -104,8 +104,6 @@ describe('Feature: save a task', () => { result[0].bugTracker = 'newBugTracker'; result[0].name = 'New Task Name'; - result[0].projectId = 6; - result[0].save(); result = await window.cvat.tasks.get({ @@ -114,7 +112,6 @@ describe('Feature: save a task', () => { expect(result[0].bugTracker).toBe('newBugTracker'); expect(result[0].name).toBe('New Task Name'); - expect(result[0].projectId).toBe(6); }); test('save some new labels in a task', async () => { @@ -124,7 +121,7 @@ describe('Feature: save a task', () => { const labelsLength = result[0].labels.length; const newLabel = new window.cvat.classes.Label({ - name: "My boss's car", + name: "Another label", attributes: [ { default_value: 'false', @@ -137,14 +134,14 @@ describe('Feature: save a task', () => { }); result[0].labels = [...result[0].labels, newLabel]; - result[0].save(); + await result[0].save(); result = await window.cvat.tasks.get({ id: 2, }); expect(result[0].labels).toHaveLength(labelsLength + 1); - const appendedLabel = result[0].labels.filter((el) => el.name === "My boss's car"); + const appendedLabel = result[0].labels.filter((el) => el.name === "Another label"); expect(appendedLabel).toHaveLength(1); expect(appendedLabel[0].attributes).toHaveLength(1); expect(appendedLabel[0].attributes[0].name).toBe('parked'); diff --git a/cvat-core/tests/mocks/dummy-data.mock.js b/cvat-core/tests/mocks/dummy-data.mock.js index 7198da19b5fd..84e6b3883b68 100644 --- a/cvat-core/tests/mocks/dummy-data.mock.js +++ b/cvat-core/tests/mocks/dummy-data.mock.js @@ -143,1051 +143,707 @@ const shareDummyData = [ }, ]; -const projectsDummyData = { - count: 2, - next: null, - previous: null, - results: [ - { - url: 'http://192.168.0.139:7000/api/projects/6', - id: 6, - name: 'Some empty project', - labels: [], - tasks: [], - owner: { - url: 'http://localhost:7000/api/users/2', - id: 2, - username: 'bsekache', - }, - assignee: { - url: 'http://localhost:7000/api/users/2', - id: 2, - username: 'bsekache', - }, - bug_tracker: '', - created_date: '2020-10-19T20:41:07.808029Z', - updated_date: '2020-10-19T20:41:07.808084Z', - status: 'annotation', - }, - { - url: 'http://192.168.0.139:7000/api/projects/1', - id: 2, - name: 'Test project with roads', - labels: [ - { - id: 1, - name: 'car', - color: '#2080c0', - attributes: [ - { - id: 199, - name: 'color', - mutable: false, - input_type: 'select', - default_value: 'red', - values: ['red', 'black', 'white', 'yellow', 'pink', 'green', 'blue', 'orange'], - }, - ], - }, +const projectsDummyLabelsData = { + 6: [], + 2: [{ + id: 10, + name: 'bicycle', + attributes: [ { - id: 2, - name: 'bicycle', - color: '#bb20c0', - attributes: [], + id: 28, + name: 'driver', + mutable: false, + input_type: 'radio', + default_value: 'man', + values: ['man', 'woman'], }, - ], - tasks: [ { - url: 'http://192.168.0.139:7000/api/tasks/2', - id: 2, - name: 'road 1', - project_id: 1, - mode: 'interpolation', - owner: { - url: 'http://localhost:7000/api/users/1', - id: 1, - username: 'admin', - }, - assignee: null, - bug_tracker: '', - created_date: '2020-10-12T08:59:59.878083Z', - updated_date: '2020-10-18T21:02:20.831294Z', - overlap: 5, - segment_size: 100, - z_order: false, - status: 'completed', - labels: [ - { - id: 1, - name: 'car', - color: '#2080c0', - attributes: [ - { - id: 199, - name: 'color', - mutable: false, - input_type: 'select', - default_value: 'red', - values: ['red', 'black', 'white', 'yellow', 'pink', 'green', 'blue', 'orange'], - }, - ], - }, - ], - jobs: "http://localhost:7000/api/jobs?task_id=2", - data_chunk_size: 36, - data_compressed_chunk_type: 'imageset', - data_original_chunk_type: 'video', - size: 432, - image_quality: 100, - data: 1, + id: 29, + name: 'sport', + mutable: true, + input_type: 'checkbox', + default_value: 'false', + values: ['false'], }, ], - owner: { - url: 'http://localhost:7000/api/users/1', - id: 1, - username: 'admin', - }, - assignee: null, - bug_tracker: '', - created_date: '2020-10-12T08:21:56.558898Z', - updated_date: '2020-10-12T08:21:56.558982Z', - status: 'completed', - }, - ], -}; - -const tasksDummyData = { - count: 5, - next: null, - previous: null, - results: [ - { - url: 'http://localhost:7000/api/tasks/102', - id: 102, - name: 'Test', - size: 1, - mode: 'annotation', - owner: { - url: 'http://localhost:7000/api/users/1', - id: 1, - username: 'admin', - }, - assignee: null, - bug_tracker: '', - created_date: '2019-09-05T11:59:22.987942Z', - 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: [ + }, { + id: 9, + name: 'car', + attributes: [ { - id: 5, - name: 'car', - attributes: [], + id: 25, + name: 'model', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'bmw', 'mazda', 'suzuki', 'kia'], }, - ], - jobs: "http://localhost:7000/api/jobs?task_id=102", - image_quality: 50, - start_frame: 0, - stop_frame: 0, - frame_filter: '', - }, - { - url: 'http://localhost:7000/api/tasks/100', - id: 100, - name: 'Image Task', - size: 9, - mode: 'annotation', - owner: { - url: 'http://localhost:7000/api/users/1', - id: 1, - username: 'admin', - }, - assignee: null, - bug_tracker: '', - created_date: '2019-06-18T13:05:08.941304+03:00', - 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: [ { - id: 1, - name: 'car,', - attributes: [], + id: 26, + name: 'driver', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'man', 'woman'], }, { - id: 2, - name: 'person', - attributes: [], + id: 27, + name: 'parked', + mutable: true, + input_type: 'checkbox', + default_value: 'true', + values: ['true'], }, ], - jobs: "http://localhost:7000/api/jobs?task_id=100", - image_quality: 50, - start_frame: 0, - stop_frame: 0, - frame_filter: '', - }, - { - url: 'http://localhost:7000/api/tasks/10', - id: 101, - name: 'Video Task', - size: 5002, - mode: 'interpolation', - owner: { - url: 'http://localhost:7000/api/users/1', - id: 1, - username: 'admin', - }, - assignee: null, - bug_tracker: '', - created_date: '2019-06-21T16:34:49.199691+03:00', - 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: [ - { - id: 22, - name: 'bicycle', - attributes: [ - { - id: 13, - name: 'driver', - mutable: false, - input_type: 'radio', - default_value: 'man', - values: ['man', 'woman'], - }, - { - id: 14, - name: 'sport', - mutable: true, - input_type: 'checkbox', - default_value: 'false', - values: ['false'], - }, - ], - }, + }, { + id: 8, + name: 'face', + attributes: [ { id: 21, - name: 'car', - attributes: [ - { - id: 10, - name: 'model', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'bmw', 'mazda', 'suzuki', 'kia'], - }, - { - id: 11, - name: 'driver', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'man', 'woman'], - }, - { - id: 12, - name: 'parked', - mutable: true, - input_type: 'checkbox', - default_value: 'true', - values: ['true'], - }, + name: 'age', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: [ + '__undefined__', + 'skip', + 'baby (0-5)', + 'child (6-12)', + 'adolescent (13-19)', + 'adult (20-45)', + 'middle-age (46-64)', + 'old (65-)', ], }, { - id: 20, - name: 'face', - attributes: [ - { - id: 6, - name: 'age', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: [ - '__undefined__', - 'skip', - 'baby (0-5)', - 'child (6-12)', - 'adolescent (13-19)', - 'adult (20-45)', - 'middle-age (46-64)', - 'old (65-)', - ], - }, - { - id: 7, - name: 'glass', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'skip', 'no', 'sunglass', 'transparent', 'other'], - }, - { - id: 8, - name: 'beard', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'skip', 'no', 'yes'], - }, - { - id: 9, - name: 'race', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'skip', 'asian', 'black', 'caucasian', 'other'], - }, - ], + id: 22, + name: 'glass', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'skip', 'no', 'sunglass', 'transparent', 'other'], }, { id: 23, - name: 'motorcycle', - attributes: [ - { - id: 15, - name: 'model', - mutable: false, - input_type: 'text', - default_value: 'unknown', - values: ['unknown'], - }, - ], + name: 'beard', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'skip', 'no', 'yes'], }, { - id: 19, - name: 'person, pedestrian', - attributes: [ - { - id: 1, - name: 'action', - mutable: true, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'sitting', 'raising_hand', 'standing'], - }, - { - id: 2, - name: 'age', - mutable: false, - input_type: 'number', - default_value: '1', - values: ['1', '100', '1'], - }, - { - id: 3, - name: 'gender', - mutable: false, - input_type: 'select', - default_value: 'male', - values: ['male', 'female'], - }, - { - id: 4, - name: 'false positive', - mutable: false, - input_type: 'checkbox', - default_value: 'false', - values: ['false'], - }, - { - id: 5, - name: 'clother', - mutable: true, - input_type: 'text', - default_value: 'non, initialized', - values: ['non, initialized'], - }, - ], + id: 24, + name: 'race', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'skip', 'asian', 'black', 'caucasian', 'other'], }, + ], + }, { + id: 11, + name: 'motorcycle', + attributes: [ { - id: 24, - name: 'road', - attributes: [], + id: 30, + name: 'model', + mutable: false, + input_type: 'text', + default_value: 'unknown', + values: ['unknown'], }, ], - jobs: "http://localhost:7000/api/jobs?task_id=101", - image_quality: 50, - start_frame: 0, - stop_frame: 5001, - frame_filter: '', - }, - { - url: 'http://localhost:7000/api/tasks/40', - id: 40, - name: 'test', - project_id: null, - mode: 'annotation', - owner: { - url: 'http://localhost:7000/api/users/1', - id: 1, - username: 'admin', - first_name: '', - last_name: '', - }, - assignee: null, - bug_tracker: '', - created_date: '2022-08-25T12:10:45.471663Z', - updated_date: '2022-08-25T12:10:45.993989Z', - overlap: 0, - segment_size: 4, - status: 'annotation', - labels: [{ - id: 54, - name: 'star skeleton', - color: '#9cb75a', - attributes: [], - type: 'skeleton', - sublabels: [{ - id: 55, - name: '1', - color: '#d12345', - attributes: [], - type: 'points', - has_parent: true + }, { + id: 7, + name: 'person, pedestrian', + attributes: [{ + id: 16, + name: 'action', + mutable: true, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'sitting', 'raising_hand', 'standing'], }, { - id: 56, - name: '2', - color: '#350dea', - attributes: [], - type: 'points', - has_parent: true + id: 17, + name: 'age', + mutable: false, + input_type: 'number', + default_value: '1', + values: ['1', '100', '1'], }, { - id: 57, - name: '3', - color: '#479ffe', - attributes: [], - type: 'points', - has_parent: true + id: 18, + name: 'gender', + mutable: false, + input_type: 'select', + default_value: 'male', + values: ['male', 'female'], }, { - id: 58, - name: '4', - color: '#4a649f', - attributes: [], - type: 'points', - has_parent: true + id: 19, + name: 'false positive', + mutable: false, + input_type: 'checkbox', + default_value: 'false', + values: ['false'], }, { - id: 59, - name: '5', - color: '#478144', - attributes: [], - type: 'points', - has_parent: true - }], - has_parent: false, - svg: - ` - - - - - - - - - ` - }], - jobs: "http://localhost:7000/api/jobs?task_id=40", - data_chunk_size: 17, - data_compressed_chunk_type: 'imageset', - data_original_chunk_type: 'imageset', - size: 4, - image_quality: 70, - data: 12, - dimension: '2d', - subset: '', - organization: null, - target_storage: null, - source_storage: null + id: 20, + name: 'clother', + mutable: true, + input_type: 'text', + default_value: 'non, initialized', + values: ['non, initialized'], + }, + ], + }, { + id: 12, + name: 'road', + attributes: [], + }, + ], +} + +const tasksDummyLabelsData = { + 102: [{ + id: 5, + name: 'car', + attributes: [], + }], + 100: [{ + id: 1, + name: 'car,', + attributes: [], + }, { + id: 2, + name: 'person', + attributes: [], + }, + ], + 101: [{ + id: 22, + name: 'bicycle', + attributes: [{ + id: 13, + name: 'driver', + mutable: false, + input_type: 'radio', + default_value: 'man', + values: ['man', 'woman'], + }, { + id: 14, + name: 'sport', + mutable: true, + input_type: 'checkbox', + default_value: 'false', + values: ['false'], + }, + ], + }, { + id: 21, + name: 'car', + attributes: [{ + id: 10, + name: 'model', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'bmw', 'mazda', 'suzuki', 'kia'], + }, { + id: 11, + name: 'driver', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'man', 'woman'], + }, { + id: 12, + name: 'parked', + mutable: true, + input_type: 'checkbox', + default_value: 'true', + values: ['true'], + }, + ], }, { - url: 'http://localhost:7000/api/tasks/3', - id: 3, - name: 'Test Task', - size: 5002, - mode: 'interpolation', - owner: { - url: 'http://localhost:7000/api/users/2', - id: 2, - username: 'bsekache', - }, - assignee: null, - bug_tracker: '', - created_date: '2019-05-16T13:08:00.621747+03:00', - updated_date: '2019-05-16T13:08:00.621797+03:00', - overlap: 5, - segment_size: 5000, - dimension: '2d', - data_compressed_chunk_type: 'imageset', - data_chunk_size: 1, - status: 'annotation', - labels: [ - { - id: 16, - name: 'bicycle', - attributes: [ - { - id: 43, - name: 'driver', - mutable: false, - input_type: 'radio', - default_value: 'man', - values: ['man', 'woman'], - }, - { - id: 44, - name: 'sport', - mutable: true, - input_type: 'checkbox', - default_value: 'false', - values: ['false'], - }, + id: 20, + name: 'face', + attributes: [{ + id: 6, + name: 'age', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: [ + '__undefined__', + 'skip', + 'baby (0-5)', + 'child (6-12)', + 'adolescent (13-19)', + 'adult (20-45)', + 'middle-age (46-64)', + 'old (65-)', ], + }, { + id: 7, + name: 'glass', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'skip', 'no', 'sunglass', 'transparent', 'other'], + }, { + id: 8, + name: 'beard', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'skip', 'no', 'yes'], + }, { + id: 9, + name: 'race', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'skip', 'asian', 'black', 'caucasian', 'other'], }, - { + ], + }, + { + id: 23, + name: 'motorcycle', + attributes: [{ id: 15, - name: 'car', - attributes: [ - { - id: 40, - name: 'model', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'bmw', 'mazda', 'suzuki', 'kia'], - }, - { - id: 41, - name: 'driver', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'man', 'woman'], - }, - { - id: 42, - name: 'parked', - mutable: true, - input_type: 'checkbox', - default_value: 'true', - values: ['true'], - }, - ], - }, - { - id: 14, - name: 'face', - attributes: [ - { - id: 36, - name: 'age', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: [ - '__undefined__', - 'skip', - 'baby (0-5)', - 'child (6-12)', - 'adolescent (13-19)', - 'adult (20-45)', - 'middle-age (46-64)', - 'old (65-)', - ], - }, - { - id: 37, - name: 'glass', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'skip', 'no', 'sunglass', 'transparent', 'other'], - }, - { - id: 38, - name: 'beard', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'skip', 'no', 'yes'], - }, - { - id: 39, - name: 'race', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'skip', 'asian', 'black', 'caucasian', 'other'], - }, - ], + name: 'model', + mutable: false, + input_type: 'text', + default_value: 'unknown', + values: ['unknown'], }, - { - id: 17, - name: 'motorcycle', - attributes: [ - { - id: 45, - name: 'model', - mutable: false, - input_type: 'text', - default_value: 'unknown', - values: ['unknown'], - }, - ], + ], + }, + { + id: 19, + name: 'person, pedestrian', + attributes: [{ + id: 1, + name: 'action', + mutable: true, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'sitting', 'raising_hand', 'standing'], + }, { + id: 2, + name: 'age', + mutable: false, + input_type: 'number', + default_value: '1', + values: ['1', '100', '1'], + }, { + id: 3, + name: 'gender', + mutable: false, + input_type: 'select', + default_value: 'male', + values: ['male', 'female'], + }, { + id: 4, + name: 'false positive', + mutable: false, + input_type: 'checkbox', + default_value: 'false', + values: ['false'], + }, { + id: 5, + name: 'clother', + mutable: true, + input_type: 'text', + default_value: 'non, initialized', + values: ['non, initialized'], }, - { - id: 13, - name: 'person, pedestrian', - attributes: [ - { - id: 31, - name: 'action', - mutable: true, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'sitting', 'raising_hand', 'standing'], - }, - { - id: 32, - name: 'age', - mutable: false, - input_type: 'number', - default_value: '1', - values: ['1', '100', '1'], - }, - { - id: 33, - name: 'gender', - mutable: false, - input_type: 'select', - default_value: 'male', - values: ['male', 'female'], - }, - { - id: 34, - name: 'false positive', - mutable: false, - input_type: 'checkbox', - default_value: 'false', - values: ['false'], - }, - { - id: 35, - name: 'clother', - mutable: true, - input_type: 'text', - default_value: 'non, initialized', - values: ['non, initialized'], - }, - ], + ], + }, { + id: 24, + name: 'road', + attributes: [], + }, + ], + 40: [{ + id: 54, + name: 'star skeleton', + color: '#9cb75a', + attributes: [], + type: 'skeleton', + sublabels: [{ + id: 55, + name: '1', + color: '#d12345', + attributes: [], + type: 'points', + has_parent: true + }, { + id: 56, + name: '2', + color: '#350dea', + attributes: [], + type: 'points', + has_parent: true + }, { + id: 57, + name: '3', + color: '#479ffe', + attributes: [], + type: 'points', + has_parent: true + }, { + id: 58, + name: '4', + color: '#4a649f', + attributes: [], + type: 'points', + has_parent: true + }, { + id: 59, + name: '5', + color: '#478144', + attributes: [], + type: 'points', + has_parent: true + }], + has_parent: false, + svg: + ` + + + + + + + + + ` + }], + 3: [{ + id: 16, + name: 'bicycle', + attributes: [{ + id: 43, + name: 'driver', + mutable: false, + input_type: 'radio', + default_value: 'man', + values: ['man', 'woman'], + }, { + id: 44, + name: 'sport', + mutable: true, + input_type: 'checkbox', + default_value: 'false', + values: ['false'], }, - { - id: 18, - name: 'road', - attributes: [], + ], + }, + { + id: 15, + name: 'car', + attributes: [{ + id: 40, + name: 'model', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'bmw', 'mazda', 'suzuki', 'kia'], + }, { + id: 41, + name: 'driver', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'man', 'woman'], + }, { + id: 42, + name: 'parked', + mutable: true, + input_type: 'checkbox', + default_value: 'true', + values: ['true'], }, ], - jobs: "http://localhost:7000/api/jobs?task_id=3", - image_quality: 50, }, { - url: 'http://localhost:7000/api/tasks/2', - id: 2, - name: 'Video', - size: 75, - mode: 'interpolation', - owner: { - url: 'http://localhost:7000/api/users/1', - id: 1, - username: 'admin', - }, - assignee: null, - bug_tracker: '', - project_id: 2, - created_date: '2019-05-15T11:40:19.487999+03:00', - updated_date: '2019-05-15T16:58:27.992785+03:00', - overlap: 5, - segment_size: 0, - dimension: '2d', - data_compressed_chunk_type: 'imageset', - data_chunk_size: 1, - status: 'annotation', - labels: [ - { - id: 10, - name: 'bicycle', - attributes: [ - { - id: 28, - name: 'driver', - mutable: false, - input_type: 'radio', - default_value: 'man', - values: ['man', 'woman'], - }, - { - id: 29, - name: 'sport', - mutable: true, - input_type: 'checkbox', - default_value: 'false', - values: ['false'], - }, + id: 14, + name: 'face', + attributes: [{ + id: 36, + name: 'age', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: [ + '__undefined__', + 'skip', + 'baby (0-5)', + 'child (6-12)', + 'adolescent (13-19)', + 'adult (20-45)', + 'middle-age (46-64)', + 'old (65-)', ], + }, { + id: 37, + name: 'glass', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'skip', 'no', 'sunglass', 'transparent', 'other'], + }, { + id: 38, + name: 'beard', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'skip', 'no', 'yes'], + }, { + id: 39, + name: 'race', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'skip', 'asian', 'black', 'caucasian', 'other'], }, - { - id: 9, - name: 'car', - attributes: [ - { - id: 25, - name: 'model', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'bmw', 'mazda', 'suzuki', 'kia'], - }, - { - id: 26, - name: 'driver', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'man', 'woman'], - }, - { - id: 27, - name: 'parked', - mutable: true, - input_type: 'checkbox', - default_value: 'true', - values: ['true'], - }, - ], + ], + }, + { + id: 17, + name: 'motorcycle', + attributes: [{ + id: 45, + name: 'model', + mutable: false, + input_type: 'text', + default_value: 'unknown', + values: ['unknown'], }, - { - id: 8, - name: 'face', - attributes: [ - { - id: 21, - name: 'age', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: [ - '__undefined__', - 'skip', - 'baby (0-5)', - 'child (6-12)', - 'adolescent (13-19)', - 'adult (20-45)', - 'middle-age (46-64)', - 'old (65-)', - ], - }, - { - id: 22, - name: 'glass', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'skip', 'no', 'sunglass', 'transparent', 'other'], - }, - { - id: 23, - name: 'beard', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'skip', 'no', 'yes'], - }, - { - id: 24, - name: 'race', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'skip', 'asian', 'black', 'caucasian', 'other'], - }, - ], + ], + }, + { + id: 13, + name: 'person, pedestrian', + attributes: [{ + id: 31, + name: 'action', + mutable: true, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'sitting', 'raising_hand', 'standing'], + }, { + id: 32, + name: 'age', + mutable: false, + input_type: 'number', + default_value: '1', + values: ['1', '100', '1'], + }, { + id: 33, + name: 'gender', + mutable: false, + input_type: 'select', + default_value: 'male', + values: ['male', 'female'], + }, { + id: 34, + name: 'false positive', + mutable: false, + input_type: 'checkbox', + default_value: 'false', + values: ['false'], + }, { + id: 35, + name: 'clother', + mutable: true, + input_type: 'text', + default_value: 'non, initialized', + values: ['non, initialized'], }, - { + ], + }, + { + id: 18, + name: 'road', + attributes: [], + }, + ], + 1: [ + { + id: 4, + name: 'bicycle', + attributes: [{ + id: 13, + name: 'driver', + mutable: false, + input_type: 'radio', + default_value: 'man', + values: ['man', 'woman'], + }, { + id: 14, + name: 'sport', + mutable: true, + input_type: 'checkbox', + default_value: 'false', + values: ['false'], + }, + ], + }, + { + id: 3, + name: 'car', + attributes: [{ + id: 10, + name: 'model', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'bmw', 'mazda', 'suzuki', 'kia'], + }, { id: 11, - name: 'motorcycle', - attributes: [ - { - id: 30, - name: 'model', - mutable: false, - input_type: 'text', - default_value: 'unknown', - values: ['unknown'], - }, - ], + name: 'driver', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'man', 'woman'], + }, { + id: 12, + name: 'parked', + mutable: true, + input_type: 'checkbox', + default_value: 'true', + values: ['true'], }, - { - id: 7, - name: 'person, pedestrian', - attributes: [ - { - id: 16, - name: 'action', - mutable: true, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'sitting', 'raising_hand', 'standing'], - }, - { - id: 17, - name: 'age', - mutable: false, - input_type: 'number', - default_value: '1', - values: ['1', '100', '1'], - }, - { - id: 18, - name: 'gender', - mutable: false, - input_type: 'select', - default_value: 'male', - values: ['male', 'female'], - }, - { - id: 19, - name: 'false positive', - mutable: false, - input_type: 'checkbox', - default_value: 'false', - values: ['false'], - }, - { - id: 20, - name: 'clother', - mutable: true, - input_type: 'text', - default_value: 'non, initialized', - values: ['non, initialized'], - }, + ], + }, { + id: 2, + name: 'face', + attributes: [{ + id: 6, + name: 'age', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: [ + '__undefined__', + 'skip', + 'baby (0-5)', + 'child (6-12)', + 'adolescent (13-19)', + 'adult (20-45)', + 'middle-age (46-64)', + 'old (65-)', ], + }, { + id: 7, + name: 'glass', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'skip', 'no', 'sunglass', 'transparent', 'other'], + }, { + id: 8, + name: 'beard', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'skip', 'no', 'yes'], + }, { + id: 9, + name: 'race', + mutable: false, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'skip', 'asian', 'black', 'caucasian', 'other'], }, + ], + }, { + id: 5, + name: 'motorcycle', + attributes: [ { - id: 12, - name: 'road', - attributes: [], + id: 15, + name: 'model', + mutable: false, + input_type: 'text', + default_value: 'unknown', + values: ['unknown'], }, ], - jobs: "http://localhost:7000/api/jobs?task_id=2", - image_quality: 50, }, { - url: 'http://localhost:7000/api/tasks/1', - id: 1, - name: 'Labels Set', - size: 9, - mode: 'annotation', - owner: { - url: 'http://localhost:7000/api/users/1', - id: 1, - username: 'admin', - }, - assignee: null, - bug_tracker: 'http://bugtracker.com/issue12345', - created_date: '2019-05-13T15:35:29.871003+03:00', - updated_date: '2019-05-15T11:20:55.770587+03:00', - overlap: 0, - segment_size: 0, - dimension: '2d', - data_compressed_chunk_type: 'imageset', - data_chunk_size: 1, - status: 'annotation', - labels: [ - { - id: 4, - name: 'bicycle', - attributes: [ - { - id: 13, - name: 'driver', - mutable: false, - input_type: 'radio', - default_value: 'man', - values: ['man', 'woman'], - }, - { - id: 14, - name: 'sport', - mutable: true, - input_type: 'checkbox', - default_value: 'false', - values: ['false'], - }, - ], - }, - { - id: 3, - name: 'car', - attributes: [ - { - id: 10, - name: 'model', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'bmw', 'mazda', 'suzuki', 'kia'], - }, - { - id: 11, - name: 'driver', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'man', 'woman'], - }, - { - id: 12, - name: 'parked', - mutable: true, - input_type: 'checkbox', - default_value: 'true', - values: ['true'], - }, - ], - }, - { - id: 2, - name: 'face', - attributes: [ - { - id: 6, - name: 'age', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: [ - '__undefined__', - 'skip', - 'baby (0-5)', - 'child (6-12)', - 'adolescent (13-19)', - 'adult (20-45)', - 'middle-age (46-64)', - 'old (65-)', - ], - }, - { - id: 7, - name: 'glass', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'skip', 'no', 'sunglass', 'transparent', 'other'], - }, - { - id: 8, - name: 'beard', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'skip', 'no', 'yes'], - }, - { - id: 9, - name: 'race', - mutable: false, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'skip', 'asian', 'black', 'caucasian', 'other'], - }, - ], + id: 1, + name: 'person, pedestrian', + attributes: [ + { + id: 1, + name: 'action', + mutable: true, + input_type: 'select', + default_value: '__undefined__', + values: ['__undefined__', 'sitting', 'raising_hand', 'standing'], }, { - id: 5, - name: 'motorcycle', - attributes: [ - { - id: 15, - name: 'model', - mutable: false, - input_type: 'text', - default_value: 'unknown', - values: ['unknown'], - }, - ], + id: 2, + name: 'age', + mutable: false, + input_type: 'number', + default_value: '1', + values: ['1', '100', '1'], }, { - id: 1, - name: 'person, pedestrian', - attributes: [ - { - id: 1, - name: 'action', - mutable: true, - input_type: 'select', - default_value: '__undefined__', - values: ['__undefined__', 'sitting', 'raising_hand', 'standing'], - }, - { - id: 2, - name: 'age', - mutable: false, - input_type: 'number', - default_value: '1', - values: ['1', '100', '1'], - }, - { - id: 3, - name: 'gender', - mutable: false, - input_type: 'select', - default_value: 'male', - values: ['male', 'female'], - }, - { - id: 4, - name: 'false positive', - mutable: false, - input_type: 'checkbox', - default_value: 'false', - values: ['false'], - }, - { - id: 5, - name: 'clother', - mutable: true, - input_type: 'text', - default_value: 'non, initialized', - values: ['non, initialized'], - }, - ], + id: 3, + name: 'gender', + mutable: false, + input_type: 'select', + default_value: 'male', + values: ['male', 'female'], }, { - id: 6, - name: 'road', - attributes: [], + id: 4, + name: 'false positive', + mutable: false, + input_type: 'checkbox', + default_value: 'false', + values: ['false'], + }, + { + id: 5, + name: 'clother', + mutable: true, + input_type: 'text', + default_value: 'non, initialized', + values: ['non, initialized'], }, ], - jobs: "http://localhost:7000/api/jobs?task_id=1", - image_quality: 95, + }, + { + id: 6, + name: 'road', + attributes: [], }, ], -}; +} + +function initJobFromTaskProject(job_id, task_id, proj_id = null) { + const task = tasksDummyData.results.find((task) => task.id === task_id); + const project = Number.isInteger(proj_id) ? projectsDummyData.results.find((proj) => proj.id === proj_id) : undefined; + return { + url: `http://localhost:7000/api/jobs/${job_id}`, + updated_date: '2023-02-14T15:06:53.627413Z', + project_id: proj_id, + task_id: task_id, + bug_tracker: project?.bug_tracker || task.bug_tracker || null, + mode: task.mode, + dimension: task.dimension, + data_chunk_size: task.data_chunk_size, + data_compressed_chunk_type: task.data_compressed_chunk_type, + labels: { count: project ? project.labels.count : task.labels.count, url: `http://localhost:7000/api/labels?job_id=${job_id}` } + } +} const jobsDummyData = { count: 2, @@ -1195,7 +851,6 @@ const jobsDummyData = { previous: null, results: [ { - url: 'http://localhost:7000/api/jobs/112', id: 112, assignee: null, status: 'annotation', @@ -1204,9 +859,9 @@ const jobsDummyData = { start_frame: 0, stop_frame: 0, task_id: 102, + project_id: null, }, { - url: 'http://localhost:7000/api/jobs/100', id: 100, assignee: null, status: 'annotation', @@ -1215,9 +870,9 @@ const jobsDummyData = { start_frame: 0, stop_frame: 8, task_id: 100, + project_id: null, }, { - url: 'http://localhost:7000/api/jobs/40', id: 40, assignee: null, status: 'annotation', @@ -1226,9 +881,9 @@ const jobsDummyData = { start_frame: 0, stop_frame: 3, task_id: 40, + project_id: null, }, { - url: 'http://localhost:7000/api/jobs/20', id: 111, assignee: null, status: 'annotation', @@ -1237,9 +892,9 @@ const jobsDummyData = { start_frame: 4950, stop_frame: 5001, task_id: 101, + project_id: null, }, { - url: 'http://localhost:7000/api/jobs/19', id: 110, assignee: null, status: 'annotation', @@ -1248,9 +903,9 @@ const jobsDummyData = { start_frame: 4455, stop_frame: 4954, task_id: 101, + project_id: null, }, { - url: 'http://localhost:7000/api/jobs/18', id: 109, assignee: null, status: 'annotation', @@ -1259,9 +914,9 @@ const jobsDummyData = { start_frame: 3960, stop_frame: 4459, task_id: 101, + project_id: null, }, { - url: 'http://localhost:7000/api/jobs/17', id: 108, assignee: null, status: 'annotation', @@ -1270,9 +925,9 @@ const jobsDummyData = { start_frame: 3465, stop_frame: 3964, task_id: 101, + project_id: null, }, { - url: 'http://localhost:7000/api/jobs/16', id: 107, assignee: null, status: 'annotation', @@ -1281,9 +936,9 @@ const jobsDummyData = { start_frame: 2970, stop_frame: 3469, task_id: 101, + project_id: null, }, { - url: 'http://localhost:7000/api/jobs/15', id: 106, assignee: null, status: 'annotation', @@ -1292,9 +947,9 @@ const jobsDummyData = { start_frame: 2475, stop_frame: 2974, task_id: 101, + project_id: null, }, { - url: 'http://localhost:7000/api/jobs/14', id: 105, assignee: null, status: 'annotation', @@ -1303,9 +958,9 @@ const jobsDummyData = { start_frame: 1980, stop_frame: 2479, task_id: 101, + project_id: null, }, { - url: 'http://localhost:7000/api/jobs/13', id: 104, assignee: null, status: 'annotation', @@ -1314,141 +969,465 @@ const jobsDummyData = { start_frame: 1485, stop_frame: 1984, task_id: 101, + project_id: null, }, { - url: 'http://localhost:7000/api/jobs/12', id: 103, assignee: null, - status: 'annotation', - stage: 'annotation', - state: 'new', - start_frame: 990, - stop_frame: 1489, - task_id: 101, + status: 'annotation', + stage: 'annotation', + state: 'new', + start_frame: 990, + stop_frame: 1489, + task_id: 101, + project_id: null, + }, + { + id: 102, + assignee: null, + status: 'annotation', + stage: 'annotation', + state: 'new', + start_frame: 495, + stop_frame: 994, + task_id: 101, + project_id: null, + }, + { + id: 101, + assignee: null, + status: 'annotation', + stage: 'annotation', + state: 'new', + start_frame: 0, + stop_frame: 499, + task_id: 101, + project_id: null, + }, + { + id: 9, + assignee: null, + status: 'completed', + stage: 'acceptance', + state: 'completed', + start_frame: 0, + stop_frame: 99, + task_id: 2, + project_id: null, + }, + { + id: 8, + assignee: null, + status: 'completed', + stage: 'acceptance', + state: 'completed', + start_frame: 95, + stop_frame: 194, + task_id: 2, + project_id: null, + }, + { + id: 7, + assignee: null, + status: 'completed', + stage: 'acceptance', + state: 'completed', + start_frame: 190, + stop_frame: 289, + task_id: 2, + project_id: null, + }, + { + id: 6, + assignee: null, + status: 'completed', + stage: 'acceptance', + state: 'completed', + start_frame: 285, + stop_frame: 384, + task_id: 2, + project_id: null, + }, + { + id: 5, + assignee: null, + status: 'completed', + stage: 'acceptance', + state: 'completed', + start_frame: 380, + stop_frame: 431, + task_id: 2, + project_id: null, + }, + { + id: 4, + assignee: null, + status: 'annotation', + stage: 'annotation', + state: 'new', + start_frame: 4995, + stop_frame: 5001, + task_id: 3, + project_id: null, + }, + { + id: 3, + assignee: null, + status: 'annotation', + stage: 'annotation', + state: 'new', + start_frame: 0, + stop_frame: 4999, + task_id: 3, + project_id: null, + }, + { + id: 2, + assignee: null, + status: 'annotation', + stage: 'annotation', + state: 'new', + start_frame: 0, + stop_frame: 74, + task_id: 2, + project_id: 2, + }, + { + id: 1, + assignee: null, + status: 'annotation', + stage: "annotation", + state: "new", + start_frame: 0, + stop_frame: 8, + task_id: 1, + project_id: null, + }, + ] +}; + +const projectsDummyData = { + count: 2, + next: null, + previous: null, + results: [ + { + url: 'http://localhost:7000/api/projects/6', + id: 6, + name: 'Some empty project', + labels: { count: projectsDummyLabelsData[6].length, url: 'http://localhost:7000/api/labels?project_id=6' }, + tasks: [], + owner: { + url: 'http://localhost:7000/api/users/2', + id: 2, + username: 'bsekache', + }, + assignee: { + url: 'http://localhost:7000/api/users/2', + id: 2, + username: 'bsekache', + }, + bug_tracker: '', + created_date: '2020-10-19T20:41:07.808029Z', + updated_date: '2020-10-19T20:41:07.808084Z', + status: 'annotation', + }, + { + url: 'http://localhost:7000/api/projects/1', + id: 2, + name: 'Test project with roads', + labels: { count: projectsDummyLabelsData[2].length, url: 'http://localhost:7000/api/labels?project_id=2' }, + tasks: [ + { + url: 'http://localhost:7000/api/tasks/2', + id: 2, + name: 'road 1', + project_id: 1, + mode: 'interpolation', + owner: { + url: 'http://localhost:7000/api/users/1', + id: 1, + username: 'admin', + }, + assignee: null, + bug_tracker: '', + created_date: '2020-10-12T08:59:59.878083Z', + updated_date: '2020-10-18T21:02:20.831294Z', + overlap: 5, + segment_size: 100, + z_order: false, + status: 'completed', + labels: [ + { + id: 1, + name: 'car', + color: '#2080c0', + attributes: [ + { + id: 199, + name: 'color', + mutable: false, + input_type: 'select', + default_value: 'red', + values: ['red', 'black', 'white', 'yellow', 'pink', 'green', 'blue', 'orange'], + }, + ], + }, + ], + jobs: "http://localhost:7000/api/jobs?task_id=2", + data_chunk_size: 36, + data_compressed_chunk_type: 'imageset', + data_original_chunk_type: 'video', + size: 432, + image_quality: 100, + data: 1, + }, + ], + owner: { + url: 'http://localhost:7000/api/users/1', + id: 1, + username: 'admin', + }, + assignee: null, + bug_tracker: '', + created_date: '2020-10-12T08:21:56.558898Z', + updated_date: '2020-10-12T08:21:56.558982Z', + status: 'completed', }, + ], +}; + +const tasksDummyData = { + count: 5, + next: null, + previous: null, + results: [ { - url: 'http://localhost:7000/api/jobs/11', + url: 'http://localhost:7000/api/tasks/102', id: 102, + name: 'Test', + size: 1, + mode: 'annotation', + owner: { + url: 'http://localhost:7000/api/users/1', + id: 1, + username: 'admin', + }, assignee: null, + bug_tracker: '', + created_date: '2019-09-05T11:59:22.987942Z', + 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', - stage: 'annotation', - state: 'new', - start_frame: 495, - stop_frame: 994, - task_id: 101, + labels: { count: tasksDummyLabelsData[102].length, url: 'http://localhost:7000/api/labels?task_id=102' }, + jobs: { + count: jobsDummyData.results.filter((job) => job.task_id === 102).length, + completed: jobsDummyData.results.filter((job) => job.task_id === 102 && job.stage === 'acceptance' && job.state === 'completed'), + url: 'http://localhost:7000/api/jobs?task_id=102', + }, + image_quality: 50, + start_frame: 0, + stop_frame: 0, + frame_filter: '', }, { - url: 'http://localhost:7000/api/jobs/10', - id: 101, + url: 'http://localhost:7000/api/tasks/100', + id: 100, + name: 'Image Task', + size: 9, + mode: 'annotation', + owner: { + url: 'http://localhost:7000/api/users/1', + id: 1, + username: 'admin', + }, assignee: null, + bug_tracker: '', + created_date: '2019-06-18T13:05:08.941304+03:00', + 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', - stage: 'annotation', - state: 'new', + labels: { count: tasksDummyLabelsData[100].length, url: 'http://localhost:7000/api/labels?task_id=100' }, + jobs: { + count: jobsDummyData.results.filter((job) => job.task_id === 100).length, + completed: jobsDummyData.results.filter((job) => job.task_id === 100 && job.stage === 'acceptance' && job.state === 'completed'), + url: 'http://localhost:7000/api/jobs?task_id=100', + }, + image_quality: 50, start_frame: 0, - stop_frame: 499, - task_id: 101, + stop_frame: 0, + frame_filter: '', }, { - url: 'http://192.168.0.139:7000/api/jobs/9', - id: 9, + url: 'http://localhost:7000/api/tasks/10', + id: 101, + name: 'Video Task', + size: 5002, + mode: 'interpolation', + owner: { + url: 'http://localhost:7000/api/users/1', + id: 1, + username: 'admin', + }, assignee: null, - status: 'completed', - stage: 'acceptance', - state: 'completed', + bug_tracker: '', + created_date: '2019-06-21T16:34:49.199691+03:00', + 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: { count: tasksDummyLabelsData[101].length, url: 'http://localhost:7000/api/labels?task_id=101' }, + jobs: { + count: jobsDummyData.results.filter((job) => job.task_id === 101).length, + completed: jobsDummyData.results.filter((job) => job.task_id === 101 && job.stage === 'acceptance' && job.state === 'completed'), + url: 'http://localhost:7000/api/jobs?task_id=101', + }, + image_quality: 50, start_frame: 0, - stop_frame: 99, - task_id: 2, - }, - { - url: 'http://192.168.0.139:7000/api/jobs/8', - id: 8, - assignee: null, - status: 'completed', - stage: 'acceptance', - state: 'completed', - start_frame: 95, - stop_frame: 194, - task_id: 2, - }, - { - url: 'http://192.168.0.139:7000/api/jobs/7', - id: 7, - assignee: null, - status: 'completed', - stage: 'acceptance', - state: 'completed', - start_frame: 190, - stop_frame: 289, - task_id: 2, - }, - { - url: 'http://192.168.0.139:7000/api/jobs/6', - id: 6, - assignee: null, - status: 'completed', - stage: 'acceptance', - state: 'completed', - start_frame: 285, - stop_frame: 384, - task_id: 2, - }, - { - url: 'http://192.168.0.139:7000/api/jobs/5', - id: 5, - assignee: null, - status: 'completed', - stage: 'acceptance', - state: 'completed', - start_frame: 380, - stop_frame: 431, - task_id: 2, + stop_frame: 5001, + frame_filter: '', }, { - url: 'http://localhost:7000/api/jobs/4', - id: 4, + url: 'http://localhost:7000/api/tasks/40', + id: 40, + name: 'test', + project_id: null, + mode: 'annotation', + owner: { + url: 'http://localhost:7000/api/users/1', + id: 1, + username: 'admin', + first_name: '', + last_name: '', + }, assignee: null, + bug_tracker: '', + created_date: '2022-08-25T12:10:45.471663Z', + updated_date: '2022-08-25T12:10:45.993989Z', + overlap: 0, + segment_size: 4, status: 'annotation', - stage: 'annotation', - state: 'new', - start_frame: 4995, - stop_frame: 5001, - task_id: 3, + labels: { count: tasksDummyLabelsData[40].length, url: 'http://localhost:7000/api/labels?task_id=40' }, + jobs: { + count: jobsDummyData.results.filter((job) => job.task_id === 40).length, + completed: jobsDummyData.results.filter((job) => job.task_id === 40 && job.stage === 'acceptance' && job.state === 'completed'), + url: 'http://localhost:7000/api/jobs?task_id=40', + }, + data_chunk_size: 17, + data_compressed_chunk_type: 'imageset', + data_original_chunk_type: 'imageset', + size: 4, + image_quality: 70, + data: 12, + dimension: '2d', + subset: '', + organization: null, + target_storage: null, + source_storage: null }, { - url: 'http://localhost:7000/api/jobs/3', + url: 'http://localhost:7000/api/tasks/3', id: 3, + name: 'Test Task', + size: 5002, + mode: 'interpolation', + owner: { + url: 'http://localhost:7000/api/users/2', + id: 2, + username: 'bsekache', + }, assignee: null, + bug_tracker: '', + created_date: '2019-05-16T13:08:00.621747+03:00', + updated_date: '2019-05-16T13:08:00.621797+03:00', + overlap: 5, + segment_size: 5000, + dimension: '2d', + data_compressed_chunk_type: 'imageset', + data_chunk_size: 1, status: 'annotation', - stage: 'annotation', - state: 'new', - start_frame: 0, - stop_frame: 4999, - task_id: 3, + labels: { count: tasksDummyLabelsData[3].length, url: 'http://localhost:7000/api/labels?task_id=3' }, + jobs: { + count: jobsDummyData.results.filter((job) => job.task_id === 3).length, + completed: jobsDummyData.results.filter((job) => job.task_id === 3 && job.stage === 'acceptance' && job.state === 'completed'), + url: 'http://localhost:7000/api/jobs?task_id=3', + }, + image_quality: 50, }, { - url: 'http://localhost:7000/api/jobs/2', + url: 'http://localhost:7000/api/tasks/2', id: 2, + name: 'Video', + size: 75, + mode: 'interpolation', + owner: { + url: 'http://localhost:7000/api/users/1', + id: 1, + username: 'admin', + }, assignee: null, + bug_tracker: '', + project_id: 2, + created_date: '2019-05-15T11:40:19.487999+03:00', + updated_date: '2019-05-15T16:58:27.992785+03:00', + overlap: 5, + segment_size: 0, + dimension: '2d', + data_compressed_chunk_type: 'imageset', + data_chunk_size: 1, status: 'annotation', - stage: 'annotation', - state: 'new', - start_frame: 0, - stop_frame: 74, - task_id: 2, + labels: { count: projectsDummyLabelsData[2].length, url: 'http://localhost:7000/api/labels?task_id=2' }, + jobs: { + count: jobsDummyData.results.filter((job) => job.task_id === 2).length, + completed: jobsDummyData.results.filter((job) => job.task_id === 2 && job.stage === 'acceptance' && job.state === 'completed'), + url: 'http://localhost:7000/api/jobs?task_id=2', + }, + image_quality: 50, }, { - url: 'http://localhost:7000/api/jobs/1', + url: 'http://localhost:7000/api/tasks/1', id: 1, + name: 'Labels Set', + size: 9, + mode: 'annotation', + owner: { + url: 'http://localhost:7000/api/users/1', + id: 1, + username: 'admin', + }, assignee: null, + bug_tracker: 'http://bugtracker.com/issue12345', + created_date: '2019-05-13T15:35:29.871003+03:00', + updated_date: '2019-05-15T11:20:55.770587+03:00', + overlap: 0, + segment_size: 0, + dimension: '2d', + data_compressed_chunk_type: 'imageset', + data_chunk_size: 1, status: 'annotation', - stage: "annotation", - state: "new", - start_frame: 0, - stop_frame: 8, - task_id: 1, + labels: { count: tasksDummyLabelsData[1].length, url: 'http://localhost:7000/api/labels?task_id=1' }, + jobs: { + count: jobsDummyData.results.filter((job) => job.task_id === 1).length, + completed: jobsDummyData.results.filter((job) => job.task_id === 1 && job.stage === 'acceptance' && job.state === 'completed'), + url: 'http://localhost:7000/api/jobs?task_id=1', + }, + image_quality: 95, }, - ] -} + ], +}; + +jobsDummyData.results = jobsDummyData.results.map((job) => ({ ...job, ...initJobFromTaskProject(job.id, job.task_id, job.project_id) })); const taskAnnotationsDummyData = { 112: { @@ -3378,7 +3357,9 @@ const webhooksEventsDummyData = { module.exports = { tasksDummyData, + tasksDummyLabelsData, projectsDummyData, + projectsDummyLabelsData, aboutDummyData, shareDummyData, usersDummyData, diff --git a/cvat-core/tests/mocks/server-proxy.mock.js b/cvat-core/tests/mocks/server-proxy.mock.js index 3cc25825075f..d5587a42082d 100644 --- a/cvat-core/tests/mocks/server-proxy.mock.js +++ b/cvat-core/tests/mocks/server-proxy.mock.js @@ -5,7 +5,9 @@ const { tasksDummyData, + tasksDummyLabelsData, projectsDummyData, + projectsDummyLabelsData, aboutDummyData, formatsDummyData, shareDummyData, @@ -94,6 +96,7 @@ class ServerProxy { return true; }); + result.count = result.length; return result; } @@ -105,12 +108,21 @@ class ServerProxy { Object.prototype.hasOwnProperty.call(object, prop) ) { if (prop === 'labels') { - object[prop] = projectData[prop].filter((label) => !label.deleted); + const labels = projectsDummyLabelsData[id]; + // only add new labels here + const maxId = Math.max(0, ...labels.map((label) => label.id)); + const newLabels = [...labels, ...projectData.labels.map((label, index) => ( + { ...label, id: maxId + index + 1 } + ))]; + + projectsDummyLabelsData[object.id] = newLabels; } else { object[prop] = projectData[prop]; } } } + + return (await getProjects({ id }))[0]; } async function createProject(projectData) { @@ -157,6 +169,7 @@ class ServerProxy { return true; }); + result.count = result.length; return result; } @@ -168,7 +181,18 @@ class ServerProxy { Object.prototype.hasOwnProperty.call(object, prop) ) { if (prop === 'labels') { - object[prop] = taskData[prop].filter((label) => !label.deleted); + const labels = (projectsDummyLabelsData[object.project_id] || tasksDummyLabelsData[object.id]) + // only add new labels here + const maxId = Math.max(0, ...labels.map((label) => label.id)); + const newLabels = [...labels, ...taskData.labels.map((label, index) => ( + { ...label, id: maxId + index + 1 } + ))]; + + if (Number.isInteger(object.project_id)) { + projectsDummyLabelsData[object.project_id] = newLabels; + } else { + tasksDummyLabelsData[object.id] = newLabels; + } } else { object[prop] = taskData[prop]; } @@ -216,7 +240,66 @@ class ServerProxy { } } + async function getLabels(filter) { + const { task_id, job_id, project_id } = filter; + if (Number.isInteger(task_id)) { + const object = tasksDummyData.results.find((task) => task.id === task_id); + if (Number.isInteger(object.project_id)) { + return await getLabels({ project_id: object.project_id }); + } + + const results = tasksDummyLabelsData[task_id] || []; + return { results, count: results.length }; + } + + if (Number.isInteger(project_id)) { + const results = projectsDummyLabelsData[project_id] || []; + return { results, count: results.length }; + } + + if (Number.isInteger(job_id)) { + const job = jobsDummyData.results.find((job) => job.id === job_id); + const project = job && Number.isInteger(job.project_id) ? projectsDummyData.results[job.project_id] : undefined; + const task = job ? tasksDummyData.results.find((task) => task.id === job.task_id) : undefined; + + if (project) { + return await getLabels({ project_id: project.id }); + } + + if (task) { + return await getLabels({ task_id: task.id }); + } + } + + return { results: [], count: 0 }; + } + + async function deleteLabel(id) { + const containers = [tasksDummyLabelsData, projectsDummyLabelsData]; + for (const container of containers) { + for (const instanceID in container) { + const index = container[instanceID].findIndex((label) => label.id === id); + if (index !== -1) { + container[instanceID].splice(index, 1); + } + } + } + } + + async function updateLabel(body) { + return body; + } + async function getJobs(filter = {}) { + if (Number.isInteger(filter.id)) { + // A specific object is requested + const results = jobsDummyData.results.filter((job) => job.id === filter.id); + return { + results: results, + count: results.length, + } + } + function makeJsonFilter(jsonExpr) { if (!jsonExpr) { return (job) => true; @@ -235,8 +318,15 @@ class ServerProxy { return (job) => job.task_id === task_id; }; - const id = filter.id || null; - const jobs = jobsDummyData.results.filter(makeJsonFilter(filter.filter || null)); + let jobs = []; + if (Number.isInteger(filter.id)) { + jobs = jobsDummyData.results.filter((job) => job.id === filter.id); + } else if (Number.isInteger(filter.task_id)) { + jobs = jobsDummyData.results.filter((job) => job.task_id === filter.task_id); + } else { + jobs = jobsDummyData.results.filter(makeJsonFilter(filter.filter || null)); + } + for (const job of jobs) { const task = tasksDummyData.results.find((task) => task.id === job.task_id); @@ -248,11 +338,6 @@ class ServerProxy { job.labels = task.labels; } - if (id !== null) { - // A specific object is requested - return jobs.filter((job) => job.id === id)[0] || null; - } - return ( jobs ? { results: jobs, @@ -276,7 +361,7 @@ class ServerProxy { } } - return getJobs({ id }); + return (await getJobs({ id })).results[0]; } async function getUsers() { @@ -523,6 +608,15 @@ class ServerProxy { writable: false, }, + labels: { + value: Object.freeze({ + get: getLabels, + delete: deleteLabel, + update: updateLabel, + }), + writable: false, + }, + jobs: { value: Object.freeze({ get: getJobs, diff --git a/cvat-sdk/cvat_sdk/core/proxies/jobs.py b/cvat-sdk/cvat_sdk/core/proxies/jobs.py index 4c6047edf883..c18af1282212 100644 --- a/cvat-sdk/cvat_sdk/core/proxies/jobs.py +++ b/cvat-sdk/cvat_sdk/core/proxies/jobs.py @@ -151,6 +151,11 @@ def get_meta(self) -> models.IDataMetaRead: (meta, _) = self.api.retrieve_data_meta(self.id) return meta + def get_labels(self) -> List[models.ILabel]: + return get_paginated_collection( + self._client.api_client.labels_api.list_endpoint, job_id=str(self.id) + ) + def get_frames_info(self) -> List[models.IFrameMeta]: return self.get_meta().frames diff --git a/cvat-sdk/cvat_sdk/core/proxies/projects.py b/cvat-sdk/cvat_sdk/core/proxies/projects.py index b8a3764b9945..cacacdd917f7 100644 --- a/cvat-sdk/cvat_sdk/core/proxies/projects.py +++ b/cvat-sdk/cvat_sdk/core/proxies/projects.py @@ -132,6 +132,11 @@ def get_tasks(self) -> List[Task]: ) ] + def get_labels(self) -> List[models.ILabel]: + return get_paginated_collection( + self._client.api_client.labels_api.list_endpoint, project_id=str(self.id) + ) + def get_preview( self, ) -> io.RawIOBase: diff --git a/cvat-sdk/cvat_sdk/core/proxies/tasks.py b/cvat-sdk/cvat_sdk/core/proxies/tasks.py index 46c04612ce72..1c3c926f42d7 100644 --- a/cvat-sdk/cvat_sdk/core/proxies/tasks.py +++ b/cvat-sdk/cvat_sdk/core/proxies/tasks.py @@ -314,6 +314,11 @@ def get_meta(self) -> models.IDataMetaRead: (meta, _) = self.api.retrieve_data_meta(self.id) return meta + def get_labels(self) -> List[models.ILabel]: + return get_paginated_collection( + self._client.api_client.labels_api.list_endpoint, task_id=str(self.id) + ) + def get_frames_info(self) -> List[models.IFrameMeta]: return self.get_meta().frames diff --git a/cvat-sdk/cvat_sdk/pytorch/caching.py b/cvat-sdk/cvat_sdk/pytorch/caching.py index aba0e4b9a6e5..215ab23147cf 100644 --- a/cvat-sdk/cvat_sdk/pytorch/caching.py +++ b/cvat-sdk/cvat_sdk/pytorch/caching.py @@ -151,7 +151,7 @@ def _initialize_task_dir(self, task: Task) -> None: task_json_path = self.task_json_path(task.id) try: - saved_task = self.load_model(task_json_path, models.TaskRead) + saved_task = self.load_model(task_json_path, _OfflineTaskModel) except Exception: self._logger.info(f"Task {task.id} is not yet cached or the cache is corrupted") @@ -166,7 +166,7 @@ def _initialize_task_dir(self, task: Task) -> None: shutil.rmtree(task_dir) task_dir.mkdir(exist_ok=True, parents=True) - self.save_model(task_json_path, task._model) + self.save_model(task_json_path, _OfflineTaskModel.from_entity(task)) def ensure_task_model( self, @@ -224,7 +224,8 @@ def retrieve_project(self, project_id: int) -> Project: class _CacheManagerOffline(CacheManager): def retrieve_task(self, task_id: int) -> Task: self._logger.info(f"Retrieving task {task_id} from cache...") - return Task(self._client, self.load_model(self.task_json_path(task_id), models.TaskRead)) + cached_model = self.load_model(self.task_json_path(task_id), _OfflineTaskModel) + return _OfflineTaskProxy(self._client, cached_model, cache_manager=self) def ensure_task_model( self, @@ -249,27 +250,72 @@ def retrieve_project(self, project_id: int) -> Project: return _OfflineProjectProxy(self._client, cached_model, cache_manager=self) +@define +class _OfflineTaskModel(_CacheObjectModel): + api_model: models.ITaskRead + labels: List[models.ILabel] + + def dump(self) -> _CacheObject: + return { + "model": to_json(self.api_model), + "labels": to_json(self.labels), + } + + @classmethod + def load(cls, obj: _CacheObject): + return cls( + api_model=models.TaskRead._from_openapi_data(**obj["model"]), + labels=[models.Label._from_openapi_data(**label) for label in obj["labels"]], + ) + + @classmethod + def from_entity(cls, entity: Task): + return cls( + api_model=entity._model, + labels=entity.get_labels(), + ) + + +class _OfflineTaskProxy(Task): + def __init__( + self, client: Client, cached_model: _OfflineTaskModel, *, cache_manager: CacheManager + ) -> None: + super().__init__(client, cached_model.api_model) + self._offline_model = cached_model + self._cache_manager = cache_manager + + def get_labels(self) -> List[models.ILabel]: + return self._offline_model.labels + + @define class _OfflineProjectModel(_CacheObjectModel): api_model: models.IProjectRead task_ids: List[int] + labels: List[models.ILabel] def dump(self) -> _CacheObject: return { "model": to_json(self.api_model), "tasks": self.task_ids, + "labels": to_json(self.labels), } @classmethod def load(cls, obj: _CacheObject): return cls( - api_model=obj["model"], + api_model=models.ProjectRead._from_openapi_data(**obj["model"]), task_ids=obj["tasks"], + labels=[models.Label._from_openapi_data(**label) for label in obj["labels"]], ) @classmethod def from_entity(cls, entity: Project): - return cls(api_model=entity._model, task_ids=[t.id for t in entity.get_tasks()]) + return cls( + api_model=entity._model, + task_ids=[t.id for t in entity.get_tasks()], + labels=entity.get_labels(), + ) class _OfflineProjectProxy(Project): @@ -283,6 +329,9 @@ def __init__( def get_tasks(self) -> List[Task]: return [self._cache_manager.retrieve_task(t) for t in self._offline_model.task_ids] + def get_labels(self) -> List[models.ILabel]: + return self._offline_model.labels + _CACHE_MANAGER_CLASSES: Mapping[UpdatePolicy, Type[CacheManager]] = { UpdatePolicy.IF_MISSING_OR_STALE: _CacheManagerOnline, diff --git a/cvat-sdk/cvat_sdk/pytorch/task_dataset.py b/cvat-sdk/cvat_sdk/pytorch/task_dataset.py index aecd6b74bea4..6edd3ec24aa2 100644 --- a/cvat-sdk/cvat_sdk/pytorch/task_dataset.py +++ b/cvat-sdk/cvat_sdk/pytorch/task_dataset.py @@ -132,13 +132,13 @@ def ensure_chunk(chunk_index): { label.id: label_index for label_index, label in enumerate( - sorted(self._task.labels, key=lambda l: l.id) + sorted(self._task.get_labels(), key=lambda l: l.id) ) } ) else: self._label_id_to_index = types.MappingProxyType( - {label.id: label_name_to_index[label.name] for label in self._task.labels} + {label.id: label_name_to_index[label.name] for label in self._task.get_labels()} ) annotations = cache_manager.ensure_task_model( diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 2fbcb2b4c94b..b3bb8b9d73f3 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -26,7 +26,6 @@ import { OpenCVTool, Rotation, ShapeType, - Task, Workspace, } from 'reducers'; import { updateJobAsync } from './tasks-actions'; @@ -190,10 +189,6 @@ export enum AnnotationActionTypes { INTERACT_WITH_CANVAS = 'INTERACT_WITH_CANVAS', GET_DATA_FAILED = 'GET_DATA_FAILED', SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG = 'SET_FORCE_EXIT_ANNOTATION_PAGE_FLAG', - UPDATE_PREDICTOR_STATE = 'UPDATE_PREDICTOR_STATE', - GET_PREDICTIONS = 'GET_PREDICTIONS', - GET_PREDICTIONS_FAILED = 'GET_PREDICTIONS_FAILED', - GET_PREDICTIONS_SUCCESS = 'GET_PREDICTIONS_SUCCESS', SWITCH_NAVIGATION_BLOCKED = 'SWITCH_NAVIGATION_BLOCKED', DELETE_FRAME = 'DELETE_FRAME', DELETE_FRAME_SUCCESS = 'DELETE_FRAME_SUCCESS', @@ -575,86 +570,6 @@ export function switchPlay(playing: boolean): AnyAction { }; } -export function getPredictionsAsync(): ThunkAction { - return async (dispatch: ActionCreator): Promise => { - const { - annotations: { - states: currentStates, - zLayer: { cur: curZOrder }, - }, - predictor: { enabled, annotatedFrames }, - } = getStore().getState().annotation; - - const { - filters, frame, showAllInterpolationTracks, jobInstance: job, - } = receiveAnnotationsParameters(); - if (!enabled || currentStates.length || annotatedFrames.includes(frame)) return; - - dispatch({ - type: AnnotationActionTypes.GET_PREDICTIONS, - payload: {}, - }); - - let annotations = []; - try { - annotations = await job.predictor.predict(frame); - // current frame could be changed during a request above, need to fetch it from store again - const { number: currentFrame } = getStore().getState().annotation.player.frame; - if (frame !== currentFrame || annotations === null) { - // another request has already been sent or user went to another frame - // we do not need dispatch predictions success action - return; - } - annotations = annotations.map( - (data: any): any => new cvat.classes.ObjectState({ - shapeType: data.type, - label: job.labels.filter((label: any): boolean => label.id === data.label)[0], - points: data.points, - objectType: ObjectType.SHAPE, - frame, - occluded: false, - source: 'auto', - attributes: {}, - zOrder: curZOrder, - }), - ); - - dispatch({ - type: AnnotationActionTypes.GET_PREDICTIONS_SUCCESS, - payload: { frame }, - }); - } catch (error) { - dispatch({ - type: AnnotationActionTypes.GET_PREDICTIONS_FAILED, - payload: { - error, - }, - }); - } - - try { - await job.annotations.put(annotations); - const states = await job.annotations.get(frame, showAllInterpolationTracks, filters); - const history = await job.actions.get(); - - dispatch({ - type: AnnotationActionTypes.CREATE_ANNOTATIONS_SUCCESS, - payload: { - states, - history, - }, - }); - } catch (error) { - dispatch({ - type: AnnotationActionTypes.CREATE_ANNOTATIONS_FAILED, - payload: { - error, - }, - }); - } - }; -} - export function confirmCanvasReady(): AnyAction { return { type: AnnotationActionTypes.CONFIRM_CANVAS_READY, @@ -770,7 +685,6 @@ export function changeFrameAsync( delay, }, }); - dispatch(getPredictionsAsync()); } catch (error) { if (error !== 'not needed') { dispatch({ @@ -987,20 +901,8 @@ export function getJobAsync( true, ); - // Check if the task was already downloaded to the state - let job: any | null = null; - const [task] = state.tasks.current - .filter((_task: Task) => _task.id === tid); - 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 }); - } - - // opening correct first frame according to setup + const [job] = await cvat.jobs.get({ jobID: jid }); + // navigate to correct first frame according to setup let frameNumber; if (initialFrame === null && !showDeletedFrames) { frameNumber = (await job.frames.search( @@ -1054,35 +956,6 @@ export function getJobAsync( dispatch(changeWorkspace(workspace)); } - const updatePredictorStatus = async (): Promise => { - // get current job - 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 - return; - } - - try { - const status = await job.predictor.status(); - dispatch({ - type: AnnotationActionTypes.UPDATE_PREDICTOR_STATE, - payload: status, - }); - setTimeout(updatePredictorStatus, 60 * 1000); - } catch (error) { - dispatch({ - type: AnnotationActionTypes.UPDATE_PREDICTOR_STATE, - payload: { error }, - }); - setTimeout(updatePredictorStatus, 20 * 1000); - } - }; - - if (state.plugins.list.PREDICT && job.projectId !== null) { - updatePredictorStatus(); - } - dispatch(changeFrameAsync(frameNumber, false)); } catch (error) { dispatch({ @@ -1641,15 +1514,6 @@ export function setForceExitAnnotationFlag(forceExit: boolean): AnyAction { }; } -export function switchPredictor(predictorEnabled: boolean): AnyAction { - return { - type: AnnotationActionTypes.UPDATE_PREDICTOR_STATE, - payload: { - enabled: predictorEnabled, - }, - }; -} - export function switchNavigationBlocked(navigationBlocked: boolean): AnyAction { return { type: AnnotationActionTypes.SWITCH_NAVIGATION_BLOCKED, diff --git a/cvat-ui/src/actions/projects-actions.ts b/cvat-ui/src/actions/projects-actions.ts index b62523e660cc..db3cf1f396ff 100644 --- a/cvat-ui/src/actions/projects-actions.ts +++ b/cvat-ui/src/actions/projects-actions.ts @@ -24,9 +24,6 @@ export enum ProjectsActionTypes { CREATE_PROJECT = 'CREATE_PROJECT', CREATE_PROJECT_SUCCESS = 'CREATE_PROJECT_SUCCESS', CREATE_PROJECT_FAILED = 'CREATE_PROJECT_FAILED', - UPDATE_PROJECT = 'UPDATE_PROJECT', - UPDATE_PROJECT_SUCCESS = 'UPDATE_PROJECT_SUCCESS', - UPDATE_PROJECT_FAILED = 'UPDATE_PROJECT_FAILED', DELETE_PROJECT = 'DELETE_PROJECT', DELETE_PROJECT_SUCCESS = 'DELETE_PROJECT_SUCCESS', DELETE_PROJECT_FAILED = 'DELETE_PROJECT_FAILED', @@ -50,11 +47,6 @@ const projectActions = { createAction(ProjectsActionTypes.CREATE_PROJECT_SUCCESS, { projectId }) ), createProjectFailed: (error: any) => createAction(ProjectsActionTypes.CREATE_PROJECT_FAILED, { error }), - updateProject: () => createAction(ProjectsActionTypes.UPDATE_PROJECT), - updateProjectSuccess: (project: any) => createAction(ProjectsActionTypes.UPDATE_PROJECT_SUCCESS, { project }), - updateProjectFailed: (project: any, error: any) => ( - createAction(ProjectsActionTypes.UPDATE_PROJECT_FAILED, { project, error }) - ), deleteProject: (projectId: number) => createAction(ProjectsActionTypes.DELETE_PROJECT, { projectId }), deleteProjectSuccess: (projectId: number) => ( createAction(ProjectsActionTypes.DELETE_PROJECT_SUCCESS, { projectId }) @@ -143,28 +135,6 @@ export function createProjectAsync(data: any): ThunkAction { }; } -export function updateProjectAsync(projectInstance: any): ThunkAction { - return async (dispatch, getState): Promise => { - try { - const state = getState(); - dispatch(projectActions.updateProject()); - await projectInstance.save(); - const [project] = await cvat.projects.get({ id: projectInstance.id }); - dispatch(projectActions.updateProjectSuccess(project)); - dispatch(getProjectTasksAsync(state.projects.tasksGettingQuery)); - } catch (error) { - let project = null; - try { - [project] = await cvat.projects.get({ id: projectInstance.id }); - } catch (fetchError) { - dispatch(projectActions.updateProjectFailed(projectInstance, error)); - return; - } - dispatch(projectActions.updateProjectFailed(project, error)); - } - }; -} - export function deleteProjectAsync(projectInstance: any): ThunkAction { return async (dispatch: ActionCreator): Promise => { dispatch(projectActions.deleteProject(projectInstance.id)); diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index ee6d8d805a1c..c8681a5dc61a 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -5,9 +5,7 @@ import { AnyAction, Dispatch, ActionCreator } from 'redux'; import { ThunkAction } from 'redux-thunk'; -import { - TasksQuery, CombinedState, StorageLocation, -} from 'reducers'; +import { TasksQuery, StorageLocation } from 'reducers'; import { getCore, Storage } from 'cvat-core-wrapper'; import { filterNull } from 'utils/filter-null'; import { getInferenceStatusAsync } from './models-actions'; @@ -22,11 +20,6 @@ export enum TasksActionTypes { DELETE_TASK_SUCCESS = 'DELETE_TASK_SUCCESS', DELETE_TASK_FAILED = 'DELETE_TASK_FAILED', CREATE_TASK_FAILED = 'CREATE_TASK_FAILED', - 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', SWITCH_MOVE_TASK_MODAL_VISIBLE = 'SWITCH_MOVE_TASK_MODAL_VISIBLE', @@ -172,25 +165,25 @@ ThunkAction, {}, {}, AnyAction> { description.bug_tracker = data.advanced.bugTracker; } if (data.advanced.segmentSize) { - description.segment_size = data.advanced.segmentSize; + description.segment_size = +data.advanced.segmentSize; } if (data.advanced.overlapSize) { description.overlap = data.advanced.overlapSize; } if (data.advanced.startFrame) { - description.start_frame = data.advanced.startFrame; + description.start_frame = +data.advanced.startFrame; } if (data.advanced.stopFrame) { - description.stop_frame = data.advanced.stopFrame; + description.stop_frame = +data.advanced.stopFrame; } if (data.advanced.frameFilter) { description.frame_filter = data.advanced.frameFilter; } if (data.advanced.imageQuality) { - description.image_quality = data.advanced.imageQuality; + description.image_quality = +data.advanced.imageQuality; } if (data.advanced.dataChunkSize) { - description.data_chunk_size = data.advanced.dataChunkSize; + description.data_chunk_size = +data.advanced.dataChunkSize; } if (data.advanced.copyData) { description.copy_data = data.advanced.copyData; @@ -233,51 +226,6 @@ ThunkAction, {}, {}, AnyAction> { }; } -function updateTask(): AnyAction { - const action = { - type: TasksActionTypes.UPDATE_TASK, - payload: {}, - }; - - return action; -} - -export function updateTaskSuccess(task: any, taskID: number): AnyAction { - const action = { - type: TasksActionTypes.UPDATE_TASK_SUCCESS, - payload: { task, taskID }, - }; - - return action; -} - -function updateTaskFailed(error: any, task: any): AnyAction { - const action = { - type: TasksActionTypes.UPDATE_TASK_FAILED, - payload: { error, task }, - }; - - return action; -} - -function updateJob(jobID: number): AnyAction { - const action = { - type: TasksActionTypes.UPDATE_JOB, - payload: { jobID }, - }; - - return action; -} - -function updateJobSuccess(jobInstance: any, jobID: number): AnyAction { - const action = { - type: TasksActionTypes.UPDATE_JOB_SUCCESS, - payload: { jobID, jobInstance }, - }; - - return action; -} - function updateJobFailed(jobID: number, error: any): AnyAction { const action = { type: TasksActionTypes.UPDATE_JOB_FAILED, @@ -287,35 +235,10 @@ function updateJobFailed(jobID: number, error: any): AnyAction { return action; } -export function updateTaskAsync(taskInstance: any): ThunkAction, CombinedState, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { - try { - dispatch(updateTask()); - const task = await taskInstance.save(); - dispatch(updateTaskSuccess(task, taskInstance.id)); - } catch (error) { - // try abort all changes - let task = null; - try { - [task] = await cvat.tasks.get({ id: taskInstance.id }); - } catch (fetchError) { - dispatch(updateTaskFailed(error, taskInstance)); - return; - } - - dispatch(updateTaskFailed(error, task)); - } - }; -} - -// a job is a part of a task, so for simplify we consider -// updating the job as updating a task export function updateJobAsync(jobInstance: any): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { try { - dispatch(updateJob(jobInstance.id)); - const newJob = await jobInstance.save(); - dispatch(updateJobSuccess(newJob, newJob.id)); + await jobInstance.save(); } catch (error) { dispatch(updateJobFailed(jobInstance.id, error)); } @@ -345,37 +268,6 @@ export function switchMoveTaskModalVisible(visible: boolean, taskId: number | nu return action; } -interface LabelMap { - label_id: number; - new_label_name: string | null; - clear_attributes: boolean; -} - -export function moveTaskToProjectAsync( - taskInstance: any, - projectId: any, - labelMap: LabelMap[], -): ThunkAction, {}, {}, AnyAction> { - return async (dispatch: ActionCreator): Promise => { - dispatch(updateTask()); - try { - // eslint-disable-next-line no-param-reassign - taskInstance.labels = labelMap.map((mapper) => { - const [label] = taskInstance.labels.filter((_label: any) => mapper.label_id === _label.id); - label.name = mapper.new_label_name; - return label; - }); - // eslint-disable-next-line no-param-reassign - taskInstance.projectId = projectId; - await taskInstance.save(); - const [task] = await cvat.tasks.get({ id: taskInstance.id }); - dispatch(updateTaskSuccess(task, task.id)); - } catch (error) { - dispatch(updateTaskFailed(error, taskInstance)); - } - }; -} - function getTaskPreview(taskID: number): AnyAction { const action = { type: TasksActionTypes.GET_TASK_PREVIEW, diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index e8ff662cd9d8..90136383219f 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -107,7 +107,7 @@ export default function AnnotationPageComponent(props: Props): JSX.Element { {`${job.projectId ? 'Project' : 'Task'} ${ job.projectId || job.taskId } does not contain any label. `} - + Add {' the first one for editing annotation.'} diff --git a/cvat-ui/src/components/annotation-page/styles.scss b/cvat-ui/src/components/annotation-page/styles.scss index f776f43bd766..f88d90fa6912 100644 --- a/cvat-ui/src/components/annotation-page/styles.scss +++ b/cvat-ui/src/components/annotation-page/styles.scss @@ -77,52 +77,6 @@ } } -button.cvat-predictor-button { - &.cvat-predictor-inprogress { - > span { - > svg { - fill: $inprogress-progress-color; - } - } - } - - &.cvat-predictor-fetching { - > span { - > svg { - animation-duration: 500ms; - animation-name: predictorBlinking; - animation-iteration-count: infinite; - - @keyframes predictorBlinking { - 0% { - fill: $inprogress-progress-color; - } - - 50% { - fill: $completed-progress-color; - } - - 100% { - fill: $inprogress-progress-color; - } - } - } - } - } - - &.cvat-predictor-disabled { - opacity: 0.5; - - &:active { - pointer-events: none; - } - - > span[role='img'] { - transform: scale(0.8) !important; - } - } -} - .cvat-annotation-disabled-header-button { @extend .cvat-annotation-header-button; diff --git a/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx b/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx index de0363b1581b..8016fba1fbd5 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/right-group.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2023 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -7,29 +8,18 @@ import { Col } from 'antd/lib/grid'; import Icon from '@ant-design/icons'; import Select from 'antd/lib/select'; import Button from 'antd/lib/button'; -import Text from 'antd/lib/typography/Text'; -import Tooltip from 'antd/lib/tooltip'; -import Moment from 'react-moment'; - -import moment from 'moment'; import { useSelector } from 'react-redux'; +import { FilterIcon, FullscreenIcon, InfoIcon } from 'icons'; import { - FilterIcon, FullscreenIcon, InfoIcon, BrainIcon, -} from 'icons'; -import { - CombinedState, DimensionType, Workspace, PredictorState, + CombinedState, DimensionType, Workspace, } from 'reducers'; interface Props { workspace: Workspace; - predictor: PredictorState; - isTrainingActive: boolean; showStatistics(): void; - switchPredictor(predictorEnabled: boolean): void; showFilters(): void; changeWorkspace(workspace: Workspace): void; - jobInstance: any; } @@ -37,108 +27,15 @@ function RightGroup(props: Props): JSX.Element { const { showStatistics, changeWorkspace, - switchPredictor, workspace, - predictor, jobInstance, - isTrainingActive, showFilters, } = props; - const annotationAmount = predictor.annotationAmount || 0; - const mediaAmount = predictor.mediaAmount || 0; - const formattedScore = `${(predictor.projectScore * 100).toFixed(0)}%`; - const predictorTooltip = ( -
- Adaptive auto annotation is - {predictor.enabled ? ( - - {' active'} - - ) : ( - - {' inactive'} - - )} -
- - Annotations amount: - {annotationAmount} - -
- - Media amount: - {mediaAmount} - -
- {annotationAmount > 0 ? ( - - Model mAP is - {' '} - {formattedScore} -
-
- ) : null} - {predictor.error ? ( - - {predictor.error.toString()} -
-
- ) : null} - {predictor.message ? ( - - Status: - {' '} - {predictor.message} -
-
- ) : null} - {predictor.timeRemaining > 0 ? ( - - Time Remaining: - {' '} - -
-
- ) : null} - {predictor.progress > 0 ? ( - - Progress: - {predictor.progress.toFixed(1)} - {' '} - % - - ) : null} -
- ); - - let predictorClassName = 'cvat-annotation-header-button cvat-predictor-button'; - if (!!predictor.error || !predictor.projectScore) { - predictorClassName += ' cvat-predictor-disabled'; - } else if (predictor.enabled) { - if (predictor.fetching) { - predictorClassName += ' cvat-predictor-fetching'; - } - predictorClassName += ' cvat-predictor-inprogress'; - } const filters = useSelector((state: CombinedState) => state.annotation.annotations.filters); return ( - {isTrainingActive && ( - - )}