diff --git a/cvat-core/src/session.js b/cvat-core/src/session.js index b0bc69a8ea74f327fde7494e21d134e542209764..aa681c0e30595f23b2b55fd9b8ab06e701ac2923 100644 --- a/cvat-core/src/session.js +++ b/cvat-core/src/session.js @@ -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); diff --git a/cvat-ui/package-lock.json b/cvat-ui/package-lock.json index 5073a3ee154c991718d67e59646d1a83e33c050e..cdde84574394b4e28e9aa58747c28af69b4507b9 100644 --- a/cvat-ui/package-lock.json +++ b/cvat-ui/package-lock.json @@ -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", diff --git a/cvat-ui/package.json b/cvat-ui/package.json index 441b4b5d4b3bac022f9da06e1f5e1fde7daa7ac6..5781b1aa7c7cae7be1be73012ffdbf059e47e0da 100644 --- a/cvat-ui/package.json +++ b/cvat-ui/package.json @@ -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" } diff --git a/cvat-ui/src/actions/share-actions.ts b/cvat-ui/src/actions/share-actions.ts new file mode 100644 index 0000000000000000000000000000000000000000..47fbcb9014589cbb23e4a223711faea01c886250 --- /dev/null +++ b/cvat-ui/src/actions/share-actions.ts @@ -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, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + try { + dispatch(loadShareData()); + const values = await core.server.share(directory); + success(); + dispatch(loadShareDataSuccess(values as ShareFileInfo[], directory)); + } catch (error) { + dispatch(loadShareDataFailed(error)); + failure(); + } + }; +} diff --git a/cvat-ui/src/actions/task-actions.ts b/cvat-ui/src/actions/task-actions.ts index 82bc1adf28cb49e79c9329db2a085417efbf7a80..063b60223b6c667743ece9efbcd9126d418fd931 100644 --- a/cvat-ui/src/actions/task-actions.ts +++ b/cvat-ui/src/actions/task-actions.ts @@ -109,9 +109,34 @@ ThunkAction, {}, {}, 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, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + 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)); diff --git a/cvat-ui/src/actions/tasks-actions.ts b/cvat-ui/src/actions/tasks-actions.ts index b378a90bf9706bb7e28fcc3695c8d87a195ae378..46d92c0c0995b39c6646ab4b8af1ba4ffaf21f95 100644 --- a/cvat-ui/src/actions/tasks-actions.ts +++ b/cvat-ui/src/actions/tasks-actions.ts @@ -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, {}, {}, 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, {}, {}, AnyAction> { + return async (dispatch: ActionCreator): Promise => { + 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)); + } + }; +} diff --git a/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx new file mode 100644 index 0000000000000000000000000000000000000000..a4a17547cd9d34312d99c69425519478504a1434 --- /dev/null +++ b/cvat-ui/src/components/create-task-page/advanced-configuration-form.tsx @@ -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 { + 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 ( + + + {this.props.form.getFieldDecorator('zOrder', { + initialValue: false, + valuePropName: 'checked', + })( + + + Z-order + + + )} + + + ); + } + + private renderImageQuality() { + return ( + + + Image quality + {this.props.form.getFieldDecorator('imageQuality', { + initialValue: 70, + rules: [{ + required: true, + message: 'This field is required' + }], + })( + } + /> + )} + + + ); + } + + private renderOverlap() { + return ( + + + Overlap size + {this.props.form.getFieldDecorator('overlapSize')( + + )} + + + ); + } + + private renderSegmentSize() { + return ( + + + Segment size + {this.props.form.getFieldDecorator('segmentSize')( + + )} + + + ); + } + + private renderStartFrame() { + return ( + + Start frame + {this.props.form.getFieldDecorator('startFrame')( + + )} + + ); + } + + private renderStopFrame() { + return ( + + Stop frame + {this.props.form.getFieldDecorator('stopFrame')( + + )} + + ); + } + + private renderFrameStep() { + return ( + + Frame step + {this.props.form.getFieldDecorator('frameStep')( + + )} + + ); + } + + private renderGitLFSBox() { + return ( + + + {this.props.form.getFieldDecorator('lfs', { + valuePropName: 'checked', + initialValue: false, + })( + + + Use LFS (Large File Support) + + + )} + + + ); + } + + private renderGitRepositoryURL() { + return ( + + + Dataset repository URL + {this.props.form.getFieldDecorator('repository', { + // TODO: Add pattern + })( + + )} + + + ); + } + + private renderGit() { + return ( + <> + + + {this.renderGitRepositoryURL()} + + + + + + {this.renderGitLFSBox()} + + + + ); + } + + private renderBugTracker() { + return ( + + + Issue tracker + {this.props.form.getFieldDecorator('bugTracker', { + rules: [{ + ...patterns.validateURL, + }] + })( + + )} + + + ) + } + + public render() { + return ( +
+ + {this.renderZOrder()} + + + + + {this.renderImageQuality()} + + + {this.renderOverlap()} + + + {this.renderSegmentSize()} + + + + + + {this.renderStartFrame()} + + + {this.renderStopFrame()} + + + {this.renderFrameStep()} + + + + { this.props.installedGit ? this.renderGit() : null} + + + + {this.renderBugTracker()} + + +
+ ); + } +} + +export default Form.create()(AdvancedConfigurationForm); diff --git a/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx b/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5ffa9a4b096a37bca2c09b5a77c07ee52307f316 --- /dev/null +++ b/cvat-ui/src/components/create-task-page/basic-configuration-form.tsx @@ -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 { + 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 ( +
e.preventDefault()}> + Name + + { getFieldDecorator('name', { + rules: [{ + required: true, + message: 'Please, specify a name', + }] // TODO: Add task name pattern + })( + + ) } + +
+ ); + } +} + +export default Form.create()(BasicConfigurationForm); diff --git a/cvat-ui/src/components/create-task-page/create-task-content.tsx b/cvat-ui/src/components/create-task-page/create-task-content.tsx new file mode 100644 index 0000000000000000000000000000000000000000..4274bd9f87daccdeea9c68ef8203e91c16f78da8 --- /dev/null +++ b/cvat-ui/src/components/create-task-page/create-task-content.tsx @@ -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 { + 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 ( + + { this.basicConfigurationComponent = component } + } onSubmit={this.handleSubmitBasicConfiguration}/> + + ); + } + + private renderLabelsBlock() { + return ( + + Labels + { + this.setState({ + labels, + }); + } + } + /> + + ); + } + + private renderFilesBlock() { + return ( + + + this.fileManagerContainer = container + }/> + + ); + } + + private renderAdvancedBlock() { + return ( + + + Advanced configuration + } key='1'> + { + this.advancedConfigurationComponent = component + } + } + onSubmit={this.handleSubmitAdvancedConfiguration} + /> + + + + ); + } + + 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 ( + + + Basic configuration + + + { this.renderBasicBlock() } + { this.renderLabelsBlock() } + { this.renderFilesBlock() } + { this.renderAdvancedBlock() } + + + {loading ? : null} + + + + + + ); + } +} diff --git a/cvat-ui/src/components/create-task-page/create-task-page.tsx b/cvat-ui/src/components/create-task-page/create-task-page.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6271f1a41073b026cf984a8a050d5e00b1d2b5da --- /dev/null +++ b/cvat-ui/src/components/create-task-page/create-task-page.tsx @@ -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 ( + + + Create a new task + + + + ); +} diff --git a/cvat-ui/src/components/cvat-app.tsx b/cvat-ui/src/components/cvat-app.tsx index 45b980771e9ba815ef0100cddbfe0b0192136e1e..6d49cfc28057e5ce5c333920e9efdfe24ec36db7 100644 --- a/cvat-ui/src/components/cvat-app.tsx +++ b/cvat-ui/src/components/cvat-app.tsx @@ -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 { + @@ -127,7 +130,7 @@ export default class CVATApplication extends React.PureComponent { } } else { return ( - + ); } } diff --git a/cvat-ui/src/components/feedback.tsx b/cvat-ui/src/components/feedback.tsx new file mode 100644 index 0000000000000000000000000000000000000000..bd00ea8155ccbabcbdf46803991318a577841944 --- /dev/null +++ b/cvat-ui/src/components/feedback.tsx @@ -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 ( + <> + + + Star us on GitHub + +
+ + + Left a feedback + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + Do you need help? Contact us on gitter + + + ); + } + + public render() { + return ( + <> + Help to make CVAT better + } + content={this.renderContent()} + visible={this.state.active} + > + + + + ); + } +} \ No newline at end of file diff --git a/cvat-ui/src/components/file-manager/file-manager.tsx b/cvat-ui/src/components/file-manager/file-manager.tsx new file mode 100644 index 0000000000000000000000000000000000000000..150757db7657b7caf53552406cf77242c966ac59 --- /dev/null +++ b/cvat-ui/src/components/file-manager/file-manager.tsx @@ -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 { + 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((resolve, reject) => { + const success = () => resolve(); + const failure = () => reject(); + this.props.onLoadData(key, success, failure); + }); + + return promise; + } + + private renderLocalSelector() { + return ( + + { + this.setState({ + files: { + ...this.state.files, + local: files + }, + }); + return false; + } + }> +

+ +

+

Click or drag files to this area

+

+ Support for a bulk images or a single video +

+
+ { this.state.files.local.length ? + <> +
+ + {this.state.files.local.length} file(s) selected + + : null + } +
+ ); + } + + private renderShareSelector() { + function renderTreeNodes(data: TreeNodeNormal[]) { + return data.map((item: TreeNodeNormal) => { + if (item.children) { + return ( + + {renderTreeNodes(item.children)} + + ); + } + + return ; + }); + } + + return ( + + { this.props.treeData.length ? + { + 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) } + : No data found + } + + ); + } + + private renderRemoteSelector() { + return ( + + ) => { + this.setState({ + files: { + ...this.state.files, + remote: event.target.value.split('\n'), + }, + }); + }}/> + + ); + } + + 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 ( + <> + Select files + this.setState({ + active: activeKey as any, + })}> + { this.renderLocalSelector() } + { this.renderShareSelector() } + { this.renderRemoteSelector() } + + + ); + } +} diff --git a/cvat-ui/src/components/header/header.tsx b/cvat-ui/src/components/header/header.tsx index 4bd4658ac4d980f7e4d648f4eb8cbed68cbdec0b..ff02f6b81caffdb4e4c6c2b417f0040c893a90c7 100644 --- a/cvat-ui/src/components/header/header.tsx +++ b/cvat-ui/src/components/header/header.tsx @@ -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) {
- - props.history.push('/tasks') - }> Tasks - { props.installedAutoAnnotation ? - props.history.push('/models') - }> Models : null - } - { props.installedAnalytics ? - : null - } - + + { props.installedAutoAnnotation ? + : null + } + { props.installedAnalytics ? + : null + }
+ }> + + GitHub + + }> Help - {props.username} + + {props.username.length > 14 ? `${props.username.slice(0, 10)} ...` : props.username} + diff --git a/cvat-ui/src/components/labels-editor/label-form.tsx b/cvat-ui/src/components/labels-editor/label-form.tsx index 7859625913d43687347c468b8a1e674a9d7ec963..09e526acd88f9b5d320d0acf1dc9e48b37b64a97 100644 --- a/cvat-ui/src/components/labels-editor/label-form.tsx +++ b/cvat-ui/src/components/labels-editor/label-form.tsx @@ -370,7 +370,7 @@ class LabelForm extends React.PureComponent { private renderDoneButton() { return ( - + - - +