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 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 (
|
return (
|
||||||
<div>
|
<TaskPageComponent
|
||||||
"Task Page"
|
taskInstance={props.taskInstance}
|
||||||
</div>
|
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 { combineReducers, Reducer } from 'redux';
|
||||||
import authReducer from './auth-reducer';
|
import authReducer from './auth-reducer';
|
||||||
import tasksReducer from './tasks-reducer';
|
import tasksReducer from './tasks-reducer';
|
||||||
|
import usersReducer from './users-reducer';
|
||||||
import formatsReducer from './formats-reducer';
|
import formatsReducer from './formats-reducer';
|
||||||
|
import pluginsReducer from './plugins-reducer';
|
||||||
|
import taskReducer from './task-reducer';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AuthState,
|
AuthState,
|
||||||
TasksState,
|
TasksState,
|
||||||
|
UsersState,
|
||||||
FormatsState,
|
FormatsState,
|
||||||
|
PluginsState,
|
||||||
|
TaskState,
|
||||||
} from './interfaces';
|
} from './interfaces';
|
||||||
|
|
||||||
export interface CombinedState {
|
export interface CombinedState {
|
||||||
auth: AuthState;
|
auth: AuthState;
|
||||||
tasks: TasksState;
|
tasks: TasksState;
|
||||||
|
users: UsersState;
|
||||||
formats: FormatsState;
|
formats: FormatsState;
|
||||||
|
plugins: PluginsState;
|
||||||
|
activeTask: TaskState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function createRootReducer(): Reducer {
|
export default function createRootReducer(): Reducer {
|
||||||
return combineReducers({
|
return combineReducers({
|
||||||
auth: authReducer,
|
auth: authReducer,
|
||||||
tasks: tasksReducer,
|
tasks: tasksReducer,
|
||||||
|
users: usersReducer,
|
||||||
formats: formatsReducer,
|
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