未验证 提交 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
### 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>)
- 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
- Bumped nuclio version to 1.8.14 (<https://github.com/cvat-ai/cvat/pull/29>)
......
{
"name": "cvat-ui",
"version": "1.38.0",
"version": "1.38.1",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
......
......@@ -441,37 +441,37 @@ export function updateTaskSuccess(task: any, taskID: number): AnyAction {
return action;
}
function updateJob(): AnyAction {
function updateTaskFailed(error: any, task: any): AnyAction {
const action = {
type: TasksActionTypes.UPDATE_JOB,
payload: { },
type: TasksActionTypes.UPDATE_TASK_FAILED,
payload: { error, task },
};
return action;
}
function updateJobSuccess(jobInstance: any): AnyAction {
function updateJob(jobID: number): AnyAction {
const action = {
type: TasksActionTypes.UPDATE_JOB_SUCCESS,
payload: { jobInstance },
type: TasksActionTypes.UPDATE_JOB,
payload: { jobID },
};
return action;
}
function updateJobFailed(jobID: number, error: any): AnyAction {
function updateJobSuccess(jobInstance: any, jobID: number): AnyAction {
const action = {
type: TasksActionTypes.UPDATE_JOB_FAILED,
payload: { jobID, error },
type: TasksActionTypes.UPDATE_JOB_SUCCESS,
payload: { jobID, jobInstance },
};
return action;
}
function updateTaskFailed(error: any, task: any): AnyAction {
function updateJobFailed(jobID: number, error: any): AnyAction {
const action = {
type: TasksActionTypes.UPDATE_TASK_FAILED,
payload: { error, task },
type: TasksActionTypes.UPDATE_JOB_FAILED,
payload: { jobID, error },
};
return action;
......@@ -503,9 +503,9 @@ export function updateTaskAsync(taskInstance: any): ThunkAction<Promise<void>, C
export function updateJobAsync(jobInstance: any): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
dispatch(updateJob());
dispatch(updateJob(jobInstance.id));
const newJob = await jobInstance.save();
dispatch(updateJobSuccess(newJob));
dispatch(updateJobSuccess(newJob, newJob.id));
} catch (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>
<UserSelector
value={assignee}
onSelect={(value: User | null): void => {
if (taskInstance?.assignee?.id === value?.id) return;
taskInstance.assignee = value;
onTaskUpdate(taskInstance);
}}
......
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -234,6 +234,7 @@ function JobListComponent(props: Props & RouteComponentProps): JSX.Element {
className='cvat-job-assignee-selector'
value={jobInstance.assignee}
onSelect={(value: User | null): void => {
if (jobInstance?.assignee?.id === value?.id) return;
jobInstance.assignee = value;
onJobUpdate(jobInstance);
}}
......
......@@ -78,6 +78,11 @@
width: 100%;
}
.cvat-task-page {
position: relative;
height: 100%;
}
.cvat-task-job-list {
width: 100%;
height: auto;
......
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -13,6 +13,7 @@ import Result from 'antd/lib/result';
import DetailsContainer from 'containers/task-page/details';
import JobListContainer from 'containers/task-page/job-list';
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 { Task } from 'reducers/interfaces';
import TopBarComponent from './top-bar';
......@@ -21,6 +22,7 @@ interface TaskPageComponentProps {
task: Task | null | undefined;
fetching: boolean;
updating: boolean;
jobUpdating: boolean;
deleteActivity: boolean | null;
installedGit: boolean;
getTask: () => void;
......@@ -37,12 +39,13 @@ class TaskPageComponent extends React.PureComponent<Props> {
}
}
public componentDidUpdate(): void {
public componentDidUpdate(prevProps: Props): void {
const {
deleteActivity, history, task, fetching, getTask,
deleteActivity, history, task, fetching, getTask, jobUpdating,
} = this.props;
if (task === null && !fetching) {
const jobUpdated = prevProps.jobUpdating && !jobUpdating;
if ((task === null && !fetching) || jobUpdated) {
getTask();
}
......@@ -54,7 +57,7 @@ class TaskPageComponent extends React.PureComponent<Props> {
public render(): JSX.Element {
const { task, updating, fetching } = this.props;
if (task === null || fetching) {
if (task === null || (fetching && !updating)) {
return <Spin size='large' className='cvat-spinner' />;
}
......@@ -70,10 +73,9 @@ class TaskPageComponent extends React.PureComponent<Props> {
}
return (
<>
{ updating ? <Spin size='large' className='cvat-spinner' /> : null }
<div className='cvat-task-page'>
{ updating ? <CVATLoadingSpinner size='large' /> : null }
<Row
style={{ display: updating ? 'none' : undefined }}
justify='center'
align='top'
className='cvat-task-details-wrapper'
......@@ -86,7 +88,7 @@ class TaskPageComponent extends React.PureComponent<Props> {
</Row>
<ModelRunnerModal />
<MoveTaskModal />
</>
</div>
);
}
}
......
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -17,6 +17,7 @@ interface StateToProps {
task: Task | null | undefined;
fetching: boolean;
updating: boolean;
jobUpdating: boolean;
deleteActivity: boolean | null;
installedGit: boolean;
}
......@@ -28,8 +29,10 @@ interface DispatchToProps {
function mapStateToProps(state: CombinedState, own: Props): StateToProps {
const { list } = state.plugins;
const { tasks } = state;
const { gettingQuery, fetching, updating } = tasks;
const { deletes } = tasks.activities;
const {
gettingQuery, fetching, updating,
} = tasks;
const { deletes, jobUpdates } = tasks.activities;
const id = +own.match.params.id;
......@@ -42,8 +45,13 @@ function mapStateToProps(state: CombinedState, own: Props): StateToProps {
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 {
task,
jobUpdating,
deleteActivity,
fetching,
updating,
......
......@@ -116,6 +116,9 @@ export interface TasksState {
backups: {
[tid: number]: boolean;
};
jobUpdates: {
[jid: number]: boolean,
};
};
}
......
......@@ -8,6 +8,7 @@ import { BoundariesActionTypes } from 'actions/boundaries-actions';
import { TasksActionTypes } from 'actions/tasks-actions';
import { AuthActionTypes } from 'actions/auth-actions';
import { AnnotationActionTypes } from 'actions/annotation-actions';
import { TasksState, Task } from './interfaces';
const defaultState: TasksState = {
......@@ -38,6 +39,7 @@ const defaultState: TasksState = {
error: '',
},
backups: {},
jobUpdates: {},
},
importing: false,
};
......@@ -55,7 +57,6 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
fetching: true,
hideEmpty: true,
count: 0,
current: [],
gettingQuery: action.payload.updateQuery ? { ...action.payload.query } : state.gettingQuery,
};
case TasksActionTypes.GET_TASKS_SUCCESS: {
......@@ -70,6 +71,7 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
...state,
initialized: true,
fetching: false,
updating: false,
count: action.payload.count,
current: combinedWithPreviews,
};
......@@ -313,25 +315,34 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
};
}
case TasksActionTypes.UPDATE_JOB: {
const { jobID } = action.payload;
const { jobUpdates } = state.activities;
return {
...state,
updating: true,
activities: {
...state.activities,
jobUpdates: {
...jobUpdates,
...Object.fromEntries([[jobID, true]]),
},
},
};
}
case TasksActionTypes.UPDATE_JOB_SUCCESS: {
const { jobInstance } = action.payload;
const idx = state.current.findIndex((task: Task) => task.instance.id === jobInstance.taskId);
const newCurrent = idx === -1 ?
state.current : [...(state.current.splice(idx, 1), state.current)];
case TasksActionTypes.UPDATE_JOB_SUCCESS:
case TasksActionTypes.UPDATE_JOB_FAILED: {
const { jobID } = action.payload;
const { jobUpdates } = state.activities;
delete jobUpdates[jobID];
return {
...state,
current: newCurrent,
gettingQuery: state.gettingQuery.id === jobInstance.taskId ? {
...state.gettingQuery,
id: null,
} : state.gettingQuery,
updating: false,
activities: {
...state.activities,
jobUpdates: omit(jobUpdates, [jobID]),
},
};
}
case TasksActionTypes.HIDE_EMPTY_TASKS: {
......@@ -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 AuthActionTypes.LOGOUT_SUCCESS: {
return { ...defaultState };
......
......@@ -17,6 +17,17 @@ hr {
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 {
margin: 10% 25%;
}
......
......@@ -49,8 +49,8 @@ context('Filtering, sorting jobs.', () => {
cy.get('.cvat-task-jobs-table')
.contains('a', `Job #${$job}`)
.parents('.cvat-task-jobs-table-row').within(() => {
cy.get('.cvat-job-item-stage').invoke('text').should('equal', stage);
cy.get('.cvat-job-item-state').invoke('text').should('equal', state);
cy.get('.cvat-job-item-stage .ant-select-selection-item').should('have.text', stage);
cy.get('.cvat-job-item-state').should('have.text', state);
cy.get('.cvat-job-item-assignee')
.find('[type="search"]')
.invoke('val')
......
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -23,7 +23,7 @@ context('Value must be a user instance.', () => {
.within(() => {
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', () => {
cy.get('.cvat-task-details-user-block').within(() => {
......
......@@ -698,6 +698,7 @@ Cypress.Commands.add('addNewLabel', (newLabelName, additionalAttrs, labelColor)
}
}
cy.contains('button', 'Done').click();
cy.get('.cvat-spinner').should('not.exist');
cy.get('.cvat-constructor-viewer').should('be.visible');
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.
先完成此消息的编辑!
想要评论请 注册