From 8ce5103ced0ec9073a3a8abfe004904359993a38 Mon Sep 17 00:00:00 2001 From: Dmitry Kalinin Date: Tue, 16 Nov 2021 04:52:19 +0300 Subject: [PATCH 01/13] Added project tasks search and pagination --- cvat-core/src/api-implementation.js | 18 ++-- cvat-core/src/project.js | 19 ----- cvat-ui/src/actions/projects-actions.ts | 71 +++++++--------- cvat-ui/src/actions/tasks-actions.ts | 10 ++- .../src/components/project-page/details.tsx | 3 +- .../components/project-page/project-page.tsx | 85 ++++++++++++++++--- .../src/components/project-page/styles.scss | 13 +++ .../components/projects-page/project-list.tsx | 6 +- .../components/search-field/search-field.tsx | 21 +++-- cvat-ui/src/reducers/interfaces.ts | 2 + cvat-ui/src/reducers/projects-reducer.ts | 16 ++++ cvat-ui/src/reducers/tasks-reducer.ts | 6 +- cvat-ui/src/utils/hooks.ts | 13 ++- cvat/apps/engine/serializers.py | 13 +-- cvat/apps/engine/views.py | 10 +-- 15 files changed, 190 insertions(+), 116 deletions(-) diff --git a/cvat-core/src/api-implementation.js b/cvat-core/src/api-implementation.js index d947f9fc6c67..2685e8248def 100644 --- a/cvat-core/src/api-implementation.js +++ b/cvat-core/src/api-implementation.js @@ -188,6 +188,7 @@ owner: isString, assignee: isString, search: isString, + ordering: isString, status: isEnum.bind(TaskStatus), mode: isEnum.bind(TaskMode), dimension: isEnum.bind(DimensionType), @@ -196,11 +197,13 @@ checkExclusiveFields(filter, ['id', 'search', 'projectId'], ['page']); const searchParams = new URLSearchParams(); + for (const field of [ 'name', 'owner', 'assignee', 'search', + 'ordering', 'status', 'mode', 'id', @@ -209,7 +212,7 @@ 'dimension', ]) { if (Object.prototype.hasOwnProperty.call(filter, field)) { - searchParams.set(field, filter[field]); + searchParams.set(camelToSnake(field), filter[field]); } } @@ -230,10 +233,9 @@ owner: isString, search: isString, status: isEnum.bind(TaskStatus), - withoutTasks: isBoolean, }); - checkExclusiveFields(filter, ['id', 'search'], ['page', 'withoutTasks']); + checkExclusiveFields(filter, ['id', 'search'], ['page']); if (typeof filter.withoutTasks === 'undefined') { if (typeof filter.id === 'undefined') { @@ -244,21 +246,15 @@ } const searchParams = new URLSearchParams(); - for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page', 'withoutTasks']) { + for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page']) { if (Object.prototype.hasOwnProperty.call(filter, field)) { searchParams.set(camelToSnake(field), filter[field]); } } const projectsData = await serverProxy.projects.get(searchParams.toString()); - // prettier-ignore const projects = projectsData.map((project) => { - if (filter.withoutTasks) { - project.task_ids = project.tasks; - project.tasks = []; - } else { - project.task_ids = project.tasks.map((task) => task.id); - } + project.task_ids = project.tasks; return project; }).map((project) => new Project(project)); diff --git a/cvat-core/src/project.js b/cvat-core/src/project.js index 7e324498b95f..a0ca9c23c88b 100644 --- a/cvat-core/src/project.js +++ b/cvat-core/src/project.js @@ -5,7 +5,6 @@ (() => { const PluginRegistry = require('./plugins'); const { ArgumentError } = require('./exceptions'); - const { Task } = require('./session'); const { Label } = require('./labels'); const User = require('./user'); @@ -44,7 +43,6 @@ } data.labels = []; - data.tasks = []; if (Array.isArray(initialData.labels)) { for (const label of initialData.labels) { @@ -53,12 +51,6 @@ } } - if (Array.isArray(initialData.tasks)) { - for (const task of initialData.tasks) { - const taskInstance = new Task(task); - data.tasks.push(taskInstance); - } - } if (!data.task_subsets) { const subsetsSet = new Set(); for (const task of data.tasks) { @@ -212,17 +204,6 @@ data.labels = [...deletedLabels, ...labels]; }, }, - /** - * Tasks related with the project - * @name tasks - * @type {module:API.cvat.classes.Task[]} - * @memberof module:API.cvat.classes.Project - * @readonly - * @instance - */ - tasks: { - get: () => [...data.tasks], - }, /** * Subsets array for related tasks * @name subsets diff --git a/cvat-ui/src/actions/projects-actions.ts b/cvat-ui/src/actions/projects-actions.ts index 7a8c8b0cd744..49e94698d73e 100644 --- a/cvat-ui/src/actions/projects-actions.ts +++ b/cvat-ui/src/actions/projects-actions.ts @@ -5,8 +5,9 @@ import { Dispatch, ActionCreator } from 'redux'; import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; -import { ProjectsQuery } from 'reducers/interfaces'; -import { getTasksSuccess, updateTaskSuccess } from 'actions/tasks-actions'; +import { ProjectsQuery, TasksQuery, CombinedState } from 'reducers/interfaces'; +import { getTasksAsync, updateTaskSuccess } from 'actions/tasks-actions'; +import { getCVATStore } from 'cvat-store'; import getCore from 'cvat-core-wrapper'; const cvat = getCore(); @@ -34,8 +35,8 @@ const projectActions = { createAction(ProjectsActionTypes.GET_PROJECTS_SUCCESS, { array, previews, count }) ), getProjectsFailed: (error: any) => createAction(ProjectsActionTypes.GET_PROJECTS_FAILED, { error }), - updateProjectsGettingQuery: (query: Partial) => ( - createAction(ProjectsActionTypes.UPDATE_PROJECTS_GETTING_QUERY, { query }) + updateProjectsGettingQuery: (query: Partial, tasksQuery: Partial = {}) => ( + createAction(ProjectsActionTypes.UPDATE_PROJECTS_GETTING_QUERY, { query, tasksQuery }) ), createProject: () => createAction(ProjectsActionTypes.CREATE_PROJECT), createProjectSuccess: (projectId: number) => ( @@ -58,10 +59,27 @@ const projectActions = { export type ProjectActions = ActionUnion; -export function getProjectsAsync(query: Partial): ThunkAction { - return async (dispatch: ActionCreator, getState): Promise => { +export function getProjectTasksAsync(tasksQuery: Partial = {}): ThunkAction { + return (dispatch: ActionCreator): void => { + const store = getCVATStore(); + const state: CombinedState = store.getState(); + dispatch(projectActions.updateProjectsGettingQuery({}, tasksQuery)); + const query: Partial = { + ...state.projects.tasksGettingQuery, + page: 1, + ...tasksQuery, + }; + + dispatch(getTasksAsync(query)); + }; +} + +export function getProjectsAsync( + query: Partial, tasksQuery: Partial = {}, +): ThunkAction { + return async (dispatch: ActionCreator): Promise => { dispatch(projectActions.getProjects()); - dispatch(projectActions.updateProjectsGettingQuery(query)); + dispatch(projectActions.updateProjectsGettingQuery(query, tasksQuery)); // Clear query object from null fields const filteredQuery: Partial = { @@ -85,38 +103,15 @@ export function getProjectsAsync(query: Partial): ThunkAction { const array = Array.from(result); - // Appropriate tasks fetching proccess needs with retrieving only a single project - if (Object.keys(filteredQuery).includes('id')) { - const tasks: any[] = []; - const [project] = array; - const taskPreviewPromises: Promise[] = (project as any).tasks.map((task: any): string => { - tasks.push(task); - return (task as any).frames.preview().catch(() => ''); - }); + const previewPromises = array.map((project): string => (project as any).preview().catch(() => '')); + dispatch(projectActions.getProjectsSuccess(array, await Promise.all(previewPromises), result.count)); - const taskPreviews = await Promise.all(taskPreviewPromises); - - const state = getState(); - - dispatch(projectActions.getProjectsSuccess(array, taskPreviews, result.count)); - - if (!state.tasks.fetching) { - dispatch( - getTasksSuccess(tasks, taskPreviews, tasks.length, { - page: 1, - assignee: null, - id: null, - mode: null, - name: null, - owner: null, - search: null, - status: null, - }), - ); - } - } else { - const previewPromises = array.map((project): string => (project as any).preview().catch(() => '')); - dispatch(projectActions.getProjectsSuccess(array, await Promise.all(previewPromises), result.count)); + // Appropriate tasks fetching proccess needs with retrieving only a single project + if (Object.keys(filteredQuery).includes('id') && typeof filteredQuery.id === 'number') { + dispatch(getProjectTasksAsync({ + ...tasksQuery, + projectId: filteredQuery.id, + })); } }; } diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index e5b954f6c710..c584c141c169 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -47,7 +47,9 @@ function getTasks(): AnyAction { return action; } -export function getTasksSuccess(array: any[], previews: string[], count: number, query: TasksQuery): AnyAction { +export function getTasksSuccess( + array: any[], previews: string[], count: number, query: Partial, +): AnyAction { const action = { type: TasksActionTypes.GET_TASKS_SUCCESS, payload: { @@ -61,7 +63,7 @@ export function getTasksSuccess(array: any[], previews: string[], count: number, return action; } -function getTasksFailed(error: any, query: TasksQuery): AnyAction { +function getTasksFailed(error: any, query: Partial): AnyAction { const action = { type: TasksActionTypes.GET_TASKS_FAILED, payload: { @@ -73,7 +75,7 @@ function getTasksFailed(error: any, query: TasksQuery): AnyAction { return action; } -export function getTasksAsync(query: TasksQuery): ThunkAction, {}, {}, AnyAction> { +export function getTasksAsync(query: Partial): ThunkAction, {}, {}, AnyAction> { return async (dispatch: ActionCreator): Promise => { dispatch(getTasks()); @@ -248,7 +250,7 @@ export function exportTaskAsync(taskInstance: any): ThunkAction, { downloadAnchor.click(); dispatch(exportTaskSuccess(taskInstance.id)); } catch (error) { - dispatch(exportTaskFailed(taskInstance.id, error)); + dispatch(exportTaskFailed(taskInstance.id, error as Error)); } }; } diff --git a/cvat-ui/src/components/project-page/details.tsx b/cvat-ui/src/components/project-page/details.tsx index 4718d3a19acc..6f96aee710a3 100644 --- a/cvat-ui/src/components/project-page/details.tsx +++ b/cvat-ui/src/components/project-page/details.tsx @@ -10,7 +10,6 @@ import Title from 'antd/lib/typography/Title'; import Text from 'antd/lib/typography/Text'; import getCore from 'cvat-core-wrapper'; -import { Project } from 'reducers/interfaces'; import { updateProjectAsync } from 'actions/projects-actions'; import LabelsEditor from 'components/labels-editor/labels-editor'; import BugTrackerEditor from 'components/task-page/bug-tracker-editor'; @@ -19,7 +18,7 @@ import UserSelector from 'components/task-page/user-selector'; const core = getCore(); interface DetailsComponentProps { - project: Project; + project: any; } export default function DetailsComponent(props: DetailsComponentProps): JSX.Element { diff --git a/cvat-ui/src/components/project-page/project-page.tsx b/cvat-ui/src/components/project-page/project-page.tsx index ef44d03122cf..9c03e5ea45ae 100644 --- a/cvat-ui/src/components/project-page/project-page.tsx +++ b/cvat-ui/src/components/project-page/project-page.tsx @@ -3,22 +3,25 @@ // SPDX-License-Identifier: MIT import './styles.scss'; -import React, { useEffect } from 'react'; +import React, { useEffect, useCallback } from 'react'; import { useSelector, useDispatch } from 'react-redux'; -import { useHistory, useParams } from 'react-router'; +import { useHistory, useParams, useLocation } from 'react-router'; import Spin from 'antd/lib/spin'; import { Row, Col } from 'antd/lib/grid'; import Result from 'antd/lib/result'; import Button from 'antd/lib/button'; import Title from 'antd/lib/typography/Title'; +import Pagination from 'antd/lib/pagination'; import { PlusOutlined } from '@ant-design/icons'; -import { CombinedState, Task } from 'reducers/interfaces'; -import { getProjectsAsync } from 'actions/projects-actions'; +import { CombinedState, Task, TasksQuery } from 'reducers/interfaces'; +import { getProjectsAsync, getProjectTasksAsync } from 'actions/projects-actions'; import { cancelInferenceAsync } from 'actions/models-actions'; import TaskItem from 'components/tasks-page/task-item'; +import SearchField from 'components/search-field/search-field'; import MoveTaskModal from 'components/move-task-modal/move-task-modal'; import ModelRunnerDialog from 'components/model-runner-modal/model-runner-dialog'; +import { useDidUpdateEffect } from 'utils/hooks'; import DetailsComponent from './details'; import ProjectTopBar from './top-bar'; @@ -30,25 +33,55 @@ export default function ProjectPageComponent(): JSX.Element { const id = +useParams().id; const dispatch = useDispatch(); const history = useHistory(); + const { search } = useLocation(); const projects = useSelector((state: CombinedState) => state.projects.current).map((project) => project.instance); const projectsFetching = useSelector((state: CombinedState) => state.projects.fetching); const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes); const taskDeletes = useSelector((state: CombinedState) => state.tasks.activities.deletes); const tasksActiveInferences = useSelector((state: CombinedState) => state.models.inferences); const tasks = useSelector((state: CombinedState) => state.tasks.current); + const tasksCount = useSelector((state: CombinedState) => state.tasks.count); + const tasksGettingQuery = useSelector((state: CombinedState) => state.projects.tasksGettingQuery); const [project] = projects.filter((_project) => _project.id === id); - const projectSubsets = ['']; - if (project) projectSubsets.push(...project.subsets); + const projectSubsets: Array = []; + if (tasks.length) { + for (const task of tasks) { + if (!projectSubsets.includes(task.instance.subset)) projectSubsets.push(task.instance.subset); + } + } const deleteActivity = project && id in deletes ? deletes[id] : null; + const onPageChange = useCallback( + (p: number) => { + dispatch(getProjectTasksAsync({ + projectId: id, + page: p, + })); + }, + [dispatch, getProjectTasksAsync], + ); + useEffect(() => { - dispatch( - getProjectsAsync({ - id, - }), - ); - }, [id, dispatch]); + const searchParams: Partial = {}; + for (const [param, value] of new URLSearchParams(search)) { + searchParams[param] = ['page'].includes(param) ? Number.parseInt(value, 10) : value; + } + dispatch(getProjectsAsync({ id }, searchParams)); + }, []); + + useDidUpdateEffect(() => { + const searchParams = new URLSearchParams(); + for (const [name, value] of Object.entries(tasksGettingQuery)) { + if (value !== null && typeof value !== 'undefined' && !['projectId', 'ordering'].includes(name)) { + searchParams.append(name, value.toString()); + } + } + history.push({ + pathname: `/projects/${id}`, + search: `?${searchParams.toString()}`, + }); + }, [tasksGettingQuery, id]); if (deleteActivity) { history.push('/projects'); @@ -69,14 +102,27 @@ export default function ProjectPageComponent(): JSX.Element { ); } + const paginationDimensions = { + md: 22, + lg: 18, + xl: 16, + xxl: 16, + }; + return ( - + Tasks + dispatch(getProjectTasksAsync(query))} + />