未验证 提交 a17d054e 编写于 作者: D Dmitry Kalinin 提交者: GitHub

Project tasks loading only when needed (#3223)

* Tasks loading only when needed

* Fixed project page

* Added CHANGELOG, increased packages versions

* Update CHANGELOG.md
Co-authored-by: NBoris Sekachev <boris.sekachev@intel.com>

* Fixed comments

* Fixed overflow issue

* Fixed reducer issue

* Fixed cvat-core tests
Co-authored-by: NBoris Sekachev <boris.sekachev@intel.com>
上级 ae175d9d
......@@ -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 (<https://github.com/openvinotoolkit/cvat/pull/3223>)
### Security
......
{
"name": "cvat-core",
"version": "3.12.3",
"version": "3.13.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
......
{
"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": {
......
......@@ -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));
......
......@@ -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;
};
})();
......@@ -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) {
......
......@@ -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)) {
......
{
"name": "cvat-ui",
"version": "1.20.0",
"version": "1.20.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
{
"name": "cvat-ui",
"version": "1.20.0",
"version": "1.20.1",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
......
......@@ -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<ProjectsQuery>) => (
......@@ -60,7 +59,7 @@ const projectActions = {
export type ProjectActions = ActionUnion<typeof projectActions>;
export function getProjectsAsync(query: Partial<ProjectsQuery>): ThunkAction {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
return async (dispatch: ActionCreator<Dispatch>, getState): Promise<void> => {
dispatch(projectActions.getProjects());
dispatch(projectActions.updateProjectsGettingQuery(query));
......@@ -69,6 +68,7 @@ export function getProjectsAsync(query: Partial<ProjectsQuery>): 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<ProjectsQuery>): ThunkAction {
const array = Array.from(result);
const tasks: any[] = [];
const taskPreviewPromises: Promise<any>[] = [];
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<string>[] = (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));
}
};
}
......
......@@ -28,18 +28,16 @@ export default function ProjectPageComponent(): JSX.Element {
const id = +useParams<ParamType>().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 {
</Button>
</Col>
</Row>
{projectSubsets.map((subset) => (
{projectSubsets.map((subset: string) => (
<React.Fragment key={subset}>
{subset && <Title level={4}>{subset}</Title>}
{tasks
......
......@@ -4,6 +4,11 @@
@import '../../base.scss';
.cvat-project-page {
overflow-y: auto;
height: 100%;
}
.cvat-project-details {
width: 100%;
height: auto;
......
// 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 (
<Card
cover={
projectPreview ? (
preview ? (
<img
className='cvat-projects-project-item-card-preview'
src={projectPreview}
src={preview}
alt='Preview'
onClick={onOpenProject}
aria-hidden
......@@ -73,7 +67,7 @@ export default function ProjectItemComponent(props: Props): JSX.Element {
<Meta
title={(
<span onClick={onOpenProject} className='cvat-projects-project-item-title' aria-hidden>
{projectInstance.name}
{instance.name}
</span>
)}
description={(
......@@ -88,7 +82,7 @@ export default function ProjectItemComponent(props: Props): JSX.Element {
<Text type='secondary'>{`Last updated ${updated}`}</Text>
</div>
<div>
<Dropdown overlay={<ProjectActionsMenuComponent projectInstance={projectInstance} />}>
<Dropdown overlay={<ProjectActionsMenuComponent projectInstance={instance} />}>
<Button type='link' size='large' icon={<MoreOutlined />} />
</Dropdown>
</div>
......
// 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<Project[][]>((rows, key, index) => {
if (index % 4 === 0) {
rows.push([key]);
} else {
......@@ -38,14 +38,14 @@ export default function ProjectListComponent(): JSX.Element {
return (
<>
<Row justify='center' align='middle'>
<Row justify='center' align='middle' className='cvat-project-list-content'>
<Col className='cvat-projects-list' md={22} lg={18} xl={16} xxl={14}>
{projectInstances.map(
(row: any[]): JSX.Element => (
<Row key={row[0].id} gutter={[8, 8]}>
{row.map((instance: any) => (
<Col span={6} key={instance.id}>
<ProjectItem projectInstance={instance} />
{projects.map(
(row: Project[]): JSX.Element => (
<Row key={row[0].instance.id} gutter={[8, 8]}>
{row.map((project: Project) => (
<Col span={6} key={project.instance.id}>
<ProjectItem projectInstance={project} />
</Col>
))}
</Row>
......
......@@ -117,3 +117,7 @@
object-fit: cover;
}
}
.cvat-project-list-content {
padding-bottom: $grid-unit-size;
}
......@@ -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;
......
// 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,
}),
),
};
}
......
......@@ -510,11 +510,9 @@ class ProjectWithoutTaskSerializer(serializers.ModelSerializer):
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):
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册