diff --git a/CHANGELOG.md b/CHANGELOG.md index 328e058bef53..cd0049d15bab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- +- Project page requests took a long time and did many DB queries () ### Security diff --git a/cvat-core/package-lock.json b/cvat-core/package-lock.json index 776e8b2b84e6..2bdc11180320 100644 --- a/cvat-core/package-lock.json +++ b/cvat-core/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "3.12.3", + "version": "3.13.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-core/package.json b/cvat-core/package.json index 9ed6e5e7cf03..e5e482fde5cb 100644 --- a/cvat-core/package.json +++ b/cvat-core/package.json @@ -1,6 +1,6 @@ { "name": "cvat-core", - "version": "3.12.3", + "version": "3.13.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 5e9ff610ca52..04c982631c93 100644 --- a/cvat-core/src/api-implementation.js +++ b/cvat-core/src/api-implementation.js @@ -227,6 +227,14 @@ checkExclusiveFields(filter, ['id', 'search'], ['page', 'withoutTasks']); + if (typeof filter.withoutTasks === 'undefined') { + if (typeof filter.id === 'undefined') { + filter.withoutTasks = true; + } else { + filter.withoutTasks = false; + } + } + const searchParams = new URLSearchParams(); for (const field of ['name', 'assignee', 'owner', 'search', 'status', 'id', 'page', 'withoutTasks']) { if (Object.prototype.hasOwnProperty.call(filter, field)) { @@ -238,7 +246,10 @@ // 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); } return project; }).map((project) => new Project(project)); diff --git a/cvat-core/src/project.js b/cvat-core/src/project.js index 0e2b54789141..e2a137f13eae 100644 --- a/cvat-core/src/project.js +++ b/cvat-core/src/project.js @@ -8,6 +8,7 @@ const { ArgumentError } = require('./exceptions'); const { Task } = require('./session'); const { Label } = require('./labels'); + const { getPreview } = require('./frames'); const User = require('./user'); /** @@ -34,6 +35,7 @@ updated_date: undefined, task_subsets: undefined, training_project: undefined, + task_ids: undefined, }; for (const property in data) { @@ -58,9 +60,9 @@ data.tasks.push(taskInstance); } } - if (!data.task_subsets && data.tasks.length) { + if (!data.task_subsets) { const subsetsSet = new Set(); - for (const task in data.tasks) { + for (const task of data.tasks) { if (task.subset) subsetsSet.add(task.subset); } data.task_subsets = Array.from(subsetsSet); @@ -254,6 +256,22 @@ ); } + /** + * Get the first frame of the first task of a project for preview + * @method preview + * @memberof Project + * @returns {string} - jpeg encoded image + * @instance + * @async + * @throws {module:API.cvat.exceptions.PluginError} + * @throws {module:API.cvat.exceptions.ServerError} + * @throws {module:API.cvat.exceptions.ArgumentError} + */ + async preview() { + const result = await PluginRegistry.apiWrapper.call(this, Project.prototype.preview); + return result; + } + /** * Method updates data of a created project or creates new project from scratch * @method save @@ -331,4 +349,12 @@ const result = await serverProxy.projects.delete(this.id); return result; }; + + Project.prototype.preview.implementation = async function () { + if (!this._internalData.task_ids.length) { + return ''; + } + const frameData = await getPreview(this._internalData.task_ids[0]); + return frameData; + }; })(); diff --git a/cvat-core/tests/api/projects.js b/cvat-core/tests/api/projects.js index 5a01b7d141d4..8a0fe3b09485 100644 --- a/cvat-core/tests/api/projects.js +++ b/cvat-core/tests/api/projects.js @@ -16,7 +16,7 @@ const { Project } = require('../../src/project'); describe('Feature: get projects', () => { test('get all projects', async () => { - const result = await window.cvat.projects.get(); + const result = await window.cvat.projects.get({ withoutTasks: false }); expect(Array.isArray(result)).toBeTruthy(); expect(result).toHaveLength(2); for (const el of result) { diff --git a/cvat-core/tests/mocks/server-proxy.mock.js b/cvat-core/tests/mocks/server-proxy.mock.js index 4357e1769efa..a5111756cd97 100644 --- a/cvat-core/tests/mocks/server-proxy.mock.js +++ b/cvat-core/tests/mocks/server-proxy.mock.js @@ -14,16 +14,18 @@ const { frameMetaDummyData, } = require('./dummy-data.mock'); -function QueryStringToJSON(query) { +function QueryStringToJSON(query, ignoreList = []) { const pairs = [...new URLSearchParams(query).entries()]; const result = {}; for (const pair of pairs) { const [key, value] = pair; - if (['id'].includes(key)) { - result[key] = +value; - } else { - result[key] = value; + if (!ignoreList.includes(key)) { + if (['id'].includes(key)) { + result[key] = +value; + } else { + result[key] = value; + } } } @@ -73,7 +75,7 @@ class ServerProxy { } async function getProjects(filter = '') { - const queries = QueryStringToJSON(filter); + const queries = QueryStringToJSON(filter, ['without_tasks']); const result = projectsDummyData.results.filter((x) => { for (const key in queries) { if (Object.prototype.hasOwnProperty.call(queries, key)) { diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index c2a48c19580c..b3a5bce157d2 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.20.0", + "version": "1.20.1", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/cvat-ui/package.json b/cvat-ui/package.json index f15cd39389f5..a3df3ed409b3 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.20.0", + "version": "1.20.1", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/projects-actions.ts b/cvat-ui/src/actions/projects-actions.ts index 5408a42fbe17..7a8c8b0cd744 100644 --- a/cvat-ui/src/actions/projects-actions.ts +++ b/cvat-ui/src/actions/projects-actions.ts @@ -5,9 +5,8 @@ import { Dispatch, ActionCreator } from 'redux'; import { ActionUnion, createAction, ThunkAction } from 'utils/redux'; -import { ProjectsQuery, CombinedState } from 'reducers/interfaces'; +import { ProjectsQuery } from 'reducers/interfaces'; import { getTasksSuccess, updateTaskSuccess } from 'actions/tasks-actions'; -import { getCVATStore } from 'cvat-store'; import getCore from 'cvat-core-wrapper'; const cvat = getCore(); @@ -31,8 +30,8 @@ export enum ProjectsActionTypes { // prettier-ignore const projectActions = { getProjects: () => createAction(ProjectsActionTypes.GET_PROJECTS), - getProjectsSuccess: (array: any[], count: number) => ( - createAction(ProjectsActionTypes.GET_PROJECTS_SUCCESS, { array, count }) + getProjectsSuccess: (array: any[], previews: string[], count: number) => ( + createAction(ProjectsActionTypes.GET_PROJECTS_SUCCESS, { array, previews, count }) ), getProjectsFailed: (error: any) => createAction(ProjectsActionTypes.GET_PROJECTS_FAILED, { error }), updateProjectsGettingQuery: (query: Partial) => ( @@ -60,7 +59,7 @@ const projectActions = { export type ProjectActions = ActionUnion; export function getProjectsAsync(query: Partial): ThunkAction { - return async (dispatch: ActionCreator): Promise => { + return async (dispatch: ActionCreator, getState): Promise => { dispatch(projectActions.getProjects()); dispatch(projectActions.updateProjectsGettingQuery(query)); @@ -69,6 +68,7 @@ export function getProjectsAsync(query: Partial): ThunkAction { page: 1, ...query, }; + for (const key in filteredQuery) { if (filteredQuery[key] === null || typeof filteredQuery[key] === 'undefined') { delete filteredQuery[key]; @@ -85,38 +85,38 @@ export function getProjectsAsync(query: Partial): ThunkAction { const array = Array.from(result); - const tasks: any[] = []; - const taskPreviewPromises: Promise[] = []; - - for (const project of array) { - taskPreviewPromises.push( - ...(project as any).tasks.map((task: any): string => { - tasks.push(task); - return (task as any).frames.preview().catch(() => ''); - }), - ); - } + // 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 taskPreviews = await Promise.all(taskPreviewPromises); - - dispatch(projectActions.getProjectsSuccess(array, result.count)); - - const store = getCVATStore(); - const state: CombinedState = store.getState(); - - 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, - }), - ); + 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)); } }; } diff --git a/cvat-ui/src/components/project-page/project-page.tsx b/cvat-ui/src/components/project-page/project-page.tsx index 348b5d0e8a36..385d58c519ac 100644 --- a/cvat-ui/src/components/project-page/project-page.tsx +++ b/cvat-ui/src/components/project-page/project-page.tsx @@ -28,18 +28,16 @@ export default function ProjectPageComponent(): JSX.Element { const id = +useParams().id; const dispatch = useDispatch(); const history = useHistory(); - const projects = useSelector((state: CombinedState) => state.projects.current); + 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 projectSubsets = useSelector((state: CombinedState) => { - const [project] = state.projects.current.filter((_project) => _project.id === id); - return project ? ([...new Set(project.tasks.map((task: any) => task.subset))] as string[]) : []; - }); const [project] = projects.filter((_project) => _project.id === id); + const projectSubsets = ['']; + if (project) projectSubsets.push(...project.subsets); const deleteActivity = project && id in deletes ? deletes[id] : null; useEffect(() => { @@ -90,7 +88,7 @@ export default function ProjectPageComponent(): JSX.Element { - {projectSubsets.map((subset) => ( + {projectSubsets.map((subset: string) => ( {subset && {subset}} {tasks diff --git a/cvat-ui/src/components/project-page/styles.scss b/cvat-ui/src/components/project-page/styles.scss index 4769c556bb84..71767d2d78b5 100644 --- a/cvat-ui/src/components/project-page/styles.scss +++ b/cvat-ui/src/components/project-page/styles.scss @@ -4,6 +4,11 @@ @import '../../base.scss'; +.cvat-project-page { + overflow-y: auto; + height: 100%; +} + .cvat-project-details { width: 100%; height: auto; diff --git a/cvat-ui/src/components/projects-page/project-item.tsx b/cvat-ui/src/components/projects-page/project-item.tsx index 6d80e04ca554..2ca8523f5ec9 100644 --- a/cvat-ui/src/components/projects-page/project-item.tsx +++ b/cvat-ui/src/components/projects-page/project-item.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -22,24 +22,18 @@ interface Props { } export default function ProjectItemComponent(props: Props): JSX.Element { - const { projectInstance } = props; + const { + projectInstance: { instance, preview }, + } = props; const history = useHistory(); - const ownerName = projectInstance.owner ? projectInstance.owner.username : null; - const updated = moment(projectInstance.updatedDate).fromNow(); + const ownerName = instance.owner ? instance.owner.username : null; + const updated = moment(instance.updatedDate).fromNow(); const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes); - const deleted = projectInstance.id in deletes ? deletes[projectInstance.id] : false; - - let projectPreview = null; - if (projectInstance.tasks.length) { - // prettier-ignore - projectPreview = useSelector((state: CombinedState) => ( - state.tasks.current.find((task) => task.instance.id === projectInstance.tasks[0].id)?.preview - )); - } + const deleted = instance.id in deletes ? deletes[instance.id] : false; const onOpenProject = (): void => { - history.push(`/projects/${projectInstance.id}`); + history.push(`/projects/${instance.id}`); }; const style: React.CSSProperties = {}; @@ -52,10 +46,10 @@ export default function ProjectItemComponent(props: Props): JSX.Element { return ( - {projectInstance.name} + {instance.name} )} description={( @@ -88,7 +82,7 @@ export default function ProjectItemComponent(props: Props): JSX.Element { {`Last updated ${updated}`}
- }> + }>
diff --git a/cvat-ui/src/components/projects-page/project-list.tsx b/cvat-ui/src/components/projects-page/project-list.tsx index 1d7138902dac..494b501f04dd 100644 --- a/cvat-ui/src/components/projects-page/project-list.tsx +++ b/cvat-ui/src/components/projects-page/project-list.tsx @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -8,14 +8,14 @@ import { Row, Col } from 'antd/lib/grid'; import Pagination from 'antd/lib/pagination'; import { getProjectsAsync } from 'actions/projects-actions'; -import { CombinedState } from 'reducers/interfaces'; +import { CombinedState, Project } from 'reducers/interfaces'; import ProjectItem from './project-item'; export default function ProjectListComponent(): JSX.Element { const dispatch = useDispatch(); const projectsCount = useSelector((state: CombinedState) => state.projects.count); const { page } = useSelector((state: CombinedState) => state.projects.gettingQuery); - let projectInstances = useSelector((state: CombinedState) => state.projects.current); + const projectInstances = useSelector((state: CombinedState) => state.projects.current); const gettingQuery = useSelector((state: CombinedState) => state.projects.gettingQuery); function changePage(p: number): void { @@ -27,7 +27,7 @@ export default function ProjectListComponent(): JSX.Element { ); } - projectInstances = projectInstances.reduce((rows, key, index) => { + const projects = projectInstances.reduce((rows, key, index) => { if (index % 4 === 0) { rows.push([key]); } else { @@ -38,14 +38,14 @@ export default function ProjectListComponent(): JSX.Element { return ( <> - + - {projectInstances.map( - (row: any[]): JSX.Element => ( - - {row.map((instance: any) => ( - - + {projects.map( + (row: Project[]): JSX.Element => ( + + {row.map((project: Project) => ( + + ))} diff --git a/cvat-ui/src/components/projects-page/styles.scss b/cvat-ui/src/components/projects-page/styles.scss index 44fb66158704..dd60013d2279 100644 --- a/cvat-ui/src/components/projects-page/styles.scss +++ b/cvat-ui/src/components/projects-page/styles.scss @@ -117,3 +117,7 @@ object-fit: cover; } } + +.cvat-project-list-content { + padding-bottom: $grid-unit-size; +} diff --git a/cvat-ui/src/reducers/interfaces.ts b/cvat-ui/src/reducers/interfaces.ts index 841a0ef612d1..fc8a40340d90 100644 --- a/cvat-ui/src/reducers/interfaces.ts +++ b/cvat-ui/src/reducers/interfaces.ts @@ -30,10 +30,13 @@ export interface ProjectsQuery { owner: string | null; name: string | null; status: string | null; - [key: string]: string | number | null | undefined; + [key: string]: string | boolean | number | null | undefined; } -export type Project = any; +export interface Project { + instance: any; + preview: string; +} export interface ProjectsState { initialized: boolean; diff --git a/cvat-ui/src/reducers/projects-reducer.ts b/cvat-ui/src/reducers/projects-reducer.ts index d6a7c1eee491..d8fba25d88cb 100644 --- a/cvat-ui/src/reducers/projects-reducer.ts +++ b/cvat-ui/src/reducers/projects-reducer.ts @@ -1,4 +1,4 @@ -// Copyright (C) 2020 Intel Corporation +// Copyright (C) 2020-2021 Intel Corporation // // SPDX-License-Identifier: MIT @@ -50,12 +50,19 @@ export default (state: ProjectsState = defaultState, action: AnyAction): Project current: [], }; case ProjectsActionTypes.GET_PROJECTS_SUCCESS: { + const combinedWithPreviews = action.payload.array.map( + (project: any, index: number): Project => ({ + instance: project, + preview: action.payload.previews[index], + }), + ); + return { ...state, initialized: true, fetching: false, count: action.payload.count, - current: action.payload.array, + current: combinedWithPreviews, }; } case ProjectsActionTypes.GET_PROJECTS_FAILED: { @@ -110,13 +117,11 @@ export default (state: ProjectsState = defaultState, action: AnyAction): Project return { ...state, current: state.current.map( - (project): Project => { - if (project.id === action.payload.project.id) { - return action.payload.project; - } - - return project; - }, + (project): Project => ({ + ...project, + instance: project.instance.id === action.payload.project.id ? + action.payload.project : project.instance, + }), ), }; } @@ -124,13 +129,11 @@ export default (state: ProjectsState = defaultState, action: AnyAction): Project return { ...state, current: state.current.map( - (project): Project => { - if (project.id === action.payload.project.id) { - return action.payload.project; - } - - return project; - }, + (project): Project => ({ + ...project, + instance: project.instance.id === action.payload.project.id ? + action.payload.project : project.instance, + }), ), }; } diff --git a/cvat/apps/engine/serializers.py b/cvat/apps/engine/serializers.py index 15739bbafee4..4f3495594a71 100644 --- a/cvat/apps/engine/serializers.py +++ b/cvat/apps/engine/serializers.py @@ -510,11 +510,9 @@ class Meta: def to_representation(self, instance): response = super().to_representation(instance) - subsets = set() - for task in instance.tasks.all(): - if task.subset: - subsets.add(task.subset) - response['task_subsets'] = list(subsets) + task_subsets = set(instance.tasks.values_list('subset', flat=True)) + task_subsets.discard('') + response['task_subsets'] = list(task_subsets) return response class ProjectSerializer(ProjectWithoutTaskSerializer):