You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

273 lines
12 KiB
TypeScript

// Copyright (C) 2019-2022 Intel Corporation
// Copyright (C) 2022 CVAT.ai Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, { useEffect, useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useHistory, useParams } 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 Dropdown from 'antd/lib/dropdown';
import Title from 'antd/lib/typography/Title';
import Pagination from 'antd/lib/pagination';
import { MutliPlusIcon } from 'icons';
import { PlusOutlined } from '@ant-design/icons';
import Empty from 'antd/lib/empty';
import Input from 'antd/lib/input';
import { CombinedState, Task, Indexable } from 'reducers';
import { getProjectsAsync, getProjectTasksAsync } from 'actions/projects-actions';
import { cancelInferenceAsync } from 'actions/models-actions';
import TaskItem from 'components/tasks-page/task-item';
import MoveTaskModal from 'components/move-task-modal/move-task-modal';
import ModelRunnerDialog from 'components/model-runner-modal/model-runner-dialog';
import {
SortingComponent, ResourceFilterHOC, defaultVisibility, updateHistoryFromQuery,
} from 'components/resource-sorting-filtering';
import CvatDropdownMenuPaper from 'components/common/cvat-dropdown-menu-paper';
import DetailsComponent from './details';
import ProjectTopBar from './top-bar';
import {
localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues, config,
} from './project-tasks-filter-configuration';
const FilteringComponent = ResourceFilterHOC(
config, localStorageRecentKeyword, localStorageRecentCapacity, predefinedFilterValues,
);
interface ParamType {
id: string;
}
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).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 tasksQuery = useSelector((state: CombinedState) => state.projects.tasksGettingQuery);
const tasksFetching = useSelector((state: CombinedState) => state.tasks.fetching);
const [isMounted, setIsMounted] = useState(false);
const [visibility, setVisibility] = useState(defaultVisibility);
const queryParams = new URLSearchParams(history.location.search);
const updatedQuery = { ...tasksQuery };
for (const key of Object.keys(updatedQuery)) {
(updatedQuery as Indexable)[key] = queryParams.get(key) || null;
if (key === 'page') {
updatedQuery.page = updatedQuery.page ? +updatedQuery.page : 1;
}
}
useEffect(() => {
dispatch(getProjectTasksAsync({ ...updatedQuery, projectId: id }));
setIsMounted(true);
}, []);
const [project] = projects.filter((_project) => _project.id === id);
const projectSubsets: Array<string> = [];
for (const task of tasks) {
if (!projectSubsets.includes(task.instance.subset)) projectSubsets.push(task.instance.subset);
}
useEffect(() => {
if (!project) {
dispatch(getProjectsAsync({ id }, updatedQuery));
}
}, []);
useEffect(() => {
if (isMounted) {
history.replace({
search: updateHistoryFromQuery(tasksQuery),
});
}
}, [tasksQuery]);
useEffect(() => {
if (project && id in deletes && deletes[id]) {
history.push('/projects');
}
}, [deletes]);
if (projectsFetching) {
return <Spin size='large' className='cvat-spinner' />;
}
if (!project) {
return (
<Result
className='cvat-not-found'
status='404'
title='Sorry, but this project was not found'
subTitle='Please, be sure information you tried to get exist and you have access'
/>
);
}
const content = tasksCount ? (
<>
{projectSubsets.map((subset: string) => (
<React.Fragment key={subset}>
{subset && <Title level={4}>{subset}</Title>}
{tasks
.filter((task) => task.instance.projectId === project.id && task.instance.subset === subset)
.map((task: Task) => (
<TaskItem
key={task.instance.id}
deleted={task.instance.id in taskDeletes ? taskDeletes[task.instance.id] : false}
hidden={false}
activeInference={tasksActiveInferences[task.instance.id] || null}
cancelAutoAnnotation={() => {
dispatch(cancelInferenceAsync(task.instance.id));
}}
previewImage={task.preview}
taskInstance={task.instance}
/>
))}
</React.Fragment>
))}
<Row justify='center' align='middle'>
<Col md={22} lg={18} xl={16} xxl={14}>
<Pagination
className='cvat-project-tasks-pagination'
onChange={(page: number) => {
dispatch(getProjectTasksAsync({
...tasksQuery,
projectId: id,
page,
}));
}}
showSizeChanger={false}
total={tasksCount}
pageSize={10}
current={tasksQuery.page}
showQuickJumper
/>
</Col>
</Row>
</>
) : (
<Empty description='No tasks found' />
);
return (
<Row justify='center' align='top' className='cvat-project-page'>
<Col md={22} lg={18} xl={16} xxl={14}>
<ProjectTopBar projectInstance={project} />
<DetailsComponent project={project} />
<Row justify='space-between' align='middle' className='cvat-project-page-tasks-bar'>
<Col span={24}>
<div className='cvat-project-page-tasks-filters-wrapper'>
<Input.Search
enterButton
onSearch={(_search: string) => {
dispatch(getProjectTasksAsync({
...tasksQuery,
page: 1,
projectId: id,
search: _search,
}));
}}
defaultValue={tasksQuery.search || ''}
className='cvat-project-page-tasks-search-bar'
placeholder='Search ...'
/>
<div>
<SortingComponent
visible={visibility.sorting}
onVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, sorting: visible })
)}
defaultFields={tasksQuery.sort?.split(',') || ['-ID']}
sortingFields={['ID', 'Owner', 'Status', 'Assignee', 'Updated date', 'Subset', 'Mode', 'Dimension', 'Name']}
onApplySorting={(sorting: string | null) => {
dispatch(getProjectTasksAsync({
...tasksQuery,
page: 1,
projectId: id,
sort: sorting,
}));
}}
/>
<FilteringComponent
value={updatedQuery.filter}
predefinedVisible={visibility.predefined}
builderVisible={visibility.builder}
recentVisible={visibility.recent}
onPredefinedVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, predefined: visible })
)}
onBuilderVisibleChange={(visible: boolean) => (
setVisibility({ ...defaultVisibility, builder: visible })
)}
onRecentVisibleChange={(visible: boolean) => (
setVisibility({
...defaultVisibility,
builder: visibility.builder,
recent: visible,
})
)}
onApplyFilter={(filter: string | null) => {
dispatch(getProjectTasksAsync({
...tasksQuery,
page: 1,
projectId: id,
filter,
}));
}}
/>
</div>
<Dropdown
trigger={['click']}
overlay={(
<CvatDropdownMenuPaper>
<Button
type='primary'
icon={<PlusOutlined />}
className='cvat-create-task-button'
onClick={() => history.push(`/tasks/create?projectId=${id}`)}
>
Create a new task
</Button>
<Button
type='primary'
icon={<span className='anticon'><MutliPlusIcon /></span>}
className='cvat-create-multi-tasks-button'
onClick={() => history.push(`/tasks/create?projectId=${id}&many=true`)}
>
Create multi tasks
</Button>
</CvatDropdownMenuPaper>
)}
>
<Button
type='primary'
className='cvat-create-task-dropdown'
icon={<PlusOutlined />}
/>
</Dropdown>
</div>
</Col>
</Row>
{ tasksFetching ? (
<Spin size='large' className='cvat-spinner' />
) : content }
</Col>
<MoveTaskModal />
<ModelRunnerDialog />
</Row>
);
}