提交 695fc379 编写于 作者: B Boris Sekachev 提交者: Nikita Manovich

User interface with react and antd (#755)

* Login page, router
* Registration
* Tasks view
上级 7e214f85
......@@ -14,6 +14,10 @@
"directory": "./cvat-canvas",
"changeProcessCWD": true
},
{
"directory": "./cvat-ui",
"changeProcessCWD": true
},
{
"directory": ".",
"changeProcessCWD": true
......
此差异已折叠。
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,
},
......
/*
* 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'],
},
},
},
};
# 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/
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 can’t go back!**
If you aren’t 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 you’re on your own.
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t 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/).
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' },
}),
);
<?xml version="1.0" encoding="UTF-8"?>
<svg width="90px" height="78px" viewBox="0 0 90 78" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: sketchtool 58 (101010) - https://sketch.com -->
<title>8EAC7454-72F0-4344-ACCC-8688B016EA51</title>
<desc>Created with sketchtool.</desc>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="cvat-app---tasks-empty-NEW" transform="translate(-675.000000, -200.000000)" fill="#9B9B9B">
<path d="M759.270349,200 C762.023479,200 764.276863,202.167234 764.390611,204.873941 L764.395116,205.08872 L764.395116,272.23736 C764.395116,274.971333 762.212326,277.208669 759.486633,277.321607 L759.270349,277.32608 L680.122674,277.32608 C677.371583,277.32608 675.118252,275.158846 675.004505,272.452139 L675,272.23736 L675,205.08872 C675,202.354747 677.182791,200.117411 679.906547,200.004473 L680.122674,200 L759.270349,200 Z M760.850581,216.24168 L678.544535,216.24168 L678.544535,272.23736 C678.544535,273.052604 679.177422,273.72499 679.979474,273.797183 L680.122674,273.8036 L759.270349,273.8036 C760.093663,273.8036 760.771357,273.176506 760.844115,272.379651 L760.850581,272.23736 L760.850581,216.24168 Z M687.656578,238.480495 L691.984557,238.480495 C692.574881,238.480495 693.060645,238.931945 693.12279,239.511284 L693.129507,239.637268 L693.129507,240.666335 L708.149563,240.666335 L708.149563,239.636773 C708.149563,239.040814 708.596399,238.549634 709.170215,238.486792 L709.295003,238.48 L713.622982,238.48 C714.213306,238.48 714.698644,238.93145 714.760732,239.510789 L714.767442,239.636773 L714.767442,244.008948 C714.767442,244.604906 714.32146,245.096086 713.747757,245.158928 L713.622982,245.16572 L712.603942,245.16572 L712.603942,255.71428 L713.622002,255.71428 C714.212327,255.71428 714.698517,256.165298 714.760719,256.744982 L714.767442,256.871052 L714.767442,261.243227 C714.767442,261.839186 714.320606,262.330366 713.74679,262.393208 L713.622002,262.4 L709.294513,262.4 C708.704189,262.4 708.218425,261.948981 708.15628,261.369297 L708.149563,261.243227 L708.149563,260.213665 L693.129997,260.213665 L693.129997,261.243227 C693.129997,261.839186 692.683587,262.330366 692.109828,262.393208 L691.985047,262.4 L687.657068,262.4 C687.066743,262.4 686.580979,261.948981 686.518834,261.369297 L686.512118,261.243227 L686.512118,256.871052 C686.512118,256.275094 686.958527,255.783914 687.532286,255.721072 L687.657068,255.71428 L688.675617,255.71428 L688.675617,245.166215 L687.656578,245.166215 C687.066253,245.166215 686.580489,244.714766 686.518344,244.135426 L686.511628,244.009443 L686.511628,239.637268 C686.511628,239.041309 686.958037,238.550129 687.531796,238.487287 L687.656578,238.480495 L691.984557,238.480495 Z M690.839607,258.027825 L688.802018,258.027825 L688.802018,260.086455 L690.839607,260.086455 L690.839607,258.027825 Z M712.477542,258.027825 L710.439463,258.027825 L710.439463,260.086455 L712.477542,260.086455 L712.477542,258.027825 Z M708.149563,242.97988 L693.129507,242.97988 L693.129507,244.009443 C693.129507,244.605401 692.683098,245.096581 692.109338,245.159423 L691.984557,245.166215 L690.966007,245.166215 L690.966007,255.71428 L691.985047,255.71428 C692.575371,255.71428 693.061135,256.165298 693.12328,256.744982 L693.129997,256.871052 L693.129997,257.899625 L708.149563,257.899625 L708.149563,256.871052 C708.149563,256.275094 708.595972,255.783914 709.169732,255.721072 L709.294513,255.71428 L710.313553,255.71428 L710.313553,245.16572 L709.295003,245.16572 C708.704679,245.16572 708.218488,244.714271 708.156286,244.134931 L708.149563,244.008948 L708.149563,242.97988 Z M729.724988,235.360323 C730.123223,235.360323 730.445882,235.698309 730.445882,236.11474 L730.445882,236.11474 L730.445882,236.785871 L739.902955,236.785871 L739.902955,236.114417 C739.902955,235.697987 740.225923,235.36 740.624158,235.36 L740.624158,235.36 L743.349182,235.36 C743.747416,235.36 744.069767,235.697987 744.069767,236.114417 L744.069767,236.114417 L744.069767,238.965835 C744.069767,239.382266 743.747416,239.720252 743.349182,239.720252 L743.349182,239.720252 L742.707564,239.720252 L742.707564,246.599748 L743.348565,246.599748 C743.746799,246.599748 744.069767,246.937411 744.069767,247.354165 L744.069767,247.354165 L744.069767,250.205583 C744.069767,250.622013 743.746799,250.96 743.348565,250.96 L743.348565,250.96 L740.623849,250.96 C740.225615,250.96 739.902955,250.622336 739.902955,250.205583 L739.902955,250.205583 L739.902955,249.534129 L730.446191,249.534129 L730.446191,250.205583 C730.446191,250.622013 730.123531,250.96 729.725296,250.96 L729.725296,250.96 L727.000273,250.96 C726.602038,250.96 726.279378,250.622336 726.279378,250.205583 L726.279378,250.205583 L726.279378,247.354165 C726.279378,246.937734 726.602038,246.599748 727.000273,246.599748 L727.000273,246.599748 L727.641582,246.599748 L727.641582,239.720575 L726.999964,239.720575 C726.601729,239.720575 726.27907,239.382589 726.27907,238.966158 L726.27907,238.966158 L726.27907,236.11474 C726.27907,235.698309 726.601729,235.360323 726.999964,235.360323 L726.999964,235.360323 Z M729.004094,248.108581 L727.721167,248.108581 L727.721167,249.451166 L729.004094,249.451166 L729.004094,248.108581 Z M742.627979,248.108581 L741.344744,248.108581 L741.344744,249.451166 L742.627979,249.451166 L742.627979,249.451166 L742.627979,248.108581 Z M739.902955,238.294705 L730.445882,238.294705 L730.445882,238.966158 C730.445882,239.382589 730.123223,239.720575 729.724988,239.720575 L729.724988,239.720575 L729.083679,239.720575 L729.083679,246.599748 L729.725296,246.599748 C730.123531,246.599748 730.446191,246.937411 730.446191,247.354165 L730.446191,247.354165 L730.446191,248.024973 L739.902955,248.024973 L739.902955,247.354165 C739.902955,246.937734 740.225615,246.599748 740.623849,246.599748 L740.623849,246.599748 L741.265467,246.599748 L741.265467,246.599748 L741.265467,239.720252 L740.624158,239.720252 C740.225923,239.720252 739.902955,239.382266 739.902955,238.965835 L739.902955,238.965835 L739.902955,238.294705 Z M690.839607,240.794535 L688.801528,240.794535 L688.801528,242.85267 L690.839607,242.85267 L690.839607,240.794535 Z M712.477542,240.793545 L710.439953,240.793545 L710.439953,242.85168 L712.477542,242.85168 L712.477542,240.793545 Z M742.627979,236.868834 L741.345052,236.868834 L741.345052,236.868834 L741.345052,238.211096 L742.627979,238.211096 L742.627979,236.868834 Z M729.004094,236.86948 L727.720858,236.86948 L727.720858,238.211741 L729.004094,238.211741 L729.004094,236.86948 Z M684.098372,219.76 C685.027936,219.76 685.790328,220.471459 685.864243,221.376316 L685.870116,221.52072 L685.870116,221.63928 C685.870116,222.61168 685.07686,223.4 684.098372,223.4 C683.167814,223.4 682.405373,222.688541 682.331455,221.783684 L682.325581,221.63928 L682.325581,221.52072 C682.325581,220.54832 683.118837,219.76 684.098372,219.76 Z M705.747558,219.76 C706.726047,219.76 707.519302,220.54832 707.519302,221.52072 C707.519302,222.49416 706.726047,223.28248 705.747558,223.28248 L691.422907,223.28248 C690.443372,223.28248 689.651163,222.49416 689.651163,221.52072 C689.651163,220.54832 690.443372,219.76 691.422907,219.76 L705.747558,219.76 Z M719.736279,219.76 C720.715814,219.76 721.508023,220.54832 721.508023,221.52072 C721.508023,222.49416 720.715814,223.28248 719.736279,223.28248 L714.446163,223.28248 C713.469767,223.28248 712.674419,222.49416 712.674419,221.52072 C712.674419,220.54832 713.469767,219.76 714.446163,219.76 L719.736279,219.76 Z M729.603837,219.76 C730.581279,219.76 731.376628,220.54832 731.376628,221.52072 C731.376628,222.49416 730.581279,223.28248 729.603837,223.28248 L728.050814,223.28248 C727.072326,223.28248 726.27907,222.49416 726.27907,221.52072 C726.27907,220.54832 727.072326,219.76 728.050814,219.76 L729.603837,219.76 Z M746.711163,219.76 C747.687558,219.76 748.482907,220.54832 748.482907,221.52072 C748.482907,222.49416 747.687558,223.28248 746.711163,223.28248 L738.516977,223.28248 C737.537442,223.28248 736.744186,222.49416 736.744186,221.52072 C736.744186,220.54832 737.537442,219.76 738.516977,219.76 L746.711163,219.76 Z M759.270349,203.52248 L680.122674,203.52248 C679.302326,203.52248 678.623863,204.149574 678.55101,204.946429 L678.544535,205.08872 L678.544535,212.72024 L760.850581,212.72024 L760.850581,205.08872 C760.850581,204.22552 760.142093,203.52248 759.270349,203.52248 Z M684.125581,206.24 C685.105116,206.24 685.898372,207.02728 685.898372,208.00072 C685.898372,208.97312 685.105116,209.76144 684.125581,209.76144 C683.149186,209.76144 682.325581,208.97312 682.325581,208.00072 C682.325581,207.02728 683.087442,206.24 684.066977,206.24 L684.125581,206.24 Z M691.480465,206.24 C692.458953,206.24 693.252209,207.02728 693.252209,208.00072 C693.252209,208.97312 692.458953,209.76144 691.480465,209.76144 C690.503023,209.76144 689.651163,208.97312 689.651163,208.00072 C689.651163,207.02728 690.384767,206.24 691.363256,206.24 L691.480465,206.24 Z M698.776744,206.24 C699.754186,206.24 700.549535,207.02728 700.549535,208.00072 C700.549535,208.97312 699.754186,209.76144 698.776744,209.76144 C697.798256,209.76144 696.976744,208.97312 696.976744,208.00072 C696.976744,207.02728 697.740698,206.24 698.720233,206.24 L698.776744,206.24 Z" id="empty-tasks-icon"></path>
</g>
</g>
</svg>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
<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>
\ No newline at end of file
此差异已折叠。
{
"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"
}
}
此差异已折叠。
此差异已折叠。
因为 它太大了无法显示 source diff 。你可以改为 查看blob
此差异由.gitattributes 抑制。
<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>
\ No newline at end of file
<!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>
{
"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"
}
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;
},
);
};
}
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));
}
};
}
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;
},
);
};
}
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;
},
);
};
}
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));
};
}
import { Dispatch } from 'redux';
export const filterTasks = (queryParams: { search?: string, page?: number }) => (dispatch: Dispatch) => {
dispatch({
type: 'FILTER_TASKS',
payload: queryParams,
});
}
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;
},
);
};
}
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;
},
);
};
}
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);
});
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);
.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;
}
}
}
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);
});
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);
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);
.login-form {
display: flex;
flex-direction: column;
justify-content: center;
height: 100vh;
&__title {
}
}
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);
});
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));
.ant-badge {
width: 100%;
}
.ant-tree.ant-tree-directory {
height: 108px;
overflow: auto;
}
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);
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;
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);
});
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;
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);
\ No newline at end of file
.register-form {
display: flex;
flex-direction: column;
justify-content: center;
height: 100vh;
&__title {
}
}
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);
});
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));
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>
)
}
\ No newline at end of file
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册