未验证 提交 7f86a5d8 编写于 作者: B Boris Sekachev 提交者: GitHub

Added a page with jobs (#4258)

上级 bec253f0
......@@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Support for working with ellipses (<https://github.com/openvinotoolkit/cvat/pull/4062>)
- Add several flags to task creation CLI (<https://github.com/openvinotoolkit/cvat/pull/4119>)
- 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>)
### 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>)
- Different resources (tasks, projects) are not visible anymore for all CVAT instance users by default (<https://github.com/openvinotoolkit/cvat/pull/3788>)
......
{
"name": "cvat-core",
"version": "4.1.2",
"version": "4.2.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-core",
"version": "4.1.2",
"version": "4.2.0",
"license": "MIT",
"dependencies": {
"axios": "^0.21.4",
......
{
"name": "cvat-core",
"version": "4.1.2",
"version": "4.2.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {
......
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -152,16 +152,16 @@ const config = require('./config');
cvat.jobs.get.implementation = async (filter) => {
checkFilter(filter, {
page: isInteger,
stage: isString,
state: isString,
assignee: isString,
taskID: isInteger,
jobID: isInteger,
});
if ('taskID' in filter && 'jobID' in filter) {
throw new ArgumentError('Only one of fields "taskID" and "jobID" allowed simultaneously');
}
if (!Object.keys(filter).length) {
throw new ArgumentError('Job filter must not be empty');
throw new ArgumentError('Filter fields "taskID" and "jobID" are not permitted to be used at the same time');
}
if ('taskID' in filter) {
......@@ -173,12 +173,17 @@ const config = require('./config');
return [];
}
const job = await serverProxy.jobs.get(filter.jobID);
if ('jobID' in filter) {
const job = await serverProxy.jobs.get({ id: filter.jobID });
if (job) {
return [new Job(job)];
}
}
return [];
const jobsData = await serverProxy.jobs.get(filter);
const jobs = jobsData.results.map((jobData) => new Job(jobData));
jobs.count = jobsData.count;
return jobs;
};
cvat.tasks.get.implementation = async (filter) => {
......
......@@ -637,11 +637,11 @@
return frameDataCache[taskID].frameBuffer.getContextImage(frame);
}
async function getPreview(taskID) {
async function getPreview(taskID = null, jobID = null) {
return new Promise((resolve, reject) => {
// Just go to server and get preview (no any cache)
serverProxy.frames
.getPreview(taskID)
.getPreview(taskID, jobID)
.then((result) => {
if (isNode) {
// eslint-disable-next-line no-undef
......
// Copyright (C) 2019-2021 Intel Corporation
// Copyright (C) 2019-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -924,14 +924,25 @@
return createdTask[0];
}
async function getJob(jobID) {
async function getJobs(filter = {}) {
const { backendAPI } = config;
const id = filter.id || null;
let response = null;
try {
response = await Axios.get(`${backendAPI}/jobs/${jobID}`, {
if (id !== null) {
response = await Axios.get(`${backendAPI}/jobs/${id}`, {
proxy: config.proxy,
});
} else {
response = await Axios.get(`${backendAPI}/jobs`, {
proxy: config.proxy,
params: {
...filter,
page_size: 12,
},
});
}
} catch (errorData) {
throw generateError(errorData);
}
......@@ -1069,12 +1080,13 @@
return response.data;
}
async function getPreview(tid) {
async function getPreview(tid, jid) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/tasks/${tid}/data`, {
const url = `${backendAPI}/${jid !== null ? 'jobs' : 'tasks'}/${jid || tid}/data`;
response = await Axios.get(url, {
params: {
type: 'preview',
},
......@@ -1800,7 +1812,7 @@
jobs: {
value: Object.freeze({
get: getJob,
get: getJobs,
save: saveJob,
}),
writable: false,
......
......@@ -1887,7 +1887,11 @@
};
Job.prototype.frames.preview.implementation = async function () {
const frameData = await getPreview(this.taskId);
if (this.id === null || this.taskId === null) {
return '';
}
const frameData = await getPreview(this.taskId, this.jobID);
return frameData;
};
......@@ -2220,6 +2224,10 @@
};
Task.prototype.frames.preview.implementation = async function () {
if (this.id === null) {
return '';
}
const frameData = await getPreview(this.id);
return frameData;
};
......
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -212,7 +212,8 @@ class ServerProxy {
}
}
async function getJob(jobID) {
async function getJobs(filter = {}) {
const id = filter.id || null;
const jobs = tasksDummyData.results
.reduce((acc, task) => {
for (const segment of task.segments) {
......@@ -234,7 +235,7 @@ class ServerProxy {
return acc;
}, [])
.filter((job) => job.id === jobID);
.filter((job) => job.id === id);
return (
jobs[0] || {
......@@ -265,7 +266,7 @@ class ServerProxy {
}
}
return getJob(id);
return getJobs({ id });
}
async function getUsers() {
......@@ -423,7 +424,7 @@ class ServerProxy {
jobs: {
value: Object.freeze({
get: getJob,
get: getJobs,
save: saveJob,
}),
writable: false,
......
{
"name": "cvat-ui",
"version": "1.33.3",
"version": "1.34.0",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "cvat-ui",
"version": "1.33.3",
"version": "1.34.0",
"license": "MIT",
"dependencies": {
"@ant-design/icons": "^4.6.3",
......
{
"name": "cvat-ui",
"version": "1.33.3",
"version": "1.34.0",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
......
// Copyright (C) 2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
import getCore from 'cvat-core-wrapper';
import { JobsQuery } from 'reducers/interfaces';
const cvat = getCore();
export enum JobsActionTypes {
GET_JOBS = 'GET_JOBS',
GET_JOBS_SUCCESS = 'GET_JOBS_SUCCESS',
GET_JOBS_FAILED = 'GET_JOBS_FAILED',
}
interface JobsList extends Array<any> {
count: number;
}
const jobsActions = {
getJobs: (query: Partial<JobsQuery>) => createAction(JobsActionTypes.GET_JOBS, { query }),
getJobsSuccess: (jobs: JobsList, previews: string[]) => (
createAction(JobsActionTypes.GET_JOBS_SUCCESS, { jobs, previews })
),
getJobsFailed: (error: any) => createAction(JobsActionTypes.GET_JOBS_FAILED, { error }),
};
export type JobsActions = ActionUnion<typeof jobsActions>;
export const getJobsAsync = (query: JobsQuery): ThunkAction => async (dispatch) => {
try {
// Remove all keys with null values from the query
const filteredQuery: Partial<JobsQuery> = { ...query };
for (const [key, value] of Object.entries(filteredQuery)) {
if (value === null) {
delete filteredQuery[key];
}
}
dispatch(jobsActions.getJobs(filteredQuery));
const jobs = await cvat.jobs.get(filteredQuery);
const previewPromises = jobs.map((job: any) => (job as any).frames.preview().catch(() => ''));
dispatch(jobsActions.getJobsSuccess(jobs, await Promise.all(previewPromises)));
} catch (error) {
dispatch(jobsActions.getJobsFailed(error));
}
};
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -26,6 +26,8 @@ import ShortcutsDialog from 'components/shortcuts-dialog/shortcuts-dialog';
import ExportDatasetModal from 'components/export-dataset/export-dataset-modal';
import ModelsPageContainer from 'containers/models-page/models-page';
import JobsPageComponent from 'components/jobs-page/jobs-page';
import TasksPageContainer from 'containers/tasks-page/tasks-page';
import CreateTaskPageContainer from 'containers/create-task-page/create-task-page';
import TaskPageContainer from 'containers/task-page/task-page';
......@@ -360,6 +362,7 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
<Route exact path='/tasks/create' component={CreateTaskPageContainer} />
<Route exact path='/tasks/:id' component={TaskPageContainer} />
<Route exact path='/tasks/:tid/jobs/:jid' component={AnnotationPageContainer} />
<Route exact path='/jobs' component={JobsPageComponent} />
<Route exact path='/cloudstorages' component={CloudStoragesPageComponent} />
<Route
exact
......
......@@ -378,7 +378,7 @@ function HeaderContainer(props: Props): JSX.Element {
className='cvat-header-button'
type='link'
value='projects'
href='/projects'
href='/projects?page=1'
onClick={(event: React.MouseEvent): void => {
event.preventDefault();
history.push('/projects');
......@@ -398,6 +398,18 @@ function HeaderContainer(props: Props): JSX.Element {
>
Tasks
</Button>
<Button
className='cvat-header-button'
type='link'
value='jobs'
href='/jobs?page=1'
onClick={(event: React.MouseEvent): void => {
event.preventDefault();
history.push('/jobs');
}}
>
Jobs
</Button>
<Button
className='cvat-header-button'
type='link'
......
......@@ -27,8 +27,6 @@
align-items: center;
> a.ant-btn {
height: 24px;
span[role='img'] {
font-size: 24px;
line-height: 24px;
......
// Copyright (C) 2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState } from 'react';
import { useHistory } from 'react-router';
import Card from 'antd/lib/card';
import Empty from 'antd/lib/empty';
import Descriptions from 'antd/lib/descriptions';
import { MoreOutlined } from '@ant-design/icons';
import Dropdown from 'antd/lib/dropdown';
import Menu from 'antd/lib/menu';
// eslint-disable-next-line import/no-extraneous-dependencies
import { MenuInfo } from 'rc-menu/lib/interface';
import { useCardHeightHOC } from 'utils/hooks';
const useCardHeight = useCardHeightHOC({
containerClassName: 'cvat-jobs-page',
siblingClassNames: ['cvat-jobs-page-pagination', 'cvat-jobs-page-top-bar'],
paddings: 40,
numberOfRows: 3,
});
interface Props {
job: any;
preview: string;
}
function JobCardComponent(props: Props): JSX.Element {
const { job, preview } = props;
const [expanded, setExpanded] = useState<boolean>(false);
const history = useHistory();
const height = useCardHeight();
const onClick = (): void => {
history.push(`/tasks/${job.taskId}/jobs/${job.id}`);
};
return (
<Card
onMouseEnter={() => setExpanded(true)}
onMouseLeave={() => setExpanded(false)}
style={{ height }}
className='cvat-job-page-list-item'
cover={(
<>
{preview ? (
<img
className='cvat-jobs-page-job-item-card-preview'
src={preview}
alt='Preview'
onClick={onClick}
aria-hidden
/>
) : (
<div className='cvat-jobs-page-job-item-card-preview' onClick={onClick} aria-hidden>
<Empty description='Preview not found' />
</div>
)}
<div className='cvat-job-page-list-item-id'>
ID:
{` ${job.id}`}
</div>
<div className='cvat-job-page-list-item-dimension'>{job.dimension.toUpperCase()}</div>
</>
)}
>
<Descriptions column={1} size='small'>
<Descriptions.Item label='Stage'>{job.stage}</Descriptions.Item>
<Descriptions.Item label='State'>{job.state}</Descriptions.Item>
{ expanded ? (
<Descriptions.Item label='Size'>{job.stopFrame - job.startFrame + 1}</Descriptions.Item>
) : null}
{ expanded && job.assignee ? (
<Descriptions.Item label='Assignee'>{job.assignee.username}</Descriptions.Item>
) : null}
</Descriptions>
<Dropdown overlay={(
<Menu onClick={(action: MenuInfo) => {
if (action.key === 'task') {
history.push(`/tasks/${job.taskId}`);
} else if (action.key === 'project') {
history.push(`/projects/${job.projectId}`);
} else if (action.key === 'bug_tracker') {
// false alarm
// eslint-disable-next-line security/detect-non-literal-fs-filename
window.open(job.bugTracker, '_blank', 'noopener noreferrer');
}
}}
>
<Menu.Item key='task' disabled={job.taskId === null}>Go to the task</Menu.Item>
<Menu.Item key='project' disabled={job.projectId === null}>Go to the project</Menu.Item>
<Menu.Item key='bug_tracker' disabled={!job.bugTracker}>Go to the bug tracker</Menu.Item>
</Menu>
)}
>
<MoreOutlined className='cvat-job-card-more-button' />
</Dropdown>
</Card>
);
}
export default React.memo(JobCardComponent);
// Copyright (C) 2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { useSelector } from 'react-redux';
import { Col, Row } from 'antd/lib/grid';
import { CombinedState } from 'reducers/interfaces';
import JobCard from './job-card';
function JobsContentComponent(): JSX.Element {
const jobs = useSelector((state: CombinedState) => state.jobs.current);
const previews = useSelector((state: CombinedState) => state.jobs.previews);
const dimensions = {
md: 22,
lg: 18,
xl: 16,
xxl: 16,
};
return (
<Row justify='center' align='middle'>
<Col className='cvat-jobs-page-list' {...dimensions}>
{jobs.map((job: any, idx: number): JSX.Element => (
<JobCard preview={previews[idx]} job={job} key={job.id} />
))}
</Col>
</Row>
);
}
export default React.memo(JobsContentComponent);
// Copyright (C) 2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, { useEffect } from 'react';
import { useHistory } from 'react-router';
import { useDispatch, useSelector } from 'react-redux';
import Spin from 'antd/lib/spin';
import { Col, Row } from 'antd/lib/grid';
import Pagination from 'antd/lib/pagination';
import Empty from 'antd/lib/empty';
import { CombinedState } from 'reducers/interfaces';
import { getJobsAsync } from 'actions/jobs-actions';
import TopBarComponent from './top-bar';
import JobsContentComponent from './jobs-content';
function JobsPageComponent(): JSX.Element {
const dispatch = useDispatch();
const query = useSelector((state: CombinedState) => state.jobs.query);
const fetching = useSelector((state: CombinedState) => state.jobs.fetching);
const count = useSelector((state: CombinedState) => state.jobs.count);
const history = useHistory();
useEffect(() => {
// get relevant query parameters from the url and fetch jobs according to them
const { location } = history;
const searchParams = new URLSearchParams(location.search);
const copiedQuery = { ...query };
for (const key of Object.keys(copiedQuery)) {
if (searchParams.has(key)) {
const value = searchParams.get(key);
if (value) {
copiedQuery[key] = key === 'page' ? +value : value;
}
} else {
copiedQuery[key] = null;
}
}
dispatch(getJobsAsync(copiedQuery));
}, []);
useEffect(() => {
// when query is updated, set relevant search params to url
const searchParams = new URLSearchParams();
const { location } = history;
for (const [key, value] of Object.entries(query)) {
if (value) {
searchParams.set(key, value.toString());
}
}
history.push(`${location.pathname}?${searchParams.toString()}`);
}, [query]);
if (fetching) {
return (
<div className='cvat-jobs-page'>
<Spin size='large' className='cvat-spinner' />
</div>
);
}
const dimensions = {
md: 22,
lg: 18,
xl: 16,
xxl: 16,
};
return (
<div className='cvat-jobs-page'>
<TopBarComponent
query={query}
onChangeFilters={(filters: Record<string, string | null>) => {
dispatch(
getJobsAsync({
...query,
...filters,
page: 1,
}),
);
}}
/>
{count ? (
<>
<JobsContentComponent />
<Row justify='space-around' about='middle'>
<Col {...dimensions}>
<Pagination
className='cvat-jobs-page-pagination'
onChange={(page: number) => {
dispatch(getJobsAsync({
...query,
page,
}));
}}
showSizeChanger={false}
total={count}
pageSize={12}
current={query.page}
showQuickJumper
/>
</Col>
</Row>
</>
) : <Empty />}
</div>
);
}
export default React.memo(JobsPageComponent);
// Copyright (C) 2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
@import '../../base.scss';
.cvat-jobs-page {
padding-top: $grid-unit-size * 2;
padding-bottom: $grid-unit-size;
height: 100%;
width: 100%;
.cvat-jobs-page-top-bar {
> div:nth-child(1) {
> div:nth-child(1) {
width: 100%;
> div:nth-child(1) {
display: flex;
span {
margin-right: $grid-unit-size;
}
}
}
}
}
> div:nth-child(1) {
div > {
.cvat-title {
color: $text-color;
}
}
}
> div:nth-child(2) {
&.ant-empty {
position: absolute;
top: 40%;
left: 50%;
}
padding-bottom: $grid-unit-size;
padding-top: $grid-unit-size;
}
.cvat-job-page-list-item {
width: 25%;
border-width: $grid-unit-size / 2;
display: flex;
flex-direction: column;
.ant-card-cover {
flex: 1;
height: 0;
margin: 0;
}
.ant-card-body {
padding: $grid-unit-size;
.ant-descriptions-item {
padding: 0;
}
}
&:hover {
.cvat-job-page-list-item-id {
opacity: 1;
}
.cvat-job-page-list-item-dimension {
opacity: 1;
}
}
.cvat-jobs-page-job-item-card-preview {
height: 100%;
display: flex;
align-items: center;
justify-content: space-around;
object-fit: cover;
cursor: pointer;
}
.cvat-job-page-list-item-dimension {
position: absolute;
top: 0;
right: 0;
margin: $grid-unit-size;
width: $grid-unit-size * 4;
background: white;
border-radius: 4px;
text-align: center;
opacity: 0.5;
padding: $grid-unit-size;
}
.cvat-job-page-list-item-id {
position: absolute;
top: 0;
left: 0;
margin: $grid-unit-size $grid-unit-size $grid-unit-size 0;
width: fit-content;
background: white;
border-radius: 0 4px 4px 0;
padding: $grid-unit-size;
opacity: 0.5;
transition: 0.15s all ease;
box-shadow: $box-shadow-base;
}
}
.cvat-jobs-page-pagination {
display: flex;
justify-content: center;
}
.cvat-jobs-page-list {
display: flex;
flex-wrap: wrap;
}
.cvat-job-card-more-button {
position: absolute;
bottom: $grid-unit-size * 2;
right: $grid-unit-size;
font-size: 16px;
}
.cvat-jobs-page-filters {
.ant-table-cell {
width: $grid-unit-size * 15;
background: #f0f2f5;
}
.ant-table-tbody {
display: none;
}
}
}
// Copyright (C) 2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
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 Button from 'antd/lib/button';
interface Props {
onChangeFilters(filters: Record<string, string | null>): void;
query: JobsQuery;
}
function TopBarComponent(props: Props): JSX.Element {
const { query, onChangeFilters } = props;
const columns = [
{
title: 'Stage',
dataIndex: 'stage',
key: 'stage',
filteredValue: query.stage?.split(',') || null,
className: `${query.stage ? 'cvat-jobs-page-filter cvat-jobs-page-filter-active' : 'cvat-jobs-page-filter'}`,
filters: [
{ text: 'annotation', value: 'annotation' },
{ text: 'validation', value: 'validation' },
{ text: 'acceptance', value: 'acceptance' },
],
},
{
title: 'State',
dataIndex: 'state',
key: 'state',
filteredValue: query.state?.split(',') || null,
className: `${query.state ? 'cvat-jobs-page-filter cvat-jobs-page-filter-active' : 'cvat-jobs-page-filter'}`,
filters: [
{ text: 'new', value: 'new' },
{ text: 'in progress', value: 'in progress' },
{ text: 'completed', value: 'completed' },
{ text: 'rejected', value: 'rejected' },
],
},
{
title: 'Assignee',
dataIndex: 'assignee',
key: 'assignee',
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 });
}}
enterButton
/>
<Button
type='link'
onClick={() => {
onChangeFilters({ assignee: null });
}}
>
Reset
</Button>
</div>
),
},
];
return (
<Row className='cvat-jobs-page-top-bar' justify='center' align='middle'>
<Col md={22} lg={18} xl={16} xxl={16}>
<Row justify='space-between' align='bottom'>
<Col>
<Text className='cvat-title'>Jobs</Text>
</Col>
<Table
onChange={(_: TablePaginationConfig, filters: Record<string, FilterValue | null>) => {
const processed = Object.fromEntries(
Object.entries(filters)
.map(([key, values]) => (
[key, typeof values === 'string' || values === null ? values : values.join(',')]
)),
);
onChangeFilters(processed);
}}
className='cvat-jobs-page-filters'
columns={columns}
size='small'
/>
</Row>
</Col>
</Row>
);
}
export default React.memo(TopBarComponent);
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -78,6 +78,22 @@ export interface Task {
preview: string;
}
export interface JobsQuery {
page: number;
assignee: string | null;
stage: 'annotation' | 'validation' | 'acceptance' | null;
state: 'new' | 'in progress' | 'rejected' | 'completed' | null;
[index: string]: number | null | string | undefined;
}
export interface JobsState {
query: JobsQuery;
fetching: boolean;
count: number;
current: any[];
previews: string[];
}
export interface TasksState {
importing: boolean;
initialized: boolean;
......@@ -357,6 +373,7 @@ export interface NotificationsState {
};
jobs: {
updating: null | ErrorState;
fetching: null | ErrorState;
};
formats: {
fetching: null | ErrorState;
......@@ -747,6 +764,7 @@ export interface OrganizationState {
export interface CombinedState {
auth: AuthState;
projects: ProjectsState;
jobs: JobsState;
tasks: TasksState;
about: AboutState;
share: ShareState;
......
// Copyright (C) 2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { JobsActions, JobsActionTypes } from 'actions/jobs-actions';
import { JobsState } from './interfaces';
const defaultState: JobsState = {
fetching: false,
count: 0,
query: {
page: 1,
state: null,
stage: null,
assignee: null,
},
current: [],
previews: [],
};
export default (state: JobsState = defaultState, action: JobsActions): JobsState => {
switch (action.type) {
case JobsActionTypes.GET_JOBS: {
return {
...state,
fetching: true,
query: {
...defaultState.query,
...action.payload.query,
},
};
}
case JobsActionTypes.GET_JOBS_SUCCESS: {
return {
...state,
fetching: false,
count: action.payload.jobs.count,
current: action.payload.jobs,
previews: action.payload.previews,
};
}
case JobsActionTypes.GET_JOBS_FAILED: {
return {
...state,
fetching: false,
};
}
default: {
return state;
}
}
};
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -20,6 +20,7 @@ import { ExportActionTypes } from 'actions/export-actions';
import { ImportActionTypes } from 'actions/import-actions';
import { CloudStorageActionTypes } from 'actions/cloud-storage-actions';
import { OrganizationActionsTypes } from 'actions/organization-actions';
import { JobsActionTypes } from 'actions/jobs-actions';
import getCore from 'cvat-core-wrapper';
import { NotificationsState } from './interfaces';
......@@ -60,6 +61,7 @@ const defaultState: NotificationsState = {
},
jobs: {
updating: null,
fetching: null,
},
formats: {
fetching: null,
......@@ -1577,6 +1579,22 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case JobsActionTypes.GET_JOBS_FAILED: {
return {
...state,
errors: {
...state.errors,
jobs: {
...state.errors.jobs,
fetching: {
message: 'Could not fetch a list of jobs',
reason: action.payload.error.toString(),
className: 'cvat-notification-notice-update-organization-membership-failed',
},
},
},
};
}
case BoundariesActionTypes.RESET_AFTER_ERROR:
case AuthActionTypes.LOGOUT_SUCCESS: {
return { ...defaultState };
......
// Copyright (C) 2020-2021 Intel Corporation
// Copyright (C) 2020-2022 Intel Corporation
//
// SPDX-License-Identifier: MIT
......@@ -6,6 +6,7 @@ import { combineReducers, Reducer } from 'redux';
import authReducer from './auth-reducer';
import projectsReducer from './projects-reducer';
import tasksReducer from './tasks-reducer';
import jobsReducer from './jobs-reducer';
import aboutReducer from './about-reducer';
import shareReducer from './share-reducer';
import formatsReducer from './formats-reducer';
......@@ -27,6 +28,7 @@ export default function createRootReducer(): Reducer {
auth: authReducer,
projects: projectsReducer,
tasks: tasksReducer,
jobs: jobsReducer,
about: aboutReducer,
share: shareReducer,
formats: formatsReducer,
......
......@@ -879,6 +879,18 @@ class TaskViewSet(UploadMixin, viewsets.ModelViewSet):
filename=request.query_params.get("filename", "").lower(),
)
class CharInFilter(filters.BaseInFilter, filters.CharFilter):
pass
class JobFilter(filters.FilterSet):
assignee = filters.CharFilter(field_name="assignee__username", lookup_expr="icontains")
stage = CharInFilter(field_name="stage", lookup_expr="in")
state = CharInFilter(field_name="state", lookup_expr="in")
class Meta:
model = Job
fields = ("assignee", )
@method_decorator(name='retrieve', decorator=swagger_auto_schema(operation_summary='Method returns details of a job'))
@method_decorator(name='update', decorator=swagger_auto_schema(operation_summary='Method updates a job by id'))
@method_decorator(name='partial_update', decorator=swagger_auto_schema(
......@@ -886,6 +898,7 @@ class TaskViewSet(UploadMixin, viewsets.ModelViewSet):
class JobViewSet(viewsets.GenericViewSet, mixins.ListModelMixin,
mixins.RetrieveModelMixin, mixins.UpdateModelMixin):
queryset = Job.objects.all().order_by('id')
filterset_class = JobFilter
iam_organization_field = 'segment__task__organization'
def get_queryset(self):
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册