From 696e51fe2e6b309eec90f9db9c0da77a92484255 Mon Sep 17 00:00:00 2001 From: Boris Sekachev Date: Wed, 9 Feb 2022 11:19:19 +0300 Subject: [PATCH] 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 --- CHANGELOG.md | 2 + cvat-ui/package-lock.json | 4 +- cvat-ui/package.json | 2 +- cvat-ui/src/actions/annotation-actions.ts | 16 ++-- .../annotation-page/annotation-page.tsx | 75 ++++++++++++++----- .../components/annotation-page/styles.scss | 14 ++++ cvat-ui/src/components/jobs-page/styles.scss | 6 +- cvat-ui/src/components/jobs-page/top-bar.tsx | 29 +++---- .../src/components/project-page/styles.scss | 3 +- .../src/components/project-page/top-bar.tsx | 2 +- cvat-ui/src/components/task-page/styles.scss | 3 +- cvat-ui/src/components/task-page/top-bar.tsx | 2 +- .../components/task-page/user-selector.tsx | 9 ++- cvat-ui/src/components/tasks-page/styles.scss | 6 +- .../src/components/tasks-page/task-item.tsx | 10 +-- .../annotation-page/annotation-page.tsx | 20 ++++- 16 files changed, 141 insertions(+), 62 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 948a8806..6bc812fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 () - Basic page with jobs list, basic filtration to this list () - Added OpenCV.js TrackerMIL as tracking tool () +- Ability to continue working from the latest frame where an annotator was before () + ### Changed - Users don't have access to a task object anymore if they are assigneed only on some jobs of the task () diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 0fba585d..b2303605 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -1,12 +1,12 @@ { "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", diff --git a/cvat-ui/package.json b/cvat-ui/package.json index c59d128f..e3d604dc 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -1,6 +1,6 @@ { "name": "cvat-ui", - "version": "1.35.0", + "version": "1.35.1", "description": "CVAT single-page application", "main": "src/index.tsx", "scripts": { diff --git a/cvat-ui/src/actions/annotation-actions.ts b/cvat-ui/src/actions/annotation-actions.ts index b1c30398..00869d1a 100644 --- a/cvat-ui/src/actions/annotation-actions.ts +++ b/cvat-ui/src/actions/annotation-actions.ts @@ -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; diff --git a/cvat-ui/src/components/annotation-page/annotation-page.tsx b/cvat-ui/src/components/annotation-page/annotation-page.tsx index 8ff586e5..2d1e69c3 100644 --- a/cvat-ui/src/components/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/components/annotation-page/annotation-page.tsx @@ -1,4 +1,4 @@ -// 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: ( - - {`${job.projectId ? 'Project' : 'Task'} ${ - job.projectId || job.taskId - } does not contain any label. `} - - Add - - {' the first one for editing annotation.'} - - ), - 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: ( + + Press + + if you would like to continue + + ), + placement: 'topRight', + className: 'cvat-notification-continue-job', + }); + } + } + + if (!job.labels.length) { + notification.warning({ + message: 'No labels', + description: ( + + {`${job.projectId ? 'Project' : 'Task'} ${ + job.projectId || job.taskId + } does not contain any label. `} + + Add + + {' the first one for editing annotation.'} + + ), + placement: 'topRight', + className: 'cvat-notification-no-labels', + }); + } } }, [job, fetching, prevJob, prevFetching]); diff --git a/cvat-ui/src/components/annotation-page/styles.scss b/cvat-ui/src/components/annotation-page/styles.scss index 1b34ce3e..6067596c 100644 --- a/cvat-ui/src/components/annotation-page/styles.scss +++ b/cvat-ui/src/components/annotation-page/styles.scss @@ -495,3 +495,17 @@ button.cvat-predictor-button { font-weight: bold; } + +.cvat-continue-job-button { + padding: 0; + + > span { + &::before { + content: '\00a0'; + } + + &::after { + content: '\00a0'; + } + } +} diff --git a/cvat-ui/src/components/jobs-page/styles.scss b/cvat-ui/src/components/jobs-page/styles.scss index 7a85de52..572c990f 100644 --- a/cvat-ui/src/components/jobs-page/styles.scss +++ b/cvat-ui/src/components/jobs-page/styles.scss @@ -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; +} diff --git a/cvat-ui/src/components/jobs-page/top-bar.tsx b/cvat-ui/src/components/jobs-page/top-bar.tsx index 40319632..e27d02b7 100644 --- a/cvat-ui/src/components/jobs-page/top-bar.tsx +++ b/cvat-ui/src/components/jobs-page/top-bar.tsx @@ -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: ( -
- { - onChangeFilters({ assignee: value }); +
+ { + if (value) { + if (query.assignee !== value.username) { + onChangeFilters({ assignee: value.username }); + } + } else if (query.assignee !== null) { + onChangeFilters({ assignee: null }); + } }} - enterButton /> -
diff --git a/cvat-ui/src/components/project-page/styles.scss b/cvat-ui/src/components/project-page/styles.scss index ac28906a..ea559f91 100644 --- a/cvat-ui/src/components/project-page/styles.scss +++ b/cvat-ui/src/components/project-page/styles.scss @@ -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 { diff --git a/cvat-ui/src/components/project-page/top-bar.tsx b/cvat-ui/src/components/project-page/top-bar.tsx index 73ad19db..6764ca68 100644 --- a/cvat-ui/src/components/project-page/top-bar.tsx +++ b/cvat-ui/src/components/project-page/top-bar.tsx @@ -32,7 +32,7 @@ export default function ProjectTopBar(props: DetailsComponentProps): JSX.Element }> - diff --git a/cvat-ui/src/components/task-page/styles.scss b/cvat-ui/src/components/task-page/styles.scss index 27034d76..335c0f19 100644 --- a/cvat-ui/src/components/task-page/styles.scss +++ b/cvat-ui/src/components/task-page/styles.scss @@ -38,9 +38,10 @@ } } - > div > div > div > button { + .cvat-task-page-actions-button { display: flex; align-items: center; + line-height: 14px; } } diff --git a/cvat-ui/src/components/task-page/top-bar.tsx b/cvat-ui/src/components/task-page/top-bar.tsx index 6a50eb0e..0a9195d8 100644 --- a/cvat-ui/src/components/task-page/top-bar.tsx +++ b/cvat-ui/src/components/task-page/top-bar.tsx @@ -42,7 +42,7 @@ export default function DetailsComponent(props: DetailsComponentProps): JSX.Elem }> - diff --git a/cvat-ui/src/components/task-page/user-selector.tsx b/cvat-ui/src/components/task-page/user-selector.tsx index 7bb5ffbd..ca6bb719 100644 --- a/cvat-ui/src/components/task-page/user-selector.tsx +++ b/cvat-ui/src/components/task-page/user-selector.tsx @@ -1,4 +1,4 @@ -// 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([]); const [users, setUsers] = useState([]); const autocompleteRef = useRef(null); diff --git a/cvat-ui/src/components/tasks-page/styles.scss b/cvat-ui/src/components/tasks-page/styles.scss index 669307b1..57c8b6c3 100644 --- a/cvat-ui/src/components/tasks-page/styles.scss +++ b/cvat-ui/src/components/tasks-page/styles.scss @@ -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 { diff --git a/cvat-ui/src/components/tasks-page/task-item.tsx b/cvat-ui/src/components/tasks-page/task-item.tsx index 44755a65..ae3db610 100644 --- a/cvat-ui/src/components/tasks-page/task-item.tsx +++ b/cvat-ui/src/components/tasks-page/task-item.tsx @@ -158,12 +158,12 @@ class TaskItemComponent extends React.PureComponent - - Actions - }> + }> + + Actions - - + + ); diff --git a/cvat-ui/src/containers/annotation-page/annotation-page.tsx b/cvat-ui/src/containers/annotation-page/annotation-page.tsx index 7dcb11b6..6f46bd06 100644 --- a/cvat-ui/src/containers/annotation-page/annotation-page.tsx +++ b/cvat-ui/src/containers/annotation-page/annotation-page.tsx @@ -1,4 +1,4 @@ -// 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)); + }, }; }