From 9d6bef547c8cd542c0a54be19f38c67c701f5e0d Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 18 Mar 2020 18:57:03 +0300 Subject: [PATCH 1/4] Implemented basic logging, added events --- 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 | 74 ++++-- cvat-core/src/config.js | 3 - cvat-core/src/enums.js | 123 +++++----- cvat-core/src/log.js | 210 ++++++++++++++++++ cvat-core/src/logger-storage.js | 169 ++++++++++++++ cvat-core/src/logging.js | 42 ---- cvat-core/src/server-proxy.js | 11 + cvat-core/src/session.js | 59 ++--- cvat-ui/src/actions/annotation-actions.ts | 137 +++++++++++- .../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 +- .../engine/static/engine/js/annotationUI.js | 40 ++-- 24 files changed, 880 insertions(+), 224 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 2b9f9201ba76..7fe6049176ee 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 65e272207e46..5db41045586a 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 60745f9f3ec5..4afe75ec259d 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 efaa4ac09d7d..cfb3f78c43db 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 7f9ad9c7448f..6ed4529e23b7 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,54 @@ 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 + * @returns {module:API.cvat.classes.Log} + * @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} 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 +482,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 +496,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 +553,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 e940a214c953..3b9eade8f346 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 2c34290876bf..8b6c86fcaab4 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 000000000000..45e5baa4ce11 --- /dev/null +++ b/cvat-core/src/log.js @@ -0,0 +1,210 @@ +/* +* Copyright (C) 2019 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 {string} [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 000000000000..e8e24d7d0ce8 --- /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 f6b52c4ec32a..000000000000 --- 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 25f101822cad..e811b6f1c2f5 100644 --- a/cvat-core/src/server-proxy.js +++ b/cvat-core/src/server-proxy.js @@ -584,6 +584,10 @@ }); } + async function saveLogs(logs) { + console.log(logs); + } + Object.defineProperties(this, Object.freeze({ server: { value: Object.freeze({ @@ -646,6 +650,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 32643dd200b2..b80bbc29cc4f 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} 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 a13444e5df06..dd72e1638060 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,14 @@ ThunkAction, {}, {}, AnyAction> { const frame = state.annotation.player.frame.number; await job.annotations.upload(file, loader); + await logger.log( + LogType.uploadAnnotations, { + task_id: job.task.id, + job_id: job.id, + ...(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 +560,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 +613,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 +649,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 +755,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 +781,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 +809,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 +885,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 +897,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 +933,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 +968,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 +981,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 +1003,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 a096afd2fded..db5b6500e9b6 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 d19b94ef9def..49d3b4992cf6 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 91a2e20e6986..a369eb8547a3 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 91795797980e..bab5887a1393 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 264d1d9450f7..8d2972ba66ba 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 fb11fe1de67d..4efc88037c04 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 000000000000..f1277ff281ff --- /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 e0595b90b9fd..08f54a63ff0c 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 9ca0240d6803..668e70f2e5f8 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 e298954d6f34..a172632a428c 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, diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js index 5ce8f5f8c73c..0f39d1eefe5a 100644 --- a/cvat/apps/engine/static/engine/js/annotationUI.js +++ b/cvat/apps/engine/static/engine/js/annotationUI.js @@ -209,26 +209,26 @@ function setupShortkeys(shortkeys, models) { const cancelModeHandler = Logger.shortkeyLogDecorator(() => { switch (window.cvat.mode) { - case 'aam': - models.aam.switchAAMMode(); - break; - case 'creation': - models.shapeCreator.switchCreateMode(true); - break; - case 'merge': - models.shapeMerger.cancel(); - break; - case 'groupping': - models.shapeGrouper.cancel(); - break; - case 'paste': - models.shapeBuffer.switchPaste(); - break; - case 'poly_editing': - models.shapeEditor.finish(); - break; - default: - break; + case 'aam': + models.aam.switchAAMMode(); + break; + case 'creation': + models.shapeCreator.switchCreateMode(true); + break; + case 'merge': + models.shapeMerger.cancel(); + break; + case 'groupping': + models.shapeGrouper.cancel(); + break; + case 'paste': + models.shapeBuffer.switchPaste(); + break; + case 'poly_editing': + models.shapeEditor.finish(); + break; + default: + break; } return false; }); From 8da1a32c7f3952806f40c3f3fef4d6423b78687c Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 18 Mar 2020 19:04:10 +0300 Subject: [PATCH 2/4] Added server saving --- cvat-core/src/server-proxy.js | 13 ++++++++++++- .../standard-workspace/canvas-wrapper.tsx | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js index e811b6f1c2f5..7a833fb1f92b 100644 --- a/cvat-core/src/server-proxy.js +++ b/cvat-core/src/server-proxy.js @@ -585,7 +585,18 @@ } async function saveLogs(logs) { - console.log(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({ 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 a369eb8547a3..16bc2e3ec144 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 @@ -243,7 +243,7 @@ export default class CanvasWrapperComponent extends React.PureComponent { } const { state, duration } = event.detail; - const isDrawnFromScratch = state.label; + const isDrawnFromScratch = !state.label; if (isDrawnFromScratch) { jobInstance.logger.log(LogType.drawObject, { count: 1, duration }); } else { From a20b8128efa12d6372edbbfe2087a6281ffb8130 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 18 Mar 2020 19:11:41 +0300 Subject: [PATCH 3/4] Removed extra changes --- .../engine/static/engine/js/annotationUI.js | 40 +++++++++---------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/cvat/apps/engine/static/engine/js/annotationUI.js b/cvat/apps/engine/static/engine/js/annotationUI.js index 0f39d1eefe5a..5ce8f5f8c73c 100644 --- a/cvat/apps/engine/static/engine/js/annotationUI.js +++ b/cvat/apps/engine/static/engine/js/annotationUI.js @@ -209,26 +209,26 @@ function setupShortkeys(shortkeys, models) { const cancelModeHandler = Logger.shortkeyLogDecorator(() => { switch (window.cvat.mode) { - case 'aam': - models.aam.switchAAMMode(); - break; - case 'creation': - models.shapeCreator.switchCreateMode(true); - break; - case 'merge': - models.shapeMerger.cancel(); - break; - case 'groupping': - models.shapeGrouper.cancel(); - break; - case 'paste': - models.shapeBuffer.switchPaste(); - break; - case 'poly_editing': - models.shapeEditor.finish(); - break; - default: - break; + case 'aam': + models.aam.switchAAMMode(); + break; + case 'creation': + models.shapeCreator.switchCreateMode(true); + break; + case 'merge': + models.shapeMerger.cancel(); + break; + case 'groupping': + models.shapeGrouper.cancel(); + break; + case 'paste': + models.shapeBuffer.switchPaste(); + break; + case 'poly_editing': + models.shapeEditor.finish(); + break; + default: + break; } return false; }); From b4e6f2ee383936e848cb0faaba44d0b2abef2c05 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 18 Mar 2020 19:33:01 +0300 Subject: [PATCH 4/4] Minor doc fixes --- cvat-core/src/api.js | 3 +-- cvat-core/src/log.js | 9 ++++----- cvat-core/src/session.js | 2 +- cvat-ui/src/actions/annotation-actions.ts | 4 +--- 4 files changed, 7 insertions(+), 11 deletions(-) diff --git a/cvat-core/src/api.js b/cvat-core/src/api.js index 6ed4529e23b7..4eb4e99af00b 100644 --- a/cvat-core/src/api.js +++ b/cvat-core/src/api.js @@ -435,7 +435,6 @@ function build() { * @param {object} userActivityCallback - container for a callback
* Logger put here a callback to update user activity timer
* You can call it outside - * @returns {module:API.cvat.classes.Log} * @instance * @async * @throws {module:API.cvat.exceptions.PluginError} @@ -449,7 +448,7 @@ function build() { * 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} type - log type + * @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} diff --git a/cvat-core/src/log.js b/cvat-core/src/log.js index 45e5baa4ce11..56d9592d4d99 100644 --- a/cvat-core/src/log.js +++ b/cvat-core/src/log.js @@ -1,7 +1,6 @@ -/* -* Copyright (C) 2019 Intel Corporation -* SPDX-License-Identifier: MIT -*/ +// Copyright (C) 2020 Intel Corporation +// +// SPDX-License-Identifier: MIT /* global require:false @@ -69,7 +68,7 @@ class Log { * All payloads will be shallowly combined (all top level properties will exist) * @method close * @memberof module:API.cvat.classes.Log - * @param {string} [payload] part of payload can be added when close a log + * @param {object} [payload] part of payload can be added when close a log * @readonly * @instance * @async diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index b80bbc29cc4f..edd5efc8be80 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -445,7 +445,7 @@ * 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} type - log type + * @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} diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index dd72e1638060..b80c0a0f1d56 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -426,10 +426,8 @@ ThunkAction, {}, {}, AnyAction> { const frame = state.annotation.player.frame.number; await job.annotations.upload(file, loader); - await logger.log( + await job.logger.log( LogType.uploadAnnotations, { - task_id: job.task.id, - job_id: job.id, ...(await jobInfoGenerator(job)), }, );