User interface with React and antd (#785)
* Dump & refactoring * Upload annotations, cvat-core from sources * Added download icon * Added iconmain
parent
4361bc548c
commit
5f511b7543
@ -1,5 +1,5 @@
|
|||||||
dist
|
/dist
|
||||||
docs
|
/docs
|
||||||
node_modules
|
/node_modules
|
||||||
reports
|
/reports
|
||||||
|
|
||||||
|
|||||||
@ -1 +1 @@
|
|||||||
node_modules
|
/node_modules
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
File diff suppressed because one or more lines are too long
@ -0,0 +1,43 @@
|
|||||||
|
import { AnyAction, Dispatch, ActionCreator } from 'redux';
|
||||||
|
import { ThunkAction } from 'redux-thunk';
|
||||||
|
|
||||||
|
import getCore from '../core';
|
||||||
|
|
||||||
|
const cvat = getCore();
|
||||||
|
|
||||||
|
export enum FormatsActionTypes {
|
||||||
|
GETTING_FORMATS_SUCCESS = 'GETTING_FORMATS_SUCCESS',
|
||||||
|
GETTING_FORMATS_FAILED = 'GETTING_FORMATS_FAILED',
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gettingFormatsSuccess(formats: any): AnyAction {
|
||||||
|
return {
|
||||||
|
type: FormatsActionTypes.GETTING_FORMATS_SUCCESS,
|
||||||
|
payload: {
|
||||||
|
formats,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gettingFormatsFailed(error: any): AnyAction {
|
||||||
|
return {
|
||||||
|
type: FormatsActionTypes.GETTING_FORMATS_FAILED,
|
||||||
|
payload: {
|
||||||
|
error,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function gettingFormatsAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
|
||||||
|
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
|
||||||
|
let formats = null;
|
||||||
|
try {
|
||||||
|
formats = await cvat.server.formats();
|
||||||
|
} catch (error) {
|
||||||
|
dispatch(gettingFormatsFailed(error));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(gettingFormatsSuccess(formats));
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,65 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { RouteComponentProps } from 'react-router';
|
||||||
|
import { Link, withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
import Title from 'antd/lib/typography/Title';
|
||||||
|
import Text from 'antd/lib/typography/Text';
|
||||||
|
import {
|
||||||
|
Col,
|
||||||
|
Row,
|
||||||
|
Modal,
|
||||||
|
} from 'antd';
|
||||||
|
|
||||||
|
import RegisterForm, { RegisterData } from '../../components/register-page/register-form';
|
||||||
|
|
||||||
|
interface RegisterPageComponentProps {
|
||||||
|
registerError: string;
|
||||||
|
onRegister: (username: string, firstName: string,
|
||||||
|
lastName: string, email: string,
|
||||||
|
password1: string, password2: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RegisterPageComponent(props: RegisterPageComponentProps & RouteComponentProps) {
|
||||||
|
const sizes = {
|
||||||
|
xs: { span: 14 },
|
||||||
|
sm: { span: 14 },
|
||||||
|
md: { span: 10 },
|
||||||
|
lg: { span: 4 },
|
||||||
|
xl: { span: 4 },
|
||||||
|
}
|
||||||
|
|
||||||
|
if (props.registerError) {
|
||||||
|
Modal.error({
|
||||||
|
title: 'Could not register',
|
||||||
|
content: props.registerError,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Row type='flex' justify='center' align='middle'>
|
||||||
|
<Col {...sizes}>
|
||||||
|
<Title level={2}> Create an account </Title>
|
||||||
|
<RegisterForm onSubmit={(registerData: RegisterData) => {
|
||||||
|
props.onRegister(
|
||||||
|
registerData.username,
|
||||||
|
registerData.firstName,
|
||||||
|
registerData.lastName,
|
||||||
|
registerData.email,
|
||||||
|
registerData.password1,
|
||||||
|
registerData.password2,
|
||||||
|
);
|
||||||
|
}}/>
|
||||||
|
<Row type='flex' justify='start' align='top'>
|
||||||
|
<Col>
|
||||||
|
<Text strong>
|
||||||
|
Already have an account? <Link to="/auth/login"> Login </Link>
|
||||||
|
</Text>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(RegisterPageComponent);
|
||||||
@ -0,0 +1,185 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { RouteComponentProps } from 'react-router';
|
||||||
|
import { withRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Spin,
|
||||||
|
Modal,
|
||||||
|
} from 'antd';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TasksQuery,
|
||||||
|
} from '../../reducers/interfaces';
|
||||||
|
|
||||||
|
import TopBar from './top-bar';
|
||||||
|
import EmptyListComponent from './empty-list';
|
||||||
|
import TaskListContainer from '../../containers/tasks-page/tasks-list';
|
||||||
|
|
||||||
|
interface TasksPageProps {
|
||||||
|
dumpingError: string;
|
||||||
|
loadingError: string;
|
||||||
|
tasksFetchingError: string;
|
||||||
|
loadingDoneMessage: string;
|
||||||
|
tasksAreBeingFetched: boolean;
|
||||||
|
gettingQuery: TasksQuery;
|
||||||
|
numberOfTasks: number;
|
||||||
|
numberOfVisibleTasks: number;
|
||||||
|
onGetTasks: (gettingQuery: TasksQuery) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TasksPageComponent extends React.PureComponent<TasksPageProps & RouteComponentProps> {
|
||||||
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
private updateURL(gettingQuery: TasksQuery) {
|
||||||
|
let queryString = '?';
|
||||||
|
for (const field of Object.keys(gettingQuery)) {
|
||||||
|
if (gettingQuery[field] !== null) {
|
||||||
|
queryString += `${field}=${gettingQuery[field]}&`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.props.history.replace({
|
||||||
|
search: queryString.slice(0, -1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSearchField(gettingQuery: TasksQuery): string {
|
||||||
|
let searchString = '';
|
||||||
|
for (const field of Object.keys(gettingQuery)) {
|
||||||
|
if (gettingQuery[field] !== null && field !== 'page') {
|
||||||
|
if (field === 'search') {
|
||||||
|
return (gettingQuery[field] as any) as string;
|
||||||
|
} else {
|
||||||
|
if (typeof (gettingQuery[field] === 'number')) {
|
||||||
|
searchString += `${field}:${gettingQuery[field]} AND `;
|
||||||
|
} else {
|
||||||
|
searchString += `${field}:"${gettingQuery[field]}" AND `;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return searchString.slice(0, -5);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handleSearch = (value: string): void => {
|
||||||
|
const gettingQuery = { ...this.props.gettingQuery };
|
||||||
|
const search = value.replace(/\s+/g, ' ').replace(/\s*:+\s*/g, ':').trim();
|
||||||
|
|
||||||
|
const fields = ['name', 'mode', 'owner', 'assignee', 'status', 'id'];
|
||||||
|
for (const field of fields) {
|
||||||
|
gettingQuery[field] = null;
|
||||||
|
}
|
||||||
|
gettingQuery.search = null;
|
||||||
|
|
||||||
|
let specificRequest = false;
|
||||||
|
for (const param of search.split(/[\s]+and[\s]+|[\s]+AND[\s]+/)) {
|
||||||
|
if (param.includes(':')) {
|
||||||
|
const [name, value] = param.split(':');
|
||||||
|
if (fields.includes(name) && !!value) {
|
||||||
|
specificRequest = true;
|
||||||
|
if (name === 'id') {
|
||||||
|
if (Number.isInteger(+value)) {
|
||||||
|
gettingQuery[name] = +value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gettingQuery[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
gettingQuery.page = 1;
|
||||||
|
if (!specificRequest && value) { // only id
|
||||||
|
gettingQuery.search = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateURL(gettingQuery);
|
||||||
|
this.props.onGetTasks(gettingQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
private handlePagination = (page: number): void => {
|
||||||
|
const gettingQuery = { ...this.props.gettingQuery };
|
||||||
|
|
||||||
|
gettingQuery.page = page;
|
||||||
|
this.updateURL(gettingQuery);
|
||||||
|
this.props.onGetTasks(gettingQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidMount() {
|
||||||
|
const gettingQuery = { ...this.props.gettingQuery };
|
||||||
|
const params = new URLSearchParams(this.props.location.search);
|
||||||
|
|
||||||
|
for (const field of Object.keys(gettingQuery)) {
|
||||||
|
if (params.has(field)) {
|
||||||
|
const value = params.get(field);
|
||||||
|
if (value) {
|
||||||
|
if (field === 'id' || field === 'page') {
|
||||||
|
if (Number.isInteger(+value)) {
|
||||||
|
gettingQuery[field] = +value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
gettingQuery[field] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updateURL(gettingQuery);
|
||||||
|
this.props.onGetTasks(gettingQuery);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidUpdate() {
|
||||||
|
if (this.props.tasksFetchingError) {
|
||||||
|
Modal.error({
|
||||||
|
title: 'Could not receive tasks',
|
||||||
|
content: this.props.tasksFetchingError,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.dumpingError) {
|
||||||
|
Modal.error({
|
||||||
|
title: 'Could not dump annotations',
|
||||||
|
content: this.props.dumpingError,
|
||||||
|
});;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.loadingError) {
|
||||||
|
Modal.error({
|
||||||
|
title: 'Could not load annotations',
|
||||||
|
content: this.props.loadingError,
|
||||||
|
});;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.props.loadingDoneMessage) {
|
||||||
|
Modal.info({
|
||||||
|
title: 'Successful loading of annotations',
|
||||||
|
content: this.props.loadingDoneMessage,
|
||||||
|
});;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (this.props.tasksAreBeingFetched) {
|
||||||
|
return (
|
||||||
|
<Spin size='large' style={{margin: '25% 50%'}}/>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return (
|
||||||
|
<div className='tasks-page'>
|
||||||
|
<TopBar
|
||||||
|
onSearch={this.handleSearch}
|
||||||
|
searchValue={this.getSearchField(this.props.gettingQuery)}
|
||||||
|
/>
|
||||||
|
{this.props.numberOfVisibleTasks ?
|
||||||
|
<TaskListContainer
|
||||||
|
onSwitchPage={this.handlePagination}
|
||||||
|
/> : <EmptyListComponent/>}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withRouter(TasksPageComponent);
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Col,
|
||||||
|
Row,
|
||||||
|
Button,
|
||||||
|
Input,
|
||||||
|
} from 'antd';
|
||||||
|
|
||||||
|
import Text from 'antd/lib/typography/Text';
|
||||||
|
|
||||||
|
interface VisibleTopBarProps {
|
||||||
|
onSearch: (value: string) => void;
|
||||||
|
searchValue: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class TopBarComponent extends React.PureComponent<VisibleTopBarProps> {
|
||||||
|
public render() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Row type='flex' justify='center' align='middle'>
|
||||||
|
<Col md={22} lg={18} xl={16} xxl={14}>
|
||||||
|
<Text strong> Default project </Text>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row type='flex' justify='center' align='middle'>
|
||||||
|
<Col md={11} lg={9} xl={8} xxl={7}>
|
||||||
|
<Text className='cvat-title'> Tasks </Text>
|
||||||
|
<Input.Search
|
||||||
|
defaultValue={this.props.searchValue}
|
||||||
|
onSearch={this.props.onSearch}
|
||||||
|
size='large' placeholder='Search'
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col
|
||||||
|
md={{span: 11}}
|
||||||
|
lg={{span: 9}}
|
||||||
|
xl={{span: 8}}
|
||||||
|
xxl={{span: 7}}>
|
||||||
|
<Button size='large' id='cvat-create-task-button' type='primary' onClick={
|
||||||
|
() => window.open('/tasks/create', '_blank')
|
||||||
|
}> Create new task </Button>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
export default function CreateTaskPage() {
|
export default function CreateTaskPageContainer() {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
"Create Task Page"
|
"Create Task Page"
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import { logoutAsync } from '../../actions/auth-actions';
|
||||||
|
import { CombinedState } from '../../reducers/root-reducer';
|
||||||
|
|
||||||
|
import HeaderComponent from '../../components/header/header';
|
||||||
|
|
||||||
|
interface StateToProps {
|
||||||
|
username: string;
|
||||||
|
logoutError: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchToProps {
|
||||||
|
logout(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: CombinedState): StateToProps {
|
||||||
|
return {
|
||||||
|
username: state.auth.user.username,
|
||||||
|
logoutError: state.auth.logoutError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||||
|
return {
|
||||||
|
logout: () => dispatch(logoutAsync()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function HeaderContainer(props: StateToProps & DispatchToProps) {
|
||||||
|
return (
|
||||||
|
<HeaderComponent
|
||||||
|
onLogout={props.logout}
|
||||||
|
username={props.username}
|
||||||
|
logoutError={props.logoutError ? props.logoutError.toString() : ''}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(HeaderContainer);
|
||||||
@ -0,0 +1,39 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { loginAsync } from '../../actions/auth-actions';
|
||||||
|
import { CombinedState } from '../../reducers/root-reducer';
|
||||||
|
import LoginPageComponent from '../../components/login-page/login-page';
|
||||||
|
|
||||||
|
interface StateToProps {
|
||||||
|
loginError: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchToProps {
|
||||||
|
login(username: string, password: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: CombinedState): StateToProps {
|
||||||
|
return {
|
||||||
|
loginError: state.auth.loginError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||||
|
return {
|
||||||
|
login: (...args) => dispatch(loginAsync(...args)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function LoginPageContainer(props: StateToProps & DispatchToProps) {
|
||||||
|
return (
|
||||||
|
<LoginPageComponent
|
||||||
|
onLogin={props.login}
|
||||||
|
loginError={props.loginError ? props.loginError.toString() : ''}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(LoginPageContainer);
|
||||||
@ -1,77 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { RouteComponentProps } from 'react-router';
|
|
||||||
import { Link, withRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import Title from 'antd/lib/typography/Title';
|
|
||||||
import Text from 'antd/lib/typography/Text';
|
|
||||||
import {
|
|
||||||
Col,
|
|
||||||
Row,
|
|
||||||
Modal,
|
|
||||||
} from 'antd';
|
|
||||||
|
|
||||||
|
|
||||||
import { registerAsync } from '../actions/auth-actions';
|
|
||||||
import RegisterForm, { RegisterData } from '../components/register-form';
|
|
||||||
import { AuthState } from '../reducers/interfaces';
|
|
||||||
|
|
||||||
interface StateToProps {
|
|
||||||
auth: AuthState;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DispatchToProps {
|
|
||||||
register: (registerData: RegisterData) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapStateToProps(state: any): StateToProps {
|
|
||||||
return {
|
|
||||||
auth: state.auth,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
|
||||||
return {
|
|
||||||
register: (registerData: RegisterData) => dispatch(registerAsync(registerData))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type RegisterPageProps = StateToProps & DispatchToProps & RouteComponentProps;
|
|
||||||
function RegisterPage(props: RegisterPageProps) {
|
|
||||||
const { registerError } = props.auth;
|
|
||||||
const sizes = {
|
|
||||||
xs: { span: 14 },
|
|
||||||
sm: { span: 14 },
|
|
||||||
md: { span: 10 },
|
|
||||||
lg: { span: 4 },
|
|
||||||
xl: { span: 4 },
|
|
||||||
}
|
|
||||||
|
|
||||||
if (registerError) {
|
|
||||||
Modal.error({
|
|
||||||
title: 'Could not login',
|
|
||||||
content: `${registerError.toString()}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Row type='flex' justify='center' align='middle'>
|
|
||||||
<Col {...sizes}>
|
|
||||||
<Title level={2}> Create an account </Title>
|
|
||||||
<RegisterForm onSubmit={props.register}/>
|
|
||||||
<Row type='flex' justify='start' align='top'>
|
|
||||||
<Col>
|
|
||||||
<Text strong>
|
|
||||||
Already have an account? <Link to="/auth/login"> Login </Link>
|
|
||||||
</Text>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
)(RegisterPage));
|
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
import { registerAsync } from '../../actions/auth-actions';
|
||||||
|
import { CombinedState } from '../../reducers/root-reducer';
|
||||||
|
import RegisterPageComponent from '../../components/register-page/register-page';
|
||||||
|
|
||||||
|
interface StateToProps {
|
||||||
|
registerError: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchToProps {
|
||||||
|
register: (username: string, firstName: string,
|
||||||
|
lastName: string, email: string,
|
||||||
|
password1: string, password2: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: CombinedState): StateToProps {
|
||||||
|
return {
|
||||||
|
registerError: state.auth.registerError,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||||
|
return {
|
||||||
|
register: (...args) => dispatch(registerAsync(...args))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type RegisterPageContainerProps = StateToProps & DispatchToProps;
|
||||||
|
function RegisterPageContainer(props: RegisterPageContainerProps) {
|
||||||
|
return (
|
||||||
|
<RegisterPageComponent
|
||||||
|
registerError={props.registerError ? props.registerError.toString() : ''}
|
||||||
|
onRegister={props.register}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(RegisterPageContainer);
|
||||||
@ -1,212 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import { connect } from 'react-redux';
|
|
||||||
import { RouteComponentProps } from 'react-router';
|
|
||||||
import { withRouter } from 'react-router-dom';
|
|
||||||
|
|
||||||
import Text from 'antd/lib/typography/Text';
|
|
||||||
import {
|
|
||||||
Col,
|
|
||||||
Row,
|
|
||||||
Button,
|
|
||||||
Input,
|
|
||||||
Spin,
|
|
||||||
Modal,
|
|
||||||
} from 'antd';
|
|
||||||
|
|
||||||
import { TasksState, TasksQuery } from '../reducers/interfaces';
|
|
||||||
import EmptyList from '../components/tasks-page/empty-list';
|
|
||||||
import TaskList from '../components/tasks-page/task-list';
|
|
||||||
|
|
||||||
import { getTasksAsync } from '../actions/tasks-actions';
|
|
||||||
|
|
||||||
interface StateToProps {
|
|
||||||
tasks: TasksState;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface DispatchToProps {
|
|
||||||
getTasks: (query: TasksQuery) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TasksPageState {
|
|
||||||
searchString: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapStateToProps(state: any): object {
|
|
||||||
return {
|
|
||||||
tasks: state.tasks,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
|
||||||
return {
|
|
||||||
getTasks: (query: TasksQuery) => {dispatch(getTasksAsync(query))}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type TasksPageProps = StateToProps & DispatchToProps
|
|
||||||
& RouteComponentProps;
|
|
||||||
|
|
||||||
class TasksPage extends React.PureComponent<TasksPageProps, TasksPageState> {
|
|
||||||
constructor(props: any) {
|
|
||||||
super(props);
|
|
||||||
}
|
|
||||||
|
|
||||||
private updateURL(query: TasksQuery) {
|
|
||||||
let queryString = '?';
|
|
||||||
for (const field of Object.keys(query)) {
|
|
||||||
if (query[field] != null && field !== 'page') {
|
|
||||||
queryString += `${field}=${query[field]}&`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.props.history.replace({
|
|
||||||
search: queryString.slice(0, -1),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private computeSearchField(query: TasksQuery): string {
|
|
||||||
let searchString = '';
|
|
||||||
for (const field of Object.keys(query)) {
|
|
||||||
|
|
||||||
if (query[field] != null && field !== 'page') {
|
|
||||||
if (typeof (query[field] === 'number')) {
|
|
||||||
searchString += `${field}:${query[field]} AND `;
|
|
||||||
} else {
|
|
||||||
searchString += `${field}:"${query[field]}" AND `;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return searchString.slice(0, -5);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handlePagination(page: number): void {
|
|
||||||
const query = { ...this.props.tasks.query };
|
|
||||||
|
|
||||||
query.page = page;
|
|
||||||
this.updateURL(query);
|
|
||||||
this.props.getTasks(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
private handleSearch(value: string): void {
|
|
||||||
const query = { ...this.props.tasks.query };
|
|
||||||
const search = value.replace(/\s+/g, ' ').replace(/\s*:+\s*/g, ':').trim();
|
|
||||||
|
|
||||||
const fields = ['name', 'mode', 'owner', 'assignee', 'status', 'id'];
|
|
||||||
for (const field of fields) {
|
|
||||||
query[field] = null;
|
|
||||||
}
|
|
||||||
query.search = null;
|
|
||||||
|
|
||||||
let specificRequest = false;
|
|
||||||
for (const param of search.split(/[\s]+and[\s]+|[\s]+AND[\s]+/)) {
|
|
||||||
if (param.includes(':')) {
|
|
||||||
const [name, value] = param.split(':');
|
|
||||||
if (fields.includes(name) && !!value) {
|
|
||||||
specificRequest = true;
|
|
||||||
if (name === 'id') {
|
|
||||||
if (Number.isInteger(+value)) {
|
|
||||||
query[name] = +value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
query[name] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
query.page = 1;
|
|
||||||
if (!specificRequest && value) { // only id
|
|
||||||
query.search = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateURL(query);
|
|
||||||
this.props.getTasks(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount() {
|
|
||||||
const query = { ...this.props.tasks.query };
|
|
||||||
const params = new URLSearchParams(this.props.location.search);
|
|
||||||
|
|
||||||
for (const field of Object.keys(query)) {
|
|
||||||
if (params.has(field)) {
|
|
||||||
const value = params.get(field);
|
|
||||||
if (value) {
|
|
||||||
if (field === 'id' || field === 'page') {
|
|
||||||
if (Number.isInteger(+value)) {
|
|
||||||
query[field] = +value;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
query[field] = value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.updateURL(query);
|
|
||||||
this.props.getTasks(query);
|
|
||||||
}
|
|
||||||
|
|
||||||
private renderTaskList() {
|
|
||||||
const searchString = this.computeSearchField(this.props.tasks.query);
|
|
||||||
|
|
||||||
const List = this.props.tasks.array.length ? <TaskList
|
|
||||||
tasks={this.props.tasks.array}
|
|
||||||
previews={this.props.tasks.previews}
|
|
||||||
page={this.props.tasks.query.page}
|
|
||||||
count={this.props.tasks.count}
|
|
||||||
goToPage={this.handlePagination.bind(this)}
|
|
||||||
/> : <EmptyList/>
|
|
||||||
|
|
||||||
if (this.props.tasks.error) {
|
|
||||||
Modal.error({
|
|
||||||
title: 'Could not receive tasks',
|
|
||||||
content: `${this.props.tasks.error.toString()}`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='tasks-page'>
|
|
||||||
<Row type='flex' justify='center' align='middle'>
|
|
||||||
<Col md={22} lg={18} xl={16} xxl={14}>
|
|
||||||
<Text strong> Default project </Text>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<Row type='flex' justify='center' align='middle'>
|
|
||||||
<Col md={11} lg={9} xl={8} xxl={7}>
|
|
||||||
<Text className='cvat-title'> Tasks </Text>
|
|
||||||
<Input.Search
|
|
||||||
defaultValue={searchString}
|
|
||||||
onSearch={this.handleSearch.bind(this)}
|
|
||||||
size='large' placeholder='Search'
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
<Col
|
|
||||||
md={{span: 11}}
|
|
||||||
lg={{span: 9}}
|
|
||||||
xl={{span: 8}}
|
|
||||||
xxl={{span: 7}}>
|
|
||||||
<Button size='large' id='cvat-create-task-button' type='primary' onClick={
|
|
||||||
() => window.open('/tasks/create', '_blank')
|
|
||||||
}> Create new task </Button>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
{List}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
public render() {
|
|
||||||
if (this.props.tasks.initialized) {
|
|
||||||
return this.renderTaskList();
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<Spin size='large' style={{margin: '25% 50%'}}/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default withRouter(connect(
|
|
||||||
mapStateToProps,
|
|
||||||
mapDispatchToProps,
|
|
||||||
)(TasksPage));
|
|
||||||
@ -0,0 +1,90 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TasksQuery,
|
||||||
|
} from '../../reducers/interfaces';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CombinedState,
|
||||||
|
} from '../../reducers/root-reducer';
|
||||||
|
|
||||||
|
import TaskItemComponent from '../../components/tasks-page/task-item'
|
||||||
|
|
||||||
|
import {
|
||||||
|
getTasksAsync,
|
||||||
|
dumpAnnotationsAsync,
|
||||||
|
loadAnnotationsAsync,
|
||||||
|
} from '../../actions/tasks-actions';
|
||||||
|
|
||||||
|
interface StateToProps {
|
||||||
|
dumpActivities: string[] | null;
|
||||||
|
loadActivity: string | null;
|
||||||
|
previewImage: string;
|
||||||
|
taskInstance: any;
|
||||||
|
loaders: any[];
|
||||||
|
dumpers: any[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchToProps {
|
||||||
|
getTasks: (query: TasksQuery) => void;
|
||||||
|
dump: (task: any, format: string) => void;
|
||||||
|
load: (task: any, format: string, file: File) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OwnProps {
|
||||||
|
idx: number;
|
||||||
|
taskID: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
|
||||||
|
const task = state.tasks.current[own.idx];
|
||||||
|
const { formats } = state;
|
||||||
|
const { dumps } = state.tasks.activities;
|
||||||
|
const { loads } = state.tasks.activities;
|
||||||
|
|
||||||
|
return {
|
||||||
|
dumpActivities: dumps.byTask[own.taskID] ? dumps.byTask[own.taskID] : null,
|
||||||
|
loadActivity: loads.byTask[own.taskID] ? loads.byTask[own.taskID] : null,
|
||||||
|
previewImage: task.preview,
|
||||||
|
taskInstance: task.instance,
|
||||||
|
loaders: formats.loaders,
|
||||||
|
dumpers: formats.dumpers,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||||
|
return {
|
||||||
|
getTasks: (query: TasksQuery): void => {
|
||||||
|
dispatch(getTasksAsync(query));
|
||||||
|
},
|
||||||
|
dump: (task: any, dumper: any): void => {
|
||||||
|
dispatch(dumpAnnotationsAsync(task, dumper));
|
||||||
|
},
|
||||||
|
load: (task: any, loader: any, file: File): void => {
|
||||||
|
dispatch(loadAnnotationsAsync(task, loader, file));
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TasksItemContainerProps = StateToProps & DispatchToProps & OwnProps;
|
||||||
|
|
||||||
|
function TaskItemContainer(props: TasksItemContainerProps) {
|
||||||
|
return (
|
||||||
|
<TaskItemComponent
|
||||||
|
taskInstance={props.taskInstance}
|
||||||
|
previewImage={props.previewImage}
|
||||||
|
dumpActivities={props.dumpActivities}
|
||||||
|
loadActivity={props.loadActivity}
|
||||||
|
loaders={props.loaders}
|
||||||
|
dumpers={props.dumpers}
|
||||||
|
onLoadAnnotation={props.load}
|
||||||
|
onDumpAnnotation={props.dump}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(TaskItemContainer);
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TasksState,
|
||||||
|
TasksQuery,
|
||||||
|
} from '../../reducers/interfaces';
|
||||||
|
|
||||||
|
import {
|
||||||
|
CombinedState,
|
||||||
|
} from '../../reducers/root-reducer';
|
||||||
|
|
||||||
|
import TasksListComponent from '../../components/tasks-page/task-list';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getTasksAsync,
|
||||||
|
} from '../../actions/tasks-actions';
|
||||||
|
|
||||||
|
interface StateToProps {
|
||||||
|
tasks: TasksState;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchToProps {
|
||||||
|
getTasks: (query: TasksQuery) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface OwnProps {
|
||||||
|
onSwitchPage: (page: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: CombinedState): StateToProps {
|
||||||
|
return {
|
||||||
|
tasks: state.tasks,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||||
|
return {
|
||||||
|
getTasks: (query: TasksQuery) => {dispatch(getTasksAsync(query))}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TasksListContainerProps = StateToProps & DispatchToProps & OwnProps;
|
||||||
|
|
||||||
|
function TasksListContainer(props: TasksListContainerProps) {
|
||||||
|
return (
|
||||||
|
<TasksListComponent
|
||||||
|
onSwitchPage={props.onSwitchPage}
|
||||||
|
currentTasksIndexes={props.tasks.current.map((task) => task.instance.id)}
|
||||||
|
currentPage={props.tasks.gettingQuery.page}
|
||||||
|
numberOfTasks={props.tasks.count}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(TasksListContainer);
|
||||||
@ -0,0 +1,73 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { connect } from 'react-redux';
|
||||||
|
|
||||||
|
import {
|
||||||
|
TasksQuery,
|
||||||
|
} from '../../reducers/interfaces';
|
||||||
|
import { CombinedState } from '../../reducers/root-reducer';
|
||||||
|
|
||||||
|
import TasksPageComponent from '../../components/tasks-page/tasks-page';
|
||||||
|
|
||||||
|
import { getTasksAsync } from '../../actions/tasks-actions';
|
||||||
|
|
||||||
|
interface StateToProps {
|
||||||
|
dumpingError: any;
|
||||||
|
loadingError: any;
|
||||||
|
tasksFetchingError: any;
|
||||||
|
loadingDoneMessage: string;
|
||||||
|
tasksAreBeingFetched: boolean;
|
||||||
|
gettingQuery: TasksQuery;
|
||||||
|
numberOfTasks: number;
|
||||||
|
numberOfVisibleTasks: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface DispatchToProps {
|
||||||
|
getTasks: (gettingQuery: TasksQuery) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapStateToProps(state: CombinedState): StateToProps {
|
||||||
|
const { tasks } = state;
|
||||||
|
const { activities } = tasks;
|
||||||
|
const { dumps } = activities;
|
||||||
|
const { loads } = activities;
|
||||||
|
|
||||||
|
return {
|
||||||
|
dumpingError: dumps.dumpingError,
|
||||||
|
loadingError: loads.loadingError,
|
||||||
|
tasksFetchingError: tasks.tasksFetchingError,
|
||||||
|
loadingDoneMessage: loads.loadingDoneMessage,
|
||||||
|
tasksAreBeingFetched: !state.tasks.initialized,
|
||||||
|
gettingQuery: tasks.gettingQuery,
|
||||||
|
numberOfTasks: state.tasks.count,
|
||||||
|
numberOfVisibleTasks: state.tasks.current.length,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapDispatchToProps(dispatch: any): DispatchToProps {
|
||||||
|
return {
|
||||||
|
getTasks: (query: TasksQuery) => {dispatch(getTasksAsync(query))}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TasksPageContainerProps = StateToProps & DispatchToProps;
|
||||||
|
|
||||||
|
function TasksPageContainer(props: TasksPageContainerProps) {
|
||||||
|
return (
|
||||||
|
<TasksPageComponent
|
||||||
|
dumpingError={props.dumpingError ? props.dumpingError.toString() : ''}
|
||||||
|
loadingError={props.loadingError ? props.loadingError.toString() : ''}
|
||||||
|
tasksFetchingError={props.tasksFetchingError ? props.tasksFetchingError.toString(): ''}
|
||||||
|
loadingDoneMessage={props.loadingDoneMessage}
|
||||||
|
tasksAreBeingFetched={props.tasksAreBeingFetched}
|
||||||
|
gettingQuery={props.gettingQuery}
|
||||||
|
numberOfTasks={props.numberOfTasks}
|
||||||
|
numberOfVisibleTasks={props.numberOfVisibleTasks}
|
||||||
|
onGetTasks={props.getTasks}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default connect(
|
||||||
|
mapStateToProps,
|
||||||
|
mapDispatchToProps,
|
||||||
|
)(TasksPageContainer);
|
||||||
@ -0,0 +1,32 @@
|
|||||||
|
import { AnyAction } from 'redux';
|
||||||
|
import { FormatsActionTypes } from '../actions/formats-actions';
|
||||||
|
|
||||||
|
import { FormatsState } from './interfaces';
|
||||||
|
|
||||||
|
const defaultState: FormatsState = {
|
||||||
|
loaders: [],
|
||||||
|
dumpers: [],
|
||||||
|
gettingFormatsError: null,
|
||||||
|
initialized: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default (state = defaultState, action: AnyAction): FormatsState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case FormatsActionTypes.GETTING_FORMATS_SUCCESS:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
initialized: true,
|
||||||
|
gettingFormatsError: null,
|
||||||
|
dumpers: action.payload.formats.map((format: any): any[] => format.dumpers).flat(),
|
||||||
|
loaders: action.payload.formats.map((format: any): any[] => format.loaders).flat(),
|
||||||
|
};
|
||||||
|
case FormatsActionTypes.GETTING_FORMATS_FAILED:
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
initialized: true,
|
||||||
|
gettingFormatsError: action.payload.error,
|
||||||
|
};
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -1,10 +1,24 @@
|
|||||||
import { combineReducers, Reducer } from 'redux';
|
import { combineReducers, Reducer } from 'redux';
|
||||||
import authReducer from './auth-reducer';
|
import authReducer from './auth-reducer';
|
||||||
import tasksReducer from './tasks-reducer';
|
import tasksReducer from './tasks-reducer';
|
||||||
|
import formatsReducer from './formats-reducer';
|
||||||
|
|
||||||
|
import {
|
||||||
|
AuthState,
|
||||||
|
TasksState,
|
||||||
|
FormatsState,
|
||||||
|
} from './interfaces';
|
||||||
|
|
||||||
|
export interface CombinedState {
|
||||||
|
auth: AuthState;
|
||||||
|
tasks: TasksState;
|
||||||
|
formats: FormatsState;
|
||||||
|
}
|
||||||
|
|
||||||
export default function createRootReducer(): Reducer {
|
export default function createRootReducer(): Reducer {
|
||||||
return combineReducers({
|
return combineReducers({
|
||||||
auth: authReducer,
|
auth: authReducer,
|
||||||
tasks: tasksReducer,
|
tasks: tasksReducer,
|
||||||
|
formats: formatsReducer,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue