CVAT.js: Save and delete for shapes/tracks/tags (#555)

main
Boris Sekachev 7 years ago committed by Nikita Manovich
parent 5104cc08c1
commit 22bcf1cf84

@ -48,5 +48,6 @@
"indent": ["warn", 4],
"no-useless-constructor": 0,
"func-names": [0],
"valid-typeof": [0],
},
};

@ -11,17 +11,85 @@
const serverProxy = require('./server-proxy');
const ObjectState = require('./object-state');
function objectStateFactory(frame, data) {
const objectState = new ObjectState(data);
// Rewrite default implementations of save/delete
objectState.updateInCollection = this.save.bind(this, frame, objectState);
objectState.deleteFromCollection = this.delete.bind(this);
return objectState;
}
function checkObjectType(name, value, type, instance) {
if (type) {
if (typeof (value) !== type) {
// specific case for integers which aren't native type in JS
if (type === 'integer' && Number.isInteger(value)) {
return;
}
if (value !== undefined) {
throw new window.cvat.exceptions.ArgumentError(
`Got ${typeof (value)} value for ${name}. `
+ `Expected ${type}`,
);
}
throw new window.cvat.exceptions.ArgumentError(
`Got undefined value for ${name}. `
+ `Expected ${type}`,
);
}
} else if (instance) {
if (!(value instanceof instance)) {
if (value !== undefined) {
throw new window.cvat.exceptions.ArgumentError(
`Got ${value.constructor.name} value for ${name}. `
+ `Expected instance of ${instance.name}`,
);
}
throw new window.cvat.exceptions.ArgumentError(
`Got undefined value for ${name}. `
+ `Expected instance of ${instance.name}`,
);
}
}
}
class Annotation {
constructor(data, clientID, injection) {
this.taskLabels = injection.labels;
this.clientID = clientID;
this.serverID = data.id;
this.labelID = data.label_id;
this.group = data.group;
this.label = this.taskLabels[data.label_id];
this.frame = data.frame;
this.removed = false;
this.lock = false;
this.attributes = data.attributes.reduce((attributeAccumulator, attr) => {
attributeAccumulator[attr.spec_id] = attr.value;
return attributeAccumulator;
}, {});
this.taskLabels = injection.labels;
this.appendDefaultAttributes(this.label);
}
appendDefaultAttributes(label) {
const labelAttributes = label.attributes;
for (const attribute of labelAttributes) {
if (!(attribute.id in this.attributes)) {
this.attributes[attribute.id] = attribute.defaultValue;
}
}
}
delete(force) {
if (!this.lock || force) {
this.removed = true;
}
return true;
}
}
@ -31,11 +99,11 @@
this.points = data.points;
this.occluded = data.occluded;
this.zOrder = data.z_order;
this.group = data.group;
this.color = color;
this.shape = null;
}
// Method is used to export data to the server
toJSON() {
return {
occluded: this.occluded,
@ -51,11 +119,12 @@
}, []),
id: this.serverID,
frame: this.frame,
label_id: this.labelID,
label_id: this.label.id,
group: this.group,
};
}
// Method is used to construct ObjectState objects
get(frame) {
if (frame !== this.frame) {
throw new window.cvat.exceptions.ScriptingError(
@ -68,13 +137,99 @@
shape: this.shape,
clientID: this.clientID,
occluded: this.occluded,
lock: this.lock,
zOrder: this.zOrder,
points: [...this.points],
attributes: Object.assign({}, this.attributes),
label: this.taskLabels[this.labelID],
label: this.label,
group: this.group,
color: this.color,
};
}
save(frame, data) {
if (frame !== this.frame) {
throw new window.cvat.exceptions.ScriptingError(
'Got frame is not equal to the frame of the shape',
);
}
if (this.lock && data.lock) {
return objectStateFactory.call(this, frame, this.get(frame));
}
// All changes are done in this temporary object
const copy = this.get(frame);
const updated = data.updateFlags;
if (updated.label) {
checkObjectType('label', data.label, null, window.cvat.classes.Label);
copy.label = data.label;
copy.attributes = {};
this.appendDefaultAttributes.call(copy, copy.label);
}
if (updated.attributes) {
const labelAttributes = copy.label
.attributes.map(attr => `${attr.id}`);
for (const attrID of Object.keys(data.attributes)) {
if (labelAttributes.includes(attrID)) {
copy.attributes[attrID] = data.attributes[attrID];
}
}
}
if (updated.points) {
checkObjectType('points', data.points, null, Array);
copy.points = [];
for (const coordinate of data.points) {
checkObjectType('coordinate', coordinate, 'number', null);
copy.points.push(coordinate);
}
}
if (updated.occluded) {
checkObjectType('occluded', data.occluded, 'boolean', null);
copy.occluded = data.occluded;
}
if (updated.group) {
checkObjectType('group', data.group, 'integer', null);
copy.group = data.group;
}
if (updated.zOrder) {
checkObjectType('zOrder', data.zOrder, 'integer', null);
copy.zOrder = data.zOrder;
}
if (updated.lock) {
checkObjectType('lock', data.lock, 'boolean', null);
copy.lock = data.lock;
}
if (updated.color) {
checkObjectType('color', data.color, 'string', null);
if (/^#[0-9A-F]{6}$/i.test(data.color)) {
throw new window.cvat.exceptions.ArgumentError(
`Got invalid color value: "${data.color}"`,
);
}
copy.color = data.color;
}
// Reset flags and commit all changes
updated.reset();
for (const prop of Object.keys(copy)) {
if (prop in this) {
this[prop] = copy[prop];
}
}
return objectStateFactory.call(this, frame, this.get(frame));
}
}
class Track extends Annotation {
@ -86,7 +241,6 @@
occluded: value.occluded,
zOrder: value.z_order,
points: value.points,
id: value.id,
frame: value.frame,
outside: value.outside,
attributes: value.attributes.reduce((attributeAccumulator, attr) => {
@ -98,15 +252,17 @@
return shapeAccumulator;
}, {});
this.group = data.group;
this.attributes = data.attributes.reduce((attributeAccumulator, attr) => {
attributeAccumulator[attr.spec_id] = attr.value;
return attributeAccumulator;
}, {});
this.cache = {};
this.color = color;
this.shape = null;
}
// Method is used to export data to the server
toJSON() {
return {
occluded: this.occluded,
@ -123,7 +279,7 @@
id: this.serverID,
frame: this.frame,
label_id: this.labelID,
label_id: this.label.id,
group: this.group,
shapes: Object.keys(this.shapes).reduce((shapesAccumulator, frame) => {
shapesAccumulator.push({
@ -150,18 +306,27 @@
};
}
get(targetFrame) {
return Object.assign(
{}, this.getPosition(targetFrame),
{
attributes: this.getAttributes(targetFrame),
label: this.taskLabels[this.labelID],
group: this.group,
type: window.cvat.enums.ObjectType.TRACK,
shape: this.shape,
clientID: this.clientID,
},
);
// Method is used to construct ObjectState objects
get(frame) {
if (!(frame in this.cache)) {
const interpolation = Object.assign(
{}, this.getPosition(frame),
{
attributes: this.getAttributes(frame),
label: this.label,
group: this.group,
type: window.cvat.enums.ObjectType.TRACK,
shape: this.shape,
clientID: this.clientID,
lock: this.lock,
color: this.color,
},
);
this.cache[frame] = interpolation;
}
return JSON.parse(JSON.stringify(this.cache[frame]));
}
neighborsFrames(targetFrame) {
@ -211,20 +376,173 @@
}
}
// Finally fill up remained attributes if they exist
const labelAttributes = this.taskLabels[this.labelID].attributes;
const defValuesByID = labelAttributes.reduce((accumulator, attr) => {
accumulator[attr.id] = attr.defaultValue;
return accumulator;
}, {});
return result;
}
save(frame, data) {
if (this.lock || data.lock) {
this.lock = data.lock;
return objectStateFactory.call(this, frame, this.get(frame));
}
// All changes are done in this temporary object
const copy = Object.assign(this.get(frame));
copy.attributes = Object.assign(copy.attributes);
copy.points = [...copy.points];
const updated = data.updateFlags;
let positionUpdated = false;
if (updated.label) {
checkObjectType('label', data.label, null, window.cvat.classes.Label);
copy.label = data.label;
copy.attributes = {};
for (const attrID of Object.keys(defValuesByID)) {
if (!(attrID in result)) {
result[attrID] = defValuesByID[attrID];
// Shape attributes will be removed later after all checks
this.appendDefaultAttributes.call(copy, copy.label);
}
if (updated.attributes) {
const labelAttributes = copy.label.attributes
.reduce((accumulator, value) => {
accumulator[value.id] = value;
return accumulator;
}, {});
for (const attrID of Object.keys(data.attributes)) {
if (attrID in labelAttributes) {
copy.attributes[attrID] = data.attributes[attrID];
if (!labelAttributes[attrID].mutable) {
this.attributes[attrID] = data.attributes[attrID];
} else {
// Mutable attributes will be updated later
positionUpdated = true;
}
}
}
}
return result;
if (updated.points) {
checkObjectType('points', data.points, null, Array);
copy.points = [];
for (const coordinate of data.points) {
checkObjectType('coordinate', coordinate, 'number', null);
copy.points.push(coordinate);
}
positionUpdated = true;
}
if (updated.occluded) {
checkObjectType('occluded', data.occluded, 'boolean', null);
copy.occluded = data.occluded;
positionUpdated = true;
}
if (updated.outside) {
checkObjectType('outside', data.outside, 'boolean', null);
copy.outside = data.outside;
positionUpdated = true;
}
if (updated.group) {
checkObjectType('group', data.group, 'integer', null);
copy.group = data.group;
}
if (updated.zOrder) {
checkObjectType('zOrder', data.zOrder, 'integer', null);
copy.zOrder = data.zOrder;
positionUpdated = true;
}
if (updated.lock) {
checkObjectType('lock', data.lock, 'boolean', null);
copy.lock = data.lock;
}
if (updated.color) {
checkObjectType('color', data.color, 'string', null);
if (/^#[0-9A-F]{6}$/i.test(data.color)) {
throw new window.cvat.exceptions.ArgumentError(
`Got invalid color value: "${data.color}"`,
);
}
copy.color = data.color;
}
// Commit all changes
for (const prop of Object.keys(copy)) {
if (prop in this) {
this[prop] = copy[prop];
}
this.cache[frame][prop] = copy[prop];
}
if (updated.label) {
for (const shape of this.shapes) {
shape.attributes = {};
}
}
// Remove keyframe
if (updated.keyframe && !data.keyframe) {
// Remove all cache after this keyframe because it have just become outdated
for (const cacheFrame in this.cache) {
if (+cacheFrame > frame) {
delete this.cache[frame];
}
}
this.cache[frame].keyframe = false;
delete this.shapes[frame];
updated.reset();
return objectStateFactory.call(this, frame, this.get(frame));
}
// Add/update keyframe
if (positionUpdated || (updated.keyframe && data.keyframe)) {
// Remove all cache after this keyframe because it have just become outdated
for (const cacheFrame in this.cache) {
if (+cacheFrame > frame) {
delete this.cache[frame];
}
}
this.cache[frame].keyframe = true;
data.keyframe = true;
this.shapes[frame] = {
frame,
zOrder: copy.zOrder,
points: copy.points,
outside: copy.outside,
occluded: copy.occluded,
attributes: {},
};
if (updated.attributes) {
const labelAttributes = this.label.attributes
.reduce((accumulator, value) => {
accumulator[value.id] = value;
return accumulator;
}, {});
// Unmutable attributes were updated above
for (const attrID of Object.keys(data.attributes)) {
if (attrID in labelAttributes && labelAttributes[attrID].mutable) {
this.shapes[frame].attributes[attrID] = data.attributes[attrID];
this.shapes[frame].attributes[attrID] = data.attributes[attrID];
}
}
}
}
updated.reset();
return objectStateFactory.call(this, frame, this.get(frame));
}
getPosition(targetFrame) {
@ -242,15 +560,18 @@
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: leftPosition.zOrder,
keyframe: true,
};
}
if (rightPosition && leftPosition) {
return this.interpolatePosition(
return Object.assign({}, this.interpolatePosition(
leftPosition,
rightPosition,
targetFrame,
);
), {
keyframe: false,
});
}
if (rightPosition) {
@ -259,6 +580,7 @@
occluded: rightPosition.occluded,
outside: true,
zOrder: 0,
keyframe: false,
};
}
@ -268,6 +590,7 @@
occluded: leftPosition.occluded,
outside: leftPosition.outside,
zOrder: 0,
keyframe: false,
};
}
@ -282,17 +605,94 @@
super(data, clientID, injection);
}
// Method is used to export data to the server
toJSON() {
// TODO: Tags support
return {};
return {
id: this.serverID,
frame: this.frame,
label_id: this.label.id,
group: this.group,
attributes: Object.keys(this.attributes).reduce((attributeAccumulator, attrId) => {
attributeAccumulator.push({
spec_id: attrId,
value: this.attributes[attrId],
});
return attributeAccumulator;
}, []),
};
}
// Method is used to construct ObjectState objects
get(frame) {
if (frame !== this.frame) {
throw new window.cvat.exceptions.ScriptingError(
'Got frame is not equal to the frame of the shape',
);
}
return {
type: window.cvat.enums.ObjectType.TAG,
clientID: this.clientID,
lock: this.lock,
attributes: Object.assign({}, this.attributes),
label: this.label,
group: this.group,
};
}
save(frame, data) {
if (frame !== this.frame) {
throw new window.cvat.exceptions.ScriptingError(
'Got frame is not equal to the frame of the shape',
);
}
if (this.lock && data.lock) {
return objectStateFactory.call(this, frame, this.get(frame));
}
// All changes are done in this temporary object
const copy = this.get(frame);
const updated = data.updateFlags;
if (updated.label) {
checkObjectType('label', data.label, null, window.cvat.classes.Label);
copy.label = data.label;
copy.attributes = {};
this.appendDefaultAttributes.call(copy, copy.label);
}
if (updated.attributes) {
const labelAttributes = copy.label
.attributes.map(attr => `${attr.id}`);
for (const attrID of Object.keys(data.attributes)) {
if (labelAttributes.includes(attrID)) {
copy.attributes[attrID] = data.attributes[attrID];
}
}
}
if (updated.group) {
checkObjectType('group', data.group, 'integer', null);
copy.group = data.group;
}
if (updated.lock) {
checkObjectType('lock', data.lock, 'boolean', null);
copy.lock = data.lock;
}
// Reset flags and commit all changes
updated.reset();
for (const prop of Object.keys(copy)) {
if (prop in this) {
this[prop] = copy[prop];
}
}
return objectStateFactory.call(this, frame, this.get(frame));
}
}
@ -915,15 +1315,12 @@
const shapes = this.shapes[frame] || [];
const tags = this.tags[frame] || [];
const states = tracks.map(track => track.get(frame))
.concat(shapes.map(shape => shape.get(frame)))
.concat(tags.map(tag => tag.get(frame)));
const objects = tracks.concat(shapes).concat(tags).filter(object => !object.removed);
// filtering here
const objectStates = [];
for (const state of states) {
const objectState = new ObjectState(state);
for (const object of objects) {
const objectState = objectStateFactory.call(object, frame, object.get(frame));
objectStates.push(objectState);
}

@ -116,7 +116,7 @@
if ('taskID' in filter) {
task = await serverProxy.tasks.getTasks(`id=${filter.taskID}`);
} else {
const [job] = await serverProxy.jobs.getJob(filter.jobID);
const job = await serverProxy.jobs.getJob(filter.jobID);
task = await serverProxy.tasks.getTasks(`id=${job.task_id}`);
}

@ -16,25 +16,69 @@
*/
class ObjectState {
/**
* @param {Object} type - an object which contains initialization information
* about points, group, zOrder, outside, occluded,
* attributes, lock, type, label, mode, etc.
* Types of data equal to listed below
* @param {Object} serialized - is an dictionary which contains
* initial information about an ObjectState;
* Necessary fields: type, shape
* Necessary fields for objects which haven't been added to collection yet: frame
* Optional fields: points, group, zOrder, outside, occluded,
* attributes, lock, label, mode, color, keyframe
* These fields can be set later via setters
*/
constructor(serialized) {
const data = {
label: null,
attributes: {},
points: null,
group: null,
zOrder: null,
outside: null,
occluded: null,
keyframe: null,
group: null,
zOrder: null,
lock: null,
attributes: {},
color: null,
frame: serialized.frame,
type: serialized.type,
shape: serialized.shape,
updateFlags: {},
};
// Shows whether any properties updated since last reset() or interpolation
Object.defineProperty(data.updateFlags, 'reset', {
value: function reset() {
this.label = false;
this.attributes = false;
this.points = false;
this.outside = false;
this.occluded = false;
this.keyframe = false;
this.group = false;
this.zOrder = false;
this.lock = false;
this.color = false;
},
writable: false,
});
Object.defineProperties(this, Object.freeze({
// Internal property. We don't need document it.
updateFlags: {
get: () => data.updateFlags,
},
frame: {
/**
* @name frame
* @type {integer}
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
*/
get: () => data.frame,
},
type: {
/**
* @name type
@ -61,51 +105,37 @@
* @type {module:API.cvat.classes.Label}
* @memberof module:API.cvat.classes.ObjectState
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
get: () => data.label,
set: (labelInstance) => {
if (!(labelInstance instanceof window.cvat.classes.Label)) {
throw new window.cvat.exceptions.ArgumentError(
`Expected Label instance, but got "${typeof (labelInstance.constructor.name)}"`,
);
}
data.updateFlags.label = true;
data.label = labelInstance;
},
},
points: {
color: {
/**
* @typedef {Object} Point
* @property {number} x
* @property {number} y
* @global
* @name color
* @type {string}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.color,
set: (color) => {
data.updateFlags.color = true;
data.color = color;
},
},
points: {
/**
* @name points
* @type {Point[]}
* @type {number[]}
* @memberof module:API.cvat.classes.ObjectState
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
get: () => data.position,
set: (position) => {
if (Array.isArray(position)) {
for (const point of position) {
if (typeof (point) !== 'object'
|| !('x' in point) || !('y' in point)) {
throw new window.cvat.exceptions.ArgumentError(
`Got invalid point ${point}`,
);
}
}
} else {
throw new window.cvat.exceptions.ArgumentError(
`Got invalid type "${typeof (position.constructor.name)}"`,
);
}
data.position = position;
get: () => data.points,
set: (points) => {
data.updateFlags.points = true;
data.points = [...points];
},
},
group: {
@ -114,17 +144,11 @@
* @type {integer}
* @memberof module:API.cvat.classes.ObjectState
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
get: () => data.group,
set: (groupID) => {
if (!Number.isInteger(groupID)) {
throw new window.cvat.exceptions.ArgumentError(
`Expected integer, but got ${groupID.constructor.name}`,
);
}
data.group = groupID;
set: (group) => {
data.updateFlags.group = true;
data.group = group;
},
},
zOrder: {
@ -133,16 +157,10 @@
* @type {integer}
* @memberof module:API.cvat.classes.ObjectState
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
get: () => data.zOrder,
set: (zOrder) => {
if (!Number.isInteger(zOrder)) {
throw new window.cvat.exceptions.ArgumentError(
`Expected integer, but got ${zOrder.constructor.name}`,
);
}
data.updateFlags.zOrder = true;
data.zOrder = zOrder;
},
},
@ -152,35 +170,36 @@
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
get: () => data.outside,
set: (outside) => {
if (typeof (outside) !== 'boolean') {
throw new window.cvat.exceptions.ArgumentError(
`Expected boolean, but got ${outside.constructor.name}`,
);
}
data.updateFlags.outside = true;
data.outside = outside;
},
},
keyframe: {
/**
* @name keyframe
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
*/
get: () => data.keyframe,
set: (keyframe) => {
data.updateFlags.keyframe = true;
data.keyframe = keyframe;
},
},
occluded: {
/**
* @name occluded
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
get: () => data.occluded,
set: (occluded) => {
if (typeof (occluded) !== 'boolean') {
throw new window.cvat.exceptions.ArgumentError(
`Expected boolean, but got ${occluded.constructor.name}`,
);
}
data.updateFlags.occluded = true;
data.occluded = occluded;
},
},
@ -190,16 +209,10 @@
* @type {boolean}
* @memberof module:API.cvat.classes.ObjectState
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
get: () => data.lock,
set: (lock) => {
if (typeof (lock) !== 'boolean') {
throw new window.cvat.exceptions.ArgumentError(
`Expected boolean, but got ${lock.constructor.name}`,
);
}
data.updateFlags.lock = true;
data.lock = lock;
},
},
@ -210,60 +223,55 @@
* @name attributes
* @type {Object}
* @memberof module:API.cvat.classes.ObjectState
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
*/
get: () => data.attributes,
set: (attributes) => {
if (typeof (attributes) !== 'object') {
if (typeof (attributes) === 'undefined') {
throw new window.cvat.exceptions.ArgumentError(
'Expected attributes are object, but got undefined',
);
}
throw new window.cvat.exceptions.ArgumentError(
`Expected object, but got ${attributes.constructor.name}`,
`Expected attributes are object, but got ${attributes.constructor.name}`,
);
}
for (let attrId in attributes) {
if (Object.prototype.hasOwnProperty.call(attributes, attrId)) {
attrId = +attrId;
if (!Number.isInteger(attrId)) {
throw new window.cvat.exceptions.ArgumentError(
`Expected integer attribute id, but got ${attrId.constructor.name}`,
);
}
data.attributes[attrId] = attributes[attrId];
}
for (const attrID of Object.keys(attributes)) {
data.updateFlags.attributes = true;
data.attributes[attrID] = attributes[attrID];
}
},
},
}));
this.label = serialized.label;
this.group = serialized.group;
this.zOrder = serialized.zOrder;
this.outside = serialized.outside;
this.keyframe = serialized.keyframe;
this.occluded = serialized.occluded;
this.attributes = serialized.attributes;
this.lock = false;
this.points = serialized.points;
this.color = serialized.color;
this.lock = serialized.lock;
const points = [];
for (let i = 0; i < serialized.points.length; i += 2) {
points.push({
x: serialized.points[i],
y: serialized.points[i + 1],
});
}
this.points = points;
data.updateFlags.reset();
}
/**
* Method saves object state in a collection
* Method saves/updates an object state in a collection
* @method save
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @returns {module:API.cvat.classes.ObjectState} updated state of an object
*/
async save() {
const result = await PluginRegistry
@ -272,21 +280,41 @@
}
/**
* Method deletes object from a collection
* Method deletes an object from a collection
* @method delete
* @memberof module:API.cvat.classes.ObjectState
* @readonly
* @instance
* @param {boolean} [force=false] delete object even if it is locked
* @async
* @returns {boolean} wheter object was deleted
* @throws {module:API.cvat.exceptions.PluginError}
*/
async delete() {
async delete(force = false) {
const result = await PluginRegistry
.apiWrapper.call(this, ObjectState.prototype.delete);
.apiWrapper.call(this, ObjectState.prototype.delete, force);
return result;
}
}
// Default implementation saves element in collection
ObjectState.prototype.save.implementation = async function () {
if (this.updateInCollection) {
return this.updateInCollection();
}
return this;
};
// Default implementation do nothing
ObjectState.prototype.delete.implementation = async function (force) {
if (this.deleteFromCollection) {
return this.deleteFromCollection(force);
}
return false;
};
module.exports = ObjectState;
})();

@ -179,10 +179,10 @@
}
async function logout() {
const { backendAPI } = window.cvat.config;
const host = window.cvat.config.backendAPI.slice(0, -7);
try {
await Axios.get(`${backendAPI}/auth/logout`, {
await Axios.get(`${host}/auth/logout`, {
proxy: window.cvat.config.proxy,
});
} catch (errorData) {

Loading…
Cancel
Save