Add `header-layout` component (#671)

main
Artyom Zankevich 7 years ago committed by Boris Sekachev
parent 191bf2761c
commit d802642512

1
.gitignore vendored

@ -21,7 +21,6 @@ __pycache__
._*
# Ignore development npm files
package-lock.json
node_modules
.DS_Store

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -0,0 +1 @@
<svg width="98" height="27" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path d="M101 0v29l-52.544.001C44.326 35.511 35.598 40 25.5 40 11.417 40 0 31.27 0 20.5S11.417 1 25.5 1c4.542 0 8.807.908 12.5 2.5V0h63z" id="a"/></defs><g transform="translate(-2 -1)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M48.142 1c4.736 0 6.879 3.234 6.879 5.904v2.068h-4.737V6.904c0-.79-.789-2.144-2.142-2.144-1.654 0-2.368 1.354-2.368 2.144v15.192c0 .79.714 2.144 2.368 2.144 1.353 0 2.142-1.354 2.142-2.144v-2.068h4.737v2.068c0 2.67-2.143 5.904-6.88 5.904C42.956 28 41 24.766 41 22.134V6.904C41 4.234 42.955 1 48.142 1zM19-6c9.389 0 17 7.611 17 17s-7.611 17-17 17S2 20.389 2 11 9.611-6 19-6zm42.256 7.338l3.345 19.48h.075l3.42-19.48h5l-6.052 26.324h-5L56.22 1.338h5.037zm20.706 0l5.413 26.324h-4.699l-.94-6.13h-4.548l-.902 6.13h-4.435l5.413-26.324h4.698zm18.038 0v3.723h-4.849v22.6h-4.699v-22.6h-4.81V1.338H100zM19 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zm60.557 4.295h-.113l-1.466 9.439h3.007l-1.428-9.439z" fill="#000" fill-rule="nonzero" mask="url(#b)"/></g></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

@ -0,0 +1 @@
<svg width="90" height="78" xmlns="http://www.w3.org/2000/svg"><path d="M84.27 0c2.753 0 5.007 2.167 5.12 4.874l.005.215v67.148c0 2.734-2.183 4.972-4.908 5.085l-.217.004H5.123c-2.751 0-5.005-2.167-5.118-4.874L0 72.237V5.09C0 2.355 2.183.117 4.907.004L5.123 0H84.27zm1.58 16.242H3.546v55.995c0 .816.632 1.488 1.434 1.56l.144.007H84.27c.824 0 1.501-.627 1.574-1.424l.007-.143V16.242zM12.658 38.48h4.328c.59 0 1.076.452 1.138 1.031l.007.126v1.03h15.02v-1.03c0-.596.446-1.087 1.02-1.15l.125-.007h4.328c.59 0 1.076.451 1.138 1.03l.006.127v4.372c0 .596-.446 1.087-1.02 1.15l-.124.007h-1.02v10.548h1.019c.59 0 1.077.451 1.139 1.031l.006.126v4.372c0 .596-.446 1.087-1.02 1.15l-.125.007h-4.327a1.15 1.15 0 0 1-1.139-1.03l-.006-.127v-1.03H18.13v1.03c0 .596-.446 1.087-1.02 1.15l-.125.007h-4.328a1.15 1.15 0 0 1-1.138-1.03l-.007-.127v-4.372c0-.596.447-1.087 1.02-1.15l.125-.007h1.019V45.166h-1.02a1.15 1.15 0 0 1-1.138-1.03l-.006-.127v-4.372c0-.596.446-1.087 1.02-1.15l.125-.007h4.328zm3.183 19.548h-2.038v2.058h2.038v-2.058zm21.638 0h-2.039v2.058h2.039v-2.058zM33.15 42.98H18.13v1.03c0 .595-.447 1.087-1.02 1.15l-.125.006h-1.019v10.548h1.019c.59 0 1.076.451 1.138 1.031l.007.126V57.9h15.02V56.87c0-.596.446-1.087 1.02-1.15l.125-.007h1.019V45.166h-1.019a1.15 1.15 0 0 1-1.139-1.031l-.006-.126v-1.03zm21.575-7.62c.398 0 .72.338.72.755v.67h9.458v-.67c0-.417.323-.755.721-.755h2.725c.398 0 .72.338.72.754v2.852c0 .416-.322.754-.72.754h-.641v6.88h.64c.399 0 .722.337.722.754v2.852c0 .416-.323.754-.721.754h-2.725c-.398 0-.721-.338-.721-.754v-.672h-9.457v.672c0 .416-.322.754-.72.754H52c-.398 0-.72-.338-.72-.754v-2.852c0-.416.322-.754.72-.754h.642v-6.88H52c-.398 0-.72-.337-.72-.754v-2.851c0-.417.322-.755.72-.755zm-.72 12.749H52.72v1.342h1.283V48.11zm13.623 0h-1.283v1.342h1.283V48.11zm-2.725-9.814h-9.457v.671c0 .417-.323.755-.721.755h-.641V46.6h.641c.399 0 .721.337.721.754v.671h9.457v-.67c0-.417.323-.755.72-.755h.642v-6.88h-.64c-.4 0-.722-.338-.722-.754v-.671zm-49.063 2.5h-2.038v2.058h2.038v-2.058zm21.638-.001H35.44v2.058h2.038v-2.058zm30.15-3.925h-1.283v1.342h1.283V36.87zm-13.624 0h-1.283v1.343h1.283v-1.343zM9.098 19.76c.93 0 1.692.711 1.766 1.616l.006.145v.118c0 .973-.793 1.761-1.772 1.761-.93 0-1.693-.711-1.767-1.616l-.005-.145v-.118c0-.973.793-1.761 1.772-1.761zm21.65 0c.978 0 1.771.788 1.771 1.76 0 .974-.793 1.762-1.771 1.762H16.423c-.98 0-1.772-.788-1.772-1.761 0-.973.792-1.761 1.772-1.761h14.325zm13.988 0c.98 0 1.772.788 1.772 1.76 0 .974-.792 1.762-1.772 1.762h-5.29a1.768 1.768 0 0 1-1.772-1.761c0-.973.796-1.761 1.772-1.761h5.29zm9.868 0c.977 0 1.773.788 1.773 1.76 0 .974-.796 1.762-1.773 1.762H53.05a1.766 1.766 0 0 1-1.772-1.761c0-.973.793-1.761 1.772-1.761h1.553zm17.107 0c.977 0 1.772.788 1.772 1.76 0 .974-.795 1.762-1.772 1.762h-8.194c-.98 0-1.773-.788-1.773-1.761 0-.973.793-1.761 1.773-1.761h8.194zm12.56-16.238H5.122c-.82 0-1.5.628-1.572 1.424l-.006.143v7.631H85.85V5.09c0-.863-.709-1.567-1.58-1.567zM9.125 6.24c.98 0 1.772.787 1.772 1.76s-.793 1.761-1.772 1.761c-.977 0-1.8-.788-1.8-1.76 0-.974.761-1.761 1.741-1.761h.059zm7.354 0c.979 0 1.772.787 1.772 1.76s-.793 1.761-1.772 1.761c-.977 0-1.829-.788-1.829-1.76 0-.974.734-1.761 1.712-1.761h.117zm7.297 0c.977 0 1.773.787 1.773 1.76s-.796 1.761-1.773 1.761c-.979 0-1.8-.788-1.8-1.76 0-.974.764-1.761 1.743-1.761h.057z" fill="#9B9B9B" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

@ -1,36 +1,39 @@
export const dumpAnnotation = () => (dispatch: any) => {
import { Dispatch, ActionCreator } from 'redux';
export const dumpAnnotation = () => (dispatch: Dispatch) => {
dispatch({
type: 'DUMP_ANNOTATION',
});
}
export const dumpAnnotationSuccess = (downloadLink: string) => (dispatch: any) => {
export const dumpAnnotationSuccess = (downloadLink: string) => (dispatch: Dispatch) => {
dispatch({
type: 'DUMP_ANNOTATION_SUCCESS',
payload: downloadLink,
});
}
export const dumpAnnotationError = (error = {}) => (dispatch: any) => {
export const dumpAnnotationError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'DUMP_ANNOTATION_ERROR',
payload: error,
});
}
export const uploadAnnotation = () => (dispatch: any) => {
export const uploadAnnotation = () => (dispatch: Dispatch) => {
dispatch({
type: 'UPLOAD_ANNOTATION',
});
}
export const uploadAnnotationSuccess = () => (dispatch: any) => {
export const uploadAnnotationSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'UPLOAD_ANNOTATION_SUCCESS',
});
}
export const uploadAnnotationError = (error = {}) => (dispatch: any) => {
export const uploadAnnotationError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'UPLOAD_ANNOTATION_ERROR',
payload: error,
@ -38,7 +41,7 @@ export const uploadAnnotationError = (error = {}) => (dispatch: any) => {
}
export const dumpAnnotationAsync = (task: any, dumper: any) => {
return (dispatch: any) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(dumpAnnotation());
return task.annotations.dump(task.name, dumper).then(
@ -55,7 +58,7 @@ export const dumpAnnotationAsync = (task: any, dumper: any) => {
}
export const uploadAnnotationAsync = (task: any, file: File, loader: any) => {
return (dispatch: any) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(uploadAnnotation());
return task.annotations.upload(file, loader).then(

@ -1,31 +1,98 @@
export const login = () => (dispatch: any) => {
import { History } from 'history';
import { Dispatch, ActionCreator } from 'redux';
export const login = () => (dispatch: Dispatch) => {
dispatch({
type: 'LOGIN',
});
}
export const loginSuccess = () => (dispatch: any) => {
export const loginSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'LOGIN_SUCCESS',
});
}
export const loginError = (error = {}) => (dispatch: any) => {
export const loginError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'LOGIN_ERROR',
payload: error,
});
}
export const loginAsync = (username: string, password: string, history: any) => {
return (dispatch: any) => {
export const logout = () => (dispatch: Dispatch) => {
dispatch({
type: 'LOGOUT',
});
}
export const logoutSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'LOGOUT_SUCCESS',
});
}
export const logoutError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'LOGOUT_ERROR',
payload: error,
});
}
export const isAuthenticated = () => (dispatch: Dispatch) => {
dispatch({
type: 'IS_AUTHENTICATED',
});
}
export const isAuthenticatedSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'IS_AUTHENTICATED_SUCCESS',
});
}
export const isAuthenticatedFail = () => (dispatch: Dispatch) => {
dispatch({
type: 'IS_AUTHENTICATED_FAIL',
});
}
export const isAuthenticatedError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'IS_AUTHENTICATED_ERROR',
payload: error,
});
}
export const register = () => (dispatch: Dispatch) => {
dispatch({
type: 'REGISTER',
});
}
export const registerSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'REGISTER_SUCCESS',
});
}
export const registerError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'REGISTER_ERROR',
payload: error,
});
}
export const loginAsync = (username: string, password: string, history: History) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(login());
return (window as any).cvat.server.login(username, password).then(
(authenticated: any) => {
localStorage.setItem('session', 'true');
(loggedIn: any) => {
dispatch(loginSuccess());
history.push(history.location.state ? history.location.state.from : '/dashboard');
history.push(history.location.state ? history.location.state.from : '/tasks');
},
(error: any) => {
dispatch(loginError(error));
@ -35,3 +102,71 @@ export const loginAsync = (username: string, password: string, history: any) =>
);
};
}
export const logoutAsync = () => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(logout());
return (window as any).cvat.server.logout().then(
(loggedOut: any) => {
dispatch(logoutSuccess());
},
(error: any) => {
dispatch(logoutError(error));
throw error;
},
);
};
}
export const isAuthenticatedAsync = () => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(isAuthenticated());
return (window as any).cvat.server.authorized().then(
(isAuthenticated: boolean) => {
isAuthenticated ? dispatch(isAuthenticatedSuccess()) : dispatch(isAuthenticatedFail());
},
(error: any) => {
dispatch(isAuthenticatedError(error));
throw error;
},
);
};
}
export const registerAsync = (
username: string,
firstName: string,
lastName: string,
email: string,
password: string,
passwordConfirmation: string,
history: History,
) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(register());
return (window as any).cvat.server.register(
username,
firstName,
lastName,
email,
password,
passwordConfirmation,
).then(
(registered: any) => {
dispatch(registerSuccess());
history.replace('/login');
},
(error: any) => {
dispatch(registerError(error));
throw error;
},
);
};
}

@ -1,57 +1,60 @@
export const getServerInfo = () => (dispatch: any) => {
import { Dispatch, ActionCreator } from 'redux';
export const getServerInfo = () => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SERVER_INFO',
});
}
export const getServerInfoSuccess = (information: null) => (dispatch: any) => {
export const getServerInfoSuccess = (information: null) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SERVER_INFO_SUCCESS',
payload: information,
});
}
export const getServerInfoError = (error = {}) => (dispatch: any) => {
export const getServerInfoError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SERVER_INFO_ERROR',
payload: error,
});
}
export const getShareFiles = () => (dispatch: any) => {
export const getShareFiles = () => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SHARE_FILES',
});
}
export const getShareFilesSuccess = (files: []) => (dispatch: any) => {
export const getShareFilesSuccess = (files: []) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SHARE_FILES_SUCCESS',
payload: files,
});
}
export const getShareFilesError = (error = {}) => (dispatch: any) => {
export const getShareFilesError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SHARE_FILES_ERROR',
payload: error,
});
}
export const getAnnotationFormats = () => (dispatch: any) => {
export const getAnnotationFormats = () => (dispatch: Dispatch) => {
dispatch({
type: 'GET_ANNOTATION_FORMATS',
});
}
export const getAnnotationFormatsSuccess = (annotationFormats: []) => (dispatch: any) => {
export const getAnnotationFormatsSuccess = (annotationFormats: []) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_ANNOTATION_FORMATS_SUCCESS',
payload: annotationFormats,
});
}
export const getAnnotationFormatsError = (error = {}) => (dispatch: any) => {
export const getAnnotationFormatsError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_ANNOTATION_FORMATS_ERROR',
payload: error,
@ -59,7 +62,7 @@ export const getAnnotationFormatsError = (error = {}) => (dispatch: any) => {
}
export const getServerInfoAsync = () => {
return (dispatch: any) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(getServerInfo());
return (window as any).cvat.server.about().then(
@ -74,7 +77,7 @@ export const getServerInfoAsync = () => {
}
export const getShareFilesAsync = (directory: string) => {
return (dispatch: any) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(getShareFiles());
return (window as any).cvat.server.share(directory).then(
@ -89,7 +92,7 @@ export const getShareFilesAsync = (directory: string) => {
}
export const getAnnotationFormatsAsync = () => {
return (dispatch: any) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(getAnnotationFormats());
return (window as any).cvat.server.formats().then(

@ -1,4 +1,7 @@
export const filterTasks = (queryParams: { search?: string, page?: number }) => (dispatch: any) => {
import { Dispatch } from 'redux';
export const filterTasks = (queryParams: { search?: string, page?: number }) => (dispatch: Dispatch) => {
dispatch({
type: 'FILTER_TASKS',
payload: queryParams,

@ -1,79 +1,82 @@
import { History } from 'history';
import { Dispatch, ActionCreator } from 'redux';
import queryString from 'query-string';
import setQueryObject from '../utils/tasks-filter'
export const getTasks = () => (dispatch: any) => {
export const getTasks = () => (dispatch: Dispatch) => {
dispatch({
type: 'GET_TASKS',
});
}
export const getTasksSuccess = (tasks: []) => (dispatch: any) => {
export const getTasksSuccess = (tasks: []) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_TASKS_SUCCESS',
payload: tasks,
});
}
export const getTasksError = (error: {}) => (dispatch: any) => {
export const getTasksError = (error: {}) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_TASKS_ERROR',
payload: error,
});
}
export const createTask = () => (dispatch: any) => {
export const createTask = () => (dispatch: Dispatch) => {
dispatch({
type: 'CREATE_TASK',
});
}
export const createTaskSuccess = () => (dispatch: any) => {
export const createTaskSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'CREATE_TASK_SUCCESS',
});
}
export const createTaskError = (error: {}) => (dispatch: any) => {
export const createTaskError = (error: {}) => (dispatch: Dispatch) => {
dispatch({
type: 'CREATE_TASK_ERROR',
payload: error,
});
}
export const updateTask = () => (dispatch: any) => {
export const updateTask = () => (dispatch: Dispatch) => {
dispatch({
type: 'UPDATE_TASK',
});
}
export const updateTaskSuccess = () => (dispatch: any) => {
export const updateTaskSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'UPDATE_TASK_SUCCESS',
});
}
export const updateTaskError = (error: {}) => (dispatch: any) => {
export const updateTaskError = (error: {}) => (dispatch: Dispatch) => {
dispatch({
type: 'UPDATE_TASK_ERROR',
payload: error,
});
}
export const deleteTask = () => (dispatch: any) => {
export const deleteTask = () => (dispatch: Dispatch) => {
dispatch({
type: 'DELETE_TASK',
});
}
export const deleteTaskSuccess = () => (dispatch: any) => {
export const deleteTaskSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'DELETE_TASK_SUCCESS',
});
}
export const deleteTaskError = (error: {}) => (dispatch: any) => {
export const deleteTaskError = (error: {}) => (dispatch: Dispatch) => {
dispatch({
type: 'DELETE_TASK_ERROR',
payload: error,
@ -81,7 +84,7 @@ export const deleteTaskError = (error: {}) => (dispatch: any) => {
}
export const getTasksAsync = (queryObject = {}) => {
return (dispatch: any) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(getTasks());
return (window as any).cvat.tasks.get(queryObject).then(
@ -98,7 +101,7 @@ export const getTasksAsync = (queryObject = {}) => {
}
export const createTaskAsync = (task: any) => {
return (dispatch: any) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(createTask());
return task.save().then(
@ -117,7 +120,7 @@ export const createTaskAsync = (task: any) => {
}
export const updateTaskAsync = (task: any) => {
return (dispatch: any) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(updateTask());
return task.save().then(
@ -135,8 +138,8 @@ export const updateTaskAsync = (task: any) => {
};
}
export const deleteTaskAsync = (task: any, history: any) => {
return (dispatch: any, getState: any) => {
export const deleteTaskAsync = (task: any, history: History) => {
return (dispatch: ActionCreator<Dispatch>, getState: any) => {
dispatch(deleteTask());
return task.delete().then(

@ -0,0 +1,40 @@
import { Dispatch, ActionCreator } from 'redux';
export const getUsers = () => (dispatch: Dispatch) => {
dispatch({
type: 'GET_USERS',
});
}
export const getUsersSuccess = (users: [], isCurrentUser: boolean) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_USERS_SUCCESS',
payload: users,
currentUser: isCurrentUser ? (users as any)[0] : isCurrentUser,
});
}
export const getUsersError = (error: {}) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_USERS_ERROR',
payload: error,
});
}
export const getUsersAsync = (filter = {}) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(getUsers());
return (window as any).cvat.users.get(filter).then(
(users: any) => {
dispatch(getUsersSuccess(users, (filter as any).self));
},
(error: any) => {
dispatch(getUsersError(error));
throw error;
},
);
};
}

@ -3,7 +3,9 @@ import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-d
import { connect } from 'react-redux';
import DashboardPage from '../dashboard-page/dashboard-page';
import HeaderLayout from '../header-layout/header-layout';
import TasksPage from '../tasks-page/tasks-page';
import LoginPage from '../login-page/login-page';
import RegisterPage from '../register-page/register-page';
import PageNotFound from '../page-not-found/page-not-found';
@ -16,12 +18,15 @@ const ProtectedRoute = ({ component: Component, ...rest }: any) => {
<Route
{ ...rest }
render={ (props) => {
return localStorage.getItem('session') ? (
<Component { ...props } />
return rest.isAuthenticated ? (
<>
<HeaderLayout />
<Component { ...props } />
</>
) : (
<Redirect
to={{
pathname: "/login",
pathname: '/login',
state: {
from: props.location,
},
@ -38,8 +43,8 @@ class App extends PureComponent<any, any> {
return(
<Router>
<Switch>
<Redirect path="/" exact to="/dashboard" />
<ProtectedRoute path="/dashboard" component={ DashboardPage } />
<Redirect path="/" exact to="/tasks" />
<ProtectedRoute isAuthenticated={ this.props.isAuthenticated } path="/tasks" component={ TasksPage } />
<Route path="/login" component={ LoginPage } />
<Route path="/register" component={ RegisterPage } />
<Route component={ PageNotFound } />

@ -1,53 +0,0 @@
import React, { PureComponent } from 'react';
import { Location, Action } from 'history';
import * as queryString from 'query-string';
import setQueryObject from '../../utils/tasks-filter'
import { connect } from 'react-redux';
import { getTasksAsync } from '../../actions/tasks.actions';
import { filterTasks } from '../../actions/tasks-filter.actions';
import { Layout } from 'antd';
import DashboardHeader from './header/dashboard-header';
import DashboardContent from './content/dashboard-content';
import DashboardFooter from './footer/dashboard-footer';
import './dashboard-page.scss';
class Dashboard extends PureComponent<any, any> {
componentDidMount() {
this.loadTasks(this.props.location.search);
this.props.history.listen((location: Location, action: Action) => {
this.loadTasks(location.search);
});
}
render() {
return (
<Layout className="layout">
<DashboardHeader />
<DashboardContent />
<DashboardFooter />
</Layout>
);
}
private loadTasks = (params: any) => {
const query = queryString.parse(params);
const queryObject = setQueryObject(query);
this.props.dispatch(filterTasks(queryObject));
this.props.dispatch(getTasksAsync(queryObject));
}
}
const mapStateToProps = (state: any) => {
return { ...state.tasks, ...state.tasksFilter };
};
export default connect(mapStateToProps)(Dashboard);

@ -1,34 +0,0 @@
.dashboard-header {
min-width: 1024px;
background: #001529;
&__logo {
height: 64px;
.logo {
color: white;
}
}
&__search {
text-align: center;
.search {
max-width: 300px;
}
}
&__actions {
text-align: right;
.action:not(:nth-child(1)) {
margin-left: 8px;
}
.action {
width: 100px;
}
}
.logo, .search, .action {
vertical-align: middle;
}
}

@ -0,0 +1,66 @@
.header-layout {
min-width: 1024px;
height: 100%;
padding: 0 16px;
line-height: initial;
background: #d8d8d8;
&__logo {
display: flex;
align-items: center;
justify-content: center;
img {
height: 18px;
}
}
&__menu {
.ant-menu {
font-size: 16px;
color: black;
background-color: #d8d8d8;
line-height: 44px;
border-bottom: none;
.ant-menu-item {
border-bottom: 3px solid transparent;
}
.last-menu-item {
float: right;
margin-right: 28px;
}
.ant-menu-item-selected, .ant-menu-item-active {
color: black !important;
border-bottom: 3px solid black !important;
background-color: #c3c3c3 !important;
}
a, a:hover {
color: black;
}
}
}
&__dropdown {
border-left: 1px solid #c3c3c3;
cursor: pointer;
display: flex;
align-items: center;
font-size: 16px;
color: black;
i:first-child {
margin-right: 12px;
font-size: 18px;
}
i:last-child {
margin-left: auto;
font-size: 18px;
}
}
}

@ -1,11 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import DashboardFooter from './dashboard-footer';
import HeaderLayout from './header-layout';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<DashboardFooter />, div);
ReactDOM.render(<HeaderLayout />, div);
ReactDOM.unmountComponentAtNode(div);
});

@ -0,0 +1,87 @@
import React, { PureComponent } from 'react';
import { Link, withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { logoutAsync } from '../../actions/auth.actions';
import { Layout, Row, Col, Menu, Dropdown, Icon } from 'antd';
import { ClickParam } from 'antd/lib/menu';
import './header-layout.scss';
const { Header } = Layout;
class HeaderLayout extends PureComponent<any, any> {
hostUrl: string | undefined;
constructor(props: any) {
super(props);
this.state = {
selectedMenuItem: null,
};
this.hostUrl = process.env.REACT_APP_API_HOST_URL;
}
componentDidMount() {
this.setState({ selectedMenuItem: this.props.location.pathname.split('/')[1] });
}
render() {
const dropdownMenu = (
<Menu>
<Menu.Item onClick={ this.logout } key="logout">Logout</Menu.Item>
</Menu>
);
return (
<Header className="header-layout">
<Row type="flex" gutter={24}>
<Col className="header-layout__logo" span={2}>
<img src="./images/cvat-logo.svg" alt="CVAT logo" />
</Col>
<Col className="header-layout__menu" span={18}>
<Menu onClick={ this.selectMenuItem } selectedKeys={ [this.state.selectedMenuItem] } mode="horizontal">
<Menu.Item key="tasks">
<Link to="/tasks">Tasks</Link>
</Menu.Item>
<Menu.Item disabled key="models">
<Link to="/models">Models</Link>
</Menu.Item>
<Menu.Item disabled key="analitics">
<Link to="/analitics">Analitics</Link>
</Menu.Item>
<a className="last-menu-item" href={ `${this.hostUrl}/documentation/user_guide.html` } target="blank">Help</a>
</Menu>
</Col>
<Dropdown className="header-layout__dropdown" overlay={ dropdownMenu } trigger={ ['click'] }>
<Col span={4}>
<Icon type="user" />
{ this.props.currentUser ? <span>{ this.props.currentUser.username }</span> : null }
<Icon type="caret-down" />
</Col>
</Dropdown>
</Row>
</Header>
);
}
private selectMenuItem = (event: ClickParam) => {
this.setState({ selectedMenuItem: event.key });
}
private logout = () => {
this.props.dispatch(logoutAsync());
}
}
const mapStateToProps = (state: any) => {
return { ...state.authContext, ...state.users };
};
export default withRouter(connect(mapStateToProps)(HeaderLayout) as any);

@ -1,66 +1,85 @@
import React, { PureComponent } from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { loginAsync } from '../../actions/auth.actions';
import { loginAsync, isAuthenticatedAsync } from '../../actions/auth.actions';
import { getUsersAsync } from '../../actions/users.actions';
import { Button, Icon, Input, Form, Col, Row } from 'antd';
import { Button, Icon, Input, Form, Col, Row, Spin } from 'antd';
import Title from 'antd/lib/typography/Title';
import './login-page.scss';
class LoginForm extends PureComponent<any, any> {
componentWillMount() {
if (localStorage.getItem('session')) {
this.props.history.push('/dashboard');
}
constructor(props: any) {
super(props);
this.state = { loading: false };
}
componentDidMount() {
this.setState({ loading: true });
this.props.dispatch(isAuthenticatedAsync()).then(
(isAuthenticated: boolean) => {
this.setState({ loading: false });
if (this.props.isAuthenticated) {
this.props.dispatch(getUsersAsync({ self: true }));
this.props.history.replace(this.props.location.state ? this.props.location.state.from : '/tasks');
}
}
);
}
render() {
const { getFieldDecorator } = this.props.form;
return (
<Row type="flex" justify="center" align="middle">
<Col xs={12} md={10} lg={8} xl={6}>
<Form className="login-form" onSubmit={ this.onSubmit }>
<Title className="login-form__title">Login</Title>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true, message: 'Please enter your username!' }],
})(
<Input
prefix={ <Icon type="user" /> }
type="text"
name="username"
placeholder="Username"
/>,
)}
</Form.Item>
<Form.Item>
{getFieldDecorator('password', {
rules: [{ required: true, message: 'Please enter your password!' }],
})(
<Input
prefix={ <Icon type="lock" /> }
type="password"
name="password"
placeholder="Password"
/>,
)}
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={ this.props.isFetching }>
Log in
</Button>
</Form.Item>
Have not registered yet? <a href="/register">Register here.</a>
</Form>
</Col>
</Row>
<Spin wrapperClassName="spinner" size="large" spinning={ this.state.loading }>
<Row type="flex" justify="center" align="middle">
<Col xs={12} md={10} lg={8} xl={6}>
<Form className="login-form" onSubmit={ this.onSubmit }>
<Title className="login-form__title">Login</Title>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true, message: 'Please enter your username!' }],
})(
<Input
prefix={ <Icon type="user" /> }
type="text"
name="username"
placeholder="Username"
/>,
)}
</Form.Item>
<Form.Item>
{getFieldDecorator('password', {
rules: [{ required: true, message: 'Please enter your password!' }],
})(
<Input
prefix={ <Icon type="lock" /> }
type="password"
name="password"
placeholder="Password"
/>,
)}
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={ this.props.isFetching }>
Log in
</Button>
</Form.Item>
Have not registered yet? <Link to="/register">Register here.</Link>
</Form>
</Col>
</Row>
</Spin>
);
}
@ -69,7 +88,11 @@ class LoginForm extends PureComponent<any, any> {
this.props.form.validateFields((error: any, values: any) => {
if (!error) {
this.props.dispatch(loginAsync(values.username, values.password, this.props.history));
this.props.dispatch(loginAsync(values.username, values.password, this.props.history)).then(
(loggedIn: any) => {
this.props.dispatch(getUsersAsync({ self: true }));
},
);
}
});
}

@ -189,18 +189,6 @@ class TaskCreateForm extends PureComponent<any, any> {
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Use LFS">
{getFieldDecorator('useLFS', {
rules: [],
initialValue: true,
})(
<Checkbox
defaultChecked
name="use-lfs"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Source">
{getFieldDecorator('source', {
rules: [],

@ -1,6 +1,3 @@
.not-found {
display: flex;
flex-direction: column;
justify-content: center;
.empty.not-found {
height: 100vh;
}

@ -1,16 +1,19 @@
import React, { PureComponent } from 'react';
import { Link } from 'react-router-dom';
import { Empty, Button } from 'antd';
import { Empty } from 'antd';
import './page-not-found.scss';
class PageNotFound extends PureComponent<any, any> {
render() {
return(
<Empty className="not-found" description="Page not found...">
<Button type="primary" href="/dashboard">
Go back to dashboard
</Button>
<Empty
className="empty not-found"
description="Page not found..."
image="./images/empty-tasks-icon.svg">
<Link to="/tasks">Go back to tasks</Link>
</Empty>
);
}

@ -1,9 +1,10 @@
import React, { PureComponent } from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
// import { registerAsync } from '../../actions/auth.actions';
import { registerAsync, isAuthenticatedAsync } from '../../actions/auth.actions';
import { Button, Icon, Input, Form, Col, Row } from 'antd';
import { Button, Icon, Input, Form, Col, Row, Spin } from 'antd';
import Title from 'antd/lib/typography/Title';
import './register-page.scss';
@ -13,134 +14,146 @@ class RegisterForm extends PureComponent<any, any> {
constructor(props: any) {
super(props);
this.state = { confirmDirty: false };
this.state = { confirmDirty: false, loading: false };
}
componentWillMount() {
if (localStorage.getItem('session')) {
this.props.history.push('/dashboard');
}
componentDidMount() {
this.setState({ loading: true });
this.props.dispatch(isAuthenticatedAsync()).then(
(isAuthenticated: boolean) => {
this.setState({ loading: false });
if (this.props.isAuthenticated) {
this.props.history.replace('/tasks');
}
}
);
}
render() {
const { getFieldDecorator } = this.props.form;
return (
<Row type="flex" justify="center" align="middle">
<Col xs={12} md={10} lg={8} xl={6}>
<Form className="register-form" onSubmit={ this.onSubmit }>
<Title className="register-form__title">Register</Title>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true, message: 'Please enter your username!' }],
})(
<Input
prefix={ <Icon type="user" /> }
type="text"
name="username"
placeholder="Username"
/>,
)}
</Form.Item>
<Form.Item>
{getFieldDecorator('firstName', {
rules: [],
})(
<Input
prefix={ <Icon type="idcard" /> }
type="text"
name="first-name"
placeholder="First name"
/>,
)}
</Form.Item>
<Form.Item>
{getFieldDecorator('lastName', {
rules: [],
})(
<Input
prefix={ <Icon type="idcard" /> }
type="text"
name="last-name"
placeholder="Last name"
/>,
)}
</Form.Item>
<Form.Item hasFeedback>
{getFieldDecorator('email', {
rules: [
{
type: 'email',
message: 'The input is not valid email!',
},
{
required: true,
message: 'Please input your email!',
},
],
})(
<Input
prefix={ <Icon type="mail" /> }
type="text"
name="email"
placeholder="Email"
/>,
)}
</Form.Item>
<Form.Item hasFeedback>
{getFieldDecorator('password', {
rules: [
{
required: true,
message: 'Please input your password!',
},
{
validator: this.validateToNextPassword,
},
],
})(
<Input.Password
prefix={ <Icon type="lock" /> }
name="password"
placeholder="Password"
/>,
)}
</Form.Item>
<Form.Item hasFeedback>
{getFieldDecorator('passwordConfirmation', {
rules: [
{
required: true,
message: 'Please confirm your password!',
},
{
validator: this.compareToFirstPassword,
},
],
})(
<Input.Password
onBlur={ this.handleConfirmBlur }
prefix={ <Icon type="lock" /> }
name="password-confirmation"
placeholder="Password confirmation"
/>,
)}
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={ this.props.isFetching }>
Register
</Button>
</Form.Item>
</Form>
</Col>
</Row>
<Spin wrapperClassName="spinner" size="large" spinning={ this.state.loading }>
<Row type="flex" justify="center" align="middle">
<Col xs={12} md={10} lg={8} xl={6}>
<Form className="register-form" onSubmit={ this.onSubmit }>
<Title className="register-form__title">Register</Title>
<Form.Item>
{getFieldDecorator('username', {
rules: [{ required: true, message: 'Please enter your username!' }],
})(
<Input
prefix={ <Icon type="user" /> }
type="text"
name="username"
placeholder="Username"
/>,
)}
</Form.Item>
<Form.Item>
{getFieldDecorator('firstName', {
rules: [],
})(
<Input
prefix={ <Icon type="idcard" /> }
type="text"
name="first-name"
placeholder="First name"
/>,
)}
</Form.Item>
<Form.Item>
{getFieldDecorator('lastName', {
rules: [],
})(
<Input
prefix={ <Icon type="idcard" /> }
type="text"
name="last-name"
placeholder="Last name"
/>,
)}
</Form.Item>
<Form.Item hasFeedback>
{getFieldDecorator('email', {
rules: [
{
type: 'email',
message: 'The input is not valid email!',
},
{
required: true,
message: 'Please input your email!',
},
],
})(
<Input
prefix={ <Icon type="mail" /> }
type="text"
name="email"
placeholder="Email"
/>,
)}
</Form.Item>
<Form.Item hasFeedback>
{getFieldDecorator('password', {
rules: [
{
required: true,
message: 'Please input your password!',
},
{
validator: this.validateToNextPassword,
},
],
})(
<Input.Password
prefix={ <Icon type="lock" /> }
name="password"
placeholder="Password"
/>,
)}
</Form.Item>
<Form.Item hasFeedback>
{getFieldDecorator('passwordConfirmation', {
rules: [
{
required: true,
message: 'Please confirm your password!',
},
{
validator: this.compareToFirstPassword,
},
],
})(
<Input.Password
onBlur={ this.handleConfirmBlur }
prefix={ <Icon type="lock" /> }
name="password-confirmation"
placeholder="Password confirmation"
/>,
)}
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" loading={ this.props.isFetching }>
Register
</Button>
</Form.Item>
Already have an account? <Link to="/login">Login here.</Link>
</Form>
</Col>
</Row>
</Spin>
);
}
@ -175,7 +188,17 @@ class RegisterForm extends PureComponent<any, any> {
this.props.form.validateFields((error: any, values: any) => {
if (!error) {
// this.props.dispatch(registerAsync(values.username, values.password, this.props.history));
this.props.dispatch(
registerAsync(
values.username,
values.firstName,
values.lastName,
values.email,
values.password,
values.passwordConfirmation,
this.props.history,
),
);
}
});
}

@ -1,10 +1,10 @@
.dashboard-content {
.tasks-content {
width: 1024px;
min-height: calc(100vh - 64px - 64px);
min-height: calc(100vh - 90px - 64px - 46px);
margin: 0 auto;
.dashboard-content-сard {
margin: 20px;
.tasks-content-сard {
margin-bottom: 20px;
padding: 20px;
border: 1px solid #001529;
border-radius: 3px;
@ -43,10 +43,3 @@
}
}
}
.empty {
display: flex;
flex-direction: column;
justify-content: center;
height: calc(100vh - 64px - 64px);
}

@ -1,11 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import DashboardHeader from './dashboard-header';
import TasksContent from './tasks-content';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<DashboardHeader />, div);
ReactDOM.render(<TasksContent />, div);
ReactDOM.unmountComponentAtNode(div);
});

@ -18,12 +18,12 @@ import TaskCreateForm from '../../modals/task-create/task-create';
import { deserializeLabels, taskDTO } from '../../../utils/tasks-dto';
import './dashboard-content.scss';
import './tasks-content.scss';
const { Content } = Layout;
class DashboardContent extends Component<any, any> {
class TasksContent extends Component<any, any> {
hostUrl: string | undefined;
apiUrl: string | undefined;
@ -68,35 +68,41 @@ class DashboardContent extends Component<any, any> {
render() {
return(
<>
{ this.props.tasks.length ? this.renderTasks() : this.renderPlaceholder() }
{ this.props.tasks.length ? this.renderTasks() : this.renderEmpty() }
</>
);
}
private renderPlaceholder() {
private renderEmpty() {
return (
<Empty className="empty" description="No tasks found...">
<Button type="primary" onClick={ this.onCreateTask }>
Create task
<Empty
className="empty"
description="No tasks created yet..."
image="./images/empty-tasks-icon.svg">
<span>To get started with your annotation project</span>
<Button type="link" onClick={ this.onCreateTask }>
create a new task
</Button>
{/* // TODO: uncomment when modals -> pages */}
{/* <Link to="/tasks">create a new task</Link> */}
</Empty>
)
}
private renderTasks() {
return (
<Content className="dashboard-content">
<Content className="tasks-content">
{
this.props.tasks.map(
(task: any) => (
<div className="dashboard-content-сard" key={ task.id }>
<Row className="dashboard-content-сard__header" type="flex">
<div className="tasks-content-сard" key={ task.id }>
<Row className="tasks-content-сard__header" type="flex">
<Col span={24}>
<Title level={2}>{ `${task.name}: ${task.mode}` }</Title>
</Col>
</Row>
<Row className="dashboard-content-сard__content" type="flex">
<Row className="tasks-content-сard__content" type="flex">
<Col className="card-cover" span={8}>
<img alt="Task cover" src={ `${this.apiUrl}/tasks/${task.id}/frames/0` } />
</Col>
@ -301,7 +307,7 @@ class DashboardContent extends Component<any, any> {
});
}
private onDumpAnnotation = (task: any, event: any, component: DashboardContent) => {
private onDumpAnnotation = (task: any, event: any, component: TasksContent) => {
const dumper = component.state.dumpers.find((dumper: any) => dumper.name === event.key);
component.setState({ activeTaskId: task.id });
@ -352,4 +358,4 @@ const mapStateToProps = (state: any) => {
return { ...state.tasks, ...state.server, ...state.annotations };
};
export default withRouter(connect(mapStateToProps)(DashboardContent) as any);
export default withRouter(connect(mapStateToProps)(TasksContent) as any);

@ -1,4 +1,4 @@
.dashboard-footer {
.tasks-footer {
display: flex;
align-items: center;
justify-content: center;

@ -1,11 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import DashboardContent from './dashboard-content';
import TasksFooter from './tasks-footer';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<DashboardContent />, div);
ReactDOM.render(<TasksFooter />, div);
ReactDOM.unmountComponentAtNode(div);
});

@ -8,18 +8,19 @@ import { connect } from 'react-redux';
import { Layout, Pagination, Row, Col } from 'antd';
import './dashboard-footer.scss';
import './tasks-footer.scss';
const { Footer } = Layout;
class DashboardFooter extends PureComponent<any, any> {
class TasksFooter extends PureComponent<any, any> {
render() {
return(
<Footer className="dashboard-footer">
<Footer className="tasks-footer">
<Row type="flex" gutter={16}>
<Col span={24}>
<Pagination
className="dashboard-footer__pagination"
className="tasks-footer__pagination"
current={ this.props.currentPage || 1 }
hideOnSinglePage
onChange={ this.onPageChange }
@ -42,4 +43,4 @@ const mapStateToProps = (state: any) => {
return { ...state.tasks, ...state.tasksFilter };
};
export default withRouter(connect(mapStateToProps)(DashboardFooter) as any);
export default withRouter(connect(mapStateToProps)(TasksFooter) as any);

@ -0,0 +1,41 @@
.tasks-header {
display: flex;
align-items: center;
width: 1024px;
height: 90px;
line-height: initial;
margin: 0 auto;
padding: 0;
background-color: #f0f2f5;
&__logo {
font-size: 20px;
.logo {
margin: 0;
color: black;
}
}
&__search {
text-align: center;
.search {
max-width: 300px;
}
}
&__actions {
text-align: right;
.action:not(:nth-child(1)) {
margin-left: 8px;
}
.action {
width: 180px;
font-size: 16px;
line-height: 19px;
}
}
}

@ -0,0 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import TasksHeader from './tasks-header';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<TasksHeader />, div);
ReactDOM.unmountComponentAtNode(div);
});

@ -5,30 +5,26 @@ import { withRouter } from 'react-router-dom';
import { connect } from 'react-redux';
import { createTaskAsync } from '../../../actions/tasks.actions';
import { Modal, Layout, Row, Col, Button, Input } from 'antd';
import { Modal, Layout, Col, Button, Input } from 'antd';
import Title from 'antd/lib/typography/Title';
import TaskCreateForm from '../../modals/task-create/task-create';
import { taskDTO } from '../../../utils/tasks-dto';
import './dashboard-header.scss';
import './tasks-header.scss';
const { Header } = Layout;
const { Search } = Input;
class DashboardHeader extends Component<any, any> {
hostUrl: string | undefined;
class TasksHeader extends Component<any, any> {
createFormRef: any;
constructor(props: any) {
super(props);
this.state = { searchQuery: this.props.searchQuery };
this.hostUrl = process.env.REACT_APP_API_HOST_URL;
}
componentDidUpdate(prevProps: any) {
@ -39,37 +35,29 @@ class DashboardHeader extends Component<any, any> {
render() {
return(
<Header className="dashboard-header">
<Row type="flex" gutter={16}>
<Col className="dashboard-header__logo" span={8}>
<Title className="logo">Tasks</Title>
</Col>
<Col className="dashboard-header__search" span={8}>
<Search
className="search"
placeholder="Search for tasks"
value={ this.state.searchQuery }
onChange={ this.onValueChange }
onSearch={ query => this.onSearch(query) }
enterButton>
</Search>
</Col>
<Col className="dashboard-header__actions" span={8}>
<Button
className="action"
type="primary"
onClick={ this.onCreateTask }>
Create task
</Button>
<Button
className="action"
type="primary"
href={ `${this.hostUrl}/documentation/user_guide.html` }
target="blank">
User guide
</Button>
</Col>
</Row>
<Header className="tasks-header">
<Col className="tasks-header__logo" span={3}>
<Title className="logo">Tasks</Title>
</Col>
<Col className="tasks-header__search" span={6}>
<Search
className="search"
size="large"
placeholder="Search"
value={ this.state.searchQuery }
onChange={ this.onValueChange }
onSearch={ query => this.onSearch(query) }>
</Search>
</Col>
<Col className="tasks-header__actions" span={15}>
<Button
className="action"
size="large"
type="primary"
onClick={ this.onCreateTask }>
Create task
</Button>
</Col>
</Header>
);
}
@ -129,4 +117,4 @@ const mapStateToProps = (state: any) => {
return { ...state.tasks, ...state.tasksFilter };
};
export default withRouter(connect(mapStateToProps)(DashboardHeader) as any);
export default withRouter(connect(mapStateToProps)(TasksHeader) as any);

@ -1,11 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import Dashboard from './dashboard-page';
import TasksPage from './tasks-page';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<Dashboard />, div);
ReactDOM.render(<TasksPage />, div);
ReactDOM.unmountComponentAtNode(div);
});

@ -0,0 +1,59 @@
import React, { PureComponent } from 'react';
import { Location, Action } from 'history';
import * as queryString from 'query-string';
import setQueryObject from '../../utils/tasks-filter'
import { connect } from 'react-redux';
import { getTasksAsync } from '../../actions/tasks.actions';
import { filterTasks } from '../../actions/tasks-filter.actions';
import { Layout, Spin } from 'antd';
import TasksHeader from './tasks-header/tasks-header';
import TasksContent from './tasks-content/tasks-content';
import TasksFooter from './tasks-footer/tasks-footer';
import './tasks-page.scss';
class TasksPage extends PureComponent<any, any> {
componentDidMount() {
this.loadTasks(this.props.location.search);
this.props.history.listen(
(location: Location, action: Action) => {
if (location.pathname.includes('tasks')) {
this.loadTasks(location.search);
}
}
);
}
render() {
return (
<Spin wrapperClassName="spinner" size="large" spinning={ this.props.isFetching }>
<Layout className="layout">
<TasksHeader />
<TasksContent />
<TasksFooter />
</Layout>
</Spin>
);
}
private loadTasks = (params: any) => {
const query = queryString.parse(params);
const queryObject = setQueryObject(query);
this.props.dispatch(filterTasks(queryObject));
this.props.dispatch(getTasksAsync(queryObject));
}
}
const mapStateToProps = (state: any) => {
return { ...state.authContext, ...state.tasks, ...state.tasksFilter };
};
export default connect(mapStateToProps)(TasksPage);

@ -8,10 +8,50 @@ body {
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
}
// TODO: remove when modals -> pages
.crud-modal {
width: 90% !important;
}
.spinner {
.ant-spin-spinning {
max-height: 100% !important;
background: white !important;
}
}
.empty {
display: flex;
flex-direction: column;
justify-content: center;
height: calc(100vh - 90px - 64px - 46px);
&.ant-empty {
font-size: 16px;
line-height: 19px;
.ant-empty-image {
margin-bottom: 30px;
}
.ant-empty-description {
font-size: 20px;
font-weight: bold;
}
.ant-empty-footer {
margin-top: 10px;
.ant-btn {
display: block;
margin: auto;
height: auto;
font-size: 16px;
line-height: 19px;
}
}
}
}

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

@ -1,27 +1,44 @@
import { AnyAction } from 'redux';
export default (
state = {
isAuthenticated: false,
isFetching: false,
error: null,
},
action: any,
action: AnyAction,
) => {
switch (action.type) {
case 'LOGIN':
return Object.assign({}, state, {
isFetching: true,
});
return { ...state, isFetching: true };
case 'LOGIN_SUCCESS':
return Object.assign({}, state, {
isFetching: false,
isAuthenticated: true,
});
return { ...state, isFetching: false, isAuthenticated: true };
case 'LOGIN_ERROR':
return Object.assign({}, state, {
isFetching: false,
isAuthenticated: false,
error: action.payload,
});
return { ...state, isFetching: false, isAuthenticated: false, error: action.payload };
case 'LOGOUT':
return { ...state, isFetching: true };
case 'LOGOUT_SUCCESS':
return { ...state, isFetching: false, isAuthenticated: false };
case 'LOGOUT_ERROR':
return { ...state, isFetching: false, error: action.payload };
case 'IS_AUTHENTICATED':
return { ...state, isFetching: true };
case 'IS_AUTHENTICATED_SUCCESS':
return { ...state, isFetching: false, isAuthenticated: true };
case 'IS_AUTHENTICATED_FAIL':
return { ...state, isFetching: false, isAuthenticated: false };
case 'IS_AUTHENTICATED_ERROR':
return { ...state, isFetching: false, error: action.payload };
case 'REGISTER':
return { ...state, isFetching: true };
case 'REGISTER_SUCCESS':
return { ...state, isFetching: false };
case 'REGISTER_ERROR':
return { ...state, isFetching: false, error: action.payload };
default:
return state;
}

@ -1,15 +1,33 @@
import { combineReducers } from 'redux';
import { combineReducers, AnyAction } from 'redux';
import authContext from './auth.reducer';
import tasks from './tasks.reducer';
import users from './users.reducer';
import tasksFilter from './tasks-filter.reducer';
import server from './server.reducer';
import annotations from './annotations.reducer';
// TODO: investigate a better way to handle
// INFO: global errors handler reducer
const errorMessage = (state = null, action: AnyAction) => {
const { type, payload } = action;
if (type === 'RESET_ERROR_MESSAGE') {
return null;
} else if (type.endsWith('ERROR')) {
return payload;
}
return state;
}
export default combineReducers({
authContext,
tasks,
users,
tasksFilter,
server,
annotations,
errorMessage,
});

@ -1,3 +1,6 @@
import { AnyAction } from 'redux';
export default (
state = {
info: null,
@ -6,51 +9,29 @@ export default (
isFetching: false,
error: null,
},
action: any,
action: AnyAction,
) => {
switch (action.type) {
case 'GET_SERVER_INFO':
return Object.assign({}, state, {
isFetching: true,
});
return { ...state, isFetching: true };
case 'GET_SERVER_INFO_SUCCESS':
return Object.assign({}, state, {
isFetching: false,
info: action.payload,
});
return { ...state, isFetching: false, info: action.payload };
case 'GET_SERVER_INFO_ERROR':
return Object.assign({}, state, {
isFetching: false,
error: action.payload,
});
return { ...state, isFetching: false, error: action.payload };
case 'GET_SHARE_FILES':
return Object.assign({}, state, {
isFetching: true,
});
return { ...state, isFetching: true };
case 'GET_SHARE_FILES_SUCCESS':
return Object.assign({}, state, {
isFetching: false,
files: action.payload,
});
return { ...state, isFetching: false, files: action.payload };
case 'GET_SHARE_FILES_ERROR':
return Object.assign({}, state, {
isFetching: false,
error: action.payload,
});
return { ...state, isFetching: false, error: action.payload };
case 'GET_ANNOTATION_FORMATS':
return Object.assign({}, state, {
isFetching: true,
});
return { ...state, isFetching: true };
case 'GET_ANNOTATION_FORMATS_SUCCESS':
return Object.assign({}, state, {
isFetching: false,
annotationFormats: action.payload,
});
return { ...state, isFetching: false, annotationFormats: action.payload };
case 'GET_ANNOTATION_FORMATS_ERROR':
return Object.assign({}, state, {
isFetching: false,
error: action.payload,
});
return { ...state, isFetching: false, error: action.payload };
default:
return state;
}

@ -1,16 +1,16 @@
import { AnyAction } from 'redux';
export default (
state = {
searchQuery: '',
currentPage: 1
},
action: any,
action: AnyAction,
) => {
switch (action.type) {
case 'FILTER_TASKS':
return Object.assign({}, state, {
searchQuery: action.payload.search,
currentPage: action.payload.page,
});
return { ...state, searchQuery: action.payload.search, currentPage: action.payload.page };
default:
return state;
}

@ -1,3 +1,6 @@
import { AnyAction } from 'redux';
export default (
state: any = {
tasks: [],
@ -5,68 +8,36 @@ export default (
isFetching: false,
error: null,
},
action: any,
action: AnyAction,
) => {
switch (action.type) {
case 'GET_TASKS':
return Object.assign({}, state, {
isFetching: true,
});
return { ...state, isFetching: true };
case 'GET_TASKS_SUCCESS':
return Object.assign({}, state, {
isFetching: false,
tasks: Array.from(action.payload.values()),
tasksCount: action.payload.count,
});
return { ...state, isFetching: false, tasks: Array.from(action.payload.values()), tasksCount: action.payload.count };
case 'GET_TASKS_ERROR':
return Object.assign({}, state, {
isFetching: false,
error: action.payload,
});
return { ...state, isFetching: false, error: action.payload };
case 'CREATE_TASK':
return Object.assign({}, state, {
isFetching: true,
});
return { ...state, isFetching: true };
case 'CREATE_TASK_SUCCESS':
return Object.assign({}, state, {
isFetching: false,
});
return { ...state, isFetching: false };
case 'CREATE_TASK_ERROR':
return Object.assign({}, state, {
isFetching: false,
error: action.payload,
});
return { ...state, isFetching: false, error: action.payload };
case 'UPDATE_TASK':
return Object.assign({}, state, {
isFetching: true,
});
return { ...state, isFetching: true };
case 'UPDATE_TASK_SUCCESS':
return Object.assign({}, state, {
isFetching: false,
});
return { ...state, isFetching: false };
case 'UPDATE_TASK_ERROR':
return Object.assign({}, state, {
isFetching: false,
error: action.payload,
});
return { ...state, isFetching: false, error: action.payload };
case 'DELETE_TASK':
return Object.assign({}, state, {
isFetching: true,
});
return { ...state, isFetching: true };
case 'DELETE_TASK_SUCCESS':
return Object.assign({}, state, {
isFetching: false,
});
return { ...state, isFetching: false };
case 'DELETE_TASK_ERROR':
return Object.assign({}, state, {
isFetching: false,
error: action.payload,
});
return { ...state, isFetching: false, error: action.payload };
default:
return state;
}

@ -0,0 +1,23 @@
import { AnyAction } from 'redux';
export default (
state: any = {
users: [],
currentUser: null,
isFetching: false,
error: null,
},
action: AnyAction,
) => {
switch (action.type) {
case 'GET_USERS':
return { ...state, isFetching: true };
case 'GET_USERS_SUCCESS':
return { ...state, isFetching: false, users: Array.from(action.payload.values()), currentUser: action.currentUser };
case 'GET_USERS_ERROR':
return { ...state, isFetching: false, error: action.payload };
default:
return state;
}
}

@ -77,7 +77,7 @@ export function serializeLabels(task: any) {
}
export function deserializeLabels(serialized: string) {
const normalized = serialized.replace(/'+/g, '\'').replace(/"+/g, '"').replace(/\s+/g, ' ').trim();
const normalized = serialized.replace(/'+/g, '\'').replace(/\s+/g, ' ').trim();
const fragments = customSplit(normalized, ' ');
const deserialized = [];

Loading…
Cancel
Save