未验证 提交 5d318e0f 编写于 作者: K Kirill Lakhov 提交者: GitHub

Incremental jobs update (#42)

上级 24bc214a
...@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ...@@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Support of attributes returned by serverless functions (<https://github.com/cvat-ai/cvat/pull/4>) based on (<https://github.com/openvinotoolkit/cvat/pull/4506>) - Support of attributes returned by serverless functions (<https://github.com/cvat-ai/cvat/pull/4>) based on (<https://github.com/openvinotoolkit/cvat/pull/4506>)
- Project/task backups uploading via chunk uploads (<https://github.com/cvat-ai/cvat/pull/9>) - Project/task backups uploading via chunk uploads (<https://github.com/cvat-ai/cvat/pull/9>)
- Fixed UX bug when jobs pagination is reset after changing a job (<https://github.com/cvat-ai/cvat/pull/42>)
### Changed ### Changed
- Bumped nuclio version to 1.8.14 (<https://github.com/cvat-ai/cvat/pull/29>) - Bumped nuclio version to 1.8.14 (<https://github.com/cvat-ai/cvat/pull/29>)
......
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.38.0", "version": "1.38.1",
"description": "CVAT single-page application", "description": "CVAT single-page application",
"main": "src/index.tsx", "main": "src/index.tsx",
"scripts": { "scripts": {
......
...@@ -441,37 +441,37 @@ export function updateTaskSuccess(task: any, taskID: number): AnyAction { ...@@ -441,37 +441,37 @@ export function updateTaskSuccess(task: any, taskID: number): AnyAction {
return action; return action;
} }
function updateJob(): AnyAction { function updateTaskFailed(error: any, task: any): AnyAction {
const action = { const action = {
type: TasksActionTypes.UPDATE_JOB, type: TasksActionTypes.UPDATE_TASK_FAILED,
payload: { }, payload: { error, task },
}; };
return action; return action;
} }
function updateJobSuccess(jobInstance: any): AnyAction { function updateJob(jobID: number): AnyAction {
const action = { const action = {
type: TasksActionTypes.UPDATE_JOB_SUCCESS, type: TasksActionTypes.UPDATE_JOB,
payload: { jobInstance }, payload: { jobID },
}; };
return action; return action;
} }
function updateJobFailed(jobID: number, error: any): AnyAction { function updateJobSuccess(jobInstance: any, jobID: number): AnyAction {
const action = { const action = {
type: TasksActionTypes.UPDATE_JOB_FAILED, type: TasksActionTypes.UPDATE_JOB_SUCCESS,
payload: { jobID, error }, payload: { jobID, jobInstance },
}; };
return action; return action;
} }
function updateTaskFailed(error: any, task: any): AnyAction { function updateJobFailed(jobID: number, error: any): AnyAction {
const action = { const action = {
type: TasksActionTypes.UPDATE_TASK_FAILED, type: TasksActionTypes.UPDATE_JOB_FAILED,
payload: { error, task }, payload: { jobID, error },
}; };
return action; return action;
...@@ -503,9 +503,9 @@ export function updateTaskAsync(taskInstance: any): ThunkAction<Promise<void>, C ...@@ -503,9 +503,9 @@ export function updateTaskAsync(taskInstance: any): ThunkAction<Promise<void>, C
export function updateJobAsync(jobInstance: any): ThunkAction<Promise<void>, {}, {}, AnyAction> { export function updateJobAsync(jobInstance: any): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => { return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try { try {
dispatch(updateJob()); dispatch(updateJob(jobInstance.id));
const newJob = await jobInstance.save(); const newJob = await jobInstance.save();
dispatch(updateJobSuccess(newJob)); dispatch(updateJobSuccess(newJob, newJob.id));
} catch (error) { } catch (error) {
dispatch(updateJobFailed(jobInstance.id, error)); dispatch(updateJobFailed(jobInstance.id, error));
} }
......
// Copyright (C) 2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Spin, { SpinProps } from 'antd/lib/spin';
function CVATLoadingSpinner(props: SpinProps): JSX.Element {
return (
<div className='cvat-spinner-container'>
<Spin className='cvat-spinner' {...props} />
</div>
);
}
export default React.memo(CVATLoadingSpinner);
...@@ -241,6 +241,7 @@ export default class DetailsComponent extends React.PureComponent<Props, State> ...@@ -241,6 +241,7 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
<UserSelector <UserSelector
value={assignee} value={assignee}
onSelect={(value: User | null): void => { onSelect={(value: User | null): void => {
if (taskInstance?.assignee?.id === value?.id) return;
taskInstance.assignee = value; taskInstance.assignee = value;
onTaskUpdate(taskInstance); onTaskUpdate(taskInstance);
}} }}
......
// Copyright (C) 2020-2021 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
...@@ -234,6 +234,7 @@ function JobListComponent(props: Props & RouteComponentProps): JSX.Element { ...@@ -234,6 +234,7 @@ function JobListComponent(props: Props & RouteComponentProps): JSX.Element {
className='cvat-job-assignee-selector' className='cvat-job-assignee-selector'
value={jobInstance.assignee} value={jobInstance.assignee}
onSelect={(value: User | null): void => { onSelect={(value: User | null): void => {
if (jobInstance?.assignee?.id === value?.id) return;
jobInstance.assignee = value; jobInstance.assignee = value;
onJobUpdate(jobInstance); onJobUpdate(jobInstance);
}} }}
......
...@@ -78,6 +78,11 @@ ...@@ -78,6 +78,11 @@
width: 100%; width: 100%;
} }
.cvat-task-page {
position: relative;
height: 100%;
}
.cvat-task-job-list { .cvat-task-job-list {
width: 100%; width: 100%;
height: auto; height: auto;
......
// Copyright (C) 2020-2021 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
...@@ -13,6 +13,7 @@ import Result from 'antd/lib/result'; ...@@ -13,6 +13,7 @@ import Result from 'antd/lib/result';
import DetailsContainer from 'containers/task-page/details'; import DetailsContainer from 'containers/task-page/details';
import JobListContainer from 'containers/task-page/job-list'; import JobListContainer from 'containers/task-page/job-list';
import ModelRunnerModal from 'components/model-runner-modal/model-runner-dialog'; import ModelRunnerModal from 'components/model-runner-modal/model-runner-dialog';
import CVATLoadingSpinner from 'components/common/loading-spinner';
import MoveTaskModal from 'components/move-task-modal/move-task-modal'; import MoveTaskModal from 'components/move-task-modal/move-task-modal';
import { Task } from 'reducers/interfaces'; import { Task } from 'reducers/interfaces';
import TopBarComponent from './top-bar'; import TopBarComponent from './top-bar';
...@@ -21,6 +22,7 @@ interface TaskPageComponentProps { ...@@ -21,6 +22,7 @@ interface TaskPageComponentProps {
task: Task | null | undefined; task: Task | null | undefined;
fetching: boolean; fetching: boolean;
updating: boolean; updating: boolean;
jobUpdating: boolean;
deleteActivity: boolean | null; deleteActivity: boolean | null;
installedGit: boolean; installedGit: boolean;
getTask: () => void; getTask: () => void;
...@@ -37,12 +39,13 @@ class TaskPageComponent extends React.PureComponent<Props> { ...@@ -37,12 +39,13 @@ class TaskPageComponent extends React.PureComponent<Props> {
} }
} }
public componentDidUpdate(): void { public componentDidUpdate(prevProps: Props): void {
const { const {
deleteActivity, history, task, fetching, getTask, deleteActivity, history, task, fetching, getTask, jobUpdating,
} = this.props; } = this.props;
if (task === null && !fetching) { const jobUpdated = prevProps.jobUpdating && !jobUpdating;
if ((task === null && !fetching) || jobUpdated) {
getTask(); getTask();
} }
...@@ -54,7 +57,7 @@ class TaskPageComponent extends React.PureComponent<Props> { ...@@ -54,7 +57,7 @@ class TaskPageComponent extends React.PureComponent<Props> {
public render(): JSX.Element { public render(): JSX.Element {
const { task, updating, fetching } = this.props; const { task, updating, fetching } = this.props;
if (task === null || fetching) { if (task === null || (fetching && !updating)) {
return <Spin size='large' className='cvat-spinner' />; return <Spin size='large' className='cvat-spinner' />;
} }
...@@ -70,10 +73,9 @@ class TaskPageComponent extends React.PureComponent<Props> { ...@@ -70,10 +73,9 @@ class TaskPageComponent extends React.PureComponent<Props> {
} }
return ( return (
<> <div className='cvat-task-page'>
{ updating ? <Spin size='large' className='cvat-spinner' /> : null } { updating ? <CVATLoadingSpinner size='large' /> : null }
<Row <Row
style={{ display: updating ? 'none' : undefined }}
justify='center' justify='center'
align='top' align='top'
className='cvat-task-details-wrapper' className='cvat-task-details-wrapper'
...@@ -86,7 +88,7 @@ class TaskPageComponent extends React.PureComponent<Props> { ...@@ -86,7 +88,7 @@ class TaskPageComponent extends React.PureComponent<Props> {
</Row> </Row>
<ModelRunnerModal /> <ModelRunnerModal />
<MoveTaskModal /> <MoveTaskModal />
</> </div>
); );
} }
} }
......
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
...@@ -17,6 +17,7 @@ interface StateToProps { ...@@ -17,6 +17,7 @@ interface StateToProps {
task: Task | null | undefined; task: Task | null | undefined;
fetching: boolean; fetching: boolean;
updating: boolean; updating: boolean;
jobUpdating: boolean;
deleteActivity: boolean | null; deleteActivity: boolean | null;
installedGit: boolean; installedGit: boolean;
} }
...@@ -28,8 +29,10 @@ interface DispatchToProps { ...@@ -28,8 +29,10 @@ interface DispatchToProps {
function mapStateToProps(state: CombinedState, own: Props): StateToProps { function mapStateToProps(state: CombinedState, own: Props): StateToProps {
const { list } = state.plugins; const { list } = state.plugins;
const { tasks } = state; const { tasks } = state;
const { gettingQuery, fetching, updating } = tasks; const {
const { deletes } = tasks.activities; gettingQuery, fetching, updating,
} = tasks;
const { deletes, jobUpdates } = tasks.activities;
const id = +own.match.params.id; const id = +own.match.params.id;
...@@ -42,8 +45,13 @@ function mapStateToProps(state: CombinedState, own: Props): StateToProps { ...@@ -42,8 +45,13 @@ function mapStateToProps(state: CombinedState, own: Props): StateToProps {
deleteActivity = deletes[id]; deleteActivity = deletes[id];
} }
const jobIDs = task ? Object.fromEntries(task.instance.jobs.map((job:any) => [job.id])) : {};
const updatingJobs = Object.keys(jobUpdates);
const jobUpdating = updatingJobs.some((jobID) => jobID in jobIDs);
return { return {
task, task,
jobUpdating,
deleteActivity, deleteActivity,
fetching, fetching,
updating, updating,
......
...@@ -116,6 +116,9 @@ export interface TasksState { ...@@ -116,6 +116,9 @@ export interface TasksState {
backups: { backups: {
[tid: number]: boolean; [tid: number]: boolean;
}; };
jobUpdates: {
[jid: number]: boolean,
};
}; };
} }
......
...@@ -8,6 +8,7 @@ import { BoundariesActionTypes } from 'actions/boundaries-actions'; ...@@ -8,6 +8,7 @@ import { BoundariesActionTypes } from 'actions/boundaries-actions';
import { TasksActionTypes } from 'actions/tasks-actions'; import { TasksActionTypes } from 'actions/tasks-actions';
import { AuthActionTypes } from 'actions/auth-actions'; import { AuthActionTypes } from 'actions/auth-actions';
import { AnnotationActionTypes } from 'actions/annotation-actions';
import { TasksState, Task } from './interfaces'; import { TasksState, Task } from './interfaces';
const defaultState: TasksState = { const defaultState: TasksState = {
...@@ -38,6 +39,7 @@ const defaultState: TasksState = { ...@@ -38,6 +39,7 @@ const defaultState: TasksState = {
error: '', error: '',
}, },
backups: {}, backups: {},
jobUpdates: {},
}, },
importing: false, importing: false,
}; };
...@@ -55,7 +57,6 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState ...@@ -55,7 +57,6 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
fetching: true, fetching: true,
hideEmpty: true, hideEmpty: true,
count: 0, count: 0,
current: [],
gettingQuery: action.payload.updateQuery ? { ...action.payload.query } : state.gettingQuery, gettingQuery: action.payload.updateQuery ? { ...action.payload.query } : state.gettingQuery,
}; };
case TasksActionTypes.GET_TASKS_SUCCESS: { case TasksActionTypes.GET_TASKS_SUCCESS: {
...@@ -70,6 +71,7 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState ...@@ -70,6 +71,7 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
...state, ...state,
initialized: true, initialized: true,
fetching: false, fetching: false,
updating: false,
count: action.payload.count, count: action.payload.count,
current: combinedWithPreviews, current: combinedWithPreviews,
}; };
...@@ -313,25 +315,34 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState ...@@ -313,25 +315,34 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
}; };
} }
case TasksActionTypes.UPDATE_JOB: { case TasksActionTypes.UPDATE_JOB: {
const { jobID } = action.payload;
const { jobUpdates } = state.activities;
return { return {
...state, ...state,
updating: true, updating: true,
activities: {
...state.activities,
jobUpdates: {
...jobUpdates,
...Object.fromEntries([[jobID, true]]),
},
},
}; };
} }
case TasksActionTypes.UPDATE_JOB_SUCCESS: { case TasksActionTypes.UPDATE_JOB_SUCCESS:
const { jobInstance } = action.payload; case TasksActionTypes.UPDATE_JOB_FAILED: {
const idx = state.current.findIndex((task: Task) => task.instance.id === jobInstance.taskId); const { jobID } = action.payload;
const newCurrent = idx === -1 ? const { jobUpdates } = state.activities;
state.current : [...(state.current.splice(idx, 1), state.current)];
delete jobUpdates[jobID];
return { return {
...state, ...state,
current: newCurrent, activities: {
gettingQuery: state.gettingQuery.id === jobInstance.taskId ? { ...state.activities,
...state.gettingQuery, jobUpdates: omit(jobUpdates, [jobID]),
id: null, },
} : state.gettingQuery,
updating: false,
}; };
} }
case TasksActionTypes.HIDE_EMPTY_TASKS: { case TasksActionTypes.HIDE_EMPTY_TASKS: {
...@@ -350,6 +361,12 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState ...@@ -350,6 +361,12 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
}, },
}; };
} }
case AnnotationActionTypes.CLOSE_JOB: {
return {
...state,
updating: false,
};
}
case BoundariesActionTypes.RESET_AFTER_ERROR: case BoundariesActionTypes.RESET_AFTER_ERROR:
case AuthActionTypes.LOGOUT_SUCCESS: { case AuthActionTypes.LOGOUT_SUCCESS: {
return { ...defaultState }; return { ...defaultState };
......
...@@ -17,6 +17,17 @@ hr { ...@@ -17,6 +17,17 @@ hr {
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
} }
.cvat-spinner-container {
position: absolute;
background: $background-color-1;
opacity: 0.5;
width: 100%;
height: 100%;
z-index: 2;
top: 0;
left: 0;
}
.cvat-not-found { .cvat-not-found {
margin: 10% 25%; margin: 10% 25%;
} }
......
...@@ -49,8 +49,8 @@ context('Filtering, sorting jobs.', () => { ...@@ -49,8 +49,8 @@ context('Filtering, sorting jobs.', () => {
cy.get('.cvat-task-jobs-table') cy.get('.cvat-task-jobs-table')
.contains('a', `Job #${$job}`) .contains('a', `Job #${$job}`)
.parents('.cvat-task-jobs-table-row').within(() => { .parents('.cvat-task-jobs-table-row').within(() => {
cy.get('.cvat-job-item-stage').invoke('text').should('equal', stage); cy.get('.cvat-job-item-stage .ant-select-selection-item').should('have.text', stage);
cy.get('.cvat-job-item-state').invoke('text').should('equal', state); cy.get('.cvat-job-item-state').should('have.text', state);
cy.get('.cvat-job-item-assignee') cy.get('.cvat-job-item-assignee')
.find('[type="search"]') .find('[type="search"]')
.invoke('val') .invoke('val')
......
// Copyright (C) 2020 Intel Corporation // Copyright (C) 2020-2022 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
...@@ -23,7 +23,7 @@ context('Value must be a user instance.', () => { ...@@ -23,7 +23,7 @@ context('Value must be a user instance.', () => {
.within(() => { .within(() => {
cy.get(`.ant-select-item-option[title="${Cypress.env('user')}"]`).click(); cy.get(`.ant-select-item-option[title="${Cypress.env('user')}"]`).click();
}); });
cy.get('.cvat-spinner').should('exist'); cy.get('.cvat-spinner').should('not.exist');
}); });
it('Assign the task to the same user again', () => { it('Assign the task to the same user again', () => {
cy.get('.cvat-task-details-user-block').within(() => { cy.get('.cvat-task-details-user-block').within(() => {
......
...@@ -698,6 +698,7 @@ Cypress.Commands.add('addNewLabel', (newLabelName, additionalAttrs, labelColor) ...@@ -698,6 +698,7 @@ Cypress.Commands.add('addNewLabel', (newLabelName, additionalAttrs, labelColor)
} }
} }
cy.contains('button', 'Done').click(); cy.contains('button', 'Done').click();
cy.get('.cvat-spinner').should('not.exist');
cy.get('.cvat-constructor-viewer').should('be.visible'); cy.get('.cvat-constructor-viewer').should('be.visible');
cy.contains('.cvat-constructor-viewer-item', new RegExp(`^${newLabelName}$`)).should('exist'); cy.contains('.cvat-constructor-viewer-item', new RegExp(`^${newLabelName}$`)).should('exist');
}); });
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册