From 56893f849c60fc5ffe56a985012cfdae5a8913b4 Mon Sep 17 00:00:00 2001 From: klakhov Date: Wed, 19 Apr 2023 11:05:07 +0300 Subject: [PATCH] added analytics page, empty quality page --- cvat-core/src/session.ts | 45 +++++---- .../components/actions-menu/actions-menu.tsx | 13 ++- cvat-ui/src/components/cvat-app.tsx | 2 + .../quality/empty-quality.tsx | 46 +++++++++ .../quality/task-quality-component.tsx | 28 ++++++ .../task-analytics-page/styles.scss | 22 +++++ .../task-analytics-page.tsx | 97 +++++++++++++++++++ cvat-ui/src/components/task-page/job-list.tsx | 6 +- cvat-ui/src/components/task-page/top-bar.tsx | 14 ++- .../src/components/tasks-page/task-item.tsx | 12 ++- .../containers/actions-menu/actions-menu.tsx | 5 +- 11 files changed, 259 insertions(+), 31 deletions(-) create mode 100644 cvat-ui/src/components/task-analytics-page/quality/empty-quality.tsx create mode 100644 cvat-ui/src/components/task-analytics-page/quality/task-quality-component.tsx create mode 100644 cvat-ui/src/components/task-analytics-page/styles.scss create mode 100644 cvat-ui/src/components/task-analytics-page/task-analytics-page.tsx diff --git a/cvat-core/src/session.ts b/cvat-core/src/session.ts index 91df825d6..721fd0d46 100644 --- a/cvat-core/src/session.ts +++ b/cvat-core/src/session.ts @@ -723,29 +723,28 @@ export class Task extends Session { if (Array.isArray(initialData.jobs)) { for (const job of initialData.jobs) { - if (job.type === JobType.NORMAL) { - const jobInstance = new Job({ - url: job.url, - id: job.id, - assignee: job.assignee, - state: job.state, - stage: job.stage, - start_frame: job.start_frame, - stop_frame: job.stop_frame, - - // following fields also returned when doing API request /jobs/ - // here we know them from task and append to constructor - task_id: data.id, - project_id: data.project_id, - labels: data.labels, - bug_tracker: data.bug_tracker, - mode: data.mode, - dimension: data.dimension, - data_compressed_chunk_type: data.data_compressed_chunk_type, - data_chunk_size: data.data_chunk_size, - }); - data.jobs.push(jobInstance); - } + const jobInstance = new Job({ + url: job.url, + id: job.id, + assignee: job.assignee, + state: job.state, + stage: job.stage, + type: job.type, + start_frame: job.start_frame, + stop_frame: job.stop_frame, + + // following fields also returned when doing API request /jobs/ + // here we know them from task and append to constructor + task_id: data.id, + project_id: data.project_id, + labels: data.labels, + bug_tracker: data.bug_tracker, + mode: data.mode, + dimension: data.dimension, + data_compressed_chunk_type: data.data_compressed_chunk_type, + data_chunk_size: data.data_chunk_size, + }); + data.jobs.push(jobInstance); } } diff --git a/cvat-ui/src/components/actions-menu/actions-menu.tsx b/cvat-ui/src/components/actions-menu/actions-menu.tsx index 0fe59c8d3..62426c332 100644 --- a/cvat-ui/src/components/actions-menu/actions-menu.tsx +++ b/cvat-ui/src/components/actions-menu/actions-menu.tsx @@ -35,6 +35,7 @@ export enum Actions { MOVE_TASK_TO_PROJECT = 'move_task_to_project', OPEN_BUG_TRACKER = 'open_bug_tracker', BACKUP_TASK = 'backup_task', + VIEW_ANALYTICS = 'view_analytics', } function ActionsMenuComponent(props: Props): JSX.Element { @@ -107,10 +108,18 @@ function ActionsMenuComponent(props: Props): JSX.Element { ), 40]); + menuItems.push([( + + View analytics + + ), 50]); + if (projectID === null) { menuItems.push([( Move to project - ), 50]); + ), 60]); } menuItems.push([( @@ -118,7 +127,7 @@ function ActionsMenuComponent(props: Props): JSX.Element { Delete - ), 60]); + ), 70]); menuItems.push( ...plugins.map(({ component: Component, weight }, index) => { diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 711908c1c..09f241600 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -74,6 +74,7 @@ import EmailVerificationSentPage from './email-confirmation-pages/email-verifica import IncorrectEmailConfirmationPage from './email-confirmation-pages/incorrect-email-confirmation'; import CreateModelPage from './create-model-page/create-model-page'; import CreateJobPage from './create-job-page/create-job-page'; +import TaskAnalyticsPage from './task-analytics-page/task-analytics-page'; interface CVATAppProps { loadFormats: () => void; @@ -461,6 +462,7 @@ class CVATApplication extends React.PureComponent + diff --git a/cvat-ui/src/components/task-analytics-page/quality/empty-quality.tsx b/cvat-ui/src/components/task-analytics-page/quality/empty-quality.tsx new file mode 100644 index 000000000..3f47ae189 --- /dev/null +++ b/cvat-ui/src/components/task-analytics-page/quality/empty-quality.tsx @@ -0,0 +1,46 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; + +import React from 'react'; +import { Link } from 'react-router-dom'; +import Text from 'antd/lib/typography/Text'; +import Empty from 'antd/lib/empty'; +import { Col, Row } from 'antd/lib/grid'; + +interface Props { + taskId: number, +} + +function EmptyQualityComponent(props: Props): JSX.Element { + const { taskId } = props; + + return ( +
+ + + + No Ground truth job created yet ... + + + + + To start viewing quality data + + + + + create a new one + + + + )} + /> +
+ ); +} + +export default React.memo(EmptyQualityComponent); diff --git a/cvat-ui/src/components/task-analytics-page/quality/task-quality-component.tsx b/cvat-ui/src/components/task-analytics-page/quality/task-quality-component.tsx new file mode 100644 index 000000000..8f1d2054b --- /dev/null +++ b/cvat-ui/src/components/task-analytics-page/quality/task-quality-component.tsx @@ -0,0 +1,28 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; + +import React from 'react'; +import { Job, JobType, Task } from 'cvat-core-wrapper'; +import EmptyQuality from './empty-quality'; + +interface Props { + task: Task, +} + +function TaskQualityComponent(props: Props): JSX.Element { + const { task } = props; + + const hasGTJob = task.jobs.some((job: Job) => job.type === JobType.GROUND_TRUTH); + if (!hasGTJob) { + return (); + } + + return ( +
hello
+ ); +} + +export default React.memo(TaskQualityComponent); diff --git a/cvat-ui/src/components/task-analytics-page/styles.scss b/cvat-ui/src/components/task-analytics-page/styles.scss new file mode 100644 index 000000000..61715f66f --- /dev/null +++ b/cvat-ui/src/components/task-analytics-page/styles.scss @@ -0,0 +1,22 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +@import '../../base.scss'; + +.cvat-analytics-inner { + background: $background-color-1; + min-height: $grid-unit-size * 95; + padding: $grid-unit-size * 2; + + .ant-tabs { + height: 100%; + } +} + +.cvat-task-quality-page-empty { + display: flex; + justify-content: center; + align-items: center; + height: $grid-unit-size * 68; +} diff --git a/cvat-ui/src/components/task-analytics-page/task-analytics-page.tsx b/cvat-ui/src/components/task-analytics-page/task-analytics-page.tsx new file mode 100644 index 000000000..5c49d1dca --- /dev/null +++ b/cvat-ui/src/components/task-analytics-page/task-analytics-page.tsx @@ -0,0 +1,97 @@ +// Copyright (C) 2023 CVAT.ai Corporation +// +// SPDX-License-Identifier: MIT + +import './styles.scss'; + +import React, { useEffect, useState } from 'react'; +import { Row, Col } from 'antd/lib/grid'; +import Tabs from 'antd/lib/tabs'; +import { useHistory, useParams } from 'react-router'; +import Text from 'antd/lib/typography/Text'; +import Spin from 'antd/lib/spin'; +import Button from 'antd/lib/button'; +import { Task } from 'reducers'; +import { notification } from 'antd'; +import { getCore } from 'cvat-core-wrapper'; +import { LeftOutlined } from '@ant-design/icons/lib/icons'; +import TaskQualityComponent from './quality/task-quality-component'; + +const core = getCore(); + +function TaskAnalyticsPage(): JSX.Element { + const [fetchingTask, setFetchingTask] = useState(true); + const [taskInstance, setTaskInstance] = useState(null); + + const history = useHistory(); + + const id = +useParams<{ id: string }>().id; + useEffect((): void => { + if (Number.isInteger(id)) { + core.tasks.get({ id }) + .then(([task]: Task[]) => { + if (task) { + setTaskInstance(task); + } + }).catch((error: Error) => { + notification.error({ + message: 'Could not fetch requested task from the server', + description: error.toString(), + }); + }).finally(() => { + setFetchingTask(false); + }); + } else { + notification.error({ + message: 'Could not receive the requested task from the server', + description: `Requested task id "${id}" is not valid`, + }); + setFetchingTask(false); + } + }, []); + return ( +
+ { + fetchingTask ? ( +
+ +
+ ) : ( + + + + + + + + Quality + + )} + key='quality' + > + + + + + + ) + } +
+ ); +} + +export default React.memo(TaskAnalyticsPage); diff --git a/cvat-ui/src/components/task-page/job-list.tsx b/cvat-ui/src/components/task-page/job-list.tsx index 16fe1b43b..75b68b28c 100644 --- a/cvat-ui/src/components/task-page/job-list.tsx +++ b/cvat-ui/src/components/task-page/job-list.tsx @@ -15,7 +15,7 @@ import Text from 'antd/lib/typography/Text'; import moment from 'moment'; import copy from 'copy-to-clipboard'; -import { Task, Job } from 'cvat-core-wrapper'; +import { Task, Job, JobType } from 'cvat-core-wrapper'; import { JobStage } from 'reducers'; import CVATTooltip from 'components/common/cvat-tooltip'; import UserSelector, { User } from './user-selector'; @@ -89,7 +89,9 @@ function JobListComponent(props: Props): JSX.Element { } = props; const history = useHistory(); - const { jobs, id: taskId } = taskInstance; + const { id: taskId } = taskInstance; + let { jobs } = taskInstance; + jobs = jobs.filter((job: Job) => job.type === JobType.NORMAL); function sorter(path: string) { return (obj1: any, obj2: any): number => { diff --git a/cvat-ui/src/components/task-page/top-bar.tsx b/cvat-ui/src/components/task-page/top-bar.tsx index 01542d619..6a365caf2 100644 --- a/cvat-ui/src/components/task-page/top-bar.tsx +++ b/cvat-ui/src/components/task-page/top-bar.tsx @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -import React from 'react'; +import React, { useCallback } from 'react'; import { useHistory } from 'react-router'; import { Row, Col } from 'antd/lib/grid'; import { LeftOutlined, MoreOutlined } from '@ant-design/icons'; @@ -21,6 +21,10 @@ export default function DetailsComponent(props: DetailsComponentProps): JSX.Elem const history = useHistory(); + const onViewAnalytics = useCallback(() => { + history.push(`/tasks/${taskInstance.id}/analytics`); + }, [history]); + return ( @@ -47,7 +51,13 @@ export default function DetailsComponent(props: DetailsComponentProps): JSX.Elem )} - }> + + )} + >