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 - {' '} - {`Project #${instance.id}`} - - - ); - tabs = ( - - - Performance - - )} - key='Overview' - > - - - - ); - break; - } - case 'task': { - backNavigation = ( - - - - ); - title = ( - - - Analytics for - {' '} - {`Task #${instance.id}`} - - - ); - tabs = ( - - - Performance - - )} - key='overview' - > - - - - Quality - - )} - key='quality' - > - - - - ); - break; - } - case 'job': - { - backNavigation = ( - - - - ); - title = ( - - - Analytics for - {' '} - {`Job #${instance.id}`} - - - ); - 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() : ''}`} ${config.NUMERIC_VALUE_CLAMP_THRESHOLD}`; - } - return 'N/A'; -} - -export function toRepresentation(val?: number): string { - return (!Number.isFinite(val) ? 'N/A' : `${val?.toFixed(1)}%`); -} diff --git a/cvat-ui/src/components/analytics-page/quality/mean-quality.tsx b/cvat-ui/src/components/analytics-page/quality/mean-quality.tsx deleted file mode 100644 index 9b6edf4d8236..000000000000 --- a/cvat-ui/src/components/analytics-page/quality/mean-quality.tsx +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright (C) 2023 CVAT.ai Corporation -// -// SPDX-License-Identifier: MIT - -import '../styles.scss'; - -import React from 'react'; -import Text from 'antd/lib/typography/Text'; -import moment from 'moment'; -import { QualityReport, Task, getCore } from 'cvat-core-wrapper'; -import { useSelector, useDispatch } from 'react-redux'; -import { CombinedState } from 'reducers'; -import Button from 'antd/lib/button'; -import { DownloadOutlined, MoreOutlined } from '@ant-design/icons'; -import { analyticsActions } from 'actions/analytics-actions'; -import AnalyticsCard from '../views/analytics-card'; -import { toRepresentation } from './common'; - -interface Props { - task: Task; -} - -function MeanQuality(props: Props): JSX.Element { - const { task } = props; - const dispatch = useDispatch(); - const tasksReports: QualityReport[] = useSelector((state: CombinedState) => state.analytics.quality.tasksReports); - const taskReport = tasksReports.find((report: QualityReport) => report.taskId === task.id); - const reportSummary = taskReport?.summary; - - const tooltip = ( - - - Mean annotation quality consists of: - - - Correct annotations: - {reportSummary?.validCount || 0} - - - Task annotations: - {reportSummary?.dsCount || 0} - - - GT annotations: - {reportSummary?.gtCount || 0} - - - Accuracy: - {toRepresentation(reportSummary?.accuracy)} - - - Precision: - {toRepresentation(reportSummary?.precision)} - - - Recall: - {toRepresentation(reportSummary?.recall)} - - - ); - - const dowloadReportButton = ( - - { - taskReport?.id ? ( - <> - } className='cvat-analytics-download-report-button'> - - Quality Report - - - dispatch(analyticsActions.switchQualitySettingsVisible(true))} - /> - - {taskReport?.createdDate ? moment(taskReport?.createdDate).fromNow() : ''} - - > - ) : null - } - - - ); - return ( - - ); -} - -export default React.memo(MeanQuality); diff --git a/cvat-ui/src/components/analytics-page/quality/quality-settings-modal.tsx b/cvat-ui/src/components/analytics-page/quality/quality-settings-modal.tsx deleted file mode 100644 index 4363934dee21..000000000000 --- a/cvat-ui/src/components/analytics-page/quality/quality-settings-modal.tsx +++ /dev/null @@ -1,368 +0,0 @@ -// Copyright (C) 2023 CVAT.ai Corporation -// -// SPDX-License-Identifier: MIT - -import React, { useCallback } from 'react'; -import { useDispatch, useSelector } from 'react-redux'; -import { CombinedState } from 'reducers'; -import Text from 'antd/lib/typography/Text'; -import Modal from 'antd/lib/modal'; -import InputNumber from 'antd/lib/input-number'; -import { analyticsActions, updateQualitySettingsAsync } from 'actions/analytics-actions'; -import { Col, Row } from 'antd/lib/grid'; -import { Divider } from 'antd'; -import Form from 'antd/lib/form'; -import Checkbox from 'antd/lib/checkbox/Checkbox'; -import CVATTooltip from 'components/common/cvat-tooltip'; -import { QuestionCircleOutlined } from '@ant-design/icons/lib/icons'; - -export default function QualitySettingsModal(): JSX.Element | null { - const visible = useSelector((state: CombinedState) => state.analytics.quality.settings.modalVisible); - const loading = useSelector((state: CombinedState) => state.analytics.quality.settings.fetching); - const settings = useSelector((state: CombinedState) => state.analytics.quality.settings.current); - const [form] = Form.useForm(); - - const dispatch = useDispatch(); - - const onOk = useCallback(async () => { - try { - if (settings) { - const values = await form.validateFields(); - settings.lowOverlapThreshold = values.lowOverlapThreshold / 100; - settings.iouThreshold = values.iouThreshold / 100; - settings.compareAttributes = values.compareAttributes; - - settings.oksSigma = values.oksSigma / 100; - - settings.lineThickness = values.lineThickness / 100; - settings.lineOrientationThreshold = values.lineOrientationThreshold / 100; - settings.orientedLines = values.orientedLines; - - settings.compareGroups = values.compareGroups; - settings.groupMatchThreshold = values.groupMatchThreshold / 100; - - settings.checkCoveredAnnotations = values.checkCoveredAnnotations; - settings.objectVisibilityThreshold = values.objectVisibilityThreshold / 100; - - settings.panopticComparison = values.panopticComparison; - - await dispatch(updateQualitySettingsAsync(settings)); - await dispatch(analyticsActions.switchQualitySettingsVisible(false)); - } - return settings; - } catch (e) { - return false; - } - }, [settings]); - - const onCancel = useCallback(() => { - dispatch(analyticsActions.switchQualitySettingsVisible(false)); - }, []); - - const generalTooltip = ( - - - Min overlap threshold(IoU) is used for distinction between matched / unmatched shapes. - - - Low overlap threshold is used for distinction between strong / weak (low overlap) matches. - - - ); - - const keypointTooltip = ( - - - Object Keypoint Similarity (OKS) is like IoU, but for skeleton points. - - - The Sigma value is the percent of the skeleton bbox area ^ 0.5. - Used as the radius of the circle around a GT point, - where the checked point is expected to be. - - - The value is also used to match single point annotations, in which case - the bbox is the whole image. For point groups the bbox is taken - for the whole group. - - - If there is a rectangle annotation in the points group or skeleton, - it is used as the group bbox (supposing the whole group describes a single object). - - - ); - - const linesTooltip = ( - - - Line thickness - thickness of polylines, relatively to the (image area) ^ 0.5. - The distance to the boundary around the GT line, - inside of which the checked line points should be. - - - Check orientation - Indicates that polylines have direction. - - - Min similarity gain - The minimal gain in the GT IoU between the given and reversed line directions - to consider the line inverted. Only useful with the Check orientation parameter. - - - ); - - const groupTooltip = ( - - - Compare groups - Enables or disables annotation group checks. - - - Min group match threshold - Minimal IoU for groups to be considered matching, - used when the Compare groups is enabled. - - - ); - - const segmentationTooltip = ( - - - Check object visibility - Check for partially-covered annotations. - - - Min visibility threshold - Minimal visible area percent of the spatial annotations (polygons, masks) - for reporting covered annotations, useful with the Check object visibility option. - - - Match only visible parts - Use only the visible part of the masks and polygons in comparisons. - - - ); - - return ( - Annotation Quality Settings} - visible={visible} - onOk={onOk} - onCancel={onCancel} - confirmLoading={loading} - className='cvat-modal-quality-settings' - > - { settings ? ( - - - - General - - - - - - - - - - - - - - - - - - - - - - Compare attributes - - - - - - - - Keypoint Comparison - - - - - - - - - - - - - - - - Line Comparison - - - - - - - - - - - - - - - - - Check orientation - - - - - - - - - - - - - Group Comparison - - - - - - - - - - Compare groups - - - - - - - - - - - - - Segmentation Comparison - - - - - - - - - - Check object visibility - - - - - - - - - - - - - - Match only visible parts - - - - - - ) : ( - No quality settings - )} - - ); -} diff --git a/cvat-ui/src/components/analytics-page/quality/task-quality-component.tsx b/cvat-ui/src/components/analytics-page/quality/task-quality-component.tsx deleted file mode 100644 index 25a514db7cb2..000000000000 --- a/cvat-ui/src/components/analytics-page/quality/task-quality-component.tsx +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright (C) 2023 CVAT.ai Corporation -// -// SPDX-License-Identifier: MIT - -import '../styles.scss'; - -import React, { useEffect } from 'react'; -import { - Job, JobType, Task, -} from 'cvat-core-wrapper'; -import { Row } from 'antd/lib/grid'; -import Text from 'antd/lib/typography/Text'; -import JobItem from 'components/job-item/job-item'; -import { useDispatch, useSelector } from 'react-redux'; -import { getQualityReportsAsync, getQualitySettingsAsync } from 'actions/analytics-actions'; -import { CombinedState } from 'reducers'; -import MeanQuality from './mean-quality'; -import JobList from './job-list'; -import GtConflicts from './gt-conflicts'; -import Issues from './issues'; -import EmptyGtJob from './empty-job'; -import QualitySettingsModal from './quality-settings-modal'; - -interface Props { - task: Task, - onJobUpdate: (job: Job) => void -} - -function TaskQualityComponent(props: Props): JSX.Element { - const { task, onJobUpdate } = props; - const dispatch = useDispatch(); - const query = useSelector((state: CombinedState) => state.analytics.quality.query); - - useEffect(() => { - dispatch(getQualityReportsAsync(task, { ...query, taskId: task.id })); - dispatch(getQualitySettingsAsync(task)); - }, []); - - const gtJob = task.jobs.find((job: Job) => job.type === JobType.GROUND_TRUTH); - - return ( - - - - - - - - - - - Quality reports are not computed unless the GT job is in the - completed state - and - acceptance stage. - - - - {gtJob ? - : - } - - - - - - - ); -} - -export default React.memo(TaskQualityComponent); diff --git a/cvat-ui/src/components/analytics-page/shared/quality-settings-modal.tsx b/cvat-ui/src/components/analytics-page/shared/quality-settings-modal.tsx new file mode 100644 index 000000000000..e2bfd929bff3 --- /dev/null +++ b/cvat-ui/src/components/analytics-page/shared/quality-settings-modal.tsx @@ -0,0 +1,97 @@ +// Copyright (C) 2023-2024 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React, { useCallback } from 'react'; +import Text from 'antd/lib/typography/Text'; +import Modal from 'antd/lib/modal'; +import Form from 'antd/lib/form'; +import notification from 'antd/lib/notification'; +import { QualitySettings } from 'cvat-core-wrapper'; +import QualitySettingsForm from '../task-quality/quality-settings-form'; + +interface Props { + fetching: boolean; + qualitySettings: QualitySettings | null; + visible: boolean; + setVisible: (visible: boolean) => void; + setQualitySettings: (settings: QualitySettings) => void; +} + +export default function QualitySettingsModal(props: Props): JSX.Element | null { + const { + fetching, + visible, + qualitySettings: settings, + setVisible, + setQualitySettings, + } = props; + + const [form] = Form.useForm(); + + const onOk = useCallback(async () => { + try { + if (settings) { + const values = await form.validateFields(); + settings.lowOverlapThreshold = values.lowOverlapThreshold / 100; + settings.iouThreshold = values.iouThreshold / 100; + settings.compareAttributes = values.compareAttributes; + + settings.oksSigma = values.oksSigma / 100; + + settings.lineThickness = values.lineThickness / 100; + settings.lineOrientationThreshold = values.lineOrientationThreshold / 100; + settings.orientedLines = values.orientedLines; + + settings.compareGroups = values.compareGroups; + settings.groupMatchThreshold = values.groupMatchThreshold / 100; + + settings.checkCoveredAnnotations = values.checkCoveredAnnotations; + settings.objectVisibilityThreshold = values.objectVisibilityThreshold / 100; + + settings.panopticComparison = values.panopticComparison; + + try { + const responseSettings = await settings.save(); + setQualitySettings(responseSettings); + } catch (error: unknown) { + notification.error({ + message: 'Could not save quality settings', + description: typeof Error === 'object' ? (error as object).toString() : '', + }); + throw error; + } + await settings.save(); + } + setVisible(false); + return settings; + } catch (e) { + return false; + } + }, [settings]); + + const onCancel = useCallback(() => { + setVisible(false); + }, []); + + return ( + Annotation Quality Settings} + visible={visible} + onOk={onOk} + onCancel={onCancel} + confirmLoading={fetching} + destroyOnClose + className='cvat-modal-quality-settings' + > + { settings ? ( + + ) : ( + No quality settings + )} + + ); +} diff --git a/cvat-ui/src/components/analytics-page/styles.scss b/cvat-ui/src/components/analytics-page/styles.scss index 28355d53310e..4bc8161fbf06 100644 --- a/cvat-ui/src/components/analytics-page/styles.scss +++ b/cvat-ui/src/components/analytics-page/styles.scss @@ -1,4 +1,4 @@ -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -15,21 +15,13 @@ } } -.cvat-task-quality-page { +.cvat-task-quality-page, +.cvat-project-quality-page { >.ant-row { margin-top: $grid-unit-size; } } -.cvat-task-quality-page-empty { - @extend .cvat-task-quality-page; - - display: flex; - justify-content: center; - align-items: center; - height: $grid-unit-size * 68; -} - .cvat-task-mean-annotation-quality { .ant-statistic { display: flex; @@ -108,33 +100,6 @@ padding-bottom: $grid-unit-size * 2; } -.cvat-project-analytics-title { - margin-bottom: $grid-unit-size; - - h4 { - display: inline; - margin-right: $grid-unit-size; - } -} - -.cvat-task-analytics-title { - margin-bottom: $grid-unit-size; - - h4 { - display: inline; - margin-right: $grid-unit-size; - } -} - -.cvat-job-analytics-title { - margin-bottom: $grid-unit-size; - - h4 { - display: inline; - margin-right: $grid-unit-size; - } -} - .cvat-task-quality-reports-hint { margin-bottom: $grid-unit-size * 3; } @@ -144,10 +109,6 @@ padding: $grid-unit-size * 3; } - h5 { - margin: 0; - } - .ant-btn { padding-left: $grid-unit-size * 3; padding-right: $grid-unit-size * 3; @@ -155,7 +116,10 @@ } .cvat-quality-settings-switch { - padding-left: $grid-unit-size * 2; + padding: 6px 8px; + border: 1px solid lightgray; + margin-left: 8px; + border-radius: 2px; } .cvat-quality-settings-title { @@ -180,3 +144,8 @@ justify-content: space-between; align-items: center; } + +.cvat-quality-summary-controls { + display: flex; + align-items: center; +} diff --git a/cvat-ui/src/components/analytics-page/quality/empty-job.tsx b/cvat-ui/src/components/analytics-page/task-quality/empty-job.tsx similarity index 89% rename from cvat-ui/src/components/analytics-page/quality/empty-job.tsx rename to cvat-ui/src/components/analytics-page/task-quality/empty-job.tsx index 96566559542f..7b82dcd3bc2a 100644 --- a/cvat-ui/src/components/analytics-page/quality/empty-job.tsx +++ b/cvat-ui/src/components/analytics-page/task-quality/empty-job.tsx @@ -12,11 +12,11 @@ import Button from 'antd/lib/button'; import Title from 'antd/lib/typography/Title'; interface Props { - taskId: number, + taskID: number, } function EmptyJobComponent(props: Props): JSX.Element { - const { taskId } = props; + const { taskID } = props; return ( @@ -27,7 +27,7 @@ function EmptyJobComponent(props: Props): JSX.Element { - Create new + Create new diff --git a/cvat-ui/src/components/analytics-page/quality/gt-conflicts.tsx b/cvat-ui/src/components/analytics-page/task-quality/gt-conflicts.tsx similarity index 86% rename from cvat-ui/src/components/analytics-page/quality/gt-conflicts.tsx rename to cvat-ui/src/components/analytics-page/task-quality/gt-conflicts.tsx index 228762a5d9eb..9804412cf1d2 100644 --- a/cvat-ui/src/components/analytics-page/quality/gt-conflicts.tsx +++ b/cvat-ui/src/components/analytics-page/task-quality/gt-conflicts.tsx @@ -1,20 +1,17 @@ -// 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 Text from 'antd/lib/typography/Text'; -import { QualityReport, QualitySummary, Task } from 'cvat-core-wrapper'; -import { useSelector } from 'react-redux'; -import { CombinedState } from 'reducers'; import { Col, Row } from 'antd/lib/grid'; + +import { QualityReport, QualitySummary } from 'cvat-core-wrapper'; import AnalyticsCard from '../views/analytics-card'; -import { percent, clampValue } from './common'; +import { percent, clampValue } from '../utils/text-formatting'; interface Props { - task: Task; + taskReport: QualityReport | null; } interface ConflictTooltipProps { @@ -72,10 +69,7 @@ export function ConflictsTooltip(props: ConflictTooltipProps): JSX.Element { } function GTConflicts(props: Props): JSX.Element { - const { task } = props; - const tasksReports: QualityReport[] = useSelector((state: CombinedState) => state.analytics.quality.tasksReports); - const taskReport = tasksReports.find((report: QualityReport) => report.taskId === task.id); - + const { taskReport } = props; let conflictsRepresentation: string | number = 'N/A'; let reportSummary; if (taskReport) { diff --git a/cvat-ui/src/components/analytics-page/quality/issues.tsx b/cvat-ui/src/components/analytics-page/task-quality/issues.tsx similarity index 96% rename from cvat-ui/src/components/analytics-page/quality/issues.tsx rename to cvat-ui/src/components/analytics-page/task-quality/issues.tsx index 178237cab306..f0e0dcf1936c 100644 --- a/cvat-ui/src/components/analytics-page/quality/issues.tsx +++ b/cvat-ui/src/components/analytics-page/task-quality/issues.tsx @@ -10,7 +10,7 @@ import notification from 'antd/lib/notification'; import { Task } from 'cvat-core-wrapper'; import { useIsMounted } from 'utils/hooks'; import AnalyticsCard from '../views/analytics-card'; -import { percent, clampValue } from './common'; +import { percent, clampValue } from '../utils/text-formatting'; interface Props { task: Task; diff --git a/cvat-ui/src/components/analytics-page/quality/job-list.tsx b/cvat-ui/src/components/analytics-page/task-quality/job-list.tsx similarity index 89% rename from cvat-ui/src/components/analytics-page/quality/job-list.tsx rename to cvat-ui/src/components/analytics-page/task-quality/job-list.tsx index be2b2b8c8542..538b0bbd0d31 100644 --- a/cvat-ui/src/components/analytics-page/quality/job-list.tsx +++ b/cvat-ui/src/components/analytics-page/task-quality/job-list.tsx @@ -1,8 +1,8 @@ -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -import React, { useEffect, useState } from 'react'; +import React, { useState } from 'react'; import { useHistory } from 'react-router'; import { Row, Col } from 'antd/lib/grid'; import { DownloadOutlined, QuestionCircleOutlined } from '@ant-design/icons'; @@ -15,38 +15,27 @@ import { Task, Job, JobType, QualityReport, getCore, } from 'cvat-core-wrapper'; import CVATTooltip from 'components/common/cvat-tooltip'; -import { CombinedState } from 'reducers'; -import { useSelector } from 'react-redux'; import { getQualityColor } from 'utils/quality-color'; import Tag from 'antd/lib/tag'; -import { toRepresentation } from './common'; +import { toRepresentation } from '../utils/text-formatting'; import { ConflictsTooltip } from './gt-conflicts'; interface Props { task: Task; + jobsReports: QualityReport[]; } function JobListComponent(props: Props): JSX.Element { const { task: taskInstance, + jobsReports: jobsReportsArray, } = props; + const jobsReports: Record = jobsReportsArray + .reduce((acc, report) => ({ ...acc, [report.jobID]: report }), {}); const history = useHistory(); - const { id: taskId } = taskInstance; - const { jobs } = taskInstance; + const { id: taskId, jobs } = taskInstance; const [renderedJobs] = useState(jobs.filter((job: Job) => job.type === JobType.ANNOTATION)); - const [jobsReports, setJobsReports] = useState>({}); - const jobReportsFromState: QualityReport[] = - useSelector((state: CombinedState) => state.analytics.quality.jobsReports); - - useEffect(() => { - const jobsReportsMap: Record = {}; - for (const job of jobs) { - const report = jobReportsFromState.find((_report: QualityReport) => _report.jobId === job.id); - if (report) jobsReportsMap[job.id] = report; - } - setJobsReports(jobsReportsMap); - }, [taskInstance, jobReportsFromState]); function sorter(path: string) { return (obj1: any, obj2: any): number => { @@ -68,10 +57,10 @@ function JobListComponent(props: Props): JSX.Element { } if (field1 === null || !Number.isFinite(field1)) { - return 1; + return -1; } - return -1; + return 1; }; } @@ -94,6 +83,7 @@ function JobListComponent(props: Props): JSX.Element { title: 'Job', dataIndex: 'job', key: 'job', + sorter: sorter('key'), render: (id: number): JSX.Element => ( { + const frames = report?.summary.frameCount; const frameSharePercent = report?.summary?.frameSharePercent; return ( - {toRepresentation(frameSharePercent)} + {toRepresentation(frames, false, 0)} + {frames ? ` (${toRepresentation(frameSharePercent)})` : ''} ); }, diff --git a/cvat-ui/src/components/analytics-page/task-quality/mean-quality.tsx b/cvat-ui/src/components/analytics-page/task-quality/mean-quality.tsx new file mode 100644 index 000000000000..b14e8bb18052 --- /dev/null +++ b/cvat-ui/src/components/analytics-page/task-quality/mean-quality.tsx @@ -0,0 +1,96 @@ +// Copyright (C) 2023-2024 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import moment from 'moment'; +import { DownloadOutlined, SettingOutlined } from '@ant-design/icons'; +import Text from 'antd/lib/typography/Text'; +import Button from 'antd/lib/button'; + +import { QualityReport, getCore } from 'cvat-core-wrapper'; +import AnalyticsCard from '../views/analytics-card'; +import { toRepresentation } from '../utils/text-formatting'; + +interface Props { + taskID: number; + taskReport: QualityReport | null; + setQualitySettingsVisible: (visible: boolean) => void; +} + +function MeanQuality(props: Props): JSX.Element { + const { taskID, taskReport, setQualitySettingsVisible } = props; + const reportSummary = taskReport?.summary; + + const tooltip = ( + + + Mean annotation quality consists of: + + + Correct annotations: + {reportSummary?.validCount || 0} + + + Task annotations: + {reportSummary?.dsCount || 0} + + + GT annotations: + {reportSummary?.gtCount || 0} + + + Accuracy: + {toRepresentation(reportSummary?.accuracy)} + + + Precision: + {toRepresentation(reportSummary?.precision)} + + + Recall: + {toRepresentation(reportSummary?.recall)} + + + ); + + const downloadReportButton = ( + + { + taskReport?.id ? ( + } className='cvat-analytics-download-report-button'> + + Quality Report + + + ) : null + } + setQualitySettingsVisible(true)} + /> + { + taskReport?.id ? ( + + {taskReport?.createdDate ? moment(taskReport?.createdDate).fromNow() : ''} + + ) : null + } + + + ); + return ( + + ); +} + +export default React.memo(MeanQuality); diff --git a/cvat-ui/src/components/analytics-page/task-quality/quality-settings-form.tsx b/cvat-ui/src/components/analytics-page/task-quality/quality-settings-form.tsx new file mode 100644 index 000000000000..e9eb16ba3d2a --- /dev/null +++ b/cvat-ui/src/components/analytics-page/task-quality/quality-settings-form.tsx @@ -0,0 +1,316 @@ +// Copyright (C) 2023-2024 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import { QuestionCircleOutlined } from '@ant-design/icons/lib/icons'; +import Text from 'antd/lib/typography/Text'; +import InputNumber from 'antd/lib/input-number'; +import { Col, Row } from 'antd/lib/grid'; +import Divider from 'antd/lib/divider'; +import Form, { FormInstance } from 'antd/lib/form'; +import Checkbox from 'antd/lib/checkbox/Checkbox'; +import CVATTooltip from 'components/common/cvat-tooltip'; +import { QualitySettings } from 'cvat-core-wrapper'; + +interface FormProps { + form: FormInstance; + settings: QualitySettings; +} + +export default function QualitySettingsForm(props: FormProps): JSX.Element | null { + const { form, settings } = props; + + const initialValues = { + lowOverlapThreshold: settings.lowOverlapThreshold * 100, + iouThreshold: settings.iouThreshold * 100, + compareAttributes: settings.compareAttributes, + + oksSigma: settings.oksSigma * 100, + + lineThickness: settings.lineThickness * 100, + lineOrientationThreshold: settings.lineOrientationThreshold * 100, + orientedLines: settings.orientedLines, + + compareGroups: settings.compareGroups, + groupMatchThreshold: settings.groupMatchThreshold * 100, + + checkCoveredAnnotations: settings.checkCoveredAnnotations, + objectVisibilityThreshold: settings.objectVisibilityThreshold * 100, + panopticComparison: settings.panopticComparison, + }; + + const generalTooltip = ( + + + Min overlap threshold(IoU) is used for distinction between matched / unmatched shapes. + + + Low overlap threshold is used for distinction between strong / weak (low overlap) matches. + + + ); + + const keypointTooltip = ( + + + Object Keypoint Similarity (OKS) is like IoU, but for skeleton points. + + + The Sigma value is the percent of the skeleton bbox area ^ 0.5. + Used as the radius of the circle around a GT point, + where the checked point is expected to be. + + + The value is also used to match single point annotations, in which case + the bbox is the whole image. For point groups the bbox is taken + for the whole group. + + + If there is a rectangle annotation in the points group or skeleton, + it is used as the group bbox (supposing the whole group describes a single object). + + + ); + + const linesTooltip = ( + + + Line thickness - thickness of polylines, relatively to the (image area) ^ 0.5. + The distance to the boundary around the GT line, + inside of which the checked line points should be. + + + Check orientation - Indicates that polylines have direction. + + + Min similarity gain - The minimal gain in the GT IoU between the given and reversed line directions + to consider the line inverted. Only useful with the Check orientation parameter. + + + ); + + const groupTooltip = ( + + + Compare groups - Enables or disables annotation group checks. + + + Min group match threshold - Minimal IoU for groups to be considered matching, + used when the Compare groups is enabled. + + + ); + + const segmentationTooltip = ( + + + Check object visibility - Check for partially-covered annotations. + + + Min visibility threshold - Minimal visible area percent of the spatial annotations (polygons, masks) + for reporting covered annotations, useful with the Check object visibility option. + + + Match only visible parts - Use only the visible part of the masks and polygons in comparisons. + + + ); + + return ( + + + + General + + + + + + + + + + + + + + + + + + + + + + Compare attributes + + + + + + + + Keypoint Comparison + + + + + + + + + + + + + + + + Line Comparison + + + + + + + + + + + + + + + + + Check orientation + + + + + + + + + + + + + Group Comparison + + + + + + + + + + Compare groups + + + + + + + + + + + + + Segmentation Comparison + + + + + + + + + + Check object visibility + + + + + + + + + + + + + + Match only visible parts + + + + + + ); +} diff --git a/cvat-ui/src/components/analytics-page/task-quality/task-quality-component.tsx b/cvat-ui/src/components/analytics-page/task-quality/task-quality-component.tsx new file mode 100644 index 000000000000..a69b11c378d3 --- /dev/null +++ b/cvat-ui/src/components/analytics-page/task-quality/task-quality-component.tsx @@ -0,0 +1,251 @@ +// Copyright (C) 2023-2024 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import { Row } from 'antd/lib/grid'; +import Text from 'antd/lib/typography/Text'; +import notification from 'antd/lib/notification'; +import CVATLoadingSpinner from 'components/common/loading-spinner'; +import JobItem from 'components/job-item/job-item'; +import { + Job, JobType, QualityReport, QualitySettings, Task, getCore, +} from 'cvat-core-wrapper'; +import React, { useEffect, useReducer } from 'react'; +import { useIsMounted } from 'utils/hooks'; +import { ActionUnion, createAction } from 'utils/redux'; +import EmptyGtJob from './empty-job'; +import GtConflicts from './gt-conflicts'; +import Issues from './issues'; +import JobList from './job-list'; +import MeanQuality from './mean-quality'; +import QualitySettingsModal from '../shared/quality-settings-modal'; + +const core = getCore(); + +interface Props { + task: Task; + onJobUpdate: (job: Job) => void; +} + +interface State { + fetching: boolean; + taskReport: QualityReport | null; + jobsReports: QualityReport[]; + qualitySettings: { + settings: QualitySettings | null; + fetching: boolean; + visible: boolean; + }, +} + +enum ReducerActionType { + SET_FETCHING = 'SET_FETCHING', + SET_TASK_REPORT = 'SET_TASK_REPORT', + SET_JOBS_REPORTS = 'SET_JOBS_REPORTS', + SET_QUALITY_SETTINGS = 'SET_QUALITY_SETTINGS', + SET_QUALITY_SETTINGS_VISIBLE = 'SET_QUALITY_SETTINGS_VISIBLE', + SET_QUALITY_SETTINGS_FETCHING = 'SET_QUALITY_SETTINGS_FETCHING', +} + +export const reducerActions = { + setFetching: (fetching: boolean) => ( + createAction(ReducerActionType.SET_FETCHING, { fetching }) + ), + setTaskReport: (qualityReport: QualityReport) => ( + createAction(ReducerActionType.SET_TASK_REPORT, { qualityReport }) + ), + setJobsReports: (qualityReports: QualityReport[]) => ( + createAction(ReducerActionType.SET_JOBS_REPORTS, { qualityReports }) + ), + setQualitySettings: (qualitySettings: QualitySettings) => ( + createAction(ReducerActionType.SET_QUALITY_SETTINGS, { qualitySettings }) + ), + setQualitySettingsVisible: (visible: boolean) => ( + createAction(ReducerActionType.SET_QUALITY_SETTINGS_VISIBLE, { visible }) + ), + setQualitySettingsFetching: (fetching: boolean) => ( + createAction(ReducerActionType.SET_QUALITY_SETTINGS_FETCHING, { fetching }) + ), +}; + +const reducer = (state: State, action: ActionUnion): State => { + if (action.type === ReducerActionType.SET_FETCHING) { + return { + ...state, + fetching: action.payload.fetching, + }; + } + + if (action.type === ReducerActionType.SET_TASK_REPORT) { + return { + ...state, + taskReport: action.payload.qualityReport, + }; + } + + if (action.type === ReducerActionType.SET_JOBS_REPORTS) { + return { + ...state, + jobsReports: action.payload.qualityReports, + }; + } + + if (action.type === ReducerActionType.SET_QUALITY_SETTINGS) { + return { + ...state, + qualitySettings: { + ...state.qualitySettings, + settings: action.payload.qualitySettings, + }, + }; + } + + if (action.type === ReducerActionType.SET_QUALITY_SETTINGS_VISIBLE) { + return { + ...state, + qualitySettings: { + ...state.qualitySettings, + visible: action.payload.visible, + }, + }; + } + + if (action.type === ReducerActionType.SET_QUALITY_SETTINGS_FETCHING) { + return { + ...state, + qualitySettings: { + ...state.qualitySettings, + fetching: action.payload.fetching, + }, + }; + } + + return state; +}; + +function TaskQualityComponent(props: Props): JSX.Element { + const { task, onJobUpdate } = props; + const isMounted = useIsMounted(); + + const [state, dispatch] = useReducer(reducer, { + fetching: true, + taskReport: null, + jobsReports: [], + qualitySettings: { + settings: null, + fetching: true, + visible: false, + }, + }); + + useEffect(() => { + dispatch(reducerActions.setFetching(true)); + dispatch(reducerActions.setQualitySettingsFetching(true)); + + function handleError(error: Error): void { + if (isMounted()) { + notification.error({ + description: error.toString(), + message: 'Could not initialize quality analytics page', + }); + } + } + + core.analytics.quality.reports({ pageSize: 1, target: 'task', taskID: task.id }).then(([report]) => { + let reportRequest = Promise.resolve([]); + if (report) { + reportRequest = core.analytics.quality.reports({ + pageSize: task.jobs.length, + parentID: report.id, + target: 'job', + }); + } + const settingsRequest = core.analytics.quality.settings.get({ taskID: task.id }); + + Promise.all([reportRequest, settingsRequest]).then(([jobReports, settings]) => { + dispatch(reducerActions.setQualitySettings(settings)); + dispatch(reducerActions.setTaskReport(report || null)); + dispatch(reducerActions.setJobsReports(jobReports)); + }).catch(handleError).finally(() => { + dispatch(reducerActions.setQualitySettingsFetching(false)); + dispatch(reducerActions.setFetching(false)); + }); + }).catch(handleError); + }, [task?.id]); + + const gtJob = task.jobs.find((job: Job) => job.type === JobType.GROUND_TRUTH); + + const { + fetching, taskReport, jobsReports, + qualitySettings: { + settings: qualitySettings, fetching: qualitySettingsFetching, visible: qualitySettingsVisible, + }, + } = state; + + return ( + + { + fetching ? ( + + ) : ( + <> + { + gtJob ? ( + <> + + dispatch(reducerActions.setQualitySettingsVisible(visible)) + } + taskID={task.id} + /> + + + + + + { + (!(gtJob && gtJob.stage === 'acceptance' && gtJob.state === 'completed')) ? ( + + + Quality reports are not computed unless the GT job is in the + completed state + and + acceptance stage. + + + ) : null + } + + + + + + + > + ) : ( + + + + ) + } + dispatch(reducerActions.setQualitySettings(settings)) + } + visible={qualitySettingsVisible} + setVisible={ + (visible) => dispatch(reducerActions.setQualitySettingsVisible(visible)) + } + /> + > + ) + } + + ); +} + +export default React.memo(TaskQualityComponent); diff --git a/cvat-ui/src/components/analytics-page/utils/text-formatting.ts b/cvat-ui/src/components/analytics-page/utils/text-formatting.ts new file mode 100644 index 000000000000..15bd8627ba46 --- /dev/null +++ b/cvat-ui/src/components/analytics-page/utils/text-formatting.ts @@ -0,0 +1,39 @@ +// Copyright (C) 2024 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import config from 'config'; + +export function toRepresentation(val?: number, isPercent = true, decimals = 1): string { + if (!Number.isFinite(val)) { + return 'N/A'; + } + + let repr = ''; + if (!val || (isPercent && (val === 100))) { + repr = `${val}`; // remove noise in the fractional part + } else { + repr = `${val?.toFixed(decimals)}`; + } + + if (isPercent) { + repr += `${isPercent ? '%' : ''}`; + } + + return repr; +} + +export function percent(a?: number, b?: number, decimals = 1): string | number { + if (typeof a !== 'undefined' && Number.isFinite(a) && b) { + return toRepresentation(Number(a / b) * 100, true, decimals); + } + return 'N/A'; +} + +export function clampValue(a?: number): string | number { + if (typeof a !== 'undefined' && Number.isFinite(a)) { + if (a <= config.NUMERIC_VALUE_CLAMP_THRESHOLD) return a; + return `> ${config.NUMERIC_VALUE_CLAMP_THRESHOLD}`; + } + return 'N/A'; +} 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 8fc5b0ed000d..37344eb7626b 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,7 +11,7 @@ import Text from 'antd/lib/typography/Text'; import { filterApplicableLabels } from 'utils/filter-applicable-labels'; import { Label } from 'cvat-core-wrapper'; -import { LogType } from 'cvat-logger'; +import { EventScope } from 'cvat-logger'; import { activateObject as activateObjectAction, changeFrameAsync, @@ -306,7 +306,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. currentLabel={activeObjectState.label.id} labels={applicableLabels} changeLabel={(value: Label): void => { - jobInstance.logger.log(LogType.changeLabel, { + jobInstance.logger.log(EventScope.changeLabel, { object_id: activeObjectState.clientID, from: activeObjectState.label.id, to: value.id, @@ -337,7 +337,7 @@ function AttributeAnnotationSidebar(props: StateToProps & DispatchToProps): JSX. currentValue={activeObjectState.attributes[activeAttribute.id]} onChange={(value: string) => { const { attributes } = activeObjectState; - jobInstance.logger.log(LogType.changeAttribute, { + jobInstance.logger.log(EventScope.changeAttribute, { id: activeAttribute.id, object_id: activeObjectState.clientID, value, diff --git a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx index 03538135c9e5..79295c1ea594 100644 --- a/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas2d/canvas-wrapper.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 @@ -18,7 +18,7 @@ import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import { ColorBy, GridColor, ObjectType, Workspace, ShapeType, ActiveControl, CombinedState, } from 'reducers'; -import { LogType } from 'cvat-logger'; +import { EventScope } from 'cvat-logger'; import { Canvas, HighlightSeverity, CanvasHint } from 'cvat-canvas-wrapper'; import { Canvas3d } from 'cvat-canvas3d-wrapper'; import { @@ -654,9 +654,9 @@ class CanvasWrapperComponent extends React.PureComponent { }; if (isDrawnFromScratch) { - jobInstance.logger.log(LogType.drawObject, { count: 1, duration, ...payload }); + jobInstance.logger.log(EventScope.drawObject, { count: 1, duration, ...payload }); } else { - jobInstance.logger.log(LogType.pasteObject, { count: 1, duration, ...payload }); + jobInstance.logger.log(EventScope.pasteObject, { count: 1, duration, ...payload }); } const objectState = new cvat.classes.ObjectState(state); @@ -670,7 +670,7 @@ class CanvasWrapperComponent extends React.PureComponent { updateActiveControl(ActiveControl.CURSOR); const { states, duration } = event.detail; - jobInstance.logger.log(LogType.mergeObjects, { + jobInstance.logger.log(EventScope.mergeObjects, { duration, count: states.length, }); @@ -684,7 +684,7 @@ class CanvasWrapperComponent extends React.PureComponent { updateActiveControl(ActiveControl.CURSOR); const { states, duration } = event.detail; - jobInstance.logger.log(LogType.groupObjects, { + jobInstance.logger.log(EventScope.groupObjects, { duration, count: states.length, }); @@ -698,7 +698,7 @@ class CanvasWrapperComponent extends React.PureComponent { updateActiveControl(ActiveControl.CURSOR); const { states, points, duration } = event.detail; - jobInstance.logger.log(LogType.joinObjects, { + jobInstance.logger.log(EventScope.joinObjects, { duration, count: states.length, }); @@ -712,7 +712,7 @@ class CanvasWrapperComponent extends React.PureComponent { updateActiveControl(ActiveControl.CURSOR); const { state, duration } = event.detail; - jobInstance.logger.log(LogType.splitObjects, { + jobInstance.logger.log(EventScope.splitObjects, { duration, count: 1, }); @@ -746,23 +746,23 @@ class CanvasWrapperComponent extends React.PureComponent { private onCanvasShapeDragged = (e: any): void => { const { jobInstance } = this.props; const { id } = e.detail; - jobInstance.logger.log(LogType.dragObject, { id }); + jobInstance.logger.log(EventScope.dragObject, { id }); }; private onCanvasShapeResized = (e: any): void => { const { jobInstance } = this.props; const { id } = e.detail; - jobInstance.logger.log(LogType.resizeObject, { id }); + jobInstance.logger.log(EventScope.resizeObject, { id }); }; private onCanvasImageFitted = (): void => { const { jobInstance } = this.props; - jobInstance.logger.log(LogType.fitImage); + jobInstance.logger.log(EventScope.fitImage); }; private onCanvasZoomChanged = (): void => { const { jobInstance } = this.props; - jobInstance.logger.log(LogType.zoomImage); + jobInstance.logger.log(EventScope.zoomImage); }; private onCanvasShapeClicked = (e: any): void => { @@ -837,7 +837,7 @@ class CanvasWrapperComponent extends React.PureComponent { const { jobInstance, updateActiveControl, onSliceAnnotations } = this.props; const { state, results, duration } = event.detail; updateActiveControl(ActiveControl.CURSOR); - jobInstance.logger.log(LogType.sliceObject, { + jobInstance.logger.log(EventScope.sliceObject, { count: 1, duration, clientID: state.clientID, @@ -1118,7 +1118,12 @@ class CanvasWrapperComponent extends React.PureComponent { - }> + } + > diff --git a/cvat-ui/src/components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D.tsx b/cvat-ui/src/components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D.tsx index c6754823ce1e..6054f4d72e00 100644 --- a/cvat-ui/src/components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D.tsx +++ b/cvat-ui/src/components/annotation-page/canvas/views/canvas3d/canvas-wrapper3D.tsx @@ -31,7 +31,7 @@ import { import { CameraAction, Canvas3d, ViewsDOM } from 'cvat-canvas3d-wrapper'; import CVATTooltip from 'components/common/cvat-tooltip'; -import { LogType } from 'cvat-logger'; +import { EventScope } from 'cvat-logger'; import { getCore, ObjectState, Job } from 'cvat-core-wrapper'; import GlobalHotKeys from 'utils/mousetrap-react'; @@ -481,9 +481,9 @@ const Canvas3DWrapperComponent = React.memo((props: Props): null => { const { state, duration } = event.detail; const isDrawnFromScratch = !state.label; if (isDrawnFromScratch) { - jobInstance.logger.log(LogType.drawObject, { count: 1, duration }); + jobInstance.logger.log(EventScope.drawObject, { count: 1, duration }); } else { - jobInstance.logger.log(LogType.pasteObject, { count: 1, duration }); + jobInstance.logger.log(EventScope.pasteObject, { count: 1, duration }); } state.objectType = state.objectType || activeObjectType; diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/control-visibility-observer.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/control-visibility-observer.tsx index 792f29940162..6df9f56ee13a 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/control-visibility-observer.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/control-visibility-observer.tsx @@ -1,5 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -9,7 +9,6 @@ import { SmallDashOutlined } from '@ant-design/icons'; import Popover from 'antd/lib/popover'; import React, { useEffect, useRef, useState } from 'react'; import ReactDOM from 'react-dom'; -import { ConnectedComponent } from 'react-redux'; import withVisibilityHandling from './handle-popover-visibility'; const extraControlsContentClassName = 'cvat-extra-controls-control-content'; @@ -19,13 +18,14 @@ const CustomPopover = withVisibilityHandling(Popover, 'extra-controls'); export function ExtraControlsControl(): JSX.Element { const [hasChildren, setHasChildren] = useState(false); const [initialized, setInitialized] = useState(false); + const [visible, setVisible] = useState(true); useEffect(() => { if (!initialized) { setInitialized(true); } - window.document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); + setVisible(false); }, []); onUpdateChildren = () => { @@ -37,7 +37,8 @@ export function ExtraControlsControl(): JSX.Element { return ( ( - ControlComponent: React.FunctionComponent | ConnectedComponent, + ControlComponent: React.FunctionComponent, ): React.FunctionComponent { let visibilityHeightThreshold = 0; // minimum value of height when element can be pushed to main panel diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx index 4f634ddc2bb8..8ebbfa893ace 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.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 @@ -7,10 +7,11 @@ import React from 'react'; import Layout from 'antd/lib/layout'; import { - ActiveControl, ObjectType, Rotation, ShapeType, CombinedState, + ActiveControl, Rotation, CombinedState, } from 'reducers'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; +import { LabelType } from 'cvat-core-wrapper'; import ControlVisibilityObserver, { ExtraControlsControl } from './control-visibility-observer'; import RotateControl, { Props as RotateControlProps } from './rotate-control'; @@ -105,14 +106,14 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { let tagControlVisible = withUnspecifiedType; const skeletonControlVisible = labels.some((label: Label) => label.type === 'skeleton'); labels.forEach((label: Label) => { - rectangleControlVisible = rectangleControlVisible || label.type === ShapeType.RECTANGLE; - polygonControlVisible = polygonControlVisible || label.type === ShapeType.POLYGON; - polylineControlVisible = polylineControlVisible || label.type === ShapeType.POLYLINE; - pointsControlVisible = pointsControlVisible || label.type === ShapeType.POINTS; - ellipseControlVisible = ellipseControlVisible || label.type === ShapeType.ELLIPSE; - cuboidControlVisible = cuboidControlVisible || label.type === ShapeType.CUBOID; - maskControlVisible = maskControlVisible || label.type === ShapeType.MASK; - tagControlVisible = tagControlVisible || label.type === ObjectType.TAG; + rectangleControlVisible = rectangleControlVisible || label.type === LabelType.RECTANGLE; + polygonControlVisible = polygonControlVisible || label.type === LabelType.POLYGON; + polylineControlVisible = polylineControlVisible || label.type === LabelType.POLYLINE; + pointsControlVisible = pointsControlVisible || label.type === LabelType.POINTS; + ellipseControlVisible = ellipseControlVisible || label.type === LabelType.ELLIPSE; + cuboidControlVisible = cuboidControlVisible || label.type === LabelType.CUBOID; + maskControlVisible = maskControlVisible || label.type === LabelType.MASK; + tagControlVisible = tagControlVisible || label.type === LabelType.TAG; }); const preventDefault = (event: KeyboardEvent | undefined): void => { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx index 6ebe9579ddd1..03be784ec104 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.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 @@ -150,13 +150,11 @@ function DrawShapePopoverComponent(props: Props): JSX.Element { ) : null} - + Shape - - {shapeType !== ShapeType.MASK && ( - + {shapeType !== ShapeType.MASK && ( - - )} + )} + ); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/handle-popover-visibility.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/handle-popover-visibility.tsx index f3ff680b5d26..b9b9295a7c34 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/handle-popover-visibility.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/handle-popover-visibility.tsx @@ -1,8 +1,9 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT -import React, { useState } from 'react'; +import React from 'react'; import Popover, { PopoverProps } from 'antd/lib/popover'; interface OwnProps { @@ -12,7 +13,6 @@ interface OwnProps { export default function withVisibilityHandling(WrappedComponent: typeof Popover, popoverType: string) { return function (props: OwnProps & PopoverProps): JSX.Element { - const [visible, setVisible] = useState(false); const { overlayClassName, onVisibleChange, ...rest } = props; const overlayClassNames = typeof overlayClassName === 'string' ? overlayClassName.split(/\s+/) : []; const popoverClassName = `cvat-${popoverType}-popover`; @@ -27,7 +27,7 @@ export default function withVisibilityHandling(WrappedComponent: typeof Popover, animationDuration: '0s', animationDelay: '0s', }} - trigger={visible ? ['click'] : ['click', 'hover']} + trigger={['click']} overlayClassName={overlayClassNames.join(' ').trim()} onVisibleChange={(_visible: boolean) => { if (_visible) { @@ -38,7 +38,6 @@ export default function withVisibilityHandling(WrappedComponent: typeof Popover, (element as HTMLElement).style.opacity = ''; } } - setVisible(_visible); if (onVisibleChange) onVisibleChange(_visible); }} /> diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx index edbbfa6fb3d5..f93671cec115 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/rotate-control.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -42,7 +43,6 @@ function RotateControl(props: Props): JSX.Element { > )} - trigger='hover' > diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx index fd230f5f86e2..cb88433096ff 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/tools-control.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 @@ -1100,6 +1100,7 @@ export class ToolsControlComponent extends React.PureComponent { { - if (!visible && colorPickerVisible) return; - setMenuVisible(visible); - }; - - const changeColorPickerVisible = (visible: boolean): void => { - if (!visible) { - setMenuVisible(false); - } - setColorPickerVisible(visible); - }; - return ( @@ -129,12 +117,24 @@ function ItemTopComponent(props: Props): JSX.Element { { !isGroundTruth && ( - + colorPickerVisible ? ( + { + changeColor(_color); + }} + > + + + + + ) : ( - + + + - + ) )} ); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-menu.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-menu.tsx index 42b148718c0f..1d8701914f11 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-menu.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item-menu.tsx @@ -17,8 +17,6 @@ import CVATTooltip from 'components/common/cvat-tooltip'; import { ObjectType, ShapeType, ColorBy } from 'reducers'; import { DimensionType, Job } from 'cvat-core-wrapper'; -import ColorPicker from './color-picker'; - interface Props { readonly: boolean; serverID: number | null; @@ -45,7 +43,7 @@ interface Props { toBackground(): void; toForeground(): void; resetCuboidPerspective(): void; - changeColorPickerVisible(visible: boolean): void; + setColorPickerVisible(visible: boolean): void; edit(): void; slice(): void; jobInstance: Job; @@ -223,30 +221,16 @@ function ToForegroundItem(props: ItemProps): JSX.Element { function SwitchColorItem(props: ItemProps): JSX.Element { const { toolProps, ...rest } = props; - const { - color, - colorPickerVisible, - changeColorShortcut, - colorBy, - changeColor, - changeColorPickerVisible, - } = toolProps; + const { changeColorShortcut, colorBy, setColorPickerVisible } = toolProps; + return ( - - - - - - {`Change ${colorBy.toLowerCase()} color`} - - - + setColorPickerVisible(true)}> + + + + {`Change ${colorBy.toLowerCase()} color`} + + ); } @@ -292,7 +276,11 @@ export default function ItemMenu(props: Props): JSX.Element { const is2D = jobInstance.dimension === DimensionType.DIMENSION_2D; return ( - + window.document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }))} + className='cvat-object-item-menu' + selectable={false} + > {!readonly && objectType !== ObjectType.TAG && ( diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss index d24862b91f89..5d61bd02f079 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/standard-workspace/styles.scss @@ -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 @@ -9,8 +9,7 @@ height: 100%; > .ant-layout-content { - overflow-y: hidden; - overflow-x: hidden; + overflow: hidden hidden; } } @@ -27,8 +26,7 @@ .cvat-objects-sidebar { height: 100%; - overflow-y: auto; - overflow-x: hidden; + overflow: hidden auto; > .ant-layout-sider-children { display: flex; @@ -274,21 +272,16 @@ width: 100%; } - > div:last-child { - span { - width: 100%; + > div:last-child > div { + display: flex; + justify-content: space-between; + + &:has(.cvat-draw-mask-shape-button) { + justify-content: space-around; } button { - width: 100%; - - &:nth-child(1) { - border-radius: 3px 0 0 3px; - } - - &:nth-child(2) { - border-radius: 0 3px 3px 0; - } + width: $grid-unit-size * 15; } } } diff --git a/cvat-ui/src/components/annotation-page/styles.scss b/cvat-ui/src/components/annotation-page/styles.scss index ebf03fb22009..c802b3a1fb14 100644 --- a/cvat-ui/src/components/annotation-page/styles.scss +++ b/cvat-ui/src/components/annotation-page/styles.scss @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -460,7 +460,7 @@ .ant-modal-body { padding: 1px; - .recently-used-wrapper { + .cvat-recently-used-filters-wrapper { padding-top: $grid-unit-size * 2; } diff --git a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss index 7c6bd18272d6..713749ecde07 100644 --- a/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss +++ b/cvat-ui/src/components/annotation-page/tag-annotation-workspace/styles.scss @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -84,5 +84,5 @@ .cvat-add-tag-button { margin-left: $grid-unit-size; width: $grid-unit-size * 4; - height: $grid-unit-size * 3; + height: $grid-unit-size * 4; } diff --git a/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx b/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx index dac05ace0ac5..4a0272366960 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/annotation-menu.tsx @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -7,16 +7,15 @@ import React from 'react'; import ReactDOM from 'react-dom'; import { withRouter, RouteComponentProps } from 'react-router'; -import Menu from 'antd/lib/menu'; import Modal from 'antd/lib/modal'; import Text from 'antd/lib/typography/Text'; import InputNumber from 'antd/lib/input-number'; import Checkbox from 'antd/lib/checkbox'; import Collapse from 'antd/lib/collapse'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { MenuInfo } from 'rc-menu/lib/interface'; import CVATTooltip from 'components/common/cvat-tooltip'; +import Menu, { MenuInfo } from 'components/dropdown-menu'; + import { getCore, JobStage } from 'cvat-core-wrapper'; import AnnotationsActionsModalContent from '../annotations-actions/annotations-actions-modal'; @@ -187,7 +186,14 @@ function AnnotationMenuComponent(props: Props & RouteComponentProps): JSX.Elemen }; return ( - onClickMenuWrapper(params)} className='cvat-annotation-menu' selectable={false}> + { + onClickMenuWrapper(params); + }} + className='cvat-annotation-menu' + selectable={false} + > Upload annotations Export job dataset Remove annotations diff --git a/cvat-ui/src/components/annotation-page/top-bar/filters-modal.tsx b/cvat-ui/src/components/annotation-page/top-bar/filters-modal.tsx index b1f7ec1da6bb..7e54fcd34785 100644 --- a/cvat-ui/src/components/annotation-page/top-bar/filters-modal.tsx +++ b/cvat-ui/src/components/annotation-page/top-bar/filters-modal.tsx @@ -303,10 +303,15 @@ function FiltersModalComponent(): JSX.Element { > - + - }> + } + > Menu diff --git a/cvat-ui/src/components/cloud-storages-page/cloud-storage-item.tsx b/cvat-ui/src/components/cloud-storages-page/cloud-storage-item.tsx index bf0790aef062..7664934e5956 100644 --- a/cvat-ui/src/components/cloud-storages-page/cloud-storage-item.tsx +++ b/cvat-ui/src/components/cloud-storages-page/cloud-storage-item.tsx @@ -1,5 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -13,12 +13,12 @@ import Paragraph from 'antd/lib/typography/Paragraph'; import Text from 'antd/lib/typography/Text'; import Button from 'antd/lib/button'; import Dropdown from 'antd/lib/dropdown'; -import Menu from 'antd/lib/menu'; import Modal from 'antd/lib/modal'; import moment from 'moment'; import { CloudStorage, CombinedState } from 'reducers'; import { deleteCloudStorageAsync } from 'actions/cloud-storage-actions'; +import Menu from 'components/dropdown-menu'; import CVATTooltip from 'components/common/cvat-tooltip'; import Preview from 'components/common/preview'; import Status from './cloud-storage-status'; @@ -118,6 +118,8 @@ export default function CloudStorageItemComponent(props: Props): JSX.Element { Update diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 97416ea6b3ca..9c60ae5adf80 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -89,9 +89,9 @@ interface CVATAppProps { initModels: () => void; resetErrors: () => void; resetMessages: () => void; - loadAuthActions: () => void; loadOrganization: () => void; initInvitations: () => void; + loadServerAPISchema: () => void; userInitialized: boolean; userFetching: boolean; organizationFetching: boolean; @@ -106,14 +106,16 @@ interface CVATAppProps { aboutFetching: boolean; userAgreementsFetching: boolean; userAgreementsInitialized: boolean; - authActionsFetching: boolean; - authActionsInitialized: boolean; notifications: NotificationsState; user: any; isModelPluginActive: boolean; pluginComponents: PluginsState['components']; invitationsFetching: boolean; invitationsInitialized: boolean; + serverAPISchemaFetching: boolean; + serverAPISchemaInitialized: boolean; + isPasswordResetEnabled: boolean; + isRegistrationEnabled: boolean; } interface CVATAppState { @@ -261,7 +263,7 @@ class CVATApplication extends React.PureComponent <> - + {isRegistrationEnabled && ( + + )} @@ -558,12 +564,16 @@ class CVATApplication extends React.PureComponent - - + {isPasswordResetEnabled && ( + + )} + {isPasswordResetEnabled && ( + + )} { routesToRender } diff --git a/cvat-ui/src/components/dropdown-menu.tsx b/cvat-ui/src/components/dropdown-menu.tsx new file mode 100644 index 000000000000..37b1cf37a589 --- /dev/null +++ b/cvat-ui/src/components/dropdown-menu.tsx @@ -0,0 +1,36 @@ +// Copyright (C) 2024 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import React from 'react'; +import { omit } from 'lodash'; +import Menu, { MenuProps } from 'antd/lib/menu'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { MenuInfo as MenuInfoType } from 'rc-menu/lib/interface'; + +// component is used for menu appearing as dropdown component +// the purpose is to close menu after clicking an item +export default function DropdownMenu(props: MenuProps): JSX.Element { + const { onClick } = props; + + return ( + { + // close menu + window.document.body.dispatchEvent(new MouseEvent('mousedown', { bubbles: true })); + + if (onClick) { + onClick(info); + } + }} + {...omit(props, 'onClick')} + /> + ); +} + +// set these properties to avoid extra import from antd in other modules +DropdownMenu.Item = Menu.Item; +DropdownMenu.SubMenu = Menu.SubMenu; +DropdownMenu.Divider = Menu.Divider; + +export type MenuInfo = MenuInfoType; diff --git a/cvat-ui/src/components/global-error-boundary/global-error-boundary.tsx b/cvat-ui/src/components/global-error-boundary/global-error-boundary.tsx index 6c77b0ac0b2e..6dbf4e20ee7b 100644 --- a/cvat-ui/src/components/global-error-boundary/global-error-boundary.tsx +++ b/cvat-ui/src/components/global-error-boundary/global-error-boundary.tsx @@ -17,7 +17,7 @@ import ErrorStackParser from 'error-stack-parser'; import { ThunkDispatch } from 'utils/redux'; import { resetAfterErrorAsync } from 'actions/boundaries-actions'; import { CombinedState } from 'reducers'; -import logger, { LogType } from 'cvat-logger'; +import logger, { EventScope } from 'cvat-logger'; import CVATTooltip from 'components/common/cvat-tooltip'; import config from 'config'; import { saveLogsAsync } from 'actions/annotation-actions'; @@ -103,9 +103,9 @@ class GlobalErrorBoundary extends React.PureComponent { }; if (job) { - job.logger.log(LogType.exception, logPayload).then(saveLogs); + job.logger.log(EventScope.exception, logPayload).then(saveLogs); } else { - logger.log(LogType.exception, logPayload).then(saveLogs); + logger.log(EventScope.exception, logPayload).then(saveLogs); } } diff --git a/cvat-ui/src/components/header/header.tsx b/cvat-ui/src/components/header/header.tsx index 2ac1b92414c0..124b94ba457a 100644 --- a/cvat-ui/src/components/header/header.tsx +++ b/cvat-ui/src/components/header/header.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 @@ -79,13 +79,17 @@ function mapStateToProps(state: CombinedState): StateToProps { fetching: logoutFetching, fetching: changePasswordFetching, showChangePasswordDialog: changePasswordDialogShown, - allowChangePassword: renderChangePasswordItem, }, plugins: { list }, about, shortcuts: { normalizedKeyMap, keyMap, visibleShortcutsHelp: shortcutsModalVisible }, settings: { showDialog: settingsModalVisible }, organizations: { fetching: organizationFetching, current: currentOrganization }, + serverAPI: { + configuration: { + isPasswordChangeEnabled: renderChangePasswordItem, + }, + }, } = state; return { @@ -436,7 +440,7 @@ function HeaderComponent(props: Props): JSX.Element { ); const userMenu = ( - + { menuItems.sort((menuItem1, menuItem2) => menuItem1[1] - menuItem2[1]) .map((menuItem) => menuItem[0]) } @@ -558,7 +562,13 @@ function HeaderComponent(props: Props): JSX.Element { }} /> - + diff --git a/cvat-ui/src/components/header/styles.scss b/cvat-ui/src/components/header/styles.scss index 42f54ccc54cd..835a6957d48a 100644 --- a/cvat-ui/src/components/header/styles.scss +++ b/cvat-ui/src/components/header/styles.scss @@ -81,6 +81,10 @@ } } +.cvat-header-menu { + width: $grid-unit-size * 20; +} + .cvat-header-menu-active-organization-item { ::after { content: ' \2713'; diff --git a/cvat-ui/src/components/job-item/job-actions-menu.tsx b/cvat-ui/src/components/job-item/job-actions-menu.tsx index e6b8539337ee..8d71aa83870b 100644 --- a/cvat-ui/src/components/job-item/job-actions-menu.tsx +++ b/cvat-ui/src/components/job-item/job-actions-menu.tsx @@ -5,10 +5,7 @@ import React, { useCallback } from 'react'; import { useDispatch } from 'react-redux'; import { useHistory } from 'react-router'; -import Menu from 'antd/lib/menu'; import Modal from 'antd/lib/modal'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { MenuInfo } from 'rc-menu/lib/interface'; import { exportActions } from 'actions/export-actions'; import { @@ -16,6 +13,7 @@ import { } from 'cvat-core-wrapper'; import { deleteJobAsync } from 'actions/jobs-actions'; import { importActions } from 'actions/import-actions'; +import Menu, { MenuInfo } from 'components/dropdown-menu'; const core = getCore(); @@ -54,7 +52,9 @@ function JobActionsMenu(props: Props): JSX.Element { } else if (action.key === 'project') { history.push(`/projects/${job.projectId}`); } else if (action.key === 'bug_tracker') { - if (job.bugTracker) window.open(job.bugTracker, '_blank', 'noopener noreferrer'); + if (job.bugTracker) { + window.open(job.bugTracker, '_blank', 'noopener noreferrer'); + } } else if (action.key === 'import_job') { dispatch(importActions.openImportDatasetModal(job)); } else if (action.key === 'export_job') { diff --git a/cvat-ui/src/components/job-item/job-item.tsx b/cvat-ui/src/components/job-item/job-item.tsx index 312e65aa2b56..124c01c2af8a 100644 --- a/cvat-ui/src/components/job-item/job-item.tsx +++ b/cvat-ui/src/components/job-item/job-item.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -242,7 +242,11 @@ function JobItem(props: Props): JSX.Element { - }> + } + > diff --git a/cvat-ui/src/components/jobs-page/job-card.tsx b/cvat-ui/src/components/jobs-page/job-card.tsx index 7177a33e2858..22b1fa5a335b 100644 --- a/cvat-ui/src/components/jobs-page/job-card.tsx +++ b/cvat-ui/src/components/jobs-page/job-card.tsx @@ -1,5 +1,5 @@ // Copyright (C) 2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -76,7 +76,11 @@ function JobCardComponent(props: Props): JSX.Element { {job.assignee.username} ) : null} - }> + } + > diff --git a/cvat-ui/src/components/login-page/login-form.tsx b/cvat-ui/src/components/login-page/login-form.tsx index 246f0ac55fad..c6f906c1f706 100644 --- a/cvat-ui/src/components/login-page/login-form.tsx +++ b/cvat-ui/src/components/login-page/login-form.tsx @@ -28,13 +28,15 @@ export interface LoginData { interface Props { renderResetPassword: boolean; + renderRegistrationComponent: boolean; + renderBasicLoginComponent: boolean; fetching: boolean; onSubmit(loginData: LoginData): void; } function LoginFormComponent(props: Props): JSX.Element { const { - fetching, onSubmit, renderResetPassword, + fetching, onSubmit, renderResetPassword, renderRegistrationComponent, renderBasicLoginComponent, } = props; const authQuery = useAuthQuery(); @@ -79,7 +81,7 @@ function LoginFormComponent(props: Props): JSX.Element { ) } { - !credential && ( + !credential && renderRegistrationComponent && ( @@ -110,65 +112,69 @@ function LoginFormComponent(props: Props): JSX.Element { onSubmit(loginData); }} > - - 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 && ( - - - Next - - - ) - } + { + credential && ( + + + + ) + } + { + !!credential && ( + + + Next + + + ) + } + > + )} { 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 && ( - + } /> ) diff --git a/cvat-ui/src/components/organization-page/member-item.tsx b/cvat-ui/src/components/organization-page/member-item.tsx index d669d151413c..fede4537f593 100644 --- a/cvat-ui/src/components/organization-page/member-item.tsx +++ b/cvat-ui/src/components/organization-page/member-item.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -7,14 +8,13 @@ import { useSelector } from 'react-redux'; import Select from 'antd/lib/select'; import Text from 'antd/lib/typography/Text'; import Dropdown from 'antd/lib/dropdown'; -import Menu from 'antd/lib/menu'; -// eslint-disable-next-line import/no-extraneous-dependencies -import { MenuInfo } from 'rc-menu/lib/interface'; import { Row, Col } from 'antd/lib/grid'; import moment from 'moment'; import { DeleteOutlined, MoreOutlined } from '@ant-design/icons'; import Modal from 'antd/lib/modal'; import { CombinedState } from 'reducers'; +import Menu, { MenuInfo } from 'components/dropdown-menu'; + import { Membership } from 'cvat-core-wrapper'; export interface Props { @@ -42,20 +42,23 @@ function MemberItem(props: Props): JSX.Element { const { username: selfUserName } = useSelector((state: CombinedState) => state.auth.user); const invitationActionsMenu = invitation && ( - { - if (action.key === MenuKeys.RESEND_INVITATION) { - onResendInvitation(invitation.key); - } else if (action.key === MenuKeys.DELETE_INVITATION) { - onDeleteInvitation(invitation.key); - } - }} - > - Resend invitation - - Remove invitation - - )} + { + 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} + + )} > Actions diff --git a/cvat-ui/src/components/project-page/project-page.tsx b/cvat-ui/src/components/project-page/project-page.tsx index c0764c2af393..7514641df851 100644 --- a/cvat-ui/src/components/project-page/project-page.tsx +++ b/cvat-ui/src/components/project-page/project-page.tsx @@ -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 @@ -290,6 +290,7 @@ export default function ProjectPageComponent(): JSX.Element { - }> + } + > Actions diff --git a/cvat-ui/src/components/projects-page/actions-menu.tsx b/cvat-ui/src/components/projects-page/actions-menu.tsx index 64dac4f17dbf..e412c3c55cd5 100644 --- a/cvat-ui/src/components/projects-page/actions-menu.tsx +++ b/cvat-ui/src/components/projects-page/actions-menu.tsx @@ -1,18 +1,19 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT import React, { useCallback } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import Modal from 'antd/lib/modal'; -import Menu from 'antd/lib/menu'; import { LoadingOutlined } from '@ant-design/icons'; import { CombinedState } from 'reducers'; import { deleteProjectAsync } from 'actions/projects-actions'; import { exportActions } from 'actions/export-actions'; import { importActions } from 'actions/import-actions'; import { useHistory } from 'react-router'; +import Menu from 'components/dropdown-menu'; + import { usePlugins } from 'utils/hooks'; interface Props { diff --git a/cvat-ui/src/components/projects-page/project-item.tsx b/cvat-ui/src/components/projects-page/project-item.tsx index 703029c373af..8e263225e7f8 100644 --- a/cvat-ui/src/components/projects-page/project-item.tsx +++ b/cvat-ui/src/components/projects-page/project-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 @@ -102,7 +102,11 @@ export default function ProjectItemComponent(props: Props): JSX.Element { {`Last updated ${updated}`} - }> + } + > } /> diff --git a/cvat-ui/src/components/projects-page/top-bar.tsx b/cvat-ui/src/components/projects-page/top-bar.tsx index fca5c3480b21..2ac90e795f9a 100644 --- a/cvat-ui/src/components/projects-page/top-bar.tsx +++ b/cvat-ui/src/components/projects-page/top-bar.tsx @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -91,6 +91,7 @@ function TopBarComponent(props: Props): JSX.Element { diff --git a/cvat-ui/src/components/task-page/top-bar.tsx b/cvat-ui/src/components/task-page/top-bar.tsx index 6a365caf255b..cab6e21bac0d 100644 --- a/cvat-ui/src/components/task-page/top-bar.tsx +++ b/cvat-ui/src/components/task-page/top-bar.tsx @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -51,12 +52,15 @@ export default function DetailsComponent(props: DetailsComponentProps): JSX.Elem )} - - )} + + )} > Actions diff --git a/cvat-ui/src/components/tasks-page/task-item.tsx b/cvat-ui/src/components/tasks-page/task-item.tsx index f689c8cf8c01..247e3ac01976 100644 --- a/cvat-ui/src/components/tasks-page/task-item.tsx +++ b/cvat-ui/src/components/tasks-page/task-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 @@ -258,6 +258,7 @@ class TaskItemComponent extends React.PureComponent - ( - - - { - e.preventDefault(); - history.push(`/webhooks/update/${id}`); - return false; + ( + + + { + e.preventDefault(); + history.push(`/webhooks/update/${id}`); + return false; + }} + > + Edit + + + { + Modal.confirm({ + title: 'Are you sure you want to remove the hook?', + content: 'It will stop notificating the specified URL about listed events', + className: 'cvat-modal-confirm-remove-webhook', + onOk: () => { + dispatch(deleteWebhookAsync(webhookInstance)).then(() => { + setIsRemoved(true); + }); + }, + }); }} > - Edit - - - { - Modal.confirm({ - title: 'Are you sure you want to remove the hook?', - content: 'It will stop notificating the specified URL about listed events', - className: 'cvat-modal-confirm-remove-webhook', - onOk: () => { - dispatch(deleteWebhookAsync(webhookInstance)).then(() => { - setIsRemoved(true); - }); - }, - }); - }} - > - Delete - - - )} + Delete + + + )} > Actions diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-buttons.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-buttons.tsx index 7884ba904856..32890deabd0d 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-buttons.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-buttons.tsx @@ -5,7 +5,7 @@ import React from 'react'; import { connect } from 'react-redux'; -import { LogType } from 'cvat-logger'; +import { EventScope } from 'cvat-logger'; import isAbleToChangeFrame from 'utils/is-able-to-change-frame'; import { ThunkDispatch } from 'utils/redux'; import { updateAnnotationsAsync, changeFrameAsync } from 'actions/annotation-actions'; @@ -114,7 +114,7 @@ class ItemButtonsWrapper extends React.PureComponent { const { objectState, jobInstance, readonly } = this.props; if (!readonly) { - jobInstance.logger.log(LogType.lockObject, { locked: true }); + jobInstance.logger.log(EventScope.lockObject, { locked: true }); objectState.lock = true; this.commit(); } @@ -123,7 +123,7 @@ class ItemButtonsWrapper extends React.PureComponent { const { objectState, jobInstance, readonly } = this.props; if (!readonly) { - jobInstance.logger.log(LogType.lockObject, { locked: false }); + jobInstance.logger.log(EventScope.lockObject, { locked: false }); objectState.lock = false; this.commit(); } diff --git a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item-details.tsx b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item-details.tsx index 4b9070d61dbc..a9c05460e39b 100644 --- a/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item-details.tsx +++ b/cvat-ui/src/containers/annotation-page/standard-workspace/objects-side-bar/object-item-details.tsx @@ -8,7 +8,7 @@ import { CombinedState } from 'reducers'; import ObjectItemDetails from 'components/annotation-page/standard-workspace/objects-side-bar/object-item-details'; import { AnyAction } from 'redux'; import { updateAnnotationsAsync, collapseObjectItems } from 'actions/annotation-actions'; -import { LogType } from 'cvat-logger'; +import { EventScope } from 'cvat-logger'; import { connect } from 'react-redux'; interface OwnProps { @@ -81,7 +81,7 @@ class ObjectItemDetailsContainer extends React.PureComponent { state, readonly, jobInstance, updateState, } = this.props; if (!readonly && state) { - jobInstance.logger.log(LogType.changeAttribute, { + jobInstance.logger.log(EventScope.changeAttribute, { id, value, object_id: state.clientID, 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 1bda80f1f457..f284c9bb8e32 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 @@ -29,7 +29,7 @@ import { Label, ObjectState, Attribute, Job, } from 'cvat-core-wrapper'; import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; -import { LogType } from 'cvat-logger'; +import { EventScope } from 'cvat-logger'; import { Canvas3d } from 'cvat-canvas3d-wrapper'; import { filterApplicableLabels } from 'utils/filter-applicable-labels'; @@ -314,7 +314,7 @@ class ObjectItemContainer extends React.PureComponent { private changeLabel = (label: any): void => { const { jobInstance, objectState, readonly } = this.props; if (!readonly) { - jobInstance.logger.log(LogType.changeLabel, { + jobInstance.logger.log(EventScope.changeLabel, { object_id: objectState.clientID, from: objectState.label.id, to: label.id, diff --git a/cvat-ui/src/containers/login-page/login-page.tsx b/cvat-ui/src/containers/login-page/login-page.tsx index 71c39939f952..8c8b10707c7e 100644 --- a/cvat-ui/src/containers/login-page/login-page.tsx +++ b/cvat-ui/src/containers/login-page/login-page.tsx @@ -11,6 +11,8 @@ interface StateToProps { fetching: boolean; renderResetPassword: boolean; hasEmailVerificationBeenSent: boolean; + renderRegistrationComponent: boolean; + renderBasicLoginComponent: boolean; } interface DispatchToProps { @@ -20,7 +22,9 @@ interface DispatchToProps { function mapStateToProps(state: CombinedState): StateToProps { return { fetching: state.auth.fetching, - renderResetPassword: state.auth.allowResetPassword, + renderResetPassword: state.serverAPI.configuration.isPasswordResetEnabled, + renderRegistrationComponent: state.serverAPI.configuration.isRegistrationEnabled, + renderBasicLoginComponent: state.serverAPI.configuration.isBasicLoginEnabled, hasEmailVerificationBeenSent: state.auth.hasEmailVerificationBeenSent, }; } diff --git a/cvat-ui/src/cvat-core-wrapper.ts b/cvat-ui/src/cvat-core-wrapper.ts index e8608c9baca0..3731013d3054 100644 --- a/cvat-ui/src/cvat-core-wrapper.ts +++ b/cvat-ui/src/cvat-core-wrapper.ts @@ -14,7 +14,7 @@ import { ModelProvider } from 'cvat-core/src/lambda-manager'; import { Label, Attribute, } from 'cvat-core/src/labels'; -import { SerializedAttribute, SerializedLabel } from 'cvat-core/src/server-response-types'; +import { SerializedAttribute, SerializedLabel, SerializedAPISchema } from 'cvat-core/src/server-response-types'; import { Job, Task } from 'cvat-core/src/session'; import Project from 'cvat-core/src/project'; import QualityReport, { QualitySummary } from 'cvat-core/src/quality-report'; @@ -35,7 +35,7 @@ import Organization, { Membership, Invitation } from 'cvat-core/src/organization import AnnotationGuide from 'cvat-core/src/guide'; import AnalyticsReport, { AnalyticsEntryViewType, AnalyticsEntry } from 'cvat-core/src/analytics-report'; import { Dumper } from 'cvat-core/src/annotation-formats'; -import { EventLogger } from 'cvat-core/src/log'; +import { Event } from 'cvat-core/src/event'; import { APIWrapperEnterOptions } from 'cvat-core/src/plugins'; import BaseSingleFrameAction, { ActionParameterType } from 'cvat-core/src/annotations-actions'; @@ -93,7 +93,7 @@ export { AnalyticsEntry, AnalyticsEntryViewType, ServerError, - EventLogger, + Event, FrameData, ActionParameterType, }; @@ -106,4 +106,5 @@ export type { APIWrapperEnterOptions, QualitySummary, CVATCore, + SerializedAPISchema, }; diff --git a/cvat-ui/src/cvat-logger.ts b/cvat-ui/src/cvat-logger.ts index f300f626139c..36745bded42e 100644 --- a/cvat-ui/src/cvat-logger.ts +++ b/cvat-ui/src/cvat-logger.ts @@ -6,7 +6,7 @@ import { getCore } from 'cvat-core-wrapper'; const core = getCore(); const { logger } = core; -const { LogType } = core.enums; +const { EventScope } = core.enums; export default logger; -export { LogType }; +export { EventScope }; diff --git a/cvat-ui/src/index.tsx b/cvat-ui/src/index.tsx index 48009fc6f40c..084ae7c2649f 100644 --- a/cvat-ui/src/index.tsx +++ b/cvat-ui/src/index.tsx @@ -9,7 +9,7 @@ import { connect, Provider } from 'react-redux'; import { BrowserRouter } from 'react-router-dom'; import { getAboutAsync } from 'actions/about-actions'; -import { authorizedAsync, loadAuthActionsAsync } from 'actions/auth-actions'; +import { authorizedAsync } from 'actions/auth-actions'; import { getFormatsAsync } from 'actions/formats-actions'; import { getModelsAsync } from 'actions/models-actions'; import { getPluginsAsync } from 'actions/plugins-actions'; @@ -17,12 +17,13 @@ import { getUserAgreementsAsync } from 'actions/useragreements-actions'; import CVATApplication from 'components/cvat-app'; import PluginsEntrypoint from 'components/plugins-entrypoint'; import LayoutGrid from 'components/layout-grid/layout-grid'; -import logger, { LogType } from 'cvat-logger'; +import logger, { EventScope } from 'cvat-logger'; import createCVATStore, { getCVATStore } from 'cvat-store'; import createRootReducer from 'reducers/root-reducer'; import { activateOrganizationAsync } from 'actions/organization-actions'; import { resetErrors, resetMessages } from 'actions/notification-actions'; import { getInvitationsAsync } from 'actions/invitations-actions'; +import { getServerAPISchemaAsync } from 'actions/server-actions'; import { CombinedState, NotificationsState, PluginsState } from './reducers'; createCVATStore(createRootReducer); @@ -44,16 +45,16 @@ interface StateToProps { formatsFetching: boolean; userAgreementsInitialized: boolean; userAgreementsFetching: boolean; - authActionsFetching: boolean; - authActionsInitialized: boolean; - allowChangePassword: boolean; - allowResetPassword: boolean; notifications: NotificationsState; user: any; isModelPluginActive: boolean; pluginComponents: PluginsState['components']; invitationsFetching: boolean; invitationsInitialized: boolean; + serverAPISchemaFetching: boolean; + serverAPISchemaInitialized: boolean; + isPasswordResetEnabled: boolean; + isRegistrationEnabled: boolean; } interface DispatchToProps { @@ -65,20 +66,15 @@ interface DispatchToProps { resetErrors: () => void; resetMessages: () => void; loadUserAgreements: () => void; - loadAuthActions: () => void; loadOrganization: () => void; initInvitations: () => void; + loadServerAPISchema: () => void; } function mapStateToProps(state: CombinedState): StateToProps { - const { plugins } = state; - const { auth } = state; - const { formats } = state; - const { about } = state; - const { userAgreements } = state; - const { models } = state; - const { organizations } = state; - const { invitations } = state; + const { + plugins, auth, formats, about, userAgreements, models, organizations, invitations, serverAPI, + } = state; return { userInitialized: auth.initialized, @@ -95,16 +91,16 @@ function mapStateToProps(state: CombinedState): StateToProps { formatsFetching: formats.fetching, userAgreementsInitialized: userAgreements.initialized, userAgreementsFetching: userAgreements.fetching, - authActionsFetching: auth.authActionsFetching, - authActionsInitialized: auth.authActionsInitialized, - allowChangePassword: auth.allowChangePassword, - allowResetPassword: auth.allowResetPassword, notifications: state.notifications, user: auth.user, pluginComponents: plugins.components, isModelPluginActive: plugins.list.MODELS, invitationsFetching: invitations.fetching, invitationsInitialized: invitations.initialized, + serverAPISchemaFetching: serverAPI.fetching, + serverAPISchemaInitialized: serverAPI.initialized, + isPasswordResetEnabled: serverAPI.configuration.isPasswordResetEnabled, + isRegistrationEnabled: serverAPI.configuration.isRegistrationEnabled, }; } @@ -118,9 +114,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps { loadAbout: (): void => dispatch(getAboutAsync()), resetErrors: (): void => dispatch(resetErrors()), resetMessages: (): void => dispatch(resetMessages()), - loadAuthActions: (): void => dispatch(loadAuthActionsAsync()), loadOrganization: (): void => dispatch(activateOrganizationAsync()), initInvitations: (): void => dispatch(getInvitationsAsync({ page: 1 }, true)), + loadServerAPISchema: (): void => dispatch(getServerAPISchemaAsync()), }; } @@ -168,9 +164,9 @@ window.addEventListener('error', (errorEvent: ErrorEvent): boolean => { const re = /\/tasks\/[0-9]+\/jobs\/[0-9]+$/; const { instance: job } = state.annotation.job; if (re.test(pathname) && job) { - job.logger.log(LogType.exception, logPayload); + job.logger.log(EventScope.exception, logPayload); } else { - logger.log(LogType.exception, logPayload); + logger.log(EventScope.exception, logPayload); } } diff --git a/cvat-ui/src/reducers/analytics-reducer.ts b/cvat-ui/src/reducers/analytics-reducer.ts deleted file mode 100644 index 80dda841001e..000000000000 --- a/cvat-ui/src/reducers/analytics-reducer.ts +++ /dev/null @@ -1,110 +0,0 @@ -// Copyright (C) 2023 CVAT.ai Corporation -// -// SPDX-License-Identifier: MIT - -import { AnalyticsActions, AnalyticsActionsTypes } from 'actions/analytics-actions'; -import { AnalyticsState } from 'reducers'; - -const defaultState: AnalyticsState = { - fetching: false, - quality: { - tasksReports: [], - jobsReports: [], - query: { - taskId: null, - jobId: null, - parentId: null, - }, - settings: { - modalVisible: false, - fetching: false, - current: null, - }, - }, -}; - -export default function ( - state: AnalyticsState = defaultState, - action: AnalyticsActions, -): AnalyticsState { - switch (action.type) { - case AnalyticsActionsTypes.GET_QUALITY_REPORTS: { - return { - ...state, - fetching: true, - quality: { - ...state.quality, - query: { - ...action.payload.query, - }, - }, - }; - } - case AnalyticsActionsTypes.GET_QUALITY_REPORTS_SUCCESS: - return { - ...state, - fetching: false, - quality: { - ...state.quality, - tasksReports: action.payload.tasksReports, - jobsReports: action.payload.jobsReports, - }, - }; - case AnalyticsActionsTypes.GET_QUALITY_REPORTS_FAILED: - return { - ...state, - fetching: false, - }; - case AnalyticsActionsTypes.SWITCH_QUALITY_SETTINGS_VISIBLE: - return { - ...state, - quality: { - ...state.quality, - settings: { - ...state.quality.settings, - modalVisible: action.payload.visible, - }, - }, - }; - case AnalyticsActionsTypes.UPDATE_QUALITY_SETTINGS: - case AnalyticsActionsTypes.GET_QUALITY_SETTINGS: { - return { - ...state, - quality: { - ...state.quality, - settings: { - ...state.quality.settings, - fetching: true, - }, - }, - }; - } - case AnalyticsActionsTypes.UPDATE_QUALITY_SETTINGS_SUCCESS: - case AnalyticsActionsTypes.GET_QUALITY_SETTINGS_SUCCESS: - return { - ...state, - quality: { - ...state.quality, - settings: { - ...state.quality.settings, - current: action.payload.settings, - fetching: false, - }, - }, - }; - case AnalyticsActionsTypes.UPDATE_QUALITY_SETTINGS_FAILED: - case AnalyticsActionsTypes.GET_QUALITY_SETTINGS_FAILED: - return { - ...state, - quality: { - ...state.quality, - settings: { - ...state.quality.settings, - fetching: false, - }, - }, - }; - default: - return state; - } -} diff --git a/cvat-ui/src/reducers/annotation-reducer.ts b/cvat-ui/src/reducers/annotation-reducer.ts index 7579d50b8a9b..53b8d62878a8 100644 --- a/cvat-ui/src/reducers/annotation-reducer.ts +++ b/cvat-ui/src/reducers/annotation-reducer.ts @@ -72,7 +72,7 @@ const defaultState: AnnotationState = { fetching: false, delay: 0, changeTime: null, - changeFrameLog: null, + changeFrameEvent: null, }, ranges: '', playing: false, @@ -288,7 +288,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { curZ, delay, changeTime, - changeFrameLog, + changeFrameEvent, } = action.payload; return { @@ -303,7 +303,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { fetching: false, changeTime, delay, - changeFrameLog, + changeFrameEvent, }, }, annotations: { @@ -327,7 +327,7 @@ export default (state = defaultState, action: AnyAction): AnnotationState => { frame: { ...state.player.frame, fetching: false, - changeFrameLog: null, + changeFrameEvent: null, }, }, }; diff --git a/cvat-ui/src/reducers/auth-reducer.ts b/cvat-ui/src/reducers/auth-reducer.ts index 086ea5b2e1dc..362903fad49c 100644 --- a/cvat-ui/src/reducers/auth-reducer.ts +++ b/cvat-ui/src/reducers/auth-reducer.ts @@ -11,11 +11,7 @@ const defaultState: AuthState = { initialized: false, fetching: false, user: null, - authActionsFetching: false, - authActionsInitialized: false, - allowChangePassword: false, showChangePasswordDialog: false, - allowResetPassword: false, hasEmailVerificationBeenSent: false, }; @@ -140,27 +136,6 @@ export default function (state = defaultState, action: AuthActions | BoundariesA ...state, fetching: false, }; - case AuthActionTypes.LOAD_AUTH_ACTIONS: - return { - ...state, - authActionsFetching: true, - }; - case AuthActionTypes.LOAD_AUTH_ACTIONS_SUCCESS: - return { - ...state, - authActionsFetching: false, - authActionsInitialized: true, - allowChangePassword: action.payload.allowChangePassword, - allowResetPassword: action.payload.allowResetPassword, - }; - case AuthActionTypes.LOAD_AUTH_ACTIONS_FAILED: - return { - ...state, - authActionsFetching: false, - authActionsInitialized: true, - allowChangePassword: false, - allowResetPassword: false, - }; case BoundariesActionTypes.RESET_AFTER_ERROR: { return { ...defaultState }; } diff --git a/cvat-ui/src/reducers/index.ts b/cvat-ui/src/reducers/index.ts index 8cf2b2884545..7f585b5dc184 100644 --- a/cvat-ui/src/reducers/index.ts +++ b/cvat-ui/src/reducers/index.ts @@ -7,7 +7,7 @@ import { Canvas3d } from 'cvat-canvas3d/src/typescript/canvas3d'; import { Canvas, RectDrawingMethod, CuboidDrawingMethod } from 'cvat-canvas-wrapper'; import { Webhook, MLModel, Organization, Job, Label, User, - QualityReport, QualityConflict, QualitySettings, FramesMetaData, RQStatus, EventLogger, Invitation, + QualityConflict, FramesMetaData, RQStatus, Event, Invitation, SerializedAPISchema, } from 'cvat-core-wrapper'; import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors'; import { KeyMap } from 'utils/mousetrap-react'; @@ -18,11 +18,7 @@ export interface AuthState { initialized: boolean; fetching: boolean; user: User | null; - authActionsFetching: boolean; - authActionsInitialized: boolean; showChangePasswordDialog: boolean; - allowChangePassword: boolean; - allowResetPassword: boolean; hasEmailVerificationBeenSent: boolean; } @@ -345,6 +341,18 @@ export interface AboutState { initialized: boolean; } +export interface ServerAPIState { + schema: SerializedAPISchema | null; + fetching: boolean; + initialized: boolean; + configuration: { + isRegistrationEnabled: boolean; + isBasicLoginEnabled: boolean; + isPasswordResetEnabled: boolean; + isPasswordChangeEnabled: boolean; + }; +} + export interface UserAgreement { name: string; urlDisplayText: string; @@ -434,7 +442,9 @@ export interface NotificationsState { changePassword: null | ErrorState; requestPasswordReset: null | ErrorState; resetPassword: null | ErrorState; - loadAuthActions: null | ErrorState; + }; + serverAPI: { + fetching: null | ErrorState; }; projects: { fetching: null | ErrorState; @@ -710,7 +720,7 @@ export interface AnnotationState { fetching: boolean; delay: number; changeTime: number | null; - changeFrameLog: EventLogger | null; + changeFrameEvent: Event | null; }; ranges: string; navigationBlocked: boolean; @@ -913,26 +923,6 @@ export interface WebhooksState { query: WebhooksQuery; } -export interface QualityQuery { - taskId: number | null; - jobId: number | null; - parentId: number | null; -} - -export interface AnalyticsState { - fetching: boolean; - quality: { - tasksReports: QualityReport[]; - jobsReports: QualityReport[]; - query: QualityQuery; - settings: { - modalVisible: boolean; - current: QualitySettings | null; - fetching: boolean; - } - } -} - export interface InvitationsQuery { page: number; } @@ -966,7 +956,7 @@ export interface CombinedState { organizations: OrganizationState; invitations: InvitationsState; webhooks: WebhooksState; - analytics: AnalyticsState; + serverAPI: ServerAPIState; } export interface Indexable { diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index 81049aca60a5..7d58d95e0a3c 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -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 @@ -24,8 +24,8 @@ import { OrganizationActionsTypes } from 'actions/organization-actions'; import { JobsActionTypes } from 'actions/jobs-actions'; import { WebhooksActionsTypes } from 'actions/webhooks-actions'; import { InvitationsActionTypes } from 'actions/invitations-actions'; +import { ServerAPIActionTypes } from 'actions/server-actions'; -import { AnalyticsActionsTypes } from 'actions/analytics-actions'; import { NotificationsState } from '.'; const defaultState: NotificationsState = { @@ -38,7 +38,9 @@ const defaultState: NotificationsState = { changePassword: null, requestPasswordReset: null, resetPassword: null, - loadAuthActions: null, + }, + serverAPI: { + fetching: null, }, projects: { fetching: null, @@ -381,15 +383,15 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } - case AuthActionTypes.LOAD_AUTH_ACTIONS_FAILED: { + case ServerAPIActionTypes.GET_SERVER_API_SCHEMA_FAILED: { return { ...state, errors: { ...state.errors, - auth: { - ...state.errors.auth, - loadAuthActions: { - message: 'Could not check available auth actions', + serverAPI: { + ...state.errors.serverAPI, + fetching: { + message: 'Could not receive server schema', reason: action.payload.error, shouldLog: !(action.payload.error instanceof ServerError), }, @@ -1850,57 +1852,6 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } - case AnalyticsActionsTypes.GET_QUALITY_REPORTS_FAILED: { - return { - ...state, - errors: { - ...state.errors, - analytics: { - ...state.errors.analytics, - fetching: { - message: 'Could not fetch quality reports', - reason: action.payload.error, - shouldLog: !(action.payload.error instanceof ServerError), - className: 'cvat-notification-notice-get-quality-reports-failed', - }, - }, - }, - }; - } - case AnalyticsActionsTypes.GET_QUALITY_SETTINGS_FAILED: { - return { - ...state, - errors: { - ...state.errors, - analytics: { - ...state.errors.analytics, - fetchingSettings: { - message: 'Could not fetch quality settings', - reason: action.payload.error, - shouldLog: !(action.payload.error instanceof ServerError), - className: 'cvat-notification-notice-get-quality-settings-failed', - }, - }, - }, - }; - } - case AnalyticsActionsTypes.UPDATE_QUALITY_SETTINGS_FAILED: { - return { - ...state, - errors: { - ...state.errors, - analytics: { - ...state.errors.analytics, - updatingSettings: { - message: 'Could not update quality settings', - reason: action.payload.error, - shouldLog: !(action.payload.error instanceof ServerError), - className: 'cvat-notification-notice-update-quality-settings-failed', - }, - }, - }, - }; - } case BoundariesActionTypes.RESET_AFTER_ERROR: case AuthActionTypes.LOGOUT_SUCCESS: { return { ...defaultState }; diff --git a/cvat-ui/src/reducers/root-reducer.ts b/cvat-ui/src/reducers/root-reducer.ts index 323eb016153c..a766325c7894 100644 --- a/cvat-ui/src/reducers/root-reducer.ts +++ b/cvat-ui/src/reducers/root-reducer.ts @@ -23,8 +23,8 @@ import importReducer from './import-reducer'; import cloudStoragesReducer from './cloud-storages-reducer'; import organizationsReducer from './organizations-reducer'; import webhooksReducer from './webhooks-reducer'; -import analyticsReducer from './analytics-reducer'; import invitationsReducer from './invitations-reducer'; +import serverAPIReducer from './server-api-reducer'; export default function createRootReducer(): Reducer { return combineReducers({ @@ -47,7 +47,7 @@ export default function createRootReducer(): Reducer { cloudStorages: cloudStoragesReducer, organizations: organizationsReducer, webhooks: webhooksReducer, - analytics: analyticsReducer, invitations: invitationsReducer, + serverAPI: serverAPIReducer, }); } diff --git a/cvat-ui/src/reducers/server-api-reducer.ts b/cvat-ui/src/reducers/server-api-reducer.ts new file mode 100644 index 000000000000..c57604fdb109 --- /dev/null +++ b/cvat-ui/src/reducers/server-api-reducer.ts @@ -0,0 +1,68 @@ +// Copyright (C) 2024 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import { BoundariesActions, BoundariesActionTypes } from 'actions/boundaries-actions'; +import { ServerAPIActions, ServerAPIActionTypes } from 'actions/server-actions'; +import { ServerAPIState } from '.'; + +const defaultState: ServerAPIState = { + schema: null, + fetching: false, + initialized: false, + configuration: { + isRegistrationEnabled: true, + isBasicLoginEnabled: true, + isPasswordResetEnabled: true, + isPasswordChangeEnabled: true, + }, +}; + +export default function ( + state: ServerAPIState = defaultState, + action: ServerAPIActions | BoundariesActions, +): ServerAPIState { + switch (action.type) { + case ServerAPIActionTypes.GET_SERVER_API_SCHEMA: { + return { + ...state, + fetching: true, + initialized: false, + }; + } + case ServerAPIActionTypes.GET_SERVER_API_SCHEMA_SUCCESS: { + const { schema } = action.payload; + const isRegistrationEnabled = Object.keys(schema.paths).includes('/api/auth/register'); + const isBasicLoginEnabled = Object.keys(schema.paths).includes('/api/auth/login'); + const isPasswordResetEnabled = Object.keys(schema.paths).includes('/api/auth/password/reset'); + const isPasswordChangeEnabled = Object.keys(schema.paths).includes('/api/auth/password/change'); + + return { + ...state, + fetching: false, + initialized: true, + schema, + configuration: { + isRegistrationEnabled, + isBasicLoginEnabled, + isPasswordResetEnabled, + isPasswordChangeEnabled, + }, + }; + } + case ServerAPIActionTypes.GET_SERVER_API_SCHEMA_FAILED: { + return { + ...state, + fetching: false, + initialized: true, + }; + } + case BoundariesActionTypes.RESET_AFTER_ERROR: { + return { + ...defaultState, + }; + } + default: + return state; + } +} diff --git a/cvat-ui/src/utils/controls-logger.ts b/cvat-ui/src/utils/controls-logger.ts index 8eabacb9ca36..5bee1e4d69f1 100644 --- a/cvat-ui/src/utils/controls-logger.ts +++ b/cvat-ui/src/utils/controls-logger.ts @@ -3,7 +3,7 @@ // SPDX-License-Identifier: MIT import { getCore } from 'cvat-core-wrapper'; -import { LogType } from 'cvat-logger'; +import { EventScope } from 'cvat-logger'; import config from 'config'; import { platformInfo } from 'utils/platform-checker'; @@ -17,7 +17,7 @@ class EventRecorder { #savingTimeout: number | null; public constructor() { this.#savingTimeout = null; - core.logger.log(LogType.loadTool, { + core.logger.log(EventScope.loadTool, { location: window.location.pathname + window.location.search, platform: platformInfo(), }); @@ -45,7 +45,7 @@ class EventRecorder { } if (toRecord) { - core.logger.log(LogType.clickElement, logData, false); + core.logger.log(EventScope.clickElement, logData, false); } } diff --git a/cvat/__init__.py b/cvat/__init__.py index 33f7085e67c6..2b392d9b3efc 100644 --- a/cvat/__init__.py +++ b/cvat/__init__.py @@ -4,6 +4,6 @@ from cvat.utils.version import get_version -VERSION = (2, 10, 3, 'final', 0) +VERSION = (2, 11, 0, 'final', 0) __version__ = get_version(VERSION) diff --git a/cvat/apps/analytics_report/signals.py b/cvat/apps/analytics_report/signals.py index 0b7f86a02e0f..5de53675a7c7 100644 --- a/cvat/apps/analytics_report/signals.py +++ b/cvat/apps/analytics_report/signals.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: MIT +from django.db import transaction from django.db.models.signals import post_save from django.dispatch import receiver @@ -19,12 +20,21 @@ ) def __save_job__update_analytics_report(instance, created, **kwargs): if isinstance(instance, Project): - AnalyticsReportUpdateManager().schedule_analytics_report_autoupdate_job(project=instance) + kwargs = {"project": instance} elif isinstance(instance, Task): - AnalyticsReportUpdateManager().schedule_analytics_report_autoupdate_job(task=instance) + kwargs = {"task": instance} elif isinstance(instance, Job): - AnalyticsReportUpdateManager().schedule_analytics_report_autoupdate_job(job=instance) + kwargs = {"job": instance} elif isinstance(instance, Annotation): - AnalyticsReportUpdateManager().schedule_analytics_report_autoupdate_job(job=instance.job) + kwargs = {"job": instance.job} else: assert False + + def schedule_autoupdate_job(): + if any(v.id is None for v in kwargs.values()): + # The object may have been deleted after the on_commit call. + return + + AnalyticsReportUpdateManager().schedule_analytics_report_autoupdate_job(**kwargs) + + transaction.on_commit(schedule_autoupdate_job, robust=True) diff --git a/cvat/apps/dataset_manager/formats/registry.py b/cvat/apps/dataset_manager/formats/registry.py index ddd3582805f6..41a8b980ba96 100644 --- a/cvat/apps/dataset_manager/formats/registry.py +++ b/cvat/apps/dataset_manager/formats/registry.py @@ -95,13 +95,6 @@ def make_exporter(name): return EXPORT_FORMATS[name]() -# Add checking for TF availability to avoid CVAT sever instance / interpreter -# crash and provide a meaningful diagnostic message in the case of AVX -# instructions unavailability: -# https://github.com/openvinotoolkit/cvat/pull/1567 -import datumaro.util.tf_util as TF -TF.enable_tf_check = True - # pylint: disable=unused-import import cvat.apps.dataset_manager.formats.coco import cvat.apps.dataset_manager.formats.cvat @@ -111,7 +104,6 @@ def make_exporter(name): import cvat.apps.dataset_manager.formats.mot import cvat.apps.dataset_manager.formats.mots import cvat.apps.dataset_manager.formats.pascal_voc -import cvat.apps.dataset_manager.formats.tfrecord import cvat.apps.dataset_manager.formats.yolo import cvat.apps.dataset_manager.formats.imagenet import cvat.apps.dataset_manager.formats.camvid diff --git a/cvat/apps/dataset_manager/formats/tfrecord.py b/cvat/apps/dataset_manager/formats/tfrecord.py deleted file mode 100644 index 0bc3fa112165..000000000000 --- a/cvat/apps/dataset_manager/formats/tfrecord.py +++ /dev/null @@ -1,38 +0,0 @@ -# Copyright (C) 2019-2022 Intel Corporation -# Copyright (C) 2023-2024 CVAT.ai Corporation -# -# SPDX-License-Identifier: MIT - -from pyunpack import Archive - -from cvat.apps.dataset_manager.bindings import (GetCVATDataExtractor, - import_dm_annotations) -from cvat.apps.dataset_manager.util import make_zip_archive -from datumaro.components.project import Dataset - -from .registry import dm_env, exporter, importer - -from datumaro.util.tf_util import import_tf -try: - import_tf() - tf_available = True -except ImportError: - tf_available = False - - -@exporter(name='TFRecord', ext='ZIP', version='1.0', enabled=tf_available) -def _export(dst_file, temp_dir, instance_data, save_images=False): - with GetCVATDataExtractor(instance_data, include_images=save_images) as extractor: - dataset = Dataset.from_extractors(extractor, env=dm_env) - dataset.export(temp_dir, 'tf_detection_api', save_images=save_images) - - make_zip_archive(temp_dir, dst_file) - -@importer(name='TFRecord', ext='ZIP', version='1.0', enabled=tf_available) -def _import(src_file, temp_dir, instance_data, load_data_callback=None, **kwargs): - Archive(src_file.name).extractall(temp_dir) - - dataset = Dataset.import_from(temp_dir, 'tf_detection_api', env=dm_env) - if load_data_callback is not None: - load_data_callback(dataset, instance_data) - import_dm_annotations(dataset, instance_data) diff --git a/cvat/apps/dataset_manager/task.py b/cvat/apps/dataset_manager/task.py index cbd21256a61e..9b195c26bd03 100644 --- a/cvat/apps/dataset_manager/task.py +++ b/cvat/apps/dataset_manager/task.py @@ -12,7 +12,6 @@ from django.db import transaction from django.db.models.query import Prefetch -from django.utils import timezone from rest_framework.exceptions import ValidationError from cvat.apps.engine import models, serializers @@ -389,9 +388,8 @@ def _save_tags_to_db(self, tags): self.ir_data.tags = tags def _set_updated_date(self): - db_task = self.db_job.segment.task - db_task.updated_date = timezone.now() - db_task.save() + self.db_job.segment.task.touch() + self.db_job.touch() def _save_to_db(self, data): self.reset() @@ -404,7 +402,6 @@ def _save_to_db(self, data): def _create(self, data): if self._save_to_db(data): self._set_updated_date() - self.db_job.save() def create(self, data): self._create(data) diff --git a/cvat/apps/dataset_manager/tests/assets/annotations.json b/cvat/apps/dataset_manager/tests/assets/annotations.json index 1b1d5f0cf2ae..6035e40fbd30 100644 --- a/cvat/apps/dataset_manager/tests/assets/annotations.json +++ b/cvat/apps/dataset_manager/tests/assets/annotations.json @@ -701,24 +701,6 @@ ], "tracks": [] }, - "TFRecord 1.0": { - "version": 0, - "tags": [], - "shapes": [ - { - "type": "rectangle", - "occluded": false, - "z_order": 0, - "points": [16.5, 17.2, 38.89, 25.63], - "frame": 0, - "label_id": null, - "group": 0, - "source": "manual", - "attributes": [] - } - ], - "tracks": [] - }, "YOLO 1.1": { "version": 0, "tags": [], diff --git a/cvat/apps/dataset_manager/tests/test_formats.py b/cvat/apps/dataset_manager/tests/test_formats.py index c710013d547d..1c7db60814d0 100644 --- a/cvat/apps/dataset_manager/tests/test_formats.py +++ b/cvat/apps/dataset_manager/tests/test_formats.py @@ -295,7 +295,6 @@ def test_export_formats_query(self): 'MOTS PNG 1.0', 'PASCAL VOC 1.1', 'Segmentation mask 1.1', - 'TFRecord 1.0', 'YOLO 1.1', 'ImageNet 1.0', 'CamVid 1.0', @@ -326,7 +325,6 @@ def test_import_formats_query(self): 'MOTS PNG 1.0', 'PASCAL VOC 1.1', 'Segmentation mask 1.1', - 'TFRecord 1.0', 'YOLO 1.1', 'ImageNet 1.0', 'CamVid 1.0', @@ -381,7 +379,6 @@ def test_empty_images_are_exported(self): # ('MOTS PNG 1.0', 'mots_png'), # does not support ('PASCAL VOC 1.1', 'voc'), ('Segmentation mask 1.1', 'voc'), - ('TFRecord 1.0', 'tf_detection_api'), ('YOLO 1.1', 'yolo'), ('ImageNet 1.0', 'imagenet_txt'), ('CamVid 1.0', 'camvid'), diff --git a/cvat/apps/dataset_manager/tests/test_rest_api_formats.py b/cvat/apps/dataset_manager/tests/test_rest_api_formats.py index 0c00ec178935..7c30344d2bab 100644 --- a/cvat/apps/dataset_manager/tests/test_rest_api_formats.py +++ b/cvat/apps/dataset_manager/tests/test_rest_api_formats.py @@ -417,7 +417,7 @@ def test_api_v2_dump_and_upload_annotations_with_objects_type_is_shape(self): "Cityscapes 1.0", "Datumaro 1.0", "ImageNet 1.0", "MOTS PNG 1.0", "PASCAL VOC 1.1", "Segmentation mask 1.1", - "TFRecord 1.0", "VGGFace2 1.0", + "VGGFace2 1.0", "WiderFace 1.0", "YOLO 1.1" ]: self._create_annotations(task, dump_format_name, "default") @@ -522,7 +522,7 @@ def test_api_v2_dump_annotations_with_objects_type_is_track(self): if dump_format_name in [ "Cityscapes 1.0", "ImageNet 1.0", "MOTS PNG 1.0", "PASCAL VOC 1.1", - "Segmentation mask 1.1", "TFRecord 1.0", + "Segmentation mask 1.1", "VGGFace2 1.0", "WiderFace 1.0", "YOLO 1.1" ]: self._create_annotations(task, dump_format_name, "default") @@ -968,7 +968,7 @@ def test_api_v2_rewriting_annotations(self): if dump_format_name in [ "MOT 1.1", "PASCAL VOC 1.1", "Segmentation mask 1.1", - "TFRecord 1.0", "YOLO 1.1", "ImageNet 1.0", + "YOLO 1.1", "ImageNet 1.0", "WiderFace 1.0", "VGGFace2 1.0", "Datumaro 1.0", "Open Images V6 1.0", "KITTI 1.0" ]: @@ -1084,7 +1084,7 @@ def test_api_v2_tasks_annotations_dump_and_upload_with_datumaro(self): if dump_format_name in [ "MOT 1.1", "MOTS PNG 1.0", "PASCAL VOC 1.1", "Segmentation mask 1.1", - "TFRecord 1.0", "YOLO 1.1", "ImageNet 1.0", + "YOLO 1.1", "ImageNet 1.0", "WiderFace 1.0", "VGGFace2 1.0", "LFW 1.0", "Open Images V6 1.0", "Datumaro 1.0", "KITTI 1.0" ]: @@ -1287,7 +1287,7 @@ def test_api_v2_export_import_dataset(self): dump_format_name = dump_format.DISPLAY_NAME if dump_format_name in [ 'Cityscapes 1.0', 'LFW 1.0', 'Market-1501 1.0', - 'MOT 1.1', 'TFRecord 1.0' + 'MOT 1.1', ]: # TO-DO: fix bug for this formats continue @@ -1303,7 +1303,7 @@ def test_api_v2_export_import_dataset(self): if dump_format_name in [ "Cityscapes 1.0", "Datumaro 1.0", "ImageNet 1.0", "MOT 1.1", "MOTS PNG 1.0", "PASCAL VOC 1.1", - "Segmentation mask 1.1", "TFRecord 1.0", "VGGFace2 1.0", + "Segmentation mask 1.1", "VGGFace2 1.0", "WiderFace 1.0", "YOLO 1.1" ]: self._create_annotations(task, dump_format_name, "default") @@ -1344,7 +1344,7 @@ def test_api_v2_export_import_dataset(self): upload_format_name = upload_format.DISPLAY_NAME if upload_format_name in [ 'Cityscapes 1.0', 'LFW 1.0', 'Market-1501 1.0', - 'MOT 1.1', 'TFRecord 1.0' + 'MOT 1.1', ]: # TO-DO: fix bug for this formats continue diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 53d2ed55a554..08147dfc7512 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -24,7 +24,6 @@ from drf_spectacular.utils import extend_schema_field from cvat.apps.engine.utils import parse_specific_attributes -from cvat.apps.organizations.models import Organization from cvat.apps.events.utils import cache_deleted class SafeCharField(models.CharField): @@ -316,18 +315,26 @@ class Image(models.Model): class Meta: default_permissions = () -class Project(models.Model): +class TimestampedModel(models.Model): + created_date = models.DateTimeField(auto_now_add=True) + updated_date = models.DateTimeField(auto_now=True) + + class Meta: + abstract = True + + def touch(self) -> None: + self.save(update_fields=["updated_date"]) + +class Project(TimestampedModel): name = SafeCharField(max_length=256) owner = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name="+") assignee = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name="+") bug_tracker = models.CharField(max_length=2000, blank=True, default="") - created_date = models.DateTimeField(auto_now_add=True) - updated_date = models.DateTimeField(auto_now=True) status = models.CharField(max_length=32, choices=StatusChoice.choices(), default=StatusChoice.ANNOTATION) - organization = models.ForeignKey(Organization, null=True, default=None, + organization = models.ForeignKey('organizations.Organization', null=True, default=None, blank=True, on_delete=models.SET_NULL, related_name="projects") source_storage = models.ForeignKey('Storage', null=True, default=None, blank=True, on_delete=models.SET_NULL, related_name='+') @@ -383,7 +390,7 @@ def with_job_summary(self): ) ) -class Task(models.Model): +class Task(TimestampedModel): objects = TaskQuerySet.as_manager() project = models.ForeignKey(Project, on_delete=models.CASCADE, @@ -396,8 +403,6 @@ class Task(models.Model): assignee = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name="assignees") bug_tracker = models.CharField(max_length=2000, blank=True, default="") - created_date = models.DateTimeField(auto_now_add=True) - updated_date = models.DateTimeField(auto_now=True) overlap = models.PositiveIntegerField(null=True) # Zero means that there are no limits (default) # Note that the files can be split into jobs in a custom way in this case @@ -407,7 +412,7 @@ class Task(models.Model): data = models.ForeignKey(Data, on_delete=models.CASCADE, null=True, related_name="tasks") dimension = models.CharField(max_length=2, choices=DimensionType.choices(), default=DimensionType.DIM_2D) subset = models.CharField(max_length=64, blank=True, default="") - organization = models.ForeignKey(Organization, null=True, default=None, + organization = models.ForeignKey('organizations.Organization', null=True, default=None, blank=True, on_delete=models.SET_NULL, related_name="tasks") source_storage = models.ForeignKey('Storage', null=True, default=None, blank=True, on_delete=models.SET_NULL, related_name='+') @@ -656,15 +661,12 @@ def _validate_constraints(self, obj: Dict[str, Any]): -class Job(models.Model): +class Job(TimestampedModel): objects = JobQuerySet.as_manager() segment = models.ForeignKey(Segment, on_delete=models.CASCADE) assignee = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL) - created_date = models.DateTimeField(auto_now_add=True) - updated_date = models.DateTimeField(auto_now=True) - # TODO: it has to be deleted in Job, Task, Project and replaced by (stage, state) # The stage field cannot be changed by an assignee, but state field can be. For # now status is read only and it will be updated by (stage, state). Thus we don't @@ -962,7 +964,7 @@ class Profile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE) rating = models.FloatField(default=0.0) -class Issue(models.Model): +class Issue(TimestampedModel): frame = models.PositiveIntegerField() position = FloatArrayField() job = models.ForeignKey(Job, related_name='issues', on_delete=models.CASCADE) @@ -970,8 +972,6 @@ class Issue(models.Model): on_delete=models.SET_NULL) assignee = models.ForeignKey(User, null=True, blank=True, related_name='+', on_delete=models.SET_NULL) - created_date = models.DateTimeField(auto_now_add=True) - updated_date = models.DateTimeField(auto_now=True) resolved = models.BooleanField(default=False) def get_project_id(self): @@ -991,12 +991,10 @@ def get_job_id(self): return self.job_id -class Comment(models.Model): +class Comment(TimestampedModel): issue = models.ForeignKey(Issue, related_name='comments', on_delete=models.CASCADE) owner = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL) message = models.TextField(default='') - created_date = models.DateTimeField(auto_now_add=True) - updated_date = models.DateTimeField(auto_now=True) def get_project_id(self): return self.issue.get_project_id() @@ -1072,7 +1070,7 @@ def __str__(self): def list(cls): return [i.value for i in cls] -class CloudStorage(models.Model): +class CloudStorage(TimestampedModel): # restrictions: # AWS bucket name, Azure container name - 63, Google bucket name - 63 without dots and 222 with dots # https://cloud.google.com/storage/docs/naming-buckets#requirements @@ -1090,13 +1088,11 @@ class CloudStorage(models.Model): display_name = models.CharField(max_length=63) owner = models.ForeignKey(User, null=True, blank=True, on_delete=models.SET_NULL, related_name="cloud_storages") - created_date = models.DateTimeField(auto_now_add=True) - updated_date = models.DateTimeField(auto_now=True) credentials = models.CharField(max_length=1024, null=True, blank=True) credentials_type = models.CharField(max_length=29, choices=CredentialsTypeChoice.choices())#auth_type specific_attributes = models.CharField(max_length=1024, blank=True) description = models.TextField(blank=True) - organization = models.ForeignKey(Organization, null=True, default=None, + organization = models.ForeignKey('organizations.Organization', null=True, default=None, blank=True, on_delete=models.SET_NULL, related_name="cloudstorages") class Meta: @@ -1130,12 +1126,10 @@ class Storage(models.Model): class Meta: default_permissions = () -class AnnotationGuide(models.Model): +class AnnotationGuide(TimestampedModel): task = models.OneToOneField(Task, null=True, blank=True, on_delete=models.CASCADE, related_name="annotation_guide") project = models.OneToOneField(Project, null=True, blank=True, on_delete=models.CASCADE, related_name="annotation_guide") markdown = models.TextField(blank=True, default='') - created_date = models.DateTimeField(auto_now_add=True) - updated_date = models.DateTimeField(auto_now=True) is_public = models.BooleanField(default=False) @property diff --git a/cvat/apps/engine/signals.py b/cvat/apps/engine/signals.py index bc69dcf2a7e5..297baec9488f 100644 --- a/cvat/apps/engine/signals.py +++ b/cvat/apps/engine/signals.py @@ -2,9 +2,11 @@ # Copyright (C) 2023 CVAT.ai Corporation # # SPDX-License-Identifier: MIT +import functools import shutil from django.contrib.auth.models import User +from django.db import transaction from django.db.models.signals import post_delete, post_save from django.dispatch import receiver @@ -32,7 +34,7 @@ def __save_job_handler(instance, created, **kwargs): if status != db_task.status: db_task.status = status - db_task.save() + db_task.save(update_fields=["status", "updated_date"]) @receiver(post_save, sender=User, dispatch_uid=__name__ + ".save_user_handler") @@ -45,38 +47,44 @@ def __save_user_handler(instance, **kwargs): @receiver(post_delete, sender=Project, dispatch_uid=__name__ + ".delete_project_handler") def __delete_project_handler(instance, **kwargs): - shutil.rmtree(instance.get_dirname(), ignore_errors=True) + transaction.on_commit( + functools.partial(shutil.rmtree, instance.get_dirname(), ignore_errors=True)) @receiver(post_delete, sender=Asset, dispatch_uid=__name__ + ".__delete_asset_handler") def __delete_asset_handler(instance, **kwargs): - shutil.rmtree(instance.get_asset_dir(), ignore_errors=True) + transaction.on_commit( + functools.partial(shutil.rmtree, instance.get_asset_dir(), ignore_errors=True)) @receiver(post_delete, sender=Task, dispatch_uid=__name__ + ".delete_task_handler") def __delete_task_handler(instance, **kwargs): - shutil.rmtree(instance.get_dirname(), ignore_errors=True) + transaction.on_commit( + functools.partial(shutil.rmtree, instance.get_dirname(), ignore_errors=True)) + if instance.data and not instance.data.tasks.exists(): instance.data.delete() try: - if instance.project: # update project - db_project = instance.project - db_project.save() + if db_project := instance.project: # update project + db_project.touch() except Project.DoesNotExist: pass # probably the project has been deleted @receiver(post_delete, sender=Job, dispatch_uid=__name__ + ".delete_job_handler") def __delete_job_handler(instance, **kwargs): - shutil.rmtree(instance.get_dirname(), ignore_errors=True) + transaction.on_commit( + functools.partial(shutil.rmtree, instance.get_dirname(), ignore_errors=True)) @receiver(post_delete, sender=Data, dispatch_uid=__name__ + ".delete_data_handler") def __delete_data_handler(instance, **kwargs): - shutil.rmtree(instance.get_data_dirname(), ignore_errors=True) + transaction.on_commit( + functools.partial(shutil.rmtree, instance.get_data_dirname(), ignore_errors=True)) @receiver(post_delete, sender=CloudStorage, dispatch_uid=__name__ + ".delete_cloudstorage_handler") def __delete_cloudstorage_handler(instance, **kwargs): - shutil.rmtree(instance.get_storage_dirname(), ignore_errors=True) + transaction.on_commit( + functools.partial(shutil.rmtree, instance.get_storage_dirname(), ignore_errors=True)) diff --git a/cvat/apps/engine/task.py b/cvat/apps/engine/task.py index d1a12b6bef97..7e73e1f52349 100644 --- a/cvat/apps/engine/task.py +++ b/cvat/apps/engine/task.py @@ -50,6 +50,7 @@ def create(db_task, data, request): job_id=f"create:task.id{db_task.pk}", meta=get_rq_job_meta(request=request, db_obj=db_task), depends_on=define_dependent_job(q, user_id), + failure_ttl=settings.IMPORT_CACHE_FAILED_TTL.total_seconds(), ) ############################# Internal implementation for server API diff --git a/cvat/apps/engine/tests/test_rest_api.py b/cvat/apps/engine/tests/test_rest_api.py index b83303071c16..a809450992b0 100644 --- a/cvat/apps/engine/tests/test_rest_api.py +++ b/cvat/apps/engine/tests/test_rest_api.py @@ -933,7 +933,8 @@ def test_api_v2_projects_delete_project_data_after_delete_project(self): task_dir = task.get_dirname() self.assertTrue(os.path.exists(task_dir)) - self._check_api_v2_projects_id(self.admin) + with self.captureOnCommitCallbacks(execute=True): + self._check_api_v2_projects_id(self.admin) for project in self.projects: project_dir = project.get_dirname() @@ -2019,7 +2020,10 @@ def test_api_v2_tasks_delete_task_data_after_delete_task(self): for task in self.tasks: task_dir = task.get_dirname() self.assertTrue(os.path.exists(task_dir)) - self._check_api_v2_tasks_id(self.admin) + + with self.captureOnCommitCallbacks(execute=True): + self._check_api_v2_tasks_id(self.admin) + for task in self.tasks: task_dir = task.get_dirname() self.assertFalse(os.path.exists(task_dir)) @@ -6027,8 +6031,7 @@ def _get_initial_annotation(annotation_format): annotations["shapes"] = rectangle_shapes_wo_attrs annotations["tags"] = tags_wo_attrs - elif annotation_format == "YOLO 1.1" or \ - annotation_format == "TFRecord 1.0": + elif annotation_format == "YOLO 1.1": annotations["shapes"] = rectangle_shapes_wo_attrs elif annotation_format == "COCO 1.0": @@ -6400,8 +6403,6 @@ def etree_to_dict(t): for json in jsons: coco = coco_loader.COCO(json) self.assertTrue(coco.getAnnIds()) - elif format_name == "TFRecord 1.0": - self.assertTrue(zipfile.is_zipfile(content)) elif format_name == "Segmentation mask 1.1": self.assertTrue(zipfile.is_zipfile(content)) diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 5d33c0430cc6..f428e3516119 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -43,6 +43,7 @@ import cvat.apps.dataset_manager as dm import cvat.apps.dataset_manager.views # pylint: disable=unused-import from cvat.apps.engine.cloud_provider import db_storage_to_storage_instance, download_file_from_bucket, export_resource_to_cloud_storage +from cvat.apps.events.handlers import handle_dataset_export, handle_dataset_import from cvat.apps.dataset_manager.bindings import CvatImportError from cvat.apps.dataset_manager.serializers import DatasetFormatsSerializer from cvat.apps.engine.frame_provider import FrameProvider @@ -894,9 +895,9 @@ def perform_update(self, serializer): updated_instance = serializer.instance if instance.project: - instance.project.save() - if updated_instance.project: - updated_instance.project.save() + instance.project.touch() + if updated_instance.project and updated_instance.project != instance.project: + updated_instance.project.touch() @transaction.atomic def perform_create(self, serializer, **kwargs): @@ -905,9 +906,8 @@ def perform_create(self, serializer, **kwargs): organization=self.request.iam_context['organization'] ) - if serializer.instance.project: - db_project = serializer.instance.project - db_project.save() + if db_project := serializer.instance.project: + db_project.touch() assert serializer.instance.organization == db_project.organization # Required for the extra summary information added in the queryset @@ -1944,9 +1944,9 @@ def metadata(self, request, pk): db_data.deleted_frames, )) db_data = serializer.save() - db_job.segment.task.save() + db_job.segment.task.touch() if db_job.segment.task.project: - db_job.segment.task.project.save() + db_job.segment.task.project.touch() if hasattr(db_data, 'video'): media = [db_data.video] @@ -2285,10 +2285,10 @@ def perform_destroy(self, instance: models.Label): code=status.HTTP_400_BAD_REQUEST) if project := instance.project: - project.save(update_fields=['updated_date']) + project.touch() ProjectWriteSerializer(project).update_child_objects_on_labels_update(project) elif task := instance.task: - task.save(update_fields=['updated_date']) + task.touch() TaskWriteSerializer(task).update_child_objects_on_labels_update(task) return super().perform_destroy(instance) @@ -2839,6 +2839,8 @@ def _import_annotations(request, rq_id_template, rq_func, db_obj, format_name, dependent_job = None location = location_conf.get('location') if location_conf else Location.LOCAL + db_storage = None + if not filename or location == Location.CLOUD_STORAGE: if location != Location.CLOUD_STORAGE: serializer = AnnotationFileSerializer(data=request.data) @@ -2899,6 +2901,9 @@ def _import_annotations(request, rq_id_template, rq_func, db_obj, format_name, result_ttl=settings.IMPORT_CACHE_SUCCESS_TTL.total_seconds(), failure_ttl=settings.IMPORT_CACHE_FAILED_TTL.total_seconds() ) + + handle_dataset_import(db_obj, format_name=format_name, cloud_storage=db_storage) + serializer = RqIdSerializer(data={'rq_id': rq_id}) serializer.is_valid(raise_exception=True) @@ -3046,6 +3051,8 @@ def _export_annotations( is_annotation_file=is_annotation_file, ) func_args = (db_storage, filename, filename_pattern, callback) + func_args + else: + db_storage = None with get_rq_lock_by_user(queue, user_id): queue.enqueue_call( @@ -3057,6 +3064,10 @@ def _export_annotations( result_ttl=ttl, failure_ttl=ttl, ) + + handle_dataset_export(db_instance, + format_name=format_name, cloud_storage=db_storage, save_images=not is_annotation_file) + return Response(status=status.HTTP_202_ACCEPTED) def _import_project_dataset(request, rq_id_template, rq_func, db_obj, format_name, filename=None, conv_mask_to_poly=True, location_conf=None): @@ -3081,6 +3092,8 @@ def _import_project_dataset(request, rq_id_template, rq_func, db_obj, format_nam rq_job.delete() dependent_job = None location = location_conf.get('location') if location_conf else None + db_storage = None + if not filename and location != Location.CLOUD_STORAGE: serializer = DatasetFileSerializer(data=request.data) if serializer.is_valid(raise_exception=True): @@ -3139,6 +3152,8 @@ def _import_project_dataset(request, rq_id_template, rq_func, db_obj, format_nam result_ttl=settings.IMPORT_CACHE_SUCCESS_TTL.total_seconds(), failure_ttl=settings.IMPORT_CACHE_FAILED_TTL.total_seconds() ) + + handle_dataset_import(db_obj, format_name=format_name, cloud_storage=db_storage) else: return Response(status=status.HTTP_409_CONFLICT, data='Import job already exists') diff --git a/cvat/apps/events/event.py b/cvat/apps/events/event.py index 9f3f31fbda38..eefb3cdbaa03 100644 --- a/cvat/apps/events/event.py +++ b/cvat/apps/events/event.py @@ -4,6 +4,11 @@ from rest_framework.renderers import JSONRenderer from datetime import datetime, timezone +from typing import Optional + +from django.db import transaction + +from cvat.apps.engine.log import vlogger def event_scope(action, resource): return f"{action}:{resource}" @@ -21,6 +26,7 @@ class EventScopes: "comment": ["create", "update", "delete"], "annotations": ["create", "update", "delete"], "label": ["create", "update", "delete"], + "dataset": ["export", "import"], } @classmethod @@ -31,22 +37,39 @@ def select(cls, resources): for action in cls.RESOURCES.get(resource, []) ] -def create_event(scope, - source, - **kwargs): - payload = kwargs.pop('payload', {}) - timestamp = kwargs.pop('timestamp', str(datetime.now(timezone.utc).timestamp())) +def record_server_event( + *, + scope: str, + request_id: Optional[str], + payload: Optional[dict] = None, + on_commit: bool = False, + **kwargs, +) -> None: + payload = payload or {} + + payload_with_request_id = { + **payload, + "request": { + **payload.get("request", {}), + "id": request_id, + }, + } data = { "scope": scope, - "timestamp": timestamp, - "source": source, + "timestamp": str(datetime.now(timezone.utc).timestamp()), + "source": "server", + "payload": JSONRenderer().render(payload_with_request_id).decode('UTF-8'), **kwargs, } - if payload: - data["payload"] = JSONRenderer().render(payload).decode('UTF-8') - return data + rendered_data = JSONRenderer().render(data).decode('UTF-8') + + if on_commit: + transaction.on_commit(lambda: vlogger.info(rendered_data), robust=True) + else: + vlogger.info(rendered_data) + class EventScopeChoice: @classmethod diff --git a/cvat/apps/events/handlers.py b/cvat/apps/events/handlers.py index 2fdff4794ded..5591817dce3c 100644 --- a/cvat/apps/events/handlers.py +++ b/cvat/apps/events/handlers.py @@ -3,11 +3,10 @@ # SPDX-License-Identifier: MIT from copy import deepcopy -from datetime import datetime, timezone +from typing import Optional, Union import traceback import rq -from rest_framework.renderers import JSONRenderer from rest_framework.views import exception_handler from rest_framework.exceptions import NotAuthenticated from rest_framework import status @@ -36,9 +35,8 @@ from cvat.apps.engine.models import ShapeType from cvat.apps.organizations.models import Membership, Organization, Invitation from cvat.apps.organizations.serializers import OrganizationReadSerializer, MembershipReadSerializer, InvitationReadSerializer -from cvat.apps.engine.log import vlogger -from .event import event_scope, create_event +from .event import event_scope, record_server_event from .cache import get_cache def project_id(instance): @@ -264,16 +262,6 @@ def get_serializer_without_url(instance): serializer.fields.pop("url", None) return serializer -def set_request_id(payload=None, **kwargs): - _payload = payload or {} - return { - **_payload, - "request": { - **_payload.get("request", {}), - "id": request_id(**kwargs), - }, - } - def handle_create(scope, instance, **kwargs): oid = organization_id(instance) oslug = organization_slug(instance) @@ -291,11 +279,12 @@ def handle_create(scope, instance, **kwargs): payload = {} payload = _cleanup_fields(obj=payload) - event = create_event( + record_server_event( scope=scope, + request_id=request_id(), + on_commit=True, obj_id=getattr(instance, 'id', None), obj_name=_get_object_name(instance), - source='server', org_id=oid, org_slug=oslug, project_id=pid, @@ -304,11 +293,8 @@ def handle_create(scope, instance, **kwargs): user_id=uid, user_name=uname, user_email=uemail, - payload=set_request_id(payload), + payload=payload, ) - message = JSONRenderer().render(event).decode('UTF-8') - - vlogger.info(message) def handle_update(scope, instance, old_instance, **kwargs): oid = organization_id(instance) @@ -324,16 +310,15 @@ def handle_update(scope, instance, old_instance, **kwargs): serializer = get_serializer_without_url(instance=instance) diff = get_instance_diff(old_data=old_serializer.data, data=serializer.data) - timestamp = str(datetime.now(timezone.utc).timestamp()) for prop, change in diff.items(): change = _cleanup_fields(change) - event = create_event( + record_server_event( scope=scope, - timestamp=timestamp, + request_id=request_id(), + on_commit=True, obj_name=prop, obj_id=getattr(instance, f'{prop}_id', None), obj_val=str(change["new_value"]), - source='server', org_id=oid, org_slug=oslug, project_id=pid, @@ -342,14 +327,9 @@ def handle_update(scope, instance, old_instance, **kwargs): user_id=uid, user_name=uname, user_email=uemail, - payload=set_request_id({ - "old_value": change["old_value"], - }), + payload={"old_value": change["old_value"]}, ) - message = JSONRenderer().render(event).decode('UTF-8') - vlogger.info(message) - def handle_delete(scope, instance, store_in_deletion_cache=False, **kwargs): deletion_cache = get_cache() if store_in_deletion_cache: @@ -384,11 +364,12 @@ def handle_delete(scope, instance, store_in_deletion_cache=False, **kwargs): uname = user_name(instance) uemail = user_email(instance) - event = create_event( + record_server_event( scope=scope, + request_id=request_id(), + on_commit=True, obj_id=getattr(instance, 'id', None), obj_name=_get_object_name(instance), - source='server', org_id=oid, org_slug=oslug, project_id=pid, @@ -397,11 +378,7 @@ def handle_delete(scope, instance, store_in_deletion_cache=False, **kwargs): user_id=uid, user_name=uname, user_email=uemail, - payload=set_request_id(), ) - message = JSONRenderer().render(event).decode('UTF-8') - - vlogger.info(message) def handle_annotations_change(instance, annotations, action, **kwargs): _annotations = deepcopy(annotations) @@ -429,9 +406,10 @@ def filter_shape_data(shape): tags = [filter_shape_data(tag) for tag in _annotations.get("tags", [])] if tags: - event = create_event( + record_server_event( scope=event_scope(action, "tags"), - source='server', + request_id=request_id(), + on_commit=True, count=len(tags), org_id=oid, org_slug=oslug, @@ -441,12 +419,8 @@ def filter_shape_data(shape): user_id=uid, user_name=uname, user_email=uemail, - payload=set_request_id({ - "tags": tags, - }), + payload={"tags": tags}, ) - message = JSONRenderer().render(event).decode('UTF-8') - vlogger.info(message) shapes_by_type = {shape_type[0]: [] for shape_type in ShapeType.choices()} for shape in _annotations.get("shapes", []): @@ -455,10 +429,11 @@ def filter_shape_data(shape): scope = event_scope(action, "shapes") for shape_type, shapes in shapes_by_type.items(): if shapes: - event = create_event( + record_server_event( scope=scope, + request_id=request_id(), + on_commit=True, obj_name=shape_type, - source='server', count=len(shapes), org_id=oid, org_slug=oslug, @@ -468,12 +443,8 @@ def filter_shape_data(shape): user_id=uid, user_name=uname, user_email=uemail, - payload=set_request_id({ - "shapes": shapes, - }), + payload={"shapes": shapes}, ) - message = JSONRenderer().render(event).decode('UTF-8') - vlogger.info(message) tracks_by_type = {shape_type[0]: [] for shape_type in ShapeType.choices()} for track in _annotations.get("tracks", []): @@ -487,10 +458,11 @@ def filter_shape_data(shape): scope = event_scope(action, "tracks") for track_type, tracks in tracks_by_type.items(): if tracks: - event = create_event( + record_server_event( scope=scope, + request_id=request_id(), + on_commit=True, obj_name=track_type, - source='server', count=len(tracks), org_id=oid, org_slug=oslug, @@ -500,12 +472,53 @@ def filter_shape_data(shape): user_id=uid, user_name=uname, user_email=uemail, - payload=set_request_id({ - "tracks": tracks, - }), + payload={"tracks": tracks}, ) - message = JSONRenderer().render(event).decode('UTF-8') - vlogger.info(message) + +def handle_dataset_io( + instance: Union[Project, Task, Job], + action: str, + *, + format_name: str, + cloud_storage: Optional[CloudStorage], + **payload_fields, +) -> None: + payload={"format": format_name, **payload_fields} + + if cloud_storage: + payload["cloud_storage"] = {"id": cloud_storage.id} + + record_server_event( + scope=event_scope(action, "dataset"), + request_id=request_id(), + org_id=organization_id(instance), + org_slug=organization_slug(instance), + project_id=project_id(instance), + task_id=task_id(instance), + job_id=job_id(instance), + user_id=user_id(instance), + user_name=user_name(instance), + user_email=user_email(instance), + payload=payload, + ) + +def handle_dataset_export( + instance: Union[Project, Task, Job], + *, + format_name: str, + cloud_storage: Optional[CloudStorage], + save_images: bool, +) -> None: + handle_dataset_io(instance, "export", + format_name=format_name, cloud_storage=cloud_storage, save_images=save_images) + +def handle_dataset_import( + instance: Union[Project, Task, Job], + *, + format_name: str, + cloud_storage: Optional[CloudStorage], +) -> None: + handle_dataset_io(instance, "import", format_name=format_name, cloud_storage=cloud_storage) def handle_rq_exception(rq_job, exc_type, exc_value, tb): oid = rq_job.meta.get("org_id", None) @@ -523,9 +536,9 @@ def handle_rq_exception(rq_job, exc_type, exc_value, tb): "stack": ''.join(tb_strings), } - event = create_event( + record_server_event( scope="send:exception", - source='server', + request_id=request_id(instance=rq_job), count=1, org_id=oid, org_slug=oslug, @@ -535,10 +548,8 @@ def handle_rq_exception(rq_job, exc_type, exc_value, tb): user_id=uid, user_name=uname, user_email=uemail, - payload=set_request_id(payload, instance=rq_job), + payload=payload, ) - message = JSONRenderer().render(event).decode('UTF-8') - vlogger.info(message) return False @@ -572,17 +583,14 @@ def handle_viewset_exception(exc, context): "status_code": status_code, } - event = create_event( + record_server_event( scope="send:exception", - source='server', + request_id=request_id(), count=1, user_id=getattr(request.user, "id", None), user_name=getattr(request.user, "username", None), user_email=getattr(request.user, "email", None), - payload=set_request_id(payload), + payload=payload, ) - message = JSONRenderer().render(event).decode('UTF-8') - vlogger.info(message) - return response diff --git a/cvat/apps/events/signals.py b/cvat/apps/events/signals.py index 54d14b6be750..31cdd27f9c51 100644 --- a/cvat/apps/events/signals.py +++ b/cvat/apps/events/signals.py @@ -7,7 +7,6 @@ from django.core.exceptions import ObjectDoesNotExist from cvat.apps.engine.models import ( - Organization, Project, Task, Job, @@ -17,6 +16,7 @@ Comment, Label, ) +from cvat.apps.organizations.models import Organization from .handlers import handle_update, handle_create, handle_delete from .event import EventScopeChoice, event_scope diff --git a/cvat/apps/iam/urls.py b/cvat/apps/iam/urls.py index d8d74c3e7794..8b8135fc2d9a 100644 --- a/cvat/apps/iam/urls.py +++ b/cvat/apps/iam/urls.py @@ -16,8 +16,11 @@ ConfirmEmailViewEx, LoginViewEx ) +BASIC_LOGIN_PATH_NAME = 'rest_login' +BASIC_REGISTER_PATH_NAME = 'rest_register' + urlpatterns = [ - path('login', LoginViewEx.as_view(), name='rest_login'), + path('login', LoginViewEx.as_view(), name=BASIC_LOGIN_PATH_NAME), path('logout', LogoutView.as_view(), name='rest_logout'), path('signing', SigningView.as_view(), name='signing'), path('rules', RulesView.as_view(), name='rules'), @@ -25,7 +28,7 @@ if settings.IAM_TYPE == 'BASIC': urlpatterns += [ - path('register', RegisterViewEx.as_view(), name='rest_register'), + path('register', RegisterViewEx.as_view(), name=BASIC_REGISTER_PATH_NAME), # password path('password/reset', PasswordResetView.as_view(), name='rest_password_reset'), diff --git a/cvat/apps/organizations/models.py b/cvat/apps/organizations/models.py index 49975bd14662..3da77bafbebf 100644 --- a/cvat/apps/organizations/models.py +++ b/cvat/apps/organizations/models.py @@ -15,12 +15,12 @@ from django.core.exceptions import ImproperlyConfigured from django.utils import timezone -class Organization(models.Model): +from cvat.apps.engine.models import TimestampedModel + +class Organization(TimestampedModel): slug = models.SlugField(max_length=16, blank=False, unique=True) name = models.CharField(max_length=64, blank=True) description = models.TextField(blank=True) - created_date = models.DateTimeField(auto_now_add=True) - updated_date = models.DateTimeField(auto_now=True) contact = models.JSONField(blank=True, default=dict) owner = models.ForeignKey(get_user_model(), null=True, diff --git a/cvat/apps/quality_control/signals.py b/cvat/apps/quality_control/signals.py index 609e1abe88f2..7371608c3ce9 100644 --- a/cvat/apps/quality_control/signals.py +++ b/cvat/apps/quality_control/signals.py @@ -2,6 +2,7 @@ # # SPDX-License-Identifier: MIT +from django.db import transaction from django.db.models.signals import post_save from django.dispatch import receiver @@ -37,8 +38,15 @@ def __save_job__update_quality_metrics(instance, created, **kwargs): else: assert False - for task in tasks: - qc.QualityReportUpdateManager().schedule_quality_autoupdate_job(task) + def schedule_autoupdate_jobs(): + for task in tasks: + if task.id is None: + # The task may have been deleted after the on_commit call. + continue + + qc.QualityReportUpdateManager().schedule_quality_autoupdate_job(task) + + transaction.on_commit(schedule_autoupdate_jobs, robust=True) @receiver(post_save, sender=Task, dispatch_uid=__name__ + ".save_task-initialize_quality_settings") diff --git a/cvat/apps/webhooks/models.py b/cvat/apps/webhooks/models.py index 69b0311dcd4d..104faccd60a4 100644 --- a/cvat/apps/webhooks/models.py +++ b/cvat/apps/webhooks/models.py @@ -7,7 +7,7 @@ from django.contrib.auth.models import User from django.db import models -from cvat.apps.engine.models import Project +from cvat.apps.engine.models import Project, TimestampedModel from cvat.apps.organizations.models import Organization @@ -34,7 +34,7 @@ def __str__(self): return self.value -class Webhook(models.Model): +class Webhook(TimestampedModel): target_url = models.URLField(max_length=8192) description = models.CharField(max_length=128, default="", blank=True) @@ -50,9 +50,6 @@ class Webhook(models.Model): is_active = models.BooleanField(default=True) enable_ssl = models.BooleanField(default=True) - created_date = models.DateTimeField(auto_now_add=True) - updated_date = models.DateTimeField(auto_now=True) - owner = models.ForeignKey( User, null=True, blank=True, on_delete=models.SET_NULL, related_name="+" ) @@ -82,7 +79,7 @@ class Meta: ] -class WebhookDelivery(models.Model): +class WebhookDelivery(TimestampedModel): webhook = models.ForeignKey( Webhook, on_delete=models.CASCADE, related_name="deliveries" ) @@ -91,9 +88,6 @@ class WebhookDelivery(models.Model): status_code = models.PositiveIntegerField(null=True, default=None) redelivery = models.BooleanField(default=False) - created_date = models.DateTimeField(auto_now_add=True) - updated_date = models.DateTimeField(auto_now=True) - changed_fields = models.CharField(max_length=4096, default="") request = models.JSONField(default=dict) diff --git a/cvat/apps/webhooks/signals.py b/cvat/apps/webhooks/signals.py index 9e381dd22b89..0d34950cf6ff 100644 --- a/cvat/apps/webhooks/signals.py +++ b/cvat/apps/webhooks/signals.py @@ -12,6 +12,7 @@ import requests from django.conf import settings from django.core.exceptions import ObjectDoesNotExist +from django.db import transaction from django.db.models.signals import (post_delete, post_save, pre_delete, pre_save) from django.dispatch import Signal, receiver @@ -186,7 +187,10 @@ def post_save_resource_event(sender, instance, created, **kwargs): else: return - batch_add_to_queue(filtered_webhooks, data) + transaction.on_commit( + lambda: batch_add_to_queue(filtered_webhooks, data), + robust=True, + ) @receiver(pre_delete, sender=Project, dispatch_uid=__name__ + ":project:pre_delete") @@ -232,9 +236,12 @@ def post_delete_resource_event(sender, instance, **kwargs): "sender": get_sender(instance), } - batch_add_to_queue(filtered_webhooks, data) related_webhooks = [webhook for webhook in getattr(instance, "_related_webhooks", []) if webhook.id not in map(lambda a: a.id, filtered_webhooks)] - batch_add_to_queue(related_webhooks, data) + + transaction.on_commit( + lambda: batch_add_to_queue(filtered_webhooks + related_webhooks, data), + robust=True, + ) @receiver(signal_redelivery) diff --git a/cvat/requirements/base.in b/cvat/requirements/base.in index 5b685b2842a1..e9b37657ed7e 100644 --- a/cvat/requirements/base.in +++ b/cvat/requirements/base.in @@ -51,4 +51,3 @@ rq-scheduler==0.13.1 rq==1.15.1 rules>=3.3 Shapely==1.7.1 -tensorflow==2.11.1 # Optional requirement of Datumaro. Use tensorflow-macos==2.8.0 for Mac M1 diff --git a/cvat/requirements/base.txt b/cvat/requirements/base.txt index e0f6e9e27c23..c8bf63e672a6 100644 --- a/cvat/requirements/base.txt +++ b/cvat/requirements/base.txt @@ -1,4 +1,4 @@ -# SHA1:07743309d7b390659b762ca30db20ebc07ac81bc +# SHA1:55af6f61daa4ceab3e9aa358d3109c7af9660c0a # # This file is autogenerated by pip-compile-multi # To update, run: @@ -6,14 +6,8 @@ # pip-compile-multi # -r ../../utils/dataset_manifest/requirements.txt -absl-py==2.0.0 - # via - # tensorboard - # tensorflow asgiref==3.7.2 # via django -astunparse==1.6.3 - # via tensorflow async-timeout==4.0.3 # via redis attrs==21.4.0 @@ -122,16 +116,12 @@ easyprocess==1.1 # via pyunpack entrypoint2==1.1 # via pyunpack -flatbuffers==23.5.26 - # via tensorflow fonttools==4.43.1 # via matplotlib freezegun==1.2.2 # via rq-scheduler furl==2.1.0 # via -r cvat/requirements/base.in -gast==0.4.0 - # via tensorflow google-api-core==2.12.0 # via # google-cloud-core @@ -139,32 +129,20 @@ google-api-core==2.12.0 google-auth==2.23.3 # via # google-api-core - # google-auth-oauthlib # google-cloud-core # google-cloud-storage - # tensorboard -google-auth-oauthlib==0.4.6 - # via tensorboard google-cloud-core==2.3.3 # via google-cloud-storage google-cloud-storage==1.42.0 # via -r cvat/requirements/base.in google-crc32c==1.5.0 # via google-resumable-media -google-pasta==0.2.0 - # via tensorflow google-resumable-media==2.6.0 # via google-cloud-storage googleapis-common-protos==1.60.0 # via google-api-core -grpcio==1.59.0 - # via - # tensorboard - # tensorflow h5py==3.10.0 - # via - # datumaro - # tensorflow + # via datumaro idna==3.4 # via requests importlib-metadata==6.8.0 @@ -185,24 +163,16 @@ jmespath==0.10.0 # botocore jsonschema==4.17.3 # via drf-spectacular -keras==2.11.0 - # via tensorflow kiwisolver==1.4.5 # via matplotlib -libclang==16.0.6 - # via tensorflow limits==3.6.0 # via python-logstash-async lxml==4.9.3 # via datumaro lz4==4.3.2 # via clickhouse-connect -markdown==3.5 - # via tensorboard markupsafe==2.1.3 - # via - # jinja2 - # werkzeug + # via jinja2 matplotlib==3.8.0 # via # datumaro @@ -215,8 +185,6 @@ nibabel==5.1.0 # via datumaro oauthlib==3.2.2 # via requests-oauthlib -opt-einsum==3.3.0 - # via tensorflow orderedmultidict==1.0.1 # via furl orjson==3.9.8 @@ -227,7 +195,6 @@ packaging==23.2 # matplotlib # nibabel # tensorboardx - # tensorflow pandas==2.1.1 # via datumaro patool==1.12 @@ -238,9 +205,7 @@ protobuf==3.19.6 # via # google-api-core # googleapis-common-protos - # tensorboard # tensorboardx - # tensorflow psutil==5.9.4 # via -r cvat/requirements/base.in psycopg2-binary==2.9.5 @@ -311,11 +276,9 @@ requests==2.31.0 # msrest # python-logstash-async # requests-oauthlib - # tensorboard requests-oauthlib==1.3.1 # via # django-allauth - # google-auth-oauthlib # msrest rjsmin==1.2.1 # via django-compressor @@ -342,39 +305,21 @@ shapely==1.7.1 # via -r cvat/requirements/base.in six==1.16.0 # via - # astunparse # azure-core # furl - # google-pasta # isodate # orderedmultidict # python-dateutil - # tensorflow sqlparse==0.4.4 # via django -tensorboard==2.11.2 - # via tensorflow -tensorboard-data-server==0.6.1 - # via tensorboard -tensorboard-plugin-wit==1.8.1 - # via tensorboard tensorboardx==2.6 # via datumaro -tensorflow==2.11.1 - # via -r cvat/requirements/base.in -tensorflow-estimator==2.11.0 - # via tensorflow -tensorflow-io-gcs-filesystem==0.34.0 - # via tensorflow -termcolor==2.3.0 - # via tensorflow typing-extensions==4.8.0 # via # asgiref # azure-core # datumaro # limits - # tensorflow tzdata==2023.3 # via pandas uritemplate==4.1.1 @@ -386,23 +331,9 @@ urllib3==1.26.18 # botocore # clickhouse-connect # requests -werkzeug==3.0.0 - # via tensorboard -wheel==0.41.2 - # via - # astunparse - # tensorboard wrapt==1.15.0 - # via - # deprecated - # tensorflow + # via deprecated zipp==3.17.0 # via importlib-metadata zstandard==0.21.0 # via clickhouse-connect - -# The following packages are considered to be unsafe in a requirements file: -setuptools==68.2.2 - # via - # tensorboard - # tensorflow diff --git a/cvat/requirements/development.txt b/cvat/requirements/development.txt index 44f7c896a059..995112dbc937 100644 --- a/cvat/requirements/development.txt +++ b/cvat/requirements/development.txt @@ -62,3 +62,5 @@ tornado==6.3.3 # via snakeviz # The following packages are considered to be unsafe in a requirements file: +setuptools==68.2.2 + # via astroid diff --git a/cvat/requirements/production.txt b/cvat/requirements/production.txt index 7844036189b7..a919444f96cf 100644 --- a/cvat/requirements/production.txt +++ b/cvat/requirements/production.txt @@ -28,5 +28,3 @@ watchfiles==0.20.0 # via uvicorn websockets==11.0.3 # via uvicorn - -# The following packages are considered to be unsafe in a requirements file: diff --git a/cvat/schema.yml b/cvat/schema.yml index 651fcb820415..9a40fb547a00 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -1,7 +1,7 @@ openapi: 3.0.3 info: title: CVAT REST API - version: 2.10.3 + version: 2.11.0 description: REST API for Computer Vision Annotation Tool (CVAT) termsOfService: https://www.google.com/policies/terms/ contact: @@ -6521,14 +6521,6 @@ components: type: string maxLength: 1024 default: [] - provider_type: - $ref: '#/components/schemas/ProviderTypeEnum' - resource: - type: string - maxLength: 222 - display_name: - type: string - maxLength: 63 created_date: type: string format: date-time @@ -6537,6 +6529,14 @@ components: type: string format: date-time readOnly: true + provider_type: + $ref: '#/components/schemas/ProviderTypeEnum' + resource: + type: string + maxLength: 222 + display_name: + type: string + maxLength: 63 credentials_type: $ref: '#/components/schemas/CredentialsTypeEnum' specific_attributes: diff --git a/cvat/settings/base.py b/cvat/settings/base.py index 82928bb336a7..4ea0fd38afdb 100644 --- a/cvat/settings/base.py +++ b/cvat/settings/base.py @@ -688,7 +688,7 @@ class CVAT_QUEUES(Enum): BUCKET_CONTENT_MAX_PAGE_SIZE = 500 -IMPORT_CACHE_FAILED_TTL = timedelta(days=90) +IMPORT_CACHE_FAILED_TTL = timedelta(days=30) IMPORT_CACHE_SUCCESS_TTL = timedelta(hours=1) IMPORT_CACHE_CLEAN_DELAY = timedelta(hours=12) diff --git a/docker-compose.yml b/docker-compose.yml index 4ccdf28b975f..3426542ac331 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -70,7 +70,7 @@ services: cvat_server: container_name: cvat_server - image: cvat/server:${CVAT_VERSION:-v2.10.3} + image: cvat/server:${CVAT_VERSION:-v2.11.0} restart: always depends_on: <<: *backend-deps @@ -105,7 +105,7 @@ services: cvat_utils: container_name: cvat_utils - image: cvat/server:${CVAT_VERSION:-v2.10.3} + image: cvat/server:${CVAT_VERSION:-v2.11.0} restart: always depends_on: *backend-deps environment: @@ -122,7 +122,7 @@ services: cvat_worker_import: container_name: cvat_worker_import - image: cvat/server:${CVAT_VERSION:-v2.10.3} + image: cvat/server:${CVAT_VERSION:-v2.11.0} restart: always depends_on: *backend-deps environment: @@ -138,7 +138,7 @@ services: cvat_worker_export: container_name: cvat_worker_export - image: cvat/server:${CVAT_VERSION:-v2.10.3} + image: cvat/server:${CVAT_VERSION:-v2.11.0} restart: always depends_on: *backend-deps environment: @@ -154,7 +154,7 @@ services: cvat_worker_annotation: container_name: cvat_worker_annotation - image: cvat/server:${CVAT_VERSION:-v2.10.3} + image: cvat/server:${CVAT_VERSION:-v2.11.0} restart: always depends_on: *backend-deps environment: @@ -170,7 +170,7 @@ services: cvat_worker_webhooks: container_name: cvat_worker_webhooks - image: cvat/server:${CVAT_VERSION:-v2.10.3} + image: cvat/server:${CVAT_VERSION:-v2.11.0} restart: always depends_on: *backend-deps environment: @@ -186,7 +186,7 @@ services: cvat_worker_quality_reports: container_name: cvat_worker_quality_reports - image: cvat/server:${CVAT_VERSION:-v2.10.3} + image: cvat/server:${CVAT_VERSION:-v2.11.0} restart: always depends_on: *backend-deps environment: @@ -202,7 +202,7 @@ services: cvat_worker_analytics_reports: container_name: cvat_worker_analytics_reports - image: cvat/server:${CVAT_VERSION:-v2.10.3} + image: cvat/server:${CVAT_VERSION:-v2.11.0} restart: always depends_on: *backend-deps environment: @@ -218,7 +218,7 @@ services: cvat_ui: container_name: cvat_ui - image: cvat/ui:${CVAT_VERSION:-v2.10.3} + image: cvat/ui:${CVAT_VERSION:-v2.11.0} restart: always depends_on: - cvat_server diff --git a/helm-chart/values.yaml b/helm-chart/values.yaml index be2949547367..9f949ad81084 100644 --- a/helm-chart/values.yaml +++ b/helm-chart/values.yaml @@ -113,7 +113,7 @@ cvat: additionalVolumeMounts: [] replicas: 1 image: cvat/server - tag: v2.10.3 + tag: v2.11.0 imagePullPolicy: Always permissionFix: enabled: true @@ -137,7 +137,7 @@ cvat: frontend: replicas: 1 image: cvat/ui - tag: v2.10.3 + tag: v2.11.0 imagePullPolicy: Always labels: {} # test: test diff --git a/site/content/en/docs/administration/advanced/analytics.md b/site/content/en/docs/administration/advanced/analytics.md index 2aa4cd0bdf11..e588caf78c84 100644 --- a/site/content/en/docs/administration/advanced/analytics.md +++ b/site/content/en/docs/administration/advanced/analytics.md @@ -125,6 +125,8 @@ Server events: - `create:label`, `update:label`, `delete:label` +- `export:dataset`, `import:dataset` + Client events: - `load:cvat` diff --git a/site/content/en/docs/contributing/development-environment.md b/site/content/en/docs/contributing/development-environment.md index 42bbcda4a41d..917e1566eca0 100644 --- a/site/content/en/docs/contributing/development-environment.md +++ b/site/content/en/docs/contributing/development-environment.md @@ -138,11 +138,6 @@ description: 'Installing a development environment for different operating syste > ``` > sudo ln -s /opt/homebrew/lib/libgeos_c.dylib /usr/local/lib > ``` - > - > On Mac with Apple Silicon (M1) in order to install TensorFlow you will have - > to edit `cvat/requirements/base.txt`. - > Change `tensorflow` to `tensorflow-macos` - > May need to downgrade version Python to 3.9.\* or upgrade version `tensorflow-macos` > Note for Arch Linux users: > diff --git a/site/content/en/docs/contributing/new-annotation-format.md b/site/content/en/docs/contributing/new-annotation-format.md index 95118ce522e0..4ced1e15af1e 100644 --- a/site/content/en/docs/contributing/new-annotation-format.md +++ b/site/content/en/docs/contributing/new-annotation-format.md @@ -150,7 +150,6 @@ task_data.add_shape(shape) - [COCO](/docs/manual/advanced/formats/format-coco/) - [PASCAL VOC and mask](/docs/manual/advanced/formats/format-voc/) - [YOLO](/docs/manual/advanced/formats/format-yolo/) -- [TF detection API](/docs/manual/advanced/formats/format-tfrecord/) - [ImageNet](/docs/manual/advanced/formats/format-imagenet/) - [CamVid](/docs/manual/advanced/formats/format-camvid/) - [WIDER Face](/docs/manual/advanced/formats/format-widerface/) diff --git a/site/content/en/docs/enterprise/shapes-converter.md b/site/content/en/docs/enterprise/shapes-converter.md new file mode 100644 index 000000000000..8e0a62a854f2 --- /dev/null +++ b/site/content/en/docs/enterprise/shapes-converter.md @@ -0,0 +1,81 @@ +--- +title: 'Shapes converter' +linkTitle: 'Shapes converter' +weight: 4 +description: 'How to perform bulk actions on filtered shapes' +--- + +The shapes converter is a feature that enables bulk actions on filtered **shapes**. It allows you to perform mutual +conversion between masks, polygons and rectangles. + +> **Note:** All shapes converter work only when the filter is set up. + +See: + +- [Run actions menu](#run-actions-menu) +- [Convert shapes](#convert-shapes) + +## Run actions menu + +Annotations actions can be accessed from the annotation menu. +To access it, click on the burger icon +and then select **Run actions**. + +> Note: All **Shapes converter** functions work in alignment with set up filter. + +![](/images/run-actions-menu.jpg) + +You will see the following dialog: + +![](/images/shapes-converter-dialog.jpg) + +With the following fields: + + + +| Field | Description | +| ---------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **Select action** | Drop-down list with available actions: **Remove filtered shapes** - removes all shapes in alignment with the set-up filter. Doesn't work with tracks.**Shapes converter: masks to polygons** - converts all masks to polygons.**Shapes converter: masks to rectangles** - converts all masks to rectangles in alignment with the set-up filter.**Shapes converter: polygon to masks** - converts all polygons to masks.**Shapes converter: polygon to rectangles** - converts all polygons to rectangles.**Shapes converter: rectangles to masks** - converts all rectangles to masks.**Shapes converter: rectangles to polygons** - converts all rectangles to polygons.**Note:** only **Remove filtered shapes** is available on the **Free** plan. | +| **Specify frames to run action** | Field where you can specify the frame range for the selected action. Enter the starting frame in the **Starting from frame:** field, and the ending frame in the **up to frame** field. If nothing is selected here or in **Choose one of the predefined options** section, the action will be applied to all fields. | +| **Choose one of the predefined options** | Predefined options to apply to frames. Selection here is mutually exclusive with **Specify frames to run action**. If nothing is selected here or in **Specify frames to run action** section, the action will be applied to all fields. | + + + + + +## Convert shapes + +**Recommended Precautions Before Running Annotation Actions** + +- **Saving changes:** It is recommended to save all changes prior to initiating the annotation action. + If unsaved changes are detected, a prompt will advise to save these changes + to avoid any potential loss of data. + +- **Disabу auto-save:** Prior to running the annotation action, disabling the auto-save feature +is advisable. A notification will suggest this action if auto-save is currently active. + +- **Committing changes:** Changes applied during the annotation session +will not be committed to the server until the saving process is manually +initiated. This can be done either by the user or through the +auto-save feature, should it be enabled. + +To convert shapes, do the following: + +1. Annotate your dataset. + + ![](/images/shapes-converter-annotated-dataset.jpg) + +2. Set up [filters](/docs/manual/advanced/filter/). + + ![](/images/shapes-converter-setup-filter.png) + +3. From the burger menu, select **Run actions**. +4. Choose the action you need from the **Select action** drop-down list. +5. (Optional) In the **Starting from frame** field, enter the frame number where the action should begin, + and in the **up to frame** field, specify the frame number where the action should end. +6. (Optional) Select an option from **Or choose one of the predefined options** to apply the action. +7. Click **Run**. A progress bar will appear. You may abort the process by clicking **Cancel** until the process commits modified objects at the end of pipeline. + + ![](/images/shapes-coverter-action-run.jpg) + +> **Note:** Once the action is applied, it cannot be undone. diff --git a/site/content/en/docs/manual/advanced/annotation-with-polygons/creating-mask.md b/site/content/en/docs/manual/advanced/annotation-with-polygons/creating-mask.md index 0da2b294b7fe..7ec1a13d0586 100644 --- a/site/content/en/docs/manual/advanced/annotation-with-polygons/creating-mask.md +++ b/site/content/en/docs/manual/advanced/annotation-with-polygons/creating-mask.md @@ -33,7 +33,6 @@ There are several formats in CVAT that can be used to export masks: - `MOTS` - `ICDAR` - `COCO` (RLE-encoded instance masks, [guide](/docs/manual/advanced/formats/format-coco)) -- `TFRecord` ([over Datumaro](https://github.com/cvat-ai/datumaro/blob/develop/docs/user_manual.md), [guide](/docs/manual/advanced/formats/format-tfrecord)): - `Datumaro` An example of exported masks (in the `Segmentation Mask` format): diff --git a/site/content/en/docs/manual/advanced/formats/_index.md b/site/content/en/docs/manual/advanced/formats/_index.md index 418aefb514a9..fb5b65e6a82e 100644 --- a/site/content/en/docs/manual/advanced/formats/_index.md +++ b/site/content/en/docs/manual/advanced/formats/_index.md @@ -43,7 +43,6 @@ The table below outlines the available formats for data export in CVAT. | [Open Images 1.0](format-openimages) | .csv | Detection, Classification, Semantic Segmentaion | Faster R-CNN, YOLO, U-Net, CornerNet, and others. | Bounding Boxes, Tags, Polygons | Specific attributes | Not supported | | [PASCAL VOC 1.0](format-voc) | .xml | Classification, Detection | Faster R-CNN, SSD, YOLO, AlexNet, and others. | Bounding Boxes, Tags, Polygons | Specific attributes | Not supported | | [Segmentation Mask 1.0](format-smask) | .txt | Semantic Segmentation | Faster R-CNN, SSD, YOLO, AlexNet, and others. | Polygons | No attributes | Not supported | -| [TFRecord 1.0](format-tfrecord) | .pbtxt | DetectionClassification | SSD, Faster R-CNN, YOLO, GG16, ResNet, Inception, MobileNet, and others. | Bounding Boxes, Polygons | No attributes | Not supported | | [VGGFace2 1.0](format-vggface2) | .csv | Face recognition | VGGFace, ResNet, Inception, and others. | Bounding Boxes, Points | No attributes | Not supported | | [WIDER Face 1.0](format-widerface) | .txt | Detection | SSD (Single Shot MultiBox Detector), Faster R-CNN, YOLO, and others. | Bounding Boxes, Tags | Specific attributes | Not supported | | [YOLO 1.0](format-yolo) | .txt | Detection | YOLOv1, YOLOv2 (YOLO9000), YOLOv3, YOLOv4, and others. | Bounding Boxes | No attributes | Not supported | diff --git a/site/content/en/docs/manual/advanced/formats/format-tfrecord.md b/site/content/en/docs/manual/advanced/formats/format-tfrecord.md deleted file mode 100644 index 8dcc96f92b24..000000000000 --- a/site/content/en/docs/manual/advanced/formats/format-tfrecord.md +++ /dev/null @@ -1,215 +0,0 @@ ---- -title: 'TFRecord' -linkTitle: 'TFRecord' -weight: 8 -description: 'How to export and import data in TFRecord format' ---- - -The TFRecord format is tightly integrated with TensorFlow -and is commonly used for training models within the TensorFlow ecosystem. - -TFRecord is an incredibly flexible data format. -We strive to align our implementation with the -format employed by the [TensorFlow Object Detection API](https://github.com/tensorflow/models/tree/master/research/object_detection), -making only minimal changes as necessary. - -For more information, see: - -- [TFRecord](https://www.tensorflow.org/tutorials/load_data/tfrecord) -- [Dataset examples](https://github.com/cvat-ai/datumaro/tree/v0.3/tests/assets/tf_detection_api_dataset) - -This format does not have a fixed structure, so in -CVAT the following structure is used: - -```python -image_feature_description = { - 'image/filename': tf.io.FixedLenFeature([], tf.string), - 'image/source_id': tf.io.FixedLenFeature([], tf.string), - 'image/height': tf.io.FixedLenFeature([], tf.int64), - 'image/width': tf.io.FixedLenFeature([], tf.int64), - # Object boxes and classes. - 'image/object/bbox/xmin': tf.io.VarLenFeature(tf.float32), - 'image/object/bbox/xmax': tf.io.VarLenFeature(tf.float32), - 'image/object/bbox/ymin': tf.io.VarLenFeature(tf.float32), - 'image/object/bbox/ymax': tf.io.VarLenFeature(tf.float32), - 'image/object/class/label': tf.io.VarLenFeature(tf.int64), - 'image/object/class/text': tf.io.VarLenFeature(tf.string), -} -``` - -## TFRecord export - -For export of images: - -- Supported annotations: Bounding Boxes, Polygons - (as masks, manually over [Datumaro](https://github.com/cvat-ai/datumaro/blob/develop/docs/user_manual.md)) -- Attributes: Not supported. -- Tracks: Not supported. - -The downloaded file is a .zip archive with the following structure: - -```bash -taskname.zip/ -├── default.tfrecord -└── label_map.pbtxt - -# label_map.pbtxt -item { - id: 1 - name: 'label_0' -} -item { - id: 2 - name: 'label_1' -} -... -``` - -How to export masks: - -1. Export annotations in [Datumaro](/docs/manual/advanced/formats/format-datumaro/) format. -1. Apply `polygons_to_masks` and `boxes_to_masks` transforms: - - ```bash - datum transform -t polygons_to_masks -p path/to/proj -o ptm - datum transform -t boxes_to_masks -p ptm -o btm - ``` - -1. Export in the `TF Detection API` format: - - ```bash - datum export -f tf_detection_api -p btm [-- --save-images] - ``` - -## TFRecord import - -Uploaded file: a zip archive of following structure: - -```bash -taskname.zip/ -└── .tfrecord -``` - -- supported annotations: Rectangles - -## How to create a task from TFRecord dataset (from VOC2007 for example) - -1. Create `label_map.pbtxt` file with the following content: - -```js -item { - id: 1 - name: 'aeroplane' -} -item { - id: 2 - name: 'bicycle' -} -item { - id: 3 - name: 'bird' -} -item { - id: 4 - name: 'boat' -} -item { - id: 5 - name: 'bottle' -} -item { - id: 6 - name: 'bus' -} -item { - id: 7 - name: 'car' -} -item { - id: 8 - name: 'cat' -} -item { - id: 9 - name: 'chair' -} -item { - id: 10 - name: 'cow' -} -item { - id: 11 - name: 'diningtable' -} -item { - id: 12 - name: 'dog' -} -item { - id: 13 - name: 'horse' -} -item { - id: 14 - name: 'motorbike' -} -item { - id: 15 - name: 'person' -} -item { - id: 16 - name: 'pottedplant' -} -item { - id: 17 - name: 'sheep' -} -item { - id: 18 - name: 'sofa' -} -item { - id: 19 - name: 'train' -} -item { - id: 20 - name: 'tvmonitor' -} -``` - -1. Use [create_pascal_tf_record.py](https://github.com/tensorflow/models/blob/master/research/object_detection/dataset_tools/create_pascal_tf_record.py) - -to convert VOC2007 dataset to TFRecord format. -As example: - -```bash -python create_pascal_tf_record.py --data_dir --set train --year VOC2007 --output_path pascal.tfrecord --label_map_path label_map.pbtxt -``` - -1. Zip train images - - ```bash - cat /VOC2007/ImageSets/Main/train.txt | while read p; do echo /VOC2007/JPEGImages/${p}.jpg ; done | zip images.zip -j -@ - ``` - -1. Create a CVAT task with the following labels: - - ```bash - aeroplane bicycle bird boat bottle bus car cat chair cow diningtable dog horse motorbike person pottedplant sheep sofa train tvmonitor - ``` - - Select images. zip as data. - See [Creating an annotation task](/docs/manual/basics/creating_an_annotation_task/) - guide for details. - -1. Zip `pascal.tfrecord` and `label_map.pbtxt` files together - - ```bash - zip anno.zip -j - ``` - -1. Click `Upload annotation` button, choose `TFRecord 1.0` and select the zip file - - with labels from the previous step. It may take some time. diff --git a/site/content/en/docs/manual/basics/top-panel.md b/site/content/en/docs/manual/basics/top-panel.md index 838af216dbab..17d822594935 100644 --- a/site/content/en/docs/manual/basics/top-panel.md +++ b/site/content/en/docs/manual/basics/top-panel.md @@ -27,6 +27,9 @@ Button assignment: ![](/images/image229.jpg) +- **Run actions** - opens annotations actions modal (annotations action is a feature + that allow you to modify a bulk of annotations on many frames, + e.g. [Shapes converter](/docs/enterprise/shapes-converter/)). It supports only `shape` objects. - **Open the task** — opens a page with details about the task. - **Change job state** - changes the state of the job (`new`, `in progress`, `rejected`, `completed`). - **Finish the job**/**Renew the job** - changes the job stage and state diff --git a/site/content/en/images/image051.jpg b/site/content/en/images/image051.jpg index 403d0526fc9a..3cebfe17a625 100644 Binary files a/site/content/en/images/image051.jpg and b/site/content/en/images/image051.jpg differ diff --git a/site/content/en/images/run-actions-menu.jpg b/site/content/en/images/run-actions-menu.jpg new file mode 100644 index 000000000000..405ab7310627 Binary files /dev/null and b/site/content/en/images/run-actions-menu.jpg differ diff --git a/site/content/en/images/shapes-converter-annotated-dataset.jpg b/site/content/en/images/shapes-converter-annotated-dataset.jpg new file mode 100644 index 000000000000..d30d521e7423 Binary files /dev/null and b/site/content/en/images/shapes-converter-annotated-dataset.jpg differ diff --git a/site/content/en/images/shapes-converter-dialog.jpg b/site/content/en/images/shapes-converter-dialog.jpg new file mode 100644 index 000000000000..812d7ac25bcc Binary files /dev/null and b/site/content/en/images/shapes-converter-dialog.jpg differ diff --git a/site/content/en/images/shapes-converter-setup-filter.png b/site/content/en/images/shapes-converter-setup-filter.png new file mode 100644 index 000000000000..c602a5cfc4bd Binary files /dev/null and b/site/content/en/images/shapes-converter-setup-filter.png differ diff --git a/site/content/en/images/shapes-coverter-action-run.jpg b/site/content/en/images/shapes-coverter-action-run.jpg new file mode 100644 index 000000000000..08c40ff1a805 Binary files /dev/null and b/site/content/en/images/shapes-coverter-action-run.jpg differ diff --git a/supervisord/server.conf b/supervisord/server.conf index 48992ff64417..449c9a2cc7bc 100644 --- a/supervisord/server.conf +++ b/supervisord/server.conf @@ -32,7 +32,11 @@ process_name=%(program_name)s-%(process_num)d [fcgi-program:uvicorn] socket=unix:///tmp/uvicorn.sock -command=python3 -m uvicorn --fd 0 --forwarded-allow-ips='*' cvat.asgi:application +command=%(ENV_HOME)s/wait_for_deps.sh + python3 -m uvicorn + --fd 0 + --forwarded-allow-ips='*' + cvat.asgi:application autorestart=true environment=CVAT_EVENTS_LOCAL_DB_FILENAME="events_%(process_num)03d.db",CVAT_POSTGRES_APPLICATION_NAME="cvat:server" numprocs=%(ENV_NUMPROCS)s diff --git a/tests/cypress/e2e/actions_objects/case_24_delete_unlock_lock_object.js b/tests/cypress/e2e/actions_objects/case_24_delete_unlock_lock_object.js index 8ce8ab978b74..5c4d3ad88619 100644 --- a/tests/cypress/e2e/actions_objects/case_24_delete_unlock_lock_object.js +++ b/tests/cypress/e2e/actions_objects/case_24_delete_unlock_lock_object.js @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -33,17 +33,10 @@ context('Delete unlock/lock object', () => { cy.get('body').type(shortcut); } - function clickRemoveOnDropdownMenu() { - cy.get('.cvat-object-item-menu').contains(new RegExp('^Remove$', 'g')).click({ force: true }); - } - function deleteObjectViaGUIFromSidebar() { cy.get('.cvat-objects-sidebar-states-list').within(() => { - cy.get('.cvat-objects-sidebar-state-item').within(() => { - cy.get('span[aria-label="more"]').click(); - }); + cy.interactAnnotationObjectMenu('.cvat-objects-sidebar-state-item', 'Remove'); }); - clickRemoveOnDropdownMenu(); } function deleteObjectViaGUIFromObject() { @@ -52,11 +45,8 @@ context('Delete unlock/lock object', () => { cy.get('.cvat_canvas_shape').rightclick(); }); cy.get('.cvat-canvas-context-menu').within(() => { - cy.get('.cvat-objects-sidebar-state-item').within(() => { - cy.get('span[aria-label="more"]').click(); - }); + cy.interactAnnotationObjectMenu('.cvat-objects-sidebar-state-item', 'Remove'); }); - clickRemoveOnDropdownMenu(); } function actionOnConfirmWindow(textBuntton) { diff --git a/tests/cypress/e2e/actions_objects/case_37_object_make_copy.js b/tests/cypress/e2e/actions_objects/case_37_object_make_copy.js index 859a438513bf..3f6e4a4f2c60 100644 --- a/tests/cypress/e2e/actions_objects/case_37_object_make_copy.js +++ b/tests/cypress/e2e/actions_objects/case_37_object_make_copy.js @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -111,12 +111,7 @@ context('Object make a copy.', () => { let coordX = 100; const coordY = 300; for (let id = 1; id < countObject + 1; id++) { - cy.get(`#cvat-objects-sidebar-state-item-${id}`).within(() => { - cy.get('[aria-label="more"]').trigger('mouseover'); - cy.wait(300); // Wait dropdown menu transition - }); - // Get the last element from cvat-object-item-menu array - cy.get('.cvat-object-item-menu').last().should('be.visible').contains('button', 'Make a copy').click(); + cy.interactAnnotationObjectMenu(`#cvat-objects-sidebar-state-item-${id}`, 'Make a copy'); cy.get('.cvat-canvas-container').click(coordX, coordY); cy.get('.cvat-canvas-container').click(); coordX += 100; @@ -147,11 +142,10 @@ context('Object make a copy.', () => { cy.get(`#cvat_canvas_shape_${id}`).trigger('mousemove', 'right'); cy.get(`#cvat_canvas_shape_${id}`).should('have.class', 'cvat_canvas_shape_activated'); cy.get(`#cvat_canvas_shape_${id}`).rightclick({ force: true }); - cy.get('.cvat-canvas-context-menu').last().should('be.visible'); - cy.get('.cvat-canvas-context-menu').last().find('[aria-label="more"]').trigger('mouseover'); - cy.wait(300); // Wait dropdown menu transition; + cy.get('.cvat-canvas-context-menu').should('be.visible'); + cy.get('.cvat-canvas-context-menu').find('[aria-label="more"]').click(); // Get the last element from cvat-object-item-menu array - cy.get('.cvat-object-item-menu').last().should('be.visible').contains('button', 'Make a copy').click(); + cy.get('.cvat-object-item-menu').should('be.visible').contains('button', 'Make a copy').click(); cy.get('.cvat-canvas-container').click(coordX, coordY); cy.get('.cvat-canvas-container').click(); // Deactivate all objects and hide context menu coordX += 100; diff --git a/tests/cypress/e2e/actions_objects/case_53_object_propagate.js b/tests/cypress/e2e/actions_objects/case_53_object_propagate.js index 0392ebaa573c..13ecf89d7bba 100644 --- a/tests/cypress/e2e/actions_objects/case_53_object_propagate.js +++ b/tests/cypress/e2e/actions_objects/case_53_object_propagate.js @@ -1,5 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -19,13 +19,6 @@ context('Object propagate.', () => { secondY: 450, }; - function startPropagation() { - cy.get('#cvat-objects-sidebar-state-item-1').find('[aria-label="more"]').trigger('mouseover'); - cy.get('.cvat-object-item-menu').within(() => { - cy.contains('button', 'Propagate').click(); - }); - } - function setupUpToFrame(value) { cy.get('.cvat-propagate-confirm-up-to-input').find('input').clear(); cy.get('.cvat-propagate-confirm-up-to-input').find('input').type(value); @@ -56,7 +49,7 @@ context('Object propagate.', () => { const FROM_FRAME = 0; const PROPAGATE_FRAMES = 1; - startPropagation(); + cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Propagate'); setupPropagateFrames(PROPAGATE_FRAMES); cy.get('.cvat-propagate-confirm-up-to-input') // Value of "up to the frame" field should be same .find('input') @@ -75,7 +68,7 @@ context('Object propagate.', () => { const FROM_FRAME = 0; const PROPAGATE_FRAMES = 2; - startPropagation(); + cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Propagate'); setupUpToFrame(FROM_FRAME + PROPAGATE_FRAMES); cy.get('.cvat-propagate-confirm-object-on-frames') // Value of "copy of the object on frames" field should be same .find('input') @@ -96,7 +89,7 @@ context('Object propagate.', () => { const UP_TO_FRAME = 1; cy.goCheckFrameNumber(FROM_FRAME); cy.createCuboid(createCuboidShape2Points); - startPropagation(); + cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Propagate'); setupUpToFrame(UP_TO_FRAME); cy.contains('button', 'Yes').click(); diff --git a/tests/cypress/e2e/actions_objects2/case_14_appearance_features.js b/tests/cypress/e2e/actions_objects2/case_14_appearance_features.js index f2a21afb0c3f..c072ac950033 100644 --- a/tests/cypress/e2e/actions_objects2/case_14_appearance_features.js +++ b/tests/cypress/e2e/actions_objects2/case_14_appearance_features.js @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -140,6 +140,7 @@ context('Appearance features', () => { cy.get('.cvat-appearance-outlinded-borders-checkbox').click(); cy.get('.cvat-appearance-outlined-borders-button').click(); cy.changeColorViaBadge(strokeColor); + cy.get('.cvat-label-color-picker').should('be.hidden'); cy.get('.cvat_canvas_shape').each((object) => { cy.get(object).should('have.attr', 'stroke', `#${strokeColor}`); }); diff --git a/tests/cypress/e2e/actions_objects2/case_15_group_features.js b/tests/cypress/e2e/actions_objects2/case_15_group_features.js index 937dd8fddc4d..3cf4a0484e72 100644 --- a/tests/cypress/e2e/actions_objects2/case_15_group_features.js +++ b/tests/cypress/e2e/actions_objects2/case_15_group_features.js @@ -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 @@ -87,16 +87,9 @@ context('Group features', () => { } function changeGroupColor(object, color) { - cy.get(object).within(() => { - cy.get('[aria-label="more"]').click(); - }); - cy.get('.ant-dropdown') - .should('be.visible') - .not('.ant-dropdown-hidden') - .within(() => { - cy.contains('Change group color').click(); - }); + cy.interactAnnotationObjectMenu(object, 'Change group color'); cy.changeColorViaBadge(color); + cy.get('.cvat-label-color-picker').should('not.exist'); } function testShapesFillEquality(equal) { diff --git a/tests/cypress/e2e/actions_tasks/case_102_create_link_shape_frame.js b/tests/cypress/e2e/actions_tasks/case_102_create_link_shape_frame.js index 0886279825c9..b20c39bf1668 100644 --- a/tests/cypress/e2e/actions_tasks/case_102_create_link_shape_frame.js +++ b/tests/cypress/e2e/actions_tasks/case_102_create_link_shape_frame.js @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -29,9 +30,7 @@ context('Create a link for shape, frame.', () => { cy.window().then((win) => { cy.stub(win, 'prompt').returns(win.prompt).as('copyToClipboardPromptShape'); }); - cy.get('#cvat-objects-sidebar-state-item-1').find('[aria-label="more"]').trigger('mouseover'); - cy.get('#cvat_canvas_shape_1').should('have.class', 'cvat_canvas_shape_activated'); - cy.get('.cvat-object-item-menu').last().should('be.visible').contains('button', 'Create object URL').click(); + cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Create object URL'); cy.get('@copyToClipboardPromptShape').should('be.called'); cy.get('@copyToClipboardPromptShape').then((prompt) => { const url = prompt.args[0][1]; diff --git a/tests/cypress/e2e/actions_tasks/case_52_dump_upload_annotation.js b/tests/cypress/e2e/actions_tasks/case_52_dump_upload_annotation.js index 488d6545c034..8371ea9534d3 100644 --- a/tests/cypress/e2e/actions_tasks/case_52_dump_upload_annotation.js +++ b/tests/cypress/e2e/actions_tasks/case_52_dump_upload_annotation.js @@ -1,5 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -43,7 +43,7 @@ context('Dump/Upload annotation.', { browser: '!firefox' }, () => { cy.contains('.cvat-item-task-name', toTaskName) .parents('.cvat-tasks-list-item') .find('.cvat-menu-icon') - .trigger('mouseover'); + .click(); cy.contains('Upload annotations').click(); cy.get('.cvat-modal-import-dataset').find('.cvat-modal-import-select').click(); cy.contains('.cvat-modal-import-dataset-option-item', exportFormat.split(' ')[0]).click(); diff --git a/tests/cypress/e2e/actions_tasks2/case_101_opencv_basic_actions.js b/tests/cypress/e2e/actions_tasks2/case_101_opencv_basic_actions.js index d405d937c923..711dc78f96f1 100644 --- a/tests/cypress/e2e/actions_tasks2/case_101_opencv_basic_actions.js +++ b/tests/cypress/e2e/actions_tasks2/case_101_opencv_basic_actions.js @@ -1,5 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -84,11 +84,14 @@ context('OpenCV. Intelligent scissors. Histogram Equalization. TrackerMIL.', () cy.get('.cvat-opencv-control-popover').within(() => { cy.contains('OpenCV is loading').should('not.exist'); }); - // Intelligent cissors button be visible - cy.get('.cvat-opencv-drawing-tool').should('exist').and('be.visible'); + cy.get('body').click(); }); it('Create a shape with "Intelligent cissors". Create the second shape with the label change and "Done" button.', () => { + cy.interactOpenCVControlButton(); + cy.get('.cvat-opencv-drawing-tool').should('exist').and('be.visible'); + cy.get('body').click(); + cy.opencvCreateShape(createOpencvShape); cy.opencvCreateShape(createOpencvShapeSecondLabel); }); diff --git a/tests/cypress/e2e/actions_tasks2/case_31_label_constructor_color_name_label.js b/tests/cypress/e2e/actions_tasks2/case_31_label_constructor_color_name_label.js index 4c316760b54a..74cd00c3eb39 100644 --- a/tests/cypress/e2e/actions_tasks2/case_31_label_constructor_color_name_label.js +++ b/tests/cypress/e2e/actions_tasks2/case_31_label_constructor_color_name_label.js @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -110,6 +110,7 @@ context('Label constructor. Color label. Label name editing', () => { }); cy.get('.cvat-change-task-label-color-button').click(); cy.changeColorViaBadge(labelColor.yellowHex); + cy.get('.cvat-label-color-picker').should('be.hidden'); cy.get('[placeholder="Label name"]').clear(); cy.get('[placeholder="Label name"]').type(colorYellow); // Check PR 2806 cy.contains('button', 'Done').click(); @@ -166,6 +167,7 @@ context('Label constructor. Color label. Label name editing', () => { // Change the label color cy.get('.cvat-change-task-label-color-button').click(); cy.changeColorViaBadge(labelColor.yellowHex); + cy.get('.cvat-label-color-picker').should('be.hidden'); // Reset the label color cy.get('.cvat-change-task-label-color-button').click(); diff --git a/tests/cypress/e2e/actions_tasks2/case_97_export_import_task.js b/tests/cypress/e2e/actions_tasks2/case_97_export_import_task.js index 75e963432a28..6a934e79c78b 100644 --- a/tests/cypress/e2e/actions_tasks2/case_97_export_import_task.js +++ b/tests/cypress/e2e/actions_tasks2/case_97_export_import_task.js @@ -1,5 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -80,7 +80,7 @@ context('Export, import an annotation task.', { browser: '!firefox' }, () => { cy.contains('.cvat-item-task-name', taskName) .parents('.cvat-tasks-list-item') .find('.cvat-item-open-task-actions > .cvat-menu-icon') - .trigger('mouseover'); + .click(); cy.get('.ant-dropdown') .not('.ant-dropdown-hidden') .within(() => { diff --git a/tests/cypress/e2e/actions_tasks3/case_109_dummy_cloud_storage.js b/tests/cypress/e2e/actions_tasks3/case_109_dummy_cloud_storage.js index cf022a1e4a05..f76b5b31362a 100644 --- a/tests/cypress/e2e/actions_tasks3/case_109_dummy_cloud_storage.js +++ b/tests/cypress/e2e/actions_tasks3/case_109_dummy_cloud_storage.js @@ -1,5 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -74,7 +74,7 @@ context('Dummy cloud storages.', { browser: '!firefox' }, () => { function testGoToCSUpdatePage() { cy.get('.cvat-cloud-storage-item-menu-button').trigger('mousemove'); - cy.get('.cvat-cloud-storage-item-menu-button').trigger('mouseover'); + cy.get('.cvat-cloud-storage-item-menu-button').click(); cy.get('.ant-dropdown') .not('.ant-dropdown-hidden') .within(() => { diff --git a/tests/cypress/e2e/actions_tasks3/case_18_filters_functionality.js b/tests/cypress/e2e/actions_tasks3/case_18_filters_functionality.js index ab0810227a6e..6eb67f6036bd 100644 --- a/tests/cypress/e2e/actions_tasks3/case_18_filters_functionality.js +++ b/tests/cypress/e2e/actions_tasks3/case_18_filters_functionality.js @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -308,21 +308,22 @@ context('Filters functionality.', () => { it('Verify to show all filters', () => { cy.checkFiltersModalOpened(); - cy.get('.recently-used-wrapper').trigger('mouseover'); - cy.get('.ant-dropdown') - .not('.ant-dropdown-hidden') - .within(() => { - cvatFiltesList.forEach((filterValue) => cy.contains('[role="menuitem"]', filterValue)); + cy.get('.cvat-recently-used-filters-wrapper').click(); + cy.get('.cvat-recently-used-filters-dropdown').should('exist').and('be.visible').within(() => { + cvatFiltesList.forEach((filterValue) => { + cy.get('li').contains('[role="menuitem"]', filterValue).should('have.length', 1); }); + }); + cy.get('.cvat-recently-used-filters-wrapper').click(); }); it('Select filter: type == "shape"', () => { - cy.selectFilterValue('type == "shape"'); // #cvat_canvas_shape_1,3, #cvat-objects-sidebar-state-item-1,3 + cy.selectFilterValue(new RegExp('^type == "shape"$')); checkingFilterApplication([1, 3]); }); it('Select filter: objectID == 4', () => { - cy.selectFilterValue('objectID == 4'); // #cvat_canvas_shape_4, #cvat-objects-sidebar-state-item-4 + cy.selectFilterValue(new RegExp('^objectID == 4$')); checkingFilterApplication([4]); }); }); diff --git a/tests/cypress/e2e/actions_tasks3/case_19_all_image_rotate_features.js b/tests/cypress/e2e/actions_tasks3/case_19_all_image_rotate_features.js index a02e5cf8aa80..6d72b33fe29f 100644 --- a/tests/cypress/e2e/actions_tasks3/case_19_all_image_rotate_features.js +++ b/tests/cypress/e2e/actions_tasks3/case_19_all_image_rotate_features.js @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -15,14 +15,15 @@ context('Rotate all images feature.', () => { } function imageRotate(direction = 'anticlockwise', deg) { - cy.get('.cvat-rotate-canvas-control').trigger('mouseover'); - cy.get('.cvat-rotate-canvas-control').should('be.visible'); + cy.get('.cvat-rotate-canvas-control').click(); + cy.get('.cvat-rotate-canvas-popover').should('be.visible'); if (direction === 'clockwise') { cy.get('.cvat-rotate-canvas-controls-right').should('be.visible').click(); } else { cy.get('.cvat-rotate-canvas-controls-left').should('be.visible').click(); } checkDegRotate(deg); + cy.get('body').click(); } function checkFrameNum(frameNum) { diff --git a/tests/cypress/e2e/actions_users/issue_1810_login_logout.js b/tests/cypress/e2e/actions_users/issue_1810_login_logout.js index 072946f1b844..63bba2c28e1d 100644 --- a/tests/cypress/e2e/actions_users/issue_1810_login_logout.js +++ b/tests/cypress/e2e/actions_users/issue_1810_login_logout.js @@ -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 @@ -49,7 +49,7 @@ context('When clicking on the Logout button, get the user session closed.', () = cy.get('.cvat-right-header').within(() => { cy.get('.cvat-header-menu-user-dropdown') .should('have.text', Cypress.env('user')) - .trigger('mouseover', { which: 1 }); + .click(); }); cy.get('span[aria-label="logout"]').click(); cy.url().should('include', `/auth/login?next=/tasks/${taskId}`); diff --git a/tests/cypress/e2e/actions_users/registration_involved/case_2_register_user_change_pass.js b/tests/cypress/e2e/actions_users/registration_involved/case_2_register_user_change_pass.js index a2f070beabfe..ae413a9fe9bb 100644 --- a/tests/cypress/e2e/actions_users/registration_involved/case_2_register_user_change_pass.js +++ b/tests/cypress/e2e/actions_users/registration_involved/case_2_register_user_change_pass.js @@ -1,4 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation +// Copyright (C) 2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -19,7 +20,7 @@ context('Register user, change password, login with new password', () => { cy.get('.cvat-right-header') .find('.cvat-header-menu-user-dropdown') .should('have.text', myUserName) - .trigger('mouseover'); + .click(); cy.get('.cvat-header-menu-change-password').click(); cy.get('.cvat-modal-change-password').within(() => { cy.get('#oldPassword').type(myPassword); diff --git a/tests/cypress/e2e/canvas3d_functionality/case_83_canvas3d_functionality_cuboid_grouping.js b/tests/cypress/e2e/canvas3d_functionality/case_83_canvas3d_functionality_cuboid_grouping.js index d73a31f9fcd4..49e0597a2cbd 100644 --- a/tests/cypress/e2e/canvas3d_functionality/case_83_canvas3d_functionality_cuboid_grouping.js +++ b/tests/cypress/e2e/canvas3d_functionality/case_83_canvas3d_functionality_cuboid_grouping.js @@ -1,5 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -42,16 +42,9 @@ context('Canvas 3D functionality. Grouping.', () => { let bgColorItem; function changeGroupColor(object, color) { - cy.get(object).within(() => { - cy.get('[aria-label="more"]').click(); - }); - cy.wait(300); - cy.get('.ant-dropdown') - .not('.ant-dropdown-hidden') - .within(() => { - cy.contains('Change group color').click(); - }); + cy.interactAnnotationObjectMenu(object, 'Change group color'); cy.changeColorViaBadge(color); + cy.get('.cvat-label-color-picker').should('not.exist'); } before(() => { @@ -95,7 +88,6 @@ context('Canvas 3D functionality. Grouping.', () => { it('Change group color.', () => { changeGroupColor('#cvat-objects-sidebar-state-item-2', yellowHex); - cy.get('.cvat-label-color-picker').should('be.hidden'); for (const groupedSidebarItemShape of shapeSidebarItemArray) { cy.get(groupedSidebarItemShape) .should('have.attr', 'style') diff --git a/tests/cypress/e2e/canvas3d_functionality/case_91_canvas3d_functionality_dump_upload_annotation_point_cloud_format.js b/tests/cypress/e2e/canvas3d_functionality/case_91_canvas3d_functionality_dump_upload_annotation_point_cloud_format.js index cb59e16d6898..3ec73154a1a5 100644 --- a/tests/cypress/e2e/canvas3d_functionality/case_91_canvas3d_functionality_dump_upload_annotation_point_cloud_format.js +++ b/tests/cypress/e2e/canvas3d_functionality/case_91_canvas3d_functionality_dump_upload_annotation_point_cloud_format.js @@ -1,5 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -102,7 +102,7 @@ context('Canvas 3D functionality. Dump/upload annotation. "Point Cloud" format', cy.contains('.cvat-item-task-name', taskName) .parents('.cvat-tasks-list-item') .find('.cvat-menu-icon') - .trigger('mouseover'); + .click(); cy.contains('Upload annotations').click(); uploadAnnotation( dumpTypePC.split(' ')[0], diff --git a/tests/cypress/e2e/canvas3d_functionality/case_92_canvas3d_functionality_dump_upload_annotation_velodyne_points_format.js b/tests/cypress/e2e/canvas3d_functionality/case_92_canvas3d_functionality_dump_upload_annotation_velodyne_points_format.js index a906ebdecddb..6383a33caf27 100644 --- a/tests/cypress/e2e/canvas3d_functionality/case_92_canvas3d_functionality_dump_upload_annotation_velodyne_points_format.js +++ b/tests/cypress/e2e/canvas3d_functionality/case_92_canvas3d_functionality_dump_upload_annotation_velodyne_points_format.js @@ -1,5 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2022-2023 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -102,7 +102,7 @@ context('Canvas 3D functionality. Dump/upload annotation. "Velodyne Points" form cy.contains('.cvat-item-task-name', taskName) .parents('.cvat-tasks-list-item') .find('.cvat-menu-icon') - .trigger('mouseover'); + .click(); cy.contains('Upload annotations').click(); uploadAnnotation( dumpTypeVC.split(' ')[0], diff --git a/tests/cypress/e2e/canvas3d_functionality_2/case_80_canvas3d_functionality_cuboid_make_copy.js b/tests/cypress/e2e/canvas3d_functionality_2/case_80_canvas3d_functionality_cuboid_make_copy.js index 13f292328548..5b1cf79be1c2 100644 --- a/tests/cypress/e2e/canvas3d_functionality_2/case_80_canvas3d_functionality_cuboid_make_copy.js +++ b/tests/cypress/e2e/canvas3d_functionality_2/case_80_canvas3d_functionality_cuboid_make_copy.js @@ -1,5 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -33,8 +33,7 @@ context('Canvas 3D functionality. Make a copy.', () => { cy.get('#cvat-objects-sidebar-state-item-1') .find('.cvat-objects-sidebar-state-item-label-selector') .trigger('mouseout'); - cy.get('#cvat-objects-sidebar-state-item-1').find('[aria-label="more"]').click(); - cy.get('.ant-dropdown-menu').not('.ant-dropdown-menu-hidden').find('[aria-label="copy"]').click(); + cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Make a copy'); cy.get('.cvat-canvas3d-perspective').trigger('mousemove', 480, 270); cy.get('.cvat-canvas3d-perspective').dblclick(480, 270); cy.get('#cvat-objects-sidebar-state-item-1') diff --git a/tests/cypress/e2e/canvas3d_functionality_2/case_81_canvas3d_functionality_cuboid_propagate.js b/tests/cypress/e2e/canvas3d_functionality_2/case_81_canvas3d_functionality_cuboid_propagate.js index ed988a43d1a2..cb26d419b11c 100644 --- a/tests/cypress/e2e/canvas3d_functionality_2/case_81_canvas3d_functionality_cuboid_propagate.js +++ b/tests/cypress/e2e/canvas3d_functionality_2/case_81_canvas3d_functionality_cuboid_propagate.js @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -24,8 +25,7 @@ context('Canvas 3D functionality. Cuboid propagate.', () => { describe(`Testing case "${caseId}"`, () => { it('Cuboid propagate.', () => { - cy.get('#cvat-objects-sidebar-state-item-1').find('[aria-label="more"]').click(); - cy.get('.ant-dropdown-menu').not('.ant-dropdown-menu-hidden').find('[aria-label="block"]').click(); + cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Propagate'); cy.get('.cvat-propagate-confirm-object-on-frames').should('exist'); cy.contains('button', 'Yes').click(); }); diff --git a/tests/cypress/e2e/features/analytics_pipeline.js b/tests/cypress/e2e/features/analytics_pipeline.js index 0b55e2628769..9a354571a9fa 100644 --- a/tests/cypress/e2e/features/analytics_pipeline.js +++ b/tests/cypress/e2e/features/analytics_pipeline.js @@ -1,4 +1,4 @@ -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -147,7 +147,7 @@ context('Analytics pipeline', () => { cy.get('.cvat-job-item').contains('a', `Job #${jobID}`) .parents('.cvat-job-item') .find('.cvat-job-item-more-button') - .trigger('mouseover'); + .click(); cy.get('.ant-dropdown') .not('.ant-dropdown-hidden') .within(() => { @@ -230,12 +230,15 @@ context('Analytics pipeline', () => { cy.get('.cvat-job-item').contains('a', `Job #${jobID}`) .parents('.cvat-job-item') .find('.cvat-job-item-more-button') - .trigger('mouseover'); - cy.get('.ant-dropdown') - .not('.ant-dropdown-hidden') - .within(() => { - cy.contains('[role="menuitem"]', 'View analytics').click(); - }); + .click(); + + cy.wait(500); // wait for animationend + cy.get('.cvat-job-item-menu') + .should('exist') + .and('be.visible') + .find('[role="menuitem"]') + .filter(':contains("View analytics")') + .click(); cy.wait('@getReport'); checkCards(true); checkHistograms(); diff --git a/tests/cypress/e2e/features/ground_truth_jobs.js b/tests/cypress/e2e/features/ground_truth_jobs.js index 933eed68541b..1045d612777b 100644 --- a/tests/cypress/e2e/features/ground_truth_jobs.js +++ b/tests/cypress/e2e/features/ground_truth_jobs.js @@ -10,17 +10,6 @@ context('Ground truth jobs', () => { const taskName = `Annotation task for Case ${caseId}`; const attrName = `Attr for Case ${caseId}`; const textDefaultValue = 'Some default value for type Text'; - const imagesCount = 10; - const imageFileName = 'ground_truth_1'; - const width = 800; - const height = 800; - const posX = 10; - const posY = 10; - const color = 'gray'; - const archiveName = `${imageFileName}.zip`; - const archivePath = `cypress/fixtures/${archiveName}`; - const imagesFolder = `cypress/fixtures/${imageFileName}`; - const directoryToArchive = imagesFolder; const jobOptions = { jobType: 'Ground truth', @@ -131,7 +120,7 @@ context('Ground truth jobs', () => { cy.clickInTaskMenu('View analytics', true); cy.get('.cvat-task-analytics-tabs') .within(() => { - cy.contains('span', 'Quality').click(); + cy.contains('Quality').click(); }); } @@ -175,42 +164,77 @@ context('Ground truth jobs', () => { cy.get('.cvat-conflict-label.cvat-conflict-darken').should('have.length', darkenConflicts); } - function waitForReport(authKey, rqID) { - cy.request({ + function waitForReport(cvat, rqID) { + return new Promise((resolve) => { + function request() { + cvat.server.request(`/api/quality/reports?rq_id=${rqID}`, { + method: 'POST', + }).then((response) => { + if (response.status === 201) { + qualityReportID = response.data.id; + resolve(qualityReportID); + } else { + setTimeout(request, 500); + } + }); + } + + setTimeout(request, 500); + }); + } + + function createTaskQualityReport(taskId) { + cy.window().then((window) => window.cvat.server.request('/api/quality/reports', { method: 'POST', - url: `/api/quality/reports?rq_id=${rqID}`, - headers: { - Authorization: `Token ${authKey}`, - }, - body: { - task_id: taskID, + data: { + task_id: taskId, }, }).then((response) => { - if (response.status === 201) { - qualityReportID = response.body.id; - return; - } - waitForReport(authKey, rqID); + const rqID = response.data.rq_id; + return waitForReport(window.cvat, rqID); + })).then(() => { + cy.visit('/tasks'); + cy.get('.cvat-spinner').should('not.exist'); + cy.intercept('GET', '/api/quality/reports**').as('getReport'); + + cy.openTask(taskName); + openQualityTab(); + cy.wait('@getReport'); }); } before(() => { cy.visit('auth/login'); cy.login(); - cy.visit('/tasks'); - cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount); - cy.createZipArchive(directoryToArchive, archivePath); - cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, archiveName); - cy.openTask(taskName); - cy.url().then((url) => { - taskID = Number(url.split('/').slice(-1)[0].split('?')[0]); - }); - cy.get('.cvat-job-item').first().invoke('attr', 'data-row-id').then((val) => { - jobID = val; - }); }); - describe(`Testing case "${caseId}"`, () => { + describe('Testing ground truth basics', () => { + const imagesCount = 10; + const imageFileName = 'ground_truth_1'; + const width = 800; + const height = 800; + const posX = 10; + const posY = 10; + const color = 'gray'; + const archiveName = `${imageFileName}.zip`; + const archivePath = `cypress/fixtures/${archiveName}`; + const imagesFolder = `cypress/fixtures/${imageFileName}`; + const directoryToArchive = imagesFolder; + + before(() => { + cy.visit('/tasks'); + cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount); + cy.createZipArchive(directoryToArchive, archivePath); + cy.createAnnotationTask(taskName, labelName, attrName, textDefaultValue, archiveName); + cy.openTask(taskName); + cy.url().then((url) => { + taskID = Number(url.split('/').slice(-1)[0].split('?')[0]); + }); + cy.get('.cvat-job-item').first().invoke('attr', 'data-row-id').then((val) => { + jobID = val; + }); + }); + it('Create ground truth job from task page', () => { cy.createJob({ ...jobOptions, @@ -233,9 +257,6 @@ context('Ground truth jobs', () => { it('Check quality page, create ground truth job from quality page', () => { openQualityTab(); - checkCardValue('.cvat-task-mean-annotation-quality', 'N/A'); - checkCardValue('.cvat-task-gt-conflicts', 'N/A'); - checkCardValue('.cvat-task-issues', '0'); cy.get('.cvat-job-empty-ground-truth-item') .should('be.visible') @@ -258,6 +279,9 @@ context('Ground truth jobs', () => { .parents('.cvat-job-item') .find('.ant-tag') .should('have.text', 'Ground truth'); + checkCardValue('.cvat-task-mean-annotation-quality', 'N/A'); + checkCardValue('.cvat-task-gt-conflicts', 'N/A'); + checkCardValue('.cvat-task-issues', '0'); }); }); @@ -303,31 +327,7 @@ context('Ground truth jobs', () => { }); cy.saveJob(); - cy.logout(); - cy.getAuthKey().then((res) => { - const authKey = res.body.key; - cy.request({ - method: 'POST', - url: '/api/quality/reports', - headers: { - Authorization: `Token ${authKey}`, - }, - body: { - task_id: taskID, - }, - }).then((response) => { - const rqID = response.body.rq_id; - waitForReport(authKey, rqID); - }); - }); - cy.login(); - cy.visit('/tasks'); - cy.get('.cvat-spinner').should('not.exist'); - cy.intercept('GET', '/api/quality/reports**').as('getReport'); - - cy.openTask(taskName); - openQualityTab(); - cy.wait('@getReport'); + createTaskQualityReport(taskID); checkCardValue('.cvat-task-mean-annotation-quality', '33.3%'); checkCardValue('.cvat-task-gt-conflicts', '5'); checkCardValue('.cvat-task-issues', '0'); @@ -397,4 +397,103 @@ context('Ground truth jobs', () => { cy.checkFrameNum(groundTruthFrames[3]); }); }); + + describe('Testing case ground truth job list', () => { + const imagesCount = 20; + const imageFileName = 'ground_truth_2'; + const width = 100; + const height = 100; + const posX = 10; + const posY = 10; + const color = 'gray'; + const archiveName = `${imageFileName}.zip`; + const archivePath = `cypress/fixtures/${archiveName}`; + const imagesFolder = `cypress/fixtures/${imageFileName}`; + const directoryToArchive = imagesFolder; + let labels = []; + + before(() => { + cy.visit('/tasks'); + cy.imageGenerator(imagesFolder, imageFileName, width, height, color, posX, posY, labelName, imagesCount); + cy.createZipArchive(directoryToArchive, archivePath); + cy.createAnnotationTask( + taskName, labelName, attrName, + textDefaultValue, archiveName, false, + { multiJobs: true, segmentSize: 1 }, + ); + cy.openTask(taskName); + cy.url().then((url) => { + taskID = Number(url.split('/').slice(-1)[0].split('?')[0]); + }); + cy.get('.cvat-job-item').first().invoke('attr', 'data-row-id').then((val) => { + jobID = val; + }).then(() => { + cy.intercept(`/api/labels?**job_id=${jobID}**`).as('getJobLabels'); + cy.visit(`/tasks/${taskID}/jobs/${jobID}`); + cy.wait('@getJobLabels').then((interception) => { + labels = interception.response.body.results; + }); + }); + }); + + it('Create ground truth job, compute quality report, check jobs table', () => { + cy.window().then((window) => window.cvat.server.request('/api/jobs', { + method: 'POST', + data: { + task_id: taskID, + frame_count: 20, + type: 'ground_truth', + frame_selection_method: 'random_uniform', + }, + }).then((response) => { + jobID = response.data.id; + return window.cvat.server.request(`/api/jobs/${jobID}/annotations`, { + method: 'PUT', + data: { + shapes: [], + tracks: [{ + label_id: labels[0].id, + frame: 0, + group: 0, + source: 'manual', + attributes: [], + elements: [], + shapes: [{ + type: 'rectangle', + occluded: false, + z_order: 0, + rotation: 0, + outside: false, + attributes: [], + frame: 0, + points: [250, 350, 350, 450], + }], + }], + tags: [], + }, + }); + }).then(() => ( + window.cvat.server.request(`/api/jobs/${jobID}`, { + method: 'PATCH', + data: { + stage: 'acceptance', + state: 'completed', + }, + }) + ))).then(() => { + createTaskQualityReport(taskID); + cy.get('.cvat-task-jobs-table .ant-pagination-item').last().invoke('text').then((page) => { + const lastPage = parseInt(page, 10); + + for (let i = 0; i < lastPage; i++) { + cy.get('.cvat-task-jobs-table-row').each((row) => { + cy.get(row).should('not.include.text', 'N/A'); + }); + + cy.get('.cvat-task-jobs-table .ant-pagination-next').click(); + } + }); + }); + }); + }); }); diff --git a/tests/cypress/e2e/features/masks_basics.js b/tests/cypress/e2e/features/masks_basics.js index b593cd43e131..04bf6212ceac 100644 --- a/tests/cypress/e2e/features/masks_basics.js +++ b/tests/cypress/e2e/features/masks_basics.js @@ -107,10 +107,7 @@ context('Manipulations with masks', { scrollBehavior: false }, () => { cy.drawMask(drawingActions); cy.finishMaskDrawing(); - cy.get('#cvat-objects-sidebar-state-item-1').find('[aria-label="more"]').trigger('mouseover'); - cy.get('.cvat-object-item-menu').within(() => { - cy.contains('button', 'Propagate').click(); - }); + cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Propagate'); cy.get('.cvat-propagate-confirm-up-to-input').find('input') .should('have.attr', 'value', serverFiles.length - 1); cy.contains('button', 'Yes').click(); @@ -125,10 +122,7 @@ context('Manipulations with masks', { scrollBehavior: false }, () => { cy.drawMask(drawingActions); cy.finishMaskDrawing(); - cy.get('#cvat-objects-sidebar-state-item-1').within(() => { - cy.get('[aria-label="more"]').trigger('mouseover'); - }); - cy.get('.cvat-object-item-menu').last().should('be.visible').contains('button', 'Make a copy').click(); + cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Make a copy'); cy.goCheckFrameNumber(serverFiles.length - 1); cy.get('.cvat-canvas-container').click(); cy.get('#cvat_canvas_shape_2').should('exist').and('be.visible'); @@ -160,10 +154,7 @@ context('Manipulations with masks', { scrollBehavior: false }, () => { cy.drawMask(drawingActions); cy.finishMaskDrawing(); - cy.get('#cvat-objects-sidebar-state-item-1').within(() => { - cy.get('[aria-label="more"]').trigger('mouseover'); - }); - cy.get('.cvat-object-item-menu').last().should('be.visible').contains('button', 'Edit').click(); + cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Edit'); cy.drawMask(editingActions); cy.finishMaskDrawing(); }); @@ -309,10 +300,7 @@ context('Manipulations with masks', { scrollBehavior: false }, () => { cy.drawMask(mask); cy.finishMaskDrawing(); - cy.get('#cvat-objects-sidebar-state-item-1').within(() => { - cy.get('[aria-label="more"]').trigger('mouseover'); - }); - cy.get('.cvat-object-item-menu').last().should('be.visible').contains('button', 'Edit').click(); + cy.interactAnnotationObjectMenu('#cvat-objects-sidebar-state-item-1', 'Edit'); cy.drawMask(eraseAction); cy.finishMaskDrawing(); diff --git a/tests/cypress/support/commands.js b/tests/cypress/support/commands.js index 802b03d2d550..0a492fece5af 100644 --- a/tests/cypress/support/commands.js +++ b/tests/cypress/support/commands.js @@ -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 @@ -33,7 +33,7 @@ Cypress.Commands.add('login', (username = Cypress.env('user'), password = Cypres }); Cypress.Commands.add('logout', () => { - cy.get('.cvat-header-menu-user-dropdown-user').trigger('mouseover'); + cy.get('.cvat-header-menu-user-dropdown-user').click(); cy.get('span[aria-label="logout"]').click(); cy.url().should('include', '/auth/login'); cy.visit('/auth/login'); @@ -385,16 +385,13 @@ Cypress.Commands.add('pressSplitControl', () => { cy.document().then((doc) => { const [el] = doc.getElementsByClassName('cvat-extra-controls-control'); if (el) { - el.dispatchEvent(new MouseEvent('mouseover', { bubbles: true })); + cy.get('.cvat-extra-controls-control').click(); } - }); - cy.get('.cvat-split-track-control').click(); + cy.get('.cvat-split-track-control').click(); - cy.document().then((doc) => { - const [el] = doc.getElementsByClassName('cvat-extra-controls-control'); if (el) { - el.dispatchEvent(new MouseEvent('mouseout', { bubbles: true })); + cy.get('body').click(); } }); }); @@ -406,7 +403,7 @@ Cypress.Commands.add('openTaskJob', (taskName, jobID = 0, removeAnnotations = tr Cypress.Commands.add('interactControlButton', (objectType) => { cy.get('body').trigger('mousedown'); - cy.get(`.cvat-${objectType}-control`).trigger('mouseover'); + cy.get(`.cvat-${objectType}-control`).click(); cy.get(`.cvat-${objectType}-popover`) .should('be.visible') .should('have.attr', 'style') @@ -584,8 +581,13 @@ Cypress.Commands.add('createPolygon', (createPolygonParams) => { }); Cypress.Commands.add('openSettings', () => { - cy.get('.cvat-header-menu-user-dropdown').trigger('mouseover'); - cy.get('.anticon-setting').should('exist').and('be.visible').click(); + cy.get('.cvat-header-menu-user-dropdown').click(); + cy.get('.cvat-header-menu') + .should('exist') + .and('be.visible') + .find('[role="menuitem"]') + .filter(':contains("Settings")') + .click(); cy.get('.cvat-settings-modal').should('be.visible'); }); @@ -912,7 +914,6 @@ Cypress.Commands.add('changeColorViaBadge', (labelColor) => { cy.contains('hex').prev().type(labelColor); cy.contains('button', 'Ok').click(); }); - cy.get('.cvat-label-color-picker').should('be.hidden'); }); Cypress.Commands.add('collectLabelsName', () => { @@ -955,6 +956,7 @@ Cypress.Commands.add('addNewLabel', ({ name, color }, additionalAttrs) => { if (color) { cy.get('.cvat-change-task-label-color-badge').click(); cy.changeColorViaBadge(color); + cy.get('.cvat-label-color-picker').should('be.hidden'); } if (additionalAttrs) { for (let i = 0; i < additionalAttrs.length; i++) { @@ -1278,8 +1280,7 @@ Cypress.Commands.add('goToCloudStoragesPage', () => { }); Cypress.Commands.add('deleteCloudStorage', (displayName) => { - cy.get('.cvat-cloud-storage-item-menu-button').trigger('mousemove'); - cy.get('.cvat-cloud-storage-item-menu-button').trigger('mouseover'); + cy.get('.cvat-cloud-storage-item-menu-button').click(); cy.get('.ant-dropdown') .not('.ant-dropdown-hidden') .within(() => { @@ -1353,7 +1354,7 @@ Cypress.Commands.add('deleteJob', (jobID) => { cy.get('.cvat-job-item').contains('a', `Job #${jobID}`) .parents('.cvat-job-item') .find('.cvat-job-item-more-button') - .trigger('mouseover'); + .click(); cy.get('.ant-dropdown') .not('.ant-dropdown-hidden') .within(() => { @@ -1419,7 +1420,7 @@ Cypress.Commands.add('drawMask', (instructions) => { }); Cypress.Commands.add('startMaskDrawing', () => { - cy.get('.cvat-draw-mask-control ').trigger('mouseover'); + cy.get('.cvat-draw-mask-control ').click(); cy.get('.cvat-draw-mask-popover').should('exist').and('be.visible').within(() => { cy.get('button').click(); }); @@ -1504,6 +1505,16 @@ Cypress.Commands.add('joinShapes', ( interactWithTool(); }); +Cypress.Commands.add('interactAnnotationObjectMenu', (parentSelector, button) => { + cy.get(parentSelector).within(() => { + cy.get('[aria-label="more"]').click(); + }); + + cy.document().find('.cvat-object-item-menu').within(() => { + cy.contains('button', button).click(); + }); +}); + Cypress.Commands.overwrite('visit', (orig, url, options) => { orig(url, options); cy.closeModalUnsupportedPlatform(); diff --git a/tests/cypress/support/commands_filters_feature.js b/tests/cypress/support/commands_filters_feature.js index acc9df35c1ce..af69cb4e0b94 100644 --- a/tests/cypress/support/commands_filters_feature.js +++ b/tests/cypress/support/commands_filters_feature.js @@ -1,4 +1,5 @@ // Copyright (C) 2021-2022 Intel Corporation +// Copyright (C) 2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -137,12 +138,10 @@ Cypress.Commands.add( Cypress.Commands.add('selectFilterValue', (filterValue) => { cy.checkFiltersModalOpened(); - cy.get('.recently-used-wrapper').trigger('mouseover'); - cy.get('.ant-dropdown') - .not('.ant-dropdown-hidden') - .within(() => { - cy.contains('[role="menuitem"]', new RegExp(`^${filterValue}$`)).click(); - }); + cy.get('.cvat-recently-used-filters-wrapper').click(); + cy.get('.cvat-recently-used-filters-dropdown').should('exist').and('be.visible').within(() => { + cy.get('li').contains(filterValue).click(); + }); cy.get('.cvat-filters-modal-visible').within(() => { cy.contains('button', 'Submit').click(); }); diff --git a/tests/cypress/support/commands_opencv.js b/tests/cypress/support/commands_opencv.js index a4ab396bd15f..1fef6d9fa88e 100644 --- a/tests/cypress/support/commands_opencv.js +++ b/tests/cypress/support/commands_opencv.js @@ -1,5 +1,5 @@ // Copyright (C) 2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -8,11 +8,7 @@ let selectedValueGlobal = ''; Cypress.Commands.add('interactOpenCVControlButton', () => { - cy.get('body').focus(); - cy.get('.cvat-opencv-control').trigger('mouseleave'); - cy.get('.cvat-opencv-control').trigger('mouseout'); - cy.get('.cvat-opencv-control').trigger('mousemove'); - cy.get('.cvat-opencv-control').trigger('mouseover'); + cy.get('.cvat-opencv-control').click(); cy.get('.cvat-opencv-control').should('have.class', 'ant-popover-open'); cy.get('.cvat-opencv-control-popover') .should('be.visible') diff --git a/tests/cypress/support/commands_organizations.js b/tests/cypress/support/commands_organizations.js index 28b85fc7e8f3..4bf50da95c1d 100644 --- a/tests/cypress/support/commands_organizations.js +++ b/tests/cypress/support/commands_organizations.js @@ -1,18 +1,23 @@ // Copyright (C) 2022 Intel Corporation -// Copyright (C) 2023 CVAT.ai Corporation +// Copyright (C) 2023-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT /// -Cypress.Commands.add('createOrganization', (organizationParams) => { +function openOrganizationsMenu() { cy.get('.cvat-header-menu-user-dropdown') - .should('exist').and('be.visible').trigger('mouseover'); + .should('exist').and('be.visible').click(); cy.get('.cvat-header-menu') - .should('be.visible') + .should('exist') + .and('be.visible') .find('[role="menuitem"]') .filter(':contains("Organization")') - .trigger('mouseover'); + .click(); +} + +Cypress.Commands.add('createOrganization', (organizationParams) => { + openOrganizationsMenu(); cy.get('.cvat-header-menu-create-organization') .should('be.visible') .click(); @@ -64,13 +69,7 @@ Cypress.Commands.add('deleteOrganizations', (authResponse, otrganizationsToDelet }); Cypress.Commands.add('activateOrganization', (organizationShortName) => { - cy.get('.cvat-header-menu-user-dropdown').trigger('mouseover'); - cy.get('.ant-dropdown') - .should('be.visible') - .not('ant-dropdown-hidden') - .find('[role="menuitem"]') - .filter(':contains("Organization")') - .trigger('mouseover'); + openOrganizationsMenu(); cy.contains('.cvat-header-menu-organization-item', organizationShortName) .should('be.visible') .click(); @@ -81,26 +80,14 @@ Cypress.Commands.add('activateOrganization', (organizationShortName) => { }); Cypress.Commands.add('deactivateOrganization', () => { - cy.get('.cvat-header-menu-user-dropdown').trigger('mouseover'); - cy.get('.ant-dropdown') - .should('be.visible') - .not('ant-dropdown-hidden') - .find('[role="menuitem"]') - .filter(':contains("Organization")') - .trigger('mouseover'); + openOrganizationsMenu(); cy.contains('.cvat-header-menu-organization-item', 'Personal workspace').click(); cy.get('.cvat-header-menu-user-dropdown').should('be.visible'); cy.get('.cvat-header-menu-user-dropdown-organization').should('not.exist'); }); Cypress.Commands.add('openOrganization', (organizationShortName) => { - cy.get('.cvat-header-menu-user-dropdown').trigger('mouseover'); - cy.get('.ant-dropdown') - .should('be.visible') - .not('ant-dropdown-hidden') - .find('[role="menuitem"]') - .filter(':contains("Organization")') - .trigger('mouseover'); + openOrganizationsMenu(); cy.get('.cvat-header-menu-active-organization-item') .should('have.text', organizationShortName); cy.get('.cvat-header-menu-open-organization') @@ -110,22 +97,13 @@ Cypress.Commands.add('openOrganization', (organizationShortName) => { }); Cypress.Commands.add('checkOrganizationExists', (organizationShortName, shouldExist = true) => { - cy.get('.cvat-header-menu-user-dropdown').trigger('mouseover'); - cy.get('.ant-dropdown') - .should('be.visible') - .not('ant-dropdown-hidden') - .find('[role="menuitem"]') - .filter(':contains("Organization")') - .trigger('mouseover'); + openOrganizationsMenu(); if (shouldExist) { cy.contains('.cvat-header-menu-organization-item', organizationShortName).should('exist'); - cy.contains('.cvat-header-menu-organization-item', organizationShortName).trigger('mouseout'); - cy.contains('.cvat-header-menu-organization-item', organizationShortName).should('be.hidden'); } else { cy.contains('.cvat-header-menu-organization-item', organizationShortName).should('not.exist'); - cy.get('.cvat-header-menu-active-organization-item').trigger('mouseout'); - cy.get('.cvat-header-menu-active-organization-item').should('be.hidden'); } + cy.get('body').click(); }); Cypress.Commands.add('checkOrganizationParams', (organizationParams) => { diff --git a/tests/cypress/support/commands_projects.js b/tests/cypress/support/commands_projects.js index 8cab25a80587..8b82a29a0c0e 100644 --- a/tests/cypress/support/commands_projects.js +++ b/tests/cypress/support/commands_projects.js @@ -1,5 +1,5 @@ // Copyright (C) 2020-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -83,14 +83,14 @@ Cypress.Commands.add('openProjectActions', (projectName) => { .parents('.cvat-projects-project-item-card') .within(() => { cy.get('.cvat-projects-project-item-description').within(() => { - cy.get('[type="button"]').trigger('mouseover'); + cy.get('[type="button"]').click(); }); }); }); Cypress.Commands.add('clickInProjectMenu', (item, fromProjectPage, projectName = '') => { if (fromProjectPage) { - cy.get('.cvat-project-top-bar-actions').trigger('mouseover'); + cy.get('.cvat-project-top-bar-actions').click(); } else { cy.openProjectActions(projectName); } diff --git a/tests/cypress/support/commands_webhooks.js b/tests/cypress/support/commands_webhooks.js index accf07143e3d..13694606c9fd 100644 --- a/tests/cypress/support/commands_webhooks.js +++ b/tests/cypress/support/commands_webhooks.js @@ -1,4 +1,4 @@ -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -12,7 +12,7 @@ Cypress.Commands.add('createWebhook', (webhookData) => { Cypress.Commands.add('openWebhookActions', (description) => { cy.contains(description).parents('.cvat-webhooks-list-item').within(() => { - cy.get('.cvat-webhooks-page-actions-button').trigger('mouseover'); + cy.get('.cvat-webhooks-page-actions-button').click(); }); }); @@ -66,7 +66,7 @@ Cypress.Commands.add('setUpWebhook', (webhookData) => { }); Cypress.Commands.add('openOrganizationWebhooks', () => { - cy.get('.cvat-organization-page-actions-button').trigger('mouseover'); + cy.get('.cvat-organization-page-actions-button').click(); cy.get('.cvat-organization-actions-menu').within(() => { cy.contains('[role="menuitem"]', 'Setup webhooks').click(); }); diff --git a/tests/pr_cypress.config.js b/tests/pr_cypress.config.js deleted file mode 100644 index aed25ceccec1..000000000000 --- a/tests/pr_cypress.config.js +++ /dev/null @@ -1,46 +0,0 @@ -const { defineConfig } = require('cypress'); -const plugins = require('./cypress/plugins/index'); - -module.exports = defineConfig({ - video: false, - viewportWidth: 1300, - viewportHeight: 960, - defaultCommandTimeout: 25000, - downloadsFolder: 'cypress/fixtures', - env: { - user: 'admin', - email: 'admin@localhost.company', - password: '12qwaszx', - coverage: false, - }, - e2e: { - setupNodeEvents(on, config) { - return plugins(on, config); - }, - testIsolation: false, - baseUrl: 'http://localhost:8080', - specPattern: [ - 'cypress/e2e/actions_objects2/case_108_rotated_bounding_boxes.js', - 'cypress/e2e/actions_objects2/case_10_polygon_shape_track_label_points.js', - 'cypress/e2e/actions_objects2/case_115_ellipse_shape_track_label.js', - 'cypress/e2e/actions_objects2/case_11_polylines_shape_track_label_points.js', - 'cypress/e2e/actions_objects2/case_12_points_shape_track_label.js', - 'cypress/e2e/actions_objects2/case_13_merge_split_features.js', - 'cypress/e2e/actions_objects2/case_14_appearance_features.js', - 'cypress/e2e/actions_objects2/case_15_group_features.js', - 'cypress/e2e/actions_objects2/case_16_z_order_features.js', - 'cypress/e2e/actions_objects2/case_17_lock_hide_features.js', - 'cypress/e2e/issues_prs/issue_2418_object_tag_same_labels.js', - 'cypress/e2e/issues_prs/issue_2485_navigation_empty_frames.js', - 'cypress/e2e/issues_prs/issue_2486_not_edit_object_aam.js', - 'cypress/e2e/issues_prs/issue_2487_extra_instances_canvas_grouping.js', - 'cypress/e2e/issues_prs/issue_2661_displaying_attached_files_when_creating_task.js', - 'cypress/e2e/issues_prs/issue_2753_call_HOC_component_each_render.js', - 'cypress/e2e/issues_prs/issue_2807_polyline_editing.js', - 'cypress/e2e/issues_prs/issue_2992_crop_polygon_properly.js', - 'cypress/e2e/issues_prs/pr_1370_check_UI_fail_with_object_dragging_and_go_next_frame.js', - 'cypress/e2e/issues_prs/pr_2203_error_cannot_read_property_at_saving_job.js', - 'cypress/e2e/remove_users_tasks_projects_organizations.js', - ], - }, -}); diff --git a/tests/pr_cypress_canvas3d.config.js b/tests/pr_cypress_canvas3d.config.js deleted file mode 100644 index f0a1618aa82a..000000000000 --- a/tests/pr_cypress_canvas3d.config.js +++ /dev/null @@ -1,31 +0,0 @@ -const { defineConfig } = require('cypress'); -const plugins = require('./cypress/plugins/index'); - -module.exports = defineConfig({ - video: false, - viewportWidth: 1300, - viewportHeight: 960, - defaultCommandTimeout: 25000, - downloadsFolder: 'cypress/fixtures', - env: { - user: 'admin', - email: 'admin@localhost.company', - password: '12qwaszx', - coverage: false, - }, - e2e: { - setupNodeEvents(on, config) { - return plugins(on, config); - }, - testIsolation: false, - baseUrl: 'http://localhost:8080', - specPattern: [ - 'cypress/e2e/actions_projects_models/case_104_project_export_3d.js', - 'cypress/e2e/canvas3d_functionality_2/case_56_canvas3d_functionality_basic_actions.js', - 'cypress/e2e/canvas3d_functionality_2/case_62_canvas3d_functionality_views_resize.js', - 'cypress/e2e/canvas3d_functionality_2/case_63_canvas3d_functionality_control_button_mouse_interaction.js', - 'cypress/e2e/canvas3d_functionality_2/case_64_canvas3d_functionality_cuboid.js', - 'cypress/e2e/remove_users_tasks_projects_organizations.js', - ], - }, -});
| ConnectedComponent, + ControlComponent: React.FunctionComponent, ): React.FunctionComponent { let visibilityHeightThreshold = 0; // minimum value of height when element can be pushed to main panel diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx index 4f634ddc2bb8..8ebbfa893ace 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.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 @@ -7,10 +7,11 @@ import React from 'react'; import Layout from 'antd/lib/layout'; import { - ActiveControl, ObjectType, Rotation, ShapeType, CombinedState, + ActiveControl, Rotation, CombinedState, } from 'reducers'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; +import { LabelType } from 'cvat-core-wrapper'; import ControlVisibilityObserver, { ExtraControlsControl } from './control-visibility-observer'; import RotateControl, { Props as RotateControlProps } from './rotate-control'; @@ -105,14 +106,14 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { let tagControlVisible = withUnspecifiedType; const skeletonControlVisible = labels.some((label: Label) => label.type === 'skeleton'); labels.forEach((label: Label) => { - rectangleControlVisible = rectangleControlVisible || label.type === ShapeType.RECTANGLE; - polygonControlVisible = polygonControlVisible || label.type === ShapeType.POLYGON; - polylineControlVisible = polylineControlVisible || label.type === ShapeType.POLYLINE; - pointsControlVisible = pointsControlVisible || label.type === ShapeType.POINTS; - ellipseControlVisible = ellipseControlVisible || label.type === ShapeType.ELLIPSE; - cuboidControlVisible = cuboidControlVisible || label.type === ShapeType.CUBOID; - maskControlVisible = maskControlVisible || label.type === ShapeType.MASK; - tagControlVisible = tagControlVisible || label.type === ObjectType.TAG; + rectangleControlVisible = rectangleControlVisible || label.type === LabelType.RECTANGLE; + polygonControlVisible = polygonControlVisible || label.type === LabelType.POLYGON; + polylineControlVisible = polylineControlVisible || label.type === LabelType.POLYLINE; + pointsControlVisible = pointsControlVisible || label.type === LabelType.POINTS; + ellipseControlVisible = ellipseControlVisible || label.type === LabelType.ELLIPSE; + cuboidControlVisible = cuboidControlVisible || label.type === LabelType.CUBOID; + maskControlVisible = maskControlVisible || label.type === LabelType.MASK; + tagControlVisible = tagControlVisible || label.type === LabelType.TAG; }); const preventDefault = (event: KeyboardEvent | undefined): void => { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx index 6ebe9579ddd1..03be784ec104 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.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 @@ -150,13 +150,11 @@ function DrawShapePopoverComponent(props: Props): JSX.Element { ) : null} - + Shape - - {shapeType !== ShapeType.MASK && ( - + {shapeType !== ShapeType.MASK && ( - - )} + )} +
, ): React.FunctionComponent
{ let visibilityHeightThreshold = 0; // minimum value of height when element can be pushed to main panel diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx index 4f634ddc2bb8..8ebbfa893ace 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/controls-side-bar.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 @@ -7,10 +7,11 @@ import React from 'react'; import Layout from 'antd/lib/layout'; import { - ActiveControl, ObjectType, Rotation, ShapeType, CombinedState, + ActiveControl, Rotation, CombinedState, } from 'reducers'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import { Canvas, CanvasMode } from 'cvat-canvas-wrapper'; +import { LabelType } from 'cvat-core-wrapper'; import ControlVisibilityObserver, { ExtraControlsControl } from './control-visibility-observer'; import RotateControl, { Props as RotateControlProps } from './rotate-control'; @@ -105,14 +106,14 @@ export default function ControlsSideBarComponent(props: Props): JSX.Element { let tagControlVisible = withUnspecifiedType; const skeletonControlVisible = labels.some((label: Label) => label.type === 'skeleton'); labels.forEach((label: Label) => { - rectangleControlVisible = rectangleControlVisible || label.type === ShapeType.RECTANGLE; - polygonControlVisible = polygonControlVisible || label.type === ShapeType.POLYGON; - polylineControlVisible = polylineControlVisible || label.type === ShapeType.POLYLINE; - pointsControlVisible = pointsControlVisible || label.type === ShapeType.POINTS; - ellipseControlVisible = ellipseControlVisible || label.type === ShapeType.ELLIPSE; - cuboidControlVisible = cuboidControlVisible || label.type === ShapeType.CUBOID; - maskControlVisible = maskControlVisible || label.type === ShapeType.MASK; - tagControlVisible = tagControlVisible || label.type === ObjectType.TAG; + rectangleControlVisible = rectangleControlVisible || label.type === LabelType.RECTANGLE; + polygonControlVisible = polygonControlVisible || label.type === LabelType.POLYGON; + polylineControlVisible = polylineControlVisible || label.type === LabelType.POLYLINE; + pointsControlVisible = pointsControlVisible || label.type === LabelType.POINTS; + ellipseControlVisible = ellipseControlVisible || label.type === LabelType.ELLIPSE; + cuboidControlVisible = cuboidControlVisible || label.type === LabelType.CUBOID; + maskControlVisible = maskControlVisible || label.type === LabelType.MASK; + tagControlVisible = tagControlVisible || label.type === LabelType.TAG; }); const preventDefault = (event: KeyboardEvent | undefined): void => { diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx index 6ebe9579ddd1..03be784ec104 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/controls-side-bar/draw-shape-popover.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 @@ -150,13 +150,11 @@ function DrawShapePopoverComponent(props: Props): JSX.Element { ) : null} - + Shape - - {shapeType !== ShapeType.MASK && ( - + {shapeType !== ShapeType.MASK && ( - - )} + )} +