未验证 提交 696e51fe 编写于 作者: B Boris Sekachev 提交者: GitHub

Coninue from frame N, advanced user selector on jobs page (#4297)

* Coninue from frame N, advanced user selector on jobs page

* Updated version & changelog

* Fixed styles
上级 87be7bcf
......@@ -32,6 +32,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add YOLOv5 serverless function for automatic annotation (<https://github.com/openvinotoolkit/cvat/pull/4178>)
- Basic page with jobs list, basic filtration to this list (<https://github.com/openvinotoolkit/cvat/pull/4258>)
- Added OpenCV.js TrackerMIL as tracking tool (<https://github.com/openvinotoolkit/cvat/pull/4200>)
- Ability to continue working from the latest frame where an annotator was before (<https://github.com/openvinotoolkit/cvat/pull/4297>)
### Changed
- Users don't have access to a task object anymore if they are assigneed only on some jobs of the task (<https://github.com/openvinotoolkit/cvat/pull/3788>)
......
{
"name": "cvat-ui",
"version": "1.35.0",
"version": "1.35.1",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-ui",
"version": "1.35.0",
"version": "1.35.1",
"license": "MIT",
"dependencies": {
"@ant-design/icons": "^4.6.3",
......
{
"name": "cvat-ui",
"version": "1.35.0",
"version": "1.35.1",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
......
......@@ -733,21 +733,23 @@ export function changeFrameAsync(
return;
}
// Start async requests
await job.logger.log(LogType.changeFrame, {
from: frame,
to: toFrame,
});
const data = await job.frames.get(toFrame, fillBuffer, frameStep);
const states = await job.annotations.get(toFrame, showAllInterpolationTracks, filters);
if (!isAbleToChangeFrame()) {
// while doing async actions above, canvas can become used by user in another way
// so, we need additional check and if it is used, we do not update state
// while doing async actions above, canvas can become used by a user in another way
// so, we need an additional check and if it is used, we do not update state
dispatch(abortAction());
return;
}
// commit the latest job frame to local storage
localStorage.setItem(`Job_${job.id}_frame`, `${toFrame}`);
await job.logger.log(LogType.changeFrame, {
from: frame,
to: toFrame,
});
const [minZ, maxZ] = computeZRange(states);
const currentTime = new Date().getTime();
let frameSpeed;
......
// Copyright (C) 2021 Intel Corporation
// Copyright (C) 2021-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -20,19 +20,22 @@ import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-ba
import { Workspace } from 'reducers/interfaces';
import { usePrevious } from 'utils/hooks';
import './styles.scss';
import Button from 'antd/lib/button';
interface Props {
job: any | null | undefined;
fetching: boolean;
frameNumber: number;
workspace: Workspace;
getJob(): void;
saveLogs(): void;
closeJob(): void;
workspace: Workspace;
changeFrame(frame: number): void;
}
export default function AnnotationPageComponent(props: Props): JSX.Element {
const {
job, fetching, getJob, closeJob, saveLogs, workspace,
job, fetching, workspace, frameNumber, getJob, closeJob, saveLogs, changeFrame,
} = props;
const prevJob = usePrevious(job);
const prevFetching = usePrevious(fetching);
......@@ -64,23 +67,55 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
}, [job, fetching]);
useEffect(() => {
if (prevFetching && !fetching && !prevJob && job && !job.labels.length) {
notification.warning({
message: 'No labels',
description: (
<span>
{`${job.projectId ? 'Project' : 'Task'} ${
job.projectId || job.taskId
} does not contain any label. `}
<a href={`/${job.projectId ? 'projects' : 'tasks'}/${job.projectId || job.id}/`}>
Add
</a>
{' the first one for editing annotation.'}
</span>
),
placement: 'topRight',
className: 'cvat-notification-no-labels',
});
if (prevFetching && !fetching && !prevJob && job) {
const latestFrame = localStorage.getItem(`Job_${job.id}_frame`);
if (latestFrame && Number.isInteger(+latestFrame)) {
const parsedFrame = +latestFrame;
if (parsedFrame !== frameNumber && parsedFrame >= job.startFrame && parsedFrame <= job.stopFrame) {
const notificationKey = `cvat-notification-continue-job-${job.id}`;
notification.info({
key: notificationKey,
message: `You finished working on frame ${parsedFrame}`,
description: (
<span>
Press
<Button
className='cvat-continue-job-button'
type='link'
onClick={() => {
changeFrame(parsedFrame);
notification.close(notificationKey);
}}
>
here
</Button>
if you would like to continue
</span>
),
placement: 'topRight',
className: 'cvat-notification-continue-job',
});
}
}
if (!job.labels.length) {
notification.warning({
message: 'No labels',
description: (
<span>
{`${job.projectId ? 'Project' : 'Task'} ${
job.projectId || job.taskId
} does not contain any label. `}
<a href={`/${job.projectId ? 'projects' : 'tasks'}/${job.projectId || job.id}/`}>
Add
</a>
{' the first one for editing annotation.'}
</span>
),
placement: 'topRight',
className: 'cvat-notification-no-labels',
});
}
}
}, [job, fetching, prevJob, prevFetching]);
......
......@@ -495,3 +495,17 @@ button.cvat-predictor-button {
font-weight: bold;
}
.cvat-continue-job-button {
padding: 0;
> span {
&::before {
content: '\00a0';
}
&::after {
content: '\00a0';
}
}
}
......@@ -47,7 +47,7 @@
.cvat-job-page-list-item {
width: 25%;
border-width: $grid-unit-size / 2;
border-width: 4px;
display: flex;
flex-direction: column;
......@@ -140,3 +140,7 @@
}
}
}
.cvat-jobs-filter-dropdown-users {
padding: $grid-unit-size;
}
......@@ -7,8 +7,9 @@ import { Col, Row } from 'antd/lib/grid';
import Text from 'antd/lib/typography/Text';
import Table from 'antd/lib/table';
import { FilterValue, TablePaginationConfig } from 'antd/lib/table/interface';
import { JobsQuery } from 'reducers/interfaces';
import Input from 'antd/lib/input';
import UserSelector, { User } from 'components/task-page/user-selector';
import Button from 'antd/lib/button';
interface Props {
......@@ -52,21 +53,21 @@ function TopBarComponent(props: Props): JSX.Element {
filteredValue: query.assignee ? [query.assignee] : null,
className: `${query.assignee ? 'cvat-jobs-page-filter cvat-jobs-page-filter-active' : 'cvat-jobs-page-filter'}`,
filterDropdown: (
<div>
<Input.Search
defaultValue={query.assignee || ''}
placeholder='Filter by assignee'
onSearch={(value: string) => {
onChangeFilters({ assignee: value });
<div className='cvat-jobs-filter-dropdown-users'>
<UserSelector
username={query.assignee ? query.assignee : undefined}
value={null}
onSelect={(value: User | null): void => {
if (value) {
if (query.assignee !== value.username) {
onChangeFilters({ assignee: value.username });
}
} else if (query.assignee !== null) {
onChangeFilters({ assignee: null });
}
}}
enterButton
/>
<Button
type='link'
onClick={() => {
onChangeFilters({ assignee: null });
}}
>
<Button disabled={query.assignee === null} type='link' onClick={() => onChangeFilters({ assignee: null })}>
Reset
</Button>
</div>
......
......@@ -55,9 +55,10 @@
}
}
.cvat-project-top-bar-actions > button {
.cvat-project-page-actions-button {
display: flex;
align-items: center;
line-height: 14px;
}
.cvat-project-tasks-pagination {
......
......@@ -32,7 +32,7 @@ export default function ProjectTopBar(props: DetailsComponentProps): JSX.Element
</Col>
<Col className='cvat-project-top-bar-actions'>
<Dropdown overlay={<ActionsMenu projectInstance={projectInstance} />}>
<Button size='large'>
<Button size='middle' className='cvat-project-page-actions-button'>
<Text className='cvat-text-color'>Actions</Text>
<MoreOutlined className='cvat-menu-icon' />
</Button>
......
......@@ -38,9 +38,10 @@
}
}
> div > div > div > button {
.cvat-task-page-actions-button {
display: flex;
align-items: center;
line-height: 14px;
}
}
......
......@@ -42,7 +42,7 @@ export default function DetailsComponent(props: DetailsComponentProps): JSX.Elem
</Col>
<Col>
<Dropdown overlay={<ActionsMenuContainer taskInstance={taskInstance} />}>
<Button size='large'>
<Button size='middle' className='cvat-task-page-actions-button'>
<Text className='cvat-text-color'>Actions</Text>
<MoreOutlined className='cvat-menu-icon' />
</Button>
......
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -19,6 +19,7 @@ export interface User {
interface Props {
value: User | null;
username?: string;
className?: string;
onSelect: (user: User | null) => void;
}
......@@ -44,8 +45,10 @@ const searchUsers = debounce(
);
export default function UserSelector(props: Props): JSX.Element {
const { value, className, onSelect } = props;
const [searchPhrase, setSearchPhrase] = useState('');
const {
value, className, username, onSelect,
} = props;
const [searchPhrase, setSearchPhrase] = useState(username || '');
const [initialUsers, setInitialUsers] = useState<User[]>([]);
const [users, setUsers] = useState<User[]>([]);
const autocompleteRef = useRef<RefSelectProps | null>(null);
......
......@@ -150,10 +150,12 @@
}
.cvat-item-open-task-actions {
margin-right: 5px;
margin-top: 10px;
margin-right: $grid-unit-size;
margin-top: $grid-unit-size;
display: flex;
align-items: center;
padding: $grid-unit-size;
line-height: 14px;
}
.cvat-item-open-task-button {
......
......@@ -158,12 +158,12 @@ class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteCompone
</Col>
</Row>
<Row justify='end'>
<Col className='cvat-item-open-task-actions'>
<Text className='cvat-text-color'>Actions</Text>
<Dropdown overlay={<ActionsMenuContainer taskInstance={taskInstance} />}>
<Dropdown overlay={<ActionsMenuContainer taskInstance={taskInstance} />}>
<Col className='cvat-item-open-task-actions'>
<Text className='cvat-text-color'>Actions</Text>
<MoreOutlined className='cvat-menu-icon' />
</Dropdown>
</Col>
</Col>
</Dropdown>
</Row>
</Col>
);
......
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -7,7 +7,10 @@ import { withRouter } from 'react-router-dom';
import { RouteComponentProps } from 'react-router';
import AnnotationPageComponent from 'components/annotation-page/annotation-page';
import { getJobAsync, saveLogsAsync, closeJob as closeJobAction } from 'actions/annotation-actions';
import {
getJobAsync, saveLogsAsync, changeFrameAsync,
closeJob as closeJobAction,
} from 'actions/annotation-actions';
import { CombinedState, Workspace } from 'reducers/interfaces';
......@@ -18,12 +21,14 @@ type OwnProps = RouteComponentProps<{
interface StateToProps {
job: any | null | undefined;
frameNumber: number;
fetching: boolean;
workspace: Workspace;
}
interface DispatchToProps {
getJob(): void;
changeFrame(frame: number): void;
saveLogs(): void;
closeJob(): void;
}
......@@ -35,6 +40,11 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
annotation: {
job: { requestedId, instance: job, fetching },
workspace,
player: {
frame: {
number: frameNumber,
},
},
},
} = state;
......@@ -42,6 +52,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
job: jobID === requestedId ? job : null,
fetching,
workspace,
frameNumber,
};
}
......@@ -71,7 +82,7 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
}
if (searchParams.has('frame') || searchParams.has('object')) {
own.history.replace(own.history.location.state);
own.history.replace(own.history.location.pathname);
}
return {
......@@ -84,6 +95,9 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
closeJob(): void {
dispatch(closeJobAction());
},
changeFrame(frame: number): void {
dispatch(changeFrameAsync(frame));
},
};
}
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册