diff --git a/.github/workflows/cancel.yml b/.github/workflows/cancel.yml deleted file mode 100644 index 9e523d9c2193..000000000000 --- a/.github/workflows/cancel.yml +++ /dev/null @@ -1,17 +0,0 @@ -name: Cancelling Duplicates -on: - workflow_run: - workflows: ['CI', 'Helm'] - types: ['requested'] - -jobs: - cancel-duplicate-workflow-runs: - name: "Cancel duplicate workflow runs" - runs-on: ubuntu-latest - steps: - - uses: potiuk/cancel-workflow-runs@master - name: "Cancel duplicate workflow runs" - with: - cancelMode: duplicates - token: ${{ secrets.GITHUB_TOKEN }} - sourceRunId: ${{ github.event.workflow_run.id }} diff --git a/.github/workflows/helm.yml b/.github/workflows/helm.yml index 582770494b05..d05bb5a24ee4 100644 --- a/.github/workflows/helm.yml +++ b/.github/workflows/helm.yml @@ -11,6 +11,10 @@ on: - '**/*.md' workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: testing: if: | diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 2464d05a9d59..477968fd6dc4 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,6 +10,10 @@ on: - 'site/**' - '**/*.md' +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + env: CYPRESS_VERIFY_TIMEOUT: 180000 # https://docs.cypress.io/guides/guides/command-line#cypress-verify CVAT_VERSION: "local" diff --git a/CHANGELOG.md b/CHANGELOG.md index 796c17de7a37..5967fc679be8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 + +## \[2.11.0\] - 2024-02-23 + +### Added + +- Added `dataset:export` and `dataset:import` events that are logged when + the user initiates an export or import of a project, task or job + () + +### Changed + +- Now menus in the web interface are triggered by click, not by hover as before + () + +### Removed + +- Removed support for the TFRecord dataset format + () + +### Fixed + +- On quality page for a task, only the first page with jobs has quality report metrics + () + +- Side effects of data changes, such as the sending of webhooks, + are no longer triggered until after the changes have been committed + to the database + (, + ) + ## \[2.10.3\] - 2024-02-09 diff --git a/README.md b/README.md index 5391fc2e5197..8d617c01eb9b 100644 --- a/README.md +++ b/README.md @@ -155,7 +155,6 @@ For more information about the supported formats, see: | [YOLO](https://pjreddie.com/darknet/yolo/) | ✔️ | ✔️ | | [MS COCO Object Detection](http://cocodataset.org/#format-data) | ✔️ | ✔️ | | [MS COCO Keypoints Detection](http://cocodataset.org/#format-data) | ✔️ | ✔️ | -| [TFrecord](https://www.tensorflow.org/tutorials/load_data/tfrecord) | ✔️ | ✔️ | | [MOT](https://motchallenge.net/) | ✔️ | ✔️ | | [MOTS PNG](https://www.vision.rwth-aachen.de/page/mots) | ✔️ | ✔️ | | [LabelMe 3.0](http://labelme.csail.mit.edu/Release3.0) | ✔️ | ✔️ | diff --git a/cvat-cli/requirements/base.txt b/cvat-cli/requirements/base.txt index 4fe841aaabe3..923a5c573f90 100644 --- a/cvat-cli/requirements/base.txt +++ b/cvat-cli/requirements/base.txt @@ -1,3 +1,3 @@ -cvat-sdk~=2.10.3 +cvat-sdk~=2.11.0 Pillow>=10.1.0 setuptools>=65.5.1 # not directly required, pinned by Snyk to avoid a vulnerability diff --git a/cvat-cli/src/cvat_cli/version.py b/cvat-cli/src/cvat_cli/version.py index 5c4b788149b9..e80dd8624369 100644 --- a/cvat-cli/src/cvat_cli/version.py +++ b/cvat-cli/src/cvat_cli/version.py @@ -1 +1 @@ -VERSION = "2.10.3" +VERSION = "2.11.0" diff --git a/cvat-core/src/analytics-report.ts b/cvat-core/src/analytics-report.ts index 8d390046d4ab..4d3637c7ad67 100644 --- a/cvat-core/src/analytics-report.ts +++ b/cvat-core/src/analytics-report.ts @@ -2,41 +2,11 @@ // // SPDX-License-Identifier: MIT +import { + SerializedAnalyticsEntry, SerializedAnalyticsReport, SerializedDataEntry, SerializedTransformationEntry, +} from './server-response-types'; import { ArgumentError } from './exceptions'; -export interface SerializedDataEntry { - date?: string; - value?: number | Record -} - -export interface SerializedTransformBinaryOp { - left: string; - operator: string; - right: string; -} - -export interface SerializedTransformationEntry { - name: string; - binary?: SerializedTransformBinaryOp; -} - -export interface SerializedAnalyticsEntry { - name?: string; - title?: string; - description?: string; - granularity?: string; - default_view?: string; - data_series?: Record; - transformations?: SerializedTransformationEntry[]; -} - -export interface SerializedAnalyticsReport { - id?: number; - target?: string; - created_date?: string; - statistics?: SerializedAnalyticsEntry[]; -} - export enum AnalyticsReportTarget { JOB = 'job', TASK = 'task', diff --git a/cvat-core/src/annotations-actions.ts b/cvat-core/src/annotations-actions.ts index ae8f546daf08..1408dcf25593 100644 --- a/cvat-core/src/annotations-actions.ts +++ b/cvat-core/src/annotations-actions.ts @@ -6,7 +6,7 @@ import { omit, throttle } from 'lodash'; import { ArgumentError } from './exceptions'; import { SerializedCollection } from './server-response-types'; import { Job, Task } from './session'; -import { LogType, ObjectType } from './enums'; +import { EventScope, ObjectType } from './enums'; import ObjectState from './object-state'; import { getAnnotations, getCollection } from './annotations'; @@ -114,7 +114,7 @@ async function runSingleFrameChain( cancelled: () => boolean, ): Promise { type IDsToHandle = { shapes: number[] }; - const event = await instance.logger.log(LogType.annotationsAction, { + const event = await instance.logger.log(EventScope.annotationsAction, { from: frameFrom, to: frameTo, chain: actionsChain.map((action) => action.name).join(' => '), diff --git a/cvat-core/src/api-implementation.ts b/cvat-core/src/api-implementation.ts index 1e365965edbb..443edf7a91aa 100644 --- a/cvat-core/src/api-implementation.ts +++ b/cvat-core/src/api-implementation.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -13,10 +13,12 @@ import { isBoolean, isInteger, isString, + isPageSize, checkFilter, checkExclusiveFields, checkObjectType, filterFieldsToSnakeCase, + fieldsToSnakeCase, } from './common'; import User from './user'; @@ -136,6 +138,8 @@ export default function implementAPI(cvat: CVATCore): CVATCore { return result; }); + implementationMixin(cvat.server.apiSchema, serverProxy.server.apiSchema); + implementationMixin(cvat.assets.create, async (file: File, guideId: number): Promise => { if (!(file instanceof File)) { throw new ArgumentError('Assets expect a file'); @@ -400,34 +404,36 @@ export default function implementAPI(cvat: CVATCore): CVATCore { }); implementationMixin(cvat.analytics.quality.reports, async (filter) => { - let updatedParams: Record = {}; - if ('taskId' in filter) { - updatedParams = { - task_id: filter.taskId, - sort: '-created_date', - target: filter.target, - }; - } - if ('jobId' in filter) { - updatedParams = { - job_id: filter.jobId, - sort: '-created_date', - target: filter.target, - }; - } - const reportsData = await serverProxy.analytics.quality.reports(updatedParams); + checkFilter(filter, { + page: isInteger, + pageSize: isPageSize, + parentID: isInteger, + projectID: isInteger, + taskID: isInteger, + jobID: isInteger, + target: isString, + filter: isString, + search: isString, + sort: isString, + }); - return reportsData.map((report) => new QualityReport({ ...report })); + const params = fieldsToSnakeCase({ ...filter, sort: '-created_date' }); + + const reportsData = await serverProxy.analytics.quality.reports(params); + const reports = Object.assign( + reportsData.map((report) => new QualityReport({ ...report })), + { count: reportsData.count }, + ); + return reports; }); implementationMixin(cvat.analytics.quality.conflicts, async (filter) => { - let updatedParams: Record = {}; - if ('reportId' in filter) { - updatedParams = { - report_id: filter.reportId, - }; - } + checkFilter(filter, { + reportID: isInteger, + }); - const conflictsData = await serverProxy.analytics.quality.conflicts(updatedParams); + const params = fieldsToSnakeCase(filter); + + const conflictsData = await serverProxy.analytics.quality.conflicts(params); const conflicts = conflictsData.map((conflict) => new QualityConflict({ ...conflict })); const frames = Array.from(new Set(conflicts.map((conflict) => conflict.frame))) .sort((a, b) => a - b); @@ -496,8 +502,14 @@ export default function implementAPI(cvat: CVATCore): CVATCore { return mergedConflicts; }); - implementationMixin(cvat.analytics.quality.settings.get, async (taskID: number) => { - const settings = await serverProxy.analytics.quality.settings.get(taskID); + implementationMixin(cvat.analytics.quality.settings.get, async (filter) => { + checkFilter(filter, { + taskID: isInteger, + }); + + const params = fieldsToSnakeCase(filter); + + const settings = await serverProxy.analytics.quality.settings.get(params); return new QualitySettings({ ...settings }); }); implementationMixin(cvat.analytics.performance.reports, async (filter) => { @@ -511,25 +523,8 @@ export default function implementAPI(cvat: CVATCore): CVATCore { checkExclusiveFields(filter, ['jobID', 'taskID', 'projectID'], ['startDate', 'endDate']); - const updatedParams: Record = {}; - - if ('taskID' in filter) { - updatedParams.task_id = filter.taskID; - } - if ('jobID' in filter) { - updatedParams.job_id = filter.jobID; - } - if ('projectID' in filter) { - updatedParams.project_id = filter.projectID; - } - if ('startDate' in filter) { - updatedParams.start_date = filter.startDate; - } - if ('endDate' in filter) { - updatedParams.end_date = filter.endDate; - } - - const reportData = await serverProxy.analytics.performance.reports(updatedParams); + const params = fieldsToSnakeCase(filter); + const reportData = await serverProxy.analytics.performance.reports(params); return new AnalyticsReport(reportData); }); implementationMixin(cvat.frames.getMeta, async (type, id) => { diff --git a/cvat-core/src/api.ts b/cvat-core/src/api.ts index 86a6ae827663..90007704b887 100644 --- a/cvat-core/src/api.ts +++ b/cvat-core/src/api.ts @@ -4,8 +4,8 @@ // SPDX-License-Identifier: MIT import PluginRegistry from './plugins'; -import loggerStorage from './logger-storage'; -import { EventLogger } from './log'; +import logger from './logger'; +import { Event } from './event'; import ObjectState from './object-state'; import Statistics from './statistics'; import Comment from './comment'; @@ -129,6 +129,10 @@ function build(): CVATCore { const result = await PluginRegistry.apiWrapper(cvat.server.installedApps); return result; }, + async apiSchema() { + const result = await PluginRegistry.apiWrapper(cvat.server.apiSchema); + return result; + }, }, projects: { async get(filter = {}) { @@ -242,7 +246,7 @@ function build(): CVATCore { return result; }, }, - logger: loggerStorage, + logger, config: { get backendAPI() { return config.backendAPI; @@ -346,17 +350,17 @@ function build(): CVATCore { }, }, quality: { - async reports(filter: any) { + async reports(filter = {}) { const result = await PluginRegistry.apiWrapper(cvat.analytics.quality.reports, filter); return result; }, - async conflicts(filter: any) { + async conflicts(filter = {}) { const result = await PluginRegistry.apiWrapper(cvat.analytics.quality.conflicts, filter); return result; }, settings: { - async get(taskID: number) { - const result = await PluginRegistry.apiWrapper(cvat.analytics.quality.settings.get, taskID); + async get(filter = {}) { + const result = await PluginRegistry.apiWrapper(cvat.analytics.quality.settings.get, filter); return result; }, }, @@ -367,7 +371,7 @@ function build(): CVATCore { Project: implementProject(Project), Task: implementTask(Task), Job: implementJob(Job), - EventLogger, + Event, Attribute, Label, Statistics, diff --git a/cvat-core/src/common.ts b/cvat-core/src/common.ts index 1c09aef7ea22..b0e5f78ee4ce 100644 --- a/cvat-core/src/common.ts +++ b/cvat-core/src/common.ts @@ -1,8 +1,9 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022-2023s CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT +import { snakeCase } from 'lodash'; import { ArgumentError } from './exceptions'; export function isBoolean(value): boolean { @@ -145,3 +146,15 @@ export function filterFieldsToSnakeCase(filter: Record, keysToSn export function isResourceURL(url: string): boolean { return /\/([0-9]+)$/.test(url); } + +export function isPageSize(value: number | 'all'): boolean { + return isInteger(value) || value === 'all'; +} + +export function fieldsToSnakeCase(params: Record): Record { + const result = {}; + for (const [k, v] of Object.entries(params)) { + result[snakeCase(k)] = v; + } + return result; +} diff --git a/cvat-core/src/enums.ts b/cvat-core/src/enums.ts index 5039d3783dfe..daa4151a9407 100644 --- a/cvat-core/src/enums.ts +++ b/cvat-core/src/enums.ts @@ -90,7 +90,7 @@ export enum Source { GT = 'Ground truth', } -export enum LogType { +export enum EventScope { loadTool = 'load:cvat', loadJob = 'load:job', diff --git a/cvat-core/src/log.ts b/cvat-core/src/event.ts similarity index 77% rename from cvat-core/src/log.ts rename to cvat-core/src/event.ts index 68b8f928e884..5796b9a4ed75 100644 --- a/cvat-core/src/log.ts +++ b/cvat-core/src/event.ts @@ -5,24 +5,24 @@ import { detect } from 'detect-browser'; import PluginRegistry from './plugins'; -import { LogType } from './enums'; +import { EventScope } from './enums'; import { ArgumentError } from './exceptions'; -export class EventLogger { +export class Event { public readonly id: number; - public readonly scope: LogType; - public readonly time: Date; + public readonly scope: EventScope; + public readonly timestamp: Date; public payload: any; protected onCloseCallback: (() => void) | null; - constructor(logType: LogType, payload: any) { + constructor(scope: EventScope, payload: any) { this.onCloseCallback = null; - this.scope = logType; + this.scope = scope; this.payload = { ...payload }; - this.time = new Date(); + this.timestamp = new Date(); } public onClose(callback: () => void): void { @@ -46,7 +46,7 @@ export class EventLogger { const payload = { ...this.payload }; const body = { scope: this.scope, - timestamp: this.time.toISOString(), + timestamp: this.timestamp.toISOString(), }; for (const field of [ @@ -81,17 +81,17 @@ export class EventLogger { // Log duration will be computed based on the latest call // All payloads will be shallowly combined (all top level properties will exist) public async close(payload = {}): Promise { - const result = await PluginRegistry.apiWrapper.call(this, EventLogger.prototype.close, payload); + const result = await PluginRegistry.apiWrapper.call(this, Event.prototype.close, payload); return result; } } -Object.defineProperties(EventLogger.prototype.close, { +Object.defineProperties(Event.prototype.close, { implementation: { writable: false, enumerable: false, - value: async function implementation(payload: any) { - this.payload.duration = Date.now() - this.time.getTime(); + value: async function implementation(this: Event, payload: any) { + this.payload.duration = Date.now() - this.timestamp.getTime(); this.payload = { ...this.payload, ...payload }; if (this.onCloseCallback) { this.onCloseCallback(); @@ -100,7 +100,7 @@ Object.defineProperties(EventLogger.prototype.close, { }, }); -class LogWithCount extends EventLogger { +class EventWithCount extends Event { public validatePayload(): void { super.validatePayload.call(this); if (!Number.isInteger(this.payload.count) || this.payload.count < 1) { @@ -110,7 +110,7 @@ class LogWithCount extends EventLogger { } } -class LogWithExceptionInfo extends EventLogger { +class EventWithExceptionInfo extends Event { public validatePayload(): void { super.validatePayload.call(this); @@ -154,7 +154,7 @@ class LogWithExceptionInfo extends EventLogger { } } -class LogWithControlsInfo extends EventLogger { +class EventWithControlsInfo extends Event { public dump(): any { this.payload = { obj_val: this.payload?.text, @@ -164,27 +164,27 @@ class LogWithControlsInfo extends EventLogger { } } -export default function logFactory(logType: LogType, payload: any): EventLogger { - const logsWithCount = [ - LogType.deleteObject, - LogType.mergeObjects, - LogType.copyObject, - LogType.undoAction, - LogType.redoAction, - LogType.changeFrame, +export default function makeEvent(scope: EventScope, payload: any): Event { + const eventsWithCount = [ + EventScope.deleteObject, + EventScope.mergeObjects, + EventScope.copyObject, + EventScope.undoAction, + EventScope.redoAction, + EventScope.changeFrame, ]; - if (logsWithCount.includes(logType)) { - return new LogWithCount(logType, payload); + if (eventsWithCount.includes(scope)) { + return new EventWithCount(scope, payload); } - if (logType === LogType.exception) { - return new LogWithExceptionInfo(logType, payload); + if (scope === EventScope.exception) { + return new EventWithExceptionInfo(scope, payload); } - if (logType === LogType.clickElement) { - return new LogWithControlsInfo(logType, payload); + if (scope === EventScope.clickElement) { + return new EventWithControlsInfo(scope, payload); } - return new EventLogger(logType, payload); + return new Event(scope, payload); } diff --git a/cvat-core/src/index.ts b/cvat-core/src/index.ts index 52d40553eba8..e4bdde9ba091 100644 --- a/cvat-core/src/index.ts +++ b/cvat-core/src/index.ts @@ -2,18 +2,21 @@ // // SPDX-License-Identifier: MIT +import { + AnalyticsReportFilter, QualityConflictsFilter, QualityReportsFilter, QualitySettingsFilter, +} from './server-response-types'; import PluginRegistry from './plugins'; import serverProxy from './server-proxy'; import lambdaManager from './lambda-manager'; import { AnnotationFormats } from './annotation-formats'; -import loggerStorage from './logger-storage'; +import logger from './logger'; import * as enums from './enums'; import config from './config'; import { mask2Rle, rle2Mask } from './object-utils'; import User from './user'; import Project from './project'; import { Job, Task } from './session'; -import { EventLogger } from './log'; +import { Event } from './event'; import { Attribute, Label } from './labels'; import Statistics from './statistics'; import ObjectState from './object-state'; @@ -24,6 +27,10 @@ import { FrameData } from './frames'; import CloudStorage from './cloud-storage'; import Organization, { Invitation } from './organization'; import Webhook from './webhook'; +import QualityReport from './quality-report'; +import QualityConflict from './quality-conflict'; +import QualitySettings from './quality-settings'; +import AnalyticsReport from './analytics-report'; import AnnotationGuide from './guide'; import BaseSingleFrameAction, { listActions, registerAction, runActions } from './annotations-actions'; import { @@ -65,6 +72,7 @@ export default interface CVATCore { setAuthData: any; removeAuthData: any; installedApps: any; + apiSchema: typeof serverProxy.server.apiSchema; }; assets: { create: any; @@ -125,12 +133,14 @@ export default interface CVATCore { }; analytics: { quality: { - reports: any; - conflicts: any; - settings: any; + reports: (filter: QualityReportsFilter) => Promise>; + conflicts: (filter: QualityConflictsFilter) => Promise; + settings: { + get: (filter: QualitySettingsFilter) => Promise; + }; }; performance: { - reports: any; + reports: (filter: AnalyticsReportFilter) => Promise; }; }; frames: { @@ -141,7 +151,7 @@ export default interface CVATCore { register: typeof registerAction; run: typeof runActions; }; - logger: typeof loggerStorage; + logger: typeof logger; config: { backendAPI: typeof config.backendAPI; origin: typeof config.origin; @@ -169,7 +179,7 @@ export default interface CVATCore { Project: typeof Project; Task: typeof Task; Job: typeof Job; - EventLogger: typeof EventLogger; + Event: typeof Event; Attribute: typeof Attribute; Label: typeof Label; Statistics: typeof Statistics; diff --git a/cvat-core/src/logger-storage.ts b/cvat-core/src/logger.ts similarity index 52% rename from cvat-core/src/logger-storage.ts rename to cvat-core/src/logger.ts index f29507d53466..cfb3861e9462 100644 --- a/cvat-core/src/logger-storage.ts +++ b/cvat-core/src/logger.ts @@ -5,8 +5,8 @@ import PluginRegistry from './plugins'; import serverProxy from './server-proxy'; -import logFactory, { EventLogger } from './log'; -import { LogType } from './enums'; +import makeEvent, { Event } from './event'; +import { EventScope } from './enums'; import { ArgumentError } from './exceptions'; function sleep(ms): Promise { @@ -15,68 +15,68 @@ function sleep(ms): Promise { }); } -function defaultUpdate(previousLog: EventLogger, currentPayload: any): object { +function defaultUpdate(previousEvent: Event, currentPayload: any): object { return { - ...previousLog.payload, + ...previousEvent.payload, ...currentPayload, }; } interface IgnoreRule { - lastLog: EventLogger | null; + lastEvent: Event | null; timeThreshold?: number; - ignore: (previousLog: EventLogger, currentPayload: any) => boolean; - update: (previousLog: EventLogger, currentPayload: any) => object; + ignore: (previousEvent: Event, currentPayload: any) => boolean; + update: (previousEvent: Event, currentPayload: any) => object; } -type IgnoredRules = LogType.zoomImage | LogType.changeAttribute | LogType.changeFrame; +type IgnoredRules = EventScope.zoomImage | EventScope.changeAttribute | EventScope.changeFrame; -class LoggerStorage { +class Logger { public clientID: string; - public collection: Array; + public collection: Array; public ignoreRules: Record; public isActiveChecker: (() => boolean) | null; public saving: boolean; - public compressedLogs: Array; + public compressedScopes: Array; constructor() { this.clientID = Date.now().toString().substr(-6); this.collection = []; this.isActiveChecker = null; this.saving = false; - this.compressedLogs = [LogType.changeFrame]; + this.compressedScopes = [EventScope.changeFrame]; this.ignoreRules = { - [LogType.zoomImage]: { - lastLog: null, + [EventScope.zoomImage]: { + lastEvent: null, timeThreshold: 4000, - ignore(previousLog: EventLogger): boolean { - return (Date.now() - previousLog.time.getTime()) < this.timeThreshold; + ignore(previousEvent: Event): boolean { + return (Date.now() - previousEvent.timestamp.getTime()) < this.timeThreshold; }, update: defaultUpdate, }, - [LogType.changeAttribute]: { - lastLog: null, - ignore(previousLog: EventLogger, currentPayload: any): boolean { + [EventScope.changeAttribute]: { + lastEvent: null, + ignore(previousEvent: Event, currentPayload: any): boolean { return ( - currentPayload.object_id === previousLog.payload.object_id && - currentPayload.id === previousLog.payload.id + currentPayload.object_id === previousEvent.payload.object_id && + currentPayload.id === previousEvent.payload.id ); }, update: defaultUpdate, }, - [LogType.changeFrame]: { - lastLog: null, - ignore(previousLog: EventLogger, currentPayload: any): boolean { + [EventScope.changeFrame]: { + lastEvent: null, + ignore(previousEvent: Event, currentPayload: any): boolean { return ( - currentPayload.job_id === previousLog.payload.job_id && - currentPayload.step === previousLog.payload.step + currentPayload.job_id === previousEvent.payload.job_id && + currentPayload.step === previousEvent.payload.step ); }, - update(previousLog: EventLogger, currentPayload: any): object { + update(previousEvent: Event, currentPayload: any): object { return { - ...previousLog.payload, + ...previousEvent.payload, to: currentPayload.to, - count: previousLog.payload.count + 1, + count: previousEvent.payload.count + 1, }; }, }, @@ -86,29 +86,31 @@ class LoggerStorage { public async configure(isActiveChecker, activityHelper): Promise { const result = await PluginRegistry.apiWrapper.call( this, - LoggerStorage.prototype.configure, + Logger.prototype.configure, isActiveChecker, activityHelper, ); return result; } - public async log(logType: LogType, payload = {}, wait = false): Promise { - const result = await PluginRegistry.apiWrapper.call(this, LoggerStorage.prototype.log, logType, payload, wait); + public async log(scope: EventScope, payload = {}, wait = false): Promise { + const result = await PluginRegistry.apiWrapper.call(this, Logger.prototype.log, scope, payload, wait); return result; } public async save(): Promise { - const result = await PluginRegistry.apiWrapper.call(this, LoggerStorage.prototype.save); + const result = await PluginRegistry.apiWrapper.call(this, Logger.prototype.save); return result; } } -Object.defineProperties(LoggerStorage.prototype.configure, { +Object.defineProperties(Logger.prototype.configure, { implementation: { writable: false, enumerable: false, - value: async function implementation(isActiveChecker: () => boolean, userActivityCallback: Array) { + value: async function implementation( + this: Logger, isActiveChecker: () => boolean, userActivityCallback: Array, + ) { if (typeof isActiveChecker !== 'function') { throw new ArgumentError('isActiveChecker argument must be callable'); } @@ -122,11 +124,11 @@ Object.defineProperties(LoggerStorage.prototype.configure, { }, }); -Object.defineProperties(LoggerStorage.prototype.log, { +Object.defineProperties(Logger.prototype.log, { implementation: { writable: false, enumerable: false, - value: async function implementation(logType: LogType, payload: any, wait: boolean) { + value: async function implementation(this: Logger, scope: EventScope, payload: any, wait: boolean) { if (typeof payload !== 'object') { throw new ArgumentError('Payload must be an object'); } @@ -135,56 +137,56 @@ Object.defineProperties(LoggerStorage.prototype.log, { throw new ArgumentError('Wait must be boolean'); } - if (!this.compressedLogs.includes(logType)) { - this.compressedLogs.forEach((compressedType: LogType) => { - this.ignoreRules[compressedType].lastLog = null; + if (!(this.compressedScopes as string[]).includes(scope)) { + this.compressedScopes.forEach((compressedScope) => { + this.ignoreRules[compressedScope].lastEvent = null; }); } - if (logType in this.ignoreRules) { - const ignoreRule = this.ignoreRules[logType]; - const { lastLog } = ignoreRule; - if (lastLog && ignoreRule.ignore(lastLog, payload)) { - lastLog.payload = ignoreRule.update(lastLog, payload); + if (scope in this.ignoreRules) { + const ignoreRule = this.ignoreRules[scope as IgnoredRules]; + const { lastEvent } = ignoreRule; + if (lastEvent && ignoreRule.ignore(lastEvent, payload)) { + lastEvent.payload = ignoreRule.update(lastEvent, payload); - return ignoreRule.lastLog; + return ignoreRule.lastEvent; } } - const logPayload = { ...payload }; - logPayload.client_id = this.clientID; + const eventPayload = { ...payload }; + eventPayload.client_id = this.clientID; if (this.isActiveChecker) { - logPayload.is_active = this.isActiveChecker(); + eventPayload.is_active = this.isActiveChecker(); } - const log = logFactory(logType, { ...logPayload }); + const event = makeEvent(scope, { ...eventPayload }); const pushEvent = (): void => { - log.validatePayload(); - log.onClose(null); - this.collection.push(log); + event.validatePayload(); + event.onClose(null); + this.collection.push(event); - if (logType in this.ignoreRules) { - this.ignoreRules[logType].lastLog = log; + if (scope in this.ignoreRules) { + this.ignoreRules[scope as IgnoredRules].lastEvent = event; } }; if (wait) { - log.onClose(pushEvent); + event.onClose(pushEvent); } else { pushEvent(); } - return log; + return event; }, }, }); -Object.defineProperties(LoggerStorage.prototype.save, { +Object.defineProperties(Logger.prototype.save, { implementation: { writable: false, enumerable: false, - value: async function implementation() { + value: async function implementation(this: Logger) { if (!this.collection.length) { return; } @@ -193,12 +195,12 @@ Object.defineProperties(LoggerStorage.prototype.save, { await sleep(1000); } - const logPayload: any = { + const eventPayload: any = { client_id: this.clientID, }; if (this.isActiveChecker) { - logPayload.is_active = this.isActiveChecker(); + eventPayload.is_active = this.isActiveChecker(); } const collectionToSend = [...this.collection]; @@ -206,12 +208,12 @@ Object.defineProperties(LoggerStorage.prototype.save, { this.saving = true; this.collection = []; await serverProxy.events.save({ - events: collectionToSend.map((log) => log.dump()), + events: collectionToSend.map((event) => event.dump()), timestamp: new Date().toISOString(), }); for (const rule of Object.values(this.ignoreRules)) { - rule.lastLog = null; + rule.lastEvent = null; } } catch (error: unknown) { // if failed, put collection back @@ -225,4 +227,4 @@ Object.defineProperties(LoggerStorage.prototype.save, { }, }); -export default new LoggerStorage(); +export default new Logger(); diff --git a/cvat-core/src/quality-conflict.ts b/cvat-core/src/quality-conflict.ts index e9db6d012aca..3d7252f37e87 100644 --- a/cvat-core/src/quality-conflict.ts +++ b/cvat-core/src/quality-conflict.ts @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: MIT +import { SerializedAnnotationConflictData, SerializedQualityConflictData } from './server-response-types'; import { ObjectType } from './enums'; export enum QualityConflictType { @@ -15,25 +16,6 @@ export enum ConflictSeverity { WARNING = 'warning', } -export interface SerializedQualityConflictData { - id?: number; - frame?: number; - type?: string; - annotation_ids?: SerializedAnnotationConflictData[]; - data?: string; - severity?: string; - description?: string; -} - -export interface SerializedAnnotationConflictData { - job_id?: number; - obj_id?: number; - type?: ObjectType; - shape_type?: string | null; - conflict_type?: string; - severity?: string; -} - export class AnnotationConflict { #jobID: number; #serverID: number; diff --git a/cvat-core/src/quality-report.ts b/cvat-core/src/quality-report.ts index e174d1ff9e75..652241527e86 100644 --- a/cvat-core/src/quality-report.ts +++ b/cvat-core/src/quality-report.ts @@ -2,35 +2,7 @@ // // SPDX-License-Identifier: MIT -export interface SerializedQualityReportData { - id?: number; - parent_id?: number; - task_id?: number; - job_id?: number; - target: string; - created_date?: string; - gt_last_updated?: string; - summary?: { - frame_count: number, - frame_share: number, - conflict_count: number, - valid_count: number, - ds_count: number, - gt_count: number, - error_count: number, - warning_count: number, - conflicts_by_type: { - extra_annotation: number, - missing_annotation: number, - mismatching_label: number, - low_overlap: number, - mismatching_direction: number, - mismatching_attributes: number, - mismatching_groups: number, - covered_annotation: number, - } - }; -} +import { SerializedQualityReportData } from './server-response-types'; export interface QualitySummary { frameCount: number; @@ -58,9 +30,9 @@ export interface QualitySummary { export default class QualityReport { #id: number; - #parentId: number; - #taskId: number; - #jobId: number; + #parentID: number; + #taskID: number; + #jobID: number; #target: string; #createdDate: string; #gtLastUpdated: string; @@ -68,9 +40,9 @@ export default class QualityReport { constructor(initialData: SerializedQualityReportData) { this.#id = initialData.id; - this.#parentId = initialData.parent_id; - this.#taskId = initialData.task_id; - this.#jobId = initialData.job_id; + this.#parentID = initialData.parent_id; + this.#taskID = initialData.task_id; + this.#jobID = initialData.job_id; this.#target = initialData.target; this.#gtLastUpdated = initialData.gt_last_updated; this.#createdDate = initialData.created_date; @@ -81,16 +53,16 @@ export default class QualityReport { return this.#id; } - get parentId(): number { - return this.#parentId; + get parentID(): number { + return this.#parentID; } - get taskId(): number { - return this.#taskId; + get taskID(): number { + return this.#taskID; } - get jobId(): number { - return this.#jobId; + get jobID(): number { + return this.#jobID; } get target(): string { diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index 3e9fd8ddff6f..c258ff4be42e 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -14,18 +14,17 @@ import { SerializedLabel, SerializedAnnotationFormats, ProjectsFilter, SerializedProject, SerializedTask, TasksFilter, SerializedUser, SerializedOrganization, SerializedAbout, SerializedRemoteFile, SerializedUserAgreement, - SerializedRegister, JobsFilter, SerializedJob, SerializedGuide, SerializedAsset, - SerializedQualitySettingsData, SerializedInvitationData, SerializedCloudStorage, - SerializedFramesMetaData, SerializedCollection, + SerializedRegister, JobsFilter, SerializedJob, SerializedGuide, SerializedAsset, SerializedAPISchema, + SerializedInvitationData, SerializedCloudStorage, SerializedFramesMetaData, SerializedCollection, + SerializedQualitySettingsData, ApiQualitySettingsFilter, SerializedQualityConflictData, ApiQualityConflictsFilter, + SerializedQualityReportData, ApiQualityReportsFilter, SerializedAnalyticsReport, ApiAnalyticsReportFilter, } from './server-response-types'; -import { SerializedQualityReportData } from './quality-report'; -import { SerializedAnalyticsReport } from './analytics-report'; +import { PaginatedResource } from './core-types'; import { Storage } from './storage'; import { RQStatus, StorageLocation, WebhookSourceType } from './enums'; import { isEmail, isResourceURL } from './common'; import config from './config'; import { ServerError } from './exceptions'; -import { SerializedQualityConflictData } from './quality-conflict'; type Params = { org: number | string, @@ -1899,6 +1898,17 @@ async function installedApps() { } } +async function getApiSchema(): Promise { + const { backendAPI } = config; + + try { + const response = await Axios.get(`${backendAPI}/schema/?scheme=json`); + return response.data; + } catch (errorData) { + throw generateError(errorData); + } +} + async function createCloudStorage(storageDetail) { const { backendAPI } = config; @@ -2310,13 +2320,15 @@ async function createAsset(file: File, guideId: number): Promise { +async function getQualitySettings( + filter: ApiQualitySettingsFilter, +): Promise { const { backendAPI } = config; try { const response = await Axios.get(`${backendAPI}/quality/settings`, { params: { - task_id: taskID, + ...filter, }, }); @@ -2344,7 +2356,9 @@ async function updateQualitySettings( } } -async function getQualityConflicts(filter): Promise { +async function getQualityConflicts( + filter: ApiQualityConflictsFilter, +): Promise { const params = enableOrganization(); const { backendAPI } = config; @@ -2360,7 +2374,9 @@ async function getQualityConflicts(filter): Promise { +async function getQualityReports( + filter: ApiQualityReportsFilter, +): Promise> { const { backendAPI } = config; try { @@ -2370,13 +2386,16 @@ async function getQualityReports(filter): Promise }, }); + response.data.results.count = response.data.count; return response.data.results; } catch (errorData) { throw generateError(errorData); } } -async function getAnalyticsReports(filter): Promise { +async function getAnalyticsReports( + filter: ApiAnalyticsReportFilter, +): Promise { const { backendAPI } = config; try { @@ -2410,6 +2429,7 @@ export default Object.freeze({ request: serverRequest, userAgreements, installedApps, + apiSchema: getApiSchema, }), projects: Object.freeze({ diff --git a/cvat-core/src/server-response-types.ts b/cvat-core/src/server-response-types.ts index 01b4faf6eb49..5a0f070fb11b 100644 --- a/cvat-core/src/server-response-types.ts +++ b/cvat-core/src/server-response-types.ts @@ -7,8 +7,9 @@ import { DimensionType, JobStage, JobState, JobType, ProjectStatus, ShapeType, StorageLocation, LabelType, ShareFileType, Source, TaskMode, TaskStatus, - CloudStorageCredentialsType, CloudStorageProviderType, + CloudStorageCredentialsType, CloudStorageProviderType, ObjectType, } from './enums'; +import { Camelized } from './type-utils'; export interface SerializedAnnotationImporter { name: string; @@ -24,12 +25,19 @@ export interface SerializedAnnotationFormats { importers: SerializedAnnotationImporter[]; exporters: SerializedAnnotationExporter[]; } -export interface ProjectsFilter { + +export interface ApiCommonFilterParams { page?: number; - id?: number; + page_size?: number | 'all'; + filter?: string; sort?: string; + org_id?: number; + org?: string; search?: string; - filter?: string; +} + +export interface ProjectsFilter extends ApiCommonFilterParams { + id?: number; } export interface SerializedUser { @@ -225,6 +233,11 @@ export interface SerializedOrganization { contact?: SerializedOrganizationContact, } +export interface ApiQualitySettingsFilter extends ApiCommonFilterParams { + task_id?: number; +} +export type QualitySettingsFilter = Camelized; + export interface SerializedQualitySettingsData { id?: number; task?: number; @@ -242,6 +255,111 @@ export interface SerializedQualitySettingsData { compare_attributes?: boolean; } +export interface ApiQualityConflictsFilter extends ApiCommonFilterParams { + report_id?: number; +} +export type QualityConflictsFilter = Camelized; + +export interface SerializedAnnotationConflictData { + job_id?: number; + obj_id?: number; + type?: ObjectType; + shape_type?: string | null; + conflict_type?: string; + severity?: string; +} + +export interface SerializedQualityConflictData { + id?: number; + frame?: number; + type?: string; + annotation_ids?: SerializedAnnotationConflictData[]; + data?: string; + severity?: string; + description?: string; +} + +export interface ApiQualityReportsFilter extends ApiCommonFilterParams { + parent_id?: number; + peoject_id?: number; + task_id?: number; + job_id?: number; + target?: string; +} +export type QualityReportsFilter = Camelized; + +export interface SerializedQualityReportData { + id?: number; + parent_id?: number; + task_id?: number; + job_id?: number; + target: string; + created_date?: string; + gt_last_updated?: string; + summary?: { + frame_count: number; + frame_share: number; + conflict_count: number; + valid_count: number; + ds_count: number; + gt_count: number; + error_count: number; + warning_count: number; + conflicts_by_type: { + extra_annotation: number; + missing_annotation: number; + mismatching_label: number; + low_overlap: number; + mismatching_direction: number; + mismatching_attributes: number; + mismatching_groups: number; + covered_annotation: number; + } + }; +} + +export interface SerializedDataEntry { + date?: string; + value?: number | Record +} + +export interface SerializedTransformBinaryOp { + left: string; + operator: string; + right: string; +} + +export interface SerializedTransformationEntry { + name: string; + binary?: SerializedTransformBinaryOp; +} + +export interface SerializedAnalyticsEntry { + name?: string; + title?: string; + description?: string; + granularity?: string; + default_view?: string; + data_series?: Record; + transformations?: SerializedTransformationEntry[]; +} + +export interface ApiAnalyticsReportFilter extends ApiCommonFilterParams { + project_id?: number; + task_id?: number; + job_id?: number; + start_date?: string; + end_date?: string; +} +export type AnalyticsReportFilter = Camelized; + +export interface SerializedAnalyticsReport { + id?: number; + target?: string; + created_date?: string; + statistics?: SerializedAnalyticsEntry[]; +} + export interface SerializedInvitationData { created_date: string; key: string; @@ -344,3 +462,33 @@ export interface SerializedFramesMetaData { start_frame: number; stop_frame: number; } + +export interface SerializedAPISchema { + openapi: string; + info: { + version: string; + description: string; + termsOfService: string; + contact: { + name: string; + url: string; + email: string; + }; + license: { + name: string; + url: string; + } + }; + paths: { + [path: string]: any; + }; + components: { + schemas: { + [component: string]: any; + } + } + externalDocs: { + description: string; + url: string; + }; +} diff --git a/cvat-core/src/session-implementation.ts b/cvat-core/src/session-implementation.ts index 71230a170776..44e8639a8796 100644 --- a/cvat-core/src/session-implementation.ts +++ b/cvat-core/src/session-implementation.ts @@ -8,7 +8,7 @@ import { ArgumentError } from './exceptions'; import { HistoryActions, JobType, RQStatus } from './enums'; import { Storage } from './storage'; import { Task as TaskClass, Job as JobClass } from './session'; -import loggerStorage from './logger-storage'; +import logger from './logger'; import serverProxy from './server-proxy'; import { getFrame, @@ -364,9 +364,9 @@ export function implementJob(Job) { return getHistory(this).get(); }; - Job.prototype.logger.log.implementation = async function (logType, payload, wait) { - const result = await loggerStorage.log( - logType, + Job.prototype.logger.log.implementation = async function (scope, payload, wait) { + const result = await logger.log( + scope, { ...payload, project_id: this.projectId, @@ -824,9 +824,9 @@ export function implementTask(Task) { return getHistory(this).get(); }; - Task.prototype.logger.log.implementation = async function (logType, payload, wait) { - const result = await loggerStorage.log( - logType, + Task.prototype.logger.log.implementation = async function (scope, payload, wait) { + const result = await logger.log( + scope, { ...payload, project_id: this.projectId, diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts index aa62950ee398..8c9aea99aa69 100644 --- a/cvat-core/src/session.ts +++ b/cvat-core/src/session.ts @@ -271,11 +271,11 @@ function buildDuplicatedAPI(prototype) { }), logger: Object.freeze({ value: { - async log(logType, payload = {}, wait = false) { + async log(scope, payload = {}, wait = false) { const result = await PluginRegistry.apiWrapper.call( this, prototype.logger.log, - logType, + scope, payload, wait, ); diff --git a/cvat-core/src/type-utils.ts b/cvat-core/src/type-utils.ts new file mode 100644 index 000000000000..1b8423e8d1a7 --- /dev/null +++ b/cvat-core/src/type-utils.ts @@ -0,0 +1,15 @@ +// Copyright (C) 2024 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +type CamelizeString = +T extends string ? string extends T ? string : + T extends `${infer F}_${infer R}` ? + CamelizeString, `${C}${F}`> : (T extends 'Id' ? `${C}${'ID'}` : `${C}${T}`) : T; + +// https://stackoverflow.com/a/63715429 +// Use https://stackoverflow.com/a/64933956 for snake-ization +/** + * Returns the input type with fields in CamelCase + */ +export type Camelized = { [K in keyof T as CamelizeString]: T[K] }; diff --git a/cvat-sdk/gen/generate.sh b/cvat-sdk/gen/generate.sh index 6145e154a542..0ce2af39efd0 100755 --- a/cvat-sdk/gen/generate.sh +++ b/cvat-sdk/gen/generate.sh @@ -8,7 +8,7 @@ set -e GENERATOR_VERSION="v6.0.1" -VERSION="2.10.3" +VERSION="2.11.0" LIB_NAME="cvat_sdk" LAYER1_LIB_NAME="${LIB_NAME}/api_client" DST_DIR="$(cd "$(dirname -- "$0")/.." && pwd)" diff --git a/cvat-ui/package.json b/cvat-ui/package.json index bf7b1bfbc599..21fe7cd13727 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.61.4", + "version": "1.62.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/analytics-actions.ts b/cvat-ui/src/actions/analytics-actions.ts deleted file mode 100644 index 3c95e7476dbc..000000000000 --- a/cvat-ui/src/actions/analytics-actions.ts +++ /dev/null @@ -1,107 +0,0 @@ -// Copyright (C) 2023 CVAT.ai Corporation -// -// SPDX-License-Identifier: MIT - -import { - getCore, QualityReport, QualitySettings, Task, -} from 'cvat-core-wrapper'; -import { Dispatch, ActionCreator } from 'redux'; -import { QualityQuery } from 'reducers'; -import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; - -const cvat = getCore(); - -export enum AnalyticsActionsTypes { - GET_QUALITY_REPORTS = 'GET_QUALITY_REPORTS', - GET_QUALITY_REPORTS_SUCCESS = 'GET_QUALITY_REPORTS_SUCCESS', - GET_QUALITY_REPORTS_FAILED = 'GET_QUALITY_REPORTS_FAILED', - SWITCH_QUALITY_SETTINGS_VISIBLE = 'SWITCH_QUALITY_SETTINGS_VISIBLE', - GET_QUALITY_SETTINGS = 'GET_QUALITY_SETTINS', - GET_QUALITY_SETTINGS_SUCCESS = 'GET_QUALITY_SETTINGS_SUCCESS', - GET_QUALITY_SETTINGS_FAILED = 'GET_QUALITY_SETTINGS_FAILED', - UPDATE_QUALITY_SETTINGS = 'UPDATE_QUALITY_SETTINGS', - UPDATE_QUALITY_SETTINGS_SUCCESS = 'UPDATE_QUALITY_SETTINGS_SUCCESS', - UPDATE_QUALITY_SETTINGS_FAILED = 'UPDATE_QUALITY_SETTINGS_FAILED', -} - -export const analyticsActions = { - getQualityReports: (task: Task, query: QualityQuery) => ( - createAction(AnalyticsActionsTypes.GET_QUALITY_REPORTS, { query }) - ), - getQualityReportsSuccess: (tasksReports: QualityReport[], jobsReports: QualityReport[]) => createAction( - AnalyticsActionsTypes.GET_QUALITY_REPORTS_SUCCESS, { tasksReports, jobsReports }, - ), - getQualityReportsFailed: (error: any) => createAction(AnalyticsActionsTypes.GET_QUALITY_REPORTS_FAILED, { error }), - switchQualitySettingsVisible: (visible: boolean) => ( - createAction(AnalyticsActionsTypes.SWITCH_QUALITY_SETTINGS_VISIBLE, { visible }) - ), - getQualitySettings: (settingsID: number) => ( - createAction(AnalyticsActionsTypes.GET_QUALITY_SETTINGS, { settingsID }) - ), - getQualitySettingsSuccess: (settings: QualitySettings) => ( - createAction(AnalyticsActionsTypes.GET_QUALITY_SETTINGS_SUCCESS, { settings }) - ), - getQualitySettingsFailed: (error: any) => ( - createAction(AnalyticsActionsTypes.GET_QUALITY_SETTINGS_FAILED, { error }) - ), - updateQualitySettings: (settings: QualitySettings) => ( - createAction(AnalyticsActionsTypes.UPDATE_QUALITY_SETTINGS, { settings }) - ), - updateQualitySettingsSuccess: (settings: QualitySettings) => ( - createAction(AnalyticsActionsTypes.UPDATE_QUALITY_SETTINGS_SUCCESS, { settings }) - ), - updateQualitySettingsFailed: (error: any) => ( - createAction(AnalyticsActionsTypes.UPDATE_QUALITY_SETTINGS_FAILED, { error }) - ), -}; - -export const getQualityReportsAsync = (task: Task, query: QualityQuery): ThunkAction => ( - async (dispatch: ActionCreator): Promise => { - dispatch(analyticsActions.getQualityReports(task, query)); - - try { - // reports are returned in order -created_date - const [taskReport] = await cvat.analytics.quality.reports({ taskId: task.id, target: 'task' }); - const jobReports = await cvat.analytics.quality.reports({ taskId: task.id, target: 'job' }); - const jobIds = task.jobs.map((job) => job.id); - const relevantReports: QualityReport[] = []; - jobIds.forEach((jobId: number) => { - const report = jobReports.find((_report: QualityReport) => _report.jobId === jobId); - if (report) relevantReports.push(report); - }); - - dispatch(analyticsActions.getQualityReportsSuccess(taskReport ? [taskReport] : [], relevantReports)); - } catch (error) { - dispatch(analyticsActions.getQualityReportsFailed(error)); - } - } -); - -export const getQualitySettingsAsync = (task: Task): ThunkAction => ( - async (dispatch: ActionCreator): Promise => { - dispatch(analyticsActions.getQualitySettings(task.id)); - try { - const qualitySettings = await cvat.analytics.quality.settings.get(task.id); - - dispatch(analyticsActions.getQualitySettingsSuccess(qualitySettings)); - } catch (error) { - dispatch(analyticsActions.getQualityReportsFailed(error)); - } - } -); - -export const updateQualitySettingsAsync = (qualitySettings: QualitySettings): ThunkAction => ( - async (dispatch: ActionCreator): Promise => { - dispatch(analyticsActions.updateQualitySettings(qualitySettings)); - - try { - const updatedQualitySettings = await qualitySettings.save(); - dispatch(analyticsActions.updateQualitySettingsSuccess(updatedQualitySettings)); - } catch (error) { - dispatch(analyticsActions.updateQualitySettingsFailed(error)); - throw error; - } - } -); - -export type AnalyticsActions = ActionUnion; diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index ac3be64331b7..d07c62cad1d6 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -15,7 +15,7 @@ import { import { getCore, MLModel, JobType, Job, QualityConflict, } from 'cvat-core-wrapper'; -import logger, { LogType } from 'cvat-logger'; +import logger, { EventScope } from 'cvat-logger'; import { getCVATStore } from 'cvat-store'; import { @@ -472,7 +472,7 @@ export function propagateObjectAsync(from: number, to: number): ThunkAction { }); const copy = getCopyFromState(objectState); - await sessionInstance.logger.log(LogType.propagateObject, { count: Math.abs(to - from) }); + await sessionInstance.logger.log(EventScope.propagateObject, { count: Math.abs(to - from) }); const states = []; const sign = Math.sign(to - from); for (let frame = from + sign; sign > 0 ? frame <= to : frame >= to; frame += sign) { @@ -501,7 +501,7 @@ export function propagateObjectAsync(from: number, to: number): ThunkAction { export function removeObjectAsync(sessionInstance: NonNullable, objectState: any, force: boolean): ThunkAction { return async (dispatch: ActionCreator): Promise => { try { - await sessionInstance.logger.log(LogType.deleteObject, { count: 1 }); + await sessionInstance.logger.log(EventScope.deleteObject, { count: 1 }); const { frame } = receiveAnnotationsParameters(); const removed = await objectState.delete(frame, force); @@ -541,7 +541,7 @@ export function removeObject(objectState: any, force: boolean): AnyAction { export function copyShape(objectState: any): AnyAction { const job = getStore().getState().annotation.job.instance; - job?.logger.log(LogType.copyObject, { count: 1 }); + job?.logger.log(EventScope.copyObject, { count: 1 }); return { type: AnnotationActionTypes.COPY_SHAPE, @@ -611,7 +611,7 @@ export function confirmCanvasReadyAsync(): ThunkAction { try { const state: CombinedState = getState(); const { instance: job } = state.annotation.job; - const { changeFrameLog } = state.annotation.player.frame; + const { changeFrameEvent } = state.annotation.player.frame; const chunks = await job.frames.cachedChunks() as number[]; const { startFrame, stopFrame, dataChunkSize } = job; @@ -631,7 +631,7 @@ export function confirmCanvasReadyAsync(): ThunkAction { }, []).map(([start, end]) => `${start}:${end}`).join(';'); dispatch(confirmCanvasReady(ranges)); - await changeFrameLog?.close(); + await changeFrameEvent?.close(); } catch (error) { // even if error happens here, do not need to notify the users dispatch(confirmCanvasReady()); @@ -677,7 +677,7 @@ export function changeFrameAsync( payload: {}, }); - const changeFrameLog = await job.logger.log(LogType.changeFrame, { + const changeFrameEvent = await job.logger.log(EventScope.changeFrame, { from: frame, to: toFrame, step: toFrame - frame, @@ -718,7 +718,7 @@ export function changeFrameAsync( curZ: maxZ, changeTime: currentTime + delay, delay, - changeFrameLog, + changeFrameEvent, }, }); } catch (error) { @@ -745,7 +745,7 @@ export function undoActionAsync(): ThunkAction { const [undo] = state.annotation.annotations.history.undo.slice(-1); const undoOnFrame = undo[1]; const undoLog = await jobInstance.logger.log( - LogType.undoAction, + EventScope.undoAction, { name: undo[0], frame: undo[1], @@ -784,7 +784,7 @@ export function redoActionAsync(): ThunkAction { const [redo] = state.annotation.annotations.history.redo.slice(-1); const redoOnFrame = redo[1]; const redoLog = await jobInstance.logger.log( - LogType.redoAction, + EventScope.redoAction, { name: redo[0], frame: redo[1], @@ -833,7 +833,7 @@ export function rotateCurrentFrame(rotation: Rotation): AnyAction { const frameAngle = (frameAngles[frameNumber - startFrame] + (rotation === Rotation.CLOCKWISE90 ? 90 : 270)) % 360; - job.logger.log(LogType.rotateImage, { angle: frameAngle }); + job.logger.log(EventScope.rotateImage, { angle: frameAngle }); return { type: AnnotationActionTypes.ROTATE_FRAME, @@ -898,7 +898,7 @@ export function getJobAsync({ } const loadJobEvent = await logger.log( - LogType.loadJob, + EventScope.loadJob, { task_id: taskID, job_id: jobID, @@ -946,9 +946,9 @@ export function getJobAsync({ let conflicts: QualityConflict[] = []; if (gtJob) { - const [report] = await cvat.analytics.quality.reports({ jobId: job.id, target: 'job' }); + const [report] = await cvat.analytics.quality.reports({ jobID: job.id, target: 'job' }); if (report) { - conflicts = await cvat.analytics.quality.conflicts({ reportId: report.id }); + conflicts = await cvat.analytics.quality.conflicts({ reportID: report.id }); } } @@ -999,7 +999,7 @@ export function saveAnnotationsAsync(afterSave?: () => void): ThunkAction { }); try { - const saveJobEvent = await jobInstance.logger.log(LogType.saveJob, {}, true); + const saveJobEvent = await jobInstance.logger.log(EventScope.saveJob, {}, true); await jobInstance.frames.save(); await jobInstance.annotations.save(); diff --git a/cvat-ui/src/actions/auth-actions.ts b/cvat-ui/src/actions/auth-actions.ts index 3374907ac0ca..9d283cd5c76a 100644 --- a/cvat-ui/src/actions/auth-actions.ts +++ b/cvat-ui/src/actions/auth-actions.ts @@ -6,7 +6,6 @@ import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; import { RegisterData } from 'components/register-page/register-form'; import { getCore, User } from 'cvat-core-wrapper'; -import isReachable from 'utils/url-checker'; const cvat = getCore(); @@ -33,9 +32,6 @@ export enum AuthActionTypes { RESET_PASSWORD = 'RESET_PASSWORD_CONFIRM', RESET_PASSWORD_SUCCESS = 'RESET_PASSWORD_CONFIRM_SUCCESS', RESET_PASSWORD_FAILED = 'RESET_PASSWORD_CONFIRM_FAILED', - LOAD_AUTH_ACTIONS = 'LOAD_AUTH_ACTIONS', - LOAD_AUTH_ACTIONS_SUCCESS = 'LOAD_AUTH_ACTIONS_SUCCESS', - LOAD_AUTH_ACTIONS_FAILED = 'LOAD_AUTH_ACTIONS_FAILED', } export const authActions = { @@ -65,14 +61,6 @@ export const authActions = { resetPassword: () => createAction(AuthActionTypes.RESET_PASSWORD), resetPasswordSuccess: () => createAction(AuthActionTypes.RESET_PASSWORD_SUCCESS), resetPasswordFailed: (error: any) => createAction(AuthActionTypes.RESET_PASSWORD_FAILED, { error }), - loadServerAuthActions: () => createAction(AuthActionTypes.LOAD_AUTH_ACTIONS), - loadServerAuthActionsSuccess: (allowChangePassword: boolean, allowResetPassword: boolean) => ( - createAction(AuthActionTypes.LOAD_AUTH_ACTIONS_SUCCESS, { - allowChangePassword, - allowResetPassword, - }) - ), - loadServerAuthActionsFailed: (error: any) => createAction(AuthActionTypes.LOAD_AUTH_ACTIONS_FAILED, { error }), }; export type AuthActions = ActionUnion; @@ -188,19 +176,3 @@ export const resetPasswordAsync = ( dispatch(authActions.resetPasswordFailed(error)); } }; - -export const loadAuthActionsAsync = (): ThunkAction => async (dispatch) => { - dispatch(authActions.loadServerAuthActions()); - - try { - const promises: Promise[] = [ - isReachable(`${cvat.config.backendAPI}/auth/password/change`, 'OPTIONS'), - isReachable(`${cvat.config.backendAPI}/auth/password/reset`, 'OPTIONS'), - ]; - const [allowChangePassword, allowResetPassword] = await Promise.all(promises); - - dispatch(authActions.loadServerAuthActionsSuccess(allowChangePassword, allowResetPassword)); - } catch (error) { - dispatch(authActions.loadServerAuthActionsFailed(error)); - } -}; diff --git a/cvat-ui/src/actions/boundaries-actions.ts b/cvat-ui/src/actions/boundaries-actions.ts index a29693518d5d..097be2ec491a 100644 --- a/cvat-ui/src/actions/boundaries-actions.ts +++ b/cvat-ui/src/actions/boundaries-actions.ts @@ -7,7 +7,7 @@ import { ActionUnion, createAction, ThunkAction, ThunkDispatch, } from 'utils/redux'; import { getCore } from 'cvat-core-wrapper'; -import { LogType } from 'cvat-logger'; +import { EventScope } from 'cvat-logger'; import { fetchAnnotationsAsync } from './annotation-actions'; const cvat = getCore(); @@ -46,7 +46,7 @@ export function resetAfterErrorAsync(): ThunkAction { const frameData = await job.frames.get(frameNumber); const colors = [...cvat.enums.colors]; - await job.logger.log(LogType.restoreJob); + await job.logger.log(EventScope.restoreJob); dispatch(boundariesActions.resetAfterError({ job, diff --git a/cvat-ui/src/actions/import-actions.ts b/cvat-ui/src/actions/import-actions.ts index ed2b53408065..4f54124daaa1 100644 --- a/cvat-ui/src/actions/import-actions.ts +++ b/cvat-ui/src/actions/import-actions.ts @@ -6,7 +6,7 @@ import { createAction, ActionUnion, ThunkAction } from 'utils/redux'; import { CombinedState } from 'reducers'; import { getCore, Storage } from 'cvat-core-wrapper'; -import { LogType } from 'cvat-logger'; +import { EventScope } from 'cvat-logger'; import { getProjectsAsync } from './projects-actions'; import { AnnotationActionTypes, fetchAnnotationsAsync } from './annotation-actions'; @@ -109,7 +109,7 @@ export const importDatasetAsync = ( dispatch(importActions.importDataset(instance, format)); await instance.annotations.upload(format, useDefaultSettings, sourceStorage, file, { convMaskToPoly }); - await instance.logger.log(LogType.uploadAnnotations); + await instance.logger.log(EventScope.uploadAnnotations); await instance.annotations.clear(true); await instance.actions.clear(); const history = await instance.actions.get(); diff --git a/cvat-ui/src/actions/server-actions.ts b/cvat-ui/src/actions/server-actions.ts new file mode 100644 index 000000000000..b368853d04e2 --- /dev/null +++ b/cvat-ui/src/actions/server-actions.ts @@ -0,0 +1,37 @@ +// Copyright (C) 2024 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; +import { getCore, SerializedAPISchema } from 'cvat-core-wrapper'; + +const core = getCore(); + +export enum ServerAPIActionTypes { + GET_SERVER_API_SCHEMA = 'GET_SERVER_API_SCHEMA', + GET_SERVER_API_SCHEMA_SUCCESS = 'GET_SERVER_API_SCHEMA_SUCCESS', + GET_SERVER_API_SCHEMA_FAILED = 'GET_SERVER_API_SCHEMA_FAILED', +} + +const serverAPIActions = { + getServerAPISchema: () => createAction(ServerAPIActionTypes.GET_SERVER_API_SCHEMA), + getServerAPISchemaSuccess: (schema: SerializedAPISchema) => ( + createAction(ServerAPIActionTypes.GET_SERVER_API_SCHEMA_SUCCESS, { schema }) + ), + getServerAPISchemaFailed: (error: any) => ( + createAction(ServerAPIActionTypes.GET_SERVER_API_SCHEMA_FAILED, { error }) + ), +}; + +export type ServerAPIActions = ActionUnion; + +export const getServerAPISchemaAsync = (): ThunkAction => async (dispatch): Promise => { + dispatch(serverAPIActions.getServerAPISchema()); + + try { + const schema = await core.server.apiSchema(); + dispatch(serverAPIActions.getServerAPISchemaSuccess(schema)); + } catch (error) { + dispatch(serverAPIActions.getServerAPISchemaFailed(error)); + } +}; diff --git a/cvat-ui/src/components/actions-menu/actions-menu.tsx b/cvat-ui/src/components/actions-menu/actions-menu.tsx index 62426c332684..6f17e335c8ac 100644 --- a/cvat-ui/src/components/actions-menu/actions-menu.tsx +++ b/cvat-ui/src/components/actions-menu/actions-menu.tsx @@ -1,26 +1,26 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import './styles.scss'; import React, { useCallback } from 'react'; -import Menu from 'antd/lib/menu'; import Modal from 'antd/lib/modal'; import { LoadingOutlined } from '@ant-design/icons'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { MenuInfo } from 'rc-menu/lib/interface'; -import { DimensionType } from 'cvat-core-wrapper'; +import { DimensionType, CVATCore } from 'cvat-core-wrapper'; +import Menu, { MenuInfo } from 'components/dropdown-menu'; import { usePlugins } from 'utils/hooks'; import { CombinedState } from 'reducers'; +type AnnotationFormats = Awaited>; + interface Props { taskID: number; projectID: number | null; taskMode: string; bugTracker: string; - loaders: any[]; - dumpers: any[]; + loaders: AnnotationFormats['loaders']; + dumpers: AnnotationFormats['dumpers']; inferenceIsActive: boolean; taskDimension: DimensionType; backupIsActive: boolean; @@ -137,7 +137,11 @@ function ActionsMenuComponent(props: Props): JSX.Element { ); return ( - + { menuItems.sort((menuItem1, menuItem2) => menuItem1[1] - menuItem2[1]) .map((menuItem) => menuItem[0]) } diff --git a/cvat-ui/src/components/analytics-page/analytics-page.tsx b/cvat-ui/src/components/analytics-page/analytics-page.tsx index 8da7ff731574..8284130052f9 100644 --- a/cvat-ui/src/components/analytics-page/analytics-page.tsx +++ b/cvat-ui/src/components/analytics-page/analytics-page.tsx @@ -1,13 +1,14 @@ -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT +import './styles.scss'; + import React, { useCallback, useEffect, useState } from 'react'; import { useLocation, useParams } from 'react-router'; import { Link } from 'react-router-dom'; import { Row, Col } from 'antd/lib/grid'; import Tabs from 'antd/lib/tabs'; -import Text from 'antd/lib/typography/Text'; import Title from 'antd/lib/typography/Title'; import notification from 'antd/lib/notification'; import { useIsMounted } from 'utils/hooks'; @@ -17,10 +18,20 @@ import moment from 'moment'; import CVATLoadingSpinner from 'components/common/loading-spinner'; import GoBackButton from 'components/common/go-back-button'; import AnalyticsOverview, { DateIntervals } from './analytics-performance'; -import TaskQualityComponent from './quality/task-quality-component'; +import TaskQualityComponent from './task-quality/task-quality-component'; const core = getCore(); +enum AnalyticsTabs { + OVERVIEW = 'overview', + QUALITY = 'quality', +} + +function getTabFromHash(): AnalyticsTabs { + const tab = window.location.hash.slice(1) as AnalyticsTabs; + return Object.values(AnalyticsTabs).includes(tab) ? tab : AnalyticsTabs.OVERVIEW; +} + function handleTimePeriod(interval: DateIntervals): [string, string] { const now = moment.utc(); switch (interval) { @@ -42,321 +53,218 @@ function handleTimePeriod(interval: DateIntervals): [string, string] { } } +type InstanceType = 'project' | 'task' | 'job'; + function AnalyticsPage(): JSX.Element { const location = useLocation(); - let instanceType = ''; - if (location.pathname.includes('projects')) { - instanceType = 'project'; - } else if (location.pathname.includes('jobs')) { - instanceType = 'job'; - } else { - instanceType = 'task'; - } - const [fetching, setFetching] = useState(true); + const requestedInstanceType: InstanceType = (() => { + if (location.pathname.includes('projects')) { + return 'project'; + } + if (location.pathname.includes('jobs')) { + return 'job'; + } + return 'task'; + })(); + + const requestedInstanceID: number = (() => { + if (requestedInstanceType === 'project') { + return +useParams<{ pid: string }>().pid; + } + if (requestedInstanceType === 'job') { + return +useParams<{ jid: string }>().jid; + } + return +useParams<{ tid: string }>().tid; + })(); + + const [activeTab, setTab] = useState(getTabFromHash()); + + const [instanceType, setInstanceType] = useState(null); const [instance, setInstance] = useState(null); - const [analyticsReportInstance, setAnalyticsReportInstance] = useState(null); + const [analyticsReport, setAnalyticsReport] = useState(null); + const [timePeriod, setTimePeriod] = useState(DateIntervals.LAST_WEEK); + const [fetching, setFetching] = useState(true); const isMounted = useIsMounted(); - let instanceID: number | null = null; - let reportRequestID: number | null = null; - switch (instanceType) { - case 'project': { - instanceID = +useParams<{ pid: string }>().pid; - reportRequestID = +useParams<{ pid: string }>().pid; - break; + const receiveInstance = (type: InstanceType, id: number): Promise => { + if (type === 'project') { + return core.projects.get({ id }); } - case 'task': { - instanceID = +useParams<{ tid: string }>().tid; - reportRequestID = +useParams<{ tid: string }>().tid; - break; - } - case 'job': { - instanceID = +useParams<{ jid: string }>().jid; - reportRequestID = +useParams<{ jid: string }>().jid; - break; + + if (type === 'task') { + return core.tasks.get({ id }); } - default: { - throw new Error(`Unsupported instance type ${instanceType}`); + + return core.jobs.get({ jobID: id }); + }; + + const receiveReport = (timeInterval: DateIntervals, type: InstanceType, id: number): Promise => { + const [endDate, startDate] = handleTimePeriod(timeInterval); + if (type === 'project') { + return core.analytics.performance.reports({ + projectID: id, + endDate, + startDate, + }); } - } - const receieveInstance = (): void => { - let instanceRequest = null; - switch (instanceType) { - case 'project': { - instanceRequest = core.projects.get({ id: instanceID }); - break; - } - case 'task': { - instanceRequest = core.tasks.get({ id: instanceID }); - break; - } - case 'job': - { - instanceRequest = core.jobs.get({ jobID: instanceID }); - break; - } - default: { - throw new Error(`Unsupported instance type ${instanceType}`); - } + if (type === 'task') { + return core.analytics.performance.reports({ + taskID: id, + endDate, + startDate, + }); } - if (Number.isInteger(instanceID)) { - instanceRequest - .then(([_instance]: Task[] | Project[] | Job[]) => { - if (isMounted() && _instance) { - setInstance(_instance); + return core.analytics.performance.reports({ + jobID: id, + endDate, + startDate, + }); + }; + + useEffect(() => { + setFetching(true); + + if (Number.isInteger(requestedInstanceID) && ['project', 'task', 'job'].includes(requestedInstanceType)) { + Promise.all([ + receiveInstance(requestedInstanceType, requestedInstanceID), + receiveReport(timePeriod, requestedInstanceType, requestedInstanceID), + ]) + .then(([instanceResponse, report]) => { + const receivedInstance: Task | Project | Job = instanceResponse[0]; + if (receivedInstance && isMounted()) { + setInstance(receivedInstance); + setInstanceType(requestedInstanceType); } - }).catch((error: Error) => { - if (isMounted()) { - notification.error({ - message: 'Could not receive the requested instance from the server', - description: error.toString(), - }); + if (report && isMounted()) { + setAnalyticsReport(report); } - }).finally(() => { + }) + .catch((error: Error) => { + notification.error({ + message: 'Could not receive requested resources', + description: `${error.toString()}`, + }); + }) + .finally(() => { if (isMounted()) { setFetching(false); } }); } else { notification.error({ - message: 'Could not receive the requested task from the server', - description: `Requested "${instanceID}" is not valid`, + message: 'Could not load this page', + description: `Not valid resource ${requestedInstanceType} #${requestedInstanceID}`, }); - setFetching(false); - } - }; - - const receieveReport = (timeInterval: DateIntervals): void => { - if (Number.isInteger(instanceID) && Number.isInteger(reportRequestID)) { - let reportRequest = null; - const [endDate, startDate] = handleTimePeriod(timeInterval); - - switch (instanceType) { - case 'project': { - reportRequest = core.analytics.performance.reports({ - projectID: reportRequestID, - endDate, - startDate, - }); - break; - } - case 'task': { - reportRequest = core.analytics.performance.reports({ - taskID: reportRequestID, - endDate, - startDate, - }); - break; - } - case 'job': { - reportRequest = core.analytics.performance.reports({ - jobID: reportRequestID, - endDate, - startDate, - }); - break; - } - default: { - throw new Error(`Unsupported instance type ${instanceType}`); - } - } - - reportRequest - .then((report: AnalyticsReport) => { - if (isMounted() && report) { - setAnalyticsReportInstance(report); - } - }).catch((error: Error) => { - if (isMounted()) { - notification.error({ - message: 'Could not receive the requested report from the server', - description: error.toString(), - }); - } - }); } - }; - useEffect((): void => { - Promise.all([receieveInstance(), receieveReport(DateIntervals.LAST_WEEK)]).finally(() => { + return () => { if (isMounted()) { - setFetching(false); + setInstance(null); + setAnalyticsReport(null); } - }); - }, []); + }; + }, [requestedInstanceType, requestedInstanceID, timePeriod]); const onJobUpdate = useCallback((job: Job): void => { setFetching(true); - job.save().then(() => { - if (isMounted()) { - receieveInstance(); - } - }).catch((error: Error) => { - if (isMounted()) { + + job.save() + .catch((error: Error) => { notification.error({ message: 'Could not update the job', description: error.toString(), }); - } - }).finally(() => { - if (isMounted()) { - setFetching(false); - } - }); + }) + .finally(() => { + if (isMounted()) { + setFetching(false); + } + }); }, []); - const onAnalyticsTimePeriodChange = useCallback((val: DateIntervals): void => { - receieveReport(val); + useEffect(() => { + window.addEventListener('hashchange', () => { + const hash = getTabFromHash(); + setTab(hash); + }); }, []); + const onTabKeyChange = (key: string): void => { + setTab(key as AnalyticsTabs); + }; + + useEffect(() => { + window.location.hash = activeTab; + }, [activeTab]); + let backNavigation: JSX.Element | null = null; let title: JSX.Element | null = null; let tabs: JSX.Element | null = null; - if (instance) { - switch (instanceType) { - case 'project': { - backNavigation = ( - - - - ); - title = ( - - - Analytics for - {' '} - <Link to={`/projects/${instance.id}`}>{`Project #${instance.id}`}</Link> - - - ); - tabs = ( - - - Performance - - )} - key='Overview' - > - - - - ); - break; - } - case 'task': { - backNavigation = ( - - - - ); - title = ( - - - Analytics for - {' '} - <Link to={`/tasks/${instance.id}`}>{`Task #${instance.id}`}</Link> - - - ); - tabs = ( - - - Performance - - )} - key='overview' - > - - - - Quality - - )} - key='quality' - > - - - - ); - break; - } - case 'job': - { - backNavigation = ( - - - - ); - title = ( - - - Analytics for - {' '} - <Link to={`/tasks/${instance.taskId}/jobs/${instance.id}`}>{`Job #${instance.id}`}</Link> - - - ); - tabs = ( - - - Performance - - )} - key='overview' - > - - - - ); - break; - } - default: { - throw new Error(`Unsupported instance type ${instanceType}`); - } + if (instanceType && instance) { + backNavigation = ( + + + + ); + + let analyticsFor: JSX.Element | null = {`Project #${instance.id}`}; + if (instanceType === 'task') { + analyticsFor = {`Task #${instance.id}`}; + } else if (instanceType === 'job') { + analyticsFor = {`Job #${instance.id}`}; } + title = ( + + + Analytics for + {' '} + {analyticsFor} + + + ); + + tabs = ( + + + + + {instanceType === 'task' && ( + + + + )} + + ); } return (
- { - fetching ? ( -
- -
- ) : ( - - {backNavigation} - - {title} - {tabs} - - - ) - } + {fetching ? ( +
+ +
+ ) : ( + + {backNavigation} + + {title} + {tabs} + + + )}
); } diff --git a/cvat-ui/src/components/analytics-page/analytics-performance.tsx b/cvat-ui/src/components/analytics-page/analytics-performance.tsx index 2116c08f82ac..164504f7816c 100644 --- a/cvat-ui/src/components/analytics-page/analytics-performance.tsx +++ b/cvat-ui/src/components/analytics-page/analytics-performance.tsx @@ -1,9 +1,7 @@ -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -import './styles.scss'; - import React from 'react'; import moment from 'moment'; import RGL, { WidthProvider } from 'react-grid-layout'; @@ -26,6 +24,7 @@ export enum DateIntervals { interface Props { report: AnalyticsReport | null; + timePeriod: DateIntervals; onTimePeriodChange: (val: DateIntervals) => void; } @@ -38,7 +37,7 @@ const colors = [ ]; function AnalyticsOverview(props: Props): JSX.Element | null { - const { report, onTimePeriodChange } = props; + const { report, timePeriod, onTimePeriodChange } = props; if (!report) return null; const layout: any = []; @@ -144,15 +143,13 @@ function AnalyticsOverview(props: Props): JSX.Element | null { - Created -   - {report?.createdDate ? moment(report?.createdDate).fromNow() : ''} + {`Created ${report?.createdDate ? moment(report?.createdDate).fromNow() : ''}`} Email or username} - className={credential ? 'cvat-input-floating-label-above' : 'cvat-input-floating-label'} - suffix={credential && ( - { - setCredential(''); - form.setFieldsValue({ credential: '', password: '' }); - }} - /> - )} - onChange={(event) => { - const { value } = event.target; - setCredential(value); - if (!value) form.setFieldsValue({ credential: '', password: '' }); - }} - /> - - { - credential && ( + {renderBasicLoginComponent && ( + <> - Email or username} + className={credential ? 'cvat-input-floating-label-above' : 'cvat-input-floating-label'} + suffix={credential && ( + { + setCredential(''); + form.setFieldsValue({ credential: '', password: '' }); + }} + /> + )} + onChange={(event) => { + const { value } = event.target; + setCredential(value); + if (!value) form.setFieldsValue({ credential: '', password: '' }); + }} /> - ) - } - { - !!credential && ( - - - - ) - } + { + credential && ( + + + + ) + } + { + !!credential && ( + + + + ) + } + + )} { pluginsToRender.map(({ component: Component }, index) => ( diff --git a/cvat-ui/src/components/login-page/login-page.tsx b/cvat-ui/src/components/login-page/login-page.tsx index 0a0073a74d19..ba0948d18d78 100644 --- a/cvat-ui/src/components/login-page/login-page.tsx +++ b/cvat-ui/src/components/login-page/login-page.tsx @@ -14,6 +14,8 @@ import LoginForm, { LoginData } from './login-form'; interface LoginPageComponentProps { fetching: boolean; renderResetPassword: boolean; + renderRegistrationComponent: boolean; + renderBasicLoginComponent: boolean; hasEmailVerificationBeenSent: boolean; onLogin: (credential: string, password: string) => void; } @@ -21,13 +23,13 @@ interface LoginPageComponentProps { function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps): JSX.Element { const history = useHistory(); const { - fetching, renderResetPassword, hasEmailVerificationBeenSent, onLogin, + fetching, renderResetPassword, renderRegistrationComponent, renderBasicLoginComponent, + hasEmailVerificationBeenSent, onLogin, } = props; if (hasEmailVerificationBeenSent) { history.push('/auth/email-verification-sent'); } - return ( @@ -36,6 +38,8 @@ function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps { onLogin(loginData.credential, loginData.password); }} diff --git a/cvat-ui/src/components/models-page/deployed-model-item.tsx b/cvat-ui/src/components/models-page/deployed-model-item.tsx index 06544852657a..b7ef81e9b222 100644 --- a/cvat-ui/src/components/models-page/deployed-model-item.tsx +++ b/cvat-ui/src/components/models-page/deployed-model-item.tsx @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -17,8 +17,8 @@ import Divider from 'antd/lib/divider'; import Card from 'antd/lib/card'; import Dropdown from 'antd/lib/dropdown'; import Button from 'antd/lib/button'; -import Menu from 'antd/lib/menu'; +import Menu from 'components/dropdown-menu'; import Preview from 'components/common/preview'; import { usePlugins } from 'utils/hooks'; import { CombinedState } from 'reducers'; @@ -178,7 +178,11 @@ export default function DeployedModelItem(props: Props): JSX.Element { { menuItems.length !== 0 && ( - +
- )} + { + if (action.key === MenuKeys.RESEND_INVITATION) { + onResendInvitation(invitation.key); + } else if (action.key === MenuKeys.DELETE_INVITATION) { + onDeleteInvitation(invitation.key); + } + }} + > + Resend invitation + + Remove invitation + + )} > diff --git a/cvat-ui/src/components/organization-page/top-bar.tsx b/cvat-ui/src/components/organization-page/top-bar.tsx index bc4a700ca4b4..225410c1b10d 100644 --- a/cvat-ui/src/components/organization-page/top-bar.tsx +++ b/cvat-ui/src/components/organization-page/top-bar.tsx @@ -1,9 +1,10 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import React, { useState, useRef, useEffect } from 'react'; +import { useHistory } from 'react-router-dom'; import { useDispatch } from 'react-redux'; import moment from 'moment'; import { Row, Col } from 'antd/lib/grid'; @@ -16,7 +17,6 @@ import Input from 'antd/lib/input'; import Form from 'antd/lib/form'; import Select from 'antd/lib/select'; import Dropdown from 'antd/lib/dropdown'; -import Menu from 'antd/lib/menu'; import { useForm } from 'antd/lib/form/Form'; import { Store } from 'antd/lib/form/interface'; @@ -31,7 +31,7 @@ import { removeOrganizationAsync, updateOrganizationAsync, } from 'actions/organization-actions'; -import { useHistory } from 'react-router-dom'; +import Menu from 'components/dropdown-menu'; export interface Props { organizationInstance: any; @@ -120,32 +120,35 @@ function OrganizationTopBar(props: Props): JSX.Element { - ( - - - { - e.preventDefault(); - history.push({ - pathname: '/organization/webhooks', - }); - return false; - }} - > - Setup webhooks - - - {owner && userID === owner.id ? ( - - Remove organization + ( + + + { + e.preventDefault(); + history.push({ + pathname: '/organization/webhooks', + }); + return false; + }} + > + Setup webhooks + - ) : null} - - )} + {owner && userID === owner.id ? ( + + Remove organization + + ) : null} + + )} >