From bfd300039ec68aaa601b2b2adb61449188886b1e Mon Sep 17 00:00:00 2001
From: Boris Sekachev <40690378+bsekachev@users.noreply.github.com>
Date: Thu, 19 Mar 2020 19:23:49 +0300
Subject: [PATCH] React UI: Added logging (#1288)
---
cvat-canvas/README.md | 21 +-
cvat-canvas/src/typescript/canvasView.ts | 26 ++-
cvat-canvas/src/typescript/drawHandler.ts | 21 +-
cvat-canvas/src/typescript/mergeHandler.ts | 9 +-
cvat-core/src/api.js | 73 ++++--
cvat-core/src/config.js | 3 -
cvat-core/src/enums.js | 123 ++++++-----
cvat-core/src/log.js | 209 ++++++++++++++++++
cvat-core/src/logger-storage.js | 169 ++++++++++++++
cvat-core/src/logging.js | 42 ----
cvat-core/src/server-proxy.js | 22 ++
cvat-core/src/session.js | 59 ++---
cvat-ui/src/actions/annotation-actions.ts | 135 ++++++++++-
.../annotation-page/annotation-page.tsx | 10 +-
.../attribute-annotation-sidebar.tsx | 12 +
.../standard-workspace/canvas-wrapper.tsx | 59 +++--
cvat-ui/src/components/cvat-app.tsx | 22 +-
.../annotation-page/annotation-page.tsx | 6 +-
.../objects-side-bar/object-item.tsx | 15 +-
cvat-ui/src/cvat-logger.ts | 14 ++
cvat-ui/src/reducers/interfaces.ts | 1 +
cvat-ui/src/reducers/notifications-reducer.ts | 16 ++
cvat-ui/webpack.config.js | 4 +-
23 files changed, 867 insertions(+), 204 deletions(-)
create mode 100644 cvat-core/src/log.js
create mode 100644 cvat-core/src/logger-storage.js
delete mode 100644 cvat-core/src/logging.js
create mode 100644 cvat-ui/src/cvat-logger.ts
diff --git a/cvat-canvas/README.md b/cvat-canvas/README.md
index 2b9f9201..7fe60491 100644
--- a/cvat-canvas/README.md
+++ b/cvat-canvas/README.md
@@ -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());
diff --git a/cvat-canvas/src/typescript/canvasView.ts b/cvat-canvas/src/typescript/canvasView.ts
index 65e27220..5db41045 100644
--- a/cvat-canvas/src/typescript/canvasView.ts
+++ b/cvat-canvas/src/typescript/canvasView.ts
@@ -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);
}
});
diff --git a/cvat-canvas/src/typescript/drawHandler.ts b/cvat-canvas/src/typescript/drawHandler.ts
index 60745f9f..4afe75ec 100644
--- a/cvat-canvas/src/typescript/drawHandler.ts
+++ b/cvat-canvas/src/typescript/drawHandler.ts
@@ -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;
diff --git a/cvat-canvas/src/typescript/mergeHandler.ts b/cvat-canvas/src/typescript/mergeHandler.ts
index efaa4ac0..cfb3f78c 100644
--- a/cvat-canvas/src/typescript/mergeHandler.ts
+++ b/cvat-canvas/src/typescript/mergeHandler.ts
@@ -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 = {};
diff --git a/cvat-core/src/api.js b/cvat-core/src/api.js
index 7f9ad9c7..4eb4e99a 100644
--- a/cvat-core/src/api.js
+++ b/cvat-core/src/api.js
@@ -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
+ * Logger put here a callback to update user activity timer
+ * 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
+ * Durable logs will have been added after "close" method is called for them
+ * Ignore rules exist for some logs (e.g. zoomImage, changeAttribute)
+ * 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 here
* @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,
diff --git a/cvat-core/src/config.js b/cvat-core/src/config.js
index e940a214..3b9eade8 100644
--- a/cvat-core/src/config.js
+++ b/cvat-core/src/config.js
@@ -6,7 +6,4 @@
module.exports = {
backendAPI: 'http://localhost:7000/api/v1',
proxy: false,
- taskID: undefined,
- jobID: undefined,
- clientID: +Date.now().toString().substr(-6),
};
diff --git a/cvat-core/src/enums.js b/cvat-core/src/enums.js
index 2c342908..8b6c86fc 100644
--- a/cvat-core/src/enums.js
+++ b/cvat-core/src/enums.js
@@ -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[]}
diff --git a/cvat-core/src/log.js b/cvat-core/src/log.js
new file mode 100644
index 00000000..56d9592d
--- /dev/null
+++ b/cvat-core/src/log.js
@@ -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
+ * Note then you can call close() multiple times
+ * Log duration will be computed based on the latest call
+ * 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;
diff --git a/cvat-core/src/logger-storage.js b/cvat-core/src/logger-storage.js
new file mode 100644
index 00000000..e8e24d7d
--- /dev/null
+++ b/cvat-core/src/logger-storage.js
@@ -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();
diff --git a/cvat-core/src/logging.js b/cvat-core/src/logging.js
deleted file mode 100644
index f6b52c4e..00000000
--- a/cvat-core/src/logging.js
+++ /dev/null
@@ -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;
-})();
diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js
index 25f10182..7a833fb1 100644
--- a/cvat-core/src/server-proxy.js
+++ b/cvat-core/src/server-proxy.js
@@ -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,
+ },
}));
}
}
diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js
index 32643dd2..edd5efc8 100644
--- a/cvat-core/src/session.js
+++ b/cvat-core/src/session.js
@@ -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
+ * Durable logs will be added after "close" method is called for them
+ * The fields "task_id" and "job_id" automatically added when add logs
+ * throught a task or a job
+ * Ignore rules exist for some logs (e.g. zoomImage, changeAttribute)
+ * 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;
+ };
})();
diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts
index a13444e5..b80c0a0f 100644
--- a/cvat-ui/src/actions/annotation-actions.ts
+++ b/cvat-ui/src/actions/annotation-actions.ts
@@ -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> {
+ 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, {}, {}, AnyAction> {
+ return async (dispatch: ActionCreator) => {
+ 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, {}, {}, AnyAction> {
+export function fetchAnnotationsAsync(): ThunkAction, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
try {
const {
@@ -250,14 +289,21 @@ export function undoActionAsync(sessionInstance: any, frame: number):
ThunkAction, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
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, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
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, {}, {}, 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, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
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, {}, {}, 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, {}, {}, 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, {}, {}, 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, {}, {}, AnyAction> {
return async (dispatch: ActionCreator): Promise => {
- 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, {}, {}, 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, {}, {}, 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, {}, {}, AnyAction> {
};
}
+// used to reproduce the latest drawing (in case of tags just creating) by using N
export function rememberObject(
objectType: ObjectType,
labelID: number,
diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx
index a096afd2..db5b6500 100644
--- a/cvat-ui/src/components/annotation-page/annotation-page.tsx
+++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx
@@ -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();
diff --git a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx
index d19b94ef..49d3b499 100644
--- a/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx
+++ b/cvat-ui/src/components/annotation-page/attribute-annotation-workspace/attribute-annotation-sidebar/attribute-annotation-sidebar.tsx
@@ -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]);
diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx
index 91a2e20e..16bc2e3e 100644
--- a/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx
+++ b/cvat-ui/src/components/annotation-page/standard-workspace/canvas-wrapper.tsx
@@ -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 {
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 {
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 {
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 {
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 {
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);
diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx
index 91795797..bab5887a 100644
--- a/cvat-ui/src/components/cvat-app.tsx
+++ b/cvat-ui/src/components/cvat-app.tsx
@@ -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 {
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
- { withModels
- && }
- { installedAutoAnnotation
- && }
+ {withModels
+ && }
+ {installedAutoAnnotation
+ && }
{/* eslint-disable-next-line */}
-
+
);
diff --git a/cvat-ui/src/containers/annotation-page/annotation-page.tsx b/cvat-ui/src/containers/annotation-page/annotation-page.tsx
index 264d1d94..8d2972ba 100644
--- a/cvat-ui/src/containers/annotation-page/annotation-page.tsx
+++ b/cvat-ui/src/containers/annotation-page/annotation-page.tsx
@@ -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());
+ },
};
}
diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
index fb11fe1d..4efc8803 100644
--- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
+++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item.tsx
@@ -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 {
};
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 {
};
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 = {};
attr[id] = value;
objectState.attributes = attr;
diff --git a/cvat-ui/src/cvat-logger.ts b/cvat-ui/src/cvat-logger.ts
new file mode 100644
index 00000000..f1277ff2
--- /dev/null
+++ b/cvat-ui/src/cvat-logger.ts
@@ -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,
+};
diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts
index e0595b90..08f54a63 100644
--- a/cvat-ui/src/reducers/interfaces.ts
+++ b/cvat-ui/src/reducers/interfaces.ts
@@ -230,6 +230,7 @@ export interface NotificationsState {
undo: null | ErrorState;
redo: null | ErrorState;
search: null | ErrorState;
+ savingLogs: null | ErrorState;
};
[index: string]: any;
diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts
index 9ca0240d..668e70f2 100644
--- a/cvat-ui/src/reducers/notifications-reducer.ts
+++ b/cvat-ui/src/reducers/notifications-reducer.ts
@@ -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,
diff --git a/cvat-ui/webpack.config.js b/cvat-ui/webpack.config.js
index e298954d..a172632a 100644
--- a/cvat-ui/webpack.config.js
+++ b/cvat-ui/webpack.config.js
@@ -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,