[NEW-UI]: Create and update task (#612)

- Create task
 - Update task
 - Upload annotations
 - Download annotations
main
Artyom Zankevich 7 years ago committed by Boris Sekachev
parent 935d380d36
commit b7609cae91

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -21,7 +21,7 @@
-->
<title>CVAT</title>
<script src="./cvat.min.js"></script>
<script src="./cvat-core.min.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

@ -0,0 +1,72 @@
export const dumpAnnotation = () => (dispatch: any) => {
dispatch({
type: 'DUMP_ANNOTATION',
});
}
export const dumpAnnotationSuccess = (downloadLink: string) => (dispatch: any) => {
dispatch({
type: 'DUMP_ANNOTATION_SUCCESS',
payload: downloadLink,
});
}
export const dumpAnnotationError = (error = {}) => (dispatch: any) => {
dispatch({
type: 'DUMP_ANNOTATION_ERROR',
payload: error,
});
}
export const uploadAnnotation = () => (dispatch: any) => {
dispatch({
type: 'UPLOAD_ANNOTATION',
});
}
export const uploadAnnotationSuccess = () => (dispatch: any) => {
dispatch({
type: 'UPLOAD_ANNOTATION_SUCCESS',
});
}
export const uploadAnnotationError = (error = {}) => (dispatch: any) => {
dispatch({
type: 'UPLOAD_ANNOTATION_ERROR',
payload: error,
});
}
export const dumpAnnotationAsync = (task: any, dumper: any) => {
return (dispatch: any) => {
dispatch(dumpAnnotation());
return task.annotations.dump(task.name, dumper).then(
(downloadLink: string) => {
dispatch(dumpAnnotationSuccess(downloadLink));
},
(error: any) => {
dispatch(dumpAnnotationError(error));
throw error;
},
);
};
}
export const uploadAnnotationAsync = (task: any, file: File, loader: any) => {
return (dispatch: any) => {
dispatch(uploadAnnotation());
return task.annotations.upload(file, loader).then(
(response: any) => {
dispatch(uploadAnnotationSuccess());
},
(error: any) => {
dispatch(uploadAnnotationError(error));
throw error;
},
);
};
}

@ -29,6 +29,8 @@ export const loginAsync = (username: string, password: string, history: any) =>
},
(error: any) => {
dispatch(loginError(error));
throw error;
},
);
};

@ -0,0 +1,106 @@
export const getServerInfo = () => (dispatch: any) => {
dispatch({
type: 'GET_SERVER_INFO',
});
}
export const getServerInfoSuccess = (information: null) => (dispatch: any) => {
dispatch({
type: 'GET_SERVER_INFO_SUCCESS',
payload: information,
});
}
export const getServerInfoError = (error = {}) => (dispatch: any) => {
dispatch({
type: 'GET_SERVER_INFO_ERROR',
payload: error,
});
}
export const getShareFiles = () => (dispatch: any) => {
dispatch({
type: 'GET_SHARE_FILES',
});
}
export const getShareFilesSuccess = (files: []) => (dispatch: any) => {
dispatch({
type: 'GET_SHARE_FILES_SUCCESS',
payload: files,
});
}
export const getShareFilesError = (error = {}) => (dispatch: any) => {
dispatch({
type: 'GET_SHARE_FILES_ERROR',
payload: error,
});
}
export const getAnnotationFormats = () => (dispatch: any) => {
dispatch({
type: 'GET_ANNOTATION_FORMATS',
});
}
export const getAnnotationFormatsSuccess = (annotationFormats: []) => (dispatch: any) => {
dispatch({
type: 'GET_ANNOTATION_FORMATS_SUCCESS',
payload: annotationFormats,
});
}
export const getAnnotationFormatsError = (error = {}) => (dispatch: any) => {
dispatch({
type: 'GET_ANNOTATION_FORMATS_ERROR',
payload: error,
});
}
export const getServerInfoAsync = () => {
return (dispatch: any) => {
dispatch(getServerInfo());
return (window as any).cvat.server.about().then(
(information: any) => {
dispatch(getServerInfoSuccess(information));
},
(error: any) => {
dispatch(getServerInfoError(error));
},
);
};
}
export const getShareFilesAsync = (directory: string) => {
return (dispatch: any) => {
dispatch(getShareFiles());
return (window as any).cvat.server.share(directory).then(
(files: any) => {
dispatch(getShareFilesSuccess(files));
},
(error: any) => {
dispatch(getShareFilesError(error));
},
);
};
}
export const getAnnotationFormatsAsync = () => {
return (dispatch: any) => {
dispatch(getAnnotationFormats());
return (window as any).cvat.server.formats().then(
(formats: any) => {
dispatch(getAnnotationFormatsSuccess(formats));
},
(error: any) => {
dispatch(getAnnotationFormatsError(error));
throw error;
},
);
};
}

@ -1,41 +1,79 @@
import queryString from 'query-string';
import setQueryObject from '../utils/tasks-filter-dto'
import setQueryObject from '../utils/tasks-filter'
export const getTasks = () => (dispatch: any, getState: any) => {
export const getTasks = () => (dispatch: any) => {
dispatch({
type: 'GET_TASKS',
});
}
export const getTasksSuccess = (tasks: []) => (dispatch: any, getState: any) => {
export const getTasksSuccess = (tasks: []) => (dispatch: any) => {
dispatch({
type: 'GET_TASKS_SUCCESS',
payload: tasks,
});
}
export const getTasksError = (error: {}) => (dispatch: any, getState: any) => {
export const getTasksError = (error: {}) => (dispatch: any) => {
dispatch({
type: 'GET_TASKS_ERROR',
payload: error,
});
}
export const deleteTask = () => (dispatch: any, getState: any) => {
export const createTask = () => (dispatch: any) => {
dispatch({
type: 'CREATE_TASK',
});
}
export const createTaskSuccess = () => (dispatch: any) => {
dispatch({
type: 'CREATE_TASK_SUCCESS',
});
}
export const createTaskError = (error: {}) => (dispatch: any) => {
dispatch({
type: 'CREATE_TASK_ERROR',
payload: error,
});
}
export const updateTask = () => (dispatch: any) => {
dispatch({
type: 'UPDATE_TASK',
});
}
export const updateTaskSuccess = () => (dispatch: any) => {
dispatch({
type: 'UPDATE_TASK_SUCCESS',
});
}
export const updateTaskError = (error: {}) => (dispatch: any) => {
dispatch({
type: 'UPDATE_TASK_ERROR',
payload: error,
});
}
export const deleteTask = () => (dispatch: any) => {
dispatch({
type: 'DELETE_TASK',
});
}
export const deleteTaskSuccess = () => (dispatch: any, getState: any) => {
export const deleteTaskSuccess = () => (dispatch: any) => {
dispatch({
type: 'DELETE_TASK_SUCCESS',
});
}
export const deleteTaskError = (error: {}) => (dispatch: any, getState: any) => {
export const deleteTaskError = (error: {}) => (dispatch: any) => {
dispatch({
type: 'DELETE_TASK_ERROR',
payload: error,
@ -52,6 +90,46 @@ export const getTasksAsync = (queryObject = {}) => {
},
(error: any) => {
dispatch(getTasksError(error));
throw error;
},
);
};
}
export const createTaskAsync = (task: any) => {
return (dispatch: any) => {
dispatch(createTask());
return task.save().then(
(created: any) => {
dispatch(createTaskSuccess());
return dispatch(getTasksAsync());
},
(error: any) => {
dispatch(createTaskError(error));
throw error;
},
);
};
}
export const updateTaskAsync = (task: any) => {
return (dispatch: any) => {
dispatch(updateTask());
return task.save().then(
(updated: any) => {
dispatch(updateTaskSuccess());
return dispatch(getTasksAsync());
},
(error: any) => {
dispatch(updateTaskError(error));
throw error;
},
);
};
@ -77,14 +155,17 @@ export const deleteTaskAsync = (task: any, history: any) => {
history.push({ search: queryString.stringify(queryObject) });
} else if (state.tasks.tasksCount === 1) {
dispatch(getTasksAsync());
return dispatch(getTasksAsync());
} else {
const query = setQueryObject(queryObject);
dispatch(getTasksAsync(query));
return dispatch(getTasksAsync(query));
}
},
(error: any) => {
dispatch(deleteTaskError(error));
throw error;
},
);
};

@ -3,28 +3,68 @@ import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { deleteTaskAsync } from '../../../actions/tasks.actions';
import { createTaskAsync, updateTaskAsync, deleteTaskAsync } from '../../../actions/tasks.actions';
import { getAnnotationFormatsAsync } from '../../../actions/server.actions';
import { dumpAnnotationAsync, uploadAnnotationAsync } from '../../../actions/annotations.actions';
import { Layout, Empty, Button, Modal, Col, Row } from 'antd';
import { Layout, Empty, Button, Modal, Col, Row, Menu, Dropdown, Icon, Upload } from 'antd';
import Title from 'antd/lib/typography/Title';
import { ClickParam } from 'antd/lib/menu';
import { UploadChangeParam } from 'antd/lib/upload';
import TaskUpdateForm from '../../modals/task-update/task-update';
import TaskCreateForm from '../../modals/task-create/task-create';
import { deserializeLabels, taskDTO } from '../../../utils/tasks-dto';
import './dashboard-content.scss';
const { Content } = Layout;
const { confirm } = Modal;
class DashboardContent extends Component<any, any> {
hostUrl: string | undefined;
apiUrl: string | undefined;
createFormRef: any;
updateFormRef: any;
constructor(props: any) {
super(props);
this.state = {};
this.state = {
dumpers: [],
loaders: [],
selectedLoader: null,
activeTaskId: null,
};
this.hostUrl = process.env.REACT_APP_API_HOST_URL;
this.apiUrl = process.env.REACT_APP_API_FULL_URL;
}
componentDidMount() {
this.props.dispatch(getAnnotationFormatsAsync()).then(
(formats: any) => {
const dumpers = [];
const loaders = [];
for (const format of this.props.annotationFormats) {
for (const dumper of format.dumpers) {
dumpers.push(dumper);
}
for (const loader of format.loaders) {
loaders.push(loader);
}
}
this.setState({ dumpers, loaders });
}
);
}
render() {
return(
<>
@ -36,7 +76,7 @@ class DashboardContent extends Component<any, any> {
private renderPlaceholder() {
return (
<Empty className="empty" description="No tasks found...">
<Button type="primary" onClick={ this.createTask }>
<Button type="primary" onClick={ this.onCreateTask }>
Create task
</Button>
</Empty>
@ -44,7 +84,7 @@ class DashboardContent extends Component<any, any> {
}
private renderTasks() {
return(
return (
<Content className="dashboard-content">
{
this.props.tasks.map(
@ -63,17 +103,27 @@ class DashboardContent extends Component<any, any> {
<Col className="card-actions" span={8}>
<Row type="flex">
<Button type="primary" onClick={ this.onDumpAnnotation }>
Dump annotation
</Button>
<Dropdown
disabled={ this.props.isFetching && task.id === this.state.activeTaskId }
trigger={['click']}
overlay={ this.dumpAnnotationMenu(task) }>
<Button type="primary">
Dump annotation <Icon type="down" />
</Button>
</Dropdown>
</Row>
<Row type="flex">
<Button type="primary" onClick={ this.onUploadAnnotation }>
Upload annotation
</Button>
<Dropdown
disabled={ this.props.isFetching && task.id === this.state.activeTaskId }
trigger={['click']}
overlay={ this.uploadAnnotationMenu(task) }>
<Button type="primary">
Upload annotation <Icon type="down" />
</Button>
</Dropdown>
</Row>
<Row type="flex">
<Button type="primary" onClick={ this.onUpdateTask }>
<Button type="primary" onClick={ () => this.onUpdateTask(task) }>
Update task
</Button>
</Row>
@ -105,43 +155,201 @@ class DashboardContent extends Component<any, any> {
);
}
private createTask = () => {
console.log('Create task');
private dumpAnnotationMenu = (task: any) => {
return (
<Menu onClick={ (params: ClickParam) => this.onDumpAnnotation(task, params, this) }>
{
this.state.dumpers.map(
(dumper: any) => (
<Menu.Item key={ dumper.name }>
{ dumper.name }
</Menu.Item>
)
)
}
</Menu>
);
}
private uploadAnnotationMenu = (task: any) => {
return (
<Menu onClick={ (params: ClickParam) => this.setState({ selectedLoader: params.key, loaderTask: task }) }>
{
this.state.loaders.map(
(loader: any) => (
<Menu.Item key={ loader.name }>
<Upload
accept={ `.${loader.format}` }
showUploadList={ false }
customRequest={ this.simulateRequest }
onChange={ this.onUploaderChange }>
<Button type="link">
<Icon type="upload" />
{ loader.name }
</Button>
</Upload>
</Menu.Item>
),
)
}
</Menu>
);
}
private setTaskCreateFormRef = (ref: any) => {
this.createFormRef = ref;
}
private setTaskUpdateFormRef = (ref: any) => {
this.updateFormRef = ref;
}
private onCreateTask = () => {
Modal.confirm({
title: 'Create new task',
content: <TaskCreateForm ref={ this.setTaskCreateFormRef } />,
centered: true,
className: 'crud-modal',
okText: 'Create',
okType: 'primary',
onOk: () => {
return new Promise((resolve, reject) => {
this.createFormRef.validateFields((error: any, values: any) => {
if (!error) {
const newTask = taskDTO(values);
this.props.dispatch(createTaskAsync(newTask)).then(
(data: any) => {
resolve(data);
},
(error: any) => {
reject(error);
Modal.error({ title: error.message, centered: true, okType: 'danger' });
},
);
} else {
reject(error);
}
});
});
},
onCancel: () => {
return;
},
});
}
private onUpdateTask = (task: any) => {
console.log('Update task');
Modal.confirm({
title: 'Update task',
content: <TaskUpdateForm task={ task } ref={ this.setTaskUpdateFormRef } />,
centered: true,
className: 'crud-modal',
okText: 'Update',
okType: 'primary',
onOk: () => {
return new Promise((resolve, reject) => {
this.updateFormRef.validateFields((error: any, values: any) => {
if (!error) {
const deserializedLabels = deserializeLabels(values.newLabels);
const newLabels = deserializedLabels.map(label => new (window as any).cvat.classes.Label(label));
task.labels = newLabels;
this.props.dispatch(updateTaskAsync(task)).then(
(data: any) => {
resolve(data);
},
(error: any) => {
reject(error);
Modal.error({ title: error.message, centered: true, okType: 'danger' });
},
);
} else {
reject(error);
}
});
});
},
onCancel: () => {
return;
},
});
}
private onDeleteTask = (task: any) => {
const self = this;
confirm({
Modal.confirm({
title: 'Do you want to delete this task?',
okText: 'Yes',
okType: 'danger',
centered: true,
onOk() {
return self.props.dispatch(deleteTaskAsync(task, self.props.history));
autoFocusButton: 'cancel',
onOk: () => {
return new Promise((resolve, reject) => {
this.props.dispatch(deleteTaskAsync(task, this.props.history)).then(
(deleted: any) => {
resolve(deleted);
},
(error: any) => {
reject(error);
},
);
});
},
cancelText: 'No',
onCancel() {
onCancel: () => {
return;
},
});
}
private onDumpAnnotation = () => {
console.log('Dump annotatio');
private onDumpAnnotation = (task: any, event: any, component: DashboardContent) => {
const dumper = component.state.dumpers.find((dumper: any) => dumper.name === event.key);
component.setState({ activeTaskId: task.id });
this.props.dispatch(dumpAnnotationAsync(task, dumper)).then(
(data: any) => {
const a = document.createElement('a');
a.href = component.props.downloadLink;
document.body.appendChild(a);
a.click();
a.remove();
},
(error: any) => {
Modal.error({ title: error.message, centered: true, okType: 'danger' });
},
);
}
private onUploadAnnotation = (task: any, file: File) => {
const loader = this.state.loaders.find((loader: any) => loader.name === this.state.selectedLoader);
this.setState({ activeTaskId: task.id });
this.props.dispatch(uploadAnnotationAsync(task, file, loader)).then(
(data: any) => {
},
(error: any) => {
Modal.error({ title: error.message, centered: true, okType: 'danger' });
},
);
return true;
}
private onUploaderChange = (info: UploadChangeParam) => {
if (info.file.status === 'uploading') {
this.onUploadAnnotation(this.state.loaderTask, (info.file.originFileObj as File));
}
}
private onUploadAnnotation = () => {
console.log('Upload annotation');
private simulateRequest = ({ file, onSuccess }: any) => {
setTimeout(() => {
onSuccess(file);
}, 0);
}
}
const mapStateToProps = (state: any) => {
return state.tasks;
return { ...state.tasks, ...state.server, ...state.annotations };
};
export default withRouter(connect(mapStateToProps)(DashboardContent) as any);

@ -3,7 +3,7 @@ import { Location, Action } from 'history';
import * as queryString from 'query-string';
import setQueryObject from '../../utils/tasks-filter-dto'
import setQueryObject from '../../utils/tasks-filter'
import { connect } from 'react-redux';
import { getTasksAsync } from '../../actions/tasks.actions';

@ -3,18 +3,26 @@ import React, { Component } from 'react';
import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { createTaskAsync } from '../../../actions/tasks.actions';
import { Layout, Row, Col, Button, Input } from 'antd';
import { Modal, Layout, Row, Col, Button, Input } from 'antd';
import Title from 'antd/lib/typography/Title';
import TaskCreateForm from '../../modals/task-create/task-create';
import { taskDTO } from '../../../utils/tasks-dto';
import './dashboard-header.scss';
const { Header } = Layout;
const { Search } = Input;
class DashboardHeader extends Component<any, any> {
hostUrl: string | undefined;
createFormRef: any;
constructor(props: any) {
super(props);
@ -50,7 +58,7 @@ class DashboardHeader extends Component<any, any> {
<Button
className="action"
type="primary"
onClick={ this.createTask }>
onClick={ this.onCreateTask }>
Create task
</Button>
<Button
@ -66,8 +74,44 @@ class DashboardHeader extends Component<any, any> {
);
}
private createTask = () => {
console.log('Create task');
private setTaskCreateFormRef = (ref: any) => {
this.createFormRef = ref;
}
private onCreateTask = () => {
Modal.confirm({
title: 'Create new task',
content: <TaskCreateForm ref={ this.setTaskCreateFormRef }/>,
centered: true,
className: 'crud-modal',
okText: 'Create',
okType: 'primary',
onOk: (closeFunction: Function) => {
return new Promise((resolve, reject) => {
this.createFormRef.validateFields((error: any, values: any) => {
if (!error) {
const newTask = taskDTO(values);
this.props.dispatch(createTaskAsync(newTask)).then(
(data: any) => {
resolve(data);
closeFunction();
},
(error: any) => {
reject(error);
Modal.error({ title: error.message, centered: true, okType: 'danger' })
}
);
} else {
reject(error);
}
});
});
},
onCancel: () => {
return;
},
});
}
private onValueChange = (event: any) => {

@ -64,7 +64,7 @@ class LoginForm extends PureComponent<any, any> {
);
}
private onSubmit = (event: any) => {
private onSubmit = (event: React.FormEvent<HTMLInputElement>) => {
event.preventDefault();
this.props.form.validateFields((error: any, values: any) => {

@ -0,0 +1,8 @@
.ant-badge {
width: 100%;
}
.ant-tree.ant-tree-directory {
height: 108px;
overflow: auto;
}

@ -0,0 +1,364 @@
import React, { PureComponent } from 'react';
import { Form, Input, Icon, Checkbox, Radio, Upload, Badge, Tree, TreeSelect, InputNumber } from 'antd';
import { UploadFile, UploadChangeParam } from 'antd/lib/upload/interface';
import configureStore from '../../../store';
import { getShareFilesAsync } from '../../../actions/server.actions';
import { validateLabels, FileSource, fileModel } from '../../../utils/tasks-dto';
import './task-create.scss';
const { TreeNode } = Tree;
const { SHOW_PARENT } = TreeSelect;
const { Dragger } = Upload;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 8 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
};
const formItemTailLayout = {
labelCol: {
xs: { span: 24 },
},
wrapperCol: {
xs: { span: 24 },
},
};
class TaskCreateForm extends PureComponent<any, any> {
store: any;
constructor(props: any) {
super(props);
this.store = configureStore();
this.state = {
confirmDirty: false,
selectedFileList: [],
filesCounter: 0,
treeData: [],
};
}
componentDidMount() {
this.getSharedFiles('').then(
(data: any) => {
this.setState({ treeData: fileModel('', this.store.getState().server.files) });
},
);
}
private renderTreeNodes = (data: any) => {
return data.map((item: any) => {
if (!item.isLeaf) {
return (
<TreeNode title={ item.name } key={ item.id } value={ item.id } dataRef={ item }>
{ item.children ? this.renderTreeNodes(item.children) : '' }
</TreeNode>
);
}
return <TreeNode isLeaf title={ item.name } key={ item.id } value={ item.id } dataRef={ item } />;
});
}
private renderUploader = () => {
const { getFieldDecorator } = this.props.form;
switch (this.props.form.getFieldValue('source')) {
case FileSource.Local:
return (
<Form.Item
{ ...formItemTailLayout }
extra='Only one video, archive, pdf or many image, directory can be used simultaneously'>
<Badge
count={ this.state.filesCounter }
overflowCount={999}>
<div onClick={ this.resetUploader }>
{getFieldDecorator('localUpload', {
rules: [{ required: true, message: 'Please, add some files!' }],
})(
<Dragger
multiple
showUploadList={ false }
fileList={ this.state.selectedFileList }
customRequest={ this.simulateRequest }
onChange={ this.onUploaderChange }>
<p className="ant-upload-drag-icon">
<Icon type="inbox" />
</p>
<p className="ant-upload-text">Click or drag file to this area to upload</p>
</Dragger>
)}
</div>
</Badge>
</Form.Item>
);
case FileSource.Remote:
return (
<Form.Item { ...formItemLayout } label="URLs">
{getFieldDecorator('remoteURL', {
rules: [],
})(
<Input.TextArea
name="remote-url"
/>
)}
</Form.Item>
);
case FileSource.Share:
return (
<Form.Item { ...formItemLayout } label="Shared files"
extra='Only one video, archive, pdf or many image, directory can be used simultaneously'>
{getFieldDecorator('sharedFiles', {
rules: [{ required: true, message: 'Please, add some files!' }],
})(
<TreeSelect
multiple
treeCheckable={ true }
showCheckedStrategy={ SHOW_PARENT }
loadData={ this.onLoadData }>
{ this.renderTreeNodes(this.state.treeData) }
</TreeSelect>
)}
</Form.Item>
);
default:
break;
}
}
render() {
const { getFieldDecorator } = this.props.form;
return (
<Form>
<Form.Item { ...formItemLayout } label="Name">
{getFieldDecorator('name', {
rules: [
{
required: true,
pattern: new RegExp('[a-zA-Z0-9_]+'),
message: 'Bad task name!',
},
],
})(
<Input
prefix={ <Icon type="profile" /> }
type="text"
name="name"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Labels">
{getFieldDecorator('labels', {
rules: [
{ required: true, message: 'Please add some labels!' },
{ validator: validateLabels, message: 'Bad labels format!' },
],
})(
<Input
prefix={ <Icon type="tag" /> }
type="text"
name="labels"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Bug tracker">
{getFieldDecorator('bugTracker', {
rules: [{ type: 'url', message: 'Bad bug tracker link!' }],
})(
<Input
prefix={ <Icon type="tool" /> }
type="text"
name="bug-tracker"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Use LFS">
{getFieldDecorator('useLFS', {
rules: [],
initialValue: true,
})(
<Checkbox
defaultChecked
name="use-lfs"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Source">
{getFieldDecorator('source', {
rules: [],
initialValue: 1,
})(
<Radio.Group onChange={ this.resetUploader }>
<Radio.Button value={1}>Local</Radio.Button>
<Radio.Button value={2}>Remote</Radio.Button>
<Radio.Button value={3}>Share</Radio.Button>
</Radio.Group>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Z-Order">
{getFieldDecorator('zOrder', {
rules: [],
initialValue: false,
})(
<Checkbox
name="z-order"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Segment size" hasFeedback>
{getFieldDecorator('segmentSize', {
rules: [],
})(
<InputNumber
min={100}
max={50000}
name="segment-size"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Overlap size" hasFeedback>
{getFieldDecorator('overlapSize', {
rules: [],
initialValue: 0,
})(
<InputNumber
min={0}
max={ this.props.form.getFieldValue('segmentSize') ? this.props.form.getFieldValue('segmentSize') - 1 : 0 }
name="overlap-size"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Image quality">
{getFieldDecorator('imageQuality', {
rules: [{ required: true }],
initialValue: 50,
})(
<InputNumber
min={1}
max={95}
name="image-quality"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Start frame" hasFeedback>
{getFieldDecorator('startFrame', {
rules: [],
initialValue: 0,
})(
<InputNumber
min={0}
name="start-frame"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Stop frame" hasFeedback>
{getFieldDecorator('stopFrame', {
rules: [],
})(
<InputNumber
min={ this.props.form.getFieldValue('startFrame') }
name="stop-frame"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Frame filter">
{getFieldDecorator('frameFilter', {
rules: [],
})(
<Input
prefix={ <Icon type="profile" /> }
type="text"
name="frame-filter"
/>
)}
</Form.Item>
{ this.renderUploader() }
</Form>
);
}
private onLoadData = (treeNode: any) => {
return new Promise<void>(resolve => {
if (treeNode.props.children) {
resolve();
return;
}
this.getSharedFiles(treeNode.props.dataRef.id).then(
(data: any) => {
treeNode.props.dataRef.children = fileModel(treeNode, this.store.getState().server.files);
this.setState({
treeData: [...this.state.treeData],
});
resolve();
},
);
});
}
private getSharedFiles = (directory: string) => {
return this.store.dispatch(getShareFilesAsync(directory));
}
private onUploaderChange = (info: UploadChangeParam) => {
const nextState: { selectedFileList: UploadFile[], filesCounter: number } = {
selectedFileList: this.state.selectedFileList,
filesCounter: this.state.filesCounter,
};
switch (info.file.status) {
case 'uploading':
nextState.selectedFileList.push(info.file);
nextState.filesCounter += 1;
break;
case 'done':
break;
default:
// INFO: error or removed
nextState.selectedFileList = info.fileList;
}
this.setState(() => nextState);
}
private resetUploader = () => {
this.setState({ selectedFileList: [], filesCounter: 0 });
}
private simulateRequest = ({ file, onSuccess }: any) => {
setTimeout(() => {
onSuccess(file);
}, 0);
}
}
export default Form.create()(TaskCreateForm);

@ -0,0 +1,51 @@
import React, { PureComponent } from 'react';
import { Form, Input, Icon } from 'antd';
import { serializeLabels, validateLabels } from '../../../utils/tasks-dto'
import './task-update.scss';
class TaskUpdateForm extends PureComponent<any, any> {
render() {
const { getFieldDecorator } = this.props.form;
return (
<Form>
<Form.Item>
{getFieldDecorator('oldLabels', {
rules: [],
initialValue: serializeLabels(this.props.task),
})(
<Input
disabled
prefix={ <Icon type="tag" /> }
type="text"
name="oldLabels"
placeholder="Old labels"
/>,
)}
</Form.Item>
<Form.Item>
{getFieldDecorator('newLabels', {
rules: [
{ required: true, message: 'Please input new labels!' },
{ validator: validateLabels, message: 'Bad labels format!' },
],
})(
<Input
prefix={ <Icon type="tag" /> }
type="text"
name="new-labels"
placeholder="Expand the specification here"
/>,
)}
</Form.Item>
</Form>
);
}
}
export default Form.create()(TaskUpdateForm) as any;

@ -170,7 +170,7 @@ class RegisterForm extends PureComponent<any, any> {
callback();
};
private onSubmit = (event: any) => {
private onSubmit = (event: React.FormEvent<HTMLInputElement>) => {
event.preventDefault();
this.props.form.validateFields((error: any, values: any) => {

@ -11,3 +11,7 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
.crud-modal {
width: 90% !important;
}

@ -0,0 +1,40 @@
export default (
state = {
downloadLink: null,
isFetching: false,
error: null,
},
action: any,
) => {
switch (action.type) {
case 'DUMP_ANNOTATION':
return Object.assign({}, state, {
isFetching: true,
});
case 'DUMP_ANNOTATION_SUCCESS':
return Object.assign({}, state, {
isFetching: false,
downloadLink: action.payload,
});
case 'DUMP_ANNOTATION_ERROR':
return Object.assign({}, state, {
isFetching: false,
error: action.payload,
});
case 'UPLOAD_ANNOTATION':
return Object.assign({}, state, {
isFetching: true,
});
case 'UPLOAD_ANNOTATION_SUCCESS':
return Object.assign({}, state, {
isFetching: false,
});
case 'UPLOAD_ANNOTATION_ERROR':
return Object.assign({}, state, {
isFetching: false,
error: action.payload,
});
default:
return state;
}
}

@ -3,9 +3,13 @@ import { combineReducers } from 'redux';
import authContext from './auth.reducer';
import tasks from './tasks.reducer';
import tasksFilter from './tasks-filter.reducer';
import server from './server.reducer';
import annotations from './annotations.reducer';
export default combineReducers({
authContext,
tasks,
tasksFilter,
server,
annotations,
});

@ -0,0 +1,57 @@
export default (
state = {
info: null,
files: [],
annotationFormats: [],
isFetching: false,
error: null,
},
action: any,
) => {
switch (action.type) {
case 'GET_SERVER_INFO':
return Object.assign({}, state, {
isFetching: true,
});
case 'GET_SERVER_INFO_SUCCESS':
return Object.assign({}, state, {
isFetching: false,
info: action.payload,
});
case 'GET_SERVER_INFO_ERROR':
return Object.assign({}, state, {
isFetching: false,
error: action.payload,
});
case 'GET_SHARE_FILES':
return Object.assign({}, state, {
isFetching: true,
});
case 'GET_SHARE_FILES_SUCCESS':
return Object.assign({}, state, {
isFetching: false,
files: action.payload,
});
case 'GET_SHARE_FILES_ERROR':
return Object.assign({}, state, {
isFetching: false,
error: action.payload,
});
case 'GET_ANNOTATION_FORMATS':
return Object.assign({}, state, {
isFetching: true,
});
case 'GET_ANNOTATION_FORMATS_SUCCESS':
return Object.assign({}, state, {
isFetching: false,
annotationFormats: action.payload,
});
case 'GET_ANNOTATION_FORMATS_ERROR':
return Object.assign({}, state, {
isFetching: false,
error: action.payload,
});
default:
return state;
}
}

@ -24,6 +24,34 @@ export default (
error: action.payload,
});
case 'CREATE_TASK':
return Object.assign({}, state, {
isFetching: true,
});
case 'CREATE_TASK_SUCCESS':
return Object.assign({}, state, {
isFetching: false,
});
case 'CREATE_TASK_ERROR':
return Object.assign({}, state, {
isFetching: false,
error: action.payload,
});
case 'UPDATE_TASK':
return Object.assign({}, state, {
isFetching: true,
});
case 'UPDATE_TASK_SUCCESS':
return Object.assign({}, state, {
isFetching: false,
});
case 'UPDATE_TASK_ERROR':
return Object.assign({}, state, {
isFetching: false,
error: action.payload,
});
case 'DELETE_TASK':
return Object.assign({}, state, {
isFetching: true,

@ -11,12 +11,12 @@ export default function configureStore(initialState = {}) {
const middlewares = [];
middlewares.push(thunk);
if (process.env.NODE_ENV === `development`) {
middlewares.push(logger);
}
middlewares.push(thunk);
return createStore(
rootReducer,
initialState,

@ -0,0 +1,152 @@
export enum FileSource {
Local = 1,
Remote = 2,
Share = 3,
}
export function fileModel(parentNode: any, files: any) {
return files.map(
(file: any) => {
return {
id: parentNode.props ? `${parentNode.props.value}/${file.name}` : file.name,
isLeaf: file.type !== 'DIR',
name: file.name,
};
}
);
}
export function taskDTO(values: any) {
const newTaskDTO = {
name: values.name,
labels: deserializeLabels(values.labels),
image_quality: values.imageQuality,
z_order: values.zOrder,
bug_tracker: values.bugTracker,
segment_size: values.segmentSize,
overlap: values.overlapSize,
frame_filter: values.frameFilter,
start_frame: values.startFrame,
stop_frame: values.stopFrame,
};
const newTask = new (window as any).cvat.classes.Task(newTaskDTO);
if (values.source === FileSource.Local) {
newTask.clientFiles = values.localUpload.fileList.map((file: any) => file.response);
} else if (values.source === FileSource.Remote) {
newTask.remoteFiles = values.remoteURL
.split(/\r?\n/)
.map((url: string) => url.trim())
.filter((url: string) => url.length > 0);
} else if (values.source === FileSource.Share) {
newTask.serverFiles = values.sharedFiles;
}
return newTask;
}
export function validateLabels(rule: any, value: string, callback: Function) {
if (value) {
try {
deserializeLabels(value);
} catch (error) {
callback(error.message);
}
}
callback();
}
export function serializeLabels(task: any) {
const labels = task.labels.map((label: any) => label.toJSON());
let serialized = '';
for (const label of labels) {
serialized += ` ${label.name}`;
for (const attr of label.attributes) {
serialized += ` ${attr.mutable ? '~' : '@'}`;
serialized += `${attr.input_type}=${attr.name}:`;
serialized += attr.values.join(',');
}
}
return serialized.trim();
}
export function deserializeLabels(serialized: string) {
const normalized = serialized.replace(/'+/g, '\'').replace(/"+/g, '"').replace(/\s+/g, ' ').trim();
const fragments = customSplit(normalized, ' ');
const deserialized = [];
let latest: any = null;
for (let fragment of fragments) {
fragment = fragment.trim();
if ((fragment.startsWith('~')) || (fragment.startsWith('@'))) {
const regex = /(@|~)(checkbox|select|number|text|radio)=([-,?!_0-9a-zA-Z()\s"]+):([-,?!_0-9a-zA-Z()"\s]+)/g;
const result = regex.exec(fragment);
if (result === null || latest === null) {
throw Error('Bad labels format');
}
const values = customSplit(result[4], ',');
latest.attributes.push({
name: result[3].replace(/^"/, '').replace(/"$/, ''),
mutable: result[1] === '~',
input_type: result[2],
default_value: values[0].replace(/^"/, '').replace(/"$/, ''),
values: values.map(val => val.replace(/^"/, '').replace(/"$/, '')),
});
} else {
latest = {
name: fragment.replace(/^"/, '').replace(/"$/, ''),
attributes: [],
};
deserialized.push(latest);
}
}
return deserialized;
}
export function customSplit(string: any, separator: any) {
const regex = /"/gi;
const occurences = [];
let occurence = regex.exec(string);
while (occurence) {
occurences.push(occurence.index);
occurence = regex.exec(string);
}
if (occurences.length % 2) {
occurences.pop();
}
let copy = '';
if (occurences.length) {
let start = 0;
for (let idx = 0; idx < occurences.length; idx += 2) {
copy += string.substr(start, occurences[idx] - start);
copy += string.substr(occurences[idx], occurences[idx + 1] - occurences[idx] + 1)
.replace(new RegExp(separator, 'g'), '\0');
start = occurences[idx + 1] + 1;
}
copy += string.substr(occurences[occurences.length - 1] + 1);
} else {
copy = string;
}
return copy.split(new RegExp(separator, 'g')).map(x => x.replace(/\0/g, separator));
}

@ -611,7 +611,7 @@ class DashboardView {
if (!validateBugTracker()) {
taskMessage.css('color', 'red');
taskMessage.text('Bad bag tracker link');
taskMessage.text('Bad bug tracker link');
return;
}

Loading…
Cancel
Save