提交 3b6961f4 编写于 作者: B Boris Sekachev 提交者: Nikita Manovich

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 window
上级 690ecf9c
......@@ -1205,6 +1205,7 @@
if (this.id) {
const jobData = {
status: this.status,
assignee: this.assignee ? this.assignee.id : null,
};
await serverProxy.jobs.saveJob(this.id, jobData);
......
......@@ -1042,6 +1042,14 @@
"@types/react-router": "*"
}
},
"@types/react-share": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@types/react-share/-/react-share-3.0.1.tgz",
"integrity": "sha512-9SmC9TBOBKXvu7Otfl0Zl/1OqjPocmhEMwJnVspG3i1AW/wyKkemkL+Jshpp+tSxfcz76BbbzlBJVbsojYZHvA==",
"requires": {
"@types/react": "*"
}
},
"@types/react-slick": {
"version": "0.23.4",
"resolved": "https://registry.npmjs.org/@types/react-slick/-/react-slick-0.23.4.tgz",
......@@ -5728,6 +5736,29 @@
"minimist": "^1.2.0"
}
},
"jsonp": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/jsonp/-/jsonp-0.2.1.tgz",
"integrity": "sha1-pltPoPEL2nGaBUQep7lMVfPhW64=",
"requires": {
"debug": "^2.1.3"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g="
}
}
},
"jsx-ast-utils": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-2.2.3.tgz",
......@@ -7938,6 +7969,17 @@
"tiny-warning": "^1.0.0"
}
},
"react-share": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/react-share/-/react-share-3.0.1.tgz",
"integrity": "sha512-xo4zjYP78h6zrBN5rlC06bb877js7216KFeZELAZP6sYxVoqmU27ChrfnpKUCL9H8F5PwYXh6DLNdAp+0E17GA==",
"requires": {
"babel-runtime": "^6.26.0",
"classnames": "^2.2.5",
"jsonp": "^0.2.1",
"prop-types": "^15.5.8"
}
},
"react-slick": {
"version": "0.25.2",
"resolved": "https://registry.npmjs.org/react-slick/-/react-slick-0.25.2.tgz",
......
......@@ -38,6 +38,7 @@
"@types/react-redux": "^7.1.2",
"@types/react-router": "^5.0.5",
"@types/react-router-dom": "^5.1.0",
"@types/react-share": "^3.0.1",
"antd": "^3.24.2",
"dotenv-webpack": "^1.7.0",
"moment": "^2.24.0",
......@@ -47,6 +48,7 @@
"react-redux": "^7.1.1",
"react-router": "^5.1.0",
"react-router-dom": "^5.1.0",
"react-share": "^3.0.1",
"redux": "^4.0.4",
"redux-thunk": "^2.3.0"
}
......
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();
}
};
}
......@@ -109,9 +109,34 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
let task = null;
try {
[task] = await core.tasks.get({ id: taskInstance.id });
} catch (_) {
// server error?
} catch (fetchError) {
dispatch(updateTaskFailed(error, taskInstance));
return;
}
dispatch(updateTaskFailed(error, task));
}
};
}
// a job is a part of a task, so for simplify we consider
// updating the job as updating a task
export function updateJobAsync(jobInstance: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
dispatch(updateTask());
await jobInstance.save();
const [task] = await core.tasks.get({ id: jobInstance.task.id });
dispatch(updateTaskSuccess(task));
} catch (error) {
// try abort all changes
let task = null;
try {
[task] = await core.tasks.get({ id: jobInstance.task.id });
} catch (fetchError) {
dispatch(updateTaskFailed(error, jobInstance.task));
return;
}
dispatch(updateTaskFailed(error, task));
......
......@@ -19,6 +19,10 @@ export enum TasksActionTypes {
DELETE_TASK = 'DELETE_TASK',
DELETE_TASK_SUCCESS = 'DELETE_TASK_SUCCESS',
DELETE_TASK_FAILED = 'DELETE_TASK_FAILED',
CREATE_TASK = 'CREATE_TASK',
CREATE_TASK_STATUS_UPDATED = 'CREATE_TASK_STATUS_UPDATED',
CREATE_TASK_SUCCESS = 'CREATE_TASK_SUCCESS',
CREATE_TASK_FAILED = 'CREATE_TASK_FAILED',
}
function getTasks(): AnyAction {
......@@ -251,3 +255,92 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
dispatch(deleteTaskSuccess(taskInstance.id));
};
}
function createTask(): AnyAction {
const action = {
type: TasksActionTypes.CREATE_TASK,
payload: {},
};
return action;
}
function createTaskSuccess(): AnyAction {
const action = {
type: TasksActionTypes.CREATE_TASK_SUCCESS,
payload: {},
};
return action;
}
function createTaskFailed(error: any): AnyAction {
const action = {
type: TasksActionTypes.CREATE_TASK_FAILED,
payload: {
error,
},
};
return action;
}
function createTaskUpdateStatus(status: string): AnyAction {
const action = {
type: TasksActionTypes.CREATE_TASK_STATUS_UPDATED,
payload: {
status,
},
};
return action;
}
export function createTaskAsync(data: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
const description: any = {
name: data.basic.name,
labels: data.labels,
z_order: data.advanced.zOrder,
image_quality: 70,
};
if (data.advanced.bugTracker) {
description.bug_tracker = data.advanced.bugTracker;
}
if (data.advanced.segmentSize) {
description.segment_size = data.advanced.segmentSize;
}
if (data.advanced.overlapSize) {
description.overlap = data.advanced.overlapSize;
}
if (data.advanced.startFrame) {
description.start_frame = data.advanced.startFrame;
}
if (data.advanced.stopFrame) {
description.stop_frame = data.advanced.stopFrame;
}
if (data.advanced.frameFilter) {
description.frame_filter = data.advanced.frameFilter;
}
if (data.advanced.imageQuality) {
description.image_quality = data.advanced.imageQuality;
}
const taskInstance = new cvat.classes.Task(description);
taskInstance.clientFiles = data.files.local;
taskInstance.serverFiles = data.files.share;
taskInstance.remoteFiles = data.files.remote;
dispatch(createTask());
try {
await taskInstance.save((status: string): void => {
dispatch(createTaskUpdateStatus(status));
});
dispatch(createTaskSuccess());
} catch (error) {
dispatch(createTaskFailed(error));
}
};
}
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);
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);
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>
);
}
}
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>
);
}
......@@ -17,6 +17,8 @@ import LoginPageContainer from '../containers/login-page/login-page';
import RegisterPageContainer from '../containers/register-page/register-page';
import HeaderContainer from '../containers/header/header';
import FeedbackComponent from './feedback';
type CVATAppProps = {
loadFormats: () => void;
loadUsers: () => void;
......@@ -110,6 +112,7 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
<Route path='/tasks/:id/jobs/:id' component={AnnotationPageContainer}/>
<Redirect to='/tasks'/>
</Switch>
<FeedbackComponent/>
</Layout.Content>
</Layout>
</BrowserRouter>
......@@ -127,7 +130,7 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
}
} else {
return (
<Spin size="large" style={{margin: '25% 50%'}}/>
<Spin size='large' style={{margin: '25% 50%'}}/>
);
}
}
......
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>
</>
);
}
}
\ No newline at end of file
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>
</>
);
}
}
......@@ -5,7 +5,6 @@ import { withRouter } from 'react-router-dom';
import {
Layout,
Radio,
Icon,
Button,
Menu,
......@@ -50,41 +49,44 @@ function HeaderContainer(props: HeaderContainerProps & RouteComponentProps) {
<div className='cvat-left-header'>
<Icon className='cvat-logo-icon' component={cvatLogo}/>
<Radio.Group size='default' value={activeTab} className='cvat-header-buttons'>
<Radio.Button value='tasks'onChange={
() => props.history.push('/tasks')
}> Tasks </Radio.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>
<Button className='cvat-header-button' type='link' value='tasks' onClick={
() => props.history.push('/tasks')
}> Tasks </Button>
{ props.installedAutoAnnotation ?
<Button className='cvat-header-button' type='link' value='models' onClick={
() => props.history.push('/models')
}> Models </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
}
</div>
<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>
}>
<Icon type='github'/>
<Text className='cvat-black-color'>GitHub</Text>
</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>
}> <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>
<Text strong>
{props.username.length > 14 ? `${props.username.slice(0, 10)} ...` : props.username}
</Text>
<Icon className='cvat-header-menu-icon' type='caret-down'/>
</span>
</span>
......
......@@ -370,7 +370,7 @@ class LabelForm extends React.PureComponent<Props, State> {
private renderDoneButton() {
return (
<Col span={4}>
<Col>
<Tooltip overlay='Save the label and return'>
<Button
style={{width: '150px'}}
......@@ -388,7 +388,7 @@ class LabelForm extends React.PureComponent<Props, State> {
private renderContinueButton() {
return (
this.props.label ? <div/> :
<Col span={4}>
<Col offset={1}>
<Tooltip overlay='Save the label and create one more'>
<Button
style={{width: '150px'}}
......@@ -405,7 +405,7 @@ class LabelForm extends React.PureComponent<Props, State> {
private renderCancelButton() {
return (
<Col span={4}>
<Col offset={1}>
<Tooltip overlay='Do not save the label and return'>
<Button
style={{width: '150px'}}
......@@ -446,9 +446,7 @@ class LabelForm extends React.PureComponent<Props, State> {
{ attributeItems.reverse() }
<Row type='flex' justify='start' align='middle'>
{ this.renderDoneButton() }
<Col span={1}/>
{ this.renderContinueButton() }
<Col span={1}/>
{ this.renderCancelButton() }
</Row>
</Form>
......
......@@ -12,20 +12,15 @@ import {
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;
}
......@@ -33,21 +28,7 @@ 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,
};
}
......@@ -72,7 +53,20 @@ class RawViewer extends React.PureComponent<Props, State> {
}
public render() {
const textLabels = JSON.stringify(this.state.labels, null, 2);
const labels = this.props.labels.map((label: any) => {
return {
...label,
id: label.id < 0 ? undefined : label.id,
attributes: label.attributes.map((attribute: any) => {
return {
...attribute,
id: attribute.id < 0 ? undefined : attribute.id,
};
}),
};
});
const textLabels = JSON.stringify(labels, null, 2);
return (
<Form onSubmit={this.handleSubmit}>
......@@ -85,7 +79,7 @@ class RawViewer extends React.PureComponent<Props, State> {
})( <Input.TextArea rows={5} className='cvat-raw-labels-viewer'/> )
} </Form.Item>
<Row type='flex' justify='start' align='middle'>
<Col span={4}>
<Col>
<Tooltip overlay='Save labels and return'>
<Button
style={{width: '150px'}}
......@@ -94,8 +88,7 @@ class RawViewer extends React.PureComponent<Props, State> {
> Done </Button>
</Tooltip>
</Col>
<Col span={1}/>
<Col span={4}>
<Col offset={1}>
<Tooltip overlay='Do not save the label and return'>
<Button
style={{width: '150px'}}
......
......@@ -5,7 +5,6 @@ import {
Col,
Modal,
Button,
Select,
} from 'antd';
import Text from 'antd/lib/typography/Text';
......@@ -13,6 +12,7 @@ import Title from 'antd/lib/typography/Title';
import moment from 'moment';
import UserSelector from './user-selector';
import LabelsEditorComponent from '../labels-editor/labels-editor';
import getCore from '../../core';
import patterns from '../../utils/validation-patterns';
......@@ -30,7 +30,6 @@ interface Props {
interface State {
name: string;
bugTracker: string;
assignee: any;
}
export default class DetailsComponent extends React.PureComponent<Props, State> {
......@@ -42,7 +41,6 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
this.state = {
name: taskInstance.name,
bugTracker: taskInstance.bugTracker,
assignee: taskInstance.assignee,
};
}
......@@ -115,15 +113,13 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
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 assignee = taskInstance.assignee ? taskInstance.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) => {
const assigneeSelect = <UserSelector
users={this.props.registeredUsers}
value={assignee}
onChange={
(value: string) => {
let [userInstance] = this.props.registeredUsers
.filter((user: any) => user.username === value);
......@@ -131,24 +127,11 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
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'>
......@@ -241,7 +224,6 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
this.setState({
name: this.props.taskInstance.name,
bugTracker: this.props.taskInstance.bugTracker,
assignee: this.props.taskInstance.assignee,
});
}
}
......
......@@ -3,7 +3,6 @@ import React from 'react';
import {
Row,
Col,
Icon,
Table,
} from 'antd';
......@@ -12,13 +11,17 @@ import Title from 'antd/lib/typography/Title';
import moment from 'moment';
import UserSelector from './user-selector';
import getCore from '../../core';
const core = getCore();
const baseURL = core.config.backendAPI.slice(0, -7);
interface Props {
taskInstance: any;
registeredUsers: any[];
onJobUpdate(jobInstance: any): void;
}
export default function JobListComponent(props: Props) {
......@@ -63,7 +66,26 @@ export default function JobListComponent(props: Props) {
title: 'Assignee',
dataIndex: 'assignee',
key: 'assignee',
className: 'cvat-black-color',
render: (jobInstance: any) => {
const assignee = jobInstance.assignee ? jobInstance.assignee.username : null
return (
<UserSelector
users={props.registeredUsers}
value={assignee}
onChange={(value: string) => {
let [userInstance] = props.registeredUsers
.filter((user: any) => user.username === value);
if (userInstance === undefined) {
userInstance = null;
}
jobInstance.assignee = userInstance;
props.onJobUpdate(jobInstance);
}}
/>
);
},
}];
let completed = 0;
......@@ -81,7 +103,7 @@ export default function JobListComponent(props: Props) {
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 : ''}`,
assignee: job,
});
return acc;
......
......@@ -63,7 +63,7 @@ class TaskPageComponent extends React.PureComponent<Props> {
)
} else {
return (
<Row type='flex' justify='center' align='middle'>
<Row type='flex' justify='center' align='top' className='cvat-task-details-wrapper'>
<Col md={22} lg={18} xl={16} xxl={14}>
<TopBarContainer/>
<DetailsContainer/>
......
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>
);
}
\ No newline at end of file
......@@ -171,11 +171,11 @@ class TasksPageComponent extends React.PureComponent<TasksPageProps & RouteCompo
public render() {
if (this.props.tasksAreBeingFetched) {
return (
<Spin size='large' style={{margin: '25% 50%'}}/>
<Spin size='large' style={{margin: '25% 45%'}}/>
);
} else {
return (
<div className='tasks-page'>
<div className='cvat-tasks-page'>
<TopBar
onSearch={this.handleSearch}
searchValue={this.getSearchField(this.props.gettingQuery)}
......
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);
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);
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';
import { updateJobAsync } from '../../actions/task-actions';
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;
registeredUsers: any[];
}
interface DispatchToProps {
fetchTask: (tid: number) => void;
deleteTask: (taskInstance: any) => void;
dumpAnnotations: (task: any, format: string) => void;
loadAnnotations: (task: any, format: string, file: File) => void;
onJobUpdate(jobInstance: any): 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,
taskInstance: state.activeTask.task ? state.activeTask.task.instance : null,
registeredUsers: state.users.users,
};
}
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));
},
onJobUpdate: (jobInstance: any) => dispatch(updateJobAsync(jobInstance)),
};
}
......@@ -89,6 +31,8 @@ function TaskPageContainer(props: StateToProps & DispatchToProps) {
return (
<JobListComponent
taskInstance={props.taskInstance}
registeredUsers={props.registeredUsers}
onJobUpdate={props.onJobUpdate}
/>
);
}
......
......@@ -4,14 +4,11 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>CVAT SPA</title>
<title>Computer Vision Annotation Tool</title>
</head>
<body>
<div id="root" style="width: 100%; height: initial; min-height: 100%; display: grid;">
</div>
<div id="root"></div>
<script src="/cvat-ui.min.js" type="text/javascript"></script>
</body>
</html>
\ No newline at end of file
......@@ -52,6 +52,10 @@ export interface TasksState {
[tid: number]: boolean; // deleted (deleting if in dictionary)
};
};
creates: {
creatingError: any;
status: string;
};
};
}
......@@ -88,3 +92,19 @@ export interface UsersState {
initialized: boolean;
gettingUsersError: any;
}
export interface ShareFileInfo { // get this data from cvat-core
name: string;
type: 'DIR' | 'REG';
}
export interface ShareItem {
name: string;
type: 'DIR' | 'REG';
children: ShareItem[];
}
export interface ShareState {
root: ShareItem;
error: any;
}
......@@ -2,6 +2,7 @@ import { combineReducers, Reducer } from 'redux';
import authReducer from './auth-reducer';
import tasksReducer from './tasks-reducer';
import usersReducer from './users-reducer';
import shareReducer from './share-reducer';
import formatsReducer from './formats-reducer';
import pluginsReducer from './plugins-reducer';
import taskReducer from './task-reducer';
......@@ -10,6 +11,7 @@ import {
AuthState,
TasksState,
UsersState,
ShareState,
FormatsState,
PluginsState,
TaskState,
......@@ -19,6 +21,7 @@ export interface CombinedState {
auth: AuthState;
tasks: TasksState;
users: UsersState;
share: ShareState;
formats: FormatsState;
plugins: PluginsState;
activeTask: TaskState;
......@@ -29,6 +32,7 @@ export default function createRootReducer(): Reducer {
auth: authReducer,
tasks: tasksReducer,
users: usersReducer,
share: shareReducer,
formats: formatsReducer,
plugins: pluginsReducer,
activeTask: taskReducer,
......
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,
};
}
}
......@@ -32,6 +32,10 @@ const defaultState: TasksState = {
deletingError: null,
byTask: {},
},
creates: {
creatingError: null,
status: '',
},
},
};
......@@ -283,6 +287,58 @@ export default (inputState: TasksState = defaultState, action: AnyAction): Tasks
},
};
}
case TasksActionTypes.CREATE_TASK: {
return {
...state,
activities: {
...state.activities,
creates: {
creatingError: null,
status: '',
},
},
};
}
case TasksActionTypes.CREATE_TASK_STATUS_UPDATED: {
const { status } = action.payload;
return {
...state,
activities: {
...state.activities,
creates: {
...state.activities.creates,
status,
},
},
};
}
case TasksActionTypes.CREATE_TASK_SUCCESS: {
return {
...state,
activities: {
...state.activities,
creates: {
...state.activities.creates,
status: 'CREATED',
},
},
};
}
case TasksActionTypes.CREATE_TASK_FAILED: {
const { error } = action.payload;
return {
...state,
activities: {
...state.activities,
creates: {
...state.activities.creates,
creatingError: error,
},
},
};
}
default:
return state;
}
......
#root {
width: 100%;
height: 100%;
min-height: 100%;
display: grid;
}
.cvat-header.ant-layout-header {
display: flex;
padding-left: 0px;
......@@ -33,36 +40,17 @@
color: black;
}
.cvat-header-buttons.ant-radio-group {
height: 100%;
display: flex;
}
.cvat-header-buttons.ant-radio-group > label {
height: 100%;
background: rgba(255, 255, 255, 0);
border-radius: 0px;
display: flex;
align-items: center;
padding: 0 30px;
/* Important here is used in order to redefine all states :hover, :active, :focus-within, etc */
user-select: none !important;
border: none !important;
border-color: none !important;
box-shadow: none !important;
outline: none !important;
color: black !important;
.cvat-feedback-button {
position: absolute;
bottom: 20px;
right: 20px;
padding: 0px;
}
.cvat-header-buttons.ant-radio-group > label::before {
background-color: unset !important;
.cvat-feedback-button > i {
font-size: 40px;
}
.cvat-header-buttons.ant-radio-group > label.ant-radio-button-wrapper-checked {
height: 100%;
background: #C3C3C3;
}
.anticon.cvat-logo-icon {
display: flex;
align-items: center;
......@@ -87,17 +75,11 @@
}
.ant-btn.cvat-header-button {
height: 100%;
background: rgba(255, 255, 255, 0);
border-radius: 0px;
color: black !important;
transition: color 0.5s;
padding: 0 30px;
padding: 0px 10px;
margin-right: 10px;
}
.ant-btn.cvat-header-button:active {
background: #C3C3C3;
}
.ant-menu.cvat-header-menu {
width: fit-content;
......@@ -126,9 +108,13 @@
margin: 16px;
}
.anticon.cvat-header-menu-icon {
margin-left: 16px;
margin-right: 0px;
}
.anticon.cvat-header-menu-icon > img {
width: 14px;
margin: 10px;
transform: rotate(-90deg);
}
......@@ -143,72 +129,72 @@
padding-top: 5px;
}
.tasks-page {
.cvat-tasks-page {
padding-top: 30px;
}
.tasks-page > div:nth-child(1) {
.cvat-tasks-page > div:nth-child(1) {
margin-bottom: 10px;
}
.tasks-page > div:nth-child(1) > div > span {
.cvat-tasks-page > div:nth-child(1) > div > span {
color: black;
}
.tasks-page > div:nth-child(2) > div:nth-child(1) {
.cvat-tasks-page > div:nth-child(2) > div:nth-child(1) {
display: flex;
}
.tasks-page > div:nth-child(2) > div:nth-child(2) {
.cvat-tasks-page > div:nth-child(2) > div:nth-child(2) {
display: flex;
justify-content: flex-end;
}
.tasks-page > div:nth-child(2) > div:nth-child(1) > span:nth-child(2) {
.cvat-tasks-page > div:nth-child(2) > div:nth-child(1) > span:nth-child(2) {
width: 200px;
margin-left: 10px;
}
.tasks-page > span.ant-typography {
.cvat-tasks-page > span.ant-typography {
user-select: none;
}
.tasks-page {
.cvat-tasks-page {
height: 100%;
}
.tasks-page > div:nth-child(4) {
margin-top: 28px;
.cvat-tasks-page > div:nth-child(4) {
margin-top: 10px;
}
.tasks-page > div:nth-child(3) {
margin-top: 28px;
.cvat-tasks-page > div:nth-child(3) {
margin-top: 10px;
}
/* > 1280 */
@media only screen and (min-height: 1280px) {
.tasks-page > div:nth-child(3) {
.cvat-tasks-page > div:nth-child(3) {
height: 80%;
}
}
/* 769 => 1280 */
@media only screen and (max-height: 1280px) {
.tasks-page > div:nth-child(3) {
.cvat-tasks-page > div:nth-child(3) {
height: 80%;
}
}
/* 0 => 768 */
@media only screen and (max-height: 768px) {
.tasks-page > div:nth-child(3) {
.cvat-tasks-page > div:nth-child(3) {
height: 70%;
}
}
/* empty-tasks icon */
.cvat-empty-task-list > div:nth-child(1) {
margin-top: 100px;
margin-top: 50px;
}
.cvat-empty-task-list > div:nth-child(2) > div {
......@@ -326,6 +312,11 @@
margin-right: 5px;
}
.cvat-task-details-wrapper {
overflow-y: auto;
height: 100%;
}
.cvat-task-details {
width: 100%;
height: auto;
......@@ -370,7 +361,7 @@
margin-bottom: 20px;
}
.cvat-task-assignee-selector {
.cvat-user-selector {
margin-left: 10px;
width: 150px;
}
......@@ -491,6 +482,44 @@
color: #C1C1C1;
}
.cvat-create-task-form-wrapper {
text-align: center;
margin-top: 40px;
overflow-y: auto;
height: 90%;
}
.cvat-create-task-form-wrapper > div > span {
font-size: 36px;
}
.cvat-create-task-content {
margin-top: 20px;
width: 100%;
height: auto;
border: 1px solid #c3c3c3;
border-radius: 3px;
padding: 20px;
background: white;
text-align: initial;
}
.cvat-create-task-content > div:not(first-child) {
margin-top: 10px;
}
.cvat-create-task-content > div:nth-child(7) > button {
float: right;
width: 120px;
}
.cvat-share-tree {
height: fit-content;
min-height: 10em;
max-height: 20em;
overflow: auto;
}
#cvat-create-task-button {
padding: 0 30px;
}
......@@ -251,7 +251,7 @@ def _create_thread(tid, data):
_copy_data_from_share(data['server_files'], upload_dir)
job = rq.get_current_job()
job.meta['status'] = 'Media files is being extracted...'
job.meta['status'] = 'Media files are being extracted...'
job.save_meta()
db_images = []
......
......@@ -287,3 +287,4 @@ window.addEventListener('DOMContentLoaded', () => {
window.cvat.plugins.register(gitPlugin);
});
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册