User interface with React and antd (#785)

* Dump & refactoring
* Upload annotations, cvat-core from sources
* Added download icon
* Added icon
main
Boris Sekachev 6 years ago committed by Nikita Manovich
parent 4361bc548c
commit 5f511b7543

@ -6,6 +6,6 @@
/.vscode
/db.sqlite3
/keys
/cvat-canvas
**/node_modules
cvat-ui
cvat-canvas

@ -18,19 +18,27 @@ ENV LANG='C.UTF-8' \
RUN apt update && apt install -yq nodejs npm curl && \
npm install -g n && n 10.16.3
# Create output directory
RUN mkdir /tmp/cvat-ui
WORKDIR /tmp/cvat-ui/
# Create output directories
RUN mkdir /tmp/cvat-ui /tmp/cvat-core
# Install dependencies
COPY package*.json /tmp/cvat-ui/
COPY cvat-core/package*.json /tmp/cvat-core/
COPY cvat-ui/package*.json /tmp/cvat-ui/
# Install cvat-core dependencies
WORKDIR /tmp/cvat-core/
RUN npm install
# Install cvat-ui dependencies
WORKDIR /tmp/cvat-ui/
RUN npm install
# Build source code
COPY . /tmp/cvat-ui/
COPY cvat-core/ /tmp/cvat-core/
COPY cvat-ui/ /tmp/cvat-ui/
RUN mv .env.production .env && npm run build
FROM nginx
# Replace default.conf configuration to remove unnecessary rules
COPY react_nginx.conf /etc/nginx/conf.d/default.conf
COPY cvat-ui/react_nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=cvat-ui /tmp/cvat-ui/dist /usr/share/nginx/html/

@ -1,5 +1,5 @@
dist
docs
node_modules
reports
/dist
/docs
/node_modules
/reports

@ -187,7 +187,7 @@
* You need upload annotations from a server again after successful executing
* @method upload
* @memberof Session.annotations
* @param {File} annotations - a text file with annotations
* @param {File} annotations - a file with annotations
* @param {module:API.cvat.classes.Loader} loader - a loader
* which will be used to upload
* @instance

@ -1 +1 @@
node_modules
/node_modules

@ -2,5 +2,4 @@
/node_modules
/dist
!/dist/assets

Binary file not shown.

After

Width:  |  Height:  |  Size: 100 KiB

@ -39,7 +39,6 @@
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz",
"integrity": "sha512-27d4lZoomVyo51VegxI20xZPuSHusqbQag/ztrBC7wegWoQ1nLREPVSKSW8byhTlzTKyNE4ifaTA6lCp7JjpFw==",
"dev": true,
"requires": {
"@babel/highlight": "^7.0.0"
}
@ -70,7 +69,6 @@
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.6.2.tgz",
"integrity": "sha512-j8iHaIW4gGPnViaIHI7e9t/Hl8qLjERI6DcV9kEpAIDJsAOrcnXqRS7t+QbhL76pwbtqP+QCQLL0z1CyVmtjjQ==",
"dev": true,
"requires": {
"@babel/types": "^7.6.0",
"jsesc": "^2.5.1",
@ -122,7 +120,6 @@
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.6.0.tgz",
"integrity": "sha512-O1QWBko4fzGju6VoVvrZg0RROCVifcLxiApnGP3OWfWzvxRZFCoBD81K5ur5e3bVY2Vf/5rIJm8cqPKn8HUJng==",
"dev": true,
"requires": {
"@babel/helper-function-name": "^7.1.0",
"@babel/helper-member-expression-to-functions": "^7.5.5",
@ -157,7 +154,6 @@
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.1.0.tgz",
"integrity": "sha512-A95XEoCpb3TO+KZzJ4S/5uW5fNe26DjBGqf1o9ucyLyCmi1dXq/B3c8iaWTfBk3VvetUxl16e8tIrd5teOCfGw==",
"dev": true,
"requires": {
"@babel/helper-get-function-arity": "^7.0.0",
"@babel/template": "^7.1.0",
@ -168,7 +164,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0.tgz",
"integrity": "sha512-r2DbJeg4svYvt3HOS74U4eWKsUAMRH01Z1ds1zx8KNTPtpTL5JAsdFv8BNyOpVqdFhHkkRDIg5B4AsxmkjAlmQ==",
"dev": true,
"requires": {
"@babel/types": "^7.0.0"
}
@ -186,7 +181,6 @@
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.5.5.tgz",
"integrity": "sha512-5qZ3D1uMclSNqYcXqiHoA0meVdv+xUEex9em2fqMnrk/scphGlGgg66zjMrPJESPwrFJ6sbfFQYUSa0Mz7FabA==",
"dev": true,
"requires": {
"@babel/types": "^7.5.5"
}
@ -218,7 +212,6 @@
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.0.0.tgz",
"integrity": "sha512-u8nd9NQePYNQV8iPWu/pLLYBqZBa4ZaY1YWRFMuxrid94wKI1QNt67NEZ7GAe5Kc/0LLScbim05xZFWkAdrj9g==",
"dev": true,
"requires": {
"@babel/types": "^7.0.0"
}
@ -226,8 +219,7 @@
"@babel/helper-plugin-utils": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.0.0.tgz",
"integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA==",
"dev": true
"integrity": "sha512-CYAOUCARwExnEixLdB6sDm2dIJ/YgEAKDM1MOeMeZu9Ld/bDgVo8aiWrXwcY7OBh+1Ea2uUcVRcxKk0GJvW7QA=="
},
"@babel/helper-regex": {
"version": "7.5.5",
@ -255,7 +247,6 @@
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.5.5.tgz",
"integrity": "sha512-XvRFWrNnlsow2u7jXDuH4jDDctkxbS7gXssrP4q2nUD606ukXHRvydj346wmNg+zAgpFx4MWf4+usfC93bElJg==",
"dev": true,
"requires": {
"@babel/helper-member-expression-to-functions": "^7.5.5",
"@babel/helper-optimise-call-expression": "^7.0.0",
@ -277,7 +268,6 @@
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.4.4.tgz",
"integrity": "sha512-Ro/XkzLf3JFITkW6b+hNxzZ1n5OQ80NvIUdmHspih1XAhtN3vPTuUFT4eQnela+2MaZ5ulH+iyP513KJrxbN7Q==",
"dev": true,
"requires": {
"@babel/types": "^7.4.4"
}
@ -309,7 +299,6 @@
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.5.0.tgz",
"integrity": "sha512-7dV4eu9gBxoM0dAnj/BCFDW9LFU0zvTrkq0ugM7pnHEgguOEeOz1so2ZghEdzviYzQEED0r4EAgpsBChKy1TRQ==",
"dev": true,
"requires": {
"chalk": "^2.0.0",
"esutils": "^2.0.2",
@ -319,8 +308,7 @@
"@babel/parser": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.6.2.tgz",
"integrity": "sha512-mdFqWrSPCmikBoaBYMuBulzTIKuXVPtEISFbRRVNwMWpCms/hmE2kRq0bblUHaNRKrjRlmVbx1sDHmjmRgD2Xg==",
"dev": true
"integrity": "sha512-mdFqWrSPCmikBoaBYMuBulzTIKuXVPtEISFbRRVNwMWpCms/hmE2kRq0bblUHaNRKrjRlmVbx1sDHmjmRgD2Xg=="
},
"@babel/plugin-proposal-async-generator-functions": {
"version": "7.2.0",
@ -333,6 +321,15 @@
"@babel/plugin-syntax-async-generators": "^7.2.0"
}
},
"@babel/plugin-proposal-class-properties": {
"version": "7.5.5",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz",
"integrity": "sha512-AF79FsnWFxjlaosgdi421vmYG6/jg79bVD0dpD44QdgobzHKuLZ6S3vl8la9qIeSwGi8i1fS0O1mfuDAAdo1/A==",
"requires": {
"@babel/helper-create-class-features-plugin": "^7.5.5",
"@babel/helper-plugin-utils": "^7.0.0"
}
},
"@babel/plugin-proposal-dynamic-import": {
"version": "7.5.0",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.5.0.tgz",
@ -906,7 +903,6 @@
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz",
"integrity": "sha512-5AEH2EXD8euCk446b7edmgFdub/qfH1SN6Nii3+fyXP807QRx9Q73A2N5hNwRRslC2H9sNzaFhsPubkS4L8oNQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.0.0",
"@babel/parser": "^7.6.0",
@ -917,7 +913,6 @@
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.6.2.tgz",
"integrity": "sha512-8fRE76xNwNttVEF2TwxJDGBLWthUkHWSldmfuBzVRmEDWOtu4XdINTgN7TDWzuLg4bbeIMLvfMFD9we5YcWkRQ==",
"dev": true,
"requires": {
"@babel/code-frame": "^7.5.5",
"@babel/generator": "^7.6.2",
@ -934,7 +929,6 @@
"version": "7.6.1",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.6.1.tgz",
"integrity": "sha512-X7gdiuaCmA0uRjCmRtYJNAVCc/q+5xSgsfKJHqMN4iNLILX39677fJE1O40arPMh0TTtS9ItH67yre6c7k6t0g==",
"dev": true,
"requires": {
"esutils": "^2.0.2",
"lodash": "^4.17.13",
@ -1383,7 +1377,6 @@
"version": "3.2.1",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
"integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
"dev": true,
"requires": {
"color-convert": "^1.9.0"
}
@ -2119,7 +2112,6 @@
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
"integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.1",
"escape-string-regexp": "^1.0.5",
@ -2287,7 +2279,6 @@
"version": "1.9.3",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
"integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
"dev": true,
"requires": {
"color-name": "1.1.3"
}
@ -2295,8 +2286,7 @@
"color-name": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=",
"dev": true
"integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU="
},
"commander": {
"version": "2.20.0",
@ -2749,7 +2739,6 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz",
"integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==",
"dev": true,
"requires": {
"ms": "^2.1.1"
}
@ -3299,8 +3288,7 @@
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=",
"dev": true
"integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ="
},
"eslint-config-airbnb": {
"version": "17.1.1",
@ -3565,8 +3553,7 @@
"esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="
},
"etag": {
"version": "1.8.1",
@ -4813,8 +4800,7 @@
"globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"dev": true
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA=="
},
"globby": {
"version": "6.1.0",
@ -4890,8 +4876,7 @@
"has-flag": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=",
"dev": true
"integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0="
},
"has-symbols": {
"version": "1.0.0",
@ -5664,8 +5649,7 @@
"jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==",
"dev": true
"integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA=="
},
"json-parse-better-errors": {
"version": "1.0.2",
@ -6195,8 +6179,7 @@
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
"dev": true
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"multicast-dns": {
"version": "6.2.3",
@ -8782,8 +8765,7 @@
"source-map": {
"version": "0.5.7",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz",
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=",
"dev": true
"integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w="
},
"source-map-resolve": {
"version": "0.5.2",
@ -9139,7 +9121,6 @@
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
"integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
"dev": true,
"requires": {
"has-flag": "^3.0.0"
}
@ -9306,8 +9287,7 @@
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=",
"dev": true
"integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4="
},
"to-object-path": {
"version": "0.3.0",

@ -31,6 +31,7 @@
"webpack-dev-server": "^3.8.0"
},
"dependencies": {
"@babel/plugin-proposal-class-properties": "^7.5.5",
"@types/react": "^16.9.2",
"@types/react-dom": "^16.9.0",
"@types/react-redux": "^7.1.2",

File diff suppressed because one or more lines are too long

@ -25,11 +25,11 @@ export function registerSuccess(user: any): AnyAction {
};
}
export function registerFailed(registerError: any): AnyAction {
export function registerFailed(error: any): AnyAction {
return {
type: AuthActionTypes.REGISTER_FAILED,
payload: {
registerError,
error,
},
};
}
@ -43,11 +43,11 @@ export function loginSuccess(user: any): AnyAction {
};
}
export function loginFailed(loginError: any): AnyAction {
export function loginFailed(error: any): AnyAction {
return {
type: AuthActionTypes.LOGIN_FAILED,
payload: {
loginError,
error,
},
};
}
@ -59,11 +59,11 @@ export function logoutSuccess(): AnyAction {
};
}
export function logoutFailed(logoutError: any): AnyAction {
export function logoutFailed(error: any): AnyAction {
return {
type: AuthActionTypes.LOGOUT_FAILED,
payload: {
logoutError,
error,
},
};
}
@ -77,30 +77,23 @@ export function authorizedSuccess(user: any): AnyAction {
};
}
export function authorizedFailed(authError: any): AnyAction {
export function authorizedFailed(error: any): AnyAction {
return {
type: AuthActionTypes.AUTHORIZED_FAILED,
payload: {
authError,
error,
},
};
}
export function registerAsync({
username,
firstName,
lastName,
email,
password1,
password2,
}: {
username: string;
firstName: string;
lastName: string;
email: string;
password1: string;
password2: string;
}): ThunkAction<Promise<void>, {}, {}, AnyAction> {
export function registerAsync(
username: string,
firstName: string,
lastName: string,
email: string,
password1: string,
password2: string,
): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
let users = null;
try {
@ -116,7 +109,7 @@ export function registerAsync({
};
}
export function loginAsync({ username, password }: {username: string; password: string}):
export function loginAsync(username: string, password: string):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
let users = null;

@ -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));
};
}

@ -7,11 +7,27 @@ import getCore from '../core';
const cvat = getCore();
export enum TasksActionTypes {
GET_TASKS = 'GET_TASKS',
GET_TASKS_SUCCESS = 'GET_TASKS_SUCCESS',
GET_TASKS_FAILED = 'GET_TASKS_FAILED',
LOAD_ANNOTATIONS = 'LOAD_ANNOTATIONS',
LOAD_ANNOTATIONS_SUCCESS = 'LOAD_ANNOTATIONS_SUCCESS',
LOAD_ANNOTATIONS_FAILED = 'LOAD_ANNOTATIONS_FAILED',
DUMP_ANNOTATIONS = 'DUMP_ANNOTATIONS',
DUMP_ANNOTATIONS_SUCCESS = 'DUMP_ANNOTATIONS_SUCCESS',
DUMP_ANNOTATIONS_FAILED = 'DUMP_ANNOTATIONS_FAILED',
}
export function getTasksSuccess(array: any[], previews: string[],
function getTasks(): AnyAction {
const action = {
type: TasksActionTypes.GET_TASKS,
payload: {},
};
return action;
}
function getTasksSuccess(array: any[], previews: string[],
count: number, query: TasksQuery): AnyAction {
const action = {
type: TasksActionTypes.GET_TASKS_SUCCESS,
@ -26,7 +42,7 @@ export function getTasksSuccess(array: any[], previews: string[],
return action;
}
export function getTasksFailed(error: any, query: TasksQuery): AnyAction {
function getTasksFailed(error: any, query: TasksQuery): AnyAction {
const action = {
type: TasksActionTypes.GET_TASKS_FAILED,
payload: {
@ -41,6 +57,8 @@ export function getTasksFailed(error: any, query: TasksQuery): AnyAction {
export function getTasksAsync(query: TasksQuery):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(getTasks());
// We need remove all keys with null values from query
const filteredQuery = { ...query };
for (const key in filteredQuery) {
@ -78,3 +96,106 @@ ThunkAction<Promise<void>, {}, {}, AnyAction> {
dispatch(getTasksSuccess(array, previews, result.count, query));
};
}
function dumpAnnotation(task: any, dumper: any): AnyAction {
const action = {
type: TasksActionTypes.DUMP_ANNOTATIONS,
payload: {
task,
dumper,
},
};
return action;
}
function dumpAnnotationSuccess(task: any, dumper: any): AnyAction {
const action = {
type: TasksActionTypes.DUMP_ANNOTATIONS_SUCCESS,
payload: {
task,
dumper,
},
};
return action;
}
function dumpAnnotationFailed(task: any, dumper: any, error: any): AnyAction {
const action = {
type: TasksActionTypes.DUMP_ANNOTATIONS_FAILED,
payload: {
task,
dumper,
error,
},
};
return action;
}
export function dumpAnnotationsAsync(task: any, dumper: any):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
dispatch(dumpAnnotation(task, dumper));
const url = await task.annotations.dump(task.name, dumper);
window.location.assign(url);
} catch (error) {
dispatch(dumpAnnotationFailed(task, dumper, error));
return;
}
dispatch(dumpAnnotationSuccess(task, dumper));
};
}
function loadAnnotations(task: any, loader: any): AnyAction {
const action = {
type: TasksActionTypes.LOAD_ANNOTATIONS,
payload: {
task,
loader,
},
};
return action;
}
function loadAnnotationsSuccess(task: any): AnyAction {
const action = {
type: TasksActionTypes.LOAD_ANNOTATIONS_SUCCESS,
payload: {
task,
},
};
return action;
}
function loadAnnotationsFailed(task: any, error: any): AnyAction {
const action = {
type: TasksActionTypes.LOAD_ANNOTATIONS_FAILED,
payload: {
task,
error,
},
};
return action;
}
export function loadAnnotationsAsync(task: any, loader: any, file: File):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
dispatch(loadAnnotations(task, loader));
await task.annotations.upload(file, loader);
} catch (error) {
dispatch(loadAnnotationsFailed(task, error));
return;
}
dispatch(loadAnnotationsSuccess(task));
};
}

@ -1,5 +1,5 @@
import React from 'react';
import { connect } from 'react-redux';
import { Switch, Route, Redirect } from 'react-router';
import { BrowserRouter } from 'react-router-dom';
@ -8,81 +8,71 @@ import { Spin, Layout, Modal } from 'antd';
import 'antd/dist/antd.css';
import '../stylesheet.css';
import { authorizedAsync } from '../actions/auth-actions';
import { AuthState } from '../reducers/interfaces';
import TasksPage from './tasks-page';
import CreateTaskPage from './create-task-page';
import TaskPage from './task-page';
import ModelsPage from './models-page/models-page';
import AnnotationPage from './annotation-page/annotation-page';
import LoginPage from './login-page';
import RegisterPage from './register-page';
import Header from './cvat-header';
import TasksPageContainer from '../containers/tasks-page/tasks-page';
import CreateTaskPageContainer from '../containers/create-task-page/create-task-page';
import TaskPageContainer from '../containers/task-page/task-page';
import ModelsPageContainer from '../containers/models-page/models-page';
import AnnotationPageContainer from '../containers/annotation-page/annotation-page';
import LoginPageContainer from '../containers/login-page/login-page';
import RegisterPageContainer from '../containers/register-page/register-page';
import HeaderContainer from '../containers/header/header';
export interface CVATAppProps {
auth: AuthState;
}
export interface CVATAppActions {
type CVATAppProps = {
loadFormats: () => void;
verifyAuthorized: () => void;
userInitialized: boolean;
formatsInitialized: boolean;
gettingAuthError: string;
gettingFormatsError: string;
user: any;
}
function mapStateToProps(state: any): CVATAppProps {
return {
auth: state.auth,
};
}
function mapDispatchToProps(dispatch: any): CVATAppActions {
return {
verifyAuthorized: (): void => dispatch(authorizedAsync())
};
}
class CVATApplication extends React.PureComponent<CVATAppProps & CVATAppActions> {
export default class CVATApplication extends React.PureComponent<CVATAppProps> {
constructor(props: any) {
super(props);
}
public componentDidMount() {
this.props.loadFormats();
this.props.verifyAuthorized();
}
public componentDidUpdate() {
if (this.props.gettingAuthError) {
Modal.error({
title: 'Could not check authorization',
content: `${this.props.gettingAuthError}`,
});
}
}
// Where you go depends on your URL
public render() {
if (this.props.auth.initialized) {
if (this.props.auth.user) {
if (this.props.userInitialized && this.props.formatsInitialized) {
if (this.props.user) {
return (
<BrowserRouter>
<Layout>
<Header> </Header>
<HeaderContainer> </HeaderContainer>
<Layout.Content>
<Switch>
<Route exact path='/tasks' component={TasksPage}/>
<Route exact path='/models' component={ModelsPage}/>
<Route path='/tasks/create' component={CreateTaskPage}/>
<Route path='/tasks/:number' component={TaskPage}/>
<Route path='/tasks/:number/jobs/:number' component={AnnotationPage}/>
<Route exact path='/tasks' component={TasksPageContainer}/>
<Route exact path='/models' component={ModelsPageContainer}/>
<Route path='/tasks/create' component={CreateTaskPageContainer}/>
<Route path='/tasks/:number' component={TaskPageContainer}/>
<Route path='/tasks/:number/jobs/:number' component={AnnotationPageContainer}/>
<Redirect to='/tasks'/>
</Switch>
</Layout.Content>
</Layout>
</BrowserRouter>
);
} else if (this.props.auth.authError) {
Modal.error({
title: 'Could not authorize',
content: `${this.props.auth.authError.toString()}`,
});
return <div/>;
} else {
return (
<BrowserRouter>
<Switch>
<Route exact path='/auth/register' component={RegisterPage}/>
<Route exact path='/auth/login' component={LoginPage}/>
<Route exact path='/auth/register' component={RegisterPageContainer}/>
<Route exact path='/auth/login' component={LoginPageContainer}/>
<Redirect to='/auth/login'/>
</Switch>
</BrowserRouter>
@ -95,9 +85,3 @@ class CVATApplication extends React.PureComponent<CVATAppProps & CVATAppActions>
}
}
}
export default connect(
mapStateToProps,
mapDispatchToProps,
)(CVATApplication);

@ -1,5 +1,5 @@
import React from 'react';
import { connect } from 'react-redux';
import { RouteComponentProps } from 'react-router';
import { withRouter } from 'react-router-dom';
@ -11,53 +11,32 @@ import {
Menu,
Modal,
} from 'antd';
import Text from 'antd/lib/typography/Text';
import { logoutAsync } from '../actions/auth-actions';
import { AuthState } from '../reducers/interfaces';
interface StateToProps {
auth: AuthState;
}
interface DispatchToProps {
logout(): void;
}
function mapStateToProps(state: any): StateToProps {
return {
auth: state.auth,
};
}
import Text from 'antd/lib/typography/Text';
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
logout: () => dispatch(logoutAsync()),
}
interface HeaderContainerProps {
onLogout: () => void;
username: string;
logoutError: string;
}
type HeaderProps = StateToProps & DispatchToProps & RouteComponentProps;
function CVATHeader(props: HeaderProps) {
const cvatLogo = () => (<img src="/assets/cvat-logo.svg"/>);
const backLogo = () => (<img src="/assets/icon-playcontrol-previous.svg"/>);
const userLogo = () => (<img src="/assets/icon-account.svg" />);
const { username } = props.auth.user;
const { pathname } = props.location;
const { logoutError } = props.auth;
function HeaderContainer(props: HeaderContainerProps & RouteComponentProps) {
const cvatLogo = () => (<img src='/assets/cvat-logo.svg'/>);
const backLogo = () => (<img src='/assets/icon-playcontrol-previous.svg'/>);
const userLogo = () => (<img src='/assets/icon-account.svg' />);
if (logoutError) {
if (props.logoutError) {
Modal.error({
title: 'Could not logout',
content: `${logoutError.toString()}`,
content: `${props.logoutError}`,
});
}
let activeTab = null;
if (pathname === '/tasks') {
if (props.history.location.pathname === '/tasks') {
activeTab = 'tasks';
} else if (pathname === '/models') {
} else if (props.history.location.pathname === '/models') {
activeTab = 'models';
}
@ -80,6 +59,9 @@ function CVATHeader(props: HeaderProps) {
</Radio.Group>
</div>
<div className='right-header'>
<Button className='header-button' type='link' onClick={
() => window.open('https://github.com/opencv/cvat', '_blank')
}> <Icon type='github' /> GitHub </Button>
<Button className='header-button' type='link' onClick={
() => window.open('/documentation/user_guide.html', '_blank')
}> Help </Button>
@ -88,12 +70,12 @@ function CVATHeader(props: HeaderProps) {
<span>
<Icon className='cvat-header-user-icon' component={userLogo} />
<span>
<Text strong> {username} </Text>
<Text strong> {props.username} </Text>
<Icon className='cvat-header-menu-icon' component={backLogo} />
</span>
</span>
}>
<Menu.Item onClick={props.logout}>Logout</Menu.Item>
<Menu.Item onClick={props.onLogout}>Logout</Menu.Item>
</Menu.SubMenu>
</Menu>
</div>
@ -101,7 +83,4 @@ function CVATHeader(props: HeaderProps) {
);
}
export default withRouter(connect(
mapStateToProps,
mapDispatchToProps,
)(CVATHeader));
export default withRouter(HeaderContainer);

@ -16,12 +16,12 @@ type LoginFormProps = {
onSubmit(loginData: LoginData): void;
} & FormComponentProps;
class LoginForm extends React.PureComponent<LoginFormProps> {
class LoginFormComponent extends React.PureComponent<LoginFormProps> {
constructor(props: LoginFormProps) {
super(props);
}
private handleSubmit(e: React.FormEvent) {
private handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
this.props.form.validateFields((error, values) => {
if (!error) {
@ -75,7 +75,7 @@ class LoginForm extends React.PureComponent<LoginFormProps> {
public render() {
return (
<Form onSubmit={this.handleSubmit.bind(this)} className='login-form'>
<Form onSubmit={this.handleSubmit} className='login-form'>
{this.renderUsernameField()}
{this.renderPasswordField()}
@ -89,4 +89,4 @@ class LoginForm extends React.PureComponent<LoginFormProps> {
}
}
export default Form.create<LoginFormProps>()(LoginForm);
export default Form.create<LoginFormProps>()(LoginFormComponent);

@ -1,5 +1,5 @@
import React from 'react';
import { connect } from 'react-redux';
import { RouteComponentProps } from 'react-router';
import { Link, withRouter } from 'react-router-dom';
@ -11,35 +11,14 @@ import {
Modal,
} from 'antd';
import LoginForm, { LoginData } from '../components/login-form';
import { loginAsync } from '../actions/auth-actions';
import { AuthState } from '../reducers/interfaces';
interface StateToProps {
auth: AuthState;
}
interface DispatchToProps {
login(loginData: LoginData): void;
}
import LoginForm, { LoginData } from './login-form';
function mapStateToProps(state: any): StateToProps {
return {
auth: state.auth,
};
interface LoginPageComponentProps {
loginError: string;
onLogin: (username: string, password: string) => void;
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
login: (loginData: LoginData) => dispatch(loginAsync(loginData)),
}
}
type LoginPageProps = StateToProps & DispatchToProps & RouteComponentProps;
function LoginPage(props: LoginPageProps) {
const { loginError } = props.auth;
function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps) {
const sizes = {
xs: { span: 14 },
sm: { span: 14 },
@ -48,10 +27,10 @@ function LoginPage(props: LoginPageProps) {
xl: { span: 4 },
}
if (loginError) {
if (props.loginError) {
Modal.error({
title: 'Could not login',
content: `${loginError.toString()}`,
content: props.loginError,
});
}
@ -59,7 +38,9 @@ function LoginPage(props: LoginPageProps) {
<Row type='flex' justify='center' align='middle'>
<Col {...sizes}>
<Title level={2}> Login </Title>
<LoginForm onSubmit={props.login}/>
<LoginForm onSubmit={(loginData: LoginData) => {
props.onLogin(loginData.username, loginData.password);
}}/>
<Row type='flex' justify='start' align='top'>
<Col>
<Text strong>
@ -72,7 +53,4 @@ function LoginPage(props: LoginPageProps) {
);
}
export default withRouter(connect(
mapStateToProps,
mapDispatchToProps,
)(LoginPage));
export default withRouter(LoginPageComponent);

@ -16,18 +16,18 @@ export interface RegisterData {
password2: string;
}
import patterns from '../utils/validation-patterns';
import patterns from '../../utils/validation-patterns';
type RegisterFormProps = {
onSubmit(registerData: RegisterData): void;
} & FormComponentProps;
class RegisterForm extends React.PureComponent<RegisterFormProps> {
class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
constructor(props: RegisterFormProps) {
super(props);
}
private validateConfirmation(rule: any, value: any, callback: any) {
private validateConfirmation = (rule: any, value: any, callback: any) => {
const { form } = this.props;
if (value && value !== form.getFieldValue('password1')) {
callback('Two passwords that you enter is inconsistent!');
@ -36,7 +36,7 @@ class RegisterForm extends React.PureComponent<RegisterFormProps> {
}
};
private validatePassword(_: any, value: any, callback: any) {
private validatePassword = (_: any, value: any, callback: any) => {
const { form } = this.props;
if (!patterns.validatePasswordLength.pattern.test(value)) {
callback(patterns.validatePasswordLength.message);
@ -60,7 +60,7 @@ class RegisterForm extends React.PureComponent<RegisterFormProps> {
callback();
};
private validateUsername(_: any, value: any, callback: any) {
private validateUsername = (_: any, value: any, callback: any) => {
if (!patterns.validateUsernameLength.pattern.test(value)) {
callback(patterns.validateUsernameLength.message);
}
@ -72,7 +72,7 @@ class RegisterForm extends React.PureComponent<RegisterFormProps> {
callback();
};
private handleSubmit(e: React.FormEvent) {
private handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
this.props.form.validateFields((error, values) => {
if (!error) {
@ -169,7 +169,7 @@ class RegisterForm extends React.PureComponent<RegisterFormProps> {
required: true,
message: 'Please input your password!',
}, {
validator: this.validatePassword.bind(this),
validator: this.validatePassword,
}],
})(<Input.Password
autoComplete='new-password'
@ -188,7 +188,7 @@ class RegisterForm extends React.PureComponent<RegisterFormProps> {
required: true,
message: 'Please confirm your password!',
}, {
validator: this.validateConfirmation.bind(this),
validator: this.validateConfirmation,
}],
})(<Input.Password
autoComplete='new-password'
@ -203,7 +203,7 @@ class RegisterForm extends React.PureComponent<RegisterFormProps> {
public render() {
return (
<Form onSubmit={this.handleSubmit.bind(this)} className='login-form'>
<Form onSubmit={this.handleSubmit} className='login-form'>
{this.renderFirstNameField()}
{this.renderLastNameField()}
{this.renderUsernameField()}
@ -221,4 +221,4 @@ class RegisterForm extends React.PureComponent<RegisterFormProps> {
}
}
export default Form.create<RegisterFormProps>()(RegisterForm);
export default Form.create<RegisterFormProps>()(RegisterFormComponent);

@ -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);

@ -8,7 +8,7 @@ import {
Icon,
} from 'antd';
export default function EmptyList() {
export default function EmptyListComponent() {
const emptyTasksIcon = () => (<img src='/assets/empty-tasks-icon.svg'/>);
return (

@ -9,66 +9,117 @@ import {
Progress,
Menu,
Dropdown,
Upload,
} from 'antd';
import { ClickParam } from 'antd/lib/menu/index';
import { UploadChangeParam } from 'antd/lib/upload';
import { RcFile } from 'antd/lib/upload';
import moment from 'moment';
export interface TaskItemProps {
task: any;
preview: string;
taskInstance: any;
previewImage: string;
dumpActivities: string[] | null;
loadActivity: string | null;
loaders: any[];
dumpers: any[];
onDumpAnnotation: (task: any, dumper: any) => void;
onLoadAnnotation: (task: any, loader: any, file: File) => void;
}
export default function TaskItem(props: TaskItemProps) {
// Task info
const task = props.task;
const id = task.id;
const owner = task.owner? task.owner.username : undefined;
const updated = moment(task.updatedDate).fromNow();
const created = moment(task.createdDate).format('MMMM Do YYYY');
const preview = props.preview;
// Get and truncate a task name
let name = task.name;
name = `${name.substring(0, 70)}${name.length > 70 ? '...' : ''}`;
// Count number of jobs and performed jobs
const numOfJobs = task.jobs.length;
const numOfCompleted = task.jobs.filter(
(job: any) => job.status === 'completed'
).length;
// Progress appearence depends on number of jobs
const progressColor = numOfCompleted === numOfJobs ? 'cvat-task-completed-progress':
numOfCompleted ? 'cvat-task-progress-progress' : 'cvat-task-pending-progress';
const subMenuIcon = () => (<img src='/assets/icon-sub-menu.svg'/>);
// Menu
const menu = (
<Menu className='cvat-task-item-menu'>
<Menu.Item key='dump'>Dump annotations</Menu.Item>
<Menu.Item key='upload'>Upload annotations</Menu.Item>
<Menu.Item key='tracker'>Open bug tracker</Menu.Item>
<Menu.Item key='auto'>Run auto annotation</Menu.Item>
<Menu.Item key='tf'>Run TF annotation</Menu.Item>
<hr/>
<Menu.Item key='update'>Update</Menu.Item>
<Menu.Item key='delete'>Delete</Menu.Item>
</Menu>
);
return (
<Row className='cvat-tasks-list-item' type='flex' justify='center' align='top'>
function isDefaultFormat(dumperName: string, taskMode: string): boolean {
return (dumperName === 'CVAT XML 1.1 for videos' && taskMode === 'interpolation')
|| (dumperName === 'CVAT XML 1.1 for images' && taskMode === 'annotation');
}
export default class TaskItemComponent extends React.PureComponent<TaskItemProps> {
constructor(props: TaskItemProps) {
super(props);
}
private handleMenuClick = (params: ClickParam) => {
const tracker = this.props.taskInstance.bugTracker;
if (params.keyPath.length === 2) {
// dump or upload
if (params.keyPath[1] === 'dump') {
}
} else {
switch (params.key) {
case 'tracker': {
window.open(`${tracker}`, '_blank')
return;
} case 'auto': {
return;
} case 'tf': {
return;
} case 'update': {
return;
} case 'delete': {
return;
} default: {
return;
}
}
}
}
private renderPreview() {
return (
<Col span={4}>
<div className='cvat-task-preview-wrapper'>
<img alt='Preview' className='cvat-task-preview' src={preview}/>
<img alt='Preview' className='cvat-task-preview' src={this.props.previewImage}/>
</div>
</Col>
)
}
private renderDescription() {
// Task info
const task = this.props.taskInstance;
const { id } = task;
const owner = task.owner ? task.owner.username : null;
const updated = moment(task.updatedDate).fromNow();
const created = moment(task.createdDate).format('MMMM Do YYYY');
// Get and truncate a task name
const name = `${task.name.substring(0, 70)}${task.name.length > 70 ? '...' : ''}`;
return (
<Col span={10}>
<Text strong> {id} {name} </Text> <br/>
<Text type='secondary'> Created { owner? 'by ' + owner : '' } on {created} </Text> <br/>
{ owner ?
<>
<Text type='secondary'>
Created { owner ? 'by ' + owner : '' } on {created}
</Text> <br/>
</> : null
}
<Text type='secondary'> Last updated {updated} </Text>
</Col>
)
}
private renderProgress() {
const task = this.props.taskInstance;
// Count number of jobs and performed jobs
const numOfJobs = task.jobs.length;
const numOfCompleted = task.jobs.filter(
(job: any) => job.status === 'completed'
).length;
// Progress appearence depends on number of jobs
const progressColor = numOfCompleted === numOfJobs ? 'cvat-task-completed-progress':
numOfCompleted ? 'cvat-task-progress-progress' : 'cvat-task-pending-progress';
return (
<Col span={6}>
<Row type='flex' justify='space-between' align='top'>
<Col>
@ -97,6 +148,91 @@ export default function TaskItem(props: TaskItemProps) {
/>
</Row>
</Col>
)
}
private renderDumperItem(dumper: any) {
const task = this.props.taskInstance;
const { mode } = task;
const dumpingWithThisDumper = (this.props.dumpActivities || [])
.filter((_dumper: string) => _dumper === dumper.name)[0];
const pending = !!dumpingWithThisDumper;
return (
<Menu.Item className='cvat-task-item-dump-submenu-item' key={dumper.name}>
<Button block={true} type='link' disabled={pending}
onClick={() => {
this.props.onDumpAnnotation(task, dumper);
}}>
<Icon type='download'/>
<Text strong={isDefaultFormat(dumper.name, mode)}>
{dumper.name}
</Text>
{pending ? <Icon type='loading'/> : null}
</Button>
</Menu.Item>
);
}
private renderLoaderItem(loader: any) {
const loadingWithThisLoader = this.props.loadActivity
&& this.props.loadActivity === loader.name
? this.props.loadActivity : null;
const pending = !!loadingWithThisLoader;
return (
<Menu.Item className='cvat-task-item-load-submenu-item' key={loader.name}>
<Upload
accept={`.${loader.format}`}
multiple={false}
showUploadList={ false }
beforeUpload={(file: RcFile) => {
this.props.onLoadAnnotation(
this.props.taskInstance,
loader,
file as File,
);
return false;
}}>
<Button block={true} type='link' disabled={!!this.props.loadActivity}>
<Icon type='upload'/>
<Text> {loader.name} </Text>
{pending ? <Icon type='loading'/> : null}
</Button>
</Upload>
</Menu.Item>
);
}
private renderMenu() {
const tracker = this.props.taskInstance.bugTracker;
return (
<Menu subMenuCloseDelay={0.15} className='cvat-task-item-menu' onClick={this.handleMenuClick}>
<Menu.SubMenu key='dump' title='Dump annotations'>
{this.props.dumpers.map((dumper) => this.renderDumperItem(dumper))}
</Menu.SubMenu>
<Menu.SubMenu key='load' title='Upload annotations'>
{this.props.loaders.map((loader) => this.renderLoaderItem(loader))}
</Menu.SubMenu>
{tracker ? <Menu.Item key='tracker'>Open bug tracker</Menu.Item> : null}
<Menu.Item key='auto'>Run auto annotation</Menu.Item>
<Menu.Item key='tf'>Run TF annotation</Menu.Item>
<hr/>
<Menu.Item key='update'>Update</Menu.Item>
<Menu.Item key='delete'>Delete</Menu.Item>
</Menu>
);
}
private renderNavigation() {
const subMenuIcon = () => (<img src='/assets/icon-sub-menu.svg'/>);
return (
<Col span={4}>
<Row type='flex' justify='end'>
<Col>
@ -106,12 +242,23 @@ export default function TaskItem(props: TaskItemProps) {
<Row type='flex' justify='end'>
<Col>
<Text style={{color: 'black'}}> Actions </Text>
<Dropdown overlay={menu}>
<Dropdown overlay={this.renderMenu()}>
<Icon className='cvat-task-item-menu-icon' component={subMenuIcon}/>
</Dropdown>
</Col>
</Row>
</Col>
</Row>
)
)
}
public render() {
return (
<Row className='cvat-tasks-list-item' type='flex' justify='center' align='top'>
{this.renderPreview()}
{this.renderDescription()}
{this.renderProgress()}
{this.renderNavigation()}
</Row>
)
};
}

@ -6,26 +6,20 @@ import {
Pagination,
} from 'antd';
import TaskItem from './task-item'
import TaskItem from '../../containers/tasks-page/task-item';
export interface ContentListProps {
goToPage(page: number): void;
tasks: any[];
previews: string[];
page: number;
count: number;
onSwitchPage(page: number): void;
currentTasksIndexes: number[];
currentPage: number;
numberOfTasks: number;
}
export default function TaskList(props: ContentListProps) {
const taskViews = [];
for (let i = 0; i < props.tasks.length; i++) {
const task = props.tasks[i];
const preview = props.previews[i];
taskViews.push(
<TaskItem key={task.id} task={task} preview={preview}></TaskItem>
)
}
export default function TaskListComponent(props: ContentListProps) {
const tasks = props.currentTasksIndexes;
const taskViews = tasks.map(
(tid, id) => <TaskItem idx={id} taskID={tid} key={tid}/>
);
return (
<>
@ -38,9 +32,10 @@ export default function TaskList(props: ContentListProps) {
<Col md={22} lg={18} xl={16} xxl={14}>
<Pagination
className='cvat-tasks-pagination'
onChange={props.goToPage}
total={props.count}
onChange={props.onSwitchPage}
total={props.numberOfTasks}
pageSize={10}
current={props.currentPage}
showQuickJumper
/>
</Col>

@ -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';
export default class AnnotationPage extends React.PureComponent {
export default class AnnotationPageContainer extends React.PureComponent {
constructor(props: any) {
super(props);
}

@ -1,6 +1,6 @@
import React from 'react';
export default function CreateTaskPage() {
export default function CreateTaskPageContainer() {
return (
<div>
"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,6 +1,6 @@
import React from 'react';
export default class ModelsPage extends React.PureComponent {
export default class ModelsPageContainer extends React.PureComponent {
constructor(props: any) {
super(props);
}

@ -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);

@ -1,4 +1,4 @@
import _cvat from '../public/cvat-core.node';
import _cvat from '../../cvat-core/src/api';
const cvat: any = _cvat;

@ -1,16 +1,73 @@
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import { connect, Provider } from 'react-redux';
import CVATApplication from './containers/cvat-app';
import CVATApplication from './components/cvat-app';
import createCVATStore from './store';
import { authorizedAsync } from './actions/auth-actions';
import { gettingFormatsAsync } from './actions/formats-actions';
import { CombinedState } from './reducers/root-reducer';
const cvatStore = createCVATStore();
interface StateToProps {
userInitialized: boolean;
formatsInitialized: boolean;
gettingAuthError: any;
gettingFormatsError: any;
user: any;
}
interface DispatchToProps {
loadFormats: () => void;
verifyAuthorized: () => void;
}
function mapStateToProps(state: CombinedState): StateToProps {
const { auth } = state;
const { formats } = state;
return {
userInitialized: auth.initialized,
formatsInitialized: formats.initialized,
gettingAuthError: auth.authError,
user: auth.user,
gettingFormatsError: formats.gettingFormatsError,
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
loadFormats: (): void => dispatch(gettingFormatsAsync()),
verifyAuthorized: (): void => dispatch(authorizedAsync())
};
}
function reduxAppWrapper(props: StateToProps & DispatchToProps) {
return (
<CVATApplication
loadFormats={props.loadFormats}
verifyAuthorized={props.verifyAuthorized}
userInitialized={props.userInitialized}
formatsInitialized={props.formatsInitialized}
gettingAuthError={props.gettingAuthError ? props.gettingAuthError.toString() : ''}
gettingFormatsError={props.gettingFormatsError ? props.gettingFormatsError.toString() : ''}
user={props.user}
/>
)
}
const ReduxAppWrapper = connect(
mapStateToProps,
mapDispatchToProps,
)(reduxAppWrapper);
ReactDOM.render(
(
<Provider store={cvatStore}>
<CVATApplication />
<ReduxAppWrapper/>
</Provider>
),
document.getElementById('root')

@ -25,7 +25,7 @@ export default (state = defaultState, action: AnyAction): AuthState => {
return {
...state,
initialized: true,
authError: action.payload.authError,
authError: action.payload.error,
};
case AuthActionTypes.LOGIN_SUCCESS:
return {
@ -37,7 +37,7 @@ export default (state = defaultState, action: AnyAction): AuthState => {
return {
...state,
user: null,
loginError: action.payload.loginError,
loginError: action.payload.error,
};
case AuthActionTypes.LOGOUT_SUCCESS:
return {
@ -48,7 +48,7 @@ export default (state = defaultState, action: AnyAction): AuthState => {
case AuthActionTypes.LOGOUT_FAILED:
return {
...state,
logoutError: action.payload.logoutError,
logoutError: action.payload.error,
};
case AuthActionTypes.REGISTER_SUCCESS:
return {
@ -60,7 +60,7 @@ export default (state = defaultState, action: AnyAction): AuthState => {
return {
...state,
user: null,
registerError: action.payload.registerError,
registerError: action.payload.error,
};
default:
return state;

@ -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;
}
};

@ -19,11 +19,39 @@ export interface TasksQuery {
[key: string]: string | number | null;
}
export interface Task {
instance: any; // cvat-core instance
preview: string;
}
export interface TasksState {
initialized: boolean;
tasksFetchingError: any;
gettingQuery: TasksQuery;
count: number;
array: any[];
previews: string[];
error: any;
query: TasksQuery;
current: Task[];
activities: {
dumps: {
dumpingError: any;
byTask: {
// dumps in different formats at the same time
[tid: number]: string[]; // dumper names
};
};
loads: {
loadingError: any;
loadingDoneMessage: string;
byTask: {
// only one loading simultaneously
[tid: number]: string; // loader name
};
};
};
}
export interface FormatsState {
loaders: any[];
dumpers: any[];
initialized: boolean;
gettingFormatsError: any;
}

@ -1,10 +1,24 @@
import { combineReducers, Reducer } from 'redux';
import authReducer from './auth-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 {
return combineReducers({
auth: authReducer,
tasks: tasksReducer,
formats: formatsReducer,
});
}

@ -1,15 +1,14 @@
import { AnyAction } from 'redux';
import { TasksActionTypes } from '../actions/tasks-actions';
import { TasksState } from './interfaces';
import { TasksState, Task } from './interfaces';
const defaultState: TasksState = {
initialized: false,
tasksFetchingError: null,
count: 0,
array: [],
previews: [],
error: null,
query: {
current: [],
gettingQuery: {
page: 1,
id: null,
search: null,
@ -19,30 +18,202 @@ const defaultState: TasksState = {
status: null,
mode: null,
},
activities: {
dumps: {
dumpingError: null,
byTask: {},
},
loads: {
loadingError: null,
loadingDoneMessage: '',
byTask: {},
},
},
};
export default (state = defaultState, action: AnyAction): TasksState => {
export default (inputState: TasksState = defaultState, action: AnyAction): TasksState => {
function cleanupTemporaryInfo(stateToResetErrors: TasksState): TasksState {
return {
...stateToResetErrors,
tasksFetchingError: null,
activities: {
...stateToResetErrors.activities,
dumps: {
...stateToResetErrors.activities.dumps,
dumpingError: null,
},
loads: {
...stateToResetErrors.activities.loads,
loadingError: null,
loadingDoneMessage: '',
},
},
};
}
const state = cleanupTemporaryInfo(inputState);
switch (action.type) {
case TasksActionTypes.GET_TASKS_SUCCESS:
case TasksActionTypes.GET_TASKS:
return {
...state,
initialized: false,
};
case TasksActionTypes.GET_TASKS_SUCCESS: {
const combinedWithPreviews = action.payload.array
.map((task: any, index: number): Task => ({
instance: task,
preview: action.payload.previews[index],
}));
return {
...state,
initialized: true,
count: action.payload.count,
array: action.payload.array,
previews: action.payload.previews,
error: null,
query: { ...action.payload.query },
current: combinedWithPreviews,
gettingQuery: { ...action.payload.query },
};
}
case TasksActionTypes.GET_TASKS_FAILED:
return {
...state,
initialized: true,
array: [],
previews: [],
count: 0,
error: action.payload.error,
query: { ...action.payload.query },
current: [],
gettingQuery: { ...action.payload.query },
tasksFetchingError: action.payload.error,
};
case TasksActionTypes.DUMP_ANNOTATIONS: {
const { task } = action.payload;
const { dumper } = action.payload;
const tasksDumpingActivities = {
...state.activities.dumps,
};
const theTaskDumpingActivities = [...tasksDumpingActivities.byTask[task.id] || []];
if (!theTaskDumpingActivities.includes(dumper.name)) {
theTaskDumpingActivities.push(dumper.name);
} else {
throw Error('Dump with the same dumper for this same task has been already started');
}
tasksDumpingActivities.byTask[task.id] = theTaskDumpingActivities;
return {
...state,
activities: {
...state.activities,
dumps: tasksDumpingActivities,
},
};
}
case TasksActionTypes.DUMP_ANNOTATIONS_SUCCESS: {
const { task } = action.payload;
const { dumper } = action.payload;
const tasksDumpingActivities = {
...state.activities.dumps,
};
const theTaskDumpingActivities = tasksDumpingActivities.byTask[task.id]
.filter((dumperName: string): boolean => dumperName !== dumper.name);
tasksDumpingActivities.byTask[task.id] = theTaskDumpingActivities;
return {
...state,
activities: {
...state.activities,
dumps: tasksDumpingActivities,
},
};
}
case TasksActionTypes.DUMP_ANNOTATIONS_FAILED: {
const { task } = action.payload;
const { dumper } = action.payload;
const dumpingError = action.payload.error;
const tasksDumpingActivities = {
...state.activities.dumps,
dumpingError,
};
const theTaskDumpingActivities = tasksDumpingActivities.byTask[task.id]
.filter((dumperName: string): boolean => dumperName !== dumper.name);
tasksDumpingActivities.byTask[task.id] = theTaskDumpingActivities;
return {
...state,
activities: {
...state.activities,
dumps: tasksDumpingActivities,
},
};
}
case TasksActionTypes.LOAD_ANNOTATIONS: {
const { task } = action.payload;
const { loader } = action.payload;
const tasksLoadingActivity = {
...state.activities.loads,
};
if (task.id in tasksLoadingActivity.byTask) {
throw Error('Load for this task has been already started');
}
tasksLoadingActivity.byTask[task.id] = loader.name;
return {
...state,
activities: {
...state.activities,
loads: tasksLoadingActivity,
},
};
}
case TasksActionTypes.LOAD_ANNOTATIONS_SUCCESS: {
const { task } = action.payload;
const tasksLoadingActivity = {
...state.activities.loads,
};
delete tasksLoadingActivity.byTask[task.id];
return {
...state,
activities: {
...state.activities,
loads: {
...tasksLoadingActivity,
loadingDoneMessage: `Annotations have been loaded to the task ${task.id}`,
},
},
};
}
case TasksActionTypes.LOAD_ANNOTATIONS_FAILED: {
const { task } = action.payload;
const loadingError = action.payload.error;
const tasksLoadingActivity = {
...state.activities.loads,
};
delete tasksLoadingActivity.byTask[task.id];
return {
...state,
activities: {
...state.activities,
loads: {
...tasksLoadingActivity,
loadingError,
},
},
};
}
default:
return state;
}

@ -271,6 +271,18 @@
border: 0.5px solid #D6D6D6;
}
.cvat-task-item-load-submenu-item {
padding: 0px;
}
.cvat-task-item-load-submenu-item > span > .ant-upload {
width: 100%;
}
.cvat-task-item-dump-submenu-item {
padding: 0px;
}
.cvat-task-preview {
max-width: 140px;
max-height: 80px;
@ -279,7 +291,7 @@
.cvat-task-preview-wrapper {
display: flex;
justify-content: center;
overflow: auto;
overflow: hidden;
margin: 20px;
margin-top: 0px;
}

@ -34,6 +34,7 @@ module.exports = {
use: {
loader: 'babel-loader',
options: {
plugins: ['@babel/plugin-proposal-class-properties'],
presets: [
['@babel/preset-env', {
targets: {

@ -66,13 +66,13 @@ services:
image: nginx
restart: always
build:
context: cvat-ui
context: .
args:
http_proxy:
https_proxy:
no_proxy:
socks_proxy:
dockerfile: Dockerfile
dockerfile: Dockerfile.ui
environment:
REACT_APP_API_PROTOCOL: http
REACT_APP_API_HOST: localhost

Loading…
Cancel
Save