From cc10cb947a909455aee79e6e2c68f72d03141354 Mon Sep 17 00:00:00 2001 From: Maria Khrustaleva Date: Wed, 29 Nov 2023 14:14:07 +0100 Subject: [PATCH] Reduce the number of requests for task details (#7167) --- ..._reduce_number_of_task_details_requests.md | 4 + cvat-core/package.json | 2 +- cvat-core/src/api-implementation.ts | 10 +- cvat-core/src/project.ts | 24 ++-- cvat-core/src/server-response-types.ts | 19 +++- cvat-core/src/session.ts | 46 ++++++-- cvat-ui/package.json | 2 +- cvat-ui/src/actions/annotation-actions.ts | 5 +- .../export-backup/export-backup-modal.tsx | 4 +- .../export-dataset/export-dataset-modal.tsx | 28 +---- .../import-dataset/import-dataset-modal.tsx | 38 ++----- cvat/apps/engine/models.py | 6 + cvat/apps/engine/serializers.py | 29 +++-- cvat/schema.yml | 8 ++ tests/python/shared/assets/jobs.json | 106 ++++++++++++++++++ 15 files changed, 234 insertions(+), 97 deletions(-) create mode 100644 changelog.d/20231124_120151_maria_reduce_number_of_task_details_requests.md diff --git a/changelog.d/20231124_120151_maria_reduce_number_of_task_details_requests.md b/changelog.d/20231124_120151_maria_reduce_number_of_task_details_requests.md new file mode 100644 index 00000000000..3cb4ebf6561 --- /dev/null +++ b/changelog.d/20231124_120151_maria_reduce_number_of_task_details_requests.md @@ -0,0 +1,4 @@ +### Fixed + +- Reduce the number of requests to the server for task details + () diff --git a/cvat-core/package.json b/cvat-core/package.json index be71842b6ad..fcbf8fdf32d 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "12.1.0", + "version": "12.1.1", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "src/api.ts", "scripts": { diff --git a/cvat-core/src/api-implementation.ts b/cvat-core/src/api-implementation.ts index 78a0443648f..fedd1802eed 100644 --- a/cvat-core/src/api-implementation.ts +++ b/cvat-core/src/api-implementation.ts @@ -183,6 +183,8 @@ export default function implementAPI(cvat) { sort: isString, search: isString, jobID: isInteger, + taskID: isInteger, + type: isString, }); checkExclusiveFields(query, ['jobID', 'filter', 'search'], ['page', 'sort']); @@ -198,12 +200,16 @@ export default function implementAPI(cvat) { return []; } - const searchParams = {}; + const searchParams: Record = {}; + for (const key of Object.keys(query)) { - if (['page', 'sort', 'search', 'filter', 'task_id'].includes(key)) { + if (['page', 'sort', 'search', 'filter', 'type'].includes(key)) { searchParams[key] = query[key]; } } + if ('taskID' in query) { + searchParams.task_id = query.taskID; + } const jobsData = await serverProxy.jobs.get(searchParams); const jobs = jobsData.results.map((jobData) => new Job(jobData)); diff --git a/cvat-core/src/project.ts b/cvat-core/src/project.ts index 71ac6a07a7f..c4baf381a63 100644 --- a/cvat-core/src/project.ts +++ b/cvat-core/src/project.ts @@ -69,6 +69,16 @@ export default class Project { .map((labelData) => new Label(labelData)).filter((label) => !label.hasParent); } + data.source_storage = new Storage({ + location: initialData.source_storage?.location || StorageLocation.LOCAL, + cloudStorageId: initialData.source_storage?.cloud_storage_id, + }); + + data.target_storage = new Storage({ + location: initialData.target_storage?.location || StorageLocation.LOCAL, + cloudStorageId: initialData.target_storage?.cloud_storage_id, + }); + Object.defineProperties( this, Object.freeze({ @@ -173,20 +183,10 @@ export default class Project { get: () => [...data.task_subsets], }, sourceStorage: { - get: () => ( - new Storage({ - location: data.source_storage?.location || StorageLocation.LOCAL, - cloudStorageId: data.source_storage?.cloud_storage_id, - }) - ), + get: () => data.source_storage, }, targetStorage: { - get: () => ( - new Storage({ - location: data.target_storage?.location || StorageLocation.LOCAL, - cloudStorageId: data.target_storage?.cloud_storage_id, - }) - ), + get: () => data.target_storage, }, _internalData: { get: () => data, diff --git a/cvat-core/src/server-response-types.ts b/cvat-core/src/server-response-types.ts index aa107d05ffe..9d6c68c8b80 100644 --- a/cvat-core/src/server-response-types.ts +++ b/cvat-core/src/server-response-types.ts @@ -5,7 +5,7 @@ import { ChunkType, DimensionType, JobStage, JobState, JobType, ProjectStatus, - ShareFileType, TaskMode, TaskStatus, + ShareFileType, TaskMode, TaskStatus, StorageLocation, } from 'enums'; export interface SerializedAnnotationImporter { @@ -46,6 +46,12 @@ export interface SerializedUser { email_verification_required: boolean; } +interface SerializedStorage { + id: number; + location: StorageLocation; + cloud_storage_id: number | null; +} + export interface SerializedProject { assignee: SerializedUser | null; id: number; @@ -57,8 +63,8 @@ export interface SerializedProject { organization: number | null; guide_id: number | null; owner: SerializedUser; - source_storage: { id: number; location: 'local' | 'cloud'; cloud_storage_id: null }; - target_storage: { id: number; location: 'local' | 'cloud'; cloud_storage_id: null }; + source_storage: SerializedStorage | null; + target_storage: SerializedStorage | null; url: string; tasks: { count: number; url: string; }; task_subsets: string[]; @@ -68,6 +74,7 @@ export interface SerializedProject { export type TasksFilter = ProjectsFilter & { ordering?: string; }; // TODO: Need to clarify how "ordering" is used export type JobsFilter = ProjectsFilter & { task_id?: number; + type?: JobType; }; export interface SerializedTask { @@ -92,8 +99,8 @@ export interface SerializedTask { guide_id: number | null; segment_size: number; size: number; - source_storage: { id: number; location: 'local' | 'cloud'; cloud_storage_id: null }; - target_storage: { id: number; location: 'local' | 'cloud'; cloud_storage_id: null }; + source_storage: SerializedStorage | null; + target_storage: SerializedStorage | null; status: TaskStatus; subset: string; updated_date: string; @@ -122,6 +129,8 @@ export interface SerializedJob { updated_date: string; created_date: string; url: string; + source_storage: SerializedStorage | null; + target_storage: SerializedStorage | null; } export type AttrInputType = 'select' | 'radio' | 'checkbox' | 'number' | 'text'; diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts index 4f0e98e350c..7b8b127b551 100644 --- a/cvat-core/src/session.ts +++ b/cvat-core/src/session.ts @@ -357,6 +357,8 @@ export class Job extends Session { public readonly frameSelectionMethod: JobType; public readonly createdDate: string; public readonly updatedDate: string; + public readonly sourceStorage: Storage; + public readonly targetStorage: Storage; public annotations: { get: CallableFunction; @@ -425,6 +427,8 @@ export class Job extends Session { mode: undefined, created_date: undefined, updated_date: undefined, + source_storage: undefined, + target_storage: undefined, }; const updateTrigger = new FieldUpdateTrigger(); @@ -450,6 +454,16 @@ export class Job extends Session { }).filter((label) => !label.hasParent); } + data.source_storage = new Storage({ + location: initialData.source_storage?.location || StorageLocation.LOCAL, + cloudStorageId: initialData.source_storage?.cloud_storage_id, + }); + + data.target_storage = new Storage({ + location: initialData.target_storage?.location || StorageLocation.LOCAL, + cloudStorageId: initialData.target_storage?.cloud_storage_id, + }); + Object.defineProperties( this, Object.freeze({ @@ -567,6 +581,12 @@ export class Job extends Session { updatedDate: { get: () => data.updated_date, }, + sourceStorage: { + get: () => data.source_storage, + }, + targetStorage: { + get: () => data.target_storage, + }, _updateTrigger: { get: () => updateTrigger, }, @@ -816,6 +836,16 @@ export class Task extends Session { .map((labelData) => new Label(labelData)).filter((label) => !label.hasParent); } + data.source_storage = new Storage({ + location: initialData.source_storage?.location || StorageLocation.LOCAL, + cloudStorageId: initialData.source_storage?.cloud_storage_id, + }); + + data.target_storage = new Storage({ + location: initialData.target_storage?.location || StorageLocation.LOCAL, + cloudStorageId: initialData.target_storage?.cloud_storage_id, + }); + if (Array.isArray(initialData.jobs)) { for (const job of initialData.jobs) { const jobInstance = new Job({ @@ -842,6 +872,8 @@ export class Task extends Session { dimension: data.dimension, data_compressed_chunk_type: data.data_compressed_chunk_type, data_chunk_size: data.data_chunk_size, + target_storage: initialData.target_storage, + source_storage: initialData.source_storage, }); data.jobs.push(jobInstance); } @@ -1086,20 +1118,10 @@ export class Task extends Session { get: () => data.organization, }, sourceStorage: { - get: () => ( - new Storage({ - location: data.source_storage?.location || StorageLocation.LOCAL, - cloudStorageId: data.source_storage?.cloud_storage_id, - }) - ), + get: () => data.source_storage, }, targetStorage: { - get: () => ( - new Storage({ - location: data.target_storage?.location || StorageLocation.LOCAL, - cloudStorageId: data.target_storage?.cloud_storage_id, - }) - ), + get: () => data.target_storage, }, progress: { get: () => data.progress, diff --git a/cvat-ui/package.json b/cvat-ui/package.json index e4b2230a592..8994edd355f 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.59.0", + "version": "1.59.1", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index 3f471dffd83..6595dc64042 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -912,11 +912,10 @@ export function getJobAsync( ); const [job] = await cvat.jobs.get({ jobID: jid }); - let gtJob = null; + let gtJob: Job | null = null; if (job.type === JobType.ANNOTATION) { try { - const [task] = await cvat.tasks.get({ id: tid }); - [gtJob] = task.jobs.filter((_job: Job) => _job.type === JobType.GROUND_TRUTH); + [gtJob] = await cvat.jobs.get({ taskID: tid, type: JobType.GROUND_TRUTH }); // gtJob is not available for workers // eslint-disable-next-line no-empty } catch (e) { } diff --git a/cvat-ui/src/components/export-backup/export-backup-modal.tsx b/cvat-ui/src/components/export-backup/export-backup-modal.tsx index 8d00bfe02e9..805516030ea 100644 --- a/cvat-ui/src/components/export-backup/export-backup-modal.tsx +++ b/cvat-ui/src/components/export-backup/export-backup-modal.tsx @@ -61,8 +61,8 @@ function ExportBackupModal(): JSX.Element { useEffect(() => { if (instance) { - setDefaultStorageLocation(instance.targetStorage?.location || StorageLocation.LOCAL); - setDefaultStorageCloudId(instance.targetStorage?.cloudStorageId || null); + setDefaultStorageLocation(instance.targetStorage.location); + setDefaultStorageCloudId(instance.targetStorage.cloudStorageId); } }, [instance]); diff --git a/cvat-ui/src/components/export-dataset/export-dataset-modal.tsx b/cvat-ui/src/components/export-dataset/export-dataset-modal.tsx index 19d5b470604..89fdf151932 100644 --- a/cvat-ui/src/components/export-dataset/export-dataset-modal.tsx +++ b/cvat-ui/src/components/export-dataset/export-dataset-modal.tsx @@ -19,12 +19,9 @@ import TargetStorageField from 'components/storage/target-storage-field'; import { CombinedState, StorageLocation } from 'reducers'; import { exportActions, exportDatasetAsync } from 'actions/export-actions'; import { - Dumper, - getCore, Job, Project, Storage, StorageData, Task, + Dumper, Job, Project, Storage, StorageData, Task, } from 'cvat-core-wrapper'; -const core = getCore(); - type FormValues = { selectedFormat: string | undefined; saveImages: boolean; @@ -82,27 +79,8 @@ function ExportDatasetModal(props: StateToProps): JSX.Element { useEffect(() => { if (instance) { - if (instance instanceof Project || instance instanceof Task) { - setDefaultStorageLocation(instance.targetStorage?.location || StorageLocation.LOCAL); - setDefaultStorageCloudId(instance.targetStorage?.cloudStorageId); - } else { - core.tasks.get({ id: instance.taskId }) - .then((response: any) => { - if (response.length) { - const [taskInstance] = response; - setDefaultStorageLocation(taskInstance.targetStorage?.location || StorageLocation.LOCAL); - setDefaultStorageCloudId(taskInstance.targetStorage?.cloudStorageId); - } - }) - .catch((error: Error) => { - if ((error as any).code !== 403) { - Notification.error({ - message: `Could not fetch the task ${instance.taskId}`, - description: error.toString(), - }); - } - }); - } + setDefaultStorageLocation(instance.targetStorage.location); + setDefaultStorageCloudId(instance.targetStorage.cloudStorageId); } }, [instance]); diff --git a/cvat-ui/src/components/import-dataset/import-dataset-modal.tsx b/cvat-ui/src/components/import-dataset/import-dataset-modal.tsx index a2179053c3a..81310d4c47b 100644 --- a/cvat-ui/src/components/import-dataset/import-dataset-modal.tsx +++ b/cvat-ui/src/components/import-dataset/import-dataset-modal.tsx @@ -103,35 +103,21 @@ function ImportDatasetModal(props: StateToProps): JSX.Element { } as UploadParams); }, [resource, defaultStorageLocation, defaultStorageCloudId]); + const isProject = useCallback((): boolean => instance instanceof core.classes.Project, [instance]); + const isTask = useCallback((): boolean => instance instanceof core.classes.Task, [instance]); + useEffect(() => { if (instance) { - if (instance instanceof core.classes.Project || instance instanceof core.classes.Task) { - setDefaultStorageLocation(instance.sourceStorage?.location || StorageLocation.LOCAL); - setDefaultStorageCloudId(instance.sourceStorage?.cloudStorageId || null); - if (instance instanceof core.classes.Project) { - setInstanceType(`project #${instance.id}`); - } else { - setInstanceType(`task #${instance.id}`); - } - } else if (instance instanceof core.classes.Job) { - core.tasks.get({ id: instance.taskId }) - .then((response: any) => { - if (response.length) { - const [taskInstance] = response; - setDefaultStorageLocation(taskInstance.sourceStorage?.location || StorageLocation.LOCAL); - setDefaultStorageCloudId(taskInstance.sourceStorage?.cloudStorageId || null); - } - }) - .catch((error: Error) => { - if ((error as any).code !== 403) { - Notification.error({ - message: `Could not get task instance ${instance.taskId}`, - description: error.toString(), - }); - } - }); - setInstanceType(`job #${instance.id}`); + setDefaultStorageLocation(instance.sourceStorage.location); + setDefaultStorageCloudId(instance.sourceStorage.cloudStorageId); + let type: 'project' | 'task' | 'job' = 'job'; + + if (isProject()) { + type = 'project'; + } else if (isTask()) { + type = 'task'; } + setInstanceType(`${type} #${instance.id}`); } }, [instance, resource]); diff --git a/cvat/apps/engine/models.py b/cvat/apps/engine/models.py index 8c6fbf4c395..0ed04d442a0 100644 --- a/cvat/apps/engine/models.py +++ b/cvat/apps/engine/models.py @@ -679,6 +679,12 @@ class Job(models.Model): type = models.CharField(max_length=32, choices=JobType.choices(), default=JobType.ANNOTATION) + def get_target_storage(self) -> Optional[Storage]: + return self.segment.task.target_storage + + def get_source_storage(self) -> Optional[Storage]: + return self.segment.task.source_storage + def get_dirname(self): return os.path.join(settings.JOBS_ROOT, str(self.id)) diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index fe643d2c790..01d9f5c2f88 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -23,6 +23,7 @@ from cvat.apps.engine.cloud_provider import get_cloud_storage_instance, Credentials, Status from cvat.apps.engine.log import ServerLogManager from cvat.apps.engine.utils import parse_specific_attributes, build_field_filter_params, get_list_view_name, reverse +from cvat.apps.iam.permissions import TaskPermission from drf_spectacular.utils import OpenApiExample, extend_schema_field, extend_schema_serializer @@ -540,6 +541,12 @@ def update(self, instance, validated_data): self.instance = models.Label.objects.get(pk=instance.pk) return self.instance +class StorageSerializer(serializers.ModelSerializer): + cloud_storage_id = serializers.IntegerField(required=False, allow_null=True) + + class Meta: + model = models.Storage + fields = ('id', 'location', 'cloud_storage_id') class JobReadSerializer(serializers.ModelSerializer): task_id = serializers.ReadOnlyField(source="segment.task.id") @@ -558,19 +565,32 @@ class JobReadSerializer(serializers.ModelSerializer): allow_null=True, read_only=True) labels = LabelsSummarySerializer(source='*') issues = IssuesSummarySerializer(source='*') + target_storage = StorageSerializer(required=False, allow_null=True) + source_storage = StorageSerializer(required=False, allow_null=True) class Meta: model = models.Job fields = ('url', 'id', 'task_id', 'project_id', 'assignee', 'guide_id', 'dimension', 'bug_tracker', 'status', 'stage', 'state', 'mode', 'frame_count', 'start_frame', 'stop_frame', 'data_chunk_size', 'data_compressed_chunk_type', - 'created_date', 'updated_date', 'issues', 'labels', 'type', 'organization') + 'created_date', 'updated_date', 'issues', 'labels', 'type', 'organization', + 'target_storage', 'source_storage') read_only_fields = fields def to_representation(self, instance): data = super().to_representation(instance) if instance.segment.type == models.SegmentType.SPECIFIC_FRAMES: data['data_compressed_chunk_type'] = models.DataChoice.IMAGESET + + if request := self.context.get('request'): + perm = TaskPermission.create_scope_view(request, instance.segment.task) + result = perm.check_access() + if result.allow: + if task_source_storage := instance.get_source_storage(): + data['source_storage'] = StorageSerializer(task_source_storage).data + if task_target_storage := instance.get_target_storage(): + data['target_storage'] = StorageSerializer(task_target_storage).data + return data @@ -1036,13 +1056,6 @@ def _create_files(self, instance, files): files_model(data=instance, **f) for f in files[files_type] ) -class StorageSerializer(serializers.ModelSerializer): - cloud_storage_id = serializers.IntegerField(required=False, allow_null=True) - - class Meta: - model = models.Storage - fields = ('id', 'location', 'cloud_storage_id') - class TaskReadSerializer(serializers.ModelSerializer): data_chunk_size = serializers.ReadOnlyField(source='data.chunk_size', required=False) data_compressed_chunk_type = serializers.ReadOnlyField(source='data.compressed_chunk_type', required=False) diff --git a/cvat/schema.yml b/cvat/schema.yml index e9bec0b7eca..de644533dbc 100644 --- a/cvat/schema.yml +++ b/cvat/schema.yml @@ -7551,6 +7551,14 @@ components: type: integer readOnly: true nullable: true + target_storage: + allOf: + - $ref: '#/components/schemas/Storage' + nullable: true + source_storage: + allOf: + - $ref: '#/components/schemas/Storage' + nullable: true required: - issues - labels diff --git a/tests/python/shared/assets/jobs.json b/tests/python/shared/assets/jobs.json index 3a628d38df8..3c01eb45773 100644 --- a/tests/python/shared/assets/jobs.json +++ b/tests/python/shared/assets/jobs.json @@ -23,11 +23,13 @@ "mode": "annotation", "organization": 2, "project_id": null, + "source_storage": null, "stage": "acceptance", "start_frame": 0, "state": "completed", "status": "validation", "stop_frame": 10, + "target_storage": null, "task_id": 22, "type": "ground_truth", "updated_date": "2023-11-24T15:18:55.216000Z", @@ -53,11 +55,13 @@ "mode": "annotation", "organization": 2, "project_id": null, + "source_storage": null, "stage": "annotation", "start_frame": 0, "state": "new", "status": "validation", "stop_frame": 10, + "target_storage": null, "task_id": 22, "type": "annotation", "updated_date": "2023-11-24T15:23:30.269000Z", @@ -83,11 +87,21 @@ "mode": "annotation", "organization": null, "project_id": null, + "source_storage": { + "cloud_storage_id": null, + "id": 33, + "location": "local" + }, "stage": "annotation", "start_frame": 6, "state": "in progress", "status": "annotation", "stop_frame": 9, + "target_storage": { + "cloud_storage_id": null, + "id": 34, + "location": "local" + }, "task_id": 21, "type": "annotation", "updated_date": "2023-03-27T19:08:40.166000Z", @@ -113,11 +127,21 @@ "mode": "annotation", "organization": null, "project_id": null, + "source_storage": { + "cloud_storage_id": null, + "id": 33, + "location": "local" + }, "stage": "annotation", "start_frame": 0, "state": "in progress", "status": "annotation", "stop_frame": 5, + "target_storage": { + "cloud_storage_id": null, + "id": 34, + "location": "local" + }, "task_id": 21, "type": "annotation", "updated_date": "2023-03-27T19:08:27.216000Z", @@ -143,11 +167,21 @@ "mode": "annotation", "organization": null, "project_id": 12, + "source_storage": { + "cloud_storage_id": null, + "id": 29, + "location": "local" + }, "stage": "annotation", "start_frame": 0, "state": "in progress", "status": "annotation", "stop_frame": 1, + "target_storage": { + "cloud_storage_id": null, + "id": 30, + "location": "local" + }, "task_id": 20, "type": "annotation", "updated_date": "2023-03-10T11:57:49.019000Z", @@ -173,11 +207,21 @@ "mode": "annotation", "organization": null, "project_id": null, + "source_storage": { + "cloud_storage_id": null, + "id": 25, + "location": "local" + }, "stage": "annotation", "start_frame": 0, "state": "in progress", "status": "annotation", "stop_frame": 1, + "target_storage": { + "cloud_storage_id": null, + "id": 26, + "location": "local" + }, "task_id": 19, "type": "annotation", "updated_date": "2023-03-10T11:56:55.066000Z", @@ -203,11 +247,21 @@ "mode": "annotation", "organization": 2, "project_id": 11, + "source_storage": { + "cloud_storage_id": null, + "id": 23, + "location": "local" + }, "stage": "annotation", "start_frame": 0, "state": "in progress", "status": "annotation", "stop_frame": 1, + "target_storage": { + "cloud_storage_id": null, + "id": 24, + "location": "local" + }, "task_id": 18, "type": "annotation", "updated_date": "2023-03-01T15:36:38.114000Z", @@ -233,11 +287,13 @@ "mode": "annotation", "organization": 2, "project_id": null, + "source_storage": null, "stage": "annotation", "start_frame": 0, "state": "new", "status": "annotation", "stop_frame": 4, + "target_storage": null, "task_id": 17, "type": "annotation", "updated_date": "2023-02-10T14:05:26.022000Z", @@ -263,11 +319,21 @@ "mode": "interpolation", "organization": null, "project_id": 8, + "source_storage": { + "cloud_storage_id": null, + "id": 15, + "location": "local" + }, "stage": "annotation", "start_frame": 0, "state": "in progress", "status": "annotation", "stop_frame": 24, + "target_storage": { + "cloud_storage_id": null, + "id": 16, + "location": "local" + }, "task_id": 15, "type": "annotation", "updated_date": "2022-12-01T12:53:35.354000Z", @@ -293,11 +359,21 @@ "mode": "annotation", "organization": 2, "project_id": 5, + "source_storage": { + "cloud_storage_id": null, + "id": 7, + "location": "local" + }, "stage": "annotation", "start_frame": 0, "state": "in progress", "status": "annotation", "stop_frame": 7, + "target_storage": { + "cloud_storage_id": null, + "id": 8, + "location": "local" + }, "task_id": 14, "type": "annotation", "updated_date": "2022-09-23T11:57:02.302000Z", @@ -323,11 +399,13 @@ "mode": "annotation", "organization": 2, "project_id": 4, + "source_storage": null, "stage": "annotation", "start_frame": 0, "state": "in progress", "status": "annotation", "stop_frame": 4, + "target_storage": null, "task_id": 13, "type": "annotation", "updated_date": "2022-12-05T07:47:01.633000Z", @@ -359,11 +437,21 @@ "mode": "annotation", "organization": 2, "project_id": 2, + "source_storage": { + "cloud_storage_id": 2, + "id": 4, + "location": "cloud_storage" + }, "stage": "annotation", "start_frame": 0, "state": "in progress", "status": "annotation", "stop_frame": 10, + "target_storage": { + "cloud_storage_id": 2, + "id": 2, + "location": "cloud_storage" + }, "task_id": 11, "type": "annotation", "updated_date": "2022-06-22T09:18:45.296000Z", @@ -389,11 +477,13 @@ "mode": "annotation", "organization": null, "project_id": 1, + "source_storage": null, "stage": "annotation", "start_frame": 15, "state": "in progress", "status": "annotation", "stop_frame": 19, + "target_storage": null, "task_id": 9, "type": "annotation", "updated_date": "2022-11-03T13:57:26.346000Z", @@ -419,11 +509,13 @@ "mode": "annotation", "organization": null, "project_id": 1, + "source_storage": null, "stage": "acceptance", "start_frame": 10, "state": "new", "status": "validation", "stop_frame": 14, + "target_storage": null, "task_id": 9, "type": "annotation", "updated_date": "2022-06-22T09:18:45.296000Z", @@ -449,11 +541,13 @@ "mode": "annotation", "organization": null, "project_id": 1, + "source_storage": null, "stage": "validation", "start_frame": 5, "state": "new", "status": "validation", "stop_frame": 9, + "target_storage": null, "task_id": 9, "type": "annotation", "updated_date": "2022-06-22T09:18:45.296000Z", @@ -485,11 +579,13 @@ "mode": "annotation", "organization": null, "project_id": 1, + "source_storage": null, "stage": "annotation", "start_frame": 0, "state": "in progress", "status": "annotation", "stop_frame": 4, + "target_storage": null, "task_id": 9, "type": "annotation", "updated_date": "2022-06-22T09:18:45.296000Z", @@ -521,11 +617,13 @@ "mode": "annotation", "organization": null, "project_id": null, + "source_storage": null, "stage": "annotation", "start_frame": 0, "state": "in progress", "status": "annotation", "stop_frame": 13, + "target_storage": null, "task_id": 8, "type": "annotation", "updated_date": "2022-06-22T09:18:45.296000Z", @@ -551,11 +649,13 @@ "mode": "annotation", "organization": 2, "project_id": null, + "source_storage": null, "stage": "annotation", "start_frame": 0, "state": "in progress", "status": "annotation", "stop_frame": 10, + "target_storage": null, "task_id": 7, "type": "annotation", "updated_date": "2022-06-22T09:18:45.296000Z", @@ -581,11 +681,13 @@ "mode": "annotation", "organization": null, "project_id": null, + "source_storage": null, "stage": "annotation", "start_frame": 0, "state": "new", "status": "annotation", "stop_frame": 0, + "target_storage": null, "task_id": 6, "type": "annotation", "updated_date": "2022-06-22T09:18:45.296000Z", @@ -617,11 +719,13 @@ "mode": "interpolation", "organization": null, "project_id": null, + "source_storage": null, "stage": "annotation", "start_frame": 0, "state": "in progress", "status": "annotation", "stop_frame": 24, + "target_storage": null, "task_id": 5, "type": "annotation", "updated_date": "2022-06-22T09:18:45.296000Z", @@ -653,11 +757,13 @@ "mode": "annotation", "organization": 1, "project_id": null, + "source_storage": null, "stage": "annotation", "start_frame": 0, "state": "new", "status": "annotation", "stop_frame": 22, + "target_storage": null, "task_id": 2, "type": "annotation", "updated_date": "2022-06-22T09:18:45.296000Z",