User interface with React and antd (#811)
* Fixed links for analytics and help * Delete task functionality * Added navigation for create and open task * Added icon for help * Added easy plugin checker * Header dependes on installed plugins * Menu depends on installed plugins * Shared actions menu component, base layout for task page * Task page based (relations with redux, base layout) * Added attribute form * Finished label creator * Added jobs table * Added job assignee * Save updated labels on server * Added imports plugin, updated webpack * Editable bug tracker * Clean task update * Change assigneemain
parent
51cfab1a9e
commit
9016805f8e
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,45 @@
|
||||
import { AnyAction, Dispatch, ActionCreator } from 'redux';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
import { SupportedPlugins } from '../reducers/interfaces';
|
||||
import PluginChecker from '../utils/plugin-checker';
|
||||
|
||||
export enum PluginsActionTypes {
|
||||
CHECKED_ALL_PLUGINS = 'CHECKED_ALL_PLUGINS'
|
||||
}
|
||||
|
||||
interface PluginObjects {
|
||||
[plugin: string]: boolean;
|
||||
}
|
||||
|
||||
function checkedAllPlugins(plugins: PluginObjects): AnyAction {
|
||||
const action = {
|
||||
type: PluginsActionTypes.CHECKED_ALL_PLUGINS,
|
||||
payload: {
|
||||
plugins,
|
||||
},
|
||||
};
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
export function checkPluginsAsync():
|
||||
ThunkAction<Promise<void>, {}, {}, AnyAction> {
|
||||
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
|
||||
const plugins: PluginObjects = {};
|
||||
|
||||
const promises: Promise<boolean>[] = [];
|
||||
const keys = Object.keys(SupportedPlugins);
|
||||
for (const key of keys) {
|
||||
const plugin = SupportedPlugins[key as any];
|
||||
promises.push(PluginChecker.check(plugin as SupportedPlugins));
|
||||
}
|
||||
|
||||
const values = await Promise.all(promises);
|
||||
let i = 0;
|
||||
for (const key of keys) {
|
||||
plugins[key] = values[i++];
|
||||
}
|
||||
|
||||
dispatch(checkedAllPlugins(plugins));
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,120 @@
|
||||
import { AnyAction, Dispatch, ActionCreator } from 'redux';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
|
||||
import getCore from '../core';
|
||||
|
||||
const core = getCore();
|
||||
|
||||
export enum TaskActionTypes {
|
||||
GET_TASK = 'GET_TASK',
|
||||
GET_TASK_SUCCESS = 'GET_TASK_SUCCESS',
|
||||
GET_TASK_FAILED = 'GET_TASK_FAILED',
|
||||
UPDATE_TASK = 'UPDATE_TASK',
|
||||
UPDATE_TASK_SUCCESS = 'UPDATE_TASK_SUCCESS',
|
||||
UPDATE_TASK_FAILED = 'UPDATE_TASK_FAILED',
|
||||
}
|
||||
|
||||
function getTask(): AnyAction {
|
||||
const action = {
|
||||
type: TaskActionTypes.GET_TASK,
|
||||
payload: {},
|
||||
};
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
function getTaskSuccess(taskInstance: any, previewImage: string): AnyAction {
|
||||
const action = {
|
||||
type: TaskActionTypes.GET_TASK_SUCCESS,
|
||||
payload: {
|
||||
taskInstance,
|
||||
previewImage,
|
||||
},
|
||||
};
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
function getTaskFailed(error: any): AnyAction {
|
||||
const action = {
|
||||
type: TaskActionTypes.GET_TASK_FAILED,
|
||||
payload: {
|
||||
error,
|
||||
},
|
||||
};
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
export function getTaskAsync(tid: number):
|
||||
ThunkAction<Promise<void>, {}, {}, AnyAction> {
|
||||
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
|
||||
try {
|
||||
dispatch(getTask());
|
||||
const taskInstance = (await core.tasks.get({ id: tid }))[0];
|
||||
if (taskInstance) {
|
||||
const previewImage = await taskInstance.frames.preview();
|
||||
dispatch(getTaskSuccess(taskInstance, previewImage));
|
||||
} else {
|
||||
throw Error(`Task ${tid} wasn't found on the server`);
|
||||
}
|
||||
} catch (error) {
|
||||
dispatch(getTaskFailed(error));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function updateTask(): AnyAction {
|
||||
const action = {
|
||||
type: TaskActionTypes.UPDATE_TASK,
|
||||
payload: {},
|
||||
};
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
function updateTaskSuccess(taskInstance: any): AnyAction {
|
||||
const action = {
|
||||
type: TaskActionTypes.UPDATE_TASK_SUCCESS,
|
||||
payload: {
|
||||
taskInstance,
|
||||
},
|
||||
};
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
function updateTaskFailed(error: any, taskInstance: any): AnyAction {
|
||||
const action = {
|
||||
type: TaskActionTypes.UPDATE_TASK_FAILED,
|
||||
payload: {
|
||||
error,
|
||||
taskInstance,
|
||||
},
|
||||
};
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
export function updateTaskAsync(taskInstance: any):
|
||||
ThunkAction<Promise<void>, {}, {}, AnyAction> {
|
||||
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
|
||||
try {
|
||||
dispatch(updateTask());
|
||||
await taskInstance.save();
|
||||
const [task] = await core.tasks.get({ id: taskInstance.id });
|
||||
dispatch(updateTaskSuccess(task));
|
||||
} catch (error) {
|
||||
// try abort all changes
|
||||
let task = null;
|
||||
try {
|
||||
[task] = await core.tasks.get({ id: taskInstance.id });
|
||||
} catch (_) {
|
||||
// server error?
|
||||
dispatch(updateTaskFailed(error, taskInstance));
|
||||
}
|
||||
|
||||
dispatch(updateTaskFailed(error, task));
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
import { AnyAction, Dispatch, ActionCreator } from 'redux';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
|
||||
import getCore from '../core';
|
||||
|
||||
const core = getCore();
|
||||
|
||||
export enum UsersActionTypes {
|
||||
GET_USERS = 'GET_USERS',
|
||||
GET_USERS_SUCCESS = 'GET_USERS_SUCCESS',
|
||||
GET_USERS_FAILED = 'GET_USERS_FAILED',
|
||||
}
|
||||
|
||||
function getUsers(): AnyAction {
|
||||
const action = {
|
||||
type: UsersActionTypes.GET_USERS,
|
||||
payload: { },
|
||||
};
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
function getUsersSuccess(users: any[]): AnyAction {
|
||||
const action = {
|
||||
type: UsersActionTypes.GET_USERS_SUCCESS,
|
||||
payload: { users },
|
||||
};
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
function getUsersFailed(error: any): AnyAction {
|
||||
const action = {
|
||||
type: UsersActionTypes.GET_USERS_FAILED,
|
||||
payload: { error },
|
||||
};
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
export function getUsersAsync():
|
||||
ThunkAction<Promise<void>, {}, {}, AnyAction> {
|
||||
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
|
||||
try {
|
||||
dispatch(getUsers());
|
||||
const users = await core.users.get();
|
||||
dispatch(
|
||||
getUsersSuccess(
|
||||
users.map((userData: any): any => new core.classes.User(userData)),
|
||||
),
|
||||
);
|
||||
} catch (error) {
|
||||
dispatch(getUsersFailed(error));
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,101 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Menu,
|
||||
Modal,
|
||||
} from 'antd';
|
||||
|
||||
import { ClickParam } from 'antd/lib/menu/index';
|
||||
|
||||
import LoaderItemComponent from './loader-item';
|
||||
import DumperItemComponent from './dumper-item';
|
||||
|
||||
|
||||
interface ActionsMenuComponentProps {
|
||||
taskInstance: any;
|
||||
loaders: any[];
|
||||
dumpers: any[];
|
||||
loadActivity: string | null;
|
||||
dumpActivities: string[] | null;
|
||||
installedTFAnnotation: boolean;
|
||||
installedAutoAnnotation: boolean;
|
||||
onLoadAnnotation: (taskInstance: any, loader: any, file: File) => void;
|
||||
onDumpAnnotation: (task: any, dumper: any) => void;
|
||||
onDeleteTask: (task: any) => void;
|
||||
}
|
||||
|
||||
interface MinActionsMenuProps {
|
||||
taskInstance: any;
|
||||
onDeleteTask: (task: any) => void;
|
||||
}
|
||||
|
||||
export function handleMenuClick(props: MinActionsMenuProps, params: ClickParam) {
|
||||
const { taskInstance } = props;
|
||||
const tracker = taskInstance.bugTracker;
|
||||
|
||||
if (params.keyPath.length !== 2) {
|
||||
switch (params.key) {
|
||||
case 'tracker': {
|
||||
window.open(`${tracker}`, '_blank')
|
||||
return;
|
||||
} case 'auto': {
|
||||
|
||||
return;
|
||||
} case 'tf': {
|
||||
|
||||
return;
|
||||
} case 'delete': {
|
||||
const taskID = taskInstance.id;
|
||||
Modal.confirm({
|
||||
title: `The task ${taskID} will be deleted`,
|
||||
content: 'All related data (images, annotations) will be lost. Continue?',
|
||||
onOk: () => {
|
||||
props.onDeleteTask(taskInstance);
|
||||
},
|
||||
});
|
||||
return;
|
||||
} default: {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default function ActionsMenuComponent(props: ActionsMenuComponentProps) {
|
||||
const tracker = props.taskInstance.bugTracker;
|
||||
|
||||
return (
|
||||
<Menu subMenuCloseDelay={0.15} className='cvat-task-item-menu' onClick={
|
||||
(params: ClickParam) => handleMenuClick(props, params)
|
||||
}>
|
||||
<Menu.SubMenu key='dump' title='Dump annotations'>
|
||||
{
|
||||
props.dumpers.map((dumper) => DumperItemComponent({
|
||||
dumper,
|
||||
taskInstance: props.taskInstance,
|
||||
dumpActivities: props.dumpActivities,
|
||||
onDumpAnnotation: props.onDumpAnnotation,
|
||||
} ))}
|
||||
</Menu.SubMenu>
|
||||
<Menu.SubMenu key='load' title='Upload annotations'>
|
||||
{
|
||||
props.loaders.map((loader) => LoaderItemComponent({
|
||||
loader,
|
||||
taskInstance: props.taskInstance,
|
||||
loadActivity: props.loadActivity,
|
||||
onLoadAnnotation: props.onLoadAnnotation,
|
||||
}))
|
||||
}
|
||||
</Menu.SubMenu>
|
||||
{tracker ? <Menu.Item key='tracker'>Open bug tracker</Menu.Item> : null}
|
||||
{ props.installedTFAnnotation ?
|
||||
<Menu.Item key='tf'>Run TF annotation</Menu.Item> : null
|
||||
}
|
||||
{ props.installedAutoAnnotation ?
|
||||
<Menu.Item key='auto'>Run auto annotation</Menu.Item> : null
|
||||
}
|
||||
<hr/>
|
||||
<Menu.Item key='delete'>Delete</Menu.Item>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Menu,
|
||||
Button,
|
||||
Icon,
|
||||
} from 'antd';
|
||||
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
|
||||
interface DumperItemComponentProps {
|
||||
taskInstance: any;
|
||||
dumper: any;
|
||||
dumpActivities: string[] | null;
|
||||
onDumpAnnotation: (task: any, dumper: any) => void;
|
||||
}
|
||||
|
||||
function isDefaultFormat(dumperName: string, taskMode: string): boolean {
|
||||
return (dumperName === 'CVAT XML 1.1 for videos' && taskMode === 'interpolation')
|
||||
|| (dumperName === 'CVAT XML 1.1 for images' && taskMode === 'annotation');
|
||||
}
|
||||
|
||||
export default function DumperItemComponent(props: DumperItemComponentProps) {
|
||||
const task = props.taskInstance;
|
||||
const { mode } = task;
|
||||
const { dumper } = props;
|
||||
|
||||
const dumpingWithThisDumper = (props.dumpActivities || [])
|
||||
.filter((_dumper: string) => _dumper === dumper.name)[0];
|
||||
|
||||
const pending = !!dumpingWithThisDumper;
|
||||
|
||||
return (
|
||||
<Menu.Item className='cvat-task-item-dump-submenu-item' key={dumper.name}>
|
||||
<Button block={true} type='link' disabled={pending}
|
||||
onClick={() => {
|
||||
props.onDumpAnnotation(task, dumper);
|
||||
}}>
|
||||
<Icon type='download'/>
|
||||
<Text strong={isDefaultFormat(dumper.name, mode)}>
|
||||
{dumper.name}
|
||||
</Text>
|
||||
{pending ? <Icon type='loading'/> : null}
|
||||
</Button>
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,52 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Menu,
|
||||
Button,
|
||||
Icon,
|
||||
Upload,
|
||||
} from 'antd';
|
||||
|
||||
import { RcFile } from 'antd/lib/upload';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
|
||||
interface LoaderItemComponentProps {
|
||||
taskInstance: any;
|
||||
loader: any;
|
||||
loadActivity: string | null;
|
||||
onLoadAnnotation: (taskInstance: any, loader: any, file: File) => void;
|
||||
}
|
||||
|
||||
export default function LoaderItemComponent(props: LoaderItemComponentProps) {
|
||||
const { loader } = props;
|
||||
|
||||
const loadingWithThisLoader = props.loadActivity
|
||||
&& props.loadActivity === loader.name
|
||||
? props.loadActivity : null;
|
||||
|
||||
const pending = !!loadingWithThisLoader;
|
||||
|
||||
return (
|
||||
<Menu.Item className='cvat-task-item-load-submenu-item' key={loader.name}>
|
||||
<Upload
|
||||
accept={`.${loader.format}`}
|
||||
multiple={false}
|
||||
showUploadList={ false }
|
||||
beforeUpload={(file: RcFile) => {
|
||||
props.onLoadAnnotation(
|
||||
props.taskInstance,
|
||||
loader,
|
||||
file as File,
|
||||
);
|
||||
|
||||
return false;
|
||||
}}>
|
||||
<Button block={true} type='link' disabled={!!props.loadActivity}>
|
||||
<Icon type='upload'/>
|
||||
<Text>{loader.name}</Text>
|
||||
{pending ? <Icon type='loading'/> : null}
|
||||
</Button>
|
||||
</Upload>
|
||||
</Menu.Item>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,29 @@
|
||||
export interface Attribute {
|
||||
id: number;
|
||||
name: string;
|
||||
type: string;
|
||||
mutable: boolean;
|
||||
values: string[];
|
||||
}
|
||||
|
||||
export interface Label {
|
||||
name: string;
|
||||
id: number;
|
||||
attributes: Attribute[];
|
||||
}
|
||||
|
||||
let id = 0;
|
||||
|
||||
export function idGenerator(): number {
|
||||
return --id;
|
||||
}
|
||||
|
||||
export function equalArrayHead(arr1: string[], arr2: string[]): boolean {
|
||||
for (let i = 0; i < arr1.length; i++) {
|
||||
if (arr1[i] !== arr2[i]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import LabelForm from './label-form';
|
||||
|
||||
import { Label, Attribute } from './common';
|
||||
|
||||
interface Props {
|
||||
onCreate: (label: Label | null) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
attributes: Attribute[];
|
||||
}
|
||||
|
||||
export default class ConstructorCreator extends React.PureComponent<Props, State> {
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className='cvat-label-constructor-creator'>
|
||||
<LabelForm label={null} onSubmit={this.props.onCreate}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,28 @@
|
||||
import React from 'react';
|
||||
|
||||
import LabelForm from './label-form';
|
||||
import { Label, Attribute } from './common';
|
||||
|
||||
interface Props {
|
||||
label: Label;
|
||||
onUpdate: (label: Label | null) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
savedAttributes: Attribute[];
|
||||
unsavedAttributes: Attribute[];
|
||||
}
|
||||
|
||||
export default class ConstructorUpdater extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className='cvat-label-constructor-updater'>
|
||||
<LabelForm label={this.props.label} onSubmit={this.props.onUpdate}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Icon,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
|
||||
import { Label } from './common';
|
||||
|
||||
interface ConstructorViewerItemProps {
|
||||
label: Label;
|
||||
color: string;
|
||||
onUpdate: (label: Label) => void;
|
||||
onDelete: (label: Label) => void;
|
||||
}
|
||||
|
||||
export default function ConstructorViewerItem(props: ConstructorViewerItemProps) {
|
||||
return (
|
||||
<div style={{background: props.color}} className='cvat-constructor-viewer-item'>
|
||||
<Text>{ props.label.name }</Text>
|
||||
<Tooltip title='Update attributes'>
|
||||
<span onClick={() => props.onUpdate(props.label)}>
|
||||
<Icon theme='filled' type='edit'/>
|
||||
</span>
|
||||
</Tooltip>
|
||||
{ props.label.id >= 0 ? null :
|
||||
<Tooltip title='Delete label'>
|
||||
<span onClick={() => props.onDelete(props.label)}>
|
||||
<Icon type='close'></Icon>
|
||||
</span>
|
||||
</Tooltip>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,59 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Icon,
|
||||
Button,
|
||||
} from 'antd';
|
||||
|
||||
import ConstructorViewerItem from './constructor-viewer-item';
|
||||
import { Label } from './common';
|
||||
|
||||
interface ConstructorViewerProps {
|
||||
labels: Label[];
|
||||
onUpdate: (label: Label) => void;
|
||||
onDelete: (label: Label) => void;
|
||||
onCreate: () => void;
|
||||
}
|
||||
|
||||
const colors = [
|
||||
'#ff811e', '#9013fe', '#0074d9',
|
||||
'#549ca4', '#e8c720', '#3d9970',
|
||||
'#6b2034', '#2c344c', '#2ecc40',
|
||||
];
|
||||
|
||||
let currentColor = 0;
|
||||
|
||||
function nextColor() {
|
||||
const color = colors[currentColor];
|
||||
currentColor += 1;
|
||||
if (currentColor >= colors.length) {
|
||||
currentColor = 0;
|
||||
}
|
||||
return color;
|
||||
}
|
||||
|
||||
export default function ConstructorViewer(props: ConstructorViewerProps) {
|
||||
currentColor = 0;
|
||||
|
||||
const list = [
|
||||
<Button key='create' type='ghost' onClick={props.onCreate} className='cvat-constructor-viewer-new-item'>
|
||||
Add label <Icon type='plus-circle'/>
|
||||
</Button>];
|
||||
for (const label of props.labels) {
|
||||
list.push(
|
||||
<ConstructorViewerItem
|
||||
onUpdate={props.onUpdate}
|
||||
onDelete={props.onDelete}
|
||||
label={label}
|
||||
key={label.id}
|
||||
color={nextColor()}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='cvat-constructor-viewer'>
|
||||
{ list }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,464 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Icon,
|
||||
Input,
|
||||
Button,
|
||||
Select,
|
||||
Tooltip,
|
||||
Checkbox,
|
||||
} from 'antd';
|
||||
|
||||
import Form, { FormComponentProps } from 'antd/lib/form/Form';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
|
||||
import {
|
||||
equalArrayHead,
|
||||
idGenerator,
|
||||
Label,
|
||||
Attribute,
|
||||
} from './common';
|
||||
import patterns from '../../utils/validation-patterns';
|
||||
|
||||
export enum AttributeType {
|
||||
SELECT = 'SELECT',
|
||||
RADIO = 'RADIO',
|
||||
CHECKBOX = 'CHECKBOX',
|
||||
TEXT = 'TEXT',
|
||||
NUMBER = 'NUMBER',
|
||||
}
|
||||
|
||||
type Props = FormComponentProps & {
|
||||
label: Label | null;
|
||||
onSubmit: (label: Label | null) => void;
|
||||
};
|
||||
|
||||
interface State {
|
||||
|
||||
}
|
||||
|
||||
class LabelForm extends React.PureComponent<Props, State> {
|
||||
private continueAfterSubmit: boolean;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.continueAfterSubmit = false;
|
||||
}
|
||||
|
||||
private handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
this.props.form.validateFields((error, values) => {
|
||||
if (!error) {
|
||||
this.props.onSubmit({
|
||||
name: values.labelName,
|
||||
id: this.props.label ? this.props.label.id : idGenerator(),
|
||||
attributes: values.keys.map((key: number, index: number) => {
|
||||
return {
|
||||
name: values.attrName[key],
|
||||
type: values.type[key],
|
||||
mutable: values.mutable[key],
|
||||
id: this.props.label && index < this.props.label.attributes.length
|
||||
? this.props.label.attributes[index].id : key,
|
||||
values: Array.isArray(values.values[key])
|
||||
? values.values[key] : [values.values[key]]
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
this.props.form.resetFields();
|
||||
|
||||
if (!this.continueAfterSubmit) {
|
||||
this.props.onSubmit(null);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private addAttribute = () => {
|
||||
const { form } = this.props;
|
||||
const keys = form.getFieldValue('keys');
|
||||
const nextKeys = keys.concat(idGenerator());
|
||||
form.setFieldsValue({
|
||||
keys: nextKeys,
|
||||
});
|
||||
}
|
||||
|
||||
private removeAttribute = (key: number) => {
|
||||
const { form } = this.props;
|
||||
const keys = form.getFieldValue('keys');
|
||||
form.setFieldsValue({
|
||||
keys: keys.filter((_key: number) => _key !== key),
|
||||
});
|
||||
}
|
||||
|
||||
private renderAttributeNameInput(key: number, attr: Attribute | null) {
|
||||
const locked = attr ? attr.id >= 0 : false;
|
||||
const value = attr ? attr.name : '';
|
||||
|
||||
return (
|
||||
<Col span={5}>
|
||||
<Form.Item hasFeedback> {
|
||||
this.props.form.getFieldDecorator(`attrName[${key}]`, {
|
||||
initialValue: value,
|
||||
rules: [{
|
||||
required: true,
|
||||
message: 'Please specify a name',
|
||||
}, {
|
||||
pattern: patterns.validateAttributeName.pattern,
|
||||
message: patterns.validateAttributeName.message,
|
||||
}],
|
||||
})(<Input disabled={locked} placeholder='Name'/>)
|
||||
} </Form.Item>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
private renderAttributeTypeInput(key: number, attr: Attribute | null) {
|
||||
const locked = attr ? attr.id >= 0 : false;
|
||||
const type = attr ? attr.type.toUpperCase() : AttributeType.SELECT;
|
||||
|
||||
return (
|
||||
<Col span={4}>
|
||||
<Form.Item>
|
||||
<Tooltip overlay='An HTML element representing the attribute'>
|
||||
{this.props.form.getFieldDecorator(`type[${key}]`, {
|
||||
initialValue: type,
|
||||
})(
|
||||
<Select disabled={locked}>
|
||||
<Select.Option value={AttributeType.SELECT}> Select </Select.Option>
|
||||
<Select.Option value={AttributeType.RADIO}> Radio </Select.Option>
|
||||
<Select.Option value={AttributeType.CHECKBOX}> Checkbox </Select.Option>
|
||||
<Select.Option value={AttributeType.TEXT}> Text </Select.Option>
|
||||
<Select.Option value={AttributeType.NUMBER}> Number </Select.Option>
|
||||
</Select>
|
||||
)
|
||||
}</Tooltip>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
private renderAttributeValuesInput(key: number, attr: Attribute | null) {
|
||||
const locked = attr ? attr.id >= 0 : false;
|
||||
const existedValues = attr ? attr.values : [];
|
||||
|
||||
const validator = (_: any, values: string[], callback: any) => {
|
||||
if (locked && existedValues) {
|
||||
if (!equalArrayHead(existedValues, values)) {
|
||||
callback('You can only append new values');
|
||||
}
|
||||
}
|
||||
|
||||
for (const value of values) {
|
||||
if (!patterns.validateAttributeValue.pattern.test(value)) {
|
||||
callback(`Invalid attribute value: "${value}"`);
|
||||
}
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item>
|
||||
{ this.props.form.getFieldDecorator(`values[${key}]`, {
|
||||
initialValue: existedValues,
|
||||
rules: [{
|
||||
required: true,
|
||||
message: 'Please specify values',
|
||||
}, {
|
||||
validator,
|
||||
}],
|
||||
})(
|
||||
<Select
|
||||
mode='tags'
|
||||
dropdownMenuStyle={{display: 'none'}}
|
||||
placeholder='Attribute values'
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
private renderBooleanValueInput(key: number, attr: Attribute | null) {
|
||||
const value = attr ? attr.values[0] : 'false';
|
||||
|
||||
return (
|
||||
<Form.Item>
|
||||
{ this.props.form.getFieldDecorator(`values[${key}]`, {
|
||||
initialValue: value,
|
||||
})(
|
||||
<Select>
|
||||
<Select.Option value='false'> False </Select.Option>
|
||||
<Select.Option value='true'> True </Select.Option>
|
||||
</Select>
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
private renderNumberRangeInput(key: number, attr: Attribute | null) {
|
||||
const locked = attr ? attr.id >= 0 : false;
|
||||
const value = attr ? attr.values[0] : '';
|
||||
|
||||
const validator = (_: any, value: string, callback: any) => {
|
||||
const numbers = value.split(';').map((number) => Number.parseFloat(number));
|
||||
if (numbers.length !== 3) {
|
||||
callback('Invalid input');
|
||||
}
|
||||
|
||||
for (const number of numbers) {
|
||||
if (Number.isNaN(number)) {
|
||||
callback('Invalid input');
|
||||
}
|
||||
}
|
||||
|
||||
if (numbers[0] >= numbers[1]) {
|
||||
callback('Invalid input');
|
||||
}
|
||||
|
||||
if (+numbers[1] - +numbers[0] < +numbers[2]) {
|
||||
callback('Invalid input');
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Item>
|
||||
{ this.props.form.getFieldDecorator(`values[${key}]`, {
|
||||
initialValue: value,
|
||||
rules: [{
|
||||
required: true,
|
||||
message: 'Please set a range',
|
||||
}, {
|
||||
validator,
|
||||
}]
|
||||
})(
|
||||
<Input disabled={locked} placeholder='min;max;step'/>
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
private renderDefaultValueInput(key: number, attr: Attribute | null) {
|
||||
const value = attr ? attr.values[0] : '';
|
||||
|
||||
return (
|
||||
<Form.Item>
|
||||
{ this.props.form.getFieldDecorator(`values[${key}]`, {
|
||||
initialValue: value,
|
||||
})(
|
||||
<Input placeholder='Default value'/>
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
private renderMutableAttributeInput(key: number, attr: Attribute | null) {
|
||||
const locked = attr ? attr.id >= 0 : false;
|
||||
const value = attr ? attr.mutable : false;
|
||||
|
||||
return (
|
||||
<Form.Item>
|
||||
<Tooltip overlay='Can this attribute be changed frame to frame?'>
|
||||
{ this.props.form.getFieldDecorator(`mutable[${key}]`, {
|
||||
initialValue: value,
|
||||
valuePropName: 'checked',
|
||||
})(
|
||||
<Checkbox disabled={locked}> Mutable </Checkbox>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
private renderDeleteAttributeButton(key: number, attr: Attribute | null) {
|
||||
const locked = attr ? attr.id >= 0 : false;
|
||||
|
||||
return (
|
||||
<Form.Item>
|
||||
<Tooltip overlay='Delete the attribute'>
|
||||
<Button
|
||||
type='link'
|
||||
className='cvat-delete-attribute-button'
|
||||
disabled={locked}
|
||||
onClick={() => {
|
||||
this.removeAttribute(key);
|
||||
}}
|
||||
>
|
||||
<Icon type='close-circle'/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
private renderAttribute = (key: number, index: number) => {
|
||||
const attr = (this.props.label && index < this.props.label.attributes.length
|
||||
? this.props.label.attributes[index]
|
||||
: null);
|
||||
|
||||
return (
|
||||
<Form.Item key={key}>
|
||||
<Row type='flex' justify='space-between' align='middle'>
|
||||
{ this.renderAttributeNameInput(key, attr) }
|
||||
{ this.renderAttributeTypeInput(key, attr) }
|
||||
<Col span={6}> {
|
||||
(() => {
|
||||
const type = this.props.form.getFieldValue(`type[${key}]`);
|
||||
let element = null;
|
||||
|
||||
[AttributeType.SELECT, AttributeType.RADIO]
|
||||
.includes(type) ?
|
||||
element = this.renderAttributeValuesInput(key, attr)
|
||||
: type === AttributeType.CHECKBOX ?
|
||||
element = this.renderBooleanValueInput(key, attr)
|
||||
: type === AttributeType.NUMBER ?
|
||||
element = this.renderNumberRangeInput(key, attr)
|
||||
: element = this.renderDefaultValueInput(key, attr)
|
||||
|
||||
return element;
|
||||
})()
|
||||
} </Col>
|
||||
<Col span={5}>
|
||||
{ this.renderMutableAttributeInput(key, attr) }
|
||||
</Col>
|
||||
<Col span={2}>
|
||||
{ this.renderDeleteAttributeButton(key, attr) }
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
private renderLabelNameInput() {
|
||||
const value = this.props.label ? this.props.label.name : '';
|
||||
const locked = this.props.label ? this.props.label.id >= 0 : false;
|
||||
|
||||
return (
|
||||
<Col span={10}>
|
||||
<Form.Item hasFeedback> {
|
||||
this.props.form.getFieldDecorator('labelName', {
|
||||
initialValue: value,
|
||||
rules: [{
|
||||
required: true,
|
||||
message: 'Please specify a name',
|
||||
}, {
|
||||
pattern: patterns.validateAttributeName.pattern,
|
||||
message: patterns.validateAttributeName.message,
|
||||
}]
|
||||
})(<Input disabled={locked} placeholder='Label name'/>)
|
||||
} </Form.Item>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
private renderNewAttributeButton() {
|
||||
return (
|
||||
<Col span={3}>
|
||||
<Form.Item>
|
||||
<Button type='ghost' onClick={this.addAttribute}>
|
||||
Add an attribute <Icon type="plus"/>
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
private renderDoneButton() {
|
||||
return (
|
||||
<Col span={4}>
|
||||
<Tooltip overlay='Save the label and return'>
|
||||
<Button
|
||||
style={{width: '150px'}}
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
onClick={() => {
|
||||
this.continueAfterSubmit = false;
|
||||
}}
|
||||
> Done </Button>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
private renderContinueButton() {
|
||||
return (
|
||||
this.props.label ? <div/> :
|
||||
<Col span={4}>
|
||||
<Tooltip overlay='Save the label and create one more'>
|
||||
<Button
|
||||
style={{width: '150px'}}
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
onClick={() => {
|
||||
this.continueAfterSubmit = true;
|
||||
}}
|
||||
> Continue </Button>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
private renderCancelButton() {
|
||||
return (
|
||||
<Col span={4}>
|
||||
<Tooltip overlay='Do not save the label and return'>
|
||||
<Button
|
||||
style={{width: '150px'}}
|
||||
type='danger'
|
||||
onClick={() => {
|
||||
this.props.onSubmit(null);
|
||||
}}
|
||||
> Cancel </Button>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
this.props.form.getFieldDecorator('keys', {
|
||||
initialValue: this.props.label
|
||||
? this.props.label.attributes.map((attr: Attribute) => attr.id)
|
||||
: []
|
||||
});
|
||||
|
||||
let keys = this.props.form.getFieldValue('keys');
|
||||
const attributeItems = keys.map(this.renderAttribute);
|
||||
|
||||
return (
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<Row type='flex' justify='start' align='middle'>
|
||||
{ this.renderLabelNameInput() }
|
||||
<Col span={1}/>
|
||||
{ this.renderNewAttributeButton() }
|
||||
</Row>
|
||||
{ attributeItems.length > 0 ?
|
||||
<Row type='flex' justify='start' align='middle'>
|
||||
<Col>
|
||||
<Text> Attributes </Text>
|
||||
</Col>
|
||||
</Row> : null
|
||||
}
|
||||
{ attributeItems.reverse() }
|
||||
<Row type='flex' justify='start' align='middle'>
|
||||
{ this.renderDoneButton() }
|
||||
<Col span={1}/>
|
||||
{ this.renderContinueButton() }
|
||||
<Col span={1}/>
|
||||
{ this.renderCancelButton() }
|
||||
</Row>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create<Props>()(LabelForm);
|
||||
|
||||
|
||||
// add validators
|
||||
// add initial values
|
||||
// add readonly fields
|
||||
@ -0,0 +1,255 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Tabs,
|
||||
Icon,
|
||||
Modal,
|
||||
} from 'antd';
|
||||
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
|
||||
import RawViewer from './raw-viewer';
|
||||
import ConstructorViewer from './constructor-viewer';
|
||||
import ConstructorCreator from './constructor-creator';
|
||||
import ConstructorUpdater from './constructor-updater';
|
||||
|
||||
import {
|
||||
idGenerator,
|
||||
Label,
|
||||
Attribute,
|
||||
} from './common';
|
||||
|
||||
enum ConstructorMode {
|
||||
SHOW = 'SHOW',
|
||||
CREATE = 'CREATE',
|
||||
UPDATE = 'UPDATE',
|
||||
}
|
||||
|
||||
interface LabelsEditortProps {
|
||||
labels: Label[];
|
||||
onSubmit: (labels: any[]) => void;
|
||||
}
|
||||
|
||||
interface LabelsEditorState {
|
||||
constructorMode: ConstructorMode;
|
||||
savedLabels: Label[];
|
||||
unsavedLabels: Label[];
|
||||
labelForUpdate: Label | null;
|
||||
}
|
||||
|
||||
export default class LabelsEditor
|
||||
extends React.PureComponent<LabelsEditortProps, LabelsEditorState> {
|
||||
|
||||
public constructor(props: LabelsEditortProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
savedLabels: [],
|
||||
unsavedLabels: [],
|
||||
constructorMode: ConstructorMode.SHOW,
|
||||
labelForUpdate: null,
|
||||
};
|
||||
}
|
||||
|
||||
private handleSubmit(savedLabels: Label[], unsavedLabels: Label[]) {
|
||||
function transformLabel(label: Label): any {
|
||||
return {
|
||||
name: label.name,
|
||||
id: label.id < 0 ? undefined : label.id,
|
||||
attributes: label.attributes.map((attr: Attribute): any => {
|
||||
return {
|
||||
name: attr.name,
|
||||
id: attr.id < 0 ? undefined : attr.id,
|
||||
input_type: attr.type.toLowerCase(),
|
||||
default_value: attr.values[0],
|
||||
mutable: attr.mutable,
|
||||
values: [...attr.values],
|
||||
};
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
const output = [];
|
||||
for (const label of savedLabels.concat(unsavedLabels)) {
|
||||
output.push(transformLabel(label));
|
||||
}
|
||||
|
||||
this.props.onSubmit(output);
|
||||
}
|
||||
|
||||
private handleRawSubmit = (labels: Label[]) => {
|
||||
const unsavedLabels = [];
|
||||
const savedLabels = [];
|
||||
|
||||
for (let label of labels) {
|
||||
if (label.id >= 0) {
|
||||
savedLabels.push(label);
|
||||
} else {
|
||||
unsavedLabels.push(label);
|
||||
}
|
||||
}
|
||||
|
||||
this.setState({
|
||||
unsavedLabels,
|
||||
savedLabels,
|
||||
});
|
||||
|
||||
this.handleSubmit(savedLabels, unsavedLabels);
|
||||
}
|
||||
|
||||
private handleUpdate = (label: Label | null) => {
|
||||
if (label) {
|
||||
const savedLabels = this.state.savedLabels
|
||||
.filter((_label: Label) => _label.id !== label.id);
|
||||
const unsavedLabels = this.state.unsavedLabels
|
||||
.filter((_label: Label) => _label.id !== label.id);
|
||||
if (label.id >= 0) {
|
||||
savedLabels.push(label);
|
||||
this.setState({
|
||||
savedLabels,
|
||||
constructorMode: ConstructorMode.SHOW,
|
||||
});
|
||||
} else {
|
||||
unsavedLabels.push(label);
|
||||
this.setState({
|
||||
unsavedLabels,
|
||||
constructorMode: ConstructorMode.SHOW,
|
||||
});
|
||||
}
|
||||
|
||||
this.handleSubmit(savedLabels, unsavedLabels);
|
||||
} else {
|
||||
this.setState({
|
||||
constructorMode: ConstructorMode.SHOW,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
private handleDelete = (label: Label) => {
|
||||
// the label is saved on the server, cannot delete it
|
||||
if (typeof(label.id) !== 'undefined' && label.id >= 0) {
|
||||
Modal.error({
|
||||
title: 'Could not delete the label',
|
||||
content: 'It has been already saved on the server',
|
||||
});
|
||||
}
|
||||
|
||||
const unsavedLabels = this.state.unsavedLabels.filter(
|
||||
(_label: Label) => _label.id !== label.id
|
||||
);
|
||||
|
||||
this.setState({
|
||||
unsavedLabels: [...unsavedLabels],
|
||||
});
|
||||
|
||||
this.handleSubmit(this.state.savedLabels, unsavedLabels);
|
||||
};
|
||||
|
||||
private handleCreate = (label: Label | null) => {
|
||||
if (label === null) {
|
||||
this.setState({
|
||||
constructorMode: ConstructorMode.SHOW,
|
||||
});
|
||||
} else {
|
||||
const unsavedLabels = [...this.state.unsavedLabels,
|
||||
{
|
||||
...label,
|
||||
id: idGenerator()
|
||||
}
|
||||
];
|
||||
|
||||
this.setState({
|
||||
unsavedLabels,
|
||||
});
|
||||
|
||||
this.handleSubmit(this.state.savedLabels, unsavedLabels);
|
||||
}
|
||||
};
|
||||
|
||||
public componentDidMount() {
|
||||
this.componentDidUpdate(null as any as LabelsEditortProps);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: LabelsEditortProps) {
|
||||
function transformLabel(label: any): Label {
|
||||
return {
|
||||
name: label.name,
|
||||
id: label.id || idGenerator(),
|
||||
attributes: label.attributes.map((attr: any): Attribute => {
|
||||
return {
|
||||
id: attr.id || idGenerator(),
|
||||
name: attr.name,
|
||||
type: attr.input_type,
|
||||
mutable: attr.mutable,
|
||||
values: [...attr.values],
|
||||
};
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
if (!prevProps || prevProps.labels !== this.props.labels) {
|
||||
const transformedLabels = this.props.labels.map(transformLabel);
|
||||
this.setState({
|
||||
savedLabels: transformedLabels
|
||||
.filter((label: Label) => label.id >= 0),
|
||||
unsavedLabels: transformedLabels
|
||||
.filter((label: Label) => label.id < 0),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Tabs defaultActiveKey='2' type='card' tabBarStyle={{marginBottom: '0px'}}>
|
||||
<Tabs.TabPane tab={
|
||||
<span>
|
||||
<Icon type='edit'/>
|
||||
<Text> Raw </Text>
|
||||
</span>
|
||||
} key='1'>
|
||||
<RawViewer
|
||||
labels={[...this.state.savedLabels, ...this.state.unsavedLabels]}
|
||||
onSubmit={this.handleRawSubmit}
|
||||
/>
|
||||
</Tabs.TabPane>
|
||||
|
||||
<Tabs.TabPane tab={
|
||||
<span>
|
||||
<Icon type='build'/>
|
||||
<Text> Constructor </Text>
|
||||
</span>
|
||||
} key='2'>
|
||||
{
|
||||
this.state.constructorMode === ConstructorMode.SHOW ?
|
||||
<ConstructorViewer
|
||||
labels={[...this.state.savedLabels, ...this.state.unsavedLabels]}
|
||||
onUpdate={(label: Label) => {
|
||||
this.setState({
|
||||
constructorMode: ConstructorMode.UPDATE,
|
||||
labelForUpdate: label,
|
||||
});
|
||||
}}
|
||||
onDelete={this.handleDelete}
|
||||
onCreate={() => {
|
||||
this.setState({
|
||||
constructorMode: ConstructorMode.CREATE,
|
||||
})
|
||||
}}
|
||||
/> :
|
||||
|
||||
this.state.constructorMode === ConstructorMode.UPDATE
|
||||
&& this.state.labelForUpdate !== null ?
|
||||
<ConstructorUpdater
|
||||
label={this.state.labelForUpdate}
|
||||
onUpdate={this.handleUpdate}
|
||||
/> :
|
||||
|
||||
<ConstructorCreator
|
||||
onCreate={this.handleCreate}
|
||||
/>
|
||||
}
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,115 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Form,
|
||||
Input,
|
||||
Button,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
|
||||
import { FormComponentProps } from 'antd/lib/form/Form';
|
||||
|
||||
import {
|
||||
Attribute,
|
||||
Label,
|
||||
equalArrayHead,
|
||||
} from './common';
|
||||
|
||||
|
||||
|
||||
type Props = FormComponentProps & {
|
||||
labels: Label[];
|
||||
onSubmit: (labels: Label[]) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
labels: object[];
|
||||
valid: boolean;
|
||||
}
|
||||
|
||||
class RawViewer extends React.PureComponent<Props, State> {
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const labels = JSON.parse(JSON.stringify(this.props.labels));
|
||||
for (const label of labels) {
|
||||
for (const attr of label.attributes) {
|
||||
if (attr.id < 0) {
|
||||
delete attr.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (label.id < 0) {
|
||||
delete label.id;
|
||||
}
|
||||
}
|
||||
|
||||
this.state = {
|
||||
labels,
|
||||
valid: true,
|
||||
};
|
||||
}
|
||||
|
||||
private validateLabels = (_: any, value: string, callback: any) => {
|
||||
try {
|
||||
JSON.parse(value);
|
||||
} catch (error) {
|
||||
callback(error.toString());
|
||||
}
|
||||
|
||||
callback();
|
||||
}
|
||||
|
||||
private handleSubmit = (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
this.props.form.validateFields((error, values) => {
|
||||
if (!error) {
|
||||
this.props.onSubmit(JSON.parse(values.labels));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
const textLabels = JSON.stringify(this.state.labels, null, 2);
|
||||
|
||||
return (
|
||||
<Form onSubmit={this.handleSubmit}>
|
||||
<Form.Item> {
|
||||
this.props.form.getFieldDecorator('labels', {
|
||||
initialValue: textLabels,
|
||||
rules: [{
|
||||
validator: this.validateLabels,
|
||||
}]
|
||||
})( <Input.TextArea rows={5} className='cvat-raw-labels-viewer'/> )
|
||||
} </Form.Item>
|
||||
<Row type='flex' justify='start' align='middle'>
|
||||
<Col span={4}>
|
||||
<Tooltip overlay='Save labels and return'>
|
||||
<Button
|
||||
style={{width: '150px'}}
|
||||
type='primary'
|
||||
htmlType='submit'
|
||||
> Done </Button>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
<Col span={1}/>
|
||||
<Col span={4}>
|
||||
<Tooltip overlay='Do not save the label and return'>
|
||||
<Button
|
||||
style={{width: '150px'}}
|
||||
type='danger'
|
||||
onClick={() => {
|
||||
this.props.form.resetFields();
|
||||
}}
|
||||
> Reset </Button>
|
||||
</Tooltip>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create<Props>()(RawViewer);
|
||||
@ -0,0 +1,279 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Modal,
|
||||
Button,
|
||||
Select,
|
||||
} from 'antd';
|
||||
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import Title from 'antd/lib/typography/Title';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
import LabelsEditorComponent from '../labels-editor/labels-editor';
|
||||
import getCore from '../../core';
|
||||
import patterns from '../../utils/validation-patterns';
|
||||
|
||||
const core = getCore();
|
||||
|
||||
interface Props {
|
||||
previewImage: string;
|
||||
taskInstance: any;
|
||||
installedGit: boolean; // change to git repos url
|
||||
registeredUsers: any[];
|
||||
onTaskUpdate: (taskInstance: any) => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
name: string;
|
||||
bugTracker: string;
|
||||
assignee: any;
|
||||
}
|
||||
|
||||
export default class DetailsComponent extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
const { taskInstance } = props;
|
||||
|
||||
this.state = {
|
||||
name: taskInstance.name,
|
||||
bugTracker: taskInstance.bugTracker,
|
||||
assignee: taskInstance.assignee,
|
||||
};
|
||||
}
|
||||
|
||||
private renderTaskName() {
|
||||
const { taskInstance } = this.props;
|
||||
const { name } = this.state;
|
||||
return (
|
||||
<Title
|
||||
level={4}
|
||||
editable={{
|
||||
onChange: (value: string) => {
|
||||
this.setState({
|
||||
name: value,
|
||||
});
|
||||
|
||||
taskInstance.name = value;
|
||||
this.props.onTaskUpdate(taskInstance);
|
||||
},
|
||||
}}
|
||||
className='cvat-black-color'
|
||||
>{name}</Title>
|
||||
);
|
||||
}
|
||||
|
||||
private renderPreview() {
|
||||
return (
|
||||
<div className='cvat-task-preview-wrapper'>
|
||||
<img alt='Preview' className='cvat-task-preview' src={this.props.previewImage}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
private renderParameters() {
|
||||
const { taskInstance } = this.props;
|
||||
const { overlap } = taskInstance;
|
||||
const { segmentSize } = taskInstance;
|
||||
const { imageQuality } = taskInstance;
|
||||
const zOrder = taskInstance.zOrder.toString();
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row type='flex' justify='start' align='middle'>
|
||||
<Col span={12}>
|
||||
<Text strong className='cvat-black-color'> Overlap size </Text>
|
||||
<br/>
|
||||
<Text className='cvat-black-color'>{overlap}</Text>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Text strong className='cvat-black-color'> Segment size </Text>
|
||||
<br/>
|
||||
<Text className='cvat-black-color'>{segmentSize}</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
<Row type='flex' justify='space-between' align='middle'>
|
||||
<Col span={12}>
|
||||
<Text strong className='cvat-black-color'> Image quality </Text>
|
||||
<br/>
|
||||
<Text className='cvat-black-color'>{imageQuality}</Text>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Text strong className='cvat-black-color'> Z-order </Text>
|
||||
<br/>
|
||||
<Text className='cvat-black-color'>{zOrder}</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderUsers() {
|
||||
const { taskInstance } = this.props;
|
||||
const owner = taskInstance.owner ? taskInstance.owner.username : null;
|
||||
const assignee = this.state.assignee ? this.state.assignee.username : null;
|
||||
const created = moment(taskInstance.createdDate).format('MMMM Do YYYY');
|
||||
const assigneeSelect = (
|
||||
<Select
|
||||
value={assignee ? assignee : '\0'}
|
||||
size='small'
|
||||
showSearch
|
||||
className='cvat-task-assignee-selector'
|
||||
onChange={(value: string) => {
|
||||
let [userInstance] = this.props.registeredUsers
|
||||
.filter((user: any) => user.username === value);
|
||||
|
||||
if (userInstance === undefined) {
|
||||
userInstance = null;
|
||||
}
|
||||
|
||||
this.setState({
|
||||
assignee: userInstance,
|
||||
});
|
||||
|
||||
taskInstance.assignee = userInstance;
|
||||
this.props.onTaskUpdate(taskInstance);
|
||||
}}
|
||||
>
|
||||
<Select.Option key='-1' value='\0'>{'\0'}</Select.Option>
|
||||
{ this.props.registeredUsers.map((user) => {
|
||||
return (
|
||||
<Select.Option key={user.id} value={user.username}>
|
||||
{user.username}
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
);
|
||||
|
||||
return (
|
||||
<Row type='flex' justify='space-between' align='middle'>
|
||||
<Col span={12}>
|
||||
{ owner ? <Text type='secondary'>
|
||||
Created by {owner} on {created}
|
||||
</Text> : null }
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Text type='secondary'>
|
||||
{'Assigned to'}
|
||||
{ assigneeSelect }
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
private renderBugTracker() {
|
||||
const { taskInstance } = this.props;
|
||||
const { bugTracker } = this.state;
|
||||
|
||||
const onChangeValue = (value: string) => {
|
||||
if (value && !patterns.validateURL.pattern.test(value)) {
|
||||
Modal.error({
|
||||
title: `Could not update the task ${taskInstance.id}`,
|
||||
content: 'Issue tracker is expected to be URL',
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
bugTracker: value,
|
||||
});
|
||||
|
||||
taskInstance.bugTracker = value;
|
||||
this.props.onTaskUpdate(taskInstance);
|
||||
}
|
||||
}
|
||||
|
||||
if (bugTracker) {
|
||||
return (
|
||||
<Row>
|
||||
<Col>
|
||||
<Text strong className='cvat-black-color'> Issue Tracker </Text>
|
||||
<br/>
|
||||
<Text editable={{onChange: onChangeValue}}>{bugTracker}</Text>
|
||||
<Button type='ghost' size='small' onClick={() => {
|
||||
window.open(bugTracker, '_blank');
|
||||
}} className='cvat-open-bug-tracker-button'>{'Open the issue'}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Row>
|
||||
<Col>
|
||||
<Text strong className='cvat-black-color'> Issue Tracker </Text>
|
||||
<br/>
|
||||
<Text editable={{onChange: onChangeValue}}>{'Not specified'}</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private renderLabelsEditor() {
|
||||
const { taskInstance } = this.props;
|
||||
|
||||
return (
|
||||
<Row>
|
||||
<Col>
|
||||
<LabelsEditorComponent
|
||||
labels={taskInstance.labels.map(
|
||||
(label: any) => label.toJSON()
|
||||
)}
|
||||
onSubmit={(labels: any[]) => {
|
||||
taskInstance.labels = labels.map((labelData) => {
|
||||
return new core.classes.Label(labelData);
|
||||
});
|
||||
|
||||
this.props.onTaskUpdate(taskInstance);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props) {
|
||||
if (prevProps !== this.props) {
|
||||
this.setState({
|
||||
name: this.props.taskInstance.name,
|
||||
bugTracker: this.props.taskInstance.bugTracker,
|
||||
assignee: this.props.taskInstance.assignee,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<div className='cvat-task-details'>
|
||||
<Row type='flex' justify='start' align='middle'>
|
||||
<Col>
|
||||
{ this.renderTaskName() }
|
||||
</Col>
|
||||
</Row>
|
||||
<Row type='flex' justify='space-between' align='top'>
|
||||
<Col md={8} lg={7} xl={7} xxl={6}>
|
||||
<Row type='flex' justify='start' align='middle'>
|
||||
<Col span={24}>
|
||||
{ this.renderPreview() }
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
{ this.renderParameters() }
|
||||
</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col md={16} lg={17} xl={17} xxl={18}>
|
||||
{ this.renderUsers() }
|
||||
{ this.renderBugTracker() }
|
||||
{ this.renderLabelsEditor() }
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Icon,
|
||||
Table,
|
||||
} from 'antd';
|
||||
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import Title from 'antd/lib/typography/Title';
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
import getCore from '../../core';
|
||||
const core = getCore();
|
||||
|
||||
const baseURL = core.config.backendAPI.slice(0, -7);
|
||||
|
||||
interface Props {
|
||||
taskInstance: any;
|
||||
}
|
||||
|
||||
export default function JobListComponent(props: Props) {
|
||||
const { jobs } = props.taskInstance;
|
||||
const columns = [{
|
||||
title: 'Job',
|
||||
dataIndex: 'job',
|
||||
key: 'job',
|
||||
render: (id: number) => {
|
||||
return (
|
||||
<a href={`${baseURL}/?id=${id}`}>{ `Job #${id++}` }</a>
|
||||
);
|
||||
}
|
||||
}, {
|
||||
title: 'Frames',
|
||||
dataIndex: 'frames',
|
||||
key: 'frames',
|
||||
className: 'cvat-black-color',
|
||||
}, {
|
||||
title: 'Status',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => {
|
||||
const progressColor = status === 'completed' ? 'cvat-job-completed-color':
|
||||
status === 'validation' ? 'cvat-job-validation-color' : 'cvat-job-annotation-color';
|
||||
|
||||
return (
|
||||
<Text strong className={progressColor}>{ status }</Text>
|
||||
);
|
||||
}
|
||||
}, {
|
||||
title: 'Started on',
|
||||
dataIndex: 'started',
|
||||
key: 'started',
|
||||
className: 'cvat-black-color',
|
||||
}, {
|
||||
title: 'Duration',
|
||||
dataIndex: 'duration',
|
||||
key: 'duration',
|
||||
className: 'cvat-black-color',
|
||||
}, {
|
||||
title: 'Assignee',
|
||||
dataIndex: 'assignee',
|
||||
key: 'assignee',
|
||||
className: 'cvat-black-color',
|
||||
}];
|
||||
|
||||
let completed = 0;
|
||||
const data = jobs.reduce((acc: any[], job: any) => {
|
||||
if (job.status === 'completed') {
|
||||
completed++;
|
||||
}
|
||||
|
||||
const created = moment(props.taskInstance.createdDate);
|
||||
|
||||
acc.push({
|
||||
key: job.id,
|
||||
job: job.id,
|
||||
frames: `${job.startFrame}-${job.stopFrame}`,
|
||||
status: `${job.status}`,
|
||||
started: `${created.format('MMMM Do YYYY HH:MM')}`,
|
||||
duration: `${moment.duration(moment(moment.now()).diff(created)).humanize()}`,
|
||||
assignee: `${job.assignee ? job.assignee.username : ''}`,
|
||||
});
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className='cvat-task-job-list'>
|
||||
<Row type='flex' justify='space-between' align='middle'>
|
||||
<Col>
|
||||
<Title level={4} className='cvat-black-color cvat-jobs-header'> Jobs </Title>
|
||||
</Col>
|
||||
<Col>
|
||||
<Text className='cvat-black-color'>
|
||||
{`${completed} of ${data.length} jobs`}
|
||||
</Text>
|
||||
</Col>
|
||||
</Row>
|
||||
<Table
|
||||
className='cvat-task-jobs-table'
|
||||
columns={columns}
|
||||
dataSource={data}
|
||||
size='small'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
import React from 'react';
|
||||
import { RouteComponentProps } from 'react-router';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
Col,
|
||||
Row,
|
||||
Spin,
|
||||
Modal,
|
||||
} from 'antd';
|
||||
|
||||
import TopBarContainer from '../../containers/task-page/top-bar';
|
||||
import DetailsContainer from '../../containers/task-page/details';
|
||||
import JobListContainer from '../../containers/task-page/job-list';
|
||||
|
||||
interface TaskPageComponentProps {
|
||||
taskInstance: any;
|
||||
taskFetchingError: string;
|
||||
taskUpdatingError: string;
|
||||
deleteActivity: boolean | null;
|
||||
installedGit: boolean;
|
||||
onFetchTask: (tid: number) => void;
|
||||
}
|
||||
|
||||
type Props = TaskPageComponentProps & RouteComponentProps<{id: string}>;
|
||||
|
||||
class TaskPageComponent extends React.PureComponent<Props> {
|
||||
public componentDidUpdate() {
|
||||
if (this.props.deleteActivity) {
|
||||
this.props.history.replace('/tasks');
|
||||
}
|
||||
|
||||
const { id } = this.props.match.params;
|
||||
|
||||
if (this.props.taskFetchingError) {
|
||||
Modal.error({
|
||||
title: `Could not receive the task ${id}`,
|
||||
content: this.props.taskFetchingError,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.taskUpdatingError) {
|
||||
Modal.error({
|
||||
title: `Could not update the task ${id}`,
|
||||
content: this.props.taskUpdatingError,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { id } = this.props.match.params;
|
||||
const fetchTask = !this.props.taskInstance && !this.props.taskFetchingError
|
||||
|| (this.props.taskInstance && this.props.taskInstance.id !== +id );
|
||||
|
||||
if (fetchTask) {
|
||||
this.props.onFetchTask(+id);
|
||||
return (
|
||||
<Spin size='large' style={{margin: '25% 50%'}}/>
|
||||
);
|
||||
} else if (this.props.taskFetchingError) {
|
||||
return (
|
||||
<div> </div>
|
||||
)
|
||||
} else {
|
||||
return (
|
||||
<Row type='flex' justify='center' align='middle'>
|
||||
<Col md={22} lg={18} xl={16} xxl={14}>
|
||||
<TopBarContainer/>
|
||||
<DetailsContainer/>
|
||||
<JobListContainer/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(TaskPageComponent);
|
||||
@ -0,0 +1,60 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Button,
|
||||
Dropdown,
|
||||
Icon,
|
||||
} from 'antd';
|
||||
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
|
||||
import ActionsMenu from '../actions-menu/actions-menu';
|
||||
|
||||
interface DetailsComponentProps {
|
||||
taskInstance: any;
|
||||
loaders: any[];
|
||||
dumpers: any[];
|
||||
loadActivity: string | null;
|
||||
dumpActivities: string[] | null;
|
||||
installedTFAnnotation: boolean;
|
||||
installedAutoAnnotation: boolean;
|
||||
onLoadAnnotation: (taskInstance: any, loader: any, file: File) => void;
|
||||
onDumpAnnotation: (task: any, dumper: any) => void;
|
||||
onDeleteTask: (task: any) => void;
|
||||
}
|
||||
|
||||
export default function DetailsComponent(props: DetailsComponentProps) {
|
||||
const subMenuIcon = () => (<img src='/assets/icon-sub-menu.svg'/>);
|
||||
const { id } = props.taskInstance;
|
||||
|
||||
return (
|
||||
<Row className='cvat-task-top-bar' type='flex' justify='space-between' align='middle'>
|
||||
<Col>
|
||||
<Text className='cvat-title'> Task details #{id} </Text>
|
||||
</Col>
|
||||
<Col>
|
||||
<Dropdown overlay={
|
||||
ActionsMenu({
|
||||
taskInstance: props.taskInstance,
|
||||
loaders: props.loaders,
|
||||
dumpers: props.dumpers,
|
||||
loadActivity: props.loadActivity,
|
||||
dumpActivities: props.dumpActivities,
|
||||
installedTFAnnotation: props.installedTFAnnotation,
|
||||
installedAutoAnnotation: props.installedAutoAnnotation,
|
||||
onLoadAnnotation: props.onLoadAnnotation,
|
||||
onDumpAnnotation: props.onDumpAnnotation,
|
||||
onDeleteTask: props.onDeleteTask,
|
||||
})
|
||||
}>
|
||||
<Button size='large' className='cvat-flex cvat-flex-center'>
|
||||
<Text className='cvat-black-color'> Actions </Text>
|
||||
<Icon className='cvat-task-item-menu-icon' component={subMenuIcon}/>
|
||||
</Button>
|
||||
</Dropdown>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import DetailsComponent from '../../components/task-page/details';
|
||||
import { CombinedState } from '../../reducers/root-reducer';
|
||||
import { updateTaskAsync } from '../../actions/task-actions';
|
||||
|
||||
interface StateToProps {
|
||||
previewImage: string;
|
||||
taskInstance: any;
|
||||
registeredUsers: any[];
|
||||
installedGit: boolean;
|
||||
}
|
||||
|
||||
interface DispatchToProps {
|
||||
onTaskUpdate: (taskInstance: any) => void;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: CombinedState): StateToProps {
|
||||
const { plugins } = state.plugins;
|
||||
const taskInstance = (state.activeTask.task as any).instance;
|
||||
const previewImage = (state.activeTask.task as any).preview;
|
||||
|
||||
return {
|
||||
registeredUsers: state.users.users,
|
||||
taskInstance,
|
||||
previewImage,
|
||||
installedGit: plugins.GIT_INTEGRATION,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||
return {
|
||||
onTaskUpdate: (taskInstance: any) =>
|
||||
dispatch(updateTaskAsync(taskInstance))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function TaskPageContainer(props: StateToProps & DispatchToProps) {
|
||||
return (
|
||||
<DetailsComponent
|
||||
previewImage={props.previewImage}
|
||||
taskInstance={props.taskInstance}
|
||||
installedGit={props.installedGit}
|
||||
onTaskUpdate={props.onTaskUpdate}
|
||||
registeredUsers={props.registeredUsers}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(TaskPageContainer);
|
||||
@ -0,0 +1,99 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getTaskAsync } from '../../actions/task-actions';
|
||||
import {
|
||||
dumpAnnotationsAsync,
|
||||
loadAnnotationsAsync,
|
||||
deleteTaskAsync,
|
||||
} from '../../actions/tasks-actions';
|
||||
|
||||
import JobListComponent from '../../components/task-page/job-list';
|
||||
import { CombinedState } from '../../reducers/root-reducer';
|
||||
|
||||
interface StateToProps {
|
||||
taskFetchingError: any;
|
||||
previewImage: string;
|
||||
taskInstance: any;
|
||||
loaders: any[];
|
||||
dumpers: any[];
|
||||
loadActivity: string | null;
|
||||
dumpActivities: string[] | null;
|
||||
deleteActivity: boolean | null;
|
||||
installedTFAnnotation: boolean;
|
||||
installedAutoAnnotation: boolean;
|
||||
installedGit: boolean;
|
||||
}
|
||||
|
||||
interface DispatchToProps {
|
||||
fetchTask: (tid: number) => void;
|
||||
deleteTask: (taskInstance: any) => void;
|
||||
dumpAnnotations: (task: any, format: string) => void;
|
||||
loadAnnotations: (task: any, format: string, file: File) => void;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: CombinedState): StateToProps {
|
||||
const { plugins } = state.plugins;
|
||||
const { formats } = state;
|
||||
const { activeTask } = state;
|
||||
const { dumps } = state.tasks.activities;
|
||||
const { loads } = state.tasks.activities;
|
||||
const { deletes } = state.tasks.activities;
|
||||
|
||||
const taskInstance = activeTask.task ? activeTask.task.instance : null;
|
||||
const previewImage = activeTask.task ? activeTask.task.preview : '';
|
||||
|
||||
let dumpActivities = null;
|
||||
let loadActivity = null;
|
||||
let deleteActivity = null;
|
||||
if (taskInstance) {
|
||||
const { id } = taskInstance;
|
||||
dumpActivities = dumps.byTask[id] ? dumps.byTask[id] : null;
|
||||
loadActivity = loads.byTask[id] ? loads.byTask[id] : null;
|
||||
deleteActivity = deletes.byTask[id] ? deletes.byTask[id] : null;
|
||||
}
|
||||
|
||||
return {
|
||||
previewImage,
|
||||
taskInstance,
|
||||
taskFetchingError: activeTask.taskFetchingError,
|
||||
loaders: formats.loaders,
|
||||
dumpers: formats.dumpers,
|
||||
dumpActivities,
|
||||
loadActivity,
|
||||
deleteActivity,
|
||||
installedGit: plugins.GIT_INTEGRATION,
|
||||
installedTFAnnotation: plugins.TF_ANNOTATION,
|
||||
installedAutoAnnotation: plugins.AUTO_ANNOTATION,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||
return {
|
||||
fetchTask: (tid: number) => {
|
||||
dispatch(getTaskAsync(tid));
|
||||
},
|
||||
deleteTask: (taskInstance: any): void => {
|
||||
dispatch(deleteTaskAsync(taskInstance));
|
||||
},
|
||||
dumpAnnotations: (task: any, dumper: any): void => {
|
||||
dispatch(dumpAnnotationsAsync(task, dumper));
|
||||
},
|
||||
loadAnnotations: (task: any, loader: any, file: File): void => {
|
||||
dispatch(loadAnnotationsAsync(task, loader, file));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function TaskPageContainer(props: StateToProps & DispatchToProps) {
|
||||
return (
|
||||
<JobListComponent
|
||||
taskInstance={props.taskInstance}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(TaskPageContainer);
|
||||
@ -1,9 +1,67 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
export default function TaskPage() {
|
||||
import { getTaskAsync } from '../../actions/task-actions';
|
||||
|
||||
import TaskPageComponent from '../../components/task-page/task-page';
|
||||
import { CombinedState } from '../../reducers/root-reducer';
|
||||
|
||||
interface StateToProps {
|
||||
taskFetchingError: any;
|
||||
taskUpdatingError: any;
|
||||
taskInstance: any;
|
||||
deleteActivity: boolean | null;
|
||||
installedGit: boolean;
|
||||
}
|
||||
|
||||
interface DispatchToProps {
|
||||
fetchTask: (tid: number) => void;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: CombinedState): StateToProps {
|
||||
const { plugins } = state.plugins;
|
||||
const { activeTask } = state;
|
||||
const { deletes } = state.tasks.activities;
|
||||
|
||||
const taskInstance = activeTask.task ? activeTask.task.instance : null;
|
||||
|
||||
let deleteActivity = null;
|
||||
if (taskInstance) {
|
||||
const { id } = taskInstance;
|
||||
deleteActivity = deletes.byTask[id] ? deletes.byTask[id] : null;
|
||||
}
|
||||
|
||||
return {
|
||||
taskInstance,
|
||||
taskFetchingError: activeTask.taskFetchingError,
|
||||
taskUpdatingError: activeTask.taskUpdatingError,
|
||||
deleteActivity,
|
||||
installedGit: plugins.GIT_INTEGRATION,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||
return {
|
||||
fetchTask: (tid: number) => {
|
||||
dispatch(getTaskAsync(tid));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function TaskPageContainer(props: StateToProps & DispatchToProps) {
|
||||
return (
|
||||
<div>
|
||||
"Task Page"
|
||||
</div>
|
||||
<TaskPageComponent
|
||||
taskInstance={props.taskInstance}
|
||||
taskFetchingError={props.taskFetchingError ? props.taskFetchingError.toString() : ''}
|
||||
taskUpdatingError={props.taskUpdatingError ? props.taskUpdatingError.toString() : ''}
|
||||
deleteActivity={props.deleteActivity}
|
||||
installedGit={props.installedGit}
|
||||
onFetchTask={props.fetchTask}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(TaskPageContainer);
|
||||
@ -0,0 +1,86 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import {
|
||||
dumpAnnotationsAsync,
|
||||
loadAnnotationsAsync,
|
||||
deleteTaskAsync,
|
||||
} from '../../actions/tasks-actions';
|
||||
|
||||
import TopBarComponent from '../../components/task-page/top-bar';
|
||||
import { CombinedState } from '../../reducers/root-reducer';
|
||||
|
||||
interface StateToProps {
|
||||
taskInstance: any;
|
||||
loaders: any[];
|
||||
dumpers: any[];
|
||||
loadActivity: string | null;
|
||||
dumpActivities: string[] | null;
|
||||
installedTFAnnotation: boolean;
|
||||
installedAutoAnnotation: boolean;
|
||||
}
|
||||
|
||||
interface DispatchToProps {
|
||||
deleteTask: (taskInstance: any) => void;
|
||||
dumpAnnotations: (task: any, format: string) => void;
|
||||
loadAnnotations: (task: any, format: string, file: File) => void;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: CombinedState): StateToProps {
|
||||
const taskInstance = (state.activeTask.task as any).instance;
|
||||
|
||||
const { plugins } = state.plugins;
|
||||
const { formats } = state;
|
||||
const { dumps } = state.tasks.activities;
|
||||
const { loads } = state.tasks.activities;
|
||||
|
||||
const { id } = taskInstance;
|
||||
const dumpActivities = dumps.byTask[id] ? dumps.byTask[id] : null;
|
||||
const loadActivity = loads.byTask[id] ? loads.byTask[id] : null;
|
||||
|
||||
return {
|
||||
taskInstance,
|
||||
loaders: formats.loaders,
|
||||
dumpers: formats.dumpers,
|
||||
dumpActivities,
|
||||
loadActivity,
|
||||
installedTFAnnotation: plugins.TF_ANNOTATION,
|
||||
installedAutoAnnotation: plugins.AUTO_ANNOTATION,
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||
return {
|
||||
deleteTask: (taskInstance: any): void => {
|
||||
dispatch(deleteTaskAsync(taskInstance));
|
||||
},
|
||||
dumpAnnotations: (task: any, dumper: any): void => {
|
||||
dispatch(dumpAnnotationsAsync(task, dumper));
|
||||
},
|
||||
loadAnnotations: (task: any, loader: any, file: File): void => {
|
||||
dispatch(loadAnnotationsAsync(task, loader, file));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function TaskPageContainer(props: StateToProps & DispatchToProps) {
|
||||
return (
|
||||
<TopBarComponent
|
||||
taskInstance={props.taskInstance}
|
||||
loaders={props.loaders}
|
||||
dumpers={props.dumpers}
|
||||
loadActivity={props.loadActivity}
|
||||
dumpActivities={props.dumpActivities}
|
||||
installedTFAnnotation={props.installedTFAnnotation}
|
||||
installedAutoAnnotation={props.installedAutoAnnotation}
|
||||
onDeleteTask={props.deleteTask}
|
||||
onDumpAnnotation={props.dumpAnnotations}
|
||||
onLoadAnnotation={props.loadAnnotations}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(TaskPageContainer);
|
||||
@ -0,0 +1,32 @@
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { PluginsActionTypes } from '../actions/plugins-actions';
|
||||
|
||||
import {
|
||||
PluginsState,
|
||||
} from './interfaces';
|
||||
|
||||
const defaultState: PluginsState = {
|
||||
initialized: false,
|
||||
plugins: {
|
||||
GIT_INTEGRATION: false,
|
||||
AUTO_ANNOTATION: false,
|
||||
TF_ANNOTATION: false,
|
||||
ANALYTICS: false,
|
||||
},
|
||||
};
|
||||
|
||||
export default function (state = defaultState, action: AnyAction): PluginsState {
|
||||
switch (action.type) {
|
||||
case PluginsActionTypes.CHECKED_ALL_PLUGINS: {
|
||||
const { plugins } = action.payload;
|
||||
return {
|
||||
...state,
|
||||
initialized: true,
|
||||
plugins,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return { ...state };
|
||||
}
|
||||
}
|
||||
@ -1,24 +1,36 @@
|
||||
import { combineReducers, Reducer } from 'redux';
|
||||
import authReducer from './auth-reducer';
|
||||
import tasksReducer from './tasks-reducer';
|
||||
import usersReducer from './users-reducer';
|
||||
import formatsReducer from './formats-reducer';
|
||||
import pluginsReducer from './plugins-reducer';
|
||||
import taskReducer from './task-reducer';
|
||||
|
||||
import {
|
||||
AuthState,
|
||||
TasksState,
|
||||
UsersState,
|
||||
FormatsState,
|
||||
PluginsState,
|
||||
TaskState,
|
||||
} from './interfaces';
|
||||
|
||||
export interface CombinedState {
|
||||
auth: AuthState;
|
||||
tasks: TasksState;
|
||||
users: UsersState;
|
||||
formats: FormatsState;
|
||||
plugins: PluginsState;
|
||||
activeTask: TaskState;
|
||||
}
|
||||
|
||||
export default function createRootReducer(): Reducer {
|
||||
return combineReducers({
|
||||
auth: authReducer,
|
||||
tasks: tasksReducer,
|
||||
users: usersReducer,
|
||||
formats: formatsReducer,
|
||||
plugins: pluginsReducer,
|
||||
activeTask: taskReducer,
|
||||
});
|
||||
}
|
||||
|
||||
@ -0,0 +1,65 @@
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { TaskActionTypes } from '../actions/task-actions';
|
||||
import { Task, TaskState } from './interfaces';
|
||||
|
||||
const defaultState: TaskState = {
|
||||
taskFetchingError: null,
|
||||
taskUpdatingError: null,
|
||||
task: null,
|
||||
};
|
||||
|
||||
export default function (state = defaultState, action: AnyAction): TaskState {
|
||||
switch (action.type) {
|
||||
case TaskActionTypes.GET_TASK:
|
||||
return {
|
||||
...state,
|
||||
taskFetchingError: null,
|
||||
taskUpdatingError: null,
|
||||
};
|
||||
case TaskActionTypes.GET_TASK_SUCCESS: {
|
||||
return {
|
||||
...state,
|
||||
task: {
|
||||
instance: action.payload.taskInstance,
|
||||
preview: action.payload.previewImage,
|
||||
},
|
||||
};
|
||||
}
|
||||
case TaskActionTypes.GET_TASK_FAILED: {
|
||||
return {
|
||||
...state,
|
||||
task: null,
|
||||
taskFetchingError: action.payload.error,
|
||||
};
|
||||
}
|
||||
case TaskActionTypes.UPDATE_TASK: {
|
||||
return {
|
||||
...state,
|
||||
taskUpdatingError: null,
|
||||
taskFetchingError: null,
|
||||
};
|
||||
}
|
||||
case TaskActionTypes.UPDATE_TASK_SUCCESS: {
|
||||
return {
|
||||
...state,
|
||||
task: {
|
||||
...(state.task as Task),
|
||||
instance: action.payload.taskInstance,
|
||||
},
|
||||
};
|
||||
}
|
||||
case TaskActionTypes.UPDATE_TASK_FAILED: {
|
||||
return {
|
||||
...state,
|
||||
task: {
|
||||
...(state.task as Task),
|
||||
instance: action.payload.taskInstance,
|
||||
},
|
||||
taskUpdatingError: action.payload.error,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return { ...state };
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import { AnyAction } from 'redux';
|
||||
import { UsersState } from './interfaces';
|
||||
|
||||
import { UsersActionTypes } from '../actions/users-actions';
|
||||
|
||||
const initialState: UsersState = {
|
||||
users: [],
|
||||
initialized: false,
|
||||
gettingUsersError: null,
|
||||
};
|
||||
|
||||
export default function (state: UsersState = initialState, action: AnyAction): UsersState {
|
||||
switch (action.type) {
|
||||
case UsersActionTypes.GET_USERS:
|
||||
return {
|
||||
...state,
|
||||
initialized: false,
|
||||
gettingUsersError: null,
|
||||
};
|
||||
case UsersActionTypes.GET_USERS_SUCCESS:
|
||||
return {
|
||||
...state,
|
||||
initialized: true,
|
||||
users: action.payload.users,
|
||||
};
|
||||
case UsersActionTypes.GET_USERS_FAILED:
|
||||
return {
|
||||
...state,
|
||||
initialized: true,
|
||||
users: [],
|
||||
gettingUsersError: action.payload.error,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,46 @@
|
||||
import getCore from '../core';
|
||||
import { SupportedPlugins } from '../reducers/interfaces';
|
||||
|
||||
const core = getCore();
|
||||
|
||||
// Easy plugin checker to understand what plugins supports by a server
|
||||
class PluginChecker {
|
||||
public static async check(plugin: SupportedPlugins): Promise<boolean> {
|
||||
const serverHost = core.config.backendAPI.slice(0, -7);
|
||||
|
||||
switch (plugin) {
|
||||
case SupportedPlugins.GIT_INTEGRATION: {
|
||||
const response = await fetch(`${serverHost}/git/repository/meta/get`);
|
||||
if (response.ok) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case SupportedPlugins.AUTO_ANNOTATION: {
|
||||
const response = await fetch(`${serverHost}/auto_annotation/meta/get`);
|
||||
if (response.ok) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case SupportedPlugins.TF_ANNOTATION: {
|
||||
const response = await fetch(`${serverHost}/tensorflow/annotation/meta/get`);
|
||||
if (response.ok) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case SupportedPlugins.ANALYTICS: {
|
||||
const response = await fetch(`${serverHost}/analytics/app/kibana`);
|
||||
if (response.ok) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default PluginChecker;
|
||||
Loading…
Reference in New Issue