diff --git a/CHANGELOG.md b/CHANGELOG.md index 38d0d5b4001..e8d1d134cd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for working with ellipses () - Add several flags to task creation CLI () - Add YOLOv5 serverless function for automatic annotation () +- Basic page with jobs list, basic filtration to this list () + ### Changed - Users don't have access to a task object anymore if they are assigneed only on some jobs of the task () - Different resources (tasks, projects) are not visible anymore for all CVAT instance users by default () diff --git a/cvat-core/package-lock.json b/cvat-core/package-lock.json index 649614174d8..f67453fa882 100644 --- a/cvat-core/package-lock.json +++ b/cvat-core/package-lock.json @@ -1,12 +1,12 @@ { "name": "cvat-core", - "version": "4.1.2", + "version": "4.2.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cvat-core", - "version": "4.1.2", + "version": "4.2.0", "license": "MIT", "dependencies": { "axios": "^0.21.4", diff --git a/cvat-core/package.json b/cvat-core/package.json index b03134e5a77..106426cb2cf 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "4.1.2", + "version": "4.2.0", "description": "Part of Computer Vision Tool which presents an interface for client-side integration", "main": "babel.config.js", "scripts": { diff --git a/cvat-core/src/api-implementation.js b/cvat-core/src/api-implementation.js index 98a962b24bf..e0245df766f 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,16 @@ const config = require('./config'); cvat.jobs.get.implementation = async (filter) => { checkFilter(filter, { + page: isInteger, + stage: isString, + state: isString, + assignee: isString, 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 +173,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({ id: filter.jobID }); + if (job) { + return [new Job(job)]; + } } - return []; + const jobsData = await serverProxy.jobs.get(filter); + 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..ca46f12d5f7 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,25 @@ return createdTask[0]; } - async function getJob(jobID) { + async function getJobs(filter = {}) { const { backendAPI } = config; + const id = filter.id || null; let response = null; try { - response = await Axios.get(`${backendAPI}/jobs/${jobID}`, { - proxy: config.proxy, - }); + if (id !== null) { + response = await Axios.get(`${backendAPI}/jobs/${id}`, { + proxy: config.proxy, + }); + } else { + response = await Axios.get(`${backendAPI}/jobs`, { + proxy: config.proxy, + params: { + ...filter, + page_size: 12, + }, + }); + } } catch (errorData) { throw generateError(errorData); } @@ -1069,12 +1080,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 +1812,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..fd5809d6cab 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.id === null || this.taskId === null) { + 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 === null) { + return ''; + } + const frameData = await getPreview(this.id); return frameData; }; 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, diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index b8e4da0442b..217c5fc6d33 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,12 +1,12 @@ { "name": "cvat-ui", - "version": "1.33.3", + "version": "1.34.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "cvat-ui", - "version": "1.33.3", + "version": "1.34.0", "license": "MIT", "dependencies": { "@ant-design/icons": "^4.6.3", diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 8fe11e1dd66..21b7b1b9038 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.33.3", + "version": "1.34.0", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/jobs-actions.ts b/cvat-ui/src/actions/jobs-actions.ts new file mode 100644 index 00000000000..2482fb5ed9d --- /dev/null +++ b/cvat-ui/src/actions/jobs-actions.ts @@ -0,0 +1,48 @@ +// 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', +} + +interface JobsList extends Array { + count: number; +} + +const jobsActions = { + getJobs: (query: Partial) => createAction(JobsActionTypes.GET_JOBS, { query }), + getJobsSuccess: (jobs: JobsList, 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 = await 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 + + + ), + }, + ]; + + return ( + + + + + 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' + /> + + + + ); +} + +export default React.memo(TopBarComponent); diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 858668d4dfb..a59f6a82f57 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Intel Corporation +// Copyright (C) 2020-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -78,6 +78,22 @@ export interface Task { preview: string; } +export interface JobsQuery { + page: number; + assignee: string | null; + stage: 'annotation' | 'validation' | 'acceptance' | null; + state: 'new' | 'in progress' | 'rejected' | 'completed' | null; + [index: string]: number | null | string | undefined; +} + +export interface JobsState { + query: JobsQuery; + fetching: boolean; + count: number; + current: any[]; + previews: string[]; +} + export interface TasksState { importing: boolean; initialized: boolean; @@ -357,6 +373,7 @@ export interface NotificationsState { }; jobs: { updating: null | ErrorState; + fetching: null | ErrorState; }; formats: { fetching: null | ErrorState; @@ -747,6 +764,7 @@ export interface OrganizationState { export interface CombinedState { auth: AuthState; projects: ProjectsState; + jobs: JobsState; tasks: TasksState; about: AboutState; share: ShareState; diff --git a/cvat-ui/src/reducers/jobs-reducer.ts b/cvat-ui/src/reducers/jobs-reducer.ts new file mode 100644 index 00000000000..fca4992a8f9 --- /dev/null +++ b/cvat-ui/src/reducers/jobs-reducer.ts @@ -0,0 +1,52 @@ +// Copyright (C) 2022 Intel Corporation +// +// SPDX-License-Identifier: MIT + +import { JobsActions, JobsActionTypes } from 'actions/jobs-actions'; +import { JobsState } from './interfaces'; + +const defaultState: JobsState = { + fetching: false, + count: 0, + query: { + page: 1, + state: null, + stage: null, + assignee: null, + }, + current: [], + previews: [], +}; + +export default (state: JobsState = defaultState, action: JobsActions): JobsState => { + switch (action.type) { + case JobsActionTypes.GET_JOBS: { + return { + ...state, + fetching: true, + query: { + ...defaultState.query, + ...action.payload.query, + }, + }; + } + case JobsActionTypes.GET_JOBS_SUCCESS: { + return { + ...state, + fetching: false, + count: action.payload.jobs.count, + current: action.payload.jobs, + previews: action.payload.previews, + }; + } + case JobsActionTypes.GET_JOBS_FAILED: { + return { + ...state, + fetching: false, + }; + } + default: { + return state; + } + } +}; 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-ui/src/reducers/root-reducer.ts b/cvat-ui/src/reducers/root-reducer.ts index 73e3444a8c7..53b91afca5d 100644 --- a/cvat-ui/src/reducers/root-reducer.ts +++ b/cvat-ui/src/reducers/root-reducer.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2020-2021 Intel Corporation +// Copyright (C) 2020-2022 Intel Corporation // // SPDX-License-Identifier: MIT @@ -6,6 +6,7 @@ import { combineReducers, Reducer } from 'redux'; import authReducer from './auth-reducer'; import projectsReducer from './projects-reducer'; import tasksReducer from './tasks-reducer'; +import jobsReducer from './jobs-reducer'; import aboutReducer from './about-reducer'; import shareReducer from './share-reducer'; import formatsReducer from './formats-reducer'; @@ -27,6 +28,7 @@ export default function createRootReducer(): Reducer { auth: authReducer, projects: projectsReducer, tasks: tasksReducer, + jobs: jobsReducer, about: aboutReducer, share: shareReducer, formats: formatsReducer, 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):