You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

334 lines
12 KiB
TypeScript

// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useEffect, useState } from 'react';
import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom';
import { Row, Col } from 'antd/lib/grid';
import { LoadingOutlined, QuestionCircleOutlined, CopyOutlined } from '@ant-design/icons';
import { ColumnFilterItem } from 'antd/lib/table/interface';
import Table from 'antd/lib/table';
import Button from 'antd/lib/button';
import Text from 'antd/lib/typography/Text';
import moment from 'moment';
import copy from 'copy-to-clipboard';
import CVATTooltip from 'components/common/cvat-tooltip';
import UserSelector, { User } from './user-selector';
interface Props {
taskInstance: any;
onJobUpdate(jobInstance: any): void;
}
function ReviewSummaryComponent({ jobInstance }: { jobInstance: any }): JSX.Element {
const [summary, setSummary] = useState<Record<string, any> | null>(null);
const [error, setError] = useState<any>(null);
useEffect(() => {
setError(null);
jobInstance
.reviewsSummary()
.then((_summary: Record<string, any>) => {
setSummary(_summary);
})
.catch((_error: any) => {
// eslint-disable-next-line
console.log(_error);
setError(_error);
});
}, []);
if (!summary) {
if (error) {
if (error.toString().includes('403')) {
return <p>You do not have permissions</p>;
}
return <p>Could not fetch, check console output</p>;
}
return (
<>
<p>Loading.. </p>
<LoadingOutlined />
</>
);
}
return (
<table className='cvat-review-summary-description'>
<tbody>
<tr>
<td>
<Text strong>Reviews</Text>
</td>
<td>{summary.reviews}</td>
</tr>
<tr>
<td>
<Text strong>Average quality</Text>
</td>
<td>{Number.parseFloat(summary.average_estimated_quality).toFixed(2)}</td>
</tr>
<tr>
<td>
<Text strong>Unsolved issues</Text>
</td>
<td>{summary.issues_unsolved}</td>
</tr>
<tr>
<td>
<Text strong>Resolved issues</Text>
</td>
<td>{summary.issues_resolved}</td>
</tr>
</tbody>
</table>
);
}
function JobListComponent(props: Props & RouteComponentProps): JSX.Element {
const {
taskInstance,
onJobUpdate,
history: { push },
} = props;
const { jobs, id: taskId } = taskInstance;
function sorter(path: string) {
return (obj1: any, obj2: any): number => {
let currentObj1 = obj1;
let currentObj2 = obj2;
let field1: string | null = null;
let field2: string | null = null;
for (const pathSegment of path.split('.')) {
field1 = currentObj1 && pathSegment in currentObj1 ? currentObj1[pathSegment] : null;
field2 = currentObj2 && pathSegment in currentObj2 ? currentObj2[pathSegment] : null;
currentObj1 = currentObj1 && pathSegment in currentObj1 ? currentObj1[pathSegment] : null;
currentObj2 = currentObj2 && pathSegment in currentObj2 ? currentObj2[pathSegment] : null;
}
if (field1 && field2) {
return field1.localeCompare(field2);
}
if (field1 === null) {
return 1;
}
return -1;
};
}
function collectUsers(path: string): ColumnFilterItem[] {
return Array.from<string | null>(
new Set(
jobs.map((job: any) => {
if (job[path] === null) {
return null;
}
return job[path].username;
}),
),
).map((value: string | null) => ({ text: value || 'Is Empty', value: value || false }));
}
const columns = [
{
title: 'Job',
dataIndex: 'job',
key: 'job',
render: (id: number): JSX.Element => (
<div>
<Button
type='link'
onClick={(e: React.MouseEvent): void => {
e.preventDefault();
push(`/tasks/${taskId}/jobs/${id}`);
}}
href={`/tasks/${taskId}/jobs/${id}`}
>
{`Job #${id}`}
</Button>
</div>
),
},
{
title: 'Frames',
dataIndex: 'frames',
key: 'frames',
className: 'cvat-text-color cvat-job-item-frames',
},
{
title: 'Status',
dataIndex: 'status',
key: 'status',
className: 'cvat-job-item-status',
render: (jobInstance: any): JSX.Element => {
const { status } = jobInstance;
let progressColor = null;
if (status === 'completed') {
progressColor = 'cvat-job-completed-color';
} else if (status === 'validation') {
progressColor = 'cvat-job-validation-color';
} else {
progressColor = 'cvat-job-annotation-color';
}
return (
<Text strong className={progressColor}>
{status}
<CVATTooltip title={<ReviewSummaryComponent jobInstance={jobInstance} />}>
<QuestionCircleOutlined />
</CVATTooltip>
</Text>
);
},
sorter: sorter('status.status'),
filters: [
{ text: 'annotation', value: 'annotation' },
{ text: 'validation', value: 'validation' },
{ text: 'completed', value: 'completed' },
],
onFilter: (value: string | number | boolean, record: any) => record.status.status === value,
},
{
title: 'Started on',
dataIndex: 'started',
key: 'started',
className: 'cvat-text-color',
},
{
title: 'Duration',
dataIndex: 'duration',
key: 'duration',
className: 'cvat-text-color',
},
{
title: 'Assignee',
dataIndex: 'assignee',
key: 'assignee',
className: 'cvat-job-item-assignee',
render: (jobInstance: any): JSX.Element => (
<UserSelector
className='cvat-job-assignee-selector'
value={jobInstance.assignee}
onSelect={(value: User | null): void => {
// eslint-disable-next-line
jobInstance.assignee = value;
onJobUpdate(jobInstance);
}}
/>
),
sorter: sorter('assignee.assignee.username'),
filters: collectUsers('assignee'),
onFilter: (value: string | number | boolean, record: any) =>
(record.assignee.assignee?.username || false) === value,
},
{
title: 'Reviewer',
dataIndex: 'reviewer',
key: 'reviewer',
className: 'cvat-job-item-reviewer',
render: (jobInstance: any): JSX.Element => (
<UserSelector
className='cvat-job-reviewer-selector'
value={jobInstance.reviewer}
onSelect={(value: User | null): void => {
// eslint-disable-next-line
jobInstance.reviewer = value;
onJobUpdate(jobInstance);
}}
/>
),
sorter: sorter('reviewer.reviewer.username'),
filters: collectUsers('reviewer'),
onFilter: (value: string | number | boolean, record: any) =>
(record.reviewer.reviewer?.username || false) === value,
},
];
let completed = 0;
const data = jobs.reduce((acc: any[], job: any) => {
if (job.status === 'completed') {
completed++;
}
const created = moment(props.taskInstance.createdDate);
const now = moment(moment.now());
acc.push({
key: job.id,
job: job.id,
frames: `${job.startFrame}-${job.stopFrame}`,
status: job,
started: `${created.format('MMMM Do YYYY HH:MM')}`,
duration: `${moment.duration(now.diff(created)).humanize()}`,
assignee: job,
reviewer: job,
});
return acc;
}, []);
return (
<div className='cvat-task-job-list'>
<Row justify='space-between' align='middle'>
<Col>
<Text className='cvat-text-color cvat-jobs-header'> Jobs </Text>
<CVATTooltip trigger='click' title='Copied to clipboard!'>
<Button
type='link'
onClick={(): void => {
let serialized = '';
const [latestJob] = [...taskInstance.jobs].reverse();
for (const job of taskInstance.jobs) {
const baseURL = window.location.origin;
serialized += `Job #${job.id}`.padEnd(`${latestJob.id}`.length + 6, ' ');
serialized += `: ${baseURL}/tasks/${taskInstance.id}/jobs/${job.id}`.padEnd(
`${latestJob.id}`.length + baseURL.length + 8,
' ',
);
serialized += `: [${job.startFrame}-${job.stopFrame}]`.padEnd(
`${latestJob.startFrame}${latestJob.stopFrame}`.length + 5,
' ',
);
if (job.assignee) {
serialized += `\t assigned to "${job.assignee.username}"`;
}
if (job.reviewer) {
serialized += `\t reviewed by "${job.reviewer.username}"`;
}
serialized += '\n';
}
copy(serialized);
}}
>
<CopyOutlined />
Copy
</Button>
</CVATTooltip>
</Col>
<Col>
<Text className='cvat-text-color'>{`${completed} of ${data.length} jobs`}</Text>
</Col>
</Row>
<Table
className='cvat-task-jobs-table'
rowClassName={() => 'cvat-task-jobs-table-row'}
columns={columns}
dataSource={data}
size='small'
/>
</div>
);
}
export default withRouter(JobListComponent);