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
main
Boris Sekachev 4 years ago committed by GitHub
parent 87be7bcf53
commit 696e51fe2e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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>) - 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>) - 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>) - 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 ### 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>) - 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>)

@ -1,12 +1,12 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.35.0", "version": "1.35.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.35.0", "version": "1.35.1",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.6.3", "@ant-design/icons": "^4.6.3",

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.35.0", "version": "1.35.1",
"description": "CVAT single-page application", "description": "CVAT single-page application",
"main": "src/index.tsx", "main": "src/index.tsx",
"scripts": { "scripts": {

@ -733,21 +733,23 @@ export function changeFrameAsync(
return; return;
} }
// Start async requests
await job.logger.log(LogType.changeFrame, {
from: frame,
to: toFrame,
});
const data = await job.frames.get(toFrame, fillBuffer, frameStep); const data = await job.frames.get(toFrame, fillBuffer, frameStep);
const states = await job.annotations.get(toFrame, showAllInterpolationTracks, filters); const states = await job.annotations.get(toFrame, showAllInterpolationTracks, filters);
if (!isAbleToChangeFrame()) { if (!isAbleToChangeFrame()) {
// while doing async actions above, canvas can become used by user in another way // while doing async actions above, canvas can become used by a user in another way
// so, we need additional check and if it is used, we do not update state // so, we need an additional check and if it is used, we do not update state
dispatch(abortAction()); dispatch(abortAction());
return; 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 [minZ, maxZ] = computeZRange(states);
const currentTime = new Date().getTime(); const currentTime = new Date().getTime();
let frameSpeed; let frameSpeed;

@ -1,4 +1,4 @@
// Copyright (C) 2021 Intel Corporation // Copyright (C) 2021-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -20,19 +20,22 @@ import AnnotationTopBarContainer from 'containers/annotation-page/top-bar/top-ba
import { Workspace } from 'reducers/interfaces'; import { Workspace } from 'reducers/interfaces';
import { usePrevious } from 'utils/hooks'; import { usePrevious } from 'utils/hooks';
import './styles.scss'; import './styles.scss';
import Button from 'antd/lib/button';
interface Props { interface Props {
job: any | null | undefined; job: any | null | undefined;
fetching: boolean; fetching: boolean;
frameNumber: number;
workspace: Workspace;
getJob(): void; getJob(): void;
saveLogs(): void; saveLogs(): void;
closeJob(): void; closeJob(): void;
workspace: Workspace; changeFrame(frame: number): void;
} }
export default function AnnotationPageComponent(props: Props): JSX.Element { export default function AnnotationPageComponent(props: Props): JSX.Element {
const { const {
job, fetching, getJob, closeJob, saveLogs, workspace, job, fetching, workspace, frameNumber, getJob, closeJob, saveLogs, changeFrame,
} = props; } = props;
const prevJob = usePrevious(job); const prevJob = usePrevious(job);
const prevFetching = usePrevious(fetching); const prevFetching = usePrevious(fetching);
@ -64,23 +67,55 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
}, [job, fetching]); }, [job, fetching]);
useEffect(() => { useEffect(() => {
if (prevFetching && !fetching && !prevJob && job && !job.labels.length) { if (prevFetching && !fetching && !prevJob && job) {
notification.warning({ const latestFrame = localStorage.getItem(`Job_${job.id}_frame`);
message: 'No labels', if (latestFrame && Number.isInteger(+latestFrame)) {
description: ( const parsedFrame = +latestFrame;
<span> if (parsedFrame !== frameNumber && parsedFrame >= job.startFrame && parsedFrame <= job.stopFrame) {
{`${job.projectId ? 'Project' : 'Task'} ${ const notificationKey = `cvat-notification-continue-job-${job.id}`;
job.projectId || job.taskId notification.info({
} does not contain any label. `} key: notificationKey,
<a href={`/${job.projectId ? 'projects' : 'tasks'}/${job.projectId || job.id}/`}> message: `You finished working on frame ${parsedFrame}`,
Add description: (
</a> <span>
{' the first one for editing annotation.'} Press
</span> <Button
), className='cvat-continue-job-button'
placement: 'topRight', type='link'
className: 'cvat-notification-no-labels', 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]); }, [job, fetching, prevJob, prevFetching]);

@ -495,3 +495,17 @@ button.cvat-predictor-button {
font-weight: bold; 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 { .cvat-job-page-list-item {
width: 25%; width: 25%;
border-width: $grid-unit-size / 2; border-width: 4px;
display: flex; display: flex;
flex-direction: column; 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 Text from 'antd/lib/typography/Text';
import Table from 'antd/lib/table'; import Table from 'antd/lib/table';
import { FilterValue, TablePaginationConfig } from 'antd/lib/table/interface'; import { FilterValue, TablePaginationConfig } from 'antd/lib/table/interface';
import { JobsQuery } from 'reducers/interfaces'; 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'; import Button from 'antd/lib/button';
interface Props { interface Props {
@ -52,21 +53,21 @@ function TopBarComponent(props: Props): JSX.Element {
filteredValue: query.assignee ? [query.assignee] : null, filteredValue: query.assignee ? [query.assignee] : null,
className: `${query.assignee ? 'cvat-jobs-page-filter cvat-jobs-page-filter-active' : 'cvat-jobs-page-filter'}`, className: `${query.assignee ? 'cvat-jobs-page-filter cvat-jobs-page-filter-active' : 'cvat-jobs-page-filter'}`,
filterDropdown: ( filterDropdown: (
<div> <div className='cvat-jobs-filter-dropdown-users'>
<Input.Search <UserSelector
defaultValue={query.assignee || ''} username={query.assignee ? query.assignee : undefined}
placeholder='Filter by assignee' value={null}
onSearch={(value: string) => { onSelect={(value: User | null): void => {
onChangeFilters({ assignee: value }); if (value) {
if (query.assignee !== value.username) {
onChangeFilters({ assignee: value.username });
}
} else if (query.assignee !== null) {
onChangeFilters({ assignee: null });
}
}} }}
enterButton
/> />
<Button <Button disabled={query.assignee === null} type='link' onClick={() => onChangeFilters({ assignee: null })}>
type='link'
onClick={() => {
onChangeFilters({ assignee: null });
}}
>
Reset Reset
</Button> </Button>
</div> </div>

@ -55,9 +55,10 @@
} }
} }
.cvat-project-top-bar-actions > button { .cvat-project-page-actions-button {
display: flex; display: flex;
align-items: center; align-items: center;
line-height: 14px;
} }
.cvat-project-tasks-pagination { .cvat-project-tasks-pagination {

@ -32,7 +32,7 @@ export default function ProjectTopBar(props: DetailsComponentProps): JSX.Element
</Col> </Col>
<Col className='cvat-project-top-bar-actions'> <Col className='cvat-project-top-bar-actions'>
<Dropdown overlay={<ActionsMenu projectInstance={projectInstance} />}> <Dropdown overlay={<ActionsMenu projectInstance={projectInstance} />}>
<Button size='large'> <Button size='middle' className='cvat-project-page-actions-button'>
<Text className='cvat-text-color'>Actions</Text> <Text className='cvat-text-color'>Actions</Text>
<MoreOutlined className='cvat-menu-icon' /> <MoreOutlined className='cvat-menu-icon' />
</Button> </Button>

@ -38,9 +38,10 @@
} }
} }
> div > div > div > button { .cvat-task-page-actions-button {
display: flex; display: flex;
align-items: center; align-items: center;
line-height: 14px;
} }
} }

@ -42,7 +42,7 @@ export default function DetailsComponent(props: DetailsComponentProps): JSX.Elem
</Col> </Col>
<Col> <Col>
<Dropdown overlay={<ActionsMenuContainer taskInstance={taskInstance} />}> <Dropdown overlay={<ActionsMenuContainer taskInstance={taskInstance} />}>
<Button size='large'> <Button size='middle' className='cvat-task-page-actions-button'>
<Text className='cvat-text-color'>Actions</Text> <Text className='cvat-text-color'>Actions</Text>
<MoreOutlined className='cvat-menu-icon' /> <MoreOutlined className='cvat-menu-icon' />
</Button> </Button>

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -19,6 +19,7 @@ export interface User {
interface Props { interface Props {
value: User | null; value: User | null;
username?: string;
className?: string; className?: string;
onSelect: (user: User | null) => void; onSelect: (user: User | null) => void;
} }
@ -44,8 +45,10 @@ const searchUsers = debounce(
); );
export default function UserSelector(props: Props): JSX.Element { export default function UserSelector(props: Props): JSX.Element {
const { value, className, onSelect } = props; const {
const [searchPhrase, setSearchPhrase] = useState(''); value, className, username, onSelect,
} = props;
const [searchPhrase, setSearchPhrase] = useState(username || '');
const [initialUsers, setInitialUsers] = useState<User[]>([]); const [initialUsers, setInitialUsers] = useState<User[]>([]);
const [users, setUsers] = useState<User[]>([]); const [users, setUsers] = useState<User[]>([]);
const autocompleteRef = useRef<RefSelectProps | null>(null); const autocompleteRef = useRef<RefSelectProps | null>(null);

@ -150,10 +150,12 @@
} }
.cvat-item-open-task-actions { .cvat-item-open-task-actions {
margin-right: 5px; margin-right: $grid-unit-size;
margin-top: 10px; margin-top: $grid-unit-size;
display: flex; display: flex;
align-items: center; align-items: center;
padding: $grid-unit-size;
line-height: 14px;
} }
.cvat-item-open-task-button { .cvat-item-open-task-button {

@ -158,12 +158,12 @@ class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteCompone
</Col> </Col>
</Row> </Row>
<Row justify='end'> <Row justify='end'>
<Col className='cvat-item-open-task-actions'> <Dropdown overlay={<ActionsMenuContainer taskInstance={taskInstance} />}>
<Text className='cvat-text-color'>Actions</Text> <Col className='cvat-item-open-task-actions'>
<Dropdown overlay={<ActionsMenuContainer taskInstance={taskInstance} />}> <Text className='cvat-text-color'>Actions</Text>
<MoreOutlined className='cvat-menu-icon' /> <MoreOutlined className='cvat-menu-icon' />
</Dropdown> </Col>
</Col> </Dropdown>
</Row> </Row>
</Col> </Col>
); );

@ -1,4 +1,4 @@
// Copyright (C) 2020-2021 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
@ -7,7 +7,10 @@ import { withRouter } from 'react-router-dom';
import { RouteComponentProps } from 'react-router'; import { RouteComponentProps } from 'react-router';
import AnnotationPageComponent from 'components/annotation-page/annotation-page'; 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'; import { CombinedState, Workspace } from 'reducers/interfaces';
@ -18,12 +21,14 @@ type OwnProps = RouteComponentProps<{
interface StateToProps { interface StateToProps {
job: any | null | undefined; job: any | null | undefined;
frameNumber: number;
fetching: boolean; fetching: boolean;
workspace: Workspace; workspace: Workspace;
} }
interface DispatchToProps { interface DispatchToProps {
getJob(): void; getJob(): void;
changeFrame(frame: number): void;
saveLogs(): void; saveLogs(): void;
closeJob(): void; closeJob(): void;
} }
@ -35,6 +40,11 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
annotation: { annotation: {
job: { requestedId, instance: job, fetching }, job: { requestedId, instance: job, fetching },
workspace, workspace,
player: {
frame: {
number: frameNumber,
},
},
}, },
} = state; } = state;
@ -42,6 +52,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
job: jobID === requestedId ? job : null, job: jobID === requestedId ? job : null,
fetching, fetching,
workspace, workspace,
frameNumber,
}; };
} }
@ -71,7 +82,7 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
} }
if (searchParams.has('frame') || searchParams.has('object')) { if (searchParams.has('frame') || searchParams.has('object')) {
own.history.replace(own.history.location.state); own.history.replace(own.history.location.pathname);
} }
return { return {
@ -84,6 +95,9 @@ function mapDispatchToProps(dispatch: any, own: OwnProps): DispatchToProps {
closeJob(): void { closeJob(): void {
dispatch(closeJobAction()); dispatch(closeJobAction());
}, },
changeFrame(frame: number): void {
dispatch(changeFrameAsync(frame));
},
}; };
} }

Loading…
Cancel
Save