diff --git a/.vscode/launch.json b/.vscode/launch.json index 5ed666059a9d..af93ae24c007 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -376,6 +376,22 @@ "env": {}, "console": "internalConsole" }, + { + "name": "server: sync periodic jobs", + "type": "debugpy", + "request": "launch", + "justMyCode": false, + "stopOnEntry": false, + "python": "${command:python.interpreterPath}", + "program": "${workspaceFolder}/manage.py", + "args": [ + "syncperiodicjobs" + ], + "django": true, + "cwd": "${workspaceFolder}", + "env": {}, + "console": "internalConsole" + }, { "name": "server: tests", "type": "debugpy", diff --git a/CHANGELOG.md b/CHANGELOG.md index a195b77dc924..2b9a4fd9584c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 + +## \[2.21.2\] - 2024-10-24 + +### Added + +- Access to /analytics can now be granted + () + +### Fixed + +- Expired sessions are now cleared from the database daily + () + +- Fixed export/import errors for tracks with duplicated shapes. + Fixed a bug which caused shape duplication on track import. + () + +- Fix Grafana container restart policy + () + +- Fixed some interface tooltips having 'undefined' shortcuts + () + +- Memory consumption during preparation of image chunks + () + +- Fixed a bug where an export RQ job being retried may break scheduling + of new jobs + () + +- UI now allows the user to start automatic annotation again + if the previous request fails + () + ## \[2.21.1\] - 2024-10-18 diff --git a/Dockerfile b/Dockerfile index 8a10a34b771b..00dea1de30d0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -134,6 +134,7 @@ RUN apt-get update && \ supervisor \ tzdata \ unrar \ + wait-for-it \ && ln -fs /usr/share/zoneinfo/${TZ} /etc/localtime && \ dpkg-reconfigure -f noninteractive tzdata && \ rm -rf /var/lib/apt/lists/* && \ @@ -192,7 +193,7 @@ RUN python -m pip uninstall -y pip COPY cvat/nginx.conf /etc/nginx/nginx.conf COPY --chown=${USER} components /tmp/components COPY --chown=${USER} supervisord/ ${HOME}/supervisord -COPY --chown=${USER} wait-for-it.sh manage.py backend_entrypoint.sh wait_for_deps.sh ${HOME}/ +COPY --chown=${USER} manage.py backend_entrypoint.sh wait_for_deps.sh ${HOME}/ COPY --chown=${USER} utils/ ${HOME}/utils COPY --chown=${USER} cvat/ ${HOME}/cvat COPY --chown=${USER} rqscheduler.py ${HOME} diff --git a/backend_entrypoint.sh b/backend_entrypoint.sh index c8b681eabb4d..bac37c76e5be 100755 --- a/backend_entrypoint.sh +++ b/backend_entrypoint.sh @@ -8,7 +8,7 @@ fail() { } wait_for_db() { - ~/wait-for-it.sh "${CVAT_POSTGRES_HOST}:${CVAT_POSTGRES_PORT:-5432}" -t 0 + wait-for-it "${CVAT_POSTGRES_HOST}:${CVAT_POSTGRES_PORT:-5432}" -t 0 } cmd_bash() { @@ -18,6 +18,9 @@ cmd_bash() { cmd_init() { wait_for_db ~/manage.py migrate + + wait-for-it "${CVAT_REDIS_INMEM_HOST}:${CVAT_REDIS_INMEM_PORT:-6379}" -t 0 + ~/manage.py syncperiodicjobs } cmd_run() { diff --git a/cvat-cli/requirements/base.txt b/cvat-cli/requirements/base.txt index 5d1eadd9b89f..c307b6c96895 100644 --- a/cvat-cli/requirements/base.txt +++ b/cvat-cli/requirements/base.txt @@ -1,3 +1,3 @@ -cvat-sdk~=2.21.1 +cvat-sdk~=2.21.2 Pillow>=10.3.0 setuptools>=70.0.0 # 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 4017971cf6df..528bf553abd1 100644 --- a/cvat-cli/src/cvat_cli/version.py +++ b/cvat-cli/src/cvat_cli/version.py @@ -1 +1 @@ -VERSION = "2.21.1" +VERSION = "2.21.2" diff --git a/cvat-core/src/annotations-collection.ts b/cvat-core/src/annotations-collection.ts index 291fcc6c3e97..14879e86bcd8 100644 --- a/cvat-core/src/annotations-collection.ts +++ b/cvat-core/src/annotations-collection.ts @@ -1295,7 +1295,7 @@ export default class Collection { const predicate = sign > 0 ? (frame) => frame <= frameTo : (frame) => frame >= frameTo; const update = sign > 0 ? (frame) => frame + 1 : (frame) => frame - 1; - // if not looking for an emty frame nor frame with annotations, return the next frame + // if not looking for an empty frame nor frame with annotations, return the next frame // check if deleted frames are allowed additionally if (!annotationsFilters) { let frame = frameFrom; diff --git a/cvat-core/src/api.ts b/cvat-core/src/api.ts index 5f624ad0e8ae..ca33f431c43e 100644 --- a/cvat-core/src/api.ts +++ b/cvat-core/src/api.ts @@ -26,7 +26,7 @@ import QualityReport from './quality-report'; import QualityConflict from './quality-conflict'; import QualitySettings from './quality-settings'; import AnalyticsReport from './analytics-report'; -import ValidationLayout from './validation-layout'; +import { JobValidationLayout, TaskValidationLayout } from './validation-layout'; import { Request } from './request'; import * as enums from './enums'; @@ -427,7 +427,8 @@ function build(): CVATCore { QualityReport, Request, FramesMetaData, - ValidationLayout, + JobValidationLayout, + TaskValidationLayout, }, utils: { mask2Rle, diff --git a/cvat-core/src/cloud-storage.ts b/cvat-core/src/cloud-storage.ts index e4e4fb0e5d23..1e7cdeb8d7f7 100644 --- a/cvat-core/src/cloud-storage.ts +++ b/cvat-core/src/cloud-storage.ts @@ -290,7 +290,7 @@ Object.defineProperties(CloudStorage.prototype.save, { } // update if (typeof this.id !== 'undefined') { - // provider_type and recource should not change; + // provider_type and resource should not change; // send to the server only the values that have changed const initialData: SerializedCloudStorage = {}; if (this.displayName) { diff --git a/cvat-core/src/frames.ts b/cvat-core/src/frames.ts index 1192058c11b3..b29335865d01 100644 --- a/cvat-core/src/frames.ts +++ b/cvat-core/src/frames.ts @@ -683,7 +683,7 @@ export function getContextImage(jobID: number, frame: number): Promise setTimeout(checkAndExecute)); } else { @@ -775,7 +775,7 @@ export async function getFrame( // - getContextImage // - getCachedChunks // And from this idea we should call refreshJobCacheIfOutdated from each one - // Hovewer, following from the order, these methods are usually called + // However, following from the order, these methods are usually called // it may lead to even more confusing behaviour // // Usually user first receives frame, then user receives ranges and finally user receives context images diff --git a/cvat-core/src/index.ts b/cvat-core/src/index.ts index f361f194df73..8a4c9e8bfb53 100644 --- a/cvat-core/src/index.ts +++ b/cvat-core/src/index.ts @@ -32,7 +32,7 @@ import QualityConflict from './quality-conflict'; import QualitySettings from './quality-settings'; import AnalyticsReport from './analytics-report'; import AnnotationGuide from './guide'; -import ValidationLayout from './validation-layout'; +import { JobValidationLayout, TaskValidationLayout } from './validation-layout'; import { Request } from './request'; import BaseSingleFrameAction, { listActions, registerAction, runActions } from './annotations-actions'; import { @@ -216,7 +216,8 @@ export default interface CVATCore { AnalyticsReport: typeof AnalyticsReport; Request: typeof Request; FramesMetaData: typeof FramesMetaData; - ValidationLayout: typeof ValidationLayout; + JobValidationLayout: typeof JobValidationLayout; + TaskValidationLayout: typeof TaskValidationLayout; }; utils: { mask2Rle: typeof mask2Rle; diff --git a/cvat-core/src/object-utils.ts b/cvat-core/src/object-utils.ts index 0c4a3e5d8143..6e7fcbbd8d8c 100644 --- a/cvat-core/src/object-utils.ts +++ b/cvat-core/src/object-utils.ts @@ -60,7 +60,7 @@ export function findAngleDiff(rightAngle: number, leftAngle: number): number { angleDiff = ((angleDiff + 180) % 360) - 180; if (Math.abs(angleDiff) >= 180) { // if the main arc is bigger than 180, go another arc - // to find it, just substract absolute value from 360 and inverse sign + // to find it, just subtract absolute value from 360 and inverse sign angleDiff = 360 - Math.abs(angleDiff) * Math.sign(angleDiff) * -1; } return angleDiff; diff --git a/cvat-core/src/requests-manager.ts b/cvat-core/src/requests-manager.ts index 429c42dba2f3..c348923e68bc 100644 --- a/cvat-core/src/requests-manager.ts +++ b/cvat-core/src/requests-manager.ts @@ -74,7 +74,7 @@ class RequestsManager { const promise = new Promise((resolve, reject) => { const timeoutCallback = async (): Promise => { // We make sure that no more than REQUESTS_COUNT requests are sent simultaneously - // If thats the case, we re-schedule the timeout + // If that's the case, we re-schedule the timeout const timestamp = Date.now(); if (this.requestStack.length >= REQUESTS_COUNT) { const timestampToCheck = this.requestStack[this.requestStack.length - 1]; diff --git a/cvat-core/src/server-proxy.ts b/cvat-core/src/server-proxy.ts index 7e8819808649..eb9c15ce64b9 100644 --- a/cvat-core/src/server-proxy.ts +++ b/cvat-core/src/server-proxy.ts @@ -19,7 +19,7 @@ import { SerializedInvitationData, SerializedCloudStorage, SerializedFramesMetaData, SerializedCollection, SerializedQualitySettingsData, APIQualitySettingsFilter, SerializedQualityConflictData, APIQualityConflictsFilter, SerializedQualityReportData, APIQualityReportsFilter, SerializedAnalyticsReport, APIAnalyticsReportFilter, - SerializedRequest, SerializedValidationLayout, + SerializedRequest, SerializedJobValidationLayout, SerializedTaskValidationLayout, } from './server-response-types'; import { PaginatedResource } from './core-types'; import { Request } from './request'; @@ -102,7 +102,7 @@ function fetchAll(url, filter = {}): Promise { } }); - // removing possible dublicates + // removing possible duplicates const obj = result.results.reduce((acc: Record, item: any) => { acc[item.id] = item; return acc; @@ -1384,7 +1384,7 @@ async function deleteJob(jobID: number): Promise { const validationLayout = (instance: 'tasks' | 'jobs') => async ( id: number, -): Promise => { +): Promise => { const { backendAPI } = config; try { diff --git a/cvat-core/src/server-response-types.ts b/cvat-core/src/server-response-types.ts index 5dd8cc3d54d2..af6cd760ed40 100644 --- a/cvat-core/src/server-response-types.ts +++ b/cvat-core/src/server-response-types.ts @@ -54,6 +54,7 @@ export interface SerializedUser { last_login?: string; date_joined?: string; email_verification_required: boolean; + has_analytics_access: boolean; } interface SerializedStorage { @@ -524,8 +525,14 @@ export interface SerializedRequest { owner?: any; } -export interface SerializedValidationLayout { +export interface SerializedJobValidationLayout { honeypot_count?: number; honeypot_frames?: number[]; honeypot_real_frames?: number[]; } + +export interface SerializedTaskValidationLayout extends SerializedJobValidationLayout { + mode: 'gt' | 'gt_pool' | null; + validation_frames?: number[]; + disabled_frames?: number[]; +} diff --git a/cvat-core/src/session-implementation.ts b/cvat-core/src/session-implementation.ts index 38728a409448..369d0c9d5393 100644 --- a/cvat-core/src/session-implementation.ts +++ b/cvat-core/src/session-implementation.ts @@ -27,7 +27,10 @@ import { decodePreview, } from './frames'; import Issue from './issue'; -import { SerializedLabel, SerializedTask, SerializedValidationLayout } from './server-response-types'; +import { + SerializedLabel, SerializedTask, SerializedJobValidationLayout, + SerializedTaskValidationLayout, +} from './server-response-types'; import { checkInEnum, checkObjectType } from './common'; import { getCollection, getSaver, clearAnnotations, getAnnotations, @@ -37,7 +40,7 @@ import AnnotationGuide from './guide'; import requestsManager from './requests-manager'; import { Request } from './request'; import User from './user'; -import ValidationLayout from './validation-layout'; +import { JobValidationLayout, TaskValidationLayout } from './validation-layout'; // must be called with task/job context async function deleteFrameWrapper(jobID, frame): Promise { @@ -171,7 +174,7 @@ export function implementJob(Job: typeof JobClass): typeof JobClass { ): ReturnType { const result = await serverProxy.jobs.validationLayout(this.id); if (Object.keys(result).length) { - return new ValidationLayout(result as Required); + return new JobValidationLayout(result as SerializedJobValidationLayout); } return null; @@ -374,7 +377,7 @@ export function implementJob(Job: typeof JobClass): typeof JobClass { } if ('annotationsFilters' in searchParameters && 'generalFilters' in searchParameters) { - throw new ArgumentError('Both annotations filters and general fiters could not be used together'); + throw new ArgumentError('Both annotations filters and general filters could not be used together'); } if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { @@ -641,9 +644,9 @@ export function implementTask(Task: typeof TaskClass): typeof TaskClass { value: async function validationLayoutImplementation( this: TaskClass, ): ReturnType { - const result = await serverProxy.tasks.validationLayout(this.id); - if (Object.keys(result).length) { - return new ValidationLayout(result as Required); + const result = await serverProxy.tasks.validationLayout(this.id) as SerializedTaskValidationLayout; + if (result.mode !== null) { + return new TaskValidationLayout(result); } return null; @@ -1043,7 +1046,7 @@ export function implementTask(Task: typeof TaskClass): typeof TaskClass { } if ('annotationsFilters' in searchParameters && 'generalFilters' in searchParameters) { - throw new ArgumentError('Both annotations filters and general fiters could not be used together'); + throw new ArgumentError('Both annotations filters and general filters could not be used together'); } if (!Number.isInteger(frameFrom) || !Number.isInteger(frameTo)) { diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts index cf82aa9a050c..8ecef7e0e632 100644 --- a/cvat-core/src/session.ts +++ b/cvat-core/src/session.ts @@ -28,7 +28,7 @@ import { Request } from './request'; import logger from './logger'; import Issue from './issue'; import ObjectState from './object-state'; -import ValidationLayout from './validation-layout'; +import { JobValidationLayout, TaskValidationLayout } from './validation-layout'; function buildDuplicatedAPI(prototype) { Object.defineProperties(prototype, { @@ -686,7 +686,7 @@ export class Job extends Session { return result; } - async validationLayout(): Promise { + async validationLayout(): Promise { const result = await PluginRegistry.apiWrapper.call(this, Job.prototype.validationLayout); return result; } @@ -1186,7 +1186,7 @@ export class Task extends Session { return result; } - async validationLayout(): Promise { + async validationLayout(): Promise { const result = await PluginRegistry.apiWrapper.call(this, Task.prototype.validationLayout); return result; } diff --git a/cvat-core/src/user.ts b/cvat-core/src/user.ts index 1b0eb5ecfec9..6d7366151fb4 100644 --- a/cvat-core/src/user.ts +++ b/cvat-core/src/user.ts @@ -1,5 +1,5 @@ // Copyright (C) 2019-2022 Intel Corporation -// Copyright (C) 2022 CVAT.ai Corporation +// Copyright (C) 2022-2024 CVAT.ai Corporation // // SPDX-License-Identifier: MIT @@ -18,6 +18,7 @@ export default class User { public readonly isSuperuser: boolean; public readonly isActive: boolean; public readonly isVerified: boolean; + public readonly hasAnalyticsAccess: boolean; constructor(initialData: SerializedUser) { const data = { @@ -33,6 +34,7 @@ export default class User { is_superuser: null, is_active: null, email_verification_required: null, + has_analytics_access: null, }; for (const property in data) { @@ -80,6 +82,9 @@ export default class User { isVerified: { get: () => !data.email_verification_required, }, + hasAnalyticsAccess: { + get: () => data.has_analytics_access, + }, }), ); } @@ -98,6 +103,7 @@ export default class User { is_superuser: this.isSuperuser, is_active: this.isActive, email_verification_required: this.isVerified, + has_analytics_access: this.hasAnalyticsAccess, }; } } diff --git a/cvat-core/src/validation-layout.ts b/cvat-core/src/validation-layout.ts index ba5a94aa03a9..064af13b2514 100644 --- a/cvat-core/src/validation-layout.ts +++ b/cvat-core/src/validation-layout.ts @@ -2,37 +2,43 @@ // // SPDX-License-Identifier: MIT -import { SerializedValidationLayout } from 'server-response-types'; +import { SerializedJobValidationLayout, SerializedTaskValidationLayout } from 'server-response-types'; import PluginRegistry from './plugins'; -export default class ValidationLayout { - #honeypotFrames: number[]; - #honeypotRealFrames: number[]; +export class JobValidationLayout { + #honeypotCount: JobValidationLayout['honeypotCount']; + #honeypotFrames: JobValidationLayout['honeypotFrames']; + #honeypotRealFrames: JobValidationLayout['honeypotRealFrames']; - public constructor(data: Required) { - this.#honeypotFrames = [...data.honeypot_frames]; - this.#honeypotRealFrames = [...data.honeypot_real_frames]; + public constructor(data: SerializedJobValidationLayout) { + this.#honeypotCount = data.honeypot_count ?? 0; + this.#honeypotFrames = [...(data.honeypot_frames ?? [])]; + this.#honeypotRealFrames = [...(data.honeypot_real_frames ?? [])]; } - public get honeypotFrames() { + public get honeypotCount(): number { + return this.#honeypotCount; + } + + public get honeypotFrames(): number[] { return [...this.#honeypotFrames]; } - public get honeypotRealFrames() { + public get honeypotRealFrames(): number[] { return [...this.#honeypotRealFrames]; } async getRealFrame(frame: number): Promise { - const result = await PluginRegistry.apiWrapper.call(this, ValidationLayout.prototype.getRealFrame, frame); + const result = await PluginRegistry.apiWrapper.call(this, JobValidationLayout.prototype.getRealFrame, frame); return result; } } -Object.defineProperties(ValidationLayout.prototype.getRealFrame, { +Object.defineProperties(JobValidationLayout.prototype.getRealFrame, { implementation: { writable: false, enumerable: false, - value: function implementation(this: ValidationLayout, frame: number): number | null { + value: function implementation(this: JobValidationLayout, frame: number): number | null { const index = this.honeypotFrames.indexOf(frame); if (index !== -1) { return this.honeypotRealFrames[index]; @@ -42,3 +48,28 @@ Object.defineProperties(ValidationLayout.prototype.getRealFrame, { }, }, }); + +export class TaskValidationLayout extends JobValidationLayout { + #mode: TaskValidationLayout['mode']; + #validationFrames: TaskValidationLayout['validationFrames']; + #disabledFrames: TaskValidationLayout['disabledFrames']; + + public constructor(data: SerializedTaskValidationLayout) { + super(data); + this.#mode = data.mode; + this.#validationFrames = [...(data.validation_frames ?? [])]; + this.#disabledFrames = [...(data.disabled_frames ?? [])]; + } + + public get mode(): NonNullable { + return this.#mode; + } + + public get validationFrames(): number[] { + return [...this.#validationFrames]; + } + + public get disabledFrames(): number[] { + return [...this.#disabledFrames]; + } +} diff --git a/cvat-data/src/ts/3rdparty/README.md b/cvat-data/src/ts/3rdparty/README.md index 32ff0a20ab50..2bcd37af45b9 100644 --- a/cvat-data/src/ts/3rdparty/README.md +++ b/cvat-data/src/ts/3rdparty/README.md @@ -10,8 +10,8 @@ These files are from the [Broadway.js](https://github.com/mbebenita/Broadway) re Authors don't provide an npm package, so we need to store these components in our repository. We use this dependency to decode video chunks from a server and split them to frames on client side. -We need to run this package in node environent (for example for debug, or for running unit tests). -But there aren't any ways to do that (even with syntetic environment, provided for example by the package ``browser-env``). +We need to run this package in node environment (for example for debug, or for running unit tests). +But there aren't any ways to do that (even with synthetic environment, provided for example by the package ``browser-env``). For example there are issues with canvas using (webpack doesn't work with binary canvas package for node-js) and others. So, we have solved to write patch file for this library. It modifies source code a little to support our scenario of using. diff --git a/cvat-data/src/ts/unzip_imgs.worker.ts b/cvat-data/src/ts/unzip_imgs.worker.ts index 70d8299e7c38..4ca131a09955 100644 --- a/cvat-data/src/ts/unzip_imgs.worker.ts +++ b/cvat-data/src/ts/unzip_imgs.worker.ts @@ -34,7 +34,7 @@ onmessage = (e) => { .async('blob') .then((fileData) => { if (!errored) { - // do not need to read the rest of block if an error already occured + // do not need to read the rest of block if an error already occurred if (dimension === dimension2D) { createImageBitmap(fileData).then((img) => { postMessage({ diff --git a/cvat-sdk/gen/generate.sh b/cvat-sdk/gen/generate.sh index 553c1182f82b..b85479836a7f 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.21.1" +VERSION="2.21.2" 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 81b392eb7e54..2c43904a3fb9 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.66.1", + "version": "1.66.2", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/react_nginx.conf b/cvat-ui/react_nginx.conf index 5f1f4b48997a..6f9437ebbd75 100644 --- a/cvat-ui/react_nginx.conf +++ b/cvat-ui/react_nginx.conf @@ -1,7 +1,7 @@ server { root /usr/share/nginx/html; - # Disable server signature to make it slighty harder for + # Disable server signature to make it slightly harder for # attackers to find known vulnerabilities. See # https://datatracker.ietf.org/doc/html/rfc9110#name-server server_tokens off; diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 9e3eeb8176b3..670ace099e5a 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -12,7 +12,7 @@ import { } from 'cvat-canvas-wrapper'; import { getCore, MLModel, JobType, Job, QualityConflict, - ObjectState, JobState, ValidationLayout, + ObjectState, JobState, JobValidationLayout, } from 'cvat-core-wrapper'; import logger, { EventScope } from 'cvat-logger'; import { getCVATStore } from 'cvat-store'; @@ -38,7 +38,7 @@ interface AnnotationsParameters { showGroundTruth: boolean; jobInstance: Job; groundTruthInstance: Job | null; - validationLayout: ValidationLayout | null; + validationLayout: JobValidationLayout | null; } const cvat = getCore(); diff --git a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx index 9e6ff1f609c2..30811abad1cd 100644 --- a/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx +++ b/cvat-ui/src/components/annotation-page/standard-workspace/objects-side-bar/object-item.tsx @@ -118,7 +118,7 @@ function ObjectItemComponent(props: Props): JSX.Element { propagateShortcut={normalizedKeyMap.PROPAGATE_OBJECT} toBackgroundShortcut={normalizedKeyMap.TO_BACKGROUND} toForegroundShortcut={normalizedKeyMap.TO_FOREGROUND} - removeShortcut={normalizedKeyMap.DELETE_OBJECT} + removeShortcut={normalizedKeyMap.DELETE_OBJECT_STANDARD_WORKSPACE} changeColorShortcut={normalizedKeyMap.CHANGE_OBJECT_COLOR} sliceShortcut={normalizedKeyMap.SWITCH_SLICE_MODE} changeLabel={changeLabel} diff --git a/cvat-ui/src/components/header/header.tsx b/cvat-ui/src/components/header/header.tsx index 2621edddfa22..0feeae4be574 100644 --- a/cvat-ui/src/components/header/header.tsx +++ b/cvat-ui/src/components/header/header.tsx @@ -513,7 +513,7 @@ function HeaderComponent(props: Props): JSX.Element { Models ) : null} - {isAnalyticsPluginActive && user.isSuperuser ? ( + {isAnalyticsPluginActive && user.hasAnalyticsAccess ? (