User interface with react and antd (#755)

* Login page, router
* Registration
* Tasks view
main
Boris Sekachev 6 years ago committed by Nikita Manovich
parent 7e214f85fe
commit 695fc37924

@ -14,6 +14,10 @@
"directory": "./cvat-canvas",
"changeProcessCWD": true
},
{
"directory": "./cvat-ui",
"changeProcessCWD": true
},
{
"directory": ".",
"changeProcessCWD": true

File diff suppressed because it is too large Load Diff

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

@ -50,5 +50,6 @@
"func-names": [0],
"valid-typeof": [0],
"no-console": [0], // this rule deprecates console.log, console.warn etc. because "it is not good in production code"
"max-classes-per-file": [0],
},
};

@ -138,8 +138,8 @@
handler_file: initialData.handler_file,
};
data.dumpers = initialData.dumpers.map(el => new Dumper(el));
data.loaders = initialData.loaders.map(el => new Loader(el));
data.dumpers = initialData.dumpers.map((el) => new Dumper(el));
data.loaders = initialData.loaders.map((el) => new Loader(el));
// Now all fields are readonly
Object.defineProperties(this, {

@ -177,19 +177,19 @@
export() {
const data = {
tracks: this.tracks.filter(track => !track.removed)
.map(track => track.toJSON()),
tracks: this.tracks.filter((track) => !track.removed)
.map((track) => track.toJSON()),
shapes: Object.values(this.shapes)
.reduce((accumulator, value) => {
accumulator.push(...value);
return accumulator;
}, []).filter(shape => !shape.removed)
.map(shape => shape.toJSON()),
}, []).filter((shape) => !shape.removed)
.map((shape) => shape.toJSON()),
tags: Object.values(this.tags).reduce((accumulator, value) => {
accumulator.push(...value);
return accumulator;
}, []).filter(tag => !tag.removed)
.map(tag => tag.toJSON()),
}, []).filter((tag) => !tag.removed)
.map((tag) => tag.toJSON()),
};
return data;
@ -200,7 +200,7 @@
const shapes = this.shapes[frame] || [];
const tags = this.tags[frame] || [];
const objects = tracks.concat(shapes).concat(tags).filter(object => !object.removed);
const objects = tracks.concat(shapes).concat(tags).filter((object) => !object.removed);
// filtering here
const objectStates = [];
@ -370,7 +370,7 @@
const clientID = ++this.count;
const track = {
frame: Math.min.apply(null, Object.keys(keyframes).map(frame => +frame)),
frame: Math.min.apply(null, Object.keys(keyframes).map((frame) => +frame)),
shapes: Object.values(keyframes),
group: 0,
label_id: label.id,
@ -577,7 +577,7 @@
if (objectType === 'track') {
const keyframes = Object.keys(object.shapes)
.sort((a, b) => +a - +b).map(el => +el);
.sort((a, b) => +a - +b).map((el) => +el);
let prevKeyframe = keyframes[0];
let visible = false;
@ -699,13 +699,13 @@
} else if (state.objectType === 'track') {
constructed.tracks.push({
attributes: attributes
.filter(attr => !labelAttributes[attr.spec_id].mutable),
.filter((attr) => !labelAttributes[attr.spec_id].mutable),
frame: state.frame,
group: 0,
label_id: state.label.id,
shapes: [{
attributes: attributes
.filter(attr => labelAttributes[attr.spec_id].mutable),
.filter((attr) => labelAttributes[attr.spec_id].mutable),
frame: state.frame,
occluded: state.occluded || false,
outside: false,

@ -273,7 +273,7 @@
lock: this.lock,
zOrder: this.zOrder,
points: [...this.points],
attributes: Object.assign({}, this.attributes),
attributes: { ...this.attributes },
label: this.label,
group: this.group,
color: this.color,

@ -47,7 +47,7 @@
cvat.server.formats.implementation = async () => {
const result = await serverProxy.server.formats();
return result.map(el => new AnnotationFormat(el));
return result.map((el) => new AnnotationFormat(el));
};
cvat.server.register.implementation = async (username, firstName, lastName,
@ -82,7 +82,7 @@
users = await serverProxy.users.getUsers();
}
users = users.map(user => new User(user));
users = users.map((user) => new User(user));
return users;
};
@ -116,8 +116,11 @@
// If task was found by its id, then create task instance and get Job instance from it
if (tasks !== null && tasks.length) {
tasks[0].owner = await serverProxy.users.getUsers(tasks[0].owner);
tasks[0].assignee = await serverProxy.users.getUsers(tasks[0].assignee);
const task = new Task(tasks[0]);
return filter.jobID ? task.jobs.filter(job => job.id === filter.jobID) : task.jobs;
return filter.jobID ? task.jobs
.filter((job) => job.id === filter.jobID) : task.jobs;
}
return [];
@ -158,8 +161,13 @@
}
}
const users = await serverProxy.users.getUsers();
const tasksData = await serverProxy.tasks.getTasks(searchParams.toString());
const tasks = tasksData.map(task => new Task(task));
const tasks = tasksData.map((task) => {
[task.owner] = users.filter((user) => user.id === task.owner);
[task.assignee] = users.filter((user) => user.id === task.assignee);
return new Task(task);
});
tasks.count = tasksData.count;
return tasks;

@ -105,6 +105,26 @@
});
};
async function getPreview(taskID) {
return new Promise(async (resolve, reject) => {
try {
// Just go to server and get preview (no any cache)
const result = await serverProxy.frames.getPreview(taskID);
if (isNode) {
resolve(global.Buffer.from(result, 'binary').toString('base64'));
} else if (isBrowser) {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.readAsDataURL(result);
}
} catch (error) {
reject(error);
}
});
}
async function getFrame(taskID, mode, frame) {
if (!(taskID in frameDataCache)) {
frameDataCache[taskID] = {};
@ -140,5 +160,6 @@
module.exports = {
FrameData,
getFrame,
getPreview,
};
})();

@ -342,14 +342,20 @@
}
}
async function getUsers() {
async function getUsers(id = null) {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/users`, {
proxy: config.proxy,
});
if (id === null) {
response = await Axios.get(`${backendAPI}/users`, {
proxy: config.proxy,
});
} else {
response = await Axios.get(`${backendAPI}/users/${id}`, {
proxy: config.proxy,
});
}
} catch (errorData) {
throw generateError(errorData, 'Could not get users from the server');
}
@ -372,6 +378,27 @@
return response.data;
}
async function getPreview(tid) {
const { backendAPI } = config;
let response = null;
try {
// TODO: change 0 frame to preview
response = await Axios.get(`${backendAPI}/tasks/${tid}/frames/0`, {
proxy: config.proxy,
responseType: 'blob',
});
} catch (errorData) {
const code = errorData.response ? errorData.response.status : errorData.code;
throw new ServerError(
`Could not get preview frame for the task ${tid} from the server`,
code,
);
}
return response.data;
}
async function getData(tid, frame) {
const { backendAPI } = config;
@ -567,6 +594,7 @@
value: Object.freeze({
getData,
getMeta,
getPreview,
}),
writable: false,
},

@ -10,7 +10,7 @@
(() => {
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
const { getFrame } = require('./frames');
const { getFrame, getPreview } = require('./frames');
const { ArgumentError } = require('./exceptions');
const { TaskStatus } = require('./enums');
const { Label } = require('./labels');
@ -109,6 +109,11 @@
.apiWrapper.call(this, prototype.frames.get, frame);
return result;
},
async preview() {
const result = await PluginRegistry
.apiWrapper.call(this, prototype.frames.preview);
return result;
},
},
writable: true,
}),
@ -380,6 +385,17 @@
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
/**
* Get the first frame of a task for preview
* @method preview
* @memberof Session.frames
* @returns {string} - jpeg encoded image
* @instance
* @async
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
/**
* Namespace is used for an interaction with logs
@ -619,6 +635,7 @@
this.frames = {
get: Object.getPrototypeOf(this).frames.get.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this),
};
}
@ -780,9 +797,9 @@
get: () => data.mode,
},
/**
* Identificator of a user who has created the task
* Instance of a user who has created the task
* @name owner
* @type {integer}
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Task
* @readonly
* @instance
@ -791,9 +808,9 @@
get: () => data.owner,
},
/**
* Identificator of a user who is responsible for the task
* Instance of a user who is responsible for the task
* @name assignee
* @type {integer}
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.Task
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
@ -1122,6 +1139,7 @@
this.frames = {
get: Object.getPrototypeOf(this).frames.get.bind(this),
preview: Object.getPrototypeOf(this).frames.preview.bind(this),
};
}
@ -1218,6 +1236,11 @@
return frameData;
};
Job.prototype.frames.preview.implementation = async function () {
const frameData = await getPreview(this.task.id);
return frameData;
};
// TODO: Check filter for annotations
Job.prototype.annotations.get.implementation = async function (frame, filter) {
if (frame < this.startFrame || frame > this.stopFrame) {
@ -1293,7 +1316,7 @@
name: this.name,
bug_tracker: this.bugTracker,
z_order: this.zOrder,
labels: [...this.labels.map(el => el.toJSON())],
labels: [...this.labels.map((el) => el.toJSON())],
};
await serverProxy.tasks.saveTask(this.id, taskData);
@ -1302,7 +1325,7 @@
const taskData = {
name: this.name,
labels: this.labels.map(el => el.toJSON()),
labels: this.labels.map((el) => el.toJSON()),
image_quality: this.imageQuality,
z_order: Boolean(this.zOrder),
};
@ -1358,6 +1381,11 @@
return result;
};
Task.prototype.frames.preview.implementation = async function () {
const frameData = await getPreview(this.id);
return frameData;
};
// TODO: Check filter for annotations
Task.prototype.annotations.get.implementation = async function (frame, filter) {
if (!Number.isInteger(frame) || frame < 0) {

@ -69,3 +69,17 @@ describe('Feature: get frame data', () => {
expect(typeof (frameData)).toBe('string');
});
});
describe('Feature: get frame preview', () => {
test('get frame preview for a task', async () => {
const task = (await window.cvat.tasks.get({ id: 100 }))[0];
const frame = await task.frames.preview();
expect(typeof (frame)).toBe('string');
});
test('get frame preview for a job', async () => {
const job = (await window.cvat.jobs.get({ jobID: 100 }))[0];
const frame = await job.frames.preview();
expect(typeof (frame)).toBe('string');
});
});

@ -192,6 +192,10 @@ class ServerProxy {
return JSON.parse(JSON.stringify(usersDummyData)).results[0];
}
async function getPreview() {
return 'DUMMY_IMAGE';
}
async function getData() {
return 'DUMMY_IMAGE';
}
@ -282,6 +286,7 @@ class ServerProxy {
value: Object.freeze({
getData,
getMeta,
getPreview,
}),
writable: false,
},

@ -7,13 +7,12 @@ const path = require('path');
const nodeConfig = {
target: 'node',
mode: 'production',
mode: 'development',
devtool: 'source-map',
entry: './src/api.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'cvat-core.node.js',
library: 'cvat',
libraryTarget: 'commonjs',
},
module: {
@ -22,9 +21,6 @@ const nodeConfig = {
exclude: /node_modules/,
}],
},
externals: {
canvas: 'commonjs canvas',
},
stats: {
warnings: false,
},

@ -1,2 +1 @@
build
node_modules

@ -0,0 +1,39 @@
/*
* Copyright (C) 2019 Intel Corporation
* SPDX-License-Identifier: MIT
*/
module.exports = {
'env': {
'node': true,
'browser': true,
'es6': true,
},
'parserOptions': {
'parser': '@typescript-eslint/parser',
'ecmaVersion': 6,
},
'plugins': [
'@typescript-eslint',
'import',
],
'extends': [
'plugin:@typescript-eslint/recommended',
'airbnb-typescript/base',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
],
'rules': {
'@typescript-eslint/indent': ['warn', 4],
'@typescript-eslint/no-explicit-any': [0],
'no-restricted-syntax': [0, {'selector': 'ForOfStatement'}],
},
'settings': {
'import/resolver': {
'node': {
'extensions': ['.tsx', '.ts', '.jsx', '.js', '.json'],
},
},
},
};

21
cvat-ui/.gitignore vendored

@ -1,23 +1,6 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
/dist
!/dist/assets
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*

@ -33,4 +33,4 @@ 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 --from=cvat-ui /tmp/cvat-ui/build /usr/share/nginx/html/
COPY --from=cvat-ui /tmp/cvat-ui/dist /usr/share/nginx/html/

@ -1,44 +0,0 @@
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.<br>
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.<br>
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.<br>
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.<br>
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.<br>
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (Webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).

@ -1,14 +0,0 @@
const { override, fixBabelImports, addLessLoader } = require('customize-cra');
module.exports = override(
fixBabelImports('import', {
libraryName: 'antd',
libraryDirectory: 'es',
style: true,
}),
addLessLoader({
javascriptEnabled: true,
// modifyVars: { '@primary-color': '#1DA57A' },
}),
);

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 9.1 KiB

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M25.9 23c-1.73 0-2.561 1-5.4 1-2.839 0-3.664-1-5.4-1-4.472 0-8.1 3.762-8.1 8.4V33c0 1.656 1.296 3 2.893 3h21.214C32.704 36 34 34.656 34 33v-1.6c0-4.637-3.628-8.4-8.1-8.4zm5.207 10H9.893v-1.6c0-2.975 2.338-5.4 5.207-5.4.88 0 2.308 1 5.4 1 3.116 0 4.514-1 5.4-1 2.869 0 5.207 2.425 5.207 5.4V33zM20.5 22c4.791 0 8.679-4.031 8.679-9S25.29 4 20.5 4s-8.679 4.031-8.679 9 3.888 9 8.679 9zm0-15c3.188 0 5.786 2.694 5.786 6s-2.598 6-5.786 6c-3.188 0-5.786-2.694-5.786-6s2.598-6 5.786-6z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 591 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M30.72 25.142h-7.625l4.013 9.562c.28.663-.04 1.406-.679 1.687l-3.534 1.507a1.281 1.281 0 0 1-1.677-.683l-3.813-9.08-6.229 6.267c-.83.835-2.176.192-2.176-.904V3.287c0-1.153 1.432-1.716 2.176-.904l20.442 20.569c.825.786.216 2.19-.898 2.19z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 350 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" stroke="#000" stroke-width="2" fill="none"><path d="M3 9h34v22H3z"/><path d="M33.626 16.983h-2.571v-5.538h-3.858v5.538h-2.571l4.5 6.462z"/></g></svg>

After

Width:  |  Height:  |  Size: 235 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M4 11h32v19H4V11zm3.2 3.167h3.84m-3.84-.254v4.18m26.24 9.5H29.6m3.84.254v-4.18" stroke="#000" stroke-width="2" fill="none" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 222 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M5.816 24.607v6.572h6.53V32H5v-7.393h.816zm29.184 0V32h-7.347v-.821h6.53v-6.572H35zM12.347 9v.821h-6.53v6.572H5V9h7.347zM35 9v7.393h-.816V9.82h-6.53V9H35z" stroke="#000" fill="#000" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 281 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" fill="none"><path stroke="#4A4A4A" stroke-width="1.473" d="M4.236 9.191h31.527v21.8H4.236z"/><path fill="#4A4A4A" d="M2 7h4.5v4.364H2zM33.5 7H38v4.364h-4.5zM33.5 28.818H38v4.364h-4.5zM2 28.818h4.5v4.364H2z"/><g stroke="#4A4A4A" stroke-width="1.591"><path fill="#4A4A4A" d="M10.295 13.613h13.409v8.591H10.295z"/><path d="M17.795 17.977h13.409v8.591H17.795z"/></g></g></svg>

After

Width:  |  Height:  |  Size: 458 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M20 2c9.941 0 18 8.059 18 18s-8.059 18-18 18S2 29.941 2 20 10.059 2 20 2zm0 2.4C11.384 4.4 4.4 11.384 4.4 20S11.384 35.6 20 35.6 35.6 28.616 35.6 20 28.616 4.4 20 4.4zm1.27 25.2h-2.642V14.147h2.642V29.6zm-2.856-19.552c0-.429.13-.79.393-1.086.261-.295.65-.443 1.164-.443.514 0 .904.148 1.17.443.267.295.4.657.4 1.086 0 .428-.133.785-.4 1.07-.266.286-.656.43-1.17.43-.515 0-.903-.144-1.164-.43-.262-.285-.393-.642-.393-1.07z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 535 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M37.011 17.886L22.114 2.99A3.375 3.375 0 0 0 19.727 2H5.375A3.375 3.375 0 0 0 2 5.375v14.352c0 .895.356 1.754.989 2.387L17.886 37.01a3.375 3.375 0 0 0 4.773 0L37.011 22.66a3.375 3.375 0 0 0 0-4.773zm-1.59 3.182L21.068 35.421a1.125 1.125 0 0 1-1.59 0L4.579 20.522a1.118 1.118 0 0 1-.329-.795V5.375c0-.62.505-1.125 1.125-1.125h14.352c.3 0 .583.117.796.33L35.42 19.476a1.126 1.126 0 0 1 0 1.591zm-23.296-10.35a1.408 1.408 0 0 1 0 2.812c-.775.001-1.406-.63-1.406-1.405s.63-1.406 1.406-1.406zm0-1.968a3.375 3.375 0 1 0 0 6.75 3.375 3.375 0 0 0 0-6.75z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 659 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><g transform="translate(2 6)" fill="none" fill-rule="evenodd"><path d="M2.605 0c-1.956 6.618.068 11.128 6.072 13.53 9.005 3.604 14.282.585 19.39 3.156C31.473 18.4 33.85 21.858 35.2 27.06" stroke="#000" stroke-width="2" stroke-linecap="square"/><circle fill="#000" cx="1.956" cy="4.571" r="1.956"/><circle fill="#000" cx="11.733" cy="14.349" r="1.956"/><circle fill="#000" cx="27.378" cy="16.305" r="1.956"/></g></svg>

After

Width:  |  Height:  |  Size: 480 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><circle stroke="#000" stroke-width="2" cx="20" cy="20" r="17"/><circle fill="#000" cx="20" cy="20" r="7.5"/></g></svg>

After

Width:  |  Height:  |  Size: 216 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M20 4.236L4.057 15.82l6.09 18.742h19.707l6.09-18.742L20 4.236z" stroke="#3B3B3B" stroke-width="2" fill="none" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 209 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path stroke="#000" stroke-width="2" d="M3 8h34v25H3z" fill="none" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 157 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M5 30h30v-3.333H5V30zm0-8.333h30v-3.334H5v3.334zM5 10v3.333h30V10H5z" fill="#000" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 181 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M37 8H15v9H3v16h22v-9h12V8z" stroke="#000" stroke-width="2" fill="none" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 171 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M36.918 19.593a1.05 1.05 0 0 0-.231-.346l-3.633-3.633a1.064 1.064 0 1 0-1.505 1.505l1.817 1.816H21.062l.001-12.301L22.96 8.53a1.062 1.062 0 0 0 1.505 0 1.064 1.064 0 0 0 0-1.505L20.752 3.31a1.063 1.063 0 0 0-1.16-.23c-.13.054-.246.13-.343.228l-.002.001-3.636 3.634a1.066 1.066 0 0 0 .753 1.818c.272 0 .545-.105.753-.312l1.818-1.817-.001 12.302H6.635l1.816-1.816a1.065 1.065 0 0 0-1.506-1.505l-3.633 3.634a1.059 1.059 0 0 0 0 1.505l3.713 3.713a1.059 1.059 0 0 0 1.506 0 1.065 1.065 0 0 0 0-1.505l-1.898-1.897h12.3v12.3l-1.816-1.815a1.066 1.066 0 0 0-1.506 1.506l3.636 3.633a1.059 1.059 0 0 0 1.504 0l3.714-3.714a1.064 1.064 0 1 0-1.506-1.505l-1.897 1.898V21.064h12.304l-1.896 1.897a1.065 1.065 0 0 0 1.505 1.505l3.713-3.713c.002-.003.003-.007.007-.01a1.064 1.064 0 0 0 .223-1.15z" fill="#000" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 891 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><g transform="translate(3 7)" fill="#000" fill-rule="evenodd"><rect x="5.75" y="4.5" width="22.5" height="15.75" rx="2.25"/><path stroke="#F1F1F1" stroke-width="2.25" d="M-1.39 2.72l2.451-3.773 33.967 22.057-2.451 3.774z"/></g></svg>

After

Width:  |  Height:  |  Size: 296 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M33.632 2.58a2.058 2.058 0 0 1 2.863 0l1.912 1.868c.79.772.79 2.022.003 2.795L25.42 20l12.99 12.757a1.947 1.947 0 0 1-.003 2.795l-1.912 1.868a2.058 2.058 0 0 1-2.863 0L17.24 21.4a1.947 1.947 0 0 1 0-2.798zm-15.647 0a2.058 2.058 0 0 1 2.863 0l1.912 1.868c.79.772.79 2.022.003 2.795L9.773 20l12.99 12.757a1.947 1.947 0 0 1-.003 2.795l-1.912 1.868a2.058 2.058 0 0 1-2.863 0L1.593 21.4a1.947 1.947 0 0 1 0-2.798z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 521 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M13.762 2v14.774L32 2v36L13.762 23.225V38H7V2h6.762z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 165 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M6.368 2.58a2.058 2.058 0 0 0-2.863 0L1.593 4.448a1.947 1.947 0 0 0-.003 2.795L14.58 20 1.59 32.757a1.947 1.947 0 0 0 .003 2.795l1.912 1.868c.79.773 2.072.773 2.863 0L22.76 21.4c.79-.773.79-2.025 0-2.798zm15.647 0a2.058 2.058 0 0 0-2.863 0L17.24 4.448a1.947 1.947 0 0 0-.003 2.795L30.227 20l-12.99 12.757a1.947 1.947 0 0 0 .003 2.795l1.912 1.868c.79.773 2.072.773 2.863 0L38.407 21.4c.79-.773.79-2.025 0-2.798z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 523 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M25.238 2v14.774L7 2v36l18.238-14.775V38H32V2h-6.762z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 166 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M30.76 21.399L14.368 37.42a2.058 2.058 0 0 1-2.863 0l-1.912-1.868a1.947 1.947 0 0 1-.003-2.795L22.58 20 9.59 7.243a1.947 1.947 0 0 1 .003-2.795l1.912-1.868a2.058 2.058 0 0 1 2.863 0L30.76 18.6c.79.773.79 2.025 0 2.798z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 331 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path fill="#000" fill-rule="nonzero" d="M35.5 20l-30 19.5V.5z"/></svg>

After

Width:  |  Height:  |  Size: 134 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M9.593 21.399L25.985 37.42c.79.773 2.073.773 2.863 0l1.912-1.868c.79-.772.79-2.022.003-2.795L17.773 20l12.99-12.757a1.947 1.947 0 0 0-.003-2.795L28.848 2.58a2.058 2.058 0 0 0-2.863 0L9.593 18.6a1.947 1.947 0 0 0 0 2.798z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 333 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M19.258 20.746l.049 13.82a.7.7 0 0 0 .697.698.687.687 0 0 0 .692-.693l-.048-13.924 13.924.049a.687.687 0 0 0 .692-.692.7.7 0 0 0-.698-.698l-13.82-.048-.048-13.83a.7.7 0 0 0-.697-.697.69.69 0 0 0-.692.693l.049 13.933-13.934-.048a.69.69 0 0 0-.692.692.7.7 0 0 0 .697.697l13.83.048z" stroke="#000" stroke-width="2" fill="#000" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 423 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M0 0h34v24H0z"/></defs><g transform="translate(3 8)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M28.262 9.331C20.935.18 8.932-.428 1.308 7.643V2.968H0v6.537c0 .45.293.817.654.817h5.232V8.688H2.35c7.125-7.322 18.2-6.683 24.987 1.8 7.14 8.92 7.14 23.437 0 32.357L28.262 44c7.65-9.557 7.65-25.111 0-34.669z" fill="#000" mask="url(#b)" transform="matrix(-1 0 0 1 34 0)"/></g></svg>

After

Width:  |  Height:  |  Size: 557 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M5.322 5.322l3.365 3.365A15.947 15.947 0 0 1 20 4c8.817 0 16.006 7.195 16 16.012C35.994 28.843 28.833 36 20 36c-4.124 0-7.884-1.56-10.721-4.123a.776.776 0 0 1-.031-1.125l1.273-1.273a.777.777 0 0 1 1.065-.036A12.6 12.6 0 0 0 20 32.645c6.988 0 12.645-5.655 12.645-12.645 0-6.988-5.655-12.645-12.645-12.645a12.603 12.603 0 0 0-8.943 3.702l3.492 3.492a.774.774 0 0 1-.547 1.322H4.774A.774.774 0 0 1 4 15.097V5.869a.774.774 0 0 1 1.322-.547z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 549 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M29.131 5l5.257 5.257V35H5V5h24.131zm-.866 17.143H10.51v11.633h17.755V22.143zM10.51 6.224H6.224v27.552h3.062V20.918H29.49v12.858h3.673V10.764l-4.539-4.54h-.97v9.796H10.51V6.224zm8.878 20.205a.612.612 0 0 1 .1 1.216l-.1.008h-6.123a.612.612 0 0 1-.1-1.216l.1-.008h6.123zm2.271.177c.11.116.178.276.178.435 0 .159-.068.318-.178.435a.644.644 0 0 1-.435.177.63.63 0 0 1-.434-.177.64.64 0 0 1-.178-.435.64.64 0 0 1 .178-.435.641.641 0 0 1 .87 0zm-4.108-2.626a.612.612 0 0 1 .1 1.216l-.1.008h-4.286a.612.612 0 0 1-.1-1.216l.1-.008h4.286zm8.878-17.756H11.735v8.572h14.694V6.224zM25.51 7.45v6.122h-3.673V7.45h3.673zm-1.224 1.224H23.06v3.674h1.225V8.673z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 756 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M22.147 2C23.169 2 24 2.831 24 3.853h0v3.368c0 .542.298.988.798 1.195a1.257 1.257 0 0 0 1.41-.28h0l2.38-2.381c.701-.702 1.922-.7 2.622 0h0l3.035 3.035c.35.35.543.816.543 1.311s-.193.961-.543 1.311h0l-2.381 2.38c-.382.383-.487.91-.28 1.41.207.5.653.798 1.195.798h3.368c1.022 0 1.853.831 1.853 1.853h0v4.294A1.855 1.855 0 0 1 36.147 24h0-3.368c-.542 0-.989.298-1.196.798a1.26 1.26 0 0 0 .28 1.41h0l2.382 2.38c.35.35.542.816.542 1.31 0 .496-.192.962-.542 1.312h0l-3.036 3.035c-.7.7-1.92.702-2.622 0h0l-2.38-2.381a1.257 1.257 0 0 0-1.41-.28c-.5.207-.798.653-.798 1.195h0v3.368A1.855 1.855 0 0 1 22.146 38h0-4.293A1.855 1.855 0 0 1 16 36.147h0v-3.368c0-.542-.298-.988-.798-1.195a1.258 1.258 0 0 0-1.41.28h0l-2.38 2.381c-.701.702-1.921.701-2.622 0h0L5.755 31.21a1.844 1.844 0 0 1-.543-1.311c0-.495.193-.961.543-1.311h0l2.381-2.38c.382-.383.487-.91.28-1.41A1.257 1.257 0 0 0 7.221 24h0-3.368A1.855 1.855 0 0 1 2 22.146h0v-4.293C2 16.831 2.831 16 3.853 16h3.368c.542 0 .988-.298 1.195-.798a1.26 1.26 0 0 0-.28-1.41h0l-2.381-2.38a1.843 1.843 0 0 1-.543-1.31c0-.496.193-.962.543-1.312h0L8.79 5.755c.7-.7 1.92-.702 2.622 0h0l2.38 2.38c.383.382.911.49 1.41.281.5-.207.798-.653.798-1.195h0V3.853C16 2.831 16.831 2 17.853 2h0zm-.001 1.333h-4.293a.52.52 0 0 0-.52.52h0v3.368a2.584 2.584 0 0 1-1.621 2.426 2.586 2.586 0 0 1-2.863-.569h0l-2.38-2.38a.52.52 0 0 0-.736 0h0L6.697 9.732a.52.52 0 0 0 0 .736h0l2.382 2.38c.766.766.984 1.863.569 2.863a2.586 2.586 0 0 1-2.427 1.621h0-3.368a.52.52 0 0 0-.52.52h0v4.294c0 .286.234.52.52.52h3.368c1.083 0 2.013.621 2.427 1.62.415 1 .196 2.098-.57 2.863h0l-2.38 2.38a.52.52 0 0 0 0 .737h0l3.035 3.035a.52.52 0 0 0 .736 0h0l2.38-2.381a2.59 2.59 0 0 1 2.863-.569 2.586 2.586 0 0 1 1.621 2.427h0v3.368c0 .286.234.52.52.52h4.294a.52.52 0 0 0 .52-.52h0v-3.368c0-1.083.621-2.013 1.621-2.427 1-.413 2.097-.197 2.863.57h0l2.38 2.38a.52.52 0 0 0 .736 0h0l3.036-3.035a.52.52 0 0 0 0-.736h0l-2.382-2.38a2.585 2.585 0 0 1-.569-2.863 2.586 2.586 0 0 1 2.427-1.621h3.368a.52.52 0 0 0 .52-.52h0v-4.294a.52.52 0 0 0-.52-.519h0-3.368a2.586 2.586 0 0 1-2.427-1.621c-.415-1-.196-2.098.57-2.863h0l2.38-2.38a.52.52 0 0 0 0-.737h0l-3.035-3.035a.52.52 0 0 0-.736 0h0l-2.38 2.38a2.585 2.585 0 0 1-2.863.57 2.586 2.586 0 0 1-1.621-2.427h0V3.853a.52.52 0 0 0-.521-.52h0zM20 14c3.309 0 6 2.691 6 6s-2.691 6-6 6-6-2.691-6-6 2.691-6 6-6zm0 1.333A4.673 4.673 0 0 0 15.333 20 4.673 4.673 0 0 0 20 24.667 4.673 4.673 0 0 0 24.667 20 4.673 4.673 0 0 0 20 15.333z" stroke="#000" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><g fill-rule="nonzero" stroke="#000" stroke-width="2" fill="none"><path d="M3 9h34v22H3z"/><path d="M28.5 9H37v22h-8.5z"/></g></svg>

After

Width:  |  Height:  |  Size: 195 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M5 10.75h30v19.5H5v-19.5zM20 7v27" stroke="#4A4A4A" stroke-width="2" fill="none" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 180 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M20.5 11c2.475 0 4.5-2.025 4.5-4.5S22.975 2 20.5 2A4.513 4.513 0 0 0 16 6.5c0 2.475 2.025 4.5 4.5 4.5zm0 4.5A4.513 4.513 0 0 0 16 20c0 2.475 2.025 4.5 4.5 4.5S25 22.475 25 20s-2.025-4.5-4.5-4.5zm0 13.5a4.513 4.513 0 0 0-4.5 4.5c0 2.475 2.025 4.5 4.5 4.5s4.5-2.025 4.5-4.5-2.025-4.5-4.5-4.5z" fill="#000" fill-rule="evenodd"/></svg>

After

Width:  |  Height:  |  Size: 403 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M0 0h34v24H0z"/></defs><g transform="translate(3 8)" fill="none" fill-rule="evenodd"><mask id="b" fill="#fff"><use xlink:href="#a"/></mask><path d="M28.262 9.331C20.935.18 8.932-.428 1.308 7.643V2.968H0v6.537c0 .45.293.817.654.817h5.232V8.688H2.35c7.125-7.322 18.2-6.683 24.987 1.8 7.14 8.92 7.14 23.437 0 32.357L28.262 44c7.65-9.557 7.65-25.111 0-34.669z" fill="#000" mask="url(#b)"/></g></svg>

After

Width:  |  Height:  |  Size: 523 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><g stroke="#4A4A4A" stroke-width="2.25" fill="none" fill-rule="evenodd"><path d="M2 7h27.014v16.766H2V7z" stroke-linecap="square" stroke-dasharray="1.131428450345993,3.394285619258881"/><path d="M31.372 27.628l5.635 6.231m-10.106-4.678c3.394 0 6.146-2.723 6.146-6.082 0-3.36-2.752-6.082-6.146-6.082-3.395 0-6.147 2.723-6.147 6.082s2.752 6.082 6.147 6.082z" fill="#F1F1F1" fill-rule="nonzero" stroke-linecap="round"/></g></svg>

After

Width:  |  Height:  |  Size: 489 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M36.889 5.188H29.5L27.389 3H22.11C20.945 3 20 3.98 20 5.188v10.937c0 1.208.945 2.188 2.111 2.188H36.89c1.166 0 2.111-.98 2.111-2.188v-8.75c0-1.208-.945-2.188-2.111-2.188zm0 19.687H29.5l-2.111-2.188H22.11c-1.166 0-2.111.98-2.111 2.188v10.938C20 37.02 20.945 38 22.111 38H36.89C38.055 38 39 37.02 39 35.812v-8.75c0-1.208-.945-2.187-2.111-2.187zM5.222 4.094C5.222 3.49 4.75 3 4.167 3H2.056C1.473 3 1 3.49 1 4.094v27.343c0 1.209.945 2.188 2.111 2.188H17.89V29.25H5.222V13.937H17.89V9.564H5.222v-5.47z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 609 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M35.748 2H4.253C2.254 2 1.246 4.425 2.662 5.841L15.5 18.682v13.13c0 .65.28 1.267.768 1.694l4.5 3.936c1.437 1.258 3.732.259 3.732-1.693V18.682L37.339 5.841C38.752 4.428 37.75 2 35.748 2zM22.25 17.75v18l-4.5-3.937V17.75L4.25 4.25h31.5l-13.5 13.5z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 357 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M20 28.656c-4.267 0-7.72-3.325-8.038-7.54l-5.9-4.591c-.777.98-1.49 2.016-2.066 3.149a1.844 1.844 0 0 0 0 1.653C7.046 27.32 13.085 31.375 20 31.375c1.514 0 2.974-.227 4.381-.593l-2.919-2.274a8.053 8.053 0 0 1-1.462.148zm17.652 3.291l-6.218-4.84a18.74 18.74 0 0 0 4.57-5.78 1.844 1.844 0 0 0 0-1.654C32.954 13.68 26.914 9.625 20 9.625a17.24 17.24 0 0 0-8.287 2.135L4.557 6.191a.896.896 0 0 0-1.263.16L2.189 7.78a.91.91 0 0 0 .159 1.272l33.095 25.756a.896.896 0 0 0 1.263-.16l1.105-1.43a.91.91 0 0 0-.159-1.272zm-10.334-8.043l-2.21-1.72a5.38 5.38 0 0 0-1.812-6.027 5.3 5.3 0 0 0-4.72-.88c.34.463.523 1.023.524 1.598a2.659 2.659 0 0 1-.087.566l-4.14-3.222A7.972 7.972 0 0 1 20 12.344a8.067 8.067 0 0 1 5.729 2.387 8.18 8.18 0 0 1 2.37 5.769c0 1.225-.297 2.367-.781 3.405z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 880 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M33.464 17.75h-1.768v-5.063C31.696 6.795 26.673 2 20.5 2S9.304 6.795 9.304 12.688v5.062H7.536C5.584 17.75 4 19.262 4 21.125v13.5C4 36.488 5.584 38 7.536 38h25.928C35.416 38 37 36.488 37 34.625v-13.5c0-1.863-1.584-3.375-3.536-3.375zm-7.66 0H15.196v-5.063c0-2.79 2.38-5.062 5.304-5.062 2.924 0 5.304 2.271 5.304 5.063v5.062z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 435 B

@ -0,0 +1 @@
<svg width="40" height="40" xmlns="http://www.w3.org/2000/svg"><path d="M20.5 3c4.885 0 8.857 3.812 8.857 8.5 0 4.688-3.972 8.5-8.857 8.5s-8.857-3.812-8.857-8.5c0-4.688 3.972-8.5 8.857-8.5zM36 34.45c0 1.408-1.384 2.55-3.1 2.55H8.1C6.384 37 5 35.858 5 34.45V31.9c0-4.223 4.166-7.65 9.3-7.65h.692a14.802 14.802 0 0 0 11.016 0h.692c5.134 0 9.3 3.427 9.3 7.65v2.55z" fill="#000" fill-rule="nonzero"/></svg>

After

Width:  |  Height:  |  Size: 402 B

File diff suppressed because it is too large Load Diff

@ -1,56 +1,51 @@
{
"name": "cvat-ui",
"version": "0.1.0",
"license": "MIT",
"private": true,
"dependencies": {
"@types/jest": "24.0.13",
"@types/node": "^12.0.3",
"@types/react": "16.8.19",
"@types/react-dom": "16.8.4",
"@types/react-redux": "^7.1.1",
"@types/react-router-dom": "^4.3.4",
"@types/redux-logger": "^3.0.7",
"antd": "^3.19.1",
"babel-plugin-import": "^1.11.2",
"customize-cra": "^0.2.12",
"less": "^3.9.0",
"less-loader": "^5.0.0",
"node-sass": "^4.12.0",
"query-string": "^6.8.1",
"react": "^16.8.6",
"react-app-rewired": "^2.1.3",
"react-dom": "^16.8.6",
"react-redux": "^7.1.0",
"react-router-dom": "^5.0.1",
"react-scripts": "3.0.1",
"react-scripts-ts": "^3.1.0",
"redux": "^4.0.4",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0",
"source-map-explorer": "^1.8.0",
"typescript": "3.4.5"
},
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'",
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
"build": "webpack --config ./webpack.config.js",
"server": "nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --mode=development --open'"
},
"eslintConfig": {
"extends": "react-app"
"author": "Intel",
"license": "MIT",
"devDependencies": {
"@babel/core": "^7.6.0",
"@babel/preset-env": "^7.6.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.6.0",
"@typescript-eslint/eslint-plugin": "^1.13.0",
"babel": "^6.23.0",
"babel-loader": "^8.0.6",
"css-loader": "^3.2.0",
"eslint-config-airbnb-typescript": "^4.0.1",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.14.3",
"html-webpack-plugin": "^3.2.0",
"nodemon": "^1.19.2",
"style-loader": "^1.0.0",
"typescript": "^3.6.3",
"webpack": "^4.39.3",
"webpack-cli": "^3.3.8",
"webpack-dev-server": "^3.8.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
"dependencies": {
"@types/react": "^16.9.2",
"@types/react-dom": "^16.9.0",
"@types/react-redux": "^7.1.2",
"@types/react-router": "^5.0.5",
"@types/react-router-dom": "^5.1.0",
"antd": "^3.23.2",
"dotenv-webpack": "^1.7.0",
"moment": "^2.24.0",
"prop-types": "^15.7.2",
"react": "^16.9.0",
"react-dom": "^16.9.0",
"react-redux": "^7.1.1",
"react-router": "^5.1.0",
"react-router-dom": "^5.1.0",
"redux": "^4.0.4",
"redux-thunk": "^2.3.0"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 3.3 KiB

@ -1,40 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>CVAT</title>
<script src="./cvat-core.min.js"></script>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

@ -1,15 +0,0 @@
{
"short_name": "CVAT",
"name": "Computer Vision Annotation Tool",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

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

@ -0,0 +1,165 @@
import { AnyAction, Dispatch, ActionCreator } from 'redux';
import { ThunkAction } from 'redux-thunk';
import getCore from '../core';
const cvat = getCore();
export enum AuthActionTypes {
AUTHORIZED_SUCCESS = 'AUTHORIZED_SUCCESS',
AUTHORIZED_FAILED = 'AUTHORIZED_FAILED',
LOGIN_SUCCESS = 'LOGIN_SUCCESS',
LOGIN_FAILED = 'LOGIN_FAILED',
REGISTER_SUCCESS = 'REGISTER_SUCCESS',
REGISTER_FAILED = 'REGISTER_FAILED',
LOGOUT_SUCCESS = 'LOGOUT_SUCCESS',
LOGOUT_FAILED = 'LOGOUT_FAILED',
}
export function registerSuccess(user: any): AnyAction {
return {
type: AuthActionTypes.REGISTER_SUCCESS,
payload: {
user,
},
};
}
export function registerFailed(registerError: any): AnyAction {
return {
type: AuthActionTypes.REGISTER_FAILED,
payload: {
registerError,
},
};
}
export function loginSuccess(user: any): AnyAction {
return {
type: AuthActionTypes.LOGIN_SUCCESS,
payload: {
user,
},
};
}
export function loginFailed(loginError: any): AnyAction {
return {
type: AuthActionTypes.LOGIN_FAILED,
payload: {
loginError,
},
};
}
export function logoutSuccess(): AnyAction {
return {
type: AuthActionTypes.LOGOUT_SUCCESS,
payload: {},
};
}
export function logoutFailed(logoutError: any): AnyAction {
return {
type: AuthActionTypes.LOGOUT_FAILED,
payload: {
logoutError,
},
};
}
export function authorizedSuccess(user: any): AnyAction {
return {
type: AuthActionTypes.AUTHORIZED_SUCCESS,
payload: {
user,
},
};
}
export function authorizedFailed(authError: any): AnyAction {
return {
type: AuthActionTypes.AUTHORIZED_FAILED,
payload: {
authError,
},
};
}
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> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
let users = null;
try {
await cvat.server.register(username, firstName, lastName,
email, password1, password2);
users = await cvat.users.get({ self: true });
} catch (error) {
dispatch(registerFailed(error));
return;
}
dispatch(registerSuccess(users[0]));
};
}
export function loginAsync({ username, password }: {username: string; password: string}):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
let users = null;
try {
await cvat.server.login(username, password);
users = await cvat.users.get({ self: true });
} catch (error) {
dispatch(loginFailed(error));
return;
}
dispatch(loginSuccess(users[0]));
};
}
export function logoutAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
await cvat.server.logout();
} catch (error) {
dispatch(logoutFailed(error));
return;
}
dispatch(logoutSuccess());
};
}
export function authorizedAsync(): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
let result = null;
try {
result = await cvat.server.authorized();
} catch (error) {
dispatch(authorizedFailed(error));
return;
}
if (result) {
const userInstance = (await cvat.users.get({ self: true }))[0];
dispatch(authorizedSuccess(userInstance));
} else {
dispatch(authorizedSuccess(null));
}
};
}

@ -1,172 +0,0 @@
import { History } from 'history';
import { Dispatch, ActionCreator } from 'redux';
export const login = () => (dispatch: Dispatch) => {
dispatch({
type: 'LOGIN',
});
}
export const loginSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'LOGIN_SUCCESS',
});
}
export const loginError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'LOGIN_ERROR',
payload: error,
});
}
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(
(loggedIn: any) => {
dispatch(loginSuccess());
history.push(history.location.state ? history.location.state.from : '/tasks');
},
(error: any) => {
dispatch(loginError(error));
throw error;
},
);
};
}
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,109 +0,0 @@
import { Dispatch, ActionCreator } from 'redux';
export const getServerInfo = () => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SERVER_INFO',
});
}
export const getServerInfoSuccess = (information: null) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SERVER_INFO_SUCCESS',
payload: information,
});
}
export const getServerInfoError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SERVER_INFO_ERROR',
payload: error,
});
}
export const getShareFiles = () => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SHARE_FILES',
});
}
export const getShareFilesSuccess = (files: []) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SHARE_FILES_SUCCESS',
payload: files,
});
}
export const getShareFilesError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_SHARE_FILES_ERROR',
payload: error,
});
}
export const getAnnotationFormats = () => (dispatch: Dispatch) => {
dispatch({
type: 'GET_ANNOTATION_FORMATS',
});
}
export const getAnnotationFormatsSuccess = (annotationFormats: []) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_ANNOTATION_FORMATS_SUCCESS',
payload: annotationFormats,
});
}
export const getAnnotationFormatsError = (error = {}) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_ANNOTATION_FORMATS_ERROR',
payload: error,
});
}
export const getServerInfoAsync = () => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(getServerInfo());
return (window as any).cvat.server.about().then(
(information: any) => {
dispatch(getServerInfoSuccess(information));
},
(error: any) => {
dispatch(getServerInfoError(error));
},
);
};
}
export const getShareFilesAsync = (directory: string) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(getShareFiles());
return (window as any).cvat.server.share(directory).then(
(files: any) => {
dispatch(getShareFilesSuccess(files));
},
(error: any) => {
dispatch(getShareFilesError(error));
},
);
};
}
export const getAnnotationFormatsAsync = () => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(getAnnotationFormats());
return (window as any).cvat.server.formats().then(
(formats: any) => {
dispatch(getAnnotationFormatsSuccess(formats));
},
(error: any) => {
dispatch(getAnnotationFormatsError(error));
throw error;
},
);
};
}

@ -0,0 +1,80 @@
import { AnyAction, Dispatch, ActionCreator } from 'redux';
import { ThunkAction } from 'redux-thunk';
import { TasksQuery } from '../reducers/interfaces';
import getCore from '../core';
const cvat = getCore();
export enum TasksActionTypes {
GET_TASKS_SUCCESS = 'GET_TASKS_SUCCESS',
GET_TASKS_FAILED = 'GET_TASKS_FAILED',
}
export function getTasksSuccess(array: any[], previews: string[],
count: number, query: TasksQuery): AnyAction {
const action = {
type: TasksActionTypes.GET_TASKS_SUCCESS,
payload: {
previews,
array,
count,
query,
},
};
return action;
}
export function getTasksFailed(error: any, query: TasksQuery): AnyAction {
const action = {
type: TasksActionTypes.GET_TASKS_FAILED,
payload: {
error,
query,
},
};
return action;
}
export function getTasksAsync(query: TasksQuery):
ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
// We need remove all keys with null values from query
const filteredQuery = { ...query };
for (const key in filteredQuery) {
if (filteredQuery[key] === null) {
delete filteredQuery[key];
}
}
let result = null;
try {
result = await cvat.tasks.get(filteredQuery);
} catch (error) {
dispatch(getTasksFailed(error, query));
return;
}
const array = Array.from(result);
const previews = [];
const promises = array
.map((task): string => (task as any).frames.preview());
for (const promise of promises) {
try {
// a tricky moment
// await is okay in loop in this case, there aren't any performance bottleneck
// because all server requests have been already sent in parallel
// eslint-disable-next-line no-await-in-loop
previews.push(await promise);
} catch (error) {
previews.push('');
}
}
dispatch(getTasksSuccess(array, previews, result.count, query));
};
}

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

@ -1,175 +0,0 @@
import { History } from 'history';
import { Dispatch, ActionCreator } from 'redux';
import queryString from 'query-string';
import setQueryObject from '../utils/tasks-filter'
export const getTasks = () => (dispatch: Dispatch) => {
dispatch({
type: 'GET_TASKS',
});
}
export const getTasksSuccess = (tasks: []) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_TASKS_SUCCESS',
payload: tasks,
});
}
export const getTasksError = (error: {}) => (dispatch: Dispatch) => {
dispatch({
type: 'GET_TASKS_ERROR',
payload: error,
});
}
export const createTask = () => (dispatch: Dispatch) => {
dispatch({
type: 'CREATE_TASK',
});
}
export const createTaskSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'CREATE_TASK_SUCCESS',
});
}
export const createTaskError = (error: {}) => (dispatch: Dispatch) => {
dispatch({
type: 'CREATE_TASK_ERROR',
payload: error,
});
}
export const updateTask = () => (dispatch: Dispatch) => {
dispatch({
type: 'UPDATE_TASK',
});
}
export const updateTaskSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'UPDATE_TASK_SUCCESS',
});
}
export const updateTaskError = (error: {}) => (dispatch: Dispatch) => {
dispatch({
type: 'UPDATE_TASK_ERROR',
payload: error,
});
}
export const deleteTask = () => (dispatch: Dispatch) => {
dispatch({
type: 'DELETE_TASK',
});
}
export const deleteTaskSuccess = () => (dispatch: Dispatch) => {
dispatch({
type: 'DELETE_TASK_SUCCESS',
});
}
export const deleteTaskError = (error: {}) => (dispatch: Dispatch) => {
dispatch({
type: 'DELETE_TASK_ERROR',
payload: error,
});
}
export const getTasksAsync = (queryObject = {}) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(getTasks());
return (window as any).cvat.tasks.get(queryObject).then(
(tasks: any) => {
dispatch(getTasksSuccess(tasks));
},
(error: any) => {
dispatch(getTasksError(error));
throw error;
},
);
};
}
export const createTaskAsync = (task: any) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(createTask());
return task.save().then(
(created: any) => {
dispatch(createTaskSuccess());
return dispatch(getTasksAsync());
},
(error: any) => {
dispatch(createTaskError(error));
throw error;
},
);
};
}
export const updateTaskAsync = (task: any) => {
return (dispatch: ActionCreator<Dispatch>) => {
dispatch(updateTask());
return task.save().then(
(updated: any) => {
dispatch(updateTaskSuccess());
return dispatch(getTasksAsync());
},
(error: any) => {
dispatch(updateTaskError(error));
throw error;
},
);
};
}
export const deleteTaskAsync = (task: any, history: History) => {
return (dispatch: ActionCreator<Dispatch>, getState: any) => {
dispatch(deleteTask());
return task.delete().then(
(deleted: any) => {
dispatch(deleteTaskSuccess());
const state = getState();
const queryObject = {
page: state.tasksFilter.currentPage,
search: state.tasksFilter.searchQuery,
}
if (state.tasks.tasks.length === 1 && state.tasks.tasksCount !== 1) {
queryObject.page = queryObject.page - 1;
history.push({ search: queryString.stringify(queryObject) });
} else if (state.tasks.tasksCount === 1) {
return dispatch(getTasksAsync());
} else {
const query = setQueryObject(queryObject);
return dispatch(getTasksAsync(query));
}
},
(error: any) => {
dispatch(deleteTaskError(error));
throw error;
},
);
};
}

@ -1,40 +0,0 @@
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;
},
);
};
}

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

@ -1,65 +0,0 @@
import React, { PureComponent } from 'react';
import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
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';
import './app.scss';
const ProtectedRoute = ({ component: Component, ...rest }: any) => {
return (
<Route
{ ...rest }
render={ (props) => {
return rest.isAuthenticated ? (
<>
<HeaderLayout />
<Component { ...props } />
</>
) : (
<Redirect
to={{
pathname: '/login',
state: {
from: props.location,
},
}}
/>
);
} }
/>
);
};
class App extends PureComponent<any, any> {
componentDidMount() {
(window as any).cvat.config.backendAPI = process.env.REACT_APP_API_FULL_URL;
}
render() {
return(
<Router>
<Switch>
<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 } />
</Switch>
</Router>
);
}
}
const mapStateToProps = (state: any) => {
return state.authContext;
};
export default connect(mapStateToProps)(App);

@ -1,66 +0,0 @@
.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 +0,0 @@
import React from 'react';
import ReactDOM from 'react-dom';
import HeaderLayout from './header-layout';
it('renders without crashing', () => {
const div = document.createElement('div');
ReactDOM.render(<HeaderLayout />, div);
ReactDOM.unmountComponentAtNode(div);
});

@ -1,87 +0,0 @@
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);

@ -0,0 +1,92 @@
import React from 'react';
import { FormComponentProps } from 'antd/lib/form/Form';
import {
Button,
Icon,
Input,
Form,
} from 'antd';
export interface LoginData {
username: string;
password: string;
}
type LoginFormProps = {
onSubmit(loginData: LoginData): void;
} & FormComponentProps;
class LoginForm extends React.PureComponent<LoginFormProps> {
constructor(props: LoginFormProps) {
super(props);
}
private handleSubmit(e: React.FormEvent) {
e.preventDefault();
this.props.form.validateFields((error, values) => {
if (!error) {
this.props.onSubmit(values);
}
});
}
private renderUsernameField() {
const { getFieldDecorator } = this.props.form;
return (
<Form.Item hasFeedback>
{getFieldDecorator('username', {
rules: [{
required: true,
message: 'Please specify a username',
}],
})(
<Input
autoComplete='username'
prefix={<Icon type='user' style={{ color: 'rgba(0,0,0,.25)'}} />}
placeholder='Username'
/>
)}
</Form.Item>
)
}
private renderPasswordField() {
const { getFieldDecorator } = this.props.form;
return (
<Form.Item hasFeedback>
{getFieldDecorator('password', {
rules: [{
required: true,
message: 'Please specify a password',
}],
})(
<Input
autoComplete='current-password'
prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)'}} />}
placeholder='Password'
type='password'
/>
)}
</Form.Item>
)
}
public render() {
return (
<Form onSubmit={this.handleSubmit.bind(this)} className='login-form'>
{this.renderUsernameField()}
{this.renderPasswordField()}
<Form.Item>
<Button type='primary' htmlType='submit' className='login-form-button'>
Sign in
</Button>
</Form.Item>
</Form>
);
}
}
export default Form.create<LoginFormProps>()(LoginForm);

@ -1,10 +0,0 @@
.login-form {
display: flex;
flex-direction: column;
justify-content: center;
height: 100vh;
&__title {
}
}

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

@ -1,105 +0,0 @@
import React, { PureComponent } from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { loginAsync, isAuthenticatedAsync } from '../../actions/auth.actions';
import { getUsersAsync } from '../../actions/users.actions';
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> {
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 (
<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>
);
}
private onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
this.props.form.validateFields((error: any, values: any) => {
if (!error) {
this.props.dispatch(loginAsync(values.username, values.password, this.props.history)).then(
(loggedIn: any) => {
this.props.dispatch(getUsersAsync({ self: true }));
},
);
}
});
}
}
const mapStateToProps = (state: any) => {
return state.authContext;
};
export default Form.create()(connect(mapStateToProps)(LoginForm));

@ -1,8 +0,0 @@
.ant-badge {
width: 100%;
}
.ant-tree.ant-tree-directory {
height: 108px;
overflow: auto;
}

@ -1,352 +0,0 @@
import React, { PureComponent } from 'react';
import { Form, Input, Icon, Checkbox, Radio, Upload, Badge, Tree, TreeSelect, InputNumber } from 'antd';
import { UploadFile, UploadChangeParam } from 'antd/lib/upload/interface';
import configureStore from '../../../store';
import { getShareFilesAsync } from '../../../actions/server.actions';
import { validateLabels, FileSource, fileModel } from '../../../utils/tasks-dto';
import './task-create.scss';
const { TreeNode } = Tree;
const { SHOW_PARENT } = TreeSelect;
const { Dragger } = Upload;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 8 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
};
const formItemTailLayout = {
labelCol: {
xs: { span: 24 },
},
wrapperCol: {
xs: { span: 24 },
},
};
class TaskCreateForm extends PureComponent<any, any> {
store: any;
constructor(props: any) {
super(props);
this.store = configureStore();
this.state = {
confirmDirty: false,
selectedFileList: [],
filesCounter: 0,
treeData: [],
};
}
componentDidMount() {
this.getSharedFiles('').then(
(data: any) => {
this.setState({ treeData: fileModel('', this.store.getState().server.files) });
},
);
}
private renderTreeNodes = (data: any) => {
return data.map((item: any) => {
if (!item.isLeaf) {
return (
<TreeNode title={ item.name } key={ item.id } value={ item.id } dataRef={ item }>
{ item.children ? this.renderTreeNodes(item.children) : '' }
</TreeNode>
);
}
return <TreeNode isLeaf title={ item.name } key={ item.id } value={ item.id } dataRef={ item } />;
});
}
private renderUploader = () => {
const { getFieldDecorator } = this.props.form;
switch (this.props.form.getFieldValue('source')) {
case FileSource.Local:
return (
<Form.Item
{ ...formItemTailLayout }
extra='Only one video, archive, pdf or many image, directory can be used simultaneously'>
<Badge
count={ this.state.filesCounter }
overflowCount={999}>
<div onClick={ this.resetUploader }>
{getFieldDecorator('localUpload', {
rules: [{ required: true, message: 'Please, add some files!' }],
})(
<Dragger
multiple
showUploadList={ false }
fileList={ this.state.selectedFileList }
customRequest={ this.simulateRequest }
onChange={ this.onUploaderChange }>
<p className="ant-upload-drag-icon">
<Icon type="inbox" />
</p>
<p className="ant-upload-text">Click or drag file to this area to upload</p>
</Dragger>
)}
</div>
</Badge>
</Form.Item>
);
case FileSource.Remote:
return (
<Form.Item { ...formItemLayout } label="URLs">
{getFieldDecorator('remoteURL', {
rules: [],
})(
<Input.TextArea
name="remote-url"
/>
)}
</Form.Item>
);
case FileSource.Share:
return (
<Form.Item { ...formItemLayout } label="Shared files"
extra='Only one video, archive, pdf or many image, directory can be used simultaneously'>
{getFieldDecorator('sharedFiles', {
rules: [{ required: true, message: 'Please, add some files!' }],
})(
<TreeSelect
multiple
treeCheckable={ true }
showCheckedStrategy={ SHOW_PARENT }
loadData={ this.onLoadData }>
{ this.renderTreeNodes(this.state.treeData) }
</TreeSelect>
)}
</Form.Item>
);
default:
break;
}
}
render() {
const { getFieldDecorator } = this.props.form;
return (
<Form>
<Form.Item { ...formItemLayout } label="Name">
{getFieldDecorator('name', {
rules: [
{
required: true,
pattern: new RegExp('[a-zA-Z0-9_]+'),
message: 'Bad task name!',
},
],
})(
<Input
prefix={ <Icon type="profile" /> }
type="text"
name="name"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Labels">
{getFieldDecorator('labels', {
rules: [
{ required: true, message: 'Please add some labels!' },
{ validator: validateLabels, message: 'Bad labels format!' },
],
})(
<Input
prefix={ <Icon type="tag" /> }
type="text"
name="labels"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Bug tracker">
{getFieldDecorator('bugTracker', {
rules: [{ type: 'url', message: 'Bad bug tracker link!' }],
})(
<Input
prefix={ <Icon type="tool" /> }
type="text"
name="bug-tracker"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Source">
{getFieldDecorator('source', {
rules: [],
initialValue: 1,
})(
<Radio.Group onChange={ this.resetUploader }>
<Radio.Button value={1}>Local</Radio.Button>
<Radio.Button value={2}>Remote</Radio.Button>
<Radio.Button value={3}>Share</Radio.Button>
</Radio.Group>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Z-Order">
{getFieldDecorator('zOrder', {
rules: [],
initialValue: false,
})(
<Checkbox
name="z-order"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Segment size" hasFeedback>
{getFieldDecorator('segmentSize', {
rules: [],
})(
<InputNumber
min={100}
max={50000}
name="segment-size"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Overlap size" hasFeedback>
{getFieldDecorator('overlapSize', {
rules: [],
initialValue: 0,
})(
<InputNumber
min={0}
max={ this.props.form.getFieldValue('segmentSize') ? this.props.form.getFieldValue('segmentSize') - 1 : 0 }
name="overlap-size"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Image quality">
{getFieldDecorator('imageQuality', {
rules: [{ required: true }],
initialValue: 50,
})(
<InputNumber
min={1}
max={95}
name="image-quality"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Start frame" hasFeedback>
{getFieldDecorator('startFrame', {
rules: [],
initialValue: 0,
})(
<InputNumber
min={0}
name="start-frame"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Stop frame" hasFeedback>
{getFieldDecorator('stopFrame', {
rules: [],
})(
<InputNumber
min={ this.props.form.getFieldValue('startFrame') }
name="stop-frame"
/>
)}
</Form.Item>
<Form.Item { ...formItemLayout } label="Frame filter">
{getFieldDecorator('frameFilter', {
rules: [],
})(
<Input
prefix={ <Icon type="profile" /> }
type="text"
name="frame-filter"
/>
)}
</Form.Item>
{ this.renderUploader() }
</Form>
);
}
private onLoadData = (treeNode: any) => {
return new Promise<void>(resolve => {
if (treeNode.props.children) {
resolve();
return;
}
this.getSharedFiles(treeNode.props.dataRef.id).then(
(data: any) => {
treeNode.props.dataRef.children = fileModel(treeNode, this.store.getState().server.files);
this.setState({
treeData: [...this.state.treeData],
});
resolve();
},
);
});
}
private getSharedFiles = (directory: string) => {
return this.store.dispatch(getShareFilesAsync(directory));
}
private onUploaderChange = (info: UploadChangeParam) => {
const nextState: { selectedFileList: UploadFile[], filesCounter: number } = {
selectedFileList: this.state.selectedFileList,
filesCounter: this.state.filesCounter,
};
switch (info.file.status) {
case 'uploading':
nextState.selectedFileList.push(info.file);
nextState.filesCounter += 1;
break;
case 'done':
break;
default:
// INFO: error or removed
nextState.selectedFileList = info.fileList;
}
this.setState(() => nextState);
}
private resetUploader = () => {
this.setState({ selectedFileList: [], filesCounter: 0 });
}
private simulateRequest = ({ file, onSuccess }: any) => {
setTimeout(() => {
onSuccess(file);
}, 0);
}
}
export default Form.create()(TaskCreateForm);

@ -1,51 +0,0 @@
import React, { PureComponent } from 'react';
import { Form, Input, Icon } from 'antd';
import { serializeLabels, validateLabels } from '../../../utils/tasks-dto'
import './task-update.scss';
class TaskUpdateForm extends PureComponent<any, any> {
render() {
const { getFieldDecorator } = this.props.form;
return (
<Form>
<Form.Item>
{getFieldDecorator('oldLabels', {
rules: [],
initialValue: serializeLabels(this.props.task),
})(
<Input
disabled
prefix={ <Icon type="tag" /> }
type="text"
name="oldLabels"
placeholder="Old labels"
/>,
)}
</Form.Item>
<Form.Item>
{getFieldDecorator('newLabels', {
rules: [
{ required: true, message: 'Please input new labels!' },
{ validator: validateLabels, message: 'Bad labels format!' },
],
})(
<Input
prefix={ <Icon type="tag" /> }
type="text"
name="new-labels"
placeholder="Expand the specification here"
/>,
)}
</Form.Item>
</Form>
);
}
}
export default Form.create()(TaskUpdateForm) as any;

@ -1,3 +0,0 @@
.empty.not-found {
height: 100vh;
}

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

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

@ -0,0 +1,224 @@
import React from 'react';
import { FormComponentProps } from 'antd/lib/form/Form';
import {
Button,
Icon,
Input,
Form,
} from 'antd';
export interface RegisterData {
username: string;
firstName: string;
lastName: string;
email: string;
password1: string;
password2: string;
}
import patterns from '../utils/validation-patterns';
type RegisterFormProps = {
onSubmit(registerData: RegisterData): void;
} & FormComponentProps;
class RegisterForm extends React.PureComponent<RegisterFormProps> {
constructor(props: RegisterFormProps) {
super(props);
}
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!');
} else {
callback();
}
};
private validatePassword(_: any, value: any, callback: any) {
const { form } = this.props;
if (!patterns.validatePasswordLength.pattern.test(value)) {
callback(patterns.validatePasswordLength.message);
}
if (!patterns.passwordContainsNumericCharacters.pattern.test(value)) {
callback(patterns.passwordContainsNumericCharacters.message);
}
if (!patterns.passwordContainsUpperCaseCharacter.pattern.test(value)) {
callback(patterns.passwordContainsUpperCaseCharacter.message);
}
if (!patterns.passwordContainsLowerCaseCharacter.pattern.test(value)) {
callback(patterns.passwordContainsLowerCaseCharacter.message);
}
if (value) {
form.validateFields(['password2'], { force: true });
}
callback();
};
private validateUsername(_: any, value: any, callback: any) {
if (!patterns.validateUsernameLength.pattern.test(value)) {
callback(patterns.validateUsernameLength.message);
}
if (!patterns.validateUsernameCharacters.pattern.test(value)) {
callback(patterns.validateUsernameCharacters.message);
}
callback();
};
private handleSubmit(e: React.FormEvent) {
e.preventDefault();
this.props.form.validateFields((error, values) => {
if (!error) {
this.props.onSubmit(values);
}
});
}
private renderFirstNameField() {
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('firstName', {
rules: [{
required: true,
message: 'Please specify a first name',
pattern: patterns.validateName.pattern,
}],
})(
<Input
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)'}} />}
placeholder='First name'
/>
)}
</Form.Item>
)
}
private renderLastNameField() {
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('lastName', {
rules: [{
required: true,
message: 'Please specify a last name',
pattern: patterns.validateName.pattern,
}],
})(
<Input
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)'}} />}
placeholder='Last name'
/>
)}
</Form.Item>
)
}
private renderUsernameField() {
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('username', {
rules: [{
required: true,
message: 'Please specify a username',
}, {
validator: this.validateUsername,
}],
})(
<Input
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)'}} />}
placeholder='Username'
/>
)}
</Form.Item>
)
}
private renderEmailField() {
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('email', {
rules: [{
type: 'email',
message: 'The input is not valid E-mail!',
}, {
required: true,
message: 'Please specify an email address',
}],
})(
<Input
autoComplete='email'
prefix={<Icon type='mail' style={{ color: 'rgba(0,0,0,.25)'}} />}
placeholder='Email address'
/>
)}
</Form.Item>
)
}
private renderPasswordField() {
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('password1', {
rules: [{
required: true,
message: 'Please input your password!',
}, {
validator: this.validatePassword.bind(this),
}],
})(<Input.Password
autoComplete='new-password'
prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)'}} />}
placeholder='Password'
/>)}
</Form.Item>
)
}
private renderPasswordConfirmationField() {
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('password2', {
rules: [{
required: true,
message: 'Please confirm your password!',
}, {
validator: this.validateConfirmation.bind(this),
}],
})(<Input.Password
autoComplete='new-password'
prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)'}} />}
placeholder='Confirm password'
/>)}
</Form.Item>
)
}
public render() {
return (
<Form onSubmit={this.handleSubmit.bind(this)} className='login-form'>
{this.renderFirstNameField()}
{this.renderLastNameField()}
{this.renderUsernameField()}
{this.renderEmailField()}
{this.renderPasswordField()}
{this.renderPasswordConfirmationField()}
<Form.Item>
<Button type='primary' htmlType='submit' className='register-form-button'>
Submit
</Button>
</Form.Item>
</Form>
);
}
}
export default Form.create<RegisterFormProps>()(RegisterForm);

@ -1,10 +0,0 @@
.register-form {
display: flex;
flex-direction: column;
justify-content: center;
height: 100vh;
&__title {
}
}

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

@ -1,211 +0,0 @@
import React, { PureComponent } from 'react';
import { Link } from 'react-router-dom';
import { connect } from 'react-redux';
import { registerAsync, isAuthenticatedAsync } from '../../actions/auth.actions';
import { Button, Icon, Input, Form, Col, Row, Spin } from 'antd';
import Title from 'antd/lib/typography/Title';
import './register-page.scss';
class RegisterForm extends PureComponent<any, any> {
constructor(props: any) {
super(props);
this.state = { confirmDirty: false, loading: false };
}
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 (
<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>
);
}
private handleConfirmBlur = (event: any) => {
const { value } = event.target;
this.setState({ confirmDirty: this.state.confirmDirty || !!value });
};
private compareToFirstPassword = (rule: any, value: string, callback: Function) => {
const { form } = this.props;
if (value && value !== form.getFieldValue('password')) {
callback('Two passwords that you enter are inconsistent!');
} else {
callback();
}
};
private validateToNextPassword = (rule: any, value: string, callback: Function) => {
const { form } = this.props;
if (value && this.state.confirmDirty) {
form.validateFields(['passwordConfirmation'], { force: true });
}
callback();
};
private onSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
this.props.form.validateFields((error: any, values: any) => {
if (!error) {
this.props.dispatch(
registerAsync(
values.username,
values.firstName,
values.lastName,
values.email,
values.password,
values.passwordConfirmation,
this.props.history,
),
);
}
});
}
}
const mapStateToProps = (state: any) => {
return state.authContext;
};
export default Form.create()(connect(mapStateToProps)(RegisterForm));

@ -0,0 +1,39 @@
import React from 'react';
import { Link } from 'react-router-dom';
import Text from 'antd/lib/typography/Text';
import {
Col,
Row,
Icon,
} from 'antd';
export default function EmptyList() {
const emptyTasksIcon = () => (<img src='/assets/empty-tasks-icon.svg'/>);
return (
<div className='cvat-empty-task-list'>
<Row type='flex' justify='center' align='middle'>
<Col>
<Icon className='cvat-empty-tasks-icon' component={emptyTasksIcon}/>
</Col>
</Row>
<Row type='flex' justify='center' align='middle'>
<Col>
<Text strong> No tasks created yet ... </Text>
</Col>
</Row>
<Row type='flex' justify='center' align='middle'>
<Col>
<Text type='secondary'> To get started with your annotation project </Text>
</Col>
</Row>
<Row type='flex' justify='center' align='middle'>
<Col>
<Link to='/tasks/create'> create new task </Link>
</Col>
</Row>
</div>
)
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save