React & Antd UI: Create task (#840)
* Separated component user selector * Change job assignee * Basic create task window * Bug fixes and refactoring * Create task connected with a server * Loading status for a button * Reset loading on error response * UI improvements * Github/feedback/share windowmain
parent
690ecf9c66
commit
3b6961f4db
@ -0,0 +1,60 @@
|
||||
import { AnyAction, Dispatch, ActionCreator } from 'redux';
|
||||
import { ThunkAction } from 'redux-thunk';
|
||||
|
||||
import { ShareFileInfo } from '../reducers/interfaces';
|
||||
import getCore from '../core';
|
||||
|
||||
const core = getCore();
|
||||
|
||||
export enum ShareActionTypes {
|
||||
LOAD_SHARE_DATA = 'LOAD_SHARE_DATA',
|
||||
LOAD_SHARE_DATA_SUCCESS = 'LOAD_SHARE_DATA_SUCCESS',
|
||||
LOAD_SHARE_DATA_FAILED = 'LOAD_SHARE_DATA_FAILED',
|
||||
}
|
||||
|
||||
function loadShareData(): AnyAction {
|
||||
const action = {
|
||||
type: ShareActionTypes.LOAD_SHARE_DATA,
|
||||
payload: {},
|
||||
};
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
function loadShareDataSuccess(values: ShareFileInfo[], directory: string): AnyAction {
|
||||
const action = {
|
||||
type: ShareActionTypes.LOAD_SHARE_DATA_SUCCESS,
|
||||
payload: {
|
||||
values,
|
||||
directory,
|
||||
},
|
||||
};
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
function loadShareDataFailed(error: any): AnyAction {
|
||||
const action = {
|
||||
type: ShareActionTypes.LOAD_SHARE_DATA_FAILED,
|
||||
payload: {
|
||||
error,
|
||||
},
|
||||
};
|
||||
|
||||
return action;
|
||||
}
|
||||
|
||||
export function loadShareDataAsync(directory: string, success: () => void, failure: () => void):
|
||||
ThunkAction<Promise<void>, {}, {}, AnyAction> {
|
||||
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
|
||||
try {
|
||||
dispatch(loadShareData());
|
||||
const values = await core.server.share(directory);
|
||||
success();
|
||||
dispatch(loadShareDataSuccess(values as ShareFileInfo[], directory));
|
||||
} catch (error) {
|
||||
dispatch(loadShareDataFailed(error));
|
||||
failure();
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,295 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Icon,
|
||||
Input,
|
||||
Checkbox,
|
||||
Tooltip,
|
||||
} from 'antd';
|
||||
|
||||
import Form, { FormComponentProps } from 'antd/lib/form/Form';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
|
||||
import patterns from '../../utils/validation-patterns';
|
||||
|
||||
export interface AdvancedConfiguration {
|
||||
bugTracker?: string;
|
||||
zOrder: boolean;
|
||||
imageQuality?: number;
|
||||
overlapSize?: number;
|
||||
segmentSize?: number;
|
||||
startFrame?: number;
|
||||
stopFrame?: number;
|
||||
frameFilter?: string;
|
||||
lfs: boolean;
|
||||
repository?: string;
|
||||
}
|
||||
|
||||
type Props = FormComponentProps & {
|
||||
onSubmit(values: AdvancedConfiguration): void
|
||||
installedGit: boolean;
|
||||
};
|
||||
|
||||
class AdvancedConfigurationForm extends React.PureComponent<Props> {
|
||||
public async submit() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.props.form.validateFields((error, values) => {
|
||||
if (!error) {
|
||||
const filteredValues = { ...values };
|
||||
delete filteredValues.frameStep;
|
||||
|
||||
this.props.onSubmit({
|
||||
...values,
|
||||
frameFilter: values.frameStep ? `step=${values.frameStep}` : undefined,
|
||||
});
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
public resetFields() {
|
||||
this.props.form.resetFields();
|
||||
}
|
||||
|
||||
private renderZOrder() {
|
||||
return (
|
||||
<Form.Item style={{marginBottom: '0px'}}>
|
||||
<Tooltip overlay='Enable order for shapes. Useful for segmentation tasks'>
|
||||
{this.props.form.getFieldDecorator('zOrder', {
|
||||
initialValue: false,
|
||||
valuePropName: 'checked',
|
||||
})(
|
||||
<Checkbox>
|
||||
<Text className='cvat-black-color'>
|
||||
Z-order
|
||||
</Text>
|
||||
</Checkbox>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
private renderImageQuality() {
|
||||
return (
|
||||
<Form.Item style={{marginBottom: '0px'}}>
|
||||
<Tooltip overlay='Defines image compression level'>
|
||||
<Text className='cvat-black-color'> Image quality </Text>
|
||||
{this.props.form.getFieldDecorator('imageQuality', {
|
||||
initialValue: 70,
|
||||
rules: [{
|
||||
required: true,
|
||||
message: 'This field is required'
|
||||
}],
|
||||
})(
|
||||
<Input
|
||||
size='large'
|
||||
type='number'
|
||||
min={5}
|
||||
max={100}
|
||||
suffix={<Icon type='percentage'/>}
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
private renderOverlap() {
|
||||
return (
|
||||
<Form.Item style={{marginBottom: '0px'}}>
|
||||
<Tooltip overlay='Defines a number of intersected frames between different segments'>
|
||||
<Text className='cvat-black-color'> Overlap size </Text>
|
||||
{this.props.form.getFieldDecorator('overlapSize')(
|
||||
<Input size='large' type='number'/>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
private renderSegmentSize() {
|
||||
return (
|
||||
<Form.Item style={{marginBottom: '0px'}}>
|
||||
<Tooltip overlay='Defines a number of frames in a segment'>
|
||||
<Text className='cvat-black-color'> Segment size </Text>
|
||||
{this.props.form.getFieldDecorator('segmentSize')(
|
||||
<Input size='large' type='number'/>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
private renderStartFrame() {
|
||||
return (
|
||||
<Form.Item style={{marginBottom: '0px'}}>
|
||||
<Text className='cvat-black-color'> Start frame </Text>
|
||||
{this.props.form.getFieldDecorator('startFrame')(
|
||||
<Input
|
||||
size='large'
|
||||
type='number'
|
||||
min={0}
|
||||
step={1}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
private renderStopFrame() {
|
||||
return (
|
||||
<Form.Item style={{marginBottom: '0px'}}>
|
||||
<Text className='cvat-black-color'> Stop frame </Text>
|
||||
{this.props.form.getFieldDecorator('stopFrame')(
|
||||
<Input
|
||||
size='large'
|
||||
type='number'
|
||||
min={0}
|
||||
step={1}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
private renderFrameStep() {
|
||||
return (
|
||||
<Form.Item style={{marginBottom: '0px'}}>
|
||||
<Text className='cvat-black-color'> Frame step </Text>
|
||||
{this.props.form.getFieldDecorator('frameStep')(
|
||||
<Input
|
||||
size='large'
|
||||
type='number'
|
||||
min={1}
|
||||
step={1}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
private renderGitLFSBox() {
|
||||
return (
|
||||
<Form.Item style={{marginBottom: '0px'}}>
|
||||
<Tooltip overlay='If annotation files are large, you can use git LFS feature'>
|
||||
{this.props.form.getFieldDecorator('lfs', {
|
||||
valuePropName: 'checked',
|
||||
initialValue: false,
|
||||
})(
|
||||
<Checkbox>
|
||||
<Text className='cvat-black-color'>
|
||||
Use LFS (Large File Support)
|
||||
</Text>
|
||||
</Checkbox>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
private renderGitRepositoryURL() {
|
||||
return (
|
||||
<Form.Item style={{marginBottom: '0px'}}>
|
||||
<Tooltip overlay={`Attach a git repository to store annotations.
|
||||
Path is specified in square brackets`}>
|
||||
<Text className='cvat-black-color'> Dataset repository URL </Text>
|
||||
{this.props.form.getFieldDecorator('repository', {
|
||||
// TODO: Add pattern
|
||||
})(
|
||||
<Input
|
||||
placeholder='e.g. https//github.com/user/repos [annotation/<anno_file_name>.zip]'
|
||||
size='large'
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Form.Item>
|
||||
);
|
||||
}
|
||||
|
||||
private renderGit() {
|
||||
return (
|
||||
<>
|
||||
<Row>
|
||||
<Col>
|
||||
{this.renderGitRepositoryURL()}
|
||||
|
||||
</Col>
|
||||
</Row>
|
||||
<Row>
|
||||
<Col>
|
||||
{this.renderGitLFSBox()}
|
||||
</Col>
|
||||
</Row>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
private renderBugTracker() {
|
||||
return (
|
||||
<Form.Item style={{marginBottom: '0px'}}>
|
||||
<Tooltip overlay='Attach issue tracker where the task is described'>
|
||||
<Text className='cvat-black-color'> Issue tracker </Text>
|
||||
{this.props.form.getFieldDecorator('bugTracker', {
|
||||
rules: [{
|
||||
...patterns.validateURL,
|
||||
}]
|
||||
})(
|
||||
<Input
|
||||
size='large'
|
||||
/>
|
||||
)}
|
||||
</Tooltip>
|
||||
</Form.Item>
|
||||
)
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<Form>
|
||||
<Row><Col>
|
||||
{this.renderZOrder()}
|
||||
</Col></Row>
|
||||
|
||||
<Row type='flex' justify='start'>
|
||||
<Col span={7}>
|
||||
{this.renderImageQuality()}
|
||||
</Col>
|
||||
<Col span={7} offset={1}>
|
||||
{this.renderOverlap()}
|
||||
</Col>
|
||||
<Col span={7} offset={1}>
|
||||
{this.renderSegmentSize()}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row type='flex' justify='start'>
|
||||
<Col span={7}>
|
||||
{this.renderStartFrame()}
|
||||
</Col>
|
||||
<Col span={7} offset={1}>
|
||||
{this.renderStopFrame()}
|
||||
</Col>
|
||||
<Col span={7} offset={1}>
|
||||
{this.renderFrameStep()}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{ this.props.installedGit ? this.renderGit() : null}
|
||||
|
||||
<Row>
|
||||
<Col>
|
||||
{this.renderBugTracker()}
|
||||
</Col>
|
||||
</Row>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create<Props>()(AdvancedConfigurationForm);
|
||||
@ -0,0 +1,58 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Input,
|
||||
} from 'antd';
|
||||
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
import Form, { FormComponentProps } from 'antd/lib/form/Form';
|
||||
|
||||
export interface BaseConfiguration {
|
||||
name: string;
|
||||
}
|
||||
|
||||
type Props = FormComponentProps & {
|
||||
onSubmit(values: BaseConfiguration): void;
|
||||
};
|
||||
|
||||
class BasicConfigurationForm extends React.PureComponent<Props> {
|
||||
public async submit() {
|
||||
return new Promise((resolve, reject) => {
|
||||
this.props.form.validateFields((error, values) => {
|
||||
if (!error) {
|
||||
this.props.onSubmit({
|
||||
name: values.name,
|
||||
});
|
||||
resolve();
|
||||
} else {
|
||||
reject();
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
public resetFields() {
|
||||
this.props.form.resetFields();
|
||||
}
|
||||
|
||||
public render() {
|
||||
const { getFieldDecorator } = this.props.form;
|
||||
return (
|
||||
<Form onSubmit={(e: React.FormEvent) => e.preventDefault()}>
|
||||
<Text type='secondary'> Name </Text>
|
||||
<Form.Item style={{marginBottom: '0px'}}>
|
||||
{ getFieldDecorator('name', {
|
||||
rules: [{
|
||||
required: true,
|
||||
message: 'Please, specify a name',
|
||||
}] // TODO: Add task name pattern
|
||||
})(
|
||||
<Input/>
|
||||
) }
|
||||
</Form.Item>
|
||||
</Form>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default Form.create<Props>()(BasicConfigurationForm);
|
||||
@ -0,0 +1,243 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Alert,
|
||||
Modal,
|
||||
Button,
|
||||
Collapse,
|
||||
message,
|
||||
} from 'antd';
|
||||
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
|
||||
import BasicConfigurationForm, { BaseConfiguration } from './basic-configuration-form';
|
||||
import AdvancedConfigurationForm, { AdvancedConfiguration } from './advanced-configuration-form';
|
||||
import LabelsEditor from '../labels-editor/labels-editor';
|
||||
import FileManagerContainer from '../../containers/file-manager/file-manager';
|
||||
import { Files } from '../file-manager/file-manager';
|
||||
|
||||
export interface CreateTaskData {
|
||||
basic: BaseConfiguration;
|
||||
advanced: AdvancedConfiguration;
|
||||
labels: any[];
|
||||
files: Files,
|
||||
}
|
||||
|
||||
interface Props {
|
||||
onCreate: (data: CreateTaskData) => void;
|
||||
status: string;
|
||||
error: string;
|
||||
installedGit: boolean;
|
||||
}
|
||||
|
||||
type State = CreateTaskData;
|
||||
|
||||
const defaultState = {
|
||||
basic: {
|
||||
name: '',
|
||||
},
|
||||
advanced: {
|
||||
zOrder: false,
|
||||
lfs: false,
|
||||
},
|
||||
labels: [],
|
||||
files: {
|
||||
local: [],
|
||||
share: [],
|
||||
remote: [],
|
||||
},
|
||||
};
|
||||
|
||||
export default class CreateTaskContent extends React.PureComponent<Props, State> {
|
||||
private basicConfigurationComponent: any;
|
||||
private advancedConfigurationComponent: any;
|
||||
private fileManagerContainer: any;
|
||||
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { ...defaultState };
|
||||
}
|
||||
|
||||
private validateLabels = () => {
|
||||
return !!this.state.labels.length;
|
||||
}
|
||||
|
||||
private validateFiles = () => {
|
||||
const files = this.fileManagerContainer.getFiles();
|
||||
this.setState({
|
||||
files,
|
||||
});
|
||||
const totalLen = Object.keys(files).reduce(
|
||||
(acc, key) => acc + files[key].length, 0,
|
||||
);
|
||||
|
||||
return !!totalLen;
|
||||
}
|
||||
|
||||
private handleSubmitBasicConfiguration = (values: BaseConfiguration) => {
|
||||
this.setState({
|
||||
basic: {...values},
|
||||
});
|
||||
};
|
||||
|
||||
private handleSubmitAdvancedConfiguration = (values: AdvancedConfiguration) => {
|
||||
this.setState({
|
||||
advanced: {...values},
|
||||
});
|
||||
};
|
||||
|
||||
private handleSubmitClick = () => {
|
||||
if (!this.validateLabels()) {
|
||||
Modal.error({
|
||||
title: 'Could not create a task',
|
||||
content: 'A task must contain at least one label',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.validateFiles()) {
|
||||
Modal.error({
|
||||
title: 'Could not create a task',
|
||||
content: 'A task must contain at least one file',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.basicConfigurationComponent.submit()
|
||||
.then(() => {
|
||||
return this.advancedConfigurationComponent ?
|
||||
this.advancedConfigurationComponent.submit() :
|
||||
new Promise((resolve) => {
|
||||
resolve();
|
||||
})
|
||||
})
|
||||
.then(() => {
|
||||
this.props.onCreate(this.state);
|
||||
})
|
||||
.catch((_: any) => {
|
||||
Modal.error({
|
||||
title: 'Could not create a task',
|
||||
content: 'Please, check configuration you specified',
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private renderBasicBlock() {
|
||||
return (
|
||||
<Col span={24}>
|
||||
<BasicConfigurationForm wrappedComponentRef={
|
||||
(component: any) => { this.basicConfigurationComponent = component }
|
||||
} onSubmit={this.handleSubmitBasicConfiguration}/>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
private renderLabelsBlock() {
|
||||
return (
|
||||
<Col span={24}>
|
||||
<Text type='secondary'> Labels </Text>
|
||||
<LabelsEditor
|
||||
labels={this.state.labels}
|
||||
onSubmit={
|
||||
(labels) => {
|
||||
this.setState({
|
||||
labels,
|
||||
});
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
private renderFilesBlock() {
|
||||
return (
|
||||
<Col span={24}>
|
||||
<FileManagerContainer ref={
|
||||
(container: any) =>
|
||||
this.fileManagerContainer = container
|
||||
}/>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
private renderAdvancedBlock() {
|
||||
return (
|
||||
<Col span={24}>
|
||||
<Collapse>
|
||||
<Collapse.Panel
|
||||
header={
|
||||
<Text className='cvat-title'> Advanced configuration </Text>
|
||||
} key='1'>
|
||||
<AdvancedConfigurationForm
|
||||
installedGit={this.props.installedGit}
|
||||
wrappedComponentRef={
|
||||
(component: any) => {
|
||||
this.advancedConfigurationComponent = component
|
||||
}
|
||||
}
|
||||
onSubmit={this.handleSubmitAdvancedConfiguration}
|
||||
/>
|
||||
</Collapse.Panel>
|
||||
</Collapse>
|
||||
</Col>
|
||||
);
|
||||
}
|
||||
|
||||
public componentDidUpdate(prevProps: Props) {
|
||||
if (this.props.error && prevProps.error !== this.props.error) {
|
||||
Modal.error({
|
||||
title: 'Could not create task',
|
||||
content: this.props.error,
|
||||
});
|
||||
}
|
||||
|
||||
if (this.props.status === 'CREATED' && prevProps.status !== 'CREATED') {
|
||||
message.success('The task has been created');
|
||||
|
||||
this.basicConfigurationComponent.resetFields();
|
||||
if (this.advancedConfigurationComponent) {
|
||||
this.advancedConfigurationComponent.resetFields();
|
||||
}
|
||||
|
||||
this.fileManagerContainer.reset();
|
||||
|
||||
this.setState({
|
||||
...defaultState,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public render() {
|
||||
const loading = !!this.props.status
|
||||
&& this.props.status !== 'CREATED'
|
||||
&& !this.props.error;
|
||||
|
||||
return (
|
||||
<Row type='flex' justify='start' align='middle' className='cvat-create-task-content'>
|
||||
<Col span={24}>
|
||||
<Text className='cvat-title'> Basic configuration </Text>
|
||||
</Col>
|
||||
|
||||
{ this.renderBasicBlock() }
|
||||
{ this.renderLabelsBlock() }
|
||||
{ this.renderFilesBlock() }
|
||||
{ this.renderAdvancedBlock() }
|
||||
|
||||
<Col span={14}>
|
||||
{loading ? <Alert message={this.props.status}/> : null}
|
||||
</Col>
|
||||
<Col span={10}>
|
||||
<Button
|
||||
loading={loading}
|
||||
disabled={loading}
|
||||
type='danger'
|
||||
onClick={this.handleSubmitClick}
|
||||
> Submit </Button>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Row,
|
||||
Col,
|
||||
Modal,
|
||||
} from 'antd';
|
||||
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
|
||||
import CreateTaskContent, { CreateTaskData } from './create-task-content';
|
||||
|
||||
interface Props {
|
||||
onCreate: (data: CreateTaskData) => void;
|
||||
error: string;
|
||||
status: string;
|
||||
installedGit: boolean;
|
||||
}
|
||||
|
||||
export default function CreateTaskPage(props: Props) {
|
||||
return (
|
||||
<Row type='flex' justify='center' align='top' className='cvat-create-task-form-wrapper'>
|
||||
<Col md={20} lg={16} xl={14} xxl={9}>
|
||||
<Text className='cvat-title'> Create a new task</Text>
|
||||
<CreateTaskContent
|
||||
status={props.status}
|
||||
error={props.error}
|
||||
onCreate={props.onCreate}
|
||||
installedGit={props.installedGit}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
}
|
||||
@ -0,0 +1,118 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Icon,
|
||||
Popover,
|
||||
} from 'antd';
|
||||
|
||||
import {
|
||||
FacebookShareButton,
|
||||
LinkedinShareButton,
|
||||
TwitterShareButton,
|
||||
TelegramShareButton,
|
||||
WhatsappShareButton,
|
||||
VKShareButton,
|
||||
RedditShareButton,
|
||||
ViberShareButton,
|
||||
FacebookIcon,
|
||||
TwitterIcon,
|
||||
TelegramIcon,
|
||||
WhatsappIcon,
|
||||
VKIcon,
|
||||
RedditIcon,
|
||||
ViberIcon,
|
||||
LineIcon,
|
||||
} from 'react-share';
|
||||
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
|
||||
interface State {
|
||||
active: boolean;
|
||||
}
|
||||
|
||||
export default class Feedback extends React.PureComponent<{}, State> {
|
||||
public constructor(props: {}) {
|
||||
super(props);
|
||||
this.state = {
|
||||
active: false,
|
||||
}
|
||||
}
|
||||
|
||||
private renderContent() {
|
||||
const githubURL = 'https://github.com/opencv/cvat';
|
||||
const githubImage = 'https://raw.githubusercontent.com/opencv/'
|
||||
+ 'cvat/develop/cvat/apps/documentation/static/documentation/images/cvat.jpg';
|
||||
const questionsURL = 'https://gitter.im/opencv-cvat/public';
|
||||
const feedbackURL = 'https://gitter.im/opencv-cvat/public';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Icon type='star'/>
|
||||
<Text style={{marginLeft: '10px'}}>
|
||||
Star us on <a target='_blank' href={githubURL}>GitHub</a>
|
||||
</Text>
|
||||
<br/>
|
||||
<Icon type='like'/>
|
||||
<Text style={{marginLeft: '10px'}}>
|
||||
Left a <a target='_blank' href={feedbackURL}>feedback</a>
|
||||
</Text>
|
||||
<hr/>
|
||||
<div style={{display: 'flex'}}>
|
||||
<FacebookShareButton url={githubURL} quote='Computer Vision Annotation Tool'>
|
||||
<FacebookIcon size={32} round={true} />
|
||||
</FacebookShareButton>
|
||||
<VKShareButton url={githubURL} title='Computer Vision Annotation Tool' image={githubImage} description='CVAT'>
|
||||
<VKIcon size={32} round={true} />
|
||||
</VKShareButton>
|
||||
<TwitterShareButton url={githubURL} title='Computer Vision Annotation Tool' hashtags={['CVAT']}>
|
||||
<TwitterIcon size={32} round={true} />
|
||||
</TwitterShareButton>
|
||||
<RedditShareButton url={githubURL} title='Computer Vision Annotation Tool'>
|
||||
<RedditIcon size={32} round={true} />
|
||||
</RedditShareButton>
|
||||
<LinkedinShareButton url={githubURL}>
|
||||
<LineIcon size={32} round={true} />
|
||||
</LinkedinShareButton>
|
||||
<TelegramShareButton url={githubURL} title='Computer Vision Annotation Tool'>
|
||||
<TelegramIcon size={32} round={true} />
|
||||
</TelegramShareButton>
|
||||
<WhatsappShareButton url={githubURL} title='Computer Vision Annotation Tool'>
|
||||
<WhatsappIcon size={32} round={true} />
|
||||
</WhatsappShareButton>
|
||||
<ViberShareButton url={githubURL} title='Computer Vision Annotation Tool'>
|
||||
<ViberIcon size={32} round={true} />
|
||||
</ViberShareButton>
|
||||
</div>
|
||||
<hr/>
|
||||
<Text style={{marginTop: '50px'}}>
|
||||
Do you need help? Contact us on <a href={questionsURL}>gitter</a>
|
||||
</Text>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<Popover
|
||||
placement='leftTop'
|
||||
title={
|
||||
<Text className='cvat-title'>Help to make CVAT better</Text>
|
||||
}
|
||||
content={this.renderContent()}
|
||||
visible={this.state.active}
|
||||
>
|
||||
<Button style={{color: '#ff4d4f'}} className='cvat-feedback-button' type='link' onClick={() => {
|
||||
this.setState({
|
||||
active: !this.state.active,
|
||||
});
|
||||
}}>
|
||||
{ this.state.active ? <Icon type='close-circle' theme='filled'/> :
|
||||
<Icon type='message' theme='twoTone'/> }
|
||||
</Button>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,197 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Tabs,
|
||||
Icon,
|
||||
Input,
|
||||
Upload,
|
||||
} from 'antd';
|
||||
|
||||
import Tree, { AntTreeNode, TreeNodeNormal } from 'antd/lib/tree/Tree';
|
||||
import { RcFile } from 'antd/lib/upload';
|
||||
import Text from 'antd/lib/typography/Text';
|
||||
|
||||
export interface Files {
|
||||
local: File[];
|
||||
share: string[];
|
||||
remote: string[];
|
||||
}
|
||||
|
||||
interface State {
|
||||
files: Files;
|
||||
expandedKeys: string[];
|
||||
active: 'local' | 'share' | 'remote';
|
||||
}
|
||||
|
||||
interface Props {
|
||||
treeData: TreeNodeNormal[];
|
||||
onLoadData: (key: string, success: () => void, failure: () => void) => void;
|
||||
}
|
||||
|
||||
export default class FileManager extends React.PureComponent<Props, State> {
|
||||
public constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
files: {
|
||||
local: [],
|
||||
share: [],
|
||||
remote: [],
|
||||
},
|
||||
expandedKeys: [],
|
||||
active: 'local',
|
||||
};
|
||||
|
||||
this.loadData('/');
|
||||
};
|
||||
|
||||
private loadData = (key: string) => {
|
||||
const promise = new Promise<void>((resolve, reject) => {
|
||||
const success = () => resolve();
|
||||
const failure = () => reject();
|
||||
this.props.onLoadData(key, success, failure);
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
private renderLocalSelector() {
|
||||
return (
|
||||
<Tabs.TabPane key='local' tab='My computer'>
|
||||
<Upload.Dragger
|
||||
multiple
|
||||
fileList={this.state.files.local as any[]}
|
||||
showUploadList={false}
|
||||
beforeUpload={(_: RcFile, files: RcFile[]) => {
|
||||
this.setState({
|
||||
files: {
|
||||
...this.state.files,
|
||||
local: files
|
||||
},
|
||||
});
|
||||
return false;
|
||||
}
|
||||
}>
|
||||
<p className='ant-upload-drag-icon'>
|
||||
<Icon type='inbox' />
|
||||
</p>
|
||||
<p className='ant-upload-text'>Click or drag files to this area</p>
|
||||
<p className='ant-upload-hint'>
|
||||
Support for a bulk images or a single video
|
||||
</p>
|
||||
</Upload.Dragger>
|
||||
{ this.state.files.local.length ?
|
||||
<>
|
||||
<br/>
|
||||
<Text className='cvat-black-color'>
|
||||
{this.state.files.local.length} file(s) selected
|
||||
</Text>
|
||||
</> : null
|
||||
}
|
||||
</Tabs.TabPane>
|
||||
);
|
||||
}
|
||||
|
||||
private renderShareSelector() {
|
||||
function renderTreeNodes(data: TreeNodeNormal[]) {
|
||||
return data.map((item: TreeNodeNormal) => {
|
||||
if (item.children) {
|
||||
return (
|
||||
<Tree.TreeNode title={item.title} key={item.key} dataRef={item} isLeaf={item.isLeaf}>
|
||||
{renderTreeNodes(item.children)}
|
||||
</Tree.TreeNode>
|
||||
);
|
||||
}
|
||||
|
||||
return <Tree.TreeNode key={item.key} {...item} dataRef={item} />;
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Tabs.TabPane key='share' tab='Connected file share'>
|
||||
{ this.props.treeData.length ?
|
||||
<Tree
|
||||
className='cvat-share-tree'
|
||||
checkable
|
||||
showLine
|
||||
checkStrictly={false}
|
||||
expandedKeys={this.state.expandedKeys}
|
||||
checkedKeys={this.state.files.share}
|
||||
loadData={(node: AntTreeNode) => {
|
||||
return this.loadData(node.props.dataRef.key);
|
||||
}}
|
||||
onExpand={(expandedKeys: string[]) => {
|
||||
this.setState({
|
||||
expandedKeys,
|
||||
});
|
||||
}}
|
||||
onCheck={(checkedKeys: string[] | {checked: string[], halfChecked: string[]}) => {
|
||||
const keys = checkedKeys as string[];
|
||||
this.setState({
|
||||
files: {
|
||||
...this.state.files,
|
||||
share: keys,
|
||||
},
|
||||
});
|
||||
}}>
|
||||
{ renderTreeNodes(this.props.treeData) }
|
||||
</Tree> : <Text className='cvat-black-color'> No data found </Text>
|
||||
}
|
||||
</Tabs.TabPane>
|
||||
);
|
||||
}
|
||||
|
||||
private renderRemoteSelector() {
|
||||
return (
|
||||
<Tabs.TabPane key='remote' tab='Remote sources'>
|
||||
<Input.TextArea
|
||||
placeholder='Enter one URL per line'
|
||||
rows={6}
|
||||
value={[...this.state.files.remote].join('\n')}
|
||||
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
this.setState({
|
||||
files: {
|
||||
...this.state.files,
|
||||
remote: event.target.value.split('\n'),
|
||||
},
|
||||
});
|
||||
}}/>
|
||||
</Tabs.TabPane>
|
||||
);
|
||||
}
|
||||
|
||||
public getFiles(): Files {
|
||||
return {
|
||||
local: this.state.active === 'local' ? this.state.files.local : [],
|
||||
share: this.state.active === 'share' ? this.state.files.share : [],
|
||||
remote: this.state.active === 'remote' ? this.state.files.remote : [],
|
||||
};
|
||||
}
|
||||
|
||||
public reset() {
|
||||
this.setState({
|
||||
expandedKeys: [],
|
||||
active: 'local',
|
||||
files: {
|
||||
local: [],
|
||||
share: [],
|
||||
remote: [],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<>
|
||||
<Text type='secondary'> Select files </Text>
|
||||
<Tabs type='card' tabBarGutter={5} onChange={(activeKey: string) => this.setState({
|
||||
active: activeKey as any,
|
||||
})}>
|
||||
{ this.renderLocalSelector() }
|
||||
{ this.renderShareSelector() }
|
||||
{ this.renderRemoteSelector() }
|
||||
</Tabs>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
import React from 'react';
|
||||
|
||||
import {
|
||||
Select,
|
||||
} from 'antd';
|
||||
|
||||
interface Props {
|
||||
value: string | null;
|
||||
users: any[];
|
||||
onChange: (user: string) => void;
|
||||
}
|
||||
|
||||
export default function UserSelector(props: Props) {
|
||||
return (
|
||||
<Select
|
||||
defaultValue={props.value ? props.value : '\0'}
|
||||
size='small'
|
||||
showSearch
|
||||
className='cvat-user-selector'
|
||||
onChange={props.onChange}
|
||||
>
|
||||
<Select.Option key='-1' value='\0'>{'\0'}</Select.Option>
|
||||
{ props.users.map((user) => {
|
||||
return (
|
||||
<Select.Option key={user.id} value={user.username}>
|
||||
{user.username}
|
||||
</Select.Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
);
|
||||
}
|
||||
@ -1,9 +1,48 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
export default function CreateTaskPageContainer() {
|
||||
import { CombinedState } from '../../reducers/root-reducer';
|
||||
import CreateTaskComponent from '../../components/create-task-page/create-task-page';
|
||||
import { CreateTaskData } from '../../components/create-task-page/create-task-content';
|
||||
import { createTaskAsync } from '../../actions/tasks-actions';
|
||||
|
||||
interface StateToProps {
|
||||
creatingError: string;
|
||||
status: string;
|
||||
installedGit: boolean;
|
||||
}
|
||||
|
||||
interface DispatchToProps {
|
||||
create: (data: CreateTaskData) => void;
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||
return {
|
||||
create: (data: CreateTaskData) => dispatch(createTaskAsync(data)),
|
||||
};
|
||||
}
|
||||
|
||||
function mapStateToProps(state: CombinedState): StateToProps {
|
||||
const { creates } = state.tasks.activities;
|
||||
return {
|
||||
...creates,
|
||||
installedGit: state.plugins.plugins.GIT_INTEGRATION,
|
||||
creatingError: creates.creatingError ? creates.creatingError.toString() : '',
|
||||
};
|
||||
}
|
||||
|
||||
function CreateTaskPageContainer(props: StateToProps & DispatchToProps) {
|
||||
return (
|
||||
<div>
|
||||
"Create Task Page"
|
||||
</div>
|
||||
<CreateTaskComponent
|
||||
error={props.creatingError}
|
||||
status={props.status}
|
||||
onCreate={props.create}
|
||||
installedGit={props.installedGit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
)(CreateTaskPageContainer);
|
||||
|
||||
@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { TreeNodeNormal } from 'antd/lib/tree/Tree'
|
||||
import FileManagerComponent, { Files } from '../../components/file-manager/file-manager';
|
||||
|
||||
import { loadShareDataAsync } from '../../actions/share-actions';
|
||||
import { ShareItem } from '../../reducers/interfaces';
|
||||
import { CombinedState } from '../../reducers/root-reducer';
|
||||
|
||||
interface StateToProps {
|
||||
treeData: TreeNodeNormal[];
|
||||
}
|
||||
|
||||
interface DispatchToProps {
|
||||
getTreeData(key: string, success: () => void, failure: () => void): void;
|
||||
}
|
||||
|
||||
function mapStateToProps(state: CombinedState): StateToProps {
|
||||
function convert(items: ShareItem[], path?: string): TreeNodeNormal[] {
|
||||
return items.map((item): TreeNodeNormal => {
|
||||
const key = `${path}/${item.name}`.replace(/\/+/g, '/'); // // => /
|
||||
return {
|
||||
key,
|
||||
title: item.name,
|
||||
isLeaf: item.type !== 'DIR',
|
||||
children: convert(item.children, key),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const { root } = state.share;
|
||||
return {
|
||||
treeData: convert(root.children, root.name),
|
||||
};
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||
return {
|
||||
getTreeData: (key: string, success: () => void, failure: () => void) => {
|
||||
dispatch(loadShareDataAsync(key, success, failure));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
class FileManagerContainer extends React.PureComponent<StateToProps & DispatchToProps> {
|
||||
private managerComponentRef: any;
|
||||
|
||||
public getFiles(): Files {
|
||||
return this.managerComponentRef.getFiles();
|
||||
}
|
||||
|
||||
public reset(): Files {
|
||||
return this.managerComponentRef.reset();
|
||||
}
|
||||
|
||||
public render() {
|
||||
return (
|
||||
<FileManagerComponent
|
||||
treeData={this.props.treeData}
|
||||
onLoadData={this.props.getTreeData}
|
||||
ref={(component) => this.managerComponentRef = component}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect(
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
null,
|
||||
{ forwardRef: true },
|
||||
)(FileManagerContainer);
|
||||
@ -0,0 +1,61 @@
|
||||
import { AnyAction } from 'redux';
|
||||
|
||||
import { ShareActionTypes } from '../actions/share-actions';
|
||||
import { ShareState, ShareFileInfo, ShareItem } from './interfaces';
|
||||
|
||||
const defaultState: ShareState = {
|
||||
root: {
|
||||
name: '/',
|
||||
type: 'DIR',
|
||||
children: [],
|
||||
},
|
||||
error: null,
|
||||
};
|
||||
|
||||
export default function (state = defaultState, action: AnyAction): ShareState {
|
||||
switch (action.type) {
|
||||
case ShareActionTypes.LOAD_SHARE_DATA: {
|
||||
return {
|
||||
...state,
|
||||
error: null,
|
||||
};
|
||||
}
|
||||
case ShareActionTypes.LOAD_SHARE_DATA_SUCCESS: {
|
||||
const { values } = action.payload;
|
||||
const { directory } = action.payload;
|
||||
|
||||
// Find directory item in storage
|
||||
let dir = state.root;
|
||||
for (const dirName of directory.split('/')) {
|
||||
if (dirName) {
|
||||
[dir] = dir.children.filter(
|
||||
(child): boolean => child.name === dirName,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Update its children
|
||||
dir.children = (values as ShareFileInfo[])
|
||||
.map((value): ShareItem => ({
|
||||
...value,
|
||||
children: [],
|
||||
}));
|
||||
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
}
|
||||
case ShareActionTypes.LOAD_SHARE_DATA_FAILED: {
|
||||
const { error } = action.payload;
|
||||
|
||||
return {
|
||||
...state,
|
||||
error,
|
||||
};
|
||||
}
|
||||
default:
|
||||
return {
|
||||
...state,
|
||||
};
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue