React UI: Added logging (#1288)

main
Boris Sekachev 6 years ago committed by GitHub
parent 731b8967c0
commit bfd300039e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -37,6 +37,19 @@ Canvas itself handles:
EXTREME_POINTS = 'By 4 points'
}
enum Mode {
IDLE = 'idle',
DRAG = 'drag',
RESIZE = 'resize',
DRAW = 'draw',
EDIT = 'edit',
MERGE = 'merge',
SPLIT = 'split',
GROUP = 'group',
DRAG_CANVAS = 'drag_canvas',
ZOOM_CANVAS = 'zoom_canvas',
}
interface DrawData {
enabled: boolean;
shapeType?: string;
@ -70,6 +83,7 @@ Canvas itself handles:
}
interface Canvas {
mode(): Mode;
html(): HTMLDivElement;
setZLayer(zLayer: number | null): void;
setup(frameData: any, objectStates: any[]): void;
@ -128,6 +142,10 @@ Standard JS events are used.
- canvas.dragstop
- canvas.zoomstart
- canvas.zoomstop
- canvas.zoom
- canvas.fit
- canvas.dragshape => {id: number}
- canvas.resizeshape => {id: number}
```
### WEB
@ -135,7 +153,8 @@ Standard JS events are used.
// Create an instance of a canvas
const canvas = new window.canvas.Canvas();
console.log('Version', window.canvas.CanvasVersion);
console.log('Version ', window.canvas.CanvasVersion);
console.log('Current mode is ', window.canvas.mode());
// Put canvas to a html container
htmlContainer.appendChild(canvas.html());

@ -74,7 +74,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
return this.controller.mode;
}
private onDrawDone(data: object, continueDraw?: boolean): void {
private onDrawDone(data: object | null, duration: number, continueDraw?: boolean): void {
if (data) {
const { zLayer } = this.controller;
const event: CustomEvent = new CustomEvent('canvas.drawn', {
@ -87,6 +87,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
zOrder: zLayer || 0,
},
continue: continueDraw,
duration,
},
});
@ -137,12 +138,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.mode = Mode.IDLE;
}
private onMergeDone(objects: any[]): void {
private onMergeDone(objects: any[]| null, duration?: number): void {
if (objects) {
const event: CustomEvent = new CustomEvent('canvas.merged', {
bubbles: false,
cancelable: true,
detail: {
duration,
states: objects,
},
});
@ -701,6 +703,12 @@ export class CanvasViewImpl implements CanvasView, Listener {
} else if ([UpdateReasons.IMAGE_ZOOMED, UpdateReasons.IMAGE_FITTED].includes(reason)) {
this.moveCanvas();
this.transformCanvas();
if (reason === UpdateReasons.IMAGE_FITTED) {
this.canvas.dispatchEvent(new CustomEvent('canvas.fit', {
bubbles: false,
cancelable: true,
}));
}
} else if (reason === UpdateReasons.IMAGE_MOVED) {
this.moveCanvas();
} else if ([UpdateReasons.OBJECTS_UPDATED, UpdateReasons.SET_Z_LAYER].includes(reason)) {
@ -1159,6 +1167,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
).map((x: number): number => x - offset);
this.drawnStates[state.clientID].points = points;
this.canvas.dispatchEvent(new CustomEvent('canvas.dragshape', {
bubbles: false,
cancelable: true,
detail: {
id: state.clientID,
},
}));
this.onEditDone(state, points);
}
});
@ -1209,6 +1224,13 @@ export class CanvasViewImpl implements CanvasView, Listener {
).map((x: number): number => x - offset);
this.drawnStates[state.clientID].points = points;
this.canvas.dispatchEvent(new CustomEvent('canvas.resizeshape', {
bubbles: false,
cancelable: true,
detail: {
id: state.clientID,
},
}));
this.onEditDone(state, points);
}
});

@ -31,7 +31,8 @@ export interface DrawHandler {
export class DrawHandlerImpl implements DrawHandler {
// callback is used to notify about creating new shape
private onDrawDone: (data: object, continueDraw?: boolean) => void;
private onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void;
private startTimestamp: number;
private canvas: SVG.Container;
private text: SVG.Container;
private cursorPosition: {
@ -180,7 +181,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.onDrawDone({
shapeType,
points: [xtl, ytl, xbr, ybr],
});
}, Date.now() - this.startTimestamp);
}
}).on('drawupdate', (): void => {
this.shapeSizeElement.update(this.drawInstance);
@ -213,7 +214,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.onDrawDone({
shapeType,
points: [xtl, ytl, xbr, ybr],
});
}, Date.now() - this.startTimestamp);
}
}
}).on('undopoint', (): void => {
@ -300,7 +301,7 @@ export class DrawHandlerImpl implements DrawHandler {
this.onDrawDone({
shapeType,
points,
});
}, Date.now() - this.startTimestamp);
} else if (shapeType === 'polyline'
&& ((box.xbr - box.xtl) >= consts.SIZE_THRESHOLD
|| (box.ybr - box.ytl) >= consts.SIZE_THRESHOLD)
@ -308,13 +309,13 @@ export class DrawHandlerImpl implements DrawHandler {
this.onDrawDone({
shapeType,
points,
});
}, Date.now() - this.startTimestamp);
} else if (shapeType === 'points'
&& (e.target as any).getAttribute('points') !== '0,0') {
this.onDrawDone({
shapeType,
points,
});
}, Date.now() - this.startTimestamp);
}
});
}
@ -365,7 +366,7 @@ export class DrawHandlerImpl implements DrawHandler {
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
}, e.detail.originalEvent.ctrlKey);
}, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey);
});
}
@ -405,7 +406,7 @@ export class DrawHandlerImpl implements DrawHandler {
attributes: { ...this.drawData.initialState.attributes },
label: this.drawData.initialState.label,
color: this.drawData.initialState.color,
}, e.detail.originalEvent.ctrlKey);
}, Date.now() - this.startTimestamp, e.detail.originalEvent.ctrlKey);
});
}
@ -583,14 +584,16 @@ export class DrawHandlerImpl implements DrawHandler {
this.setupDrawEvents();
}
this.startTimestamp = Date.now();
this.initialized = true;
}
public constructor(
onDrawDone: (data: object, continueDraw?: boolean) => void,
onDrawDone: (data: object | null, duration?: number, continueDraw?: boolean) => void,
canvas: SVG.Container,
text: SVG.Container,
) {
this.startTimestamp = Date.now();
this.onDrawDone = onDrawDone;
this.canvas = canvas;
this.text = text;

@ -15,8 +15,9 @@ export interface MergeHandler {
export class MergeHandlerImpl implements MergeHandler {
// callback is used to notify about merging end
private onMergeDone: (objects: any[]) => void;
private onMergeDone: (objects: any[] | null, duration?: number) => void;
private onFindObject: (event: MouseEvent) => void;
private startTimestamp: number;
private canvas: SVG.Container;
private initialized: boolean;
private statesToBeMerged: any[]; // are being merged
@ -57,6 +58,7 @@ export class MergeHandlerImpl implements MergeHandler {
private initMerging(): void {
this.canvas.node.addEventListener('click', this.onFindObject);
this.startTimestamp = Date.now();
this.initialized = true;
}
@ -66,7 +68,7 @@ export class MergeHandlerImpl implements MergeHandler {
this.release();
if (statesToBeMerged.length > 1) {
this.onMergeDone(statesToBeMerged);
this.onMergeDone(statesToBeMerged, Date.now() - this.startTimestamp);
} else {
this.onMergeDone(null);
// here is a cycle
@ -77,12 +79,13 @@ export class MergeHandlerImpl implements MergeHandler {
}
public constructor(
onMergeDone: (objects: any[]) => void,
onMergeDone: (objects: any[] | null, duration?: number) => void,
onFindObject: (event: MouseEvent) => void,
canvas: SVG.Container,
) {
this.onMergeDone = onMergeDone;
this.onFindObject = onFindObject;
this.startTimestamp = Date.now();
this.canvas = canvas;
this.statesToBeMerged = [];
this.highlightedShapes = {};

@ -14,7 +14,8 @@
function build() {
const PluginRegistry = require('./plugins');
const User = require('./user');
const loggerStorage = require('./logger-storage');
const Log = require('./log');
const ObjectState = require('./object-state');
const Statistics = require('./statistics');
const { Job, Task } = require('./session');
@ -41,6 +42,7 @@ function build() {
ServerError,
} = require('./exceptions');
const User = require('./user');
const pjson = require('../package.json');
const config = require('./config');
@ -419,6 +421,53 @@ function build() {
return result;
},
},
/**
* Namespace to working with logs
* @namespace logger
* @memberof module:API.cvat
*/
/**
* Method to logger configuration
* @method configure
* @memberof module:API.cvat.logger
* @param {function} isActiveChecker - callback to know if logger
* should increase working time or not
* @param {object} userActivityCallback - container for a callback <br>
* Logger put here a callback to update user activity timer <br>
* You can call it outside
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
/**
* Append log to a log collection <br>
* Durable logs will have been added after "close" method is called for them <br>
* Ignore rules exist for some logs (e.g. zoomImage, changeAttribute) <br>
* Payload of ignored logs are shallowly combined to previous logs of the same type
* @method log
* @memberof module:API.cvat.logger
* @param {module:API.cvat.enums.LogType | string} type - log type
* @param {Object} [payload = {}] - any other data that will be appended to the log
* @param {boolean} [wait = false] - specifies if log is durable
* @returns {module:API.cvat.classes.Log}
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
/**
* Save accumulated logs on a server
* @method save
* @memberof module:API.cvat.logger
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
* @instance
* @async
*/
logger: loggerStorage,
/**
* Namespace contains some changeable configurations
* @namespace config
@ -432,12 +481,6 @@ function build() {
* @property {string} proxy Axios proxy settings.
* For more details please read <a href="https://github.com/axios/axios"> here </a>
* @memberof module:API.cvat.config
* @property {integer} taskID this value is displayed in a logs if available
* @memberof module:API.cvat.config
* @property {integer} jobID this value is displayed in a logs if available
* @memberof module:API.cvat.config
* @property {integer} clientID read only auto-generated
* value which is displayed in a logs
* @memberof module:API.cvat.config
*/
get backendAPI() {
@ -452,21 +495,6 @@ function build() {
set proxy(value) {
config.proxy = value;
},
get taskID() {
return config.taskID;
},
set taskID(value) {
config.taskID = value;
},
get jobID() {
return config.jobID;
},
set jobID(value) {
config.jobID = value;
},
get clientID() {
return config.clientID;
},
},
/**
* Namespace contains some library information e.g. api version
@ -524,6 +552,7 @@ function build() {
Task,
User,
Job,
Log,
Attribute,
Label,
Statistics,

@ -6,7 +6,4 @@
module.exports = {
backendAPI: 'http://localhost:7000/api/v1',
proxy: false,
taskID: undefined,
jobID: undefined,
clientID: +Date.now().toString().substr(-6),
};

@ -103,68 +103,74 @@
});
/**
* Event types
* @enum {number}
* Logger event types
* @enum {string}
* @name LogType
* @memberof module:API.cvat.enums
* @property {number} pasteObject 0
* @property {number} changeAttribute 1
* @property {number} dragObject 2
* @property {number} deleteObject 3
* @property {number} pressShortcut 4
* @property {number} resizeObject 5
* @property {number} sendLogs 6
* @property {number} saveJob 7
* @property {number} jumpFrame 8
* @property {number} drawObject 9
* @property {number} changeLabel 10
* @property {number} sendTaskInfo 11
* @property {number} loadJob 12
* @property {number} moveImage 13
* @property {number} zoomImage 14
* @property {number} lockObject 15
* @property {number} mergeObjects 16
* @property {number} copyObject 17
* @property {number} propagateObject 18
* @property {number} undoAction 19
* @property {number} redoAction 20
* @property {number} sendUserActivity 21
* @property {number} sendException 22
* @property {number} changeFrame 23
* @property {number} debugInfo 24
* @property {number} fitImage 25
* @property {number} rotateImage 26
* @property {string} loadJob Load job
* @property {string} saveJob Save job
* @property {string} uploadAnnotations Upload annotations
* @property {string} sendUserActivity Send user activity
* @property {string} sendException Send exception
* @property {string} sendTaskInfo Send task info
* @property {string} drawObject Draw object
* @property {string} pasteObject Paste object
* @property {string} copyObject Copy object
* @property {string} propagateObject Propagate object
* @property {string} dragObject Drag object
* @property {string} resizeObject Resize object
* @property {string} deleteObject Delete object
* @property {string} lockObject Lock object
* @property {string} mergeObjects Merge objects
* @property {string} changeAttribute Change attribute
* @property {string} changeLabel Change label
* @property {string} changeFrame Change frame
* @property {string} moveImage Move image
* @property {string} zoomImage Zoom image
* @property {string} fitImage Fit image
* @property {string} rotateImage Rotate image
* @property {string} undoAction Undo action
* @property {string} redoAction Redo action
* @property {string} pressShortcut Press shortcut
* @property {string} debugInfo Debug info
* @readonly
*/
const LogType = {
pasteObject: 0,
changeAttribute: 1,
dragObject: 2,
deleteObject: 3,
pressShortcut: 4,
resizeObject: 5,
sendLogs: 6,
saveJob: 7,
jumpFrame: 8,
drawObject: 9,
changeLabel: 10,
sendTaskInfo: 11,
loadJob: 12,
moveImage: 13,
zoomImage: 14,
lockObject: 15,
mergeObjects: 16,
copyObject: 17,
propagateObject: 18,
undoAction: 19,
redoAction: 20,
sendUserActivity: 21,
sendException: 22,
changeFrame: 23,
debugInfo: 24,
fitImage: 25,
rotateImage: 26,
};
const LogType = Object.freeze({
loadJob: 'Load job',
saveJob: 'Save job',
uploadAnnotations: 'Upload annotations',
sendUserActivity: 'Send user activity',
sendException: 'Send exception',
sendTaskInfo: 'Send task info',
drawObject: 'Draw object',
pasteObject: 'Paste object',
copyObject: 'Copy object',
propagateObject: 'Propagate object',
dragObject: 'Drag object',
resizeObject: 'Resize object',
deleteObject: 'Delete object',
lockObject: 'Lock object',
mergeObjects: 'Merge objects',
changeAttribute: 'Change attribute',
changeLabel: 'Change label',
changeFrame: 'Change frame',
moveImage: 'Move image',
zoomImage: 'Zoom image',
fitImage: 'Fit image',
rotateImage: 'Rotate image',
undoAction: 'Undo action',
redoAction: 'Redo action',
pressShortcut: 'Press shortcut',
debugInfo: 'Debug info',
});
/**
* Types of actions with annotations
@ -208,7 +214,6 @@
/**
* Array of hex colors
* @type {module:API.cvat.classes.Loader[]} values
* @name colors
* @memberof module:API.cvat.enums
* @type {string[]}

@ -0,0 +1,209 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
/* global
require:false
*/
const PluginRegistry = require('./plugins');
const { ArgumentError } = require('./exceptions');
const { LogType } = require('./enums');
/**
* Class representing a single log
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Log {
constructor(logType, payload) {
this.onCloseCallback = null;
this.type = logType;
this.payload = { ...payload };
this.time = new Date();
}
onClose(callback) {
this.onCloseCallback = callback;
}
validatePayload() {
if (typeof (this.payload) !== 'object') {
throw new ArgumentError('Payload must be an object');
}
try {
JSON.stringify(this.payload);
} catch (error) {
const message = `Log payload must be JSON serializable. ${error.toString()}`;
throw new ArgumentError(message);
}
}
dump() {
const payload = { ...this.payload };
const body = {
name: this.type,
time: this.time.toISOString(),
};
for (const field of ['client_id', 'job_id', 'task_id', 'is_active']) {
if (field in payload) {
body[field] = payload[field];
delete payload[field];
}
}
return {
...body,
payload,
};
}
/**
* Method saves a durable log in a storage <br>
* Note then you can call close() multiple times <br>
* Log duration will be computed based on the latest call <br>
* All payloads will be shallowly combined (all top level properties will exist)
* @method close
* @memberof module:API.cvat.classes.Log
* @param {object} [payload] part of payload can be added when close a log
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
async close(payload = {}) {
const result = await PluginRegistry
.apiWrapper.call(this, Log.prototype.close, payload);
return result;
}
}
Log.prototype.close.implementation = function (payload) {
this.payload.duration = Date.now() - this.time.getTime();
this.payload = { ...this.payload, ...payload };
if (this.onCloseCallback) {
this.onCloseCallback();
}
};
class LogWithCount extends Log {
validatePayload() {
Log.prototype.validatePayload.call(this);
if (!Number.isInteger(this.payload.count) || this.payload.count < 1) {
const message = `The field "count" is required for "${this.type}" log`
+ 'It must be a positive integer';
throw new ArgumentError(message);
}
}
}
class LogWithObjectsInfo extends Log {
validatePayload() {
const generateError = (name, range) => {
const message = `The field "${name}" is required for "${this.type}" log. ${range}`;
throw new ArgumentError(message);
};
if (!Number.isInteger(this.payload['track count']) || this.payload['track count'] < 0) {
generateError('track count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['tag count']) || this.payload['tag count'] < 0) {
generateError('tag count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['object count']) || this.payload['object count'] < 0) {
generateError('object count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['frame count']) || this.payload['frame count'] < 1) {
generateError('frame count', 'It must be an integer not less than 1');
}
if (!Number.isInteger(this.payload['box count']) || this.payload['box count'] < 0) {
generateError('box count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['polygon count']) || this.payload['polygon count'] < 0) {
generateError('polygon count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['polyline count']) || this.payload['polyline count'] < 0) {
generateError('polyline count', 'It must be an integer not less than 0');
}
if (!Number.isInteger(this.payload['points count']) || this.payload['points count'] < 0) {
generateError('points count', 'It must be an integer not less than 0');
}
}
}
class LogWithWorkingTime extends Log {
validatePayload() {
Log.prototype.validatePayload.call(this);
if (!('working_time' in this.payload)
|| !typeof (this.payload.working_time) === 'number'
|| this.payload.working_time < 0
) {
const message = `The field "working_time" is required for ${this.type} log. `
+ 'It must be a number not less than 0';
throw new ArgumentError(message);
}
}
}
class LogWithExceptionInfo extends Log {
validatePayload() {
Log.prototype.validatePayload.call(this);
if (typeof (this.payload.message) !== 'string') {
const message = `The field "message" is required for ${this.type} log. `
+ 'It must be a string';
throw new ArgumentError(message);
}
if (typeof (this.payload.filename) !== 'string') {
const message = `The field "filename" is required for ${this.type} log. `
+ 'It must be a string';
throw new ArgumentError(message);
}
if (typeof (this.payload.line) !== 'number') {
const message = `The field "line" is required for ${this.type} log. `
+ 'It must be a number';
throw new ArgumentError(message);
}
}
}
function logFactory(logType, payload) {
const logsWithCount = [
LogType.deleteObject, LogType.mergeObjects, LogType.copyObject,
LogType.undoAction, LogType.redoAction,
];
if (logsWithCount.includes(logType)) {
return new LogWithCount(logType, payload);
}
if ([LogType.sendTaskInfo, LogType.loadJob, LogType.uploadAnnotations].includes(logType)) {
return new LogWithObjectsInfo(logType, payload);
}
if (logType === LogType.sendUserActivity) {
return new LogWithWorkingTime(logType, payload);
}
if (logType === LogType.sendException) {
return new LogWithExceptionInfo(logType, payload);
}
return new Log(logType, payload);
}
module.exports = logFactory;

@ -0,0 +1,169 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
/* global
require:false
*/
const PluginRegistry = require('./plugins');
const server = require('./server-proxy');
const logFactory = require('./log');
const { ArgumentError } = require('./exceptions');
const { LogType } = require('./enums');
const WORKING_TIME_THRESHOLD = 100000; // ms, 1.66 min
class LoggerStorage {
constructor() {
this.clientID = Date.now().toString().substr(-6);
this.lastLogTime = Date.now();
this.workingTime = 0;
this.collection = [];
this.ignoreRules = {}; // by event
this.isActiveChecker = null;
this.ignoreRules[LogType.zoomImage] = {
lastLog: null,
timeThreshold: 1000,
ignore(previousLog) {
return Date.now() - previousLog.time < this.timeThreshold;
},
};
this.ignoreRules[LogType.changeAttribute] = {
lastLog: null,
ignore(previousLog, currentPayload) {
return currentPayload.object_id === previousLog.payload.object_id
&& currentPayload.id === previousLog.payload.id;
},
};
}
updateWorkingTime() {
if (!this.isActiveChecker || this.isActiveChecker()) {
const lastLogTime = Date.now();
const diff = lastLogTime - this.lastLogTime;
this.workingTime += diff < WORKING_TIME_THRESHOLD ? diff : 0;
this.lastLogTime = lastLogTime;
}
}
async configure(isActiveChecker, activityHelper) {
const result = await PluginRegistry
.apiWrapper.call(
this, LoggerStorage.prototype.configure,
isActiveChecker, activityHelper,
);
return result;
}
async log(logType, payload = {}, wait = false) {
const result = await PluginRegistry
.apiWrapper.call(this, LoggerStorage.prototype.log, logType, payload, wait);
return result;
}
async save() {
const result = await PluginRegistry
.apiWrapper.call(this, LoggerStorage.prototype.save);
return result;
}
}
LoggerStorage.prototype.configure.implementation = function (
isActiveChecker,
userActivityCallback,
) {
if (typeof (isActiveChecker) !== 'function') {
throw new ArgumentError('isActiveChecker argument must be callable');
}
if (!Array.isArray(userActivityCallback)) {
throw new ArgumentError('userActivityCallback argument must be an array');
}
this.isActiveChecker = () => !!isActiveChecker();
userActivityCallback.push(this.updateWorkingTime.bind(this));
};
LoggerStorage.prototype.log.implementation = function (logType, payload, wait) {
if (typeof (payload) !== 'object') {
throw new ArgumentError('Payload must be an object');
}
if (typeof (wait) !== 'boolean') {
throw new ArgumentError('Payload must be an object');
}
if (logType in this.ignoreRules) {
const ignoreRule = this.ignoreRules[logType];
const { lastLog } = ignoreRule;
if (lastLog && ignoreRule.ignore(lastLog, payload)) {
lastLog.payload = {
...lastLog.payload,
...payload,
};
this.updateWorkingTime();
return ignoreRule.lastLog;
}
}
const logPayload = { ...payload };
logPayload.client_id = this.clientID;
if (this.isActiveChecker) {
logPayload.is_active = this.isActiveChecker();
}
const log = logFactory(logType, { ...logPayload });
if (logType in this.ignoreRules) {
this.ignoreRules[logType].lastLog = log;
}
const pushEvent = () => {
this.updateWorkingTime();
log.validatePayload();
log.onClose(null);
this.collection.push(log);
};
if (wait) {
log.onClose(pushEvent);
} else {
pushEvent();
}
return log;
};
LoggerStorage.prototype.save.implementation = async function () {
const collectionToSend = [...this.collection];
const lastLog = this.collection[this.collection.length - 1];
const logPayload = {};
logPayload.client_id = this.clientID;
logPayload.working_time = this.workingTime;
if (this.isActiveChecker) {
logPayload.is_active = this.isActiveChecker();
}
if (lastLog && lastLog.type === LogType.sendTaskInfo) {
logPayload.job_id = lastLog.payload.job_id;
logPayload.task_id = lastLog.payload.task_id;
}
const userActivityLog = logFactory(LogType.sendUserActivity, logPayload);
collectionToSend.push(userActivityLog);
await server.logs.save(collectionToSend.map((log) => log.dump()));
for (const rule of Object.values(this.ignoreRules)) {
rule.lastLog = null;
}
this.collection = [];
this.workingTime = 0;
this.lastLogTime = Date.now();
};
module.exports = new LoggerStorage();

@ -1,42 +0,0 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
/* global
require:false
*/
(() => {
const PluginRegistry = require('./plugins');
/**
* Class describe scheme of a log object
* @memberof module:API.cvat.classes
* @hideconstructor
*/
class Log {
constructor(logType, continuous, details) {
this.type = logType;
this.continuous = continuous;
this.details = details;
}
/**
* Method closes a continue log
* @method close
* @memberof module:API.cvat.classes.Log
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
*/
async close() {
const result = await PluginRegistry
.apiWrapper.call(this, Log.prototype.close);
return result;
}
}
module.exports = Log;
})();

@ -584,6 +584,21 @@
});
}
async function saveLogs(logs) {
const { backendAPI } = config;
try {
await Axios.post(`${backendAPI}/server/logs`, JSON.stringify(logs), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
}
Object.defineProperties(this, Object.freeze({
server: {
value: Object.freeze({
@ -646,6 +661,13 @@
}),
writable: false,
},
logs: {
value: Object.freeze({
save: saveLogs,
}),
writable: false,
},
}));
}
}

@ -9,6 +9,7 @@
(() => {
const PluginRegistry = require('./plugins');
const loggerStorage = require('./logger-storage');
const serverProxy = require('./server-proxy');
const { getFrame, getPreview } = require('./frames');
const { ArgumentError } = require('./exceptions');
@ -125,16 +126,11 @@
},
writable: true,
}),
logs: Object.freeze({
logger: Object.freeze({
value: {
async put(logType, details) {
async log(logType, payload = {}, wait = false) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.logs.put, logType, details);
return result;
},
async save(onUpdate) {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.logs.save, onUpdate);
.apiWrapper.call(this, prototype.logger.log, logType, payload, wait);
return result;
},
},
@ -436,33 +432,28 @@
/**
* Namespace is used for an interaction with logs
* @namespace logs
* @namespace logger
* @memberof Session
*/
/**
* Append log to a log collection.
* Continue logs will have been added after "close" method is called
* @method put
* @memberof Session.logs
* @param {module:API.cvat.enums.LogType} type a type of a log
* @param {boolean} continuous log is a continuous log
* @param {Object} details any others data which will be append to log data
* Create a log and add it to a log collection <br>
* Durable logs will be added after "close" method is called for them <br>
* The fields "task_id" and "job_id" automatically added when add logs
* throught a task or a job <br>
* Ignore rules exist for some logs (e.g. zoomImage, changeAttribute) <br>
* Payload of ignored logs are shallowly combined to previous logs of the same type
* @method log
* @memberof Session.logger
* @param {module:API.cvat.enums.LogType | string} type - log type
* @param {Object} [payload = {}] - any other data that will be appended to the log
* @param {boolean} [wait = false] - specifies if log is durable
* @returns {module:API.cvat.classes.Log}
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
/**
* Save accumulated logs on a server
* @method save
* @memberof Session.logs
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
* @instance
* @async
*/
/**
* Namespace is used for an interaction with actions
@ -702,6 +693,10 @@
get: Object.getPrototypeOf(this).frames.get.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this),
};
this.logger = {
log: Object.getPrototypeOf(this).logger.log.bind(this),
};
}
/**
@ -1212,6 +1207,10 @@
get: Object.getPrototypeOf(this).frames.get.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this),
};
this.logger = {
log: Object.getPrototypeOf(this).logger.log.bind(this),
};
}
/**
@ -1452,6 +1451,11 @@
return result;
};
Job.prototype.logger.log.implementation = async function (logType, payload, wait) {
const result = await this.task.logger.log(logType, { ...payload, job_id: this.id }, wait);
return result;
};
Task.prototype.save.implementation = async function saveTaskImplementation(onUpdate) {
// TODO: Add ability to change an owner and an assignee
if (typeof (this.id) !== 'undefined') {
@ -1663,4 +1667,9 @@
const result = getActions(this);
return result;
};
Task.prototype.logger.log.implementation = async function (logType, payload, wait) {
const result = await loggerStorage.log(logType, { ...payload, task_id: this.id }, wait);
return result;
};
})();

@ -22,6 +22,7 @@ import {
} from 'reducers/interfaces';
import getCore from 'cvat-core';
import logger, { LogType } from 'cvat-logger';
import { RectDrawingMethod } from 'cvat-canvas';
import { getCVATStore } from 'cvat-store';
@ -88,6 +89,23 @@ function computeZRange(states: any[]): number[] {
return [minZ, maxZ];
}
async function jobInfoGenerator(job: any): Promise<Record<string, number>> {
const { total } = await job.annotations.statistics();
return {
'frame count': job.stopFrame - job.startFrame + 1,
'track count': total.rectangle.shape + total.rectangle.track
+ total.polygon.shape + total.polygon.track
+ total.polyline.shape + total.polyline.track
+ total.points.shape + total.points.track,
'object count': total.total,
'box count': total.rectangle.shape + total.rectangle.track,
'polygon count': total.polygon.shape + total.polygon.track,
'polyline count': total.polyline.shape + total.polyline.track,
'points count': total.points.shape + total.points.track,
'tag count': total.tags,
};
}
export enum AnnotationActionTypes {
GET_JOB = 'GET_JOB',
GET_JOB_SUCCESS = 'GET_JOB_SUCCESS',
@ -165,6 +183,28 @@ export enum AnnotationActionTypes {
ADD_Z_LAYER = 'ADD_Z_LAYER',
SEARCH_ANNOTATIONS_FAILED = 'SEARCH_ANNOTATIONS_FAILED',
CHANGE_WORKSPACE = 'CHANGE_WORKSPACE',
SAVE_LOGS_SUCCESS = 'SAVE_LOGS_SUCCESS',
SAVE_LOGS_FAILED = 'SAVE_LOGS_FAILED',
}
export function saveLogsAsync():
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>) => {
try {
await logger.save();
dispatch({
type: AnnotationActionTypes.SAVE_LOGS_SUCCESS,
payload: {},
});
} catch (error) {
dispatch({
type: AnnotationActionTypes.SAVE_LOGS_FAILED,
payload: {
error,
},
});
}
};
}
export function changeWorkspace(workspace: Workspace): AnyAction {
@ -192,8 +232,7 @@ export function switchZLayer(cur: number): AnyAction {
};
}
export function fetchAnnotationsAsync():
ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function fetchAnnotationsAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const {
@ -250,14 +289,21 @@ export function undoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const state = getStore().getState();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
// TODO: use affected IDs as an optimization
const [undoName] = state.annotation.annotations.history.undo.slice(-1);
const undoLog = await sessionInstance.logger.log(LogType.undoAction, {
name: undoName,
count: 1,
}, true);
await sessionInstance.actions.undo();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
await undoLog.close();
dispatch({
type: AnnotationActionTypes.UNDO_ACTION_SUCCESS,
@ -283,14 +329,21 @@ export function redoActionAsync(sessionInstance: any, frame: number):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
const state = getStore().getState();
const { filters, showAllInterpolationTracks } = receiveAnnotationsParameters();
// TODO: use affected IDs as an optimization
const [redoName] = state.annotation.annotations.history.redo.slice(-1);
const redoLog = await sessionInstance.logger.log(LogType.redoAction, {
name: redoName,
count: 1,
}, true);
await sessionInstance.actions.redo();
const history = await sessionInstance.actions.get();
const states = await sessionInstance.annotations
.get(frame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
await redoLog.close();
dispatch({
type: AnnotationActionTypes.REDO_ACTION_SUCCESS,
@ -373,6 +426,12 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
const frame = state.annotation.player.frame.number;
await job.annotations.upload(file, loader);
await job.logger.log(
LogType.uploadAnnotations, {
...(await jobInfoGenerator(job)),
},
);
// One more update to escape some problems
// in canvas when shape with the same
// clientID has different type (polygon, rectangle) for example
@ -499,6 +558,9 @@ export function propagateObjectAsync(
frame: from,
};
await sessionInstance.logger.log(
LogType.propagateObject, { count: to - from + 1 },
);
const states = [];
for (let frame = from; frame <= to; frame++) {
copy.frame = frame;
@ -549,6 +611,7 @@ export function removeObjectAsync(sessionInstance: any, objectState: any, force:
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
await sessionInstance.logger.log(LogType.deleteObject, { count: 1 });
const removed = await objectState.delete(force);
const history = await sessionInstance.actions.get();
@ -584,6 +647,9 @@ export function editShape(enabled: boolean): AnyAction {
}
export function copyShape(objectState: any): AnyAction {
const job = getStore().getState().annotation.job.instance;
job.logger.log(LogType.copyObject, { count: 1 });
return {
type: AnnotationActionTypes.COPY_SHAPE,
payload: {
@ -687,6 +753,12 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
payload: {},
});
await job.logger.log(
LogType.changeFrame, {
from: frame,
to: toFrame,
},
);
const data = await job.frames.get(toFrame);
const states = await job.annotations.get(toFrame, showAllInterpolationTracks, filters);
const [minZ, maxZ] = computeZRange(states);
@ -707,6 +779,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
}
const delay = Math.max(0, Math.round(1000 / frameSpeed)
- currentTime + (state.annotation.player.frame.changeTime as number));
dispatch({
type: AnnotationActionTypes.CHANGE_FRAME_SUCCESS,
payload: {
@ -734,14 +807,33 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function rotateCurrentFrame(rotation: Rotation): AnyAction {
const state: CombinedState = getStore().getState();
const { number: frameNumber } = state.annotation.player.frame;
const { startFrame } = state.annotation.job.instance;
const { frameAngles } = state.annotation.player;
const { rotateAll } = state.settings.player;
const {
annotation: {
player: {
frame: {
number: frameNumber,
},
frameAngles,
},
job: {
instance: job,
instance: {
startFrame,
},
},
},
settings: {
player: {
rotateAll,
},
},
} = state;
const frameAngle = (frameAngles[frameNumber - startFrame]
+ (rotation === Rotation.CLOCKWISE90 ? 90 : 270)) % 360;
job.logger.log(LogType.rotateImage, { angle: frameAngle });
return {
type: AnnotationActionTypes.ROTATE_FRAME,
payload: {
@ -791,11 +883,6 @@ export function getJobAsync(
initialFilters: string[],
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch({
type: AnnotationActionTypes.GET_JOB,
payload: {},
});
try {
const state: CombinedState = getStore().getState();
const filters = initialFilters;
@ -808,6 +895,18 @@ export function getJobAsync(
});
}
dispatch({
type: AnnotationActionTypes.GET_JOB,
payload: {},
});
const loadJobEvent = await logger.log(
LogType.loadJob, {
task_id: tid,
job_id: jid,
}, true,
);
// Check state if the task is already there
let task = state.tasks.current
.filter((_task: Task) => _task.instance.id === tid)
@ -832,6 +931,8 @@ export function getJobAsync(
const [minZ, maxZ] = computeZRange(states);
const colors = [...cvat.enums.colors];
loadJobEvent.close(await jobInfoGenerator(job));
dispatch({
type: AnnotationActionTypes.GET_JOB_SUCCESS,
payload: {
@ -865,6 +966,10 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
});
try {
const saveJobEvent = await sessionInstance.logger.log(
LogType.saveJob, {}, true,
);
await sessionInstance.annotations.save((status: string) => {
dispatch({
type: AnnotationActionTypes.SAVE_UPDATE_ANNOTATIONS_STATUS,
@ -874,6 +979,13 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
});
});
await saveJobEvent.close();
await sessionInstance.logger.log(
LogType.sendTaskInfo,
await jobInfoGenerator(sessionInstance),
);
dispatch(saveLogsAsync());
dispatch({
type: AnnotationActionTypes.SAVE_ANNOTATIONS_SUCCESS,
payload: {},
@ -889,6 +1001,7 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
};
}
// used to reproduce the latest drawing (in case of tags just creating) by using N
export function rememberObject(
objectType: ObjectType,
labelID: number,

@ -3,7 +3,7 @@
// SPDX-License-Identifier: MIT
import './styles.scss';
import React from 'react';
import React, { useEffect } from 'react';
import {
Layout,
@ -21,18 +21,24 @@ interface Props {
job: any | null | undefined;
fetching: boolean;
getJob(): void;
saveLogs(): void;
workspace: Workspace;
}
export default function AnnotationPageComponent(props: Props): JSX.Element {
const {
job,
fetching,
getJob,
saveLogs,
workspace,
} = props;
useEffect(() => {
saveLogs();
return saveLogs;
}, []);
if (job === null) {
if (!fetching) {
getJob();

@ -11,6 +11,7 @@ import { CheckboxChangeEvent } from 'antd/lib/checkbox';
import { Row, Col } from 'antd/lib/grid';
import Text from 'antd/lib/typography/Text';
import { LogType } from 'cvat-logger';
import {
activateObject as activateObjectAction,
updateAnnotationsAsync,
@ -28,6 +29,7 @@ interface StateToProps {
activatedAttributeID: number | null;
states: any[];
labels: any[];
jobInstance: any;
}
interface DispatchToProps {
@ -48,12 +50,14 @@ function mapStateToProps(state: CombinedState): StateToProps {
states,
},
job: {
instance: jobInstance,
labels,
},
},
} = state;
return {
jobInstance,
labels,
activatedStateID,
activatedAttributeID,
@ -78,6 +82,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
states,
activatedStateID,
activatedAttributeID,
jobInstance,
updateAnnotations,
activateObject,
} = props;
@ -267,6 +272,13 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX.
currentValue={activeObjectState.attributes[activeAttribute.id]}
onChange={(value: string) => {
const { attributes } = activeObjectState;
jobInstance.logger.log(
LogType.changeAttribute, {
id: activeAttribute.id,
object_id: activeObjectState.clientID,
value,
},
);
attributes[activeAttribute.id] = value;
activeObjectState.attributes = attributes;
updateAnnotations([activeObjectState]);

@ -9,6 +9,7 @@ import Layout from 'antd/lib/layout';
import Icon from 'antd/lib/icon';
import Tooltip from 'antd/lib/tooltip';
import { LogType } from 'cvat-logger';
import { Canvas } from 'cvat-canvas';
import getCore from 'cvat-core';
import {
@ -214,6 +215,10 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.html().removeEventListener('canvas.deactivated', this.onCanvasShapeDeactivated);
canvasInstance.html().removeEventListener('canvas.moved', this.onCanvasCursorMoved);
canvasInstance.html().removeEventListener('canvas.zoom', this.onCanvasZoomChanged);
canvasInstance.html().removeEventListener('canvas.fit', this.onCanvasImageFitted);
canvasInstance.html().removeEventListener('canvas.dragshape', this.onCanvasShapeDragged);
canvasInstance.html().removeEventListener('canvas.resizeshape', this.onCanvasShapeResized);
canvasInstance.html().removeEventListener('canvas.clicked', this.onCanvasShapeClicked);
canvasInstance.html().removeEventListener('canvas.drawn', this.onCanvasShapeDrawn);
canvasInstance.html().removeEventListener('canvas.merged', this.onCanvasObjectsMerged);
@ -237,20 +242,18 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onShapeDrawn();
}
const { state } = event.detail;
if (!state.objectType) {
state.objectType = activeObjectType;
}
if (!state.label) {
[state.label] = jobInstance.task.labels
.filter((label: any) => label.id === activeLabelID);
}
if (typeof (state.occluded) === 'undefined') {
state.occluded = false;
const { state, duration } = event.detail;
const isDrawnFromScratch = !state.label;
if (isDrawnFromScratch) {
jobInstance.logger.log(LogType.drawObject, { count: 1, duration });
} else {
jobInstance.logger.log(LogType.pasteObject, { count: 1, duration });
}
state.objectType = state.objectType || activeObjectType;
state.label = state.label || jobInstance.task.labels
.filter((label: any) => label.id === activeLabelID)[0];
state.occluded = state.occluded || false;
state.frame = frame;
const objectState = new cvat.classes.ObjectState(state);
onCreateAnnotations(jobInstance, frame, [objectState]);
@ -266,7 +269,11 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onMergeObjects(false);
const { states } = event.detail;
const { states, duration } = event.detail;
jobInstance.logger.log(LogType.mergeObjects, {
duration,
count: states.length,
});
onMergeAnnotations(jobInstance, frame, states);
};
@ -324,6 +331,28 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
onUpdateContextMenu(activatedStateID !== null, e.clientX, e.clientY);
};
private onCanvasShapeDragged = (e: any): void => {
const { jobInstance } = this.props;
const { id } = e.detail;
jobInstance.logger.log(LogType.dragObject, { id });
};
private onCanvasShapeResized = (e: any): void => {
const { jobInstance } = this.props;
const { id } = e.detail;
jobInstance.logger.log(LogType.resizeObject, { id });
};
private onCanvasImageFitted = (): void => {
const { jobInstance } = this.props;
jobInstance.logger.log(LogType.fitImage);
};
private onCanvasZoomChanged = (): void => {
const { jobInstance } = this.props;
jobInstance.logger.log(LogType.zoomImage);
};
private onCanvasShapeClicked = (e: any): void => {
const { clientID } = e.detail.state;
const sidebarItem = window.document
@ -581,6 +610,10 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.html().addEventListener('canvas.deactivated', this.onCanvasShapeDeactivated);
canvasInstance.html().addEventListener('canvas.moved', this.onCanvasCursorMoved);
canvasInstance.html().addEventListener('canvas.zoom', this.onCanvasZoomChanged);
canvasInstance.html().addEventListener('canvas.fit', this.onCanvasImageFitted);
canvasInstance.html().addEventListener('canvas.dragshape', this.onCanvasShapeDragged);
canvasInstance.html().addEventListener('canvas.resizeshape', this.onCanvasShapeResized);
canvasInstance.html().addEventListener('canvas.clicked', this.onCanvasShapeClicked);
canvasInstance.html().addEventListener('canvas.drawn', this.onCanvasShapeDrawn);
canvasInstance.html().addEventListener('canvas.merged', this.onCanvasObjectsMerged);

@ -27,6 +27,7 @@ import LoginPageContainer from 'containers/login-page/login-page';
import RegisterPageContainer from 'containers/register-page/register-page';
import HeaderContainer from 'containers/header/header';
import getCore from 'cvat-core';
import { NotificationsState } from 'reducers/interfaces';
interface CVATAppProps {
@ -56,8 +57,17 @@ interface CVATAppProps {
class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentProps> {
public componentDidMount(): void {
const core = getCore();
const { verifyAuthorized } = this.props;
configure({ ignoreRepeatedEventsWhenKeyHeldDown: false });
// Logger configuration
const userActivityCallback: (() => void)[] = [];
window.addEventListener('click', () => {
userActivityCallback.forEach((handler) => handler());
});
core.logger.configure(() => window.document.hasFocus, userActivityCallback);
verifyAuthorized();
}
@ -198,7 +208,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
const readyForRender = (userInitialized && user == null)
|| (userInitialized && formatsInitialized
&& pluginsInitialized && usersInitialized && aboutInitialized);
&& pluginsInitialized && usersInitialized && aboutInitialized);
const withModels = installedAutoAnnotation
|| installedTFAnnotation || installedTFSegmentation;
@ -253,15 +263,15 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
<Route exact path='/tasks/create' component={CreateTaskPageContainer} />
<Route exact path='/tasks/:id' component={TaskPageContainer} />
<Route exact path='/tasks/:tid/jobs/:jid' component={AnnotationPageContainer} />
{ withModels
&& <Route exact path='/models' component={ModelsPageContainer} /> }
{ installedAutoAnnotation
&& <Route exact path='/models/create' component={CreateModelPageContainer} /> }
{withModels
&& <Route exact path='/models' component={ModelsPageContainer} />}
{installedAutoAnnotation
&& <Route exact path='/models/create' component={CreateModelPageContainer} />}
<Redirect push to='/tasks' />
</Switch>
</GlobalHotKeys>
{/* eslint-disable-next-line */}
<a id='downloadAnchor' style={{ display: 'none' }} download/>
<a id='downloadAnchor' style={{ display: 'none' }} download />
</Layout.Content>
</Layout>
);

@ -7,7 +7,7 @@ import { withRouter } from 'react-router-dom';
import { RouteComponentProps } from 'react-router';
import AnnotationPageComponent from 'components/annotation-page/annotation-page';
import { getJobAsync } from 'actions/annotation-actions';
import { getJobAsync, saveLogsAsync } from 'actions/annotation-actions';
import { CombinedState, Workspace } from 'reducers/interfaces';
@ -24,6 +24,7 @@ interface StateToProps {
interface DispatchToProps {
getJob(): void;
saveLogs(): void;
}
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
@ -77,6 +78,9 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
getJob(): void {
dispatch(getJobAsync(taskID, jobID, initialFrame, initialFilters));
},
saveLogs(): void {
dispatch(saveLogsAsync());
},
};
}

@ -5,6 +5,8 @@
import React from 'react';
import copy from 'copy-to-clipboard';
import { connect } from 'react-redux';
import { LogType } from 'cvat-logger';
import {
ActiveControl,
CombinedState,
@ -292,13 +294,15 @@ class ObjectItemContainer extends React.PureComponent<Props> {
};
private lock = (): void => {
const { objectState } = this.props;
const { objectState, jobInstance } = this.props;
jobInstance.logger.log(LogType.lockObject, { locked: true });
objectState.lock = true;
this.commit();
};
private unlock = (): void => {
const { objectState } = this.props;
const { objectState, jobInstance } = this.props;
jobInstance.logger.log(LogType.lockObject, { locked: false });
objectState.lock = false;
this.commit();
};
@ -405,7 +409,12 @@ class ObjectItemContainer extends React.PureComponent<Props> {
};
private changeAttribute = (id: number, value: string): void => {
const { objectState } = this.props;
const { objectState, jobInstance } = this.props;
jobInstance.logger.log(LogType.changeAttribute, {
id,
value,
object_id: objectState.clientID,
});
const attr: Record<number, string> = {};
attr[id] = value;
objectState.attributes = attr;

@ -0,0 +1,14 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import getCore from 'cvat-core';
const core = getCore();
const { logger } = core;
const { LogType } = core.enums;
export default logger;
export {
LogType,
};

@ -230,6 +230,7 @@ export interface NotificationsState {
undo: null | ErrorState;
redo: null | ErrorState;
search: null | ErrorState;
savingLogs: null | ErrorState;
};
[index: string]: any;

@ -74,6 +74,7 @@ const defaultState: NotificationsState = {
undo: null,
redo: null,
search: null,
savingLogs: null,
},
},
messages: {
@ -766,6 +767,21 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case AnnotationActionTypes.SAVE_LOGS_FAILED: {
return {
...state,
errors: {
...state.errors,
annotation: {
...state.errors.annotation,
savingLogs: {
message: 'Could not send logs to the server',
reason: action.payload.error.toString(),
},
},
},
};
}
case NotificationsActionType.RESET_ERRORS: {
return {
...state,

@ -86,8 +86,8 @@ module.exports = {
},
plugins: [
new HtmlWebpackPlugin({
template: "./src/index.html",
inject: false,
template: "./src/index.html",
inject: false,
}),
new Dotenv({
systemvars: true,

Loading…
Cancel
Save