提交 9016805f 编写于 作者: B Boris Sekachev 提交者: Nikita Manovich

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 assignee
上级 51cfab1a
......@@ -31,6 +31,26 @@
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
function attachUsers(task, users) {
if (task.assignee !== null) {
[task.assignee] = users.filter((user) => user.id === task.assignee);
}
for (const segment of task.segments) {
for (const job of segment.jobs) {
if (job.assignee !== null) {
[job.assignee] = users.filter((user) => user.id === job.assignee);
}
}
}
if (task.owner !== null) {
[task.owner] = users.filter((user) => user.id === task.owner);
}
return task;
}
function implementAPI(cvat) {
cvat.plugins.list.implementation = PluginRegistry.list;
cvat.plugins.register.implementation = PluginRegistry.register.bind(cvat);
......@@ -116,9 +136,10 @@
// If task was found by its id, then create task instance and get Job instance from it
if (tasks !== null && tasks.length) {
tasks[0].owner = await serverProxy.users.getUsers(tasks[0].owner);
tasks[0].assignee = await serverProxy.users.getUsers(tasks[0].assignee);
const task = new Task(tasks[0]);
const users = (await serverProxy.users.getUsers())
.map((userData) => new User(userData));
const task = new Task(attachUsers(tasks[0], users));
return filter.jobID ? task.jobs
.filter((job) => job.id === filter.jobID) : task.jobs;
}
......@@ -161,13 +182,14 @@
}
}
const users = await serverProxy.users.getUsers();
const users = (await serverProxy.users.getUsers())
.map((userData) => new User(userData));
const tasksData = await serverProxy.tasks.getTasks(searchParams.toString());
const tasks = tasksData.map((task) => {
[task.owner] = users.filter((user) => user.id === task.owner);
[task.assignee] = users.filter((user) => user.id === task.assignee);
return new Task(task);
});
const tasks = tasksData
.map((task) => attachUsers(task, users))
.map((task) => new Task(task));
tasks.count = tasksData.count;
return tasks;
......
......@@ -14,6 +14,7 @@
const { ArgumentError } = require('./exceptions');
const { TaskStatus } = require('./enums');
const { Label } = require('./labels');
const User = require('./user');
function buildDublicatedAPI(prototype) {
Object.defineProperties(prototype, {
......@@ -536,19 +537,19 @@
get: () => data.id,
},
/**
* Identifier of a user who is responsible for the job
* Instance of a user who is responsible for the job
* @name assignee
* @type {integer}
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Job
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
assignee: {
get: () => data.assignee,
set: () => (assignee) => {
if (!Number.isInteger(assignee) || assignee < 0) {
set: (assignee) => {
if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError(
'Value must be a non negative integer',
'Value must be a user instance',
);
}
data.assignee = assignee;
......@@ -817,10 +818,10 @@
*/
assignee: {
get: () => data.assignee,
set: () => (assignee) => {
if (!Number.isInteger(assignee) || assignee < 0) {
set: (assignee) => {
if (assignee !== null && !(assignee instanceof User)) {
throw new ArgumentError(
'Value must be a non negative integer',
'Value must be a user instance',
);
}
data.assignee = assignee;
......@@ -957,11 +958,7 @@
}
}
if (typeof (data.id) === 'undefined') {
data.labels = [...labels];
} else {
data.labels = data.labels.concat([...labels]);
}
data.labels = [...labels];
},
},
/**
......@@ -1313,6 +1310,7 @@
if (typeof (this.id) !== 'undefined') {
// If the task has been already created, we update it
const taskData = {
assignee: this.assignee ? this.assignee.id : null,
name: this.name,
bug_tracker: this.bugTracker,
z_order: this.zOrder,
......
......@@ -129,7 +129,7 @@ describe('Feature: save a task', () => {
}],
});
result[0].labels = [newLabel];
result[0].labels = [...result[0].labels, newLabel];
result[0].save();
result = await window.cvat.tasks.get({
......
......@@ -28,6 +28,7 @@ module.exports = {
'@typescript-eslint/indent': ['warn', 4],
'@typescript-eslint/no-explicit-any': [0],
'no-restricted-syntax': [0, {'selector': 'ForOfStatement'}],
'no-plusplus': [0],
},
'settings': {
'import/resolver': {
......
此差异已折叠。
......@@ -17,6 +17,7 @@
"@typescript-eslint/eslint-plugin": "^1.13.0",
"babel": "^6.23.0",
"babel-loader": "^8.0.6",
"babel-plugin-import": "^1.12.2",
"css-loader": "^3.2.0",
"eslint-config-airbnb-typescript": "^4.0.1",
"eslint-plugin-import": "^2.18.2",
......@@ -26,7 +27,7 @@
"nodemon": "^1.19.2",
"style-loader": "^1.0.0",
"typescript": "^3.6.3",
"webpack": "^4.39.3",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.8",
"webpack-dev-server": "^3.8.0"
},
......@@ -37,7 +38,7 @@
"@types/react-redux": "^7.1.2",
"@types/react-router": "^5.0.5",
"@types/react-router-dom": "^5.1.0",
"antd": "^3.23.2",
"antd": "^3.24.2",
"dotenv-webpack": "^1.7.0",
"moment": "^2.24.0",
"prop-types": "^15.7.2",
......
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));
};
}
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));
}
};
}
......@@ -16,6 +16,9 @@ export enum TasksActionTypes {
DUMP_ANNOTATIONS = 'DUMP_ANNOTATIONS',
DUMP_ANNOTATIONS_SUCCESS = 'DUMP_ANNOTATIONS_SUCCESS',
DUMP_ANNOTATIONS_FAILED = 'DUMP_ANNOTATIONS_FAILED',
DELETE_TASK = 'DELETE_TASK',
DELETE_TASK_SUCCESS = 'DELETE_TASK_SUCCESS',
DELETE_TASK_FAILED = 'DELETE_TASK_FAILED',
}
function getTasks(): AnyAction {
......@@ -199,3 +202,52 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
dispatch(loadAnnotationsSuccess(task));
};
}
function deleteTask(taskID: number): AnyAction {
const action = {
type: TasksActionTypes.DELETE_TASK,
payload: {
taskID,
},
};
return action;
}
function deleteTaskSuccess(taskID: number): AnyAction {
const action = {
type: TasksActionTypes.DELETE_TASK_SUCCESS,
payload: {
taskID,
},
};
return action;
}
function deleteTaskFailed(taskID: number, error: any): AnyAction {
const action = {
type: TasksActionTypes.DELETE_TASK_FAILED,
payload: {
taskID,
error,
},
};
return action;
}
export function deleteTaskAsync(taskInstance: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
dispatch(deleteTask(taskInstance.id));
await taskInstance.delete();
} catch (error) {
dispatch(deleteTaskFailed(taskInstance.id, error));
return;
}
dispatch(deleteTaskSuccess(taskInstance.id));
};
}
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));
}
};
}
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>
);
}
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>
);
}
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>
);
}
\ No newline at end of file
......@@ -19,11 +19,16 @@ import HeaderContainer from '../containers/header/header';
type CVATAppProps = {
loadFormats: () => void;
loadUsers: () => void;
verifyAuthorized: () => void;
initPlugins: () => void;
pluginsInitialized: boolean;
userInitialized: boolean;
formatsInitialized: boolean;
usersInitialized: boolean;
gettingAuthError: string;
gettingFormatsError: string;
gettingUsersError: string;
user: any;
}
......@@ -33,22 +38,63 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
}
public componentDidMount() {
this.props.loadFormats();
this.props.verifyAuthorized();
}
public componentDidUpdate() {
if (!this.props.userInitialized) {
return;
}
if (this.props.gettingAuthError) {
Modal.error({
title: 'Could not check authorization',
content: `${this.props.gettingAuthError}`,
});
return;
}
if (!this.props.formatsInitialized) {
this.props.loadFormats();
return;
}
if (this.props.gettingFormatsError) {
Modal.error({
title: 'Could not receive annotations formats',
content: `${this.props.gettingFormatsError}`,
});
return;
}
if (!this.props.usersInitialized) {
this.props.loadUsers();
return;
}
if (this.props.gettingUsersError) {
Modal.error({
title: 'Could not receive users',
content: `${this.props.gettingUsersError}`,
});
return;
}
if (!this.props.pluginsInitialized) {
this.props.initPlugins();
return;
}
}
// Where you go depends on your URL
public render() {
if (this.props.userInitialized && this.props.formatsInitialized) {
const readyForRender = this.props.userInitialized
&& this.props.formatsInitialized
&& this.props.pluginsInitialized
&& this.props.usersInitialized;
if (readyForRender) {
if (this.props.user) {
return (
<BrowserRouter>
......@@ -59,8 +105,8 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
<Route exact path='/tasks' component={TasksPageContainer}/>
<Route exact path='/models' component={ModelsPageContainer}/>
<Route path='/tasks/create' component={CreateTaskPageContainer}/>
<Route path='/tasks/:number' component={TaskPageContainer}/>
<Route path='/tasks/:number/jobs/:number' component={AnnotationPageContainer}/>
<Route path='/tasks/:id' component={TaskPageContainer}/>
<Route path='/tasks/:id/jobs/:id' component={AnnotationPageContainer}/>
<Redirect to='/tasks'/>
</Switch>
</Layout.Content>
......
......@@ -14,15 +14,20 @@ import {
import Text from 'antd/lib/typography/Text';
import getCore from '../../core';
const core = getCore();
interface HeaderContainerProps {
onLogout: () => void;
installedAnalytics: boolean;
installedAutoAnnotation: boolean;
username: string;
logoutError: string;
}
function HeaderContainer(props: HeaderContainerProps & RouteComponentProps) {
const cvatLogo = () => (<img src='/assets/cvat-logo.svg'/>);
const backLogo = () => (<img src='/assets/icon-playcontrol-previous.svg'/>);
const userLogo = () => (<img src='/assets/icon-account.svg' />);
if (props.logoutError) {
......@@ -42,36 +47,45 @@ function HeaderContainer(props: HeaderContainerProps & RouteComponentProps) {
return (
<Layout.Header className='cvat-header'>
<div className='left-header'>
<div className='cvat-left-header'>
<Icon className='cvat-logo-icon' component={cvatLogo}/>
<Icon className='cvat-back-icon' component={backLogo}/>
<Radio.Group size='default' value={activeTab} className='cvat-header-buttons'>
<Radio.Button value='tasks'onChange={
() => props.history.push('/tasks')
}> Tasks </Radio.Button>
<Radio.Button value='models' onChange={
() => props.history.push('/models')
}> Models </Radio.Button>
<Button className='header-button' type='link' onClick={
() => window.open('/analytics/app/kibana', '_blank')
}> Analytics </Button>
{ props.installedAutoAnnotation ?
<Radio.Button value='models' onChange={
() => props.history.push('/models')
}> Models </Radio.Button> : null
}
{ props.installedAnalytics ?
<Button className='cvat-header-button' type='link' onClick={
() => {
const serverHost = core.config.backendAPI.slice(0, -7);
window.open(`${serverHost}/analytics/app/kibana`, '_blank');
}
}> Analytics </Button> : null
}
</Radio.Group>
</div>
<div className='right-header'>
<Button className='header-button' type='link' onClick={
<div className='cvat-right-header'>
<Button className='cvat-header-button' type='link' onClick={
() => window.open('https://github.com/opencv/cvat', '_blank')
}> <Icon type='github' /> GitHub </Button>
<Button className='header-button' type='link' onClick={
() => window.open('/documentation/user_guide.html', '_blank')
}> Help </Button>
<Button className='cvat-header-button' type='link' onClick={
() => {
const serverHost = core.config.backendAPI.slice(0, -7);
window.open(`${serverHost}/documentation/user_guide.html`, '_blank')
}
}> <Icon type='question-circle' /> Help </Button>
<Menu className='cvat-header-menu' subMenuCloseDelay={0.1} mode='horizontal'>
<Menu.SubMenu title={
<span>
<Icon className='cvat-header-user-icon' component={userLogo} />
<span>
<Text strong> {props.username} </Text>
<Icon className='cvat-header-menu-icon' component={backLogo} />
<Text strong>{props.username}</Text>
<Icon className='cvat-header-menu-icon' type='caret-down'/>
</span>
</span>
}>
......
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;
}
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>
);
}
}
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>
);
}
}
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>
);
}
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>
);
}
\ No newline at end of file
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
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>
);
}
}
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);
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>
);
}
}
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>
);
}
\ No newline at end of file
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);
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>
);
}
\ No newline at end of file
import React from 'react';
import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom';
import Text from 'antd/lib/typography/Text';
import {
......@@ -7,75 +9,38 @@ import {
Button,
Icon,
Progress,
Menu,
Dropdown,
Upload,
} from 'antd';
import { ClickParam } from 'antd/lib/menu/index';
import { UploadChangeParam } from 'antd/lib/upload';
import { RcFile } from 'antd/lib/upload';
import moment from 'moment';
import ActionsMenu from '../actions-menu/actions-menu';
export interface TaskItemProps {
installedTFAnnotation: boolean;
installedAutoAnnotation: boolean;
taskInstance: any;
previewImage: string;
dumpActivities: string[] | null;
loadActivity: string | null;
loaders: any[];
dumpers: any[];
deleted: boolean;
onDeleteTask: (taskInstance: any) => void;
onDumpAnnotation: (task: any, dumper: any) => void;
onLoadAnnotation: (task: any, loader: any, file: File) => 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 class TaskItemComponent extends React.PureComponent<TaskItemProps> {
constructor(props: TaskItemProps) {
class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteComponentProps> {
constructor(props: TaskItemProps & RouteComponentProps) {
super(props);
}
private handleMenuClick = (params: ClickParam) => {
const tracker = this.props.taskInstance.bugTracker;
if (params.keyPath.length === 2) {
// dump or upload
if (params.keyPath[1] === 'dump') {
}
} else {
switch (params.key) {
case 'tracker': {
window.open(`${tracker}`, '_blank')
return;
} case 'auto': {
return;
} case 'tf': {
return;
} case 'update': {
return;
} case 'delete': {
return;
} default: {
return;
}
}
}
}
private renderPreview() {
return (
<Col span={4}>
<div className='cvat-task-preview-wrapper'>
<img alt='Preview' className='cvat-task-preview' src={this.props.previewImage}/>
<div className='cvat-task-item-preview-wrapper'>
<img alt='Preview' className='cvat-task-item-preview' src={this.props.previewImage}/>
</div>
</Col>
)
......@@ -94,7 +59,7 @@ export default class TaskItemComponent extends React.PureComponent<TaskItemProps
return (
<Col span={10}>
<Text strong> {id} {name} </Text> <br/>
<Text strong>{`${id} ${name}`}</Text> <br/>
{ owner ?
<>
<Text type='secondary'>
......@@ -134,7 +99,7 @@ export default class TaskItemComponent extends React.PureComponent<TaskItemProps
}
</Col>
<Col>
<Text type='secondary'> {numOfCompleted} of {numOfJobs} jobs </Text>
<Text type='secondary'>{`${numOfCompleted} of ${numOfJobs} jobs`}</Text>
</Col>
</Row>
<Row>
......@@ -151,98 +116,36 @@ export default class TaskItemComponent extends React.PureComponent<TaskItemProps
)
}
private renderDumperItem(dumper: any) {
const task = this.props.taskInstance;
const { mode } = task;
const dumpingWithThisDumper = (this.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={() => {
this.props.onDumpAnnotation(task, dumper);
}}>
<Icon type='download'/>
<Text strong={isDefaultFormat(dumper.name, mode)}>
{dumper.name}
</Text>
{pending ? <Icon type='loading'/> : null}
</Button>
</Menu.Item>
);
}
private renderLoaderItem(loader: any) {
const loadingWithThisLoader = this.props.loadActivity
&& this.props.loadActivity === loader.name
? this.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) => {
this.props.onLoadAnnotation(
this.props.taskInstance,
loader,
file as File,
);
return false;
}}>
<Button block={true} type='link' disabled={!!this.props.loadActivity}>
<Icon type='upload'/>
<Text> {loader.name} </Text>
{pending ? <Icon type='loading'/> : null}
</Button>
</Upload>
</Menu.Item>
);
}
private renderMenu() {
const tracker = this.props.taskInstance.bugTracker;
return (
<Menu subMenuCloseDelay={0.15} className='cvat-task-item-menu' onClick={this.handleMenuClick}>
<Menu.SubMenu key='dump' title='Dump annotations'>
{this.props.dumpers.map((dumper) => this.renderDumperItem(dumper))}
</Menu.SubMenu>
<Menu.SubMenu key='load' title='Upload annotations'>
{this.props.loaders.map((loader) => this.renderLoaderItem(loader))}
</Menu.SubMenu>
{tracker ? <Menu.Item key='tracker'>Open bug tracker</Menu.Item> : null}
<Menu.Item key='auto'>Run auto annotation</Menu.Item>
<Menu.Item key='tf'>Run TF annotation</Menu.Item>
<hr/>
<Menu.Item key='update'>Update</Menu.Item>
<Menu.Item key='delete'>Delete</Menu.Item>
</Menu>
);
}
private renderNavigation() {
const subMenuIcon = () => (<img src='/assets/icon-sub-menu.svg'/>);
const { id } = this.props.taskInstance;
return (
<Col span={4}>
<Row type='flex' justify='end'>
<Col>
<Button type='primary' size='large' ghost> Open </Button>
<Button type='primary' size='large' ghost onClick={
() => this.props.history.push(`/tasks/${id}`)
}> Open </Button>
</Col>
</Row>
<Row type='flex' justify='end'>
<Col>
<Text style={{color: 'black'}}> Actions </Text>
<Dropdown overlay={this.renderMenu()}>
<Text className='cvat-black-color'> Actions </Text>
<Dropdown overlay={
ActionsMenu({
taskInstance: this.props.taskInstance,
loaders: this.props.loaders,
dumpers: this.props.dumpers,
loadActivity: this.props.loadActivity,
dumpActivities: this.props.dumpActivities,
installedTFAnnotation: this.props.installedTFAnnotation,
installedAutoAnnotation: this.props.installedAutoAnnotation,
onLoadAnnotation: this.props.onLoadAnnotation,
onDumpAnnotation: this.props.onDumpAnnotation,
onDeleteTask: this.props.onDeleteTask,
})
}>
<Icon className='cvat-task-item-menu-icon' component={subMenuIcon}/>
</Dropdown>
</Col>
......@@ -252,8 +155,14 @@ export default class TaskItemComponent extends React.PureComponent<TaskItemProps
}
public render() {
const style = {};
if (this.props.deleted) {
(style as any).pointerEvents = 'none';
(style as any).opacity = 0.5;
}
return (
<Row className='cvat-tasks-list-item' type='flex' justify='center' align='top'>
<Row className='cvat-tasks-list-item' type='flex' justify='center' align='top' style={{...style}}>
{this.renderPreview()}
{this.renderDescription()}
{this.renderProgress()}
......@@ -262,3 +171,5 @@ export default class TaskItemComponent extends React.PureComponent<TaskItemProps
)
};
}
export default withRouter(TaskItemComponent);
......@@ -16,6 +16,7 @@ import EmptyListComponent from './empty-list';
import TaskListContainer from '../../containers/tasks-page/tasks-list';
interface TasksPageProps {
deletingError: string;
dumpingError: string;
loadingError: string;
tasksFetchingError: string;
......@@ -142,21 +143,28 @@ class TasksPageComponent extends React.PureComponent<TasksPageProps & RouteCompo
Modal.error({
title: 'Could not dump annotations',
content: this.props.dumpingError,
});;
});
}
if (this.props.loadingError) {
Modal.error({
title: 'Could not load annotations',
content: this.props.loadingError,
});;
});
}
if (this.props.deletingError) {
Modal.error({
title: 'Could not delete the task',
content: this.props.deletingError,
});
}
if (this.props.loadingDoneMessage) {
Modal.info({
title: 'Successful loading of annotations',
content: this.props.loadingDoneMessage,
});;
});
}
}
......
import React from 'react';
import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom';
import {
Col,
Row,
......@@ -13,7 +16,7 @@ interface VisibleTopBarProps {
searchValue: string;
}
export default class TopBarComponent extends React.PureComponent<VisibleTopBarProps> {
class TopBarComponent extends React.PureComponent<VisibleTopBarProps & RouteComponentProps> {
public render() {
return (
<>
......@@ -37,11 +40,13 @@ export default class TopBarComponent extends React.PureComponent<VisibleTopBarPr
xl={{span: 8}}
xxl={{span: 7}}>
<Button size='large' id='cvat-create-task-button' type='primary' onClick={
() => window.open('/tasks/create', '_blank')
() => this.props.history.push('/tasks/create')
}> Create new task </Button>
</Col>
</Row>
</>
)
}
}
\ No newline at end of file
}
export default withRouter(TopBarComponent);
\ No newline at end of file
......@@ -3,10 +3,13 @@ import { connect } from 'react-redux';
import { logoutAsync } from '../../actions/auth-actions';
import { CombinedState } from '../../reducers/root-reducer';
import { SupportedPlugins } from '../../reducers/interfaces';
import HeaderComponent from '../../components/header/header';
interface StateToProps {
installedAnalytics: boolean;
installedAutoAnnotation: boolean;
username: string;
logoutError: any;
}
......@@ -16,9 +19,13 @@ interface DispatchToProps {
}
function mapStateToProps(state: CombinedState): StateToProps {
const { auth } = state;
const { plugins } = state.plugins;
return {
username: state.auth.user.username,
logoutError: state.auth.logoutError,
installedAnalytics: plugins[SupportedPlugins.ANALYTICS],
installedAutoAnnotation: plugins[SupportedPlugins.AUTO_ANNOTATION],
username: auth.user.username,
logoutError: auth.logoutError,
};
}
......@@ -31,6 +38,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
function HeaderContainer(props: StateToProps & DispatchToProps) {
return (
<HeaderComponent
installedAnalytics={props.installedAnalytics}
installedAutoAnnotation={props.installedAutoAnnotation}
onLogout={props.logout}
username={props.username}
logoutError={props.logoutError ? props.logoutError.toString() : ''}
......
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);
\ No newline at end of file
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);
\ No newline at end of file
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);
\ No newline at end of file
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);
\ No newline at end of file
......@@ -3,6 +3,7 @@ import { connect } from 'react-redux';
import {
TasksQuery,
SupportedPlugins,
} from '../../reducers/interfaces';
import {
......@@ -15,11 +16,15 @@ import {
getTasksAsync,
dumpAnnotationsAsync,
loadAnnotationsAsync,
deleteTaskAsync,
} from '../../actions/tasks-actions';
interface StateToProps {
installedTFAnnotation: boolean;
installedAutoAnnotation: boolean;
dumpActivities: string[] | null;
loadActivity: string | null;
deleteActivity: boolean | null;
previewImage: string;
taskInstance: any;
loaders: any[];
......@@ -28,6 +33,7 @@ interface StateToProps {
interface DispatchToProps {
getTasks: (query: TasksQuery) => void;
delete: (taskInstance: any) => void;
dump: (task: any, format: string) => void;
load: (task: any, format: string, file: File) => void;
}
......@@ -42,10 +48,16 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const { formats } = state;
const { dumps } = state.tasks.activities;
const { loads } = state.tasks.activities;
const { deletes } = state.tasks.activities;
const { plugins } = state.plugins;
const id = own.taskID;
return {
dumpActivities: dumps.byTask[own.taskID] ? dumps.byTask[own.taskID] : null,
loadActivity: loads.byTask[own.taskID] ? loads.byTask[own.taskID] : null,
installedTFAnnotation: plugins.TF_ANNOTATION,
installedAutoAnnotation: plugins.AUTO_ANNOTATION,
dumpActivities: dumps.byTask[id] ? dumps.byTask[id] : null,
loadActivity: loads.byTask[id] ? loads.byTask[id] : null,
deleteActivity: deletes.byTask[id] ? deletes.byTask[id] : null,
previewImage: task.preview,
taskInstance: task.instance,
loaders: formats.loaders,
......@@ -64,6 +76,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
load: (task: any, loader: any, file: File): void => {
dispatch(loadAnnotationsAsync(task, loader, file));
},
delete: (taskInstance: any): void => {
dispatch(deleteTaskAsync(taskInstance));
},
}
}
......@@ -72,12 +87,16 @@ type TasksItemContainerProps = StateToProps & DispatchToProps & OwnProps;
function TaskItemContainer(props: TasksItemContainerProps) {
return (
<TaskItemComponent
installedTFAnnotation={props.installedTFAnnotation}
installedAutoAnnotation={props.installedAutoAnnotation}
deleted={props.deleteActivity === true}
taskInstance={props.taskInstance}
previewImage={props.previewImage}
dumpActivities={props.dumpActivities}
loadActivity={props.loadActivity}
loaders={props.loaders}
dumpers={props.dumpers}
onDeleteTask={props.delete}
onLoadAnnotation={props.load}
onDumpAnnotation={props.dump}
/>
......
......@@ -11,6 +11,7 @@ import TasksPageComponent from '../../components/tasks-page/tasks-page';
import { getTasksAsync } from '../../actions/tasks-actions';
interface StateToProps {
deletingError: any;
dumpingError: any;
loadingError: any;
tasksFetchingError: any;
......@@ -30,8 +31,10 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { activities } = tasks;
const { dumps } = activities;
const { loads } = activities;
const { deletes } = activities;
return {
deletingError: deletes.deletingError,
dumpingError: dumps.dumpingError,
loadingError: loads.loadingError,
tasksFetchingError: tasks.tasksFetchingError,
......@@ -54,6 +57,7 @@ type TasksPageContainerProps = StateToProps & DispatchToProps;
function TasksPageContainer(props: TasksPageContainerProps) {
return (
<TasksPageComponent
deletingError={props.deletingError ? props.deletingError.toString() : ''}
dumpingError={props.dumpingError ? props.dumpingError.toString() : ''}
loadingError={props.loadingError ? props.loadingError.toString() : ''}
tasksFetchingError={props.tasksFetchingError ? props.tasksFetchingError.toString(): ''}
......
......@@ -8,7 +8,7 @@
</head>
<body>
<div id="root" style="width: 100%; height: 100%; display: grid;">
<div id="root" style="width: 100%; height: initial; min-height: 100%; display: grid;">
</div>
......
......@@ -7,53 +7,72 @@ import createCVATStore from './store';
import { authorizedAsync } from './actions/auth-actions';
import { gettingFormatsAsync } from './actions/formats-actions';
import { checkPluginsAsync } from './actions/plugins-actions';
import { getUsersAsync } from './actions/users-actions';
import { CombinedState } from './reducers/root-reducer';
const cvatStore = createCVATStore();
interface StateToProps {
pluginsInitialized: boolean;
userInitialized: boolean;
usersInitialized: boolean;
formatsInitialized: boolean;
gettingAuthError: any;
gettingFormatsError: any;
gettingUsersError: any;
user: any;
}
interface DispatchToProps {
loadFormats: () => void;
verifyAuthorized: () => void;
loadUsers: () => void;
initPlugins: () => void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const { plugins } = state;
const { auth } = state;
const { formats } = state;
const { users } = state;
return {
pluginsInitialized: plugins.initialized,
userInitialized: auth.initialized,
usersInitialized: users.initialized,
formatsInitialized: formats.initialized,
gettingAuthError: auth.authError,
user: auth.user,
gettingUsersError: users.gettingUsersError,
gettingFormatsError: formats.gettingFormatsError,
user: auth.user,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
loadFormats: (): void => dispatch(gettingFormatsAsync()),
verifyAuthorized: (): void => dispatch(authorizedAsync())
verifyAuthorized: (): void => dispatch(authorizedAsync()),
initPlugins: (): void => dispatch(checkPluginsAsync()),
loadUsers: (): void => dispatch(getUsersAsync()),
};
}
function reduxAppWrapper(props: StateToProps & DispatchToProps) {
return (
<CVATApplication
initPlugins={props.initPlugins}
loadFormats={props.loadFormats}
loadUsers={props.loadUsers}
verifyAuthorized={props.verifyAuthorized}
pluginsInitialized={props.pluginsInitialized}
userInitialized={props.userInitialized}
usersInitialized={props.usersInitialized}
formatsInitialized={props.formatsInitialized}
gettingAuthError={props.gettingAuthError ? props.gettingAuthError.toString() : ''}
gettingFormatsError={props.gettingFormatsError ? props.gettingFormatsError.toString() : ''}
gettingUsersError={props.gettingUsersError ? props.gettingUsersError.toString() : ''}
user={props.user}
/>
)
......
......@@ -46,6 +46,12 @@ export interface TasksState {
[tid: number]: string; // loader name
};
};
deletes: {
deletingError: any;
byTask: {
[tid: number]: boolean; // deleted (deleting if in dictionary)
};
};
};
}
......@@ -55,3 +61,30 @@ export interface FormatsState {
initialized: boolean;
gettingFormatsError: any;
}
// eslint-disable-next-line import/prefer-default-export
export enum SupportedPlugins {
GIT_INTEGRATION = 'GIT_INTEGRATION',
AUTO_ANNOTATION = 'AUTO_ANNOTATION',
TF_ANNOTATION = 'TF_ANNOTATION',
ANALYTICS = 'ANALYTICS',
}
export interface PluginsState {
initialized: boolean;
plugins: {
[name in SupportedPlugins]: boolean;
};
}
export interface TaskState {
task: Task | null;
taskFetchingError: any;
taskUpdatingError: any;
}
export interface UsersState {
users: any[];
initialized: boolean;
gettingUsersError: any;
}
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 };
}
}
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,
});
}
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 };
}
}
......@@ -28,6 +28,10 @@ const defaultState: TasksState = {
loadingDoneMessage: '',
byTask: {},
},
deletes: {
deletingError: null,
byTask: {},
},
},
};
......@@ -57,6 +61,13 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks
case TasksActionTypes.GET_TASKS:
return {
...state,
activities: {
...state.activities,
deletes: {
deletingError: null,
byTask: {},
},
},
initialized: false,
};
case TasksActionTypes.GET_TASKS_SUCCESS: {
......@@ -214,6 +225,64 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks
},
};
}
case TasksActionTypes.DELETE_TASK: {
const { taskID } = action.payload;
const deletesActivities = state.activities.deletes;
const activities = { ...state.activities };
activities.deletes = { ...activities.deletes };
activities.deletes.byTask[taskID] = false;
return {
...state,
activities: {
...state.activities,
deletes: deletesActivities,
},
};
}
case TasksActionTypes.DELETE_TASK_SUCCESS: {
const { taskID } = action.payload;
const deletesActivities = state.activities.deletes;
const activities = { ...state.activities };
activities.deletes = { ...activities.deletes };
activities.deletes.byTask[taskID] = true;
return {
...state,
activities: {
...state.activities,
deletes: deletesActivities,
},
};
}
case TasksActionTypes.DELETE_TASK_FAILED: {
const { taskID } = action.payload;
const { error } = action.payload;
const deletesActivities = state.activities.deletes;
const activities = { ...state.activities };
activities.deletes = { ...activities.deletes };
delete activities.deletes.byTask[taskID];
return {
...state,
activities: {
...state.activities,
deletes: {
...deletesActivities,
deletingError: error,
},
},
};
}
default:
return state;
}
......
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,
};
}
}
此差异已折叠。
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;
......@@ -35,6 +35,26 @@ const validationPatterns = {
pattern: /^[a-zA-Z]{2,}(([',. -][a-zA-Z ])?[a-zA-Z]*)*$/,
message: 'Invalid name',
},
validateAttributeName: {
pattern: /\S+/,
message: 'Invalid name',
},
validateLabelName: {
pattern: /\S+/,
message: 'Invalid name',
},
validateAttributeValue: {
pattern: /\S+/,
message: 'Invalid attribute value',
},
validateURL: {
pattern: /^(https?):\/\/[^\s$.?#].[^\s]*$/,
message: 'URL is not valid',
},
};
export default { ...validationPatterns };
......@@ -34,7 +34,9 @@ module.exports = {
use: {
loader: 'babel-loader',
options: {
plugins: ['@babel/plugin-proposal-class-properties'],
plugins: ['@babel/plugin-proposal-class-properties', ['import', {
'libraryName': 'antd',
}]],
presets: [
['@babel/preset-env', {
targets: {
......@@ -48,7 +50,7 @@ module.exports = {
},
},
}, {
test: /\.(css|scss)$/,
test: /\.(css|sass)$/,
use: ['style-loader', 'css-loader']
}],
},
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册