From 0c600e7e65893c31a3496f802e3806f785d35944 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 26 Jan 2022 11:51:48 +0300 Subject: [PATCH 1/9] Added core functionality --- cvat-core/src/api-implementation.js | 22 ++++---- cvat-core/src/frames.js | 4 +- cvat-core/src/server-proxy.js | 23 +++++--- cvat-core/src/session.js | 10 +++- cvat-ui/src/actions/jobs-actions.ts | 44 +++++++++++++++ cvat-ui/src/components/cvat-app.tsx | 5 +- cvat-ui/src/components/header/header.tsx | 14 ++++- cvat-ui/src/components/jobs-page/job-card.tsx | 3 + .../src/components/jobs-page/jobs-content.tsx | 3 + .../src/components/jobs-page/jobs-page.tsx | 11 ++++ cvat-ui/src/components/jobs-page/top-bar.tsx | 3 + cvat-ui/src/reducers/interfaces.ts | 21 ++++++- cvat-ui/src/reducers/jobs-reducer.ts | 55 +++++++++++++++++++ cvat-ui/src/reducers/root-reducer.ts | 4 +- 14 files changed, 197 insertions(+), 25 deletions(-) create mode 100644 cvat-ui/src/actions/jobs-actions.ts create mode 100644 cvat-ui/src/components/jobs-page/job-card.tsx create mode 100644 cvat-ui/src/components/jobs-page/jobs-content.tsx create mode 100644 cvat-ui/src/components/jobs-page/jobs-page.tsx create mode 100644 cvat-ui/src/components/jobs-page/top-bar.tsx create mode 100644 cvat-ui/src/reducers/jobs-reducer.ts diff --git a/cvat-core/src/api-implementation.js b/cvat-core/src/api-implementation.js index 98a962b24bf..f75037d8ab2 100644 --- a/cvat-core/src/api-implementation.js +++ b/cvat-core/src/api-implementation.js @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2021 Intel Corporation +// Copyright (C) 2019-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -152,16 +152,13 @@ const config = require('./config'); cvat.jobs.get.implementation = async (filter) => { checkFilter(filter, { + page: isInteger, taskID: isInteger, jobID: isInteger, }); if ('taskID' in filter && 'jobID' in filter) { - throw new ArgumentError('Only one of fields "taskID" and "jobID" allowed simultaneously'); - } - - if (!Object.keys(filter).length) { - throw new ArgumentError('Job filter must not be empty'); + throw new ArgumentError('Filter fields "taskID" and "jobID" are not permitted to be used at the same time'); } if ('taskID' in filter) { @@ -173,12 +170,17 @@ const config = require('./config'); return []; } - const job = await serverProxy.jobs.get(filter.jobID); - if (job) { - return [new Job(job)]; + if ('jobID' in filter) { + const job = await serverProxy.jobs.get(filter.jobID); + if (job) { + return [new Job(job)]; + } } - return []; + const jobsData = await serverProxy.jobs.get(filter.jobID); + const jobs = jobsData.results.map((jobData) => new Job(jobData)); + jobs.count = jobsData.count; + return jobs; }; cvat.tasks.get.implementation = async (filter) => { diff --git a/cvat-core/src/frames.js b/cvat-core/src/frames.js index f9c5b2e4f6d..fef97285892 100644 --- a/cvat-core/src/frames.js +++ b/cvat-core/src/frames.js @@ -637,11 +637,11 @@ return frameDataCache[taskID].frameBuffer.getContextImage(frame); } - async function getPreview(taskID) { + async function getPreview(taskID = null, jobID = null) { return new Promise((resolve, reject) => { // Just go to server and get preview (no any cache) serverProxy.frames - .getPreview(taskID) + .getPreview(taskID, jobID) .then((result) => { if (isNode) { // eslint-disable-next-line no-undef diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js index 40786a634eb..55198c648d8 100644 --- a/cvat-core/src/server-proxy.js +++ b/cvat-core/src/server-proxy.js @@ -1,4 +1,4 @@ -// Copyright (C) 2019-2021 Intel Corporation +// Copyright (C) 2019-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -924,14 +924,20 @@ return createdTask[0]; } - async function getJob(jobID) { + async function getJobs(jobID = null) { const { backendAPI } = config; let response = null; try { - response = await Axios.get(`${backendAPI}/jobs/${jobID}`, { - proxy: config.proxy, - }); + if (jobID !== null) { + response = await Axios.get(`${backendAPI}/jobs/${jobID}`, { + proxy: config.proxy, + }); + } else { + response = await Axios.get(`${backendAPI}/jobs`, { + proxy: config.proxy, + }); + } } catch (errorData) { throw generateError(errorData); } @@ -1069,12 +1075,13 @@ return response.data; } - async function getPreview(tid) { + async function getPreview(tid, jid) { const { backendAPI } = config; let response = null; try { - response = await Axios.get(`${backendAPI}/tasks/${tid}/data`, { + const url = `${backendAPI}/${jid !== null ? 'jobs' : 'tasks'}/${jid || tid}/data`; + response = await Axios.get(url, { params: { type: 'preview', }, @@ -1800,7 +1807,7 @@ jobs: { value: Object.freeze({ - get: getJob, + get: getJobs, save: saveJob, }), writable: false, diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index 84ca252ade7..f1974a65ed8 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -1887,7 +1887,11 @@ }; Job.prototype.frames.preview.implementation = async function () { - const frameData = await getPreview(this.taskId); + if (!this.jobID || !this.taskId) { + return ''; + } + + const frameData = await getPreview(this.taskId, this.jobID); return frameData; }; @@ -2220,6 +2224,10 @@ }; Task.prototype.frames.preview.implementation = async function () { + if (!this.id) { + return ''; + } + const frameData = await getPreview(this.id); return frameData; }; diff --git a/cvat-ui/src/actions/jobs-actions.ts b/cvat-ui/src/actions/jobs-actions.ts new file mode 100644 index 00000000000..22919325c3d --- /dev/null +++ b/cvat-ui/src/actions/jobs-actions.ts @@ -0,0 +1,44 @@ +// Copyright (C) 2022 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; +import getCore from 'cvat-core-wrapper'; +import { JobsQuery } from 'reducers/interfaces'; + +const cvat = getCore(); + +export enum JobsActionTypes { + GET_JOBS = 'GET_JOBS', + GET_JOBS_SUCCESS = 'GET_JOBS_SUCCESS', + GET_JOBS_FAILED = 'GET_JOBS_FAILED', +} + +const jobsActions = { + getJobs: (query: Partial) => createAction(JobsActionTypes.GET_JOBS, { query }), + getJobsSuccess: (jobs: any[], previews: string[]) => ( + createAction(JobsActionTypes.GET_JOBS_SUCCESS, { jobs, previews }) + ), + getJobsFailed: (error: any) => createAction(JobsActionTypes.GET_JOBS_FAILED, { error }), +}; + +export type JobsActions = ActionUnion; + +export const getJobsAsync = (query: JobsQuery): ThunkAction => async (dispatch) => { + try { + // Remove all keys with null values from the query + const filteredQuery: Partial = { ...query }; + for (const [key, value] of Object.entries(filteredQuery)) { + if (value === null) { + delete filteredQuery[key]; + } + } + + dispatch(jobsActions.getJobs(filteredQuery)); + const jobs = cvat.jobs.get(filteredQuery); + const previewPromises = jobs.map((job: any) => (job as any).frames.preview().catch(() => '')); + dispatch(jobsActions.getJobsSuccess(jobs, await Promise.all(previewPromises))); + } catch (error) { + dispatch(jobsActions.getJobsFailed(error)); + } +}; diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 6d0c57182b8..a155061c941 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Intel Corporation +// Copyright (C) 2020-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -26,6 +26,8 @@ import ShortcutsDialog from 'components/shortcuts-dialog/shortcuts-dialog'; import ExportDatasetModal from 'components/export-dataset/export-dataset-modal'; import ModelsPageContainer from 'containers/models-page/models-page'; +import JobsPageComponent from 'components/jobs-page/jobs-page'; + import TasksPageContainer from 'containers/tasks-page/tasks-page'; import CreateTaskPageContainer from 'containers/create-task-page/create-task-page'; import TaskPageContainer from 'containers/task-page/task-page'; @@ -360,6 +362,7 @@ class CVATApplication extends React.PureComponent + { event.preventDefault(); history.push('/projects'); @@ -398,6 +398,18 @@ function HeaderContainer(props: Props): JSX.Element { > Tasks + + + ), + }, + ]; -function TopBarComponent(): JSX.Element { return ( @@ -14,6 +81,20 @@ function TopBarComponent(): JSX.Element { Jobs + ) => { + const processed = Object.fromEntries( + Object.entries(filters) + .map(([key, values]) => ( + [key, typeof values === 'string' || values === null ? values : values.join(',')] + )), + ); + onChangeFilters(processed); + }} + className='cvat-jobs-page-filters' + columns={columns} + size='small' + /> diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 3ebfcc9dd04..a59f6a82f57 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -373,6 +373,7 @@ export interface NotificationsState { }; jobs: { updating: null | ErrorState; + fetching: null | ErrorState; }; formats: { fetching: null | ErrorState; diff --git a/cvat-ui/src/reducers/notifications-reducer.ts b/cvat-ui/src/reducers/notifications-reducer.ts index 788eb4366f9..7ed9b5286a2 100644 --- a/cvat-ui/src/reducers/notifications-reducer.ts +++ b/cvat-ui/src/reducers/notifications-reducer.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Intel Corporation +// Copyright (C) 2020-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -20,6 +20,7 @@ import { ExportActionTypes } from 'actions/export-actions'; import { ImportActionTypes } from 'actions/import-actions'; import { CloudStorageActionTypes } from 'actions/cloud-storage-actions'; import { OrganizationActionsTypes } from 'actions/organization-actions'; +import { JobsActionTypes } from 'actions/jobs-actions'; import getCore from 'cvat-core-wrapper'; import { NotificationsState } from './interfaces'; @@ -60,6 +61,7 @@ const defaultState: NotificationsState = { }, jobs: { updating: null, + fetching: null, }, formats: { fetching: null, @@ -1577,6 +1579,22 @@ export default function (state = defaultState, action: AnyAction): Notifications }, }; } + case JobsActionTypes.GET_JOBS_FAILED: { + return { + ...state, + errors: { + ...state.errors, + jobs: { + ...state.errors.jobs, + fetching: { + message: 'Could not fetch a list of jobs', + reason: action.payload.error.toString(), + className: 'cvat-notification-notice-update-organization-membership-failed', + }, + }, + }, + }; + } case BoundariesActionTypes.RESET_AFTER_ERROR: case AuthActionTypes.LOGOUT_SUCCESS: { return { ...defaultState }; diff --git a/cvat/apps/engine/views.py b/cvat/apps/engine/views.py index 50cf67cb4b0..bb9847c05b2 100644 --- a/cvat/apps/engine/views.py +++ b/cvat/apps/engine/views.py @@ -879,6 +879,18 @@ def dataset_export(self, request, pk): filename=request.query_params.get("filename", "").lower(), ) +class CharInFilter(filters.BaseInFilter, filters.CharFilter): + pass + +class JobFilter(filters.FilterSet): + assignee = filters.CharFilter(field_name="assignee__username", lookup_expr="icontains") + stage = CharInFilter(field_name="stage", lookup_expr="in") + state = CharInFilter(field_name="state", lookup_expr="in") + + class Meta: + model = Job + fields = ("assignee", ) + @method_decorator(name='retrieve', decorator=swagger_auto_schema(operation_summary='Method returns details of a job')) @method_decorator(name='update', decorator=swagger_auto_schema(operation_summary='Method updates a job by id')) @method_decorator(name='partial_update', decorator=swagger_auto_schema( @@ -886,6 +898,7 @@ def dataset_export(self, request, pk): class JobViewSet(viewsets.GenericViewSet, mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin): queryset = Job.objects.all().order_by('id') + filterset_class = JobFilter iam_organization_field = 'segment__task__organization' def get_queryset(self): From 4e27a7f8f6465f69039907f4815bd99139bbc6b0 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Fri, 28 Jan 2022 17:40:07 +0300 Subject: [PATCH 5/9] Hide extra info by default --- cvat-ui/src/components/jobs-page/job-card.tsx | 19 ++++++++++++------- cvat-ui/src/components/jobs-page/styles.scss | 4 ++++ cvat-ui/src/components/jobs-page/top-bar.tsx | 2 +- 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/cvat-ui/src/components/jobs-page/job-card.tsx b/cvat-ui/src/components/jobs-page/job-card.tsx index 001f2b5ddfe..06e8a960e2c 100644 --- a/cvat-ui/src/components/jobs-page/job-card.tsx +++ b/cvat-ui/src/components/jobs-page/job-card.tsx @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: MIT -import React from 'react'; +import React, { useState } from 'react'; import { useHistory } from 'react-router'; import Card from 'antd/lib/card'; import Empty from 'antd/lib/empty'; @@ -29,6 +29,7 @@ interface Props { function JobCardComponent(props: Props): JSX.Element { const { job, preview } = props; + const [expanded, setExpanded] = useState(false); const history = useHistory(); const height = useCardHeight(); const onClick = (): void => { @@ -37,6 +38,8 @@ function JobCardComponent(props: Props): JSX.Element { return ( setExpanded(true)} + onMouseLeave={() => setExpanded(false)} style={{ height }} className='cvat-job-page-list-item' cover={( @@ -62,12 +65,14 @@ function JobCardComponent(props: Props): JSX.Element { )} > - - {job.stage} - {job.stopFrame - job.startFrame + 1} - {job.state} - { job.assignee ? ( - {job.assignee.username} + + {job.stage} + {job.state} + { expanded ? ( + {job.stopFrame - job.startFrame + 1} + ) : null} + { expanded && job.assignee ? ( + {job.assignee.username} ) : null} From cf4a3667aabf0acc6124701c89ce5fb2dbc35ed8 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Fri, 28 Jan 2022 17:43:38 +0300 Subject: [PATCH 6/9] Removed extra css rule --- cvat-ui/src/components/header/styles.scss | 2 -- 1 file changed, 2 deletions(-) diff --git a/cvat-ui/src/components/header/styles.scss b/cvat-ui/src/components/header/styles.scss index c4d6952dd4e..d77e09ae096 100644 --- a/cvat-ui/src/components/header/styles.scss +++ b/cvat-ui/src/components/header/styles.scss @@ -27,8 +27,6 @@ align-items: center; > a.ant-btn { - height: 24px; - span[role='img'] { font-size: 24px; line-height: 24px; From 3c43cd538e16ec0e13dfc6fce50a0ed0019d825b Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Fri, 28 Jan 2022 17:54:53 +0300 Subject: [PATCH 7/9] Added empty --- .../src/components/jobs-page/jobs-page.tsx | 55 +++++++++++-------- cvat-ui/src/components/jobs-page/styles.scss | 6 ++ 2 files changed, 39 insertions(+), 22 deletions(-) diff --git a/cvat-ui/src/components/jobs-page/jobs-page.tsx b/cvat-ui/src/components/jobs-page/jobs-page.tsx index c1b3ba0b9b1..0bd76b12803 100644 --- a/cvat-ui/src/components/jobs-page/jobs-page.tsx +++ b/cvat-ui/src/components/jobs-page/jobs-page.tsx @@ -9,6 +9,7 @@ import { useDispatch, useSelector } from 'react-redux'; import Spin from 'antd/lib/spin'; import { Col, Row } from 'antd/lib/grid'; import Pagination from 'antd/lib/pagination'; +import Empty from 'antd/lib/empty'; import { CombinedState } from 'reducers/interfaces'; import { getJobsAsync } from 'actions/jobs-actions'; @@ -28,9 +29,14 @@ function JobsPageComponent(): JSX.Element { const { location } = history; const searchParams = new URLSearchParams(location.search); const copiedQuery = { ...query }; - for (const [key, value] of searchParams.entries()) { - if (key in copiedQuery) { - copiedQuery[key] = key === 'page' ? +value : value; + for (const key of Object.keys(copiedQuery)) { + if (searchParams.has(key)) { + const value = searchParams.get(key); + if (value) { + copiedQuery[key] = key === 'page' ? +value : value; + } + } else { + copiedQuery[key] = null; } } @@ -79,25 +85,30 @@ function JobsPageComponent(): JSX.Element { ); }} /> - - - - { - dispatch(getJobsAsync({ - ...query, - page, - })); - }} - showSizeChanger={false} - total={count} - pageSize={12} - current={query.page} - showQuickJumper - /> - - + {count ? ( + <> + + + + { + dispatch(getJobsAsync({ + ...query, + page, + })); + }} + showSizeChanger={false} + total={count} + pageSize={12} + current={query.page} + showQuickJumper + /> + + + + ) : } + ); } diff --git a/cvat-ui/src/components/jobs-page/styles.scss b/cvat-ui/src/components/jobs-page/styles.scss index 089e47710e1..52d6df08ba3 100644 --- a/cvat-ui/src/components/jobs-page/styles.scss +++ b/cvat-ui/src/components/jobs-page/styles.scss @@ -35,6 +35,12 @@ } > div:nth-child(2) { + &.ant-empty { + position: absolute; + top: 40%; + left: 50%; + } + padding-bottom: $grid-unit-size; padding-top: $grid-unit-size; } From f5cba0df1aaf7eca3d03e9172d97468cf7087b1e Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 31 Jan 2022 14:00:18 +0300 Subject: [PATCH 8/9] Fixed minor comments --- cvat-core/src/api-implementation.js | 4 ++-- cvat-core/src/server-proxy.js | 7 ++++--- cvat-ui/src/base.scss | 1 - cvat-ui/src/components/jobs-page/styles.scss | 3 +-- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/cvat-core/src/api-implementation.js b/cvat-core/src/api-implementation.js index 8e82f2ae47f..e0245df766f 100644 --- a/cvat-core/src/api-implementation.js +++ b/cvat-core/src/api-implementation.js @@ -174,13 +174,13 @@ const config = require('./config'); } if ('jobID' in filter) { - const job = await serverProxy.jobs.get(filter.jobID); + const job = await serverProxy.jobs.get({ id: filter.jobID }); if (job) { return [new Job(job)]; } } - const jobsData = await serverProxy.jobs.get(null, filter); + const jobsData = await serverProxy.jobs.get(filter); const jobs = jobsData.results.map((jobData) => new Job(jobData)); jobs.count = jobsData.count; return jobs; diff --git a/cvat-core/src/server-proxy.js b/cvat-core/src/server-proxy.js index be9b4235d6e..ca46f12d5f7 100644 --- a/cvat-core/src/server-proxy.js +++ b/cvat-core/src/server-proxy.js @@ -924,13 +924,14 @@ return createdTask[0]; } - async function getJobs(jobID = null, filter = {}) { + async function getJobs(filter = {}) { const { backendAPI } = config; + const id = filter.id || null; let response = null; try { - if (jobID !== null) { - response = await Axios.get(`${backendAPI}/jobs/${jobID}`, { + if (id !== null) { + response = await Axios.get(`${backendAPI}/jobs/${id}`, { proxy: config.proxy, }); } else { diff --git a/cvat-ui/src/base.scss b/cvat-ui/src/base.scss index 0a738ee6b07..23092de0ab5 100644 --- a/cvat-ui/src/base.scss +++ b/cvat-ui/src/base.scss @@ -22,7 +22,6 @@ $border-color-1: #c3c3c3; $border-color-2: rgb(240, 240, 240); $border-color-3: #242424; $border-color-hover: #40a9ff; -$card-id-background-color: #1890ff; $background-color-1: white; $background-color-2: #f1f1f1; $notification-background-color-1: #d9ecff; diff --git a/cvat-ui/src/components/jobs-page/styles.scss b/cvat-ui/src/components/jobs-page/styles.scss index 52d6df08ba3..7a85de5276a 100644 --- a/cvat-ui/src/components/jobs-page/styles.scss +++ b/cvat-ui/src/components/jobs-page/styles.scss @@ -68,7 +68,6 @@ &:hover { .cvat-job-page-list-item-id { opacity: 1; - padding: $grid-unit-size $grid-unit-size * 4 $grid-unit-size $grid-unit-size; } .cvat-job-page-list-item-dimension { @@ -106,7 +105,7 @@ width: fit-content; background: white; border-radius: 0 4px 4px 0; - padding: $grid-unit-size $grid-unit-size * 4 $grid-unit-size $grid-unit-size; + padding: $grid-unit-size; opacity: 0.5; transition: 0.15s all ease; box-shadow: $box-shadow-base; From ae5b6062ef071356407730361df806bf26d41ad4 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Mon, 31 Jan 2022 14:33:05 +0300 Subject: [PATCH 9/9] Fixed tests mocks --- cvat-core/tests/mocks/server-proxy.mock.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cvat-core/tests/mocks/server-proxy.mock.js b/cvat-core/tests/mocks/server-proxy.mock.js index dbc1f91477b..03dbb78e499 100644 --- a/cvat-core/tests/mocks/server-proxy.mock.js +++ b/cvat-core/tests/mocks/server-proxy.mock.js @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Intel Corporation +// Copyright (C) 2020-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -212,7 +212,8 @@ class ServerProxy { } } - async function getJob(jobID) { + async function getJobs(filter = {}) { + const id = filter.id || null; const jobs = tasksDummyData.results .reduce((acc, task) => { for (const segment of task.segments) { @@ -234,7 +235,7 @@ class ServerProxy { return acc; }, []) - .filter((job) => job.id === jobID); + .filter((job) => job.id === id); return ( jobs[0] || { @@ -265,7 +266,7 @@ class ServerProxy { } } - return getJob(id); + return getJobs({ id }); } async function getUsers() { @@ -423,7 +424,7 @@ class ServerProxy { jobs: { value: Object.freeze({ - get: getJob, + get: getJobs, save: saveJob, }), writable: false,