UI eslint fixes (#908)

* Installed airbnb fullsettings
* Fixed actions menu
* Create model/task page
* File manager, header
* Labels editor
* Login, register
* Models page & model runner
* Tasks page
* Feedback and base app
* Tasks page
* Containers
* Reducers
* Fixed additional issues
* Small pagination fix
main
Boris Sekachev 6 years ago committed by Nikita Manovich
parent c0f1854f79
commit 32027ce884

@ -3,7 +3,8 @@
"eslint.enable": true,
"eslint.validate": [
"javascript",
"typescript"
"typescript",
"typescriptreact",
],
"eslint.workingDirectories": [
{

@ -19,16 +19,23 @@ module.exports = {
],
'extends': [
'plugin:@typescript-eslint/recommended',
'airbnb-typescript/base',
'airbnb-typescript',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
],
'rules': {
'@typescript-eslint/indent': ['warn', 4],
'react/jsx-indent': ['warn', 4],
'react/jsx-indent-props': ['warn', 4],
'react/jsx-props-no-spreading': 0,
'jsx-quotes': ['error', 'prefer-single'],
'arrow-parens': ['error', 'always'],
'@typescript-eslint/no-explicit-any': [0],
'no-restricted-syntax': [0, {'selector': 'ForOfStatement'}],
'no-plusplus': [0],
'lines-between-class-members': 0,
'react/no-did-update-set-state': 0, // https://github.com/airbnb/javascript/issues/1875
},
'settings': {
'import/resolver': {

@ -899,6 +899,24 @@
}
}
},
"@babel/runtime-corejs3": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.7.4.tgz",
"integrity": "sha512-BBIEhzk8McXDcB3IbOi8zQPzzINUp4zcLesVlBSOcyGhzPUU8Xezk5GAG7Sy5GVhGmAO0zGd2qRSeY2g4Obqxw==",
"dev": true,
"requires": {
"core-js-pure": "^3.0.0",
"regenerator-runtime": "^0.13.2"
},
"dependencies": {
"regenerator-runtime": {
"version": "0.13.3",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==",
"dev": true
}
}
},
"@babel/template": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz",
@ -1059,27 +1077,39 @@
}
},
"@typescript-eslint/eslint-plugin": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.13.0.tgz",
"integrity": "sha512-WQHCozMnuNADiqMtsNzp96FNox5sOVpU8Xt4meaT4em8lOG1SrOv92/mUbEHQVh90sldKSfcOc/I0FOb/14G1g==",
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.10.0.tgz",
"integrity": "sha512-rT51fNLW0u3fnDGnAHVC5nu+Das+y2CpW10yqvf6/j5xbuUV3FxA3mBaIbM24CXODXjbgUznNb4Kg9XZOUxKAw==",
"dev": true,
"requires": {
"@typescript-eslint/experimental-utils": "1.13.0",
"eslint-utils": "^1.3.1",
"@typescript-eslint/experimental-utils": "2.10.0",
"eslint-utils": "^1.4.3",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^2.0.1",
"tsutils": "^3.7.0"
"regexpp": "^3.0.0",
"tsutils": "^3.17.1"
}
},
"@typescript-eslint/experimental-utils": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz",
"integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==",
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.10.0.tgz",
"integrity": "sha512-FZhWq6hWWZBP76aZ7bkrfzTMP31CCefVIImrwP3giPLcoXocmLTmr92NLZxuIcTL4GTEOE33jQMWy9PwelL+yQ==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/typescript-estree": "1.13.0",
"eslint-scope": "^4.0.0"
"@typescript-eslint/typescript-estree": "2.10.0",
"eslint-scope": "^5.0.0"
},
"dependencies": {
"eslint-scope": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
"integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
"dev": true,
"requires": {
"esrecurse": "^4.1.0",
"estraverse": "^4.1.1"
}
}
}
},
"@typescript-eslint/parser": {
@ -1092,22 +1122,70 @@
"@typescript-eslint/experimental-utils": "1.13.0",
"@typescript-eslint/typescript-estree": "1.13.0",
"eslint-visitor-keys": "^1.0.0"
},
"dependencies": {
"@typescript-eslint/experimental-utils": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz",
"integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/typescript-estree": "1.13.0",
"eslint-scope": "^4.0.0"
}
},
"@typescript-eslint/typescript-estree": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz",
"integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==",
"dev": true,
"requires": {
"lodash.unescape": "4.0.1",
"semver": "5.5.0"
}
},
"semver": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
"dev": true
}
}
},
"@typescript-eslint/typescript-estree": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz",
"integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==",
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.10.0.tgz",
"integrity": "sha512-oOYnplddQNm/LGVkqbkAwx4TIBuuZ36cAQq9v3nFIU9FmhemHuVzAesMSXNQDdAzCa5bFgCrfD3JWhYVKlRN2g==",
"dev": true,
"requires": {
"debug": "^4.1.1",
"eslint-visitor-keys": "^1.1.0",
"glob": "^7.1.6",
"is-glob": "^4.0.1",
"lodash.unescape": "4.0.1",
"semver": "5.5.0"
"semver": "^6.3.0",
"tsutils": "^3.17.1"
},
"dependencies": {
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"semver": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
@ -1634,12 +1712,30 @@
"dev": true
},
"axobject-query": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz",
"integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.1.1.tgz",
"integrity": "sha512-lF98xa/yvy6j3fBHAgQXIYl+J4eZadOSqsPojemUqClzNbBV38wWGpUbQbVEyf4eUF5yF7eHmGgGA2JiHyjeqw==",
"dev": true,
"requires": {
"ast-types-flow": "0.0.7"
"@babel/runtime": "^7.7.4",
"@babel/runtime-corejs3": "^7.7.4"
},
"dependencies": {
"@babel/runtime": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.4.tgz",
"integrity": "sha512-r24eVUUr0QqNZa+qrImUk8fn5SPhHq+IfYvIoIMg0do3GdK9sMdiLKP3GYVVaxpPKORgm8KRKaNTEhAjgIpLMw==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.2"
}
},
"regenerator-runtime": {
"version": "0.13.3",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==",
"dev": true
}
}
},
"babel": {
@ -2580,6 +2676,12 @@
}
}
},
"core-js-pure": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.4.7.tgz",
"integrity": "sha512-Am3uRS8WCdTFA3lP7LtKR0PxgqYzjAMGKXaZKSNSC/8sqU0Wfq8R/YzoRs2rqtOVEunfgH+0q3O0BKOg0AvjPw==",
"dev": true
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
@ -3484,6 +3586,12 @@
}
}
},
"eslint-plugin-eslint-plugin": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-2.1.0.tgz",
"integrity": "sha512-kT3A/ZJftt28gbl/Cv04qezb/NQ1dwYIbi8lyf806XMxkus7DvOVCLIfTXMrorp322Pnoez7+zabXH29tADIDg==",
"dev": true
},
"eslint-plugin-import": {
"version": "2.18.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz",
@ -3538,20 +3646,21 @@
}
},
"eslint-plugin-react": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.16.0.tgz",
"integrity": "sha512-GacBAATewhhptbK3/vTP09CbFrgUJmBSaaRcWdbQLFvUZy9yVcQxigBNHGPU/KE2AyHpzj3AWXpxoMTsIDiHug==",
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.17.0.tgz",
"integrity": "sha512-ODB7yg6lxhBVMeiH1c7E95FLD4E/TwmFjltiU+ethv7KPdCwgiFuOZg9zNRHyufStTDLl/dEFqI2Q1VPmCd78A==",
"dev": true,
"requires": {
"array-includes": "^3.0.3",
"doctrine": "^2.1.0",
"eslint-plugin-eslint-plugin": "^2.1.0",
"has": "^1.0.3",
"jsx-ast-utils": "^2.2.1",
"jsx-ast-utils": "^2.2.3",
"object.entries": "^1.1.0",
"object.fromentries": "^2.0.0",
"object.fromentries": "^2.0.1",
"object.values": "^1.1.0",
"prop-types": "^15.7.2",
"resolve": "^1.12.0"
"resolve": "^1.13.1"
},
"dependencies": {
"doctrine": {
@ -3562,9 +3671,24 @@
"requires": {
"esutils": "^2.0.2"
}
},
"resolve": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.13.1.tgz",
"integrity": "sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
}
}
},
"eslint-plugin-react-hooks": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz",
"integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==",
"dev": true
},
"eslint-scope": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
@ -8141,9 +8265,9 @@
}
},
"regexpp": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
"integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz",
"integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==",
"dev": true
},
"regexpu-core": {
@ -9444,9 +9568,9 @@
"dev": true
},
"typescript": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz",
"integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==",
"version": "3.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz",
"integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==",
"dev": true
},
"ua-parser-js": {

@ -14,7 +14,7 @@
"@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",
"@typescript-eslint/eslint-plugin": "^2.10.0",
"babel": "^6.23.0",
"babel-loader": "^8.0.6",
"babel-plugin-import": "^1.12.2",
@ -22,11 +22,12 @@
"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",
"eslint-plugin-react": "^7.17.0",
"eslint-plugin-react-hooks": "^1.7.0",
"html-webpack-plugin": "^3.2.0",
"nodemon": "^1.19.2",
"style-loader": "^1.0.0",
"typescript": "^3.6.3",
"typescript": "^3.7.3",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.8",
"webpack-dev-server": "^3.8.0"

@ -431,7 +431,7 @@ async function timeoutCallback(
dispatch(getInferenceStatusSuccess(taskID, activeInference));
} catch (error) {
dispatch(getInferenceStatusFailed(taskID, new Error(
`Server request for the task ${taskID} was failed`
`Server request for the task ${taskID} was failed`,
)));
}
}

@ -36,14 +36,16 @@ interface MinActionsMenuProps {
onOpenRunWindow: (taskInstance: any) => void;
}
export function handleMenuClick(props: MinActionsMenuProps, params: ClickParam) {
export function handleMenuClick(props: MinActionsMenuProps, params: ClickParam): void {
const { taskInstance } = props;
const tracker = taskInstance.bugTracker;
if (params.keyPath.length !== 2) {
switch (params.key) {
case 'tracker': {
window.open(`${tracker}`, '_blank')
// false positive eslint(security/detect-non-literal-fs-filename)
// eslint-disable-next-line
window.open(`${tracker}`, '_blank');
return;
} case 'auto_annotation': {
props.onOpenRunWindow(taskInstance);
@ -57,36 +59,51 @@ export function handleMenuClick(props: MinActionsMenuProps, params: ClickParam)
props.onDeleteTask(taskInstance);
},
});
return;
break;
} default: {
return;
// do nothing
}
}
}
}
export default function ActionsMenuComponent(props: ActionsMenuComponentProps) {
const tracker = props.taskInstance.bugTracker;
const renderModelRunner = props.installedAutoAnnotation ||
props.installedTFAnnotation || props.installedTFSegmentation;
export default function ActionsMenuComponent(props: ActionsMenuComponentProps): JSX.Element {
const {
taskInstance,
installedAutoAnnotation,
installedTFAnnotation,
installedTFSegmentation,
dumpers,
loaders,
exporters,
inferenceIsActive,
} = props;
const tracker = taskInstance.bugTracker;
const renderModelRunner = installedAutoAnnotation
|| installedTFAnnotation || installedTFSegmentation;
return (
<Menu selectable={false} className='cvat-actions-menu' onClick={
(params: ClickParam) => handleMenuClick(props, params)
}>
<Menu
selectable={false}
className='cvat-actions-menu'
onClick={
(params: ClickParam): void => handleMenuClick(props, params)
}
>
<Menu.SubMenu key='dump' title='Dump annotations'>
{
props.dumpers.map((dumper) => DumperItemComponent({
dumpers.map((dumper): JSX.Element => DumperItemComponent({
dumper,
taskInstance: props.taskInstance,
dumpActivity: (props.dumpActivities || [])
.filter((_dumper: string) => _dumper === dumper.name)[0] || null,
onDumpAnnotation: props.onDumpAnnotation,
} ))}
}))
}
</Menu.SubMenu>
<Menu.SubMenu key='load' title='Upload annotations'>
{
props.loaders.map((loader) => LoaderItemComponent({
loaders.map((loader): JSX.Element => LoaderItemComponent({
loader,
taskInstance: props.taskInstance,
loadActivity: props.loadActivity,
@ -96,7 +113,7 @@ export default function ActionsMenuComponent(props: ActionsMenuComponentProps) {
</Menu.SubMenu>
<Menu.SubMenu key='export' title='Export as a dataset'>
{
props.exporters.map((exporter) => ExportItemComponent({
exporters.map((exporter): JSX.Element => ExportItemComponent({
exporter,
taskInstance: props.taskInstance,
exportActivity: (props.exportActivities || [])
@ -107,10 +124,17 @@ export default function ActionsMenuComponent(props: ActionsMenuComponentProps) {
</Menu.SubMenu>
{tracker && <Menu.Item key='tracker'>Open bug tracker</Menu.Item>}
{
renderModelRunner &&
<Menu.Item disabled={props.inferenceIsActive} key='auto_annotation'>Automatic annotation</Menu.Item>
renderModelRunner
&& (
<Menu.Item
disabled={inferenceIsActive}
key='auto_annotation'
>
Automatic annotation
</Menu.Item>
)
}
<hr/>
<hr />
<Menu.Item key='delete'>Delete</Menu.Item>
</Menu>
);

@ -20,25 +20,31 @@ function isDefaultFormat(dumperName: string, taskMode: string): boolean {
|| (dumperName === 'CVAT XML 1.1 for images' && taskMode === 'annotation');
}
export default function DumperItemComponent(props: DumperItemComponentProps) {
const task = props.taskInstance;
const { mode } = task;
export default function DumperItemComponent(props: DumperItemComponentProps): JSX.Element {
const {
taskInstance,
dumpActivity,
} = props;
const { mode } = taskInstance;
const { dumper } = props;
const pending = !!props.dumpActivity;
const pending = !!dumpActivity;
return (
<Menu.Item className='cvat-actions-menu-dump-submenu-item' key={dumper.name}>
<Button block={true} type='link' disabled={pending}
onClick={() => {
props.onDumpAnnotation(task, dumper);
}}>
<Icon type='download'/>
<Button
block
type='link'
disabled={pending}
onClick={(): void => {
props.onDumpAnnotation(taskInstance, dumper);
}}
>
<Icon type='download' />
<Text strong={isDefaultFormat(dumper.name, mode)}>
{dumper.name}
</Text>
{pending && <Icon type='loading'/>}
{pending && <Icon type='loading' />}
</Button>
</Menu.Item>
);
}

@ -15,24 +15,31 @@ interface DumperItemComponentProps {
onExportDataset: (task: any, exporter: any) => void;
}
export default function DumperItemComponent(props: DumperItemComponentProps) {
const task = props.taskInstance;
const { exporter } = props;
const pending = !!props.exportActivity;
export default function DumperItemComponent(props: DumperItemComponentProps): JSX.Element {
const {
taskInstance,
exporter,
exportActivity,
} = props;
const pending = !!exportActivity;
return (
<Menu.Item className='cvat-actions-menu-export-submenu-item' key={exporter.name}>
<Button block={true} type='link' disabled={pending}
onClick={() => {
props.onExportDataset(task, exporter);
}}>
<Icon type='export'/>
<Text strong={props.exporter.is_default}>
<Button
block
type='link'
disabled={pending}
onClick={(): void => {
props.onExportDataset(taskInstance, exporter);
}}
>
<Icon type='export' />
<Text strong={exporter.is_default}>
{exporter.name}
</Text>
{pending && <Icon type='loading'/>}
{pending && <Icon type='loading' />}
</Button>
</Menu.Item>
);
}

@ -17,12 +17,15 @@ interface LoaderItemComponentProps {
onLoadAnnotation: (taskInstance: any, loader: any, file: File) => void;
}
export default function LoaderItemComponent(props: LoaderItemComponentProps) {
const { loader } = props;
export default function LoaderItemComponent(props: LoaderItemComponentProps): JSX.Element {
const {
loader,
loadActivity,
} = props;
const loadingWithThisLoader = props.loadActivity
&& props.loadActivity === loader.name
? props.loadActivity : null;
const loadingWithThisLoader = loadActivity
&& loadActivity === loader.name
? loadActivity : null;
const pending = !!loadingWithThisLoader;
@ -31,8 +34,8 @@ export default function LoaderItemComponent(props: LoaderItemComponentProps) {
<Upload
accept={`.${loader.format}`}
multiple={false}
showUploadList={ false }
beforeUpload={(file: RcFile) => {
showUploadList={false}
beforeUpload={(file: RcFile): boolean => {
props.onLoadAnnotation(
props.taskInstance,
loader,
@ -40,13 +43,14 @@ export default function LoaderItemComponent(props: LoaderItemComponentProps) {
);
return false;
}}>
<Button block={true} type='link' disabled={!!props.loadActivity}>
<Icon type='upload'/>
}}
>
<Button block type='link' disabled={!!loadActivity}>
<Icon type='upload' />
<Text>{loader.name}</Text>
{pending && <Icon type='loading'/>}
{pending && <Icon type='loading' />}
</Button>
</Upload>
</Menu.Item>
);
}
}

@ -14,10 +14,10 @@ import {
import Text from 'antd/lib/typography/Text';
import CreateModelForm, {
CreateModelForm as WrappedCreateModelForm
CreateModelForm as WrappedCreateModelForm,
} from './create-model-form';
import ConnectedFileManager, {
FileManagerContainer
FileManagerContainer,
} from '../../containers/file-manager/file-manager';
import { ModelFiles } from '../../reducers/interfaces';
@ -30,19 +30,32 @@ interface Props {
export default class CreateModelContent extends React.PureComponent<Props> {
private modelForm: WrappedCreateModelForm;
private fileManagerContainer: FileManagerContainer;
public constructor(props: Props) {
super(props);
this.modelForm = null as any as WrappedCreateModelForm;
this.fileManagerContainer = null as any as FileManagerContainer;
}
private handleSubmitClick = () => {
public componentDidUpdate(prevProps: Props): void {
const { modelCreatingStatus } = this.props;
if (prevProps.modelCreatingStatus !== 'CREATED'
&& modelCreatingStatus === 'CREATED') {
message.success('The model has been uploaded');
this.modelForm.resetFields();
this.fileManagerContainer.reset();
}
}
private handleSubmitClick = (): void => {
const { createModel } = this.props;
this.modelForm.submit()
.then((data) => {
const {
local,
share,
} = this.fileManagerContainer.getFiles();
} = this.fileManagerContainer.getFiles();
const files = local.length ? local : share;
const grouppedFiles: ModelFiles = {
@ -65,50 +78,51 @@ export default class CreateModelContent extends React.PureComponent<Props> {
if (Object.keys(grouppedFiles)
.map((key: string) => grouppedFiles[key])
.filter((val) => !!val).length !== 4) {
notification.error({
message: 'Could not upload a model',
description: 'Please, specify correct files',
});
} else {
this.props.createModel(data.name, grouppedFiles, data.global);
}
notification.error({
message: 'Could not upload a model',
description: 'Please, specify correct files',
});
} else {
createModel(data.name, grouppedFiles, data.global);
}
}).catch(() => {
notification.error({
message: 'Could not upload a model',
description: 'Please, check input fields',
});
})
}
});
};
public componentDidUpdate(prevProps: Props) {
if (prevProps.modelCreatingStatus !== 'CREATED'
&& this.props.modelCreatingStatus === 'CREATED') {
message.success('The model has been uploaded');
this.modelForm.resetFields();
this.fileManagerContainer.reset();
}
}
public render() {
const loading = !!this.props.modelCreatingStatus
&& this.props.modelCreatingStatus !== 'CREATED';
const status = this.props.modelCreatingStatus
&& this.props.modelCreatingStatus !== 'CREATED' ? this.props.modelCreatingStatus : '';
public render(): JSX.Element {
const {
modelCreatingStatus,
} = this.props;
const loading = !!modelCreatingStatus
&& modelCreatingStatus !== 'CREATED';
const status = modelCreatingStatus
&& modelCreatingStatus !== 'CREATED' ? modelCreatingStatus : '';
const guideLink = 'https://github.com/opencv/cvat/blob/develop/cvat/apps/auto_annotation/README.md';
return (
<Row type='flex' justify='start' align='middle' className='cvat-create-model-content'>
<Col span={24}>
<Tooltip overlay='Click to open guide'>
<Icon onClick={() => {
window.open(guideLink, '_blank')
}} type='question-circle'/>
<Icon
onClick={(): void => {
// false positive
// eslint-disable-next-line
window.open(guideLink, '_blank');
}}
type='question-circle'
/>
</Tooltip>
</Col>
<Col span={24}>
<CreateModelForm
wrappedComponentRef={
(ref: WrappedCreateModelForm) => this.modelForm = ref
(ref: WrappedCreateModelForm): void => {
this.modelForm = ref;
}
}
/>
</Col>
@ -117,13 +131,17 @@ export default class CreateModelContent extends React.PureComponent<Props> {
<Text className='cvat-black-color'>Select files:</Text>
</Col>
<Col span={24}>
<ConnectedFileManager ref={
(container: FileManagerContainer) =>
this.fileManagerContainer = container
} withRemote={true}/>
<ConnectedFileManager
ref={
(container: FileManagerContainer): void => {
this.fileManagerContainer = container;
}
}
withRemote={false}
/>
</Col>
<Col span={18}>
{status && <Alert message={`${status}`}/>}
{status && <Alert message={`${status}`} />}
</Col>
<Col span={6}>
<Button
@ -131,9 +149,11 @@ export default class CreateModelContent extends React.PureComponent<Props> {
disabled={loading}
loading={loading}
onClick={this.handleSubmitClick}
> Submit </Button>
>
Submit
</Button>
</Col>
</Row>
);
}
}
}

@ -15,13 +15,10 @@ import Text from 'antd/lib/typography/Text';
type Props = FormComponentProps;
export class CreateModelForm extends React.PureComponent<Props> {
public constructor(props: Props) {
super(props);
}
public submit(): Promise<{name: string, global: boolean}> {
public submit(): Promise<{name: string; global: boolean}> {
const { form } = this.props;
return new Promise((resolve, reject) => {
this.props.form.validateFields((errors, values) => {
form.validateFields((errors, values): void => {
if (!errors) {
resolve({
name: values.name,
@ -34,15 +31,17 @@ export class CreateModelForm extends React.PureComponent<Props> {
});
}
public resetFields() {
this.props.form.resetFields();
public resetFields(): void {
const { form } = this.props;
form.resetFields();
}
public render() {
const { getFieldDecorator } = this.props.form;
public render(): JSX.Element {
const { form } = this.props;
const { getFieldDecorator } = form;
return (
<Form onSubmit={(e: React.FormEvent) => e.preventDefault()}>
<Form onSubmit={(e: React.FormEvent): void => e.preventDefault()}>
<Row>
<Col span={24}>
<Text type='danger'>* </Text>
@ -55,7 +54,7 @@ export class CreateModelForm extends React.PureComponent<Props> {
required: true,
message: 'Please, specify a model name',
}],
})(<Input placeholder='Model name'/>)}
})(<Input placeholder='Model name' />)}
</Form.Item>
</Col>
<Col span={8} offset={2}>
@ -64,11 +63,13 @@ export class CreateModelForm extends React.PureComponent<Props> {
{ getFieldDecorator('global', {
initialValue: false,
valuePropName: 'checked',
})(<Checkbox>
<Text className='cvat-black-color'>
Load globally
</Text>
</Checkbox>)}
})(
<Checkbox>
<Text className='cvat-black-color'>
Load globally
</Text>
</Checkbox>,
)}
</Tooltip>
</Form.Item>
</Col>

@ -16,17 +16,23 @@ interface Props {
modelCreatingStatus: string;
}
export default function CreateModelPageComponent(props: Props) {
export default function CreateModelPageComponent(props: Props): JSX.Element {
const {
isAdmin,
modelCreatingStatus,
createModel,
} = props;
return (
<Row type='flex' justify='center' align='top' className='cvat-create-model-form-wrapper'>
<Col md={20} lg={16} xl={14} xxl={9}>
<Text className='cvat-title'>Upload a new model</Text>
<CreateModelContent
isAdmin={props.isAdmin}
modelCreatingStatus={props.modelCreatingStatus}
createModel={props.createModel}
isAdmin={isAdmin}
modelCreatingStatus={modelCreatingStatus}
createModel={createModel}
/>
</Col>
</Row>
);
}
}

@ -28,19 +28,24 @@ export interface AdvancedConfiguration {
}
type Props = FormComponentProps & {
onSubmit(values: AdvancedConfiguration): void
onSubmit(values: AdvancedConfiguration): void;
installedGit: boolean;
};
class AdvancedConfigurationForm extends React.PureComponent<Props> {
public submit() {
public submit(): Promise<void> {
return new Promise((resolve, reject) => {
this.props.form.validateFields((error, values) => {
const {
form,
onSubmit,
} = this.props;
form.validateFields((error, values): void => {
if (!error) {
const filteredValues = { ...values };
delete filteredValues.frameStep;
this.props.onSubmit({
onSubmit({
...values,
frameFilter: values.frameStep ? `step=${values.frameStep}` : undefined,
});
@ -49,17 +54,19 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
reject();
}
});
})
});
}
public resetFields() {
this.props.form.resetFields();
public resetFields(): void {
const { form } = this.props;
form.resetFields();
}
private renderZOrder() {
private renderZOrder(): JSX.Element {
const { form } = this.props;
return (
<Form.Item help='Enables order for shapes. Useful for segmentation tasks'>
{this.props.form.getFieldDecorator('zOrder', {
{form.getFieldDecorator('zOrder', {
initialValue: false,
valuePropName: 'checked',
})(
@ -67,21 +74,23 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
<Text className='cvat-black-color'>
Z-order
</Text>
</Checkbox>
</Checkbox>,
)}
</Form.Item>
);
}
private renderImageQuality() {
private renderImageQuality(): JSX.Element {
const { form } = this.props;
return (
<Form.Item label='Image quality'>
<Tooltip overlay='Defines image compression level'>
{this.props.form.getFieldDecorator('imageQuality', {
{form.getFieldDecorator('imageQuality', {
initialValue: 70,
rules: [{
required: true,
message: 'This field is required'
message: 'This field is required',
}],
})(
<Input
@ -89,87 +98,99 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
type='number'
min={5}
max={100}
suffix={<Icon type='percentage'/>}
/>
suffix={<Icon type='percentage' />}
/>,
)}
</Tooltip>
</Form.Item>
);
}
private renderOverlap() {
private renderOverlap(): JSX.Element {
const { form } = this.props;
return (
<Form.Item label='Overlap size'>
<Tooltip overlay='Defines a number of intersected frames between different segments'>
{this.props.form.getFieldDecorator('overlapSize')(
<Input size='large' type='number'/>
{form.getFieldDecorator('overlapSize')(
<Input size='large' type='number' />,
)}
</Tooltip>
</Form.Item>
);
}
private renderSegmentSize() {
private renderSegmentSize(): JSX.Element {
const { form } = this.props;
return (
<Form.Item label='Segment size'>
<Tooltip overlay='Defines a number of frames in a segment'>
{this.props.form.getFieldDecorator('segmentSize')(
<Input size='large' type='number'/>
{form.getFieldDecorator('segmentSize')(
<Input size='large' type='number' />,
)}
</Tooltip>
</Form.Item>
);
}
private renderStartFrame() {
private renderStartFrame(): JSX.Element {
const { form } = this.props;
return (
<Form.Item label='Start frame'>
{this.props.form.getFieldDecorator('startFrame')(
{form.getFieldDecorator('startFrame')(
<Input
size='large'
type='number'
min={0}
step={1}
/>
/>,
)}
</Form.Item>
);
}
private renderStopFrame() {
private renderStopFrame(): JSX.Element {
const { form } = this.props;
return (
<Form.Item label='Stop frame'>
{this.props.form.getFieldDecorator('stopFrame')(
{form.getFieldDecorator('stopFrame')(
<Input
size='large'
type='number'
min={0}
step={1}
/>
/>,
)}
</Form.Item>
);
}
private renderFrameStep() {
private renderFrameStep(): JSX.Element {
const { form } = this.props;
return (
<Form.Item label='Frame step'>
{this.props.form.getFieldDecorator('frameStep')(
{form.getFieldDecorator('frameStep')(
<Input
size='large'
type='number'
min={1}
step={1}
/>
/>,
)}
</Form.Item>
);
}
private renderGitLFSBox() {
private renderGitLFSBox(): JSX.Element {
const { form } = this.props;
return (
<Form.Item help='If annotation files are large, you can use git LFS feature'>
{this.props.form.getFieldDecorator('lfs', {
{form.getFieldDecorator('lfs', {
valuePropName: 'checked',
initialValue: false,
})(
@ -177,22 +198,24 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
<Text className='cvat-black-color'>
Use LFS (Large File Support):
</Text>
</Checkbox>
</Checkbox>,
)}
</Form.Item>
);
}
private renderGitRepositoryURL() {
private renderGitRepositoryURL(): JSX.Element {
const { form } = this.props;
return (
<Form.Item
hasFeedback
label='Dataset repository URL'
extra='Attach a repository to store annotations there'
>
{this.props.form.getFieldDecorator('repository', {
{form.getFieldDecorator('repository', {
rules: [{
validator: (_, value, callback) => {
validator: (_, value, callback): void => {
if (!value) {
callback();
} else {
@ -207,18 +230,19 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
callback();
}
}
}]
},
}],
})(
<Input size='large'
<Input
size='large'
placeholder='e.g. https//github.com/user/repos [annotation/<anno_file_name>.zip]'
/>
/>,
)}
</Form.Item>
);
}
private renderGit() {
private renderGit(): JSX.Element {
return (
<>
<Row>
@ -235,36 +259,42 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
);
}
private renderBugTracker() {
private renderBugTracker(): JSX.Element {
const { form } = this.props;
return (
<Form.Item
hasFeedback
label='Issue tracker'
extra='Attach issue tracker where the task is described'
>
{this.props.form.getFieldDecorator('bugTracker', {
{form.getFieldDecorator('bugTracker', {
rules: [{
validator: (_, value, callback) => {
validator: (_, value, callback): void => {
if (value && !patterns.validateURL.pattern.test(value)) {
callback('Issue tracker must be URL');
} else {
callback();
}
}
}]
},
}],
})(
<Input size='large'/>
<Input size='large' />,
)}
</Form.Item>
)
);
}
public render() {
public render(): JSX.Element {
const { installedGit } = this.props;
return (
<Form>
<Row><Col>
{this.renderZOrder()}
</Col></Row>
<Row>
<Col>
{this.renderZOrder()}
</Col>
</Row>
<Row type='flex' justify='start'>
<Col span={7}>
@ -290,11 +320,11 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
</Col>
</Row>
{ this.props.installedGit ? this.renderGit() : null}
{ installedGit ? this.renderGit() : null}
<Row>
<Col>
{this.renderBugTracker()}
{this.renderBugTracker()}
</Col>
</Row>
</Form>

@ -4,7 +4,6 @@ import {
Input,
} from 'antd';
import Text from 'antd/lib/typography/Text';
import Form, { FormComponentProps } from 'antd/lib/form/Form';
export interface BaseConfiguration {
@ -16,11 +15,16 @@ type Props = FormComponentProps & {
};
class BasicConfigurationForm extends React.PureComponent<Props> {
public submit() {
public submit(): Promise<void> {
return new Promise((resolve, reject) => {
this.props.form.validateFields((error, values) => {
const {
form,
onSubmit,
} = this.props;
form.validateFields((error, values): void => {
if (!error) {
this.props.onSubmit({
onSubmit({
name: values.name,
});
resolve();
@ -28,25 +32,28 @@ class BasicConfigurationForm extends React.PureComponent<Props> {
reject();
}
});
})
});
}
public resetFields() {
this.props.form.resetFields();
public resetFields(): void {
const { form } = this.props;
form.resetFields();
}
public render() {
const { getFieldDecorator } = this.props.form;
public render(): JSX.Element {
const { form } = this.props;
const { getFieldDecorator } = form;
return (
<Form onSubmit={(e: React.FormEvent) => e.preventDefault()}>
<Form onSubmit={(e: React.FormEvent): void => e.preventDefault()}>
<Form.Item hasFeedback label='Name'>
{ getFieldDecorator('name', {
rules: [{
required: true,
message: 'Please, specify a name',
}]
}],
})(
<Input/>
<Input />,
) }
</Form.Item>
</Form>

@ -21,7 +21,7 @@ export interface CreateTaskData {
basic: BaseConfiguration;
advanced: AdvancedConfiguration;
labels: any[];
files: Files,
files: Files;
}
interface Props {
@ -58,11 +58,33 @@ export default class CreateTaskContent extends React.PureComponent<Props, State>
this.state = { ...defaultState };
}
private validateLabels = () => {
return !!this.state.labels.length;
public componentDidUpdate(prevProps: Props): void {
const { status } = this.props;
if (status === 'CREATED' && prevProps.status !== 'CREATED') {
notification.info({
message: 'The task has been created',
});
this.basicConfigurationComponent.resetFields();
if (this.advancedConfigurationComponent) {
this.advancedConfigurationComponent.resetFields();
}
this.fileManagerContainer.reset();
this.setState({
...defaultState,
});
}
}
private validateFiles = () => {
private validateLabels = (): boolean => {
const { labels } = this.state;
return !!labels.length;
};
private validateFiles = (): boolean => {
const files = this.fileManagerContainer.getFiles();
this.setState({
files,
@ -72,21 +94,21 @@ export default class CreateTaskContent extends React.PureComponent<Props, State>
);
return !!totalLen;
}
};
private handleSubmitBasicConfiguration = (values: BaseConfiguration) => {
private handleSubmitBasicConfiguration = (values: BaseConfiguration): void => {
this.setState({
basic: {...values},
basic: { ...values },
});
};
private handleSubmitAdvancedConfiguration = (values: AdvancedConfiguration) => {
private handleSubmitAdvancedConfiguration = (values: AdvancedConfiguration): void => {
this.setState({
advanced: {...values},
advanced: { ...values },
});
};
private handleSubmitClick = () => {
private handleSubmitClick = (): void => {
if (!this.validateLabels()) {
notification.error({
message: 'Could not create a task',
@ -105,44 +127,50 @@ export default class CreateTaskContent extends React.PureComponent<Props, State>
this.basicConfigurationComponent.submit()
.then(() => {
return this.advancedConfigurationComponent ?
this.advancedConfigurationComponent.submit() :
new Promise((resolve) => {
resolve();
})
})
.then(() => {
this.props.onCreate(this.state);
})
.catch((_: any) => {
if (this.advancedConfigurationComponent) {
return this.advancedConfigurationComponent.submit();
}
return new Promise((resolve): void => {
resolve();
});
}).then((): void => {
const { onCreate } = this.props;
onCreate(this.state);
}).catch((): void => {
notification.error({
message: 'Could not create a task',
description: 'Please, check configuration you specified',
});
});
}
};
private renderBasicBlock() {
private renderBasicBlock(): JSX.Element {
return (
<Col span={24}>
<BasicConfigurationForm wrappedComponentRef={
(component: any) => { this.basicConfigurationComponent = component }
} onSubmit={this.handleSubmitBasicConfiguration}/>
<BasicConfigurationForm
wrappedComponentRef={
(component: any): void => { this.basicConfigurationComponent = component; }
}
onSubmit={this.handleSubmitBasicConfiguration}
/>
</Col>
);
}
private renderLabelsBlock() {
private renderLabelsBlock(): JSX.Element {
const { labels } = this.state;
return (
<Col span={24}>
<Text type='danger'>* </Text>
<Text className='cvat-black-color'>Labels:</Text>
<LabelsEditor
labels={this.state.labels}
labels={labels}
onSubmit={
(labels) => {
(newLabels): void => {
this.setState({
labels,
labels: newLabels,
});
}
}
@ -151,32 +179,37 @@ export default class CreateTaskContent extends React.PureComponent<Props, State>
);
}
private renderFilesBlock() {
private renderFilesBlock(): JSX.Element {
return (
<Col span={24}>
<Text type='danger'>* </Text>
<Text className='cvat-black-color'>Select files:</Text>
<FileManagerContainer ref={
(container: any) =>
this.fileManagerContainer = container
} withRemote={true}/>
<FileManagerContainer
ref={
(container: any): void => { this.fileManagerContainer = container; }
}
withRemote
/>
</Col>
);
}
private renderAdvancedBlock() {
private renderAdvancedBlock(): JSX.Element {
const { installedGit } = this.props;
return (
<Col span={24}>
<Collapse>
<Collapse.Panel
key='1'
header={
<Text className='cvat-title'>Advanced configuration</Text>
} key='1'>
}
>
<AdvancedConfigurationForm
installedGit={this.props.installedGit}
installedGit={installedGit}
wrappedComponentRef={
(component: any) => {
this.advancedConfigurationComponent = component
(component: any): void => {
this.advancedConfigurationComponent = component;
}
}
onSubmit={this.handleSubmitAdvancedConfiguration}
@ -187,29 +220,9 @@ export default class CreateTaskContent extends React.PureComponent<Props, State>
);
}
public componentDidUpdate(prevProps: Props) {
if (this.props.status === 'CREATED' && prevProps.status !== 'CREATED') {
notification.info({
message: 'The task has been created',
});
this.basicConfigurationComponent.resetFields();
if (this.advancedConfigurationComponent) {
this.advancedConfigurationComponent.resetFields();
}
this.fileManagerContainer.reset();
this.setState({
...defaultState,
});
}
}
public render() {
const loading = !!this.props.status
&& this.props.status !== 'CREATED'
&& this.props.status !== 'FAILED';
public render(): JSX.Element {
const { status } = this.props;
const loading = !!status && status !== 'CREATED' && status !== 'FAILED';
return (
<Row type='flex' justify='start' align='middle' className='cvat-create-task-content'>
@ -223,7 +236,7 @@ export default class CreateTaskContent extends React.PureComponent<Props, State>
{ this.renderAdvancedBlock() }
<Col span={18}>
{loading ? <Alert message={this.props.status}/> : null}
{loading ? <Alert message={status} /> : null}
</Col>
<Col span={6}>
<Button
@ -231,7 +244,9 @@ export default class CreateTaskContent extends React.PureComponent<Props, State>
disabled={loading}
type='danger'
onClick={this.handleSubmitClick}
> Submit </Button>
>
Submit
</Button>
</Col>
</Row>
);

@ -15,15 +15,21 @@ interface Props {
installedGit: boolean;
}
export default function CreateTaskPage(props: Props) {
export default function CreateTaskPage(props: Props): JSX.Element {
const {
status,
onCreate,
installedGit,
} = props;
return (
<Row type='flex' justify='center' align='top' className='cvat-create-task-form-wrapper'>
<Col md={20} lg={16} xl={14} xxl={9}>
<Text className='cvat-title'>Create a new task</Text>
<CreateTaskContent
status={props.status}
onCreate={props.onCreate}
installedGit={props.installedGit}
status={status}
onCreate={onCreate}
installedGit={installedGit}
/>
</Col>
</Row>

@ -46,28 +46,73 @@ type CVATAppProps = {
installedTFSegmentation: boolean;
notifications: NotificationsState;
user: any;
}
};
export default class CVATApplication extends React.PureComponent<CVATAppProps> {
constructor(props: any) {
super(props);
public componentDidMount(): void {
const { verifyAuthorized } = this.props;
verifyAuthorized();
}
private showMessages() {
function showMessage(title: string) {
public componentDidUpdate(): void {
const {
loadFormats,
loadUsers,
initPlugins,
userInitialized,
formatsInitialized,
formatsFetching,
usersInitialized,
usersFetching,
pluginsInitialized,
pluginsFetching,
user,
} = this.props;
this.showErrors();
this.showMessages();
if (!userInitialized || user == null) {
// not authorized user
return;
}
if (!formatsInitialized && !formatsFetching) {
loadFormats();
}
if (!usersInitialized && !usersFetching) {
loadUsers();
}
if (!pluginsInitialized && !pluginsFetching) {
initPlugins();
}
}
private showMessages(): void {
function showMessage(title: string): void {
notification.info({
message: (
<div dangerouslySetInnerHTML={{
__html: title,
}}/>
<div
// eslint-disable-next-line
dangerouslySetInnerHTML={{
__html: title,
}}
/>
),
duration: null,
});
}
const { tasks } = this.props.notifications.messages;
const { models } = this.props.notifications.messages;
let shown = !!tasks.loadingDone || !!models.inferenceDone;
const {
notifications,
resetMessages,
} = this.props;
const { tasks } = notifications.messages;
const { models } = notifications.messages;
const shown = !!tasks.loadingDone || !!models.inferenceDone;
if (tasks.loadingDone) {
showMessage(tasks.loadingDone);
@ -77,18 +122,21 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
}
if (shown) {
this.props.resetMessages();
resetMessages();
}
}
private showErrors() {
function showError(title: string, _error: any) {
private showErrors(): void {
function showError(title: string, _error: any): void {
const error = _error.toString();
notification.error({
message: (
<div dangerouslySetInnerHTML={{
__html: title,
}}/>
<div
// eslint-disable-next-line
dangerouslySetInnerHTML={{
__html: title,
}}
/>
),
duration: null,
description: error.length > 200 ? '' : error,
@ -97,14 +145,19 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
console.error(error);
}
const { auth } = this.props.notifications.errors;
const { tasks } = this.props.notifications.errors;
const { formats } = this.props.notifications.errors;
const { users } = this.props.notifications.errors;
const { share } = this.props.notifications.errors;
const { models } = this.props.notifications.errors;
const {
notifications,
resetErrors,
} = this.props;
const { auth } = notifications.errors;
const { tasks } = notifications.errors;
const { formats } = notifications.errors;
const { users } = notifications.errors;
const { share } = notifications.errors;
const { models } = notifications.errors;
let shown = !!auth.authorized || !!auth.login || !!auth.logout || !!auth.register
const shown = !!auth.authorized || !!auth.login || !!auth.logout || !!auth.register
|| !!tasks.fetching || !!tasks.updating || !!tasks.dumping || !!tasks.loading
|| !!tasks.exporting || !!tasks.deleting || !!tasks.creating || !!formats.fetching
|| !!users.fetching || !!share.fetching || !!models.creating || !!models.starting
@ -169,90 +222,78 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
showError(models.metaFetching.message, models.metaFetching.reason);
}
if (models.inferenceStatusFetching) {
showError(models.inferenceStatusFetching.message, models.inferenceStatusFetching.reason);
showError(
models.inferenceStatusFetching.message,
models.inferenceStatusFetching.reason,
);
}
if (shown) {
this.props.resetErrors();
}
}
public componentDidMount() {
this.props.verifyAuthorized();
}
public componentDidUpdate() {
this.showErrors();
this.showMessages();
if (!this.props.userInitialized || this.props.user == null) {
// not authorized user
return;
}
if (!this.props.formatsInitialized && !this.props.formatsFetching) {
this.props.loadFormats();
}
if (!this.props.usersInitialized && !this.props.usersFetching) {
this.props.loadUsers();
}
if (!this.props.pluginsInitialized && !this.props.pluginsFetching) {
this.props.initPlugins();
resetErrors();
}
}
// Where you go depends on your URL
public render() {
const readyForRender =
(this.props.userInitialized && this.props.user == null) ||
(this.props.userInitialized && this.props.formatsInitialized &&
this.props.pluginsInitialized && this.props.usersInitialized);
public render(): JSX.Element {
const {
userInitialized,
usersInitialized,
pluginsInitialized,
formatsInitialized,
installedAutoAnnotation,
installedTFSegmentation,
installedTFAnnotation,
user,
} = this.props;
const withModels = this.props.installedAutoAnnotation
|| this.props.installedTFAnnotation || this.props.installedTFSegmentation;
const readyForRender = (userInitialized && user == null)
|| (userInitialized && formatsInitialized
&& pluginsInitialized && usersInitialized);
const withModels = installedAutoAnnotation
|| installedTFAnnotation || installedTFSegmentation;
if (readyForRender) {
if (this.props.user) {
if (user) {
return (
<BrowserRouter>
<Layout>
<HeaderContainer> </HeaderContainer>
<Layout.Content>
<Switch>
<Route exact path='/tasks' component={TasksPageContainer}/>
<Route path='/tasks/create' component={CreateTaskPageContainer}/>
<Route exact path='/tasks/:id' component={TaskPageContainer}/>
<Route path='/tasks/:id/jobs/:id' component={AnnotationPageContainer}/>
{ withModels &&
<Route exact path='/models' component={ModelsPageContainer}/> }
{ this.props.installedAutoAnnotation &&
<Route path='/models/create' component={CreateModelPageContainer}/> }
<Redirect push to='/tasks'/>
<Route exact path='/tasks' component={TasksPageContainer} />
<Route path='/tasks/create' component={CreateTaskPageContainer} />
<Route exact path='/tasks/:id' component={TaskPageContainer} />
<Route path='/tasks/:id/jobs/:id' component={AnnotationPageContainer} />
{ withModels
&& <Route exact path='/models' component={ModelsPageContainer} /> }
{ installedAutoAnnotation
&& <Route path='/models/create' component={CreateModelPageContainer} /> }
<Redirect push to='/tasks' />
</Switch>
<FeedbackComponent/>
<ModelRunnerModalContainer/>
<FeedbackComponent />
<ModelRunnerModalContainer />
{/* eslint-disable-next-line */}
<a id='downloadAnchor' style={{ display: 'none' }} download/>
</Layout.Content>
</Layout>
</BrowserRouter>
);
} else {
return (
<BrowserRouter>
<Switch>
<Route exact path='/auth/register' component={RegisterPageContainer}/>
<Route exact path='/auth/login' component={LoginPageContainer}/>
<Redirect to='/auth/login'/>
</Switch>
</BrowserRouter>
);
}
} else {
return (
<Spin size='large' style={{margin: '25% 50%'}}/>
<BrowserRouter>
<Switch>
<Route exact path='/auth/register' component={RegisterPageContainer} />
<Route exact path='/auth/login' component={LoginPageContainer} />
<Redirect to='/auth/login' />
</Switch>
</BrowserRouter>
);
}
return (
<Spin size='large' style={{ margin: '25% 50%' }} />
);
}
}

@ -31,88 +31,87 @@ interface State {
active: boolean;
}
export default class Feedback extends React.PureComponent<{}, State> {
public constructor(props: {}) {
super(props);
this.state = {
active: false,
}
}
function renderContent(): JSX.Element {
const githubURL = 'https://github.com/opencv/cvat';
const githubImage = 'https://raw.githubusercontent.com/opencv/'
+ 'cvat/develop/cvat/apps/documentation/static/documentation/images/cvat.jpg';
const questionsURL = 'https://gitter.im/opencv-cvat/public';
const feedbackURL = 'https://gitter.im/opencv-cvat/public';
private renderContent() {
const githubURL = 'https://github.com/opencv/cvat';
const githubImage = 'https://raw.githubusercontent.com/opencv/'
+ 'cvat/develop/cvat/apps/documentation/static/documentation/images/cvat.jpg';
const questionsURL = 'https://gitter.im/opencv-cvat/public';
const feedbackURL = 'https://gitter.im/opencv-cvat/public';
return (
<>
<Icon type='star' />
<Text style={{ marginLeft: '10px' }}>
Star us on
<a target='_blank' rel='noopener noreferrer' href={githubURL}> GitHub</a>
</Text>
<br />
<Icon type='like' />
<Text style={{ marginLeft: '10px' }}>
Leave a
<a target='_blank' rel='noopener noreferrer' href={feedbackURL}> feedback</a>
</Text>
<hr />
<div style={{ display: 'flex' }}>
<FacebookShareButton url={githubURL} quote='Computer Vision Annotation Tool'>
<FacebookIcon size={32} round />
</FacebookShareButton>
<VKShareButton url={githubURL} title='Computer Vision Annotation Tool' image={githubImage} description='CVAT'>
<VKIcon size={32} round />
</VKShareButton>
<TwitterShareButton url={githubURL} title='Computer Vision Annotation Tool' hashtags={['CVAT']}>
<TwitterIcon size={32} round />
</TwitterShareButton>
<RedditShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<RedditIcon size={32} round />
</RedditShareButton>
<LinkedinShareButton url={githubURL}>
<LinkedinIcon size={32} round />
</LinkedinShareButton>
<TelegramShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<TelegramIcon size={32} round />
</TelegramShareButton>
<WhatsappShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<WhatsappIcon size={32} round />
</WhatsappShareButton>
<ViberShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<ViberIcon size={32} round />
</ViberShareButton>
</div>
<hr />
<Text style={{ marginTop: '50px' }}>
Do you need help? Contact us on
<a target='_blank' rel='noopener noreferrer' href={questionsURL}> gitter</a>
</Text>
</>
);
}
return (
<>
<Icon type='star'/>
<Text style={{marginLeft: '10px'}}>
Star us on <a target='_blank' href={githubURL}>GitHub</a>
</Text>
<br/>
<Icon type='like'/>
<Text style={{marginLeft: '10px'}}>
Leave a <a target='_blank' href={feedbackURL}>feedback</a>
</Text>
<hr/>
<div style={{display: 'flex'}}>
<FacebookShareButton url={githubURL} quote='Computer Vision Annotation Tool'>
<FacebookIcon size={32} round={true} />
</FacebookShareButton>
<VKShareButton url={githubURL} title='Computer Vision Annotation Tool' image={githubImage} description='CVAT'>
<VKIcon size={32} round={true} />
</VKShareButton>
<TwitterShareButton url={githubURL} title='Computer Vision Annotation Tool' hashtags={['CVAT']}>
<TwitterIcon size={32} round={true} />
</TwitterShareButton>
<RedditShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<RedditIcon size={32} round={true} />
</RedditShareButton>
<LinkedinShareButton url={githubURL}>
<LinkedinIcon size={32} round={true} />
</LinkedinShareButton>
<TelegramShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<TelegramIcon size={32} round={true} />
</TelegramShareButton>
<WhatsappShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<WhatsappIcon size={32} round={true} />
</WhatsappShareButton>
<ViberShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<ViberIcon size={32} round={true} />
</ViberShareButton>
</div>
<hr/>
<Text style={{marginTop: '50px'}}>
Do you need help? Contact us on <a href={questionsURL}>gitter</a>
</Text>
</>
);
}
export default function Feedback(): JSX.Element {
const [visible, setVisible] = React.useState(false);
public render() {
return (
<>
<Popover
placement='leftTop'
title={
<Text className='cvat-black-color'>Help to make CVAT better</Text>
}
content={this.renderContent()}
visible={this.state.active}
return (
<>
<Popover
placement='leftTop'
title={
<Text className='cvat-black-color'>Help to make CVAT better</Text>
}
content={renderContent()}
visible={visible}
>
<Button
style={{ color: '#ff4d4f' }}
className='cvat-feedback-button'
type='link'
onClick={(): void => {
setVisible(!visible);
}}
>
<Button style={{color: '#ff4d4f'}} className='cvat-feedback-button' type='link' onClick={() => {
this.setState({
active: !this.state.active,
});
}}>
{ this.state.active ? <Icon type='close-circle' theme='filled'/> :
<Icon type='message' theme='twoTone'/> }
</Button>
</Popover>
</>
);
}
}
{ visible ? <Icon type='close-circle' theme='filled' />
: <Icon type='message' theme='twoTone' /> }
</Button>
</Popover>
</>
);
}

@ -44,35 +44,61 @@ export default class FileManager extends React.PureComponent<Props, State> {
};
this.loadData('/');
};
}
private loadData = (key: string) => {
const promise = new Promise<void>((resolve, reject) => {
const success = () => resolve();
const failure = () => reject();
this.props.onLoadData(key, success, failure);
});
public getFiles(): Files {
const {
active,
files,
} = this.state;
return {
local: active === 'local' ? files.local : [],
share: active === 'share' ? files.share : [],
remote: active === 'remote' ? files.remote : [],
};
}
private loadData = (key: string): Promise<void> => new Promise<void>(
(resolve, reject): void => {
const { onLoadData } = this.props;
const success = (): void => resolve();
const failure = (): void => reject();
onLoadData(key, success, failure);
},
);
return promise;
public reset(): void {
this.setState({
expandedKeys: [],
active: 'local',
files: {
local: [],
share: [],
remote: [],
},
});
}
private renderLocalSelector() {
private renderLocalSelector(): JSX.Element {
const { files } = this.state;
return (
<Tabs.TabPane key='local' tab='My computer'>
<Upload.Dragger
multiple
fileList={this.state.files.local as any[]}
fileList={files.local as any[]}
showUploadList={false}
beforeUpload={(_: RcFile, files: RcFile[]) => {
beforeUpload={(_: RcFile, newLocalFiles: RcFile[]): boolean => {
this.setState({
files: {
...this.state.files,
local: files
...files,
local: newLocalFiles,
},
});
return false;
}
}>
}}
>
<p className='ant-upload-drag-icon'>
<Icon type='inbox' />
</p>
@ -81,115 +107,126 @@ export default class FileManager extends React.PureComponent<Props, State> {
Support for a bulk images or a single video
</p>
</Upload.Dragger>
{ !!this.state.files.local.length &&
<>
<br/>
<Text className='cvat-black-color'>
{`${this.state.files.local.length} file(s) selected`}
</Text>
</>
{ !!files.local.length
&& (
<>
<br />
<Text className='cvat-black-color'>
{`${files.local.length} file(s) selected`}
</Text>
</>
)
}
</Tabs.TabPane>
);
}
private renderShareSelector() {
function renderTreeNodes(data: TreeNodeNormal[]) {
private renderShareSelector(): JSX.Element {
function renderTreeNodes(data: TreeNodeNormal[]): JSX.Element[] {
return data.map((item: TreeNodeNormal) => {
if (item.children) {
return (
<Tree.TreeNode title={item.title} key={item.key} dataRef={item} isLeaf={item.isLeaf}>
{renderTreeNodes(item.children)}
</Tree.TreeNode>
);
return (
<Tree.TreeNode
title={item.title}
key={item.key}
dataRef={item}
isLeaf={item.isLeaf}
>
{renderTreeNodes(item.children)}
</Tree.TreeNode>
);
}
return <Tree.TreeNode key={item.key} {...item} dataRef={item} />;
});
}
const { treeData } = this.props;
const {
expandedKeys,
files,
} = this.state;
return (
<Tabs.TabPane key='share' tab='Connected file share'>
{ this.props.treeData.length ?
<Tree
className='cvat-share-tree'
checkable
showLine
checkStrictly={false}
expandedKeys={this.state.expandedKeys}
checkedKeys={this.state.files.share}
loadData={(node: AntTreeNode) => {
return this.loadData(node.props.dataRef.key);
}}
onExpand={(expandedKeys: string[]) => {
this.setState({
expandedKeys,
});
}}
onCheck={(checkedKeys: string[] | {checked: string[], halfChecked: string[]}) => {
const keys = checkedKeys as string[];
this.setState({
files: {
...this.state.files,
share: keys,
},
});
}}>
{ renderTreeNodes(this.props.treeData) }
</Tree> : <Text className='cvat-black-color'>{'No data found'}</Text>
{ treeData.length
? (
<Tree
className='cvat-share-tree'
checkable
showLine
checkStrictly={false}
expandedKeys={expandedKeys}
checkedKeys={files.share}
loadData={(node: AntTreeNode): Promise<void> => this.loadData(
node.props.dataRef.key,
)}
onExpand={(newExpandedKeys: string[]): void => {
this.setState({
expandedKeys: newExpandedKeys,
});
}}
onCheck={
(checkedKeys: string[] | {
checked: string[];
halfChecked: string[];
}): void => {
const keys = checkedKeys as string[];
this.setState({
files: {
...files,
share: keys,
},
});
}}
>
{ renderTreeNodes(treeData) }
</Tree>
) : <Text className='cvat-black-color'>No data found</Text>
}
</Tabs.TabPane>
);
}
private renderRemoteSelector() {
private renderRemoteSelector(): JSX.Element {
const { files } = this.state;
return (
<Tabs.TabPane key='remote' tab='Remote sources'>
<Input.TextArea
placeholder='Enter one URL per line'
rows={6}
value={[...this.state.files.remote].join('\n')}
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => {
value={[...files.remote].join('\n')}
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>): void => {
this.setState({
files: {
...this.state.files,
...files,
remote: event.target.value.split('\n'),
},
});
}}/>
}}
/>
</Tabs.TabPane>
);
}
public getFiles(): Files {
return {
local: this.state.active === 'local' ? this.state.files.local : [],
share: this.state.active === 'share' ? this.state.files.share : [],
remote: this.state.active === 'remote' ? this.state.files.remote : [],
};
}
public reset() {
this.setState({
expandedKeys: [],
active: 'local',
files: {
local: [],
share: [],
remote: [],
},
});
}
public render(): JSX.Element {
const { withRemote } = this.props;
public render() {
return (
<>
<Tabs type='card' tabBarGutter={5} onChange={(activeKey: string) => this.setState({
active: activeKey as any,
})}>
<Tabs
type='card'
tabBarGutter={5}
onChange={
(activeKey: string): void => this.setState({
active: activeKey as any,
})
}
>
{ this.renderLocalSelector() }
{ this.renderShareSelector() }
{ this.props.withRemote && this.renderRemoteSelector() }
{ withRemote && this.renderRemoteSelector() }
</Tabs>
</>
);

@ -15,6 +15,7 @@ import Text from 'antd/lib/typography/Text';
import getCore from '../../core';
const core = getCore();
const serverHost = core.config.backendAPI.slice(0, -7);
interface HeaderContainerProps {
onLogout: () => void;
@ -28,66 +29,121 @@ interface HeaderContainerProps {
type Props = HeaderContainerProps & RouteComponentProps;
const cvatLogo = () => <img src='/assets/cvat-logo.svg'/>;
const userLogo = () => <img src='/assets/icon-account.svg'/>;
const cvatLogo = (): JSX.Element => <img alt='' src='/assets/cvat-logo.svg' />;
const userLogo = (): JSX.Element => <img alt='' src='/assets/icon-account.svg' />;
function HeaderContainer(props: Props): JSX.Element {
const {
installedTFSegmentation,
installedAutoAnnotation,
installedTFAnnotation,
installedAnalytics,
username,
onLogout,
logoutFetching,
} = props;
const renderModels = installedAutoAnnotation
|| installedTFAnnotation
|| installedTFSegmentation;
function HeaderContainer(props: Props) {
const renderModels = props.installedAutoAnnotation
|| props.installedTFAnnotation
|| props.installedTFSegmentation;
return (
<Layout.Header className='cvat-header'>
<div className='cvat-left-header'>
<Icon className='cvat-logo-icon' component={cvatLogo}/>
<Icon className='cvat-logo-icon' component={cvatLogo} />
<Button className='cvat-header-button' type='link' value='tasks' onClick={
() => props.history.push('/tasks')
}> Tasks </Button>
{ renderModels ?
<Button className='cvat-header-button' type='link' value='models' onClick={
() => props.history.push('/models')
}> Models </Button> : null
<Button
className='cvat-header-button'
type='link'
value='tasks'
onClick={
(): void => props.history.push('/tasks')
}
>
Tasks
</Button>
{ renderModels
&& (
<Button
className='cvat-header-button'
type='link'
value='models'
onClick={
(): void => props.history.push('/models')
}
>
Models
</Button>
)
}
{ props.installedAnalytics ?
<Button className='cvat-header-button' type='link' onClick={
() => {
const serverHost = core.config.backendAPI.slice(0, -7);
window.open(`${serverHost}/analytics/app/kibana`, '_blank');
}
}> Analytics </Button> : null
{ installedAnalytics
&& (
<Button
className='cvat-header-button'
type='link'
onClick={
(): void => {
// false positive
// eslint-disable-next-line
window.open(`${serverHost}/analytics/app/kibana`, '_blank');
}
}
>
Analytics
</Button>
)
}
</div>
<div className='cvat-right-header'>
<Button className='cvat-header-button' type='link' onClick={
() => window.open('https://github.com/opencv/cvat', '_blank')
}>
<Icon type='github'/>
<Button
className='cvat-header-button'
type='link'
onClick={
(): void => {
window.open('https://github.com/opencv/cvat', '_blank');
}
}
>
<Icon type='github' />
<Text className='cvat-black-color'>GitHub</Text>
</Button>
<Button className='cvat-header-button' type='link' onClick={
() => {
const serverHost = core.config.backendAPI.slice(0, -7);
<Button
className='cvat-header-button'
type='link'
onClick={
(): void => {
// false positive
// eslint-disable-next-line
window.open(`${serverHost}/documentation/user_guide.html`, '_blank')
}
}> <Icon type='question-circle'/> Help </Button>
}
>
<Icon type='question-circle' />
Help
</Button>
<Menu className='cvat-header-menu' subMenuCloseDelay={0.1} mode='horizontal'>
<Menu.SubMenu title={
<span>
<Icon className='cvat-header-user-icon' component={userLogo} />
<span>
<Text strong>
{props.username.length > 14 ? `${props.username.slice(0, 10)} ...` : props.username}
</Text>
<Icon className='cvat-header-menu-icon' type='caret-down'/>
</span>
</span>
}>
<Menu.SubMenu
title={
(
<span>
<Icon className='cvat-header-user-icon' component={userLogo} />
<span>
<Text strong>
{username.length > 14 ? `${username.slice(0, 10)} ...` : username}
</Text>
<Icon className='cvat-header-menu-icon' type='caret-down' />
</span>
</span>
)
}
>
<Menu.Item
onClick={props.onLogout}
disabled={props.logoutFetching}
onClick={onLogout}
disabled={logoutFetching}
className='cvat-header-button'
>
{props.logoutFetching && <Icon type='loading'/>} Logout
{logoutFetching && <Icon type='loading' />}
Logout
</Menu.Item>
</Menu.SubMenu>
</Menu>

@ -1,26 +1,17 @@
import React from 'react';
import LabelForm from './label-form';
import { Label, Attribute } from './common';
import LabelForm from './label-form';
import { Label } from './common';
interface Props {
onCreate: (label: Label | null) => void;
}
interface State {
attributes: Attribute[];
}
export default class ConstructorCreator extends React.PureComponent<Props, State> {
public constructor(props: Props) {
super(props);
}
public render() {
return (
<div className='cvat-label-constructor-creator'>
<LabelForm label={null} onSubmit={this.props.onCreate}/>
</div>
);
}
export default function ConstructorCreator(props: Props): JSX.Element {
const { onCreate } = props;
return (
<div className='cvat-label-constructor-creator'>
<LabelForm label={null} onSubmit={onCreate} />
</div>
);
}

@ -1,28 +1,22 @@
import React from 'react';
import LabelForm from './label-form';
import { Label, Attribute } from './common';
import { Label } from './common';
interface Props {
label: Label;
onUpdate: (label: Label | null) => void;
}
interface State {
savedAttributes: Attribute[];
unsavedAttributes: Attribute[];
}
export default class ConstructorUpdater extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
}
export default function ConstructorUpdater(props: Props): JSX.Element {
const {
label,
onUpdate,
} = props;
public render() {
return (
<div className='cvat-label-constructor-updater'>
<LabelForm label={this.props.label} onSubmit={this.props.onUpdate}/>
</div>
);
}
return (
<div className='cvat-label-constructor-updater'>
<LabelForm label={label} onSubmit={onUpdate} />
</div>
);
}

@ -16,21 +16,40 @@ interface ConstructorViewerItemProps {
onDelete: (label: Label) => void;
}
export default function ConstructorViewerItem(props: ConstructorViewerItemProps) {
export default function ConstructorViewerItem(props: ConstructorViewerItemProps): JSX.Element {
const {
color,
label,
onUpdate,
onDelete,
} = props;
return (
<div style={{background: props.color}} className='cvat-constructor-viewer-item'>
<Text>{ props.label.name }</Text>
<div style={{ background: color }} className='cvat-constructor-viewer-item'>
<Text>{label.name}</Text>
<Tooltip title='Update attributes'>
<span onClick={() => props.onUpdate(props.label)}>
<Icon theme='filled' type='edit'/>
<span
role='button'
tabIndex={0}
onClick={(): void => onUpdate(label)}
onKeyPress={(): boolean => false}
>
<Icon theme='filled' type='edit' />
</span>
</Tooltip>
{ props.label.id >= 0 ? null :
<Tooltip title='Delete label'>
<span onClick={() => props.onDelete(props.label)}>
<Icon type='close'></Icon>
</span>
</Tooltip>
{ label.id < 0
&& (
<Tooltip title='Delete label'>
<span
role='button'
tabIndex={0}
onClick={(): void => onDelete(label)}
onKeyPress={(): boolean => false}
>
<Icon type='close' />
</span>
</Tooltip>
)
}
</div>
);

@ -23,7 +23,7 @@ const colors = [
let currentColor = 0;
function nextColor() {
function nextColor(): string {
const color = colors[currentColor];
currentColor += 1;
if (currentColor >= colors.length) {
@ -32,12 +32,14 @@ function nextColor() {
return color;
}
export default function ConstructorViewer(props: ConstructorViewerProps) {
export default function ConstructorViewer(props: ConstructorViewerProps): JSX.Element {
const { onCreate } = props;
currentColor = 0;
const list = [
<Button key='create' type='ghost' onClick={props.onCreate} className='cvat-constructor-viewer-new-item'>
Add label <Icon type='plus-circle'/>
<Button key='create' type='ghost' onClick={onCreate} className='cvat-constructor-viewer-new-item'>
Add label
<Icon type='plus-circle' />
</Button>];
for (const label of props.labels) {
list.push(
@ -47,8 +49,8 @@ export default function ConstructorViewer(props: ConstructorViewerProps) {
label={label}
key={label.id}
color={nextColor()}
/>
)
/>,
);
}
return (
@ -56,4 +58,4 @@ export default function ConstructorViewer(props: ConstructorViewerProps) {
{ list }
</div>
);
}
}

@ -35,73 +35,76 @@ type Props = FormComponentProps & {
onSubmit: (label: Label | null) => void;
};
interface State {
}
class LabelForm extends React.PureComponent<Props, State> {
class LabelForm extends React.PureComponent<Props, {}> {
private continueAfterSubmit: boolean;
constructor(props: Props) {
super(props);
this.continueAfterSubmit = false;
}
private handleSubmit = (e: React.FormEvent) => {
private handleSubmit = (e: React.FormEvent): void => {
e.preventDefault();
this.props.form.validateFields((error, values) => {
const {
form,
label,
onSubmit,
} = this.props;
form.validateFields((error, values): void => {
if (!error) {
this.props.onSubmit({
onSubmit({
name: values.labelName,
id: this.props.label ? this.props.label.id : idGenerator(),
attributes: values.keys.map((key: number, index: number) => {
return {
id: label ? label.id : idGenerator(),
attributes: values.keys.map((key: number, index: number): Attribute => (
{
name: values.attrName[key],
type: values.type[key],
mutable: values.mutable[key],
id: this.props.label && index < this.props.label.attributes.length
? this.props.label.attributes[index].id : key,
id: label && index < label.attributes.length
? label.attributes[index].id : key,
values: Array.isArray(values.values[key])
? values.values[key] : [values.values[key]]
};
}),
? values.values[key] : [values.values[key]],
}
)),
});
this.props.form.resetFields();
form.resetFields();
if (!this.continueAfterSubmit) {
this.props.onSubmit(null);
onSubmit(null);
}
}
});
}
};
private addAttribute = () => {
private addAttribute = (): void => {
const { form } = this.props;
const keys = form.getFieldValue('keys');
const nextKeys = keys.concat(idGenerator());
form.setFieldsValue({
keys: nextKeys,
});
}
};
private removeAttribute = (key: number) => {
private removeAttribute = (key: number): void => {
const { form } = this.props;
const keys = form.getFieldValue('keys');
form.setFieldsValue({
keys: keys.filter((_key: number) => _key !== key),
});
}
};
private renderAttributeNameInput(key: number, attr: Attribute | null) {
private renderAttributeNameInput(key: number, attr: Attribute | null): JSX.Element {
const locked = attr ? attr.id >= 0 : false;
const value = attr ? attr.name : '';
const { form } = this.props;
return (
<Col span={5}>
<Form.Item hasFeedback> {
this.props.form.getFieldDecorator(`attrName[${key}]`, {
<Form.Item hasFeedback>
{form.getFieldDecorator(`attrName[${key}]`, {
initialValue: value,
rules: [{
required: true,
@ -110,42 +113,54 @@ class LabelForm extends React.PureComponent<Props, State> {
pattern: patterns.validateAttributeName.pattern,
message: patterns.validateAttributeName.message,
}],
})(<Input disabled={locked} placeholder='Name'/>)
} </Form.Item>
})(<Input disabled={locked} placeholder='Name' />)}
</Form.Item>
</Col>
);
}
private renderAttributeTypeInput(key: number, attr: Attribute | null) {
private renderAttributeTypeInput(key: number, attr: Attribute | null): JSX.Element {
const locked = attr ? attr.id >= 0 : false;
const type = attr ? attr.type.toUpperCase() : AttributeType.SELECT;
const { form } = this.props;
return (
<Col span={4}>
<Form.Item>
<Tooltip overlay='An HTML element representing the attribute'>
{this.props.form.getFieldDecorator(`type[${key}]`, {
{ form.getFieldDecorator(`type[${key}]`, {
initialValue: type,
})(
<Select disabled={locked}>
<Select.Option value={AttributeType.SELECT}> Select </Select.Option>
<Select.Option value={AttributeType.RADIO}> Radio </Select.Option>
<Select.Option value={AttributeType.CHECKBOX}> Checkbox </Select.Option>
<Select.Option value={AttributeType.TEXT}> Text </Select.Option>
<Select.Option value={AttributeType.NUMBER}> Number </Select.Option>
</Select>
)
}</Tooltip>
<Select.Option value={AttributeType.SELECT}>
Select
</Select.Option>
<Select.Option value={AttributeType.RADIO}>
Radio
</Select.Option>
<Select.Option value={AttributeType.CHECKBOX}>
Checkbox
</Select.Option>
<Select.Option value={AttributeType.TEXT}>
Text
</Select.Option>
<Select.Option value={AttributeType.NUMBER}>
Number
</Select.Option>
</Select>,
)}
</Tooltip>
</Form.Item>
</Col>
);
}
private renderAttributeValuesInput(key: number, attr: Attribute | null) {
private renderAttributeValuesInput(key: number, attr: Attribute | null): JSX.Element {
const locked = attr ? attr.id >= 0 : false;
const existedValues = attr ? attr.values : [];
const { form } = this.props;
const validator = (_: any, values: string[], callback: any) => {
const validator = (_: any, values: string[], callback: any): void => {
if (locked && existedValues) {
if (!equalArrayHead(existedValues, values)) {
callback('You can only append new values');
@ -159,11 +174,11 @@ class LabelForm extends React.PureComponent<Props, State> {
}
callback();
}
};
return (
<Form.Item>
{ this.props.form.getFieldDecorator(`values[${key}]`, {
{ form.getFieldDecorator(`values[${key}]`, {
initialValue: existedValues,
rules: [{
required: true,
@ -174,37 +189,41 @@ class LabelForm extends React.PureComponent<Props, State> {
})(
<Select
mode='tags'
dropdownMenuStyle={{display: 'none'}}
dropdownMenuStyle={{ display: 'none' }}
placeholder='Attribute values'
/>
/>,
)}
</Form.Item>
);
}
private renderBooleanValueInput(key: number, attr: Attribute | null) {
private renderBooleanValueInput(key: number, attr: Attribute | null): JSX.Element {
const value = attr ? attr.values[0] : 'false';
const { form } = this.props;
return (
<Form.Item>
{ this.props.form.getFieldDecorator(`values[${key}]`, {
{ form.getFieldDecorator(`values[${key}]`, {
initialValue: value,
})(
<Select>
<Select.Option value='false'> False </Select.Option>
<Select.Option value='true'> True </Select.Option>
</Select>
</Select>,
)}
</Form.Item>
);
}
private renderNumberRangeInput(key: number, attr: Attribute | null) {
private renderNumberRangeInput(key: number, attr: Attribute | null): JSX.Element {
const locked = attr ? attr.id >= 0 : false;
const value = attr ? attr.values[0] : '';
const { form } = this.props;
const validator = (_: any, value: string, callback: any) => {
const numbers = value.split(';').map((number) => Number.parseFloat(number));
const validator = (_: any, strNumbers: string, callback: any): void => {
const numbers = strNumbers
.split(';')
.map((number): number => Number.parseFloat(number));
if (numbers.length !== 3) {
callback('Invalid input');
}
@ -224,58 +243,60 @@ class LabelForm extends React.PureComponent<Props, State> {
}
callback();
}
};
return (
<Form.Item>
{ this.props.form.getFieldDecorator(`values[${key}]`, {
{ form.getFieldDecorator(`values[${key}]`, {
initialValue: value,
rules: [{
required: true,
message: 'Please set a range',
}, {
validator,
}]
}],
})(
<Input disabled={locked} placeholder='min;max;step'/>
<Input disabled={locked} placeholder='min;max;step' />,
)}
</Form.Item>
);
}
private renderDefaultValueInput(key: number, attr: Attribute | null) {
private renderDefaultValueInput(key: number, attr: Attribute | null): JSX.Element {
const value = attr ? attr.values[0] : '';
const { form } = this.props;
return (
<Form.Item>
{ this.props.form.getFieldDecorator(`values[${key}]`, {
{ form.getFieldDecorator(`values[${key}]`, {
initialValue: value,
})(
<Input placeholder='Default value'/>
<Input placeholder='Default value' />,
)}
</Form.Item>
);
}
private renderMutableAttributeInput(key: number, attr: Attribute | null) {
private renderMutableAttributeInput(key: number, attr: Attribute | null): JSX.Element {
const locked = attr ? attr.id >= 0 : false;
const value = attr ? attr.mutable : false;
const { form } = this.props;
return (
<Form.Item>
<Tooltip overlay='Can this attribute be changed frame to frame?'>
{ this.props.form.getFieldDecorator(`mutable[${key}]`, {
{ form.getFieldDecorator(`mutable[${key}]`, {
initialValue: value,
valuePropName: 'checked',
})(
<Checkbox disabled={locked}> Mutable </Checkbox>
<Checkbox disabled={locked}> Mutable </Checkbox>,
)}
</Tooltip>
</Form.Item>
);
}
private renderDeleteAttributeButton(key: number, attr: Attribute | null) {
private renderDeleteAttributeButton(key: number, attr: Attribute | null): JSX.Element {
const locked = attr ? attr.id >= 0 : false;
return (
@ -285,20 +306,25 @@ class LabelForm extends React.PureComponent<Props, State> {
type='link'
className='cvat-delete-attribute-button'
disabled={locked}
onClick={() => {
onClick={(): void => {
this.removeAttribute(key);
}}
>
<Icon type='close-circle'/>
<Icon type='close-circle' />
</Button>
</Tooltip>
</Form.Item>
);
}
private renderAttribute = (key: number, index: number) => {
const attr = (this.props.label && index < this.props.label.attributes.length
? this.props.label.attributes[index]
private renderAttribute = (key: number, index: number): JSX.Element => {
const {
label,
form,
} = this.props;
const attr = (label && index < label.attributes.length
? label.attributes[index]
: null);
return (
@ -306,23 +332,23 @@ class LabelForm extends React.PureComponent<Props, State> {
<Row type='flex' justify='space-between' align='middle'>
{ this.renderAttributeNameInput(key, attr) }
{ this.renderAttributeTypeInput(key, attr) }
<Col span={6}> {
(() => {
const type = this.props.form.getFieldValue(`type[${key}]`);
<Col span={6}>
{((): JSX.Element => {
const type = form.getFieldValue(`type[${key}]`);
let element = null;
[AttributeType.SELECT, AttributeType.RADIO]
.includes(type) ?
element = this.renderAttributeValuesInput(key, attr)
: type === AttributeType.CHECKBOX ?
element = this.renderBooleanValueInput(key, attr)
: type === AttributeType.NUMBER ?
element = this.renderNumberRangeInput(key, attr)
: element = this.renderDefaultValueInput(key, attr)
if ([AttributeType.SELECT, AttributeType.RADIO].includes(type)) {
element = this.renderAttributeValuesInput(key, attr);
} else if (type === AttributeType.CHECKBOX) {
element = this.renderBooleanValueInput(key, attr);
} else if (type === AttributeType.NUMBER) {
element = this.renderNumberRangeInput(key, attr);
} else {
element = this.renderDefaultValueInput(key, attr);
}
return element;
})()
} </Col>
})()}
</Col>
<Col span={5}>
{ this.renderMutableAttributeInput(key, attr) }
</Col>
@ -332,16 +358,20 @@ class LabelForm extends React.PureComponent<Props, State> {
</Row>
</Form.Item>
);
}
};
private renderLabelNameInput() {
const value = this.props.label ? this.props.label.name : '';
const locked = this.props.label ? this.props.label.id >= 0 : false;
private renderLabelNameInput(): JSX.Element {
const {
label,
form,
} = this.props;
const value = label ? label.name : '';
const locked = label ? label.id >= 0 : false;
return (
<Col span={10}>
<Form.Item hasFeedback> {
this.props.form.getFieldDecorator('labelName', {
<Form.Item hasFeedback>
{form.getFieldDecorator('labelName', {
initialValue: value,
rules: [{
required: true,
@ -349,99 +379,119 @@ class LabelForm extends React.PureComponent<Props, State> {
}, {
pattern: patterns.validateAttributeName.pattern,
message: patterns.validateAttributeName.message,
}]
})(<Input disabled={locked} placeholder='Label name'/>)
} </Form.Item>
}],
})(<Input disabled={locked} placeholder='Label name' />)}
</Form.Item>
</Col>
);
}
private renderNewAttributeButton() {
private renderNewAttributeButton(): JSX.Element {
return (
<Col span={3}>
<Form.Item>
<Button type='ghost' onClick={this.addAttribute}>
Add an attribute <Icon type="plus"/>
Add an attribute
<Icon type='plus' />
</Button>
</Form.Item>
</Col>
);
}
private renderDoneButton() {
private renderDoneButton(): JSX.Element {
return (
<Col>
<Tooltip overlay='Save the label and return'>
<Button
style={{width: '150px'}}
style={{ width: '150px' }}
type='primary'
htmlType='submit'
onClick={() => {
onClick={(): void => {
this.continueAfterSubmit = false;
}}
> Done </Button>
>
Done
</Button>
</Tooltip>
</Col>
);
}
private renderContinueButton() {
private renderContinueButton(): JSX.Element {
const { label } = this.props;
return (
this.props.label ? <div/> :
<Col offset={1}>
<Tooltip overlay='Save the label and create one more'>
<Button
style={{width: '150px'}}
type='primary'
htmlType='submit'
onClick={() => {
this.continueAfterSubmit = true;
}}
> Continue </Button>
</Tooltip>
</Col>
);
label ? <div />
: (
<Col offset={1}>
<Tooltip overlay='Save the label and create one more'>
<Button
style={{ width: '150px' }}
type='primary'
htmlType='submit'
onClick={(): void => {
this.continueAfterSubmit = true;
}}
>
Continue
</Button>
</Tooltip>
</Col>
)
);
}
private renderCancelButton() {
private renderCancelButton(): JSX.Element {
const { onSubmit } = this.props;
return (
<Col offset={1}>
<Tooltip overlay='Do not save the label and return'>
<Button
style={{width: '150px'}}
style={{ width: '150px' }}
type='danger'
onClick={() => {
this.props.onSubmit(null);
onClick={(): void => {
onSubmit(null);
}}
> Cancel </Button>
>
Cancel
</Button>
</Tooltip>
</Col>
);
}
public render() {
this.props.form.getFieldDecorator('keys', {
initialValue: this.props.label
? this.props.label.attributes.map((attr: Attribute) => attr.id)
: []
public render(): JSX.Element {
const {
label,
form,
} = this.props;
form.getFieldDecorator('keys', {
initialValue: label
? label.attributes.map((attr: Attribute): number => attr.id)
: [],
});
let keys = this.props.form.getFieldValue('keys');
const keys = form.getFieldValue('keys');
const attributeItems = keys.map(this.renderAttribute);
return (
<Form onSubmit={this.handleSubmit}>
<Row type='flex' justify='start' align='middle'>
{ this.renderLabelNameInput() }
<Col span={1}/>
<Col span={1} />
{ this.renderNewAttributeButton() }
</Row>
{ attributeItems.length > 0 ?
<Row type='flex' justify='start' align='middle'>
<Col>
<Text>Attributes</Text>
</Col>
</Row> : null
{ attributeItems.length > 0
&& (
<Row type='flex' justify='start' align='middle'>
<Col>
<Text>Attributes</Text>
</Col>
</Row>
)
}
{ attributeItems.reverse() }
<Row type='flex' justify='start' align='middle'>

@ -39,7 +39,6 @@ interface LabelsEditorState {
export default class LabelsEditor
extends React.PureComponent<LabelsEditortProps, LabelsEditorState> {
public constructor(props: LabelsEditortProps) {
super(props);
@ -51,37 +50,46 @@ export default class LabelsEditor
};
}
private handleSubmit(savedLabels: Label[], unsavedLabels: Label[]) {
function transformLabel(label: Label): any {
public componentDidMount(): void {
// just need performe the same code
this.componentDidUpdate(null as any as LabelsEditortProps);
}
public componentDidUpdate(prevProps: LabelsEditortProps): void {
function transformLabel(label: any): Label {
return {
name: label.name,
id: label.id < 0 ? undefined : label.id,
attributes: label.attributes.map((attr: Attribute): any => {
return {
id: label.id || idGenerator(),
attributes: label.attributes.map((attr: any): Attribute => (
{
id: attr.id || idGenerator(),
name: attr.name,
id: attr.id < 0 ? undefined : attr.id,
input_type: attr.type.toLowerCase(),
default_value: attr.values[0],
type: attr.input_type,
mutable: attr.mutable,
values: [...attr.values],
};
}),
}
}
)),
};
}
const output = [];
for (const label of savedLabels.concat(unsavedLabels)) {
output.push(transformLabel(label));
}
const { labels } = this.props;
this.props.onSubmit(output);
if (!prevProps || prevProps.labels !== labels) {
const transformedLabels = labels.map(transformLabel);
this.setState({
savedLabels: transformedLabels
.filter((label: Label) => label.id >= 0),
unsavedLabels: transformedLabels
.filter((label: Label) => label.id < 0),
});
}
}
private handleRawSubmit = (labels: Label[]) => {
private handleRawSubmit = (labels: Label[]): void => {
const unsavedLabels = [];
const savedLabels = [];
for (let label of labels) {
for (const label of labels) {
if (label.id >= 0) {
savedLabels.push(label);
} else {
@ -95,29 +103,60 @@ export default class LabelsEditor
});
this.handleSubmit(savedLabels, unsavedLabels);
}
};
private handleCreate = (label: Label | null): void => {
if (label === null) {
this.setState({
constructorMode: ConstructorMode.SHOW,
});
} else {
const {
unsavedLabels,
savedLabels,
} = this.state;
const newUnsavedLabels = [
...unsavedLabels,
{
...label,
id: idGenerator(),
},
];
this.setState({
unsavedLabels: newUnsavedLabels,
});
this.handleSubmit(savedLabels, newUnsavedLabels);
}
};
private handleUpdate = (label: Label | null): void => {
const {
savedLabels,
unsavedLabels,
} = this.state;
private handleUpdate = (label: Label | null) => {
if (label) {
const savedLabels = this.state.savedLabels
const filteredSavedLabels = savedLabels
.filter((_label: Label) => _label.id !== label.id);
const unsavedLabels = this.state.unsavedLabels
const filteredUnsavedLabels = unsavedLabels
.filter((_label: Label) => _label.id !== label.id);
if (label.id >= 0) {
savedLabels.push(label);
filteredSavedLabels.push(label);
this.setState({
savedLabels,
savedLabels: filteredSavedLabels,
constructorMode: ConstructorMode.SHOW,
});
} else {
unsavedLabels.push(label);
filteredUnsavedLabels.push(label);
this.setState({
unsavedLabels,
unsavedLabels: filteredUnsavedLabels,
constructorMode: ConstructorMode.SHOW,
});
}
this.handleSubmit(savedLabels, unsavedLabels);
this.handleSubmit(filteredSavedLabels, filteredUnsavedLabels);
} else {
this.setState({
constructorMode: ConstructorMode.SHOW,
@ -125,128 +164,132 @@ export default class LabelsEditor
}
};
private handleDelete = (label: Label) => {
private handleDelete = (label: Label): void => {
// the label is saved on the server, cannot delete it
if (typeof(label.id) !== 'undefined' && label.id >= 0) {
if (typeof (label.id) !== 'undefined' && label.id >= 0) {
notification.error({
message: 'Could not delete the label',
description: 'It has been already saved on the server',
});
}
const unsavedLabels = this.state.unsavedLabels.filter(
(_label: Label) => _label.id !== label.id
const {
unsavedLabels,
savedLabels,
} = this.state;
const filteredUnsavedLabels = unsavedLabels.filter(
(_label: Label): boolean => _label.id !== label.id,
);
this.setState({
unsavedLabels: [...unsavedLabels],
unsavedLabels: filteredUnsavedLabels,
});
this.handleSubmit(this.state.savedLabels, unsavedLabels);
};
private handleCreate = (label: Label | null) => {
if (label === null) {
this.setState({
constructorMode: ConstructorMode.SHOW,
});
} else {
const unsavedLabels = [...this.state.unsavedLabels,
{
...label,
id: idGenerator()
}
];
this.setState({
unsavedLabels,
});
this.handleSubmit(this.state.savedLabels, unsavedLabels);
}
this.handleSubmit(savedLabels, filteredUnsavedLabels);
};
public componentDidMount() {
this.componentDidUpdate(null as any as LabelsEditortProps);
}
public componentDidUpdate(prevProps: LabelsEditortProps) {
function transformLabel(label: any): Label {
private handleSubmit(savedLabels: Label[], unsavedLabels: Label[]): void {
function transformLabel(label: Label): any {
return {
name: label.name,
id: label.id || idGenerator(),
attributes: label.attributes.map((attr: any): Attribute => {
return {
id: attr.id || idGenerator(),
id: label.id < 0 ? undefined : label.id,
attributes: label.attributes.map((attr: Attribute): any => (
{
name: attr.name,
type: attr.input_type,
id: attr.id < 0 ? undefined : attr.id,
input_type: attr.type.toLowerCase(),
default_value: attr.values[0],
mutable: attr.mutable,
values: [...attr.values],
};
}),
}
}
)),
};
}
if (!prevProps || prevProps.labels !== this.props.labels) {
const transformedLabels = this.props.labels.map(transformLabel);
this.setState({
savedLabels: transformedLabels
.filter((label: Label) => label.id >= 0),
unsavedLabels: transformedLabels
.filter((label: Label) => label.id < 0),
});
const { onSubmit } = this.props;
const output = [];
for (const label of savedLabels.concat(unsavedLabels)) {
output.push(transformLabel(label));
}
onSubmit(output);
}
public render() {
public render(): JSX.Element {
const {
savedLabels,
unsavedLabels,
constructorMode,
labelForUpdate,
} = this.state;
return (
<Tabs defaultActiveKey='2' type='card' tabBarStyle={{marginBottom: '0px'}}>
<Tabs.TabPane tab={
<span>
<Icon type='edit'/>
<Text>Raw</Text>
</span>
} key='1'>
<Tabs defaultActiveKey='2' type='card' tabBarStyle={{ marginBottom: '0px' }}>
<Tabs.TabPane
tab={
(
<span>
<Icon type='edit' />
<Text>Raw</Text>
</span>
)
}
key='1'
>
<RawViewer
labels={[...this.state.savedLabels, ...this.state.unsavedLabels]}
labels={[...savedLabels, ...unsavedLabels]}
onSubmit={this.handleRawSubmit}
/>
</Tabs.TabPane>
<Tabs.TabPane tab={
<span>
<Icon type='build'/>
<Text>Constructor</Text>
</span>
} key='2'>
<Tabs.TabPane
tab={
(
<span>
<Icon type='build' />
<Text>Constructor</Text>
</span>
)
}
key='2'
>
{
constructorMode === ConstructorMode.SHOW
&& (
<ConstructorViewer
labels={[...savedLabels, ...unsavedLabels]}
onUpdate={(label: Label): void => {
this.setState({
constructorMode: ConstructorMode.UPDATE,
labelForUpdate: label,
});
}}
onDelete={this.handleDelete}
onCreate={(): void => {
this.setState({
constructorMode: ConstructorMode.CREATE,
});
}}
/>
)
}
{
constructorMode === ConstructorMode.UPDATE
&& labelForUpdate !== null && (
<ConstructorUpdater
label={labelForUpdate}
onUpdate={this.handleUpdate}
/>
)
}
{
this.state.constructorMode === ConstructorMode.SHOW ?
<ConstructorViewer
labels={[...this.state.savedLabels, ...this.state.unsavedLabels]}
onUpdate={(label: Label) => {
this.setState({
constructorMode: ConstructorMode.UPDATE,
labelForUpdate: label,
});
}}
onDelete={this.handleDelete}
onCreate={() => {
this.setState({
constructorMode: ConstructorMode.CREATE,
})
}}
/> :
this.state.constructorMode === ConstructorMode.UPDATE
&& this.state.labelForUpdate !== null ?
<ConstructorUpdater
label={this.state.labelForUpdate}
onUpdate={this.handleUpdate}
/> :
<ConstructorCreator
onCreate={this.handleCreate}
/>
constructorMode === ConstructorMode.CREATE
&& (
<ConstructorCreator
onCreate={this.handleCreate}
/>
)
}
</Tabs.TabPane>
</Tabs>

@ -13,27 +13,16 @@ import { FormComponentProps } from 'antd/lib/form/Form';
import {
Label,
Attribute,
} from './common';
type Props = FormComponentProps & {
labels: Label[];
onSubmit: (labels: Label[]) => void;
}
interface State {
valid: boolean;
}
class RawViewer extends React.PureComponent<Props, State> {
public constructor(props: Props) {
super(props);
this.state = {
valid: true,
};
}
};
private validateLabels = (_: any, value: string, callback: any) => {
class RawViewer extends React.PureComponent<Props> {
private validateLabels = (_: any, value: string, callback: any): void => {
try {
JSON.parse(value);
} catch (error) {
@ -41,62 +30,73 @@ class RawViewer extends React.PureComponent<Props, State> {
}
callback();
}
};
private handleSubmit = (e: React.FormEvent): void => {
const {
form,
onSubmit,
} = this.props;
private handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
this.props.form.validateFields((error, values) => {
form.validateFields((error, values): void => {
if (!error) {
this.props.onSubmit(JSON.parse(values.labels));
onSubmit(JSON.parse(values.labels));
}
});
}
};
public render() {
const labels = this.props.labels.map((label: any) => {
return {
public render(): JSX.Element {
const { labels } = this.props;
const convertedLabels = labels.map((label: any): Label => (
{
...label,
id: label.id < 0 ? undefined : label.id,
attributes: label.attributes.map((attribute: any) => {
return {
attributes: label.attributes.map((attribute: any): Attribute => (
{
...attribute,
id: attribute.id < 0 ? undefined : attribute.id,
};
}),
};
});
}
)),
}
));
const textLabels = JSON.stringify(labels, null, 2);
const textLabels = JSON.stringify(convertedLabels, null, 2);
const { form } = this.props;
return (
<Form onSubmit={this.handleSubmit}>
<Form.Item> {
this.props.form.getFieldDecorator('labels', {
<Form.Item>
{form.getFieldDecorator('labels', {
initialValue: textLabels,
rules: [{
validator: this.validateLabels,
}]
})( <Input.TextArea rows={5} className='cvat-raw-labels-viewer'/> )
} </Form.Item>
}],
})(<Input.TextArea rows={5} className='cvat-raw-labels-viewer' />)}
</Form.Item>
<Row type='flex' justify='start' align='middle'>
<Col>
<Tooltip overlay='Save labels and return'>
<Button
style={{width: '150px'}}
style={{ width: '150px' }}
type='primary'
htmlType='submit'
> Done </Button>
>
Done
</Button>
</Tooltip>
</Col>
<Col offset={1}>
<Tooltip overlay='Do not save the label and return'>
<Button
style={{width: '150px'}}
style={{ width: '150px' }}
type='danger'
onClick={() => {
this.props.form.resetFields();
onClick={(): void => {
form.resetFields();
}}
> Reset </Button>
>
Reset
</Button>
</Tooltip>
</Col>
</Row>

@ -18,21 +18,23 @@ type LoginFormProps = {
} & FormComponentProps;
class LoginFormComponent extends React.PureComponent<LoginFormProps> {
constructor(props: LoginFormProps) {
super(props);
}
private handleSubmit = (e: React.FormEvent) => {
private handleSubmit = (e: React.FormEvent): void => {
e.preventDefault();
this.props.form.validateFields((error, values) => {
const {
form,
onSubmit,
} = this.props;
form.validateFields((error, values): void => {
if (!error) {
this.props.onSubmit(values);
onSubmit(values);
}
});
}
};
private renderUsernameField() {
const { getFieldDecorator } = this.props.form;
private renderUsernameField(): JSX.Element {
const { form } = this.props;
const { getFieldDecorator } = form;
return (
<Form.Item hasFeedback>
@ -44,16 +46,17 @@ class LoginFormComponent extends React.PureComponent<LoginFormProps> {
})(
<Input
autoComplete='username'
prefix={<Icon type='user' style={{ color: 'rgba(0,0,0,.25)'}} />}
prefix={<Icon type='user' style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder='Username'
/>
/>,
)}
</Form.Item>
)
);
}
private renderPasswordField() {
const { getFieldDecorator } = this.props.form;
private renderPasswordField(): JSX.Element {
const { form } = this.props;
const { getFieldDecorator } = form;
return (
<Form.Item hasFeedback>
@ -65,16 +68,17 @@ class LoginFormComponent extends React.PureComponent<LoginFormProps> {
})(
<Input
autoComplete='current-password'
prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)'}} />}
prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder='Password'
type='password'
/>
/>,
)}
</Form.Item>
)
);
}
public render() {
public render(): JSX.Element {
const { fetching } = this.props;
return (
<Form onSubmit={this.handleSubmit} className='login-form'>
{this.renderUsernameField()}
@ -83,8 +87,8 @@ class LoginFormComponent extends React.PureComponent<LoginFormProps> {
<Form.Item>
<Button
type='primary'
loading={this.props.fetching}
disabled={this.props.fetching}
loading={fetching}
disabled={fetching}
htmlType='submit'
className='login-form-button'
>

@ -17,26 +17,35 @@ interface LoginPageComponentProps {
onLogin: (username: string, password: string) => void;
}
function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps) {
function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps): JSX.Element {
const sizes = {
xs: { span: 14 },
sm: { span: 14 },
md: { span: 10 },
lg: { span: 4 },
xl: { span: 4 },
}
};
const {
fetching,
onLogin,
} = props;
return (
<Row type='flex' justify='center' align='middle'>
<Col {...sizes}>
<Title level={2}> Login </Title>
<LoginForm fetching={props.fetching} onSubmit={(loginData: LoginData) => {
props.onLogin(loginData.username, loginData.password);
}}/>
<LoginForm
fetching={fetching}
onSubmit={(loginData: LoginData): void => {
onLogin(loginData.username, loginData.password);
}}
/>
<Row type='flex' justify='start' align='top'>
<Col>
<Text strong>
New to CVAT? Create <Link to="/auth/register">an account</Link>
New to CVAT? Create
<Link to='/auth/register'> an account</Link>
</Text>
</Col>
</Row>

@ -41,25 +41,31 @@ interface State {
mapping: StringObject;
colors: StringObject;
matching: {
model: string,
task: string,
model: string;
task: string;
};
}
const nextColor = (function *() {
function colorGenerator(): () => string {
const values = [
'magenta', 'green', 'geekblue',
'orange', 'red', 'cyan',
'blue', 'volcano', 'purple',
];
for (let i = 0; i < values.length; i++) {
yield values[i];
if (i === values.length) {
i = 0;
let index = 0;
return (): string => {
const color = values[index++];
if (index >= values.length) {
index = 0;
}
}
})();
return color;
};
}
const nextColor = colorGenerator();
export default class ModelRunnerModalComponent extends React.PureComponent<Props, State> {
public constructor(props: Props) {
@ -76,51 +82,115 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
};
}
private renderModelSelector() {
public componentDidUpdate(prevProps: Props, prevState: State): void {
const {
taskInstance,
modelsInitialized,
modelsFetching,
models,
visible,
getModels,
} = this.props;
const {
selectedModel,
} = this.state;
if (!modelsInitialized && !modelsFetching) {
getModels();
}
if (!prevProps.visible && visible) {
this.setState({
selectedModel: null,
mapping: {},
matching: {
model: '',
task: '',
},
cleanOut: false,
});
}
if (selectedModel && prevState.selectedModel !== selectedModel) {
const selectedModelInstance = models
.filter((model) => model.name === selectedModel)[0];
if (!selectedModelInstance.primary) {
let taskLabels: string[] = taskInstance.labels
.map((label: any): string => label.name);
const [defaultMapping, defaultColors]: StringObject[] = selectedModelInstance.labels
.reduce((acc: StringObject[], label): StringObject[] => {
if (taskLabels.includes(label)) {
acc[0][label] = label;
acc[1][label] = nextColor();
taskLabels = taskLabels.filter((_label): boolean => _label !== label);
}
return acc;
}, [{}, {}]);
this.setState({
mapping: defaultMapping,
colors: defaultColors,
});
}
}
}
private renderModelSelector(): JSX.Element {
const { models } = this.props;
return (
<Row type='flex' align='middle'>
<Col span={4}>Model:</Col>
<Col span={19}>
<Select
placeholder='Select a model'
style={{width: '100%'}}
onChange={(value: string) => this.setState({
style={{ width: '100%' }}
onChange={(value: string): void => this.setState({
selectedModel: value,
mapping: {},
})
}>
{this.props.models.map((model) =>
})}
>
{models.map((model): JSX.Element => (
<Select.Option key={model.name}>
{model.name}
</Select.Option>
)}
))}
</Select>
</Col>
</Row>
);
}
private renderMappingTag(modelLabel: string, taskLabel: string) {
private renderMappingTag(modelLabel: string, taskLabel: string): JSX.Element {
const {
colors,
mapping,
} = this.state;
return (
<Row key={`${modelLabel}-${taskLabel}`} type='flex' justify='start' align='middle'>
<Col span={10}>
<Tag color={this.state.colors[modelLabel]}>{modelLabel}</Tag>
<Tag color={colors[modelLabel]}>{modelLabel}</Tag>
</Col>
<Col span={10} offset={1}>
<Tag color={this.state.colors[modelLabel]}>{taskLabel}</Tag>
<Tag color={colors[modelLabel]}>{taskLabel}</Tag>
</Col>
<Col span={1} offset={1}>
<Tooltip overlay='Remove the mapped values'>
<Icon
className='cvat-run-model-dialog-remove-mapping-icon'
type='close-circle'
onClick={() => {
const mapping = {...this.state.mapping};
delete mapping[modelLabel];
this.setState({
mapping,
});
}}/>
onClick={(): void => {
const newMapping = { ...mapping };
delete newMapping[modelLabel];
this.setState({
mapping: newMapping,
});
}}
/>
</Tooltip>
</Col>
</Row>
@ -130,106 +200,126 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
private renderMappingInputSelector(
value: string,
current: string,
options: string[]
) {
options: string[],
): JSX.Element {
const {
matching,
mapping,
colors,
} = this.state;
return (
<Select
value={value}
placeholder={`${current} labels`}
style={{width: '100%'}}
onChange={(value: string) => {
const anotherValue = current === 'Model' ?
this.state.matching.task : this.state.matching.model;
style={{ width: '100%' }}
onChange={(selectedValue: string): void => {
const anotherValue = current === 'Model'
? matching.task : matching.model;
if (!anotherValue) {
const matching = { ...this.state.matching };
const newMatching = { ...matching };
if (current === 'Model') {
matching.model = value;
newMatching.model = selectedValue;
} else {
matching.task = value;
newMatching.task = selectedValue;
}
this.setState({
matching,
matching: newMatching,
});
} else {
const colors = {...this.state.colors};
const mapping = {...this.state.mapping};
const newColors = { ...colors };
const newMapping = { ...mapping };
if (current === 'Model') {
colors[value] = nextColor.next().value;
mapping[value] = anotherValue;
newColors[selectedValue] = nextColor();
newMapping[selectedValue] = anotherValue;
} else {
colors[anotherValue] = nextColor.next().value;
mapping[anotherValue] = value;
newColors[anotherValue] = nextColor();
newMapping[anotherValue] = selectedValue;
}
this.setState({
colors,
mapping,
colors: newColors,
mapping: newMapping,
matching: {
task: '',
model: '',
},
})
});
}
}}
>
{options.map((label: string) =>
{options.map((label: string): JSX.Element => (
<Select.Option key={label}>
{label}
</Select.Option>
)}
))}
</Select>
);
}
private renderMappingInput(availableModelLabels: string[], availableTaskLabels: string[]) {
private renderMappingInput(
availableModelLabels: string[],
availableTaskLabels: string[],
): JSX.Element {
const { matching } = this.state;
return (
<Row type='flex' justify='start' align='middle'>
<Col span={10}>
{this.renderMappingInputSelector(
this.state.matching.model,
matching.model,
'Model',
availableModelLabels,
)}
</Col>
<Col span={10} offset={1}>
{this.renderMappingInputSelector(
this.state.matching.task,
matching.task,
'Task',
availableTaskLabels,
)}
</Col>
<Col span={1} offset={1}>
<Tooltip overlay='Specify a label mapping between model labels and task labels'>
<Icon className='cvat-run-model-dialog-info-icon' type='question-circle'/>
<Icon className='cvat-run-model-dialog-info-icon' type='question-circle' />
</Tooltip>
</Col>
</Row>
);
}
private renderContent() {
const model = this.state.selectedModel && this.props.models
.filter((model) => model.name === this.state.selectedModel)[0];
private renderContent(): JSX.Element {
const {
selectedModel,
cleanOut,
mapping,
} = this.state;
const {
models,
taskInstance,
} = this.props;
const model = selectedModel && models
.filter((_model): boolean => _model.name === selectedModel)[0];
const excludedLabels: {
model: string[],
task: string[],
model: string[];
task: string[];
} = {
model: [],
task: [],
};
const withMapping = model && !model.primary;
const tags = withMapping ? Object.keys(this.state.mapping)
const tags = withMapping ? Object.keys(mapping)
.map((modelLabel: string) => {
const taskLabel = this.state.mapping[modelLabel];
const taskLabel = mapping[modelLabel];
excludedLabels.model.push(modelLabel);
excludedLabels.task.push(taskLabel);
return this.renderMappingTag(
modelLabel,
this.state.mapping[modelLabel],
mapping[modelLabel],
);
}) : [];
@ -237,10 +327,10 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
.filter(
(label: string) => !excludedLabels.model.includes(label),
) : [];
const availableTaskLabels = this.props.taskInstance.labels
const availableTaskLabels = taskInstance.labels
.map(
(label: any) => label.name,
).filter((label: string) => !excludedLabels.task.includes(label))
).filter((label: string): boolean => !excludedLabels.task.includes(label));
const mappingISAvailable = !!availableModelLabels.length
&& !!availableTaskLabels.length;
@ -253,97 +343,73 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
&& mappingISAvailable
&& this.renderMappingInput(availableModelLabels, availableTaskLabels)
}
{ withMapping &&
<div>
<Checkbox
checked={this.state.cleanOut}
onChange={(e: any) => this.setState({
cleanOut: e.target.checked,
})}
> Clean old annotations </Checkbox>
</div>
{ withMapping
&& (
<div>
<Checkbox
checked={cleanOut}
onChange={(e: any): void => this.setState({
cleanOut: e.target.checked,
})}
>
Clean old annotations
</Checkbox>
</div>
)
}
</div>
);
}
private renderSpin() {
return (
<Spin size='large' style={{margin: '25% 50%'}}/>
);
}
public componentDidUpdate(prevProps: Props, prevState: State) {
if (!this.props.modelsInitialized && !this.props.modelsFetching) {
this.props.getModels();
}
if (!prevProps.visible && this.props.visible) {
this.setState({
selectedModel: null,
mapping: {},
matching: {
model: '',
task: '',
},
cleanOut: false,
});
}
if (this.state.selectedModel && prevState.selectedModel !== this.state.selectedModel) {
const model = this.props.models
.filter((model) => model.name === this.state.selectedModel)[0];
if (!model.primary) {
let taskLabels: string[] = this.props.taskInstance.labels
.map((label: any) => label.name);
const defaultMapping: StringObject = model.labels
.reduce((acc: StringObject, label) => {
if (taskLabels.includes(label)) {
acc[label] = label;
taskLabels = taskLabels.filter((_label) => _label !== label)
}
return acc;
}, {});
public render(): JSX.Element | false {
const {
selectedModel,
mapping,
cleanOut,
} = this.state;
this.setState({
mapping: defaultMapping,
});
}
}
}
const {
models,
visible,
taskInstance,
modelsInitialized,
runInference,
closeDialog,
} = this.props;
public render() {
const activeModel = this.props.models.filter(
(model) => model.name === this.state.selectedModel
const activeModel = models.filter(
(model): boolean => model.name === selectedModel,
)[0];
let enabledSubmit = !!activeModel
&& activeModel.primary || !!Object.keys(this.state.mapping).length;
const enabledSubmit = (!!activeModel
&& activeModel.primary) || !!Object.keys(mapping).length;
return (
this.props.visible && <Modal
closable={false}
okType='danger'
okText='Submit'
onOk={() => {
this.props.runInference(
this.props.taskInstance,
this.props.models
.filter((model) => model.name === this.state.selectedModel)[0],
this.state.mapping,
this.state.cleanOut,
);
this.props.closeDialog()
}}
onCancel={() => this.props.closeDialog()}
okButtonProps={{disabled: !enabledSubmit}}
title='Automatic annotation'
visible={true}
>
{!this.props.modelsInitialized && this.renderSpin()}
{this.props.modelsInitialized && this.renderContent()}
</Modal>
visible && (
<Modal
closable={false}
okType='danger'
okText='Submit'
onOk={(): void => {
runInference(
taskInstance,
models
.filter((model): boolean => model.name === selectedModel)[0],
mapping,
cleanOut,
);
closeDialog();
}}
onCancel={(): void => closeDialog()}
okButtonProps={{ disabled: !enabledSubmit }}
title='Automatic annotation'
visible
>
{!modelsInitialized
&& <Spin size='large' style={{ margin: '25% 50%' }} />}
{modelsInitialized && this.renderContent()}
</Modal>
)
);
}
}
}

@ -15,7 +15,9 @@ interface Props {
model: Model;
}
export default function BuiltModelItemComponent(props: Props) {
export default function BuiltModelItemComponent(props: Props): JSX.Element {
const { model } = props;
return (
<Row className='cvat-models-list-item' type='flex'>
<Col span={4} xxl={3}>
@ -23,24 +25,26 @@ export default function BuiltModelItemComponent(props: Props) {
</Col>
<Col span={6} xxl={7}>
<Text className='cvat-black-color'>
{props.model.name}
{model.name}
</Text>
</Col>
<Col span={5} offset={7}>
<Select
showSearch
placeholder='Supported labels'
style={{width: '90%'}}
style={{ width: '90%' }}
value='Supported labels'
>
{props.model.labels.map(
(label) => <Select.Option key={label}>
{label}
</Select.Option>)
}
{model.labels.map(
(label): JSX.Element => (
<Select.Option key={label}>
{label}
</Select.Option>
),
)}
</Select>
</Col>
<Col span={2}/>
<Col span={2} />
</Row>
);
}

@ -14,10 +14,11 @@ interface Props {
models: Model[];
}
export default function IntegratedModelsListComponent(props: Props) {
const items = props.models.map((model) =>
<BuiltModelItemComponent key={model.name} model={model}/>
);
export default function IntegratedModelsListComponent(props: Props): JSX.Element {
const { models } = props;
const items = models.map((model): JSX.Element => (
<BuiltModelItemComponent key={model.name} model={model} />
));
return (
<>
@ -28,12 +29,12 @@ export default function IntegratedModelsListComponent(props: Props) {
</Row>
<Row type='flex' justify='center' align='middle'>
<Col md={22} lg={18} xl={16} xxl={14} className='cvat-models-list'>
<Row type='flex' align='middle' style={{padding: '10px'}}>
<Row type='flex' align='middle' style={{ padding: '10px' }}>
<Col span={4} xxl={3}>
<Text strong>{'Framework'}</Text>
<Text strong>Framework</Text>
</Col>
<Col span={6} xxl={7}>
<Text strong>{'Name'}</Text>
<Text strong>Name</Text>
</Col>
<Col span={5} offset={7}>
<Text strong>Labels</Text>

@ -8,32 +8,31 @@ import {
Icon,
} from 'antd';
export default function EmptyListComponent() {
const emptyTasksIcon = () => (<img src='/assets/empty-tasks-icon.svg'/>);
export default function EmptyListComponent(): JSX.Element {
const emptyTasksIcon = (): JSX.Element => (<img alt='' src='/assets/empty-tasks-icon.svg' />);
return (
<div className='cvat-empty-models-list'>
<Row type='flex' justify='center' align='middle'>
<Col>
<Icon className='cvat-empty-models-icon' component={emptyTasksIcon}/>
<Icon className='cvat-empty-models-icon' component={emptyTasksIcon} />
</Col>
</Row>
<Row type='flex' justify='center' align='middle'>
<Col>
<Text strong>{'No models uploaded yet ...'}</Text>
<Text strong>No models uploaded yet ...</Text>
</Col>
</Row>
<Row type='flex' justify='center' align='middle'>
<Col>
<Text type='secondary'>{'To annotate your tasks automatically'}</Text>
<Text type='secondary'>To annotate your tasks automatically</Text>
</Col>
</Row>
<Row type='flex' justify='center' align='middle'>
<Row type='flex' justify='center' align='middle'>
<Col>
<Link to='/models/create'>{'upload a new model'}</Link>
<Link to='/models/create'>upload a new model</Link>
</Col>
</Row>
</div>
)
}
);
}

@ -22,36 +22,48 @@ interface Props {
deleteModel(id: number): void;
}
export default function ModelsPageComponent(props: Props) {
if (!props.modelsInitialized && !props.modelsFetching) {
props.getModels();
return (
<Spin size='large' style={{margin: '25% 45%'}}/>
);
} else {
const uploadedModels = props.models.filter((model) => model.id !== null);
const integratedModels = props.models.filter((model) => model.id === null);
export default function ModelsPageComponent(props: Props): JSX.Element {
const {
installedAutoAnnotation,
installedTFSegmentation,
installedTFAnnotation,
modelsInitialized,
modelsFetching,
registeredUsers,
models,
deleteModel,
} = props;
if (!modelsInitialized && !modelsFetching) {
props.getModels();
return (
<div className='cvat-models-page'>
<TopBarComponent installedAutoAnnotation={props.installedAutoAnnotation}/>
{ !!integratedModels.length &&
<BuiltModelsList models={integratedModels}/>
}
{ !!uploadedModels.length &&
<UploadedModelsList
registeredUsers={props.registeredUsers}
models={uploadedModels}
deleteModel={props.deleteModel}
/>
}
{ props.installedAutoAnnotation &&
!uploadedModels.length &&
!props.installedTFAnnotation &&
!props.installedTFSegmentation &&
<EmptyListComponent/>
}
</div>
<Spin size='large' style={{ margin: '25% 45%' }} />
);
}
const uploadedModels = models.filter((model): boolean => model.id !== null);
const integratedModels = models.filter((model): boolean => model.id === null);
return (
<div className='cvat-models-page'>
<TopBarComponent installedAutoAnnotation={installedAutoAnnotation} />
{ !!integratedModels.length
&& <BuiltModelsList models={integratedModels} />
}
{ !!uploadedModels.length && (
<UploadedModelsList
registeredUsers={registeredUsers}
models={uploadedModels}
deleteModel={deleteModel}
/>
)}
{ installedAutoAnnotation
&& !uploadedModels.length
&& !installedTFAnnotation
&& !installedTFSegmentation
&& <EmptyListComponent />
}
</div>
);
}

@ -14,26 +14,40 @@ type Props = {
installedAutoAnnotation: boolean;
} & RouteComponentProps;
function TopBarComponent(props: Props) {
function TopBarComponent(props: Props): JSX.Element {
const {
installedAutoAnnotation,
history,
} = props;
return (
<Row type='flex' justify='center' align='middle'>
<Col md={11} lg={9} xl={8} xxl={7}>
<Text className='cvat-title'>Models</Text>
</Col>
<Col
md={{span: 11}}
lg={{span: 9}}
xl={{span: 8}}
xxl={{span: 7}}
md={{ span: 11 }}
lg={{ span: 9 }}
xl={{ span: 8 }}
xxl={{ span: 7 }}
>
{ props.installedAutoAnnotation &&
<Button size='large' id='cvat-create-model-button' type='primary' onClick={
() => props.history.push('/models/create')
}> Create new model </Button>
{ installedAutoAnnotation
&& (
<Button
size='large'
id='cvat-create-model-button'
type='primary'
onClick={
(): void => history.push('/models/create')
}
>
Create new model
</Button>
)
}
</Col>
</Row>
)
);
}
export default withRouter(TopBarComponent);

@ -21,8 +21,13 @@ interface Props {
onDelete(): void;
}
export default function UploadedModelItem(props: Props) {
const subMenuIcon = () => (<img src='/assets/icon-sub-menu.svg'/>);
export default function UploadedModelItem(props: Props): JSX.Element {
const subMenuIcon = (): JSX.Element => (<img alt='' src='/assets/icon-sub-menu.svg' />);
const {
model,
owner,
onDelete,
} = props;
return (
<Row className='cvat-models-list-item' type='flex'>
@ -31,43 +36,52 @@ export default function UploadedModelItem(props: Props) {
</Col>
<Col span={6} xxl={7}>
<Text className='cvat-black-color'>
{props.model.name}
{model.name}
</Text>
</Col>
<Col span={3}>
<Text className='cvat-black-color'>
{props.owner ? props.owner.username : 'undefined'}
{owner ? owner.username : 'undefined'}
</Text>
</Col>
<Col span={4}>
<Text className='cvat-black-color'>
{moment(props.model.uploadDate).format('MMMM Do YYYY')}
{moment(model.uploadDate).format('MMMM Do YYYY')}
</Text>
</Col>
<Col span={5}>
<Select
showSearch
placeholder='Supported labels'
style={{width: '90%'}}
style={{ width: '90%' }}
value='Supported labels'
>
{props.model.labels.map(
(label) => <Select.Option key={label}>
{label}
</Select.Option>)
}
{model.labels.map(
(label): JSX.Element => (
<Select.Option key={label}>
{label}
</Select.Option>
),
)}
</Select>
</Col>
<Col span={2}>
<Text className='cvat-black-color'>Actions</Text>
<Dropdown overlay={
(
<Menu className='cvat-task-item-menu'>
<Menu.Item onClick={() => {
props.onDelete();
}}key='delete'>Delete</Menu.Item>
<Menu.Item
onClick={(): void => {
onDelete();
}}
key='delete'
>
Delete
</Menu.Item>
</Menu>
}>
<Icon className='cvat-task-item-menu-icon' component={subMenuIcon}/>
)}
>
<Icon className='cvat-task-item-menu-icon' component={subMenuIcon} />
</Dropdown>
</Col>
</Row>

@ -16,15 +16,21 @@ interface Props {
deleteModel(id: number): void;
}
export default function UploadedModelsListComponent(props: Props) {
const items = props.models.map((model) => {
const owner = props.registeredUsers.filter((user) => user.id === model.ownerID)[0];
export default function UploadedModelsListComponent(props: Props): JSX.Element {
const {
models,
registeredUsers,
deleteModel,
} = props;
const items = models.map((model): JSX.Element => {
const owner = registeredUsers.filter((user) => user.id === model.ownerID)[0];
return (
<UploadedModelItem
key={model.id as number}
owner={owner}
model={model}
onDelete={() => props.deleteModel(model.id as number)}
onDelete={(): void => deleteModel(model.id as number)}
/>
);
});
@ -33,17 +39,17 @@ export default function UploadedModelsListComponent(props: Props) {
<>
<Row type='flex' justify='center' align='middle'>
<Col md={22} lg={18} xl={16} xxl={14}>
<Text className='cvat-black-color' strong>{'Uploaded by a user'}</Text>
<Text className='cvat-black-color' strong>Uploaded by a user</Text>
</Col>
</Row>
<Row type='flex' justify='center' align='middle'>
<Col md={22} lg={18} xl={16} xxl={14} className='cvat-models-list'>
<Row type='flex' align='middle' style={{padding: '10px'}}>
<Row type='flex' align='middle' style={{ padding: '10px' }}>
<Col span={4} xxl={3}>
<Text strong>{'Framework'}</Text>
<Text strong>Framework</Text>
</Col>
<Col span={6} xxl={7}>
<Text strong>{'Name'}</Text>
<Text strong>Name</Text>
</Col>
<Col span={3}>
<Text strong>Owner</Text>
@ -54,7 +60,7 @@ export default function UploadedModelsListComponent(props: Props) {
<Col span={5}>
<Text strong>Labels</Text>
</Col>
<Col span={2}/>
<Col span={2} />
</Row>
{ items }
</Col>

@ -7,6 +7,8 @@ import {
Form,
} from 'antd';
import patterns from '../../utils/validation-patterns';
export interface RegisterData {
username: string;
firstName: string;
@ -16,28 +18,22 @@ export interface RegisterData {
password2: string;
}
import patterns from '../../utils/validation-patterns';
type RegisterFormProps = {
fetching: boolean;
onSubmit(registerData: RegisterData): void;
} & FormComponentProps;
class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
constructor(props: RegisterFormProps) {
super(props);
}
private validateConfirmation = (rule: any, value: any, callback: any) => {
private validateConfirmation = (rule: any, value: any, callback: any): void => {
const { form } = this.props;
if (value && value !== form.getFieldValue('password1')) {
callback('Two passwords that you enter is inconsistent!');
callback('Two passwords that you enter is inconsistent!');
} else {
callback();
callback();
}
};
};
private validatePassword = (_: any, value: any, callback: any) => {
private validatePassword = (_: any, value: any, callback: any): void => {
const { form } = this.props;
if (!patterns.validatePasswordLength.pattern.test(value)) {
callback(patterns.validatePasswordLength.message);
@ -56,12 +52,12 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}
if (value) {
form.validateFields(['password2'], { force: true });
form.validateFields(['password2'], { force: true });
}
callback();
};
private validateUsername = (_: any, value: any, callback: any) => {
private validateUsername = (_: any, value: any, callback: any): void => {
if (!patterns.validateUsernameLength.pattern.test(value)) {
callback(patterns.validateUsernameLength.message);
}
@ -73,19 +69,26 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
callback();
};
private handleSubmit = (e: React.FormEvent) => {
private handleSubmit = (e: React.FormEvent): void => {
e.preventDefault();
this.props.form.validateFields((error, values) => {
const {
form,
onSubmit,
} = this.props;
form.validateFields((error, values): void => {
if (!error) {
this.props.onSubmit(values);
onSubmit(values);
}
});
}
};
private renderFirstNameField(): JSX.Element {
const { form } = this.props;
private renderFirstNameField() {
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('firstName', {
{form.getFieldDecorator('firstName', {
rules: [{
required: true,
message: 'Please specify a first name',
@ -93,18 +96,20 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}],
})(
<Input
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)'}} />}
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder='First name'
/>
/>,
)}
</Form.Item>
)
);
}
private renderLastNameField() {
private renderLastNameField(): JSX.Element {
const { form } = this.props;
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('lastName', {
{form.getFieldDecorator('lastName', {
rules: [{
required: true,
message: 'Please specify a last name',
@ -112,18 +117,20 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}],
})(
<Input
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)'}} />}
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder='Last name'
/>
/>,
)}
</Form.Item>
)
);
}
private renderUsernameField() {
private renderUsernameField(): JSX.Element {
const { form } = this.props;
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('username', {
{form.getFieldDecorator('username', {
rules: [{
required: true,
message: 'Please specify a username',
@ -132,40 +139,44 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}],
})(
<Input
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)'}} />}
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder='Username'
/>
/>,
)}
</Form.Item>
)
);
}
private renderEmailField() {
private renderEmailField(): JSX.Element {
const { form } = this.props;
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('email', {
{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)'}} />}
prefix={<Icon type='mail' style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder='Email address'
/>
/>,
)}
</Form.Item>
)
);
}
private renderPasswordField() {
private renderPasswordField(): JSX.Element {
const { form } = this.props;
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('password1', {
{form.getFieldDecorator('password1', {
rules: [{
required: true,
message: 'Please input your password!',
@ -174,17 +185,19 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}],
})(<Input.Password
autoComplete='new-password'
prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)'}} />}
prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder='Password'
/>)}
</Form.Item>
)
);
}
private renderPasswordConfirmationField() {
private renderPasswordConfirmationField(): JSX.Element {
const { form } = this.props;
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('password2', {
{form.getFieldDecorator('password2', {
rules: [{
required: true,
message: 'Please confirm your password!',
@ -193,16 +206,16 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}],
})(<Input.Password
autoComplete='new-password'
prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)'}} />}
prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder='Confirm password'
/>)}
</Form.Item>
)
);
}
public render(): JSX.Element {
const { fetching } = this.props;
public render() {
return (
<Form onSubmit={this.handleSubmit} className='login-form'>
{this.renderFirstNameField()}
@ -217,8 +230,8 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
type='primary'
htmlType='submit'
className='register-form-button'
loading={this.props.fetching}
disabled={this.props.fetching}
loading={fetching}
disabled={fetching}
>
Submit
</Button>
@ -228,4 +241,4 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}
}
export default Form.create<RegisterFormProps>()(RegisterFormComponent);
export default Form.create<RegisterFormProps>()(RegisterFormComponent);

@ -10,7 +10,7 @@ import {
Row,
} from 'antd';
import RegisterForm, { RegisterData } from '../../components/register-page/register-form';
import RegisterForm, { RegisterData } from './register-form';
interface RegisterPageComponentProps {
fetching: boolean;
@ -19,33 +19,44 @@ interface RegisterPageComponentProps {
password1: string, password2: string) => void;
}
function RegisterPageComponent(props: RegisterPageComponentProps & RouteComponentProps) {
function RegisterPageComponent(
props: RegisterPageComponentProps & RouteComponentProps,
): JSX.Element {
const sizes = {
xs: { span: 14 },
sm: { span: 14 },
md: { span: 10 },
lg: { span: 4 },
xl: { span: 4 },
}
};
const {
fetching,
onRegister,
} = props;
return (
<Row type='flex' justify='center' align='middle'>
<Col {...sizes}>
<Title level={2}> Create an account </Title>
<RegisterForm fetching={props.fetching} onSubmit={(registerData: RegisterData) => {
props.onRegister(
registerData.username,
registerData.firstName,
registerData.lastName,
registerData.email,
registerData.password1,
registerData.password2,
);
}}/>
<RegisterForm
fetching={fetching}
onSubmit={(registerData: RegisterData): void => {
onRegister(
registerData.username,
registerData.firstName,
registerData.lastName,
registerData.email,
registerData.password1,
registerData.password2,
);
}}
/>
<Row type='flex' justify='start' align='top'>
<Col>
<Text strong>
Already have an account? <Link to="/auth/login"> Login </Link>
Already have an account?
<Link to='/auth/login'> Login </Link>
</Text>
</Col>
</Row>

@ -55,36 +55,91 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
};
}
private renderTaskName() {
public componentDidMount(): void {
const { taskInstance } = this.props;
this.mounted = true;
getReposData(taskInstance.id)
.then((data): void => {
if (data !== null && this.mounted) {
if (data.status.error) {
notification.error({
message: 'Could not receive repository status',
description: data.status.error,
});
} else {
this.setState({
repositoryStatus: data.status.value,
});
}
this.setState({
repository: data.url,
});
}
}).catch((error): void => {
if (this.mounted) {
notification.error({
message: 'Could not receive repository status',
description: error.toString(),
});
}
});
}
public componentDidUpdate(prevProps: Props): void {
const { taskInstance } = this.props;
if (prevProps !== this.props) {
this.setState({
name: taskInstance.name,
bugTracker: taskInstance.bugTracker,
});
}
}
public componentWillUnmount(): void {
this.mounted = false;
}
private renderTaskName(): JSX.Element {
const { name } = this.state;
const {
taskInstance,
onTaskUpdate,
} = this.props;
return (
<Title
level={4}
editable={{
onChange: (value: string) => {
onChange: (value: string): void => {
this.setState({
name: value,
});
taskInstance.name = value;
this.props.onTaskUpdate(taskInstance);
onTaskUpdate(taskInstance);
},
}}
className='cvat-black-color'
>{name}</Title>
>
{name}
</Title>
);
}
private renderPreview() {
private renderPreview(): JSX.Element {
const { previewImage } = this.props;
return (
<div className='cvat-task-preview-wrapper'>
<img alt='Preview' className='cvat-task-preview' src={this.props.previewImage}/>
<img alt='Preview' className='cvat-task-preview' src={previewImage} />
</div>
);
}
private renderParameters() {
private renderParameters(): JSX.Element {
const { taskInstance } = this.props;
const { overlap } = taskInstance;
const { segmentSize } = taskInstance;
@ -95,25 +150,25 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
<>
<Row type='flex' justify='start' align='middle'>
<Col span={12}>
<Text strong className='cvat-black-color'>{'Overlap size'}</Text>
<br/>
<Text strong className='cvat-black-color'>Overlap size</Text>
<br />
<Text className='cvat-black-color'>{overlap}</Text>
</Col>
<Col span={12}>
<Text strong className='cvat-black-color'>{'Segment size'}</Text>
<br/>
<Text strong className='cvat-black-color'>Segment size</Text>
<br />
<Text className='cvat-black-color'>{segmentSize}</Text>
</Col>
</Row>
<Row type='flex' justify='space-between' align='middle'>
<Col span={12}>
<Text strong className='cvat-black-color'>{'Image quality'}</Text>
<br/>
<Text strong className='cvat-black-color'>Image quality</Text>
<br />
<Text className='cvat-black-color'>{imageQuality}</Text>
</Col>
<Col span={12}>
<Text strong className='cvat-black-color'>{'Z-order'}</Text>
<br/>
<Text strong className='cvat-black-color'>Z-order</Text>
<br />
<Text className='cvat-black-color'>{zOrder}</Text>
</Col>
</Row>
@ -121,39 +176,47 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
);
}
private renderUsers() {
const { taskInstance } = this.props;
private renderUsers(): JSX.Element {
const {
taskInstance,
registeredUsers,
onTaskUpdate,
} = this.props;
const owner = taskInstance.owner ? taskInstance.owner.username : null;
const assignee = taskInstance.assignee ? taskInstance.assignee.username : null;
const created = moment(taskInstance.createdDate).format('MMMM Do YYYY');
const assigneeSelect = <UserSelector
users={this.props.registeredUsers}
value={assignee}
onChange={
(value: string) => {
let [userInstance] = this.props.registeredUsers
.filter((user: any) => user.username === value);
if (userInstance === undefined) {
userInstance = null;
}
const assigneeSelect = (
<UserSelector
users={registeredUsers}
value={assignee}
onChange={
(value: string): void => {
let [userInstance] = registeredUsers
.filter((user: any) => user.username === value);
if (userInstance === undefined) {
userInstance = null;
}
taskInstance.assignee = userInstance;
this.props.onTaskUpdate(taskInstance);
taskInstance.assignee = userInstance;
onTaskUpdate(taskInstance);
}
}
}
/>
/>
);
return (
<Row type='flex' justify='space-between' align='middle'>
<Col span={12}>
{ owner ? <Text type='secondary'>
Created by {owner} on {created}
</Text> : null }
{ owner && (
<Text type='secondary'>
{`Created by ${owner} on ${created}`}
</Text>
)}
</Col>
<Col span={10}>
<Text type='secondary'>
{'Assigned to'}
Assigned to
{ assigneeSelect }
</Text>
</Col>
@ -161,57 +224,89 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
);
}
private renderDatasetRepository() {
const { repository } = this.state;
const { repositoryStatus } = this.state;
private renderDatasetRepository(): JSX.Element | boolean {
const { taskInstance } = this.props;
const {
repository,
repositoryStatus,
} = this.state;
return (
repository ?
<Row>
<Col className='cvat-dataset-repository-url'>
<Text strong className='cvat-black-color'>{'Dataset Repository'}</Text>
<br/>
<a href={repository} target='_blank'>{repository}</a>
{repositoryStatus === 'sync' ?
<Tag color='blue'>
<Icon type='check-circle'/> Synchronized
</Tag> : repositoryStatus === 'merged' ?
<Tag color='green'>
<Icon type='check-circle'/> Merged
</Tag> : repositoryStatus === 'syncing' ?
<Tag color='purple'>
<Icon type='loading'/> Syncing </Tag> :
<Tag color='red' onClick={() => {
this.setState({
repositoryStatus: 'syncing',
});
syncRepos(this.props.taskInstance.id).then(() => {
if (this.mounted) {
this.setState({
repositoryStatus: 'sync',
});
}
}).catch(() => {
if (this.mounted) {
this.setState({
repositoryStatus: '!sync',
});
}
});
}}> <Icon type='warning'/> Synchronize </Tag>
}
</Col>
</Row> : null
!!repository
&& (
<Row>
<Col className='cvat-dataset-repository-url'>
<Text strong className='cvat-black-color'>Dataset Repository</Text>
<br />
<a href={repository} rel='noopener noreferrer' target='_blank'>{repository}</a>
{repositoryStatus === 'sync'
&& (
<Tag color='blue'>
<Icon type='check-circle' />
Synchronized
</Tag>
)
}
{repositoryStatus === 'merged'
&& (
<Tag color='green'>
<Icon type='check-circle' />
Merged
</Tag>
)
}
{repositoryStatus === 'syncing'
&& (
<Tag color='purple'>
<Icon type='loading' />
Syncing
</Tag>
)
}
{repositoryStatus === '!sync'
&& (
<Tag
color='red'
onClick={(): void => {
this.setState({
repositoryStatus: 'syncing',
});
syncRepos(taskInstance.id).then((): void => {
if (this.mounted) {
this.setState({
repositoryStatus: 'sync',
});
}
}).catch((): void => {
if (this.mounted) {
this.setState({
repositoryStatus: '!sync',
});
}
});
}}
>
<Icon type='warning' />
Synchronize
</Tag>
)
}
</Col>
</Row>
)
);
}
private renderBugTracker() {
const { taskInstance } = this.props;
private renderBugTracker(): JSX.Element {
const {
taskInstance,
onTaskUpdate,
} = this.props;
const { bugTracker } = this.state;
let shown = false;
const onChangeValue = (value: string) => {
const onChangeValue = (value: string): void => {
if (value && !patterns.validateURL.pattern.test(value)) {
if (!shown) {
Modal.error({
@ -229,52 +324,62 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
});
taskInstance.bugTracker = value;
this.props.onTaskUpdate(taskInstance);
onTaskUpdate(taskInstance);
}
}
};
if (bugTracker) {
return (
<Row>
<Col>
<Text strong className='cvat-black-color'>{'Issue Tracker'}</Text>
<br/>
<Text editable={{onChange: onChangeValue}}>{bugTracker}</Text>
<Button type='ghost' size='small' onClick={() => {
window.open(bugTracker, '_blank');
}} className='cvat-open-bug-tracker-button'>{'Open the issue'}</Button>
</Col>
</Row>
);
} else {
return (
<Row>
<Col>
<Text strong className='cvat-black-color'>{'Issue Tracker'}</Text>
<br/>
<Text editable={{onChange: onChangeValue}}>{'Not specified'}</Text>
<Text strong className='cvat-black-color'>Issue Tracker</Text>
<br />
<Text editable={{ onChange: onChangeValue }}>{bugTracker}</Text>
<Button
type='ghost'
size='small'
onClick={(): void => {
// false positive
// eslint-disable-next-line
window.open(bugTracker, '_blank');
}}
className='cvat-open-bug-tracker-button'
>
Open the issue
</Button>
</Col>
</Row>
);
}
return (
<Row>
<Col>
<Text strong className='cvat-black-color'>Issue Tracker</Text>
<br />
<Text editable={{ onChange: onChangeValue }}>Not specified</Text>
</Col>
</Row>
);
}
private renderLabelsEditor() {
const { taskInstance } = this.props;
private renderLabelsEditor(): JSX.Element {
const {
taskInstance,
onTaskUpdate,
} = this.props;
return (
<Row>
<Col>
<LabelsEditorComponent
labels={taskInstance.labels.map(
(label: any) => label.toJSON()
(label: any): string => label.toJSON(),
)}
onSubmit={(labels: any[]) => {
taskInstance.labels = labels.map((labelData) => {
return new core.classes.Label(labelData);
});
this.props.onTaskUpdate(taskInstance);
onSubmit={(labels: any[]): void => {
taskInstance.labels = labels
.map((labelData): any => new core.classes.Label(labelData));
onTaskUpdate(taskInstance);
}}
/>
</Col>
@ -282,50 +387,7 @@ export default class DetailsComponent extends React.PureComponent<Props, State>
);
}
public componentDidMount() {
this.mounted = true;
getReposData(this.props.taskInstance.id)
.then((data) => {
if (data !== null && this.mounted) {
if (data.status.error) {
notification.error({
message: 'Could not receive repository status',
description: data.status.error
});
} else {
this.setState({
repositoryStatus: data.status.value,
});
}
this.setState({
repository: data.url,
});
}
}).catch((error) => {
if (this.mounted) {
notification.error({
message: 'Could not receive repository status',
description: error.toString(),
});
}
});
}
public componentWillUnmount() {
this.mounted = false;
}
public componentDidUpdate(prevProps: Props) {
if (prevProps !== this.props) {
this.setState({
name: this.props.taskInstance.name,
bugTracker: this.props.taskInstance.bugTracker,
});
}
}
public render() {
public render(): JSX.Element {
return (
<div className='cvat-task-details'>
<Row type='flex' justify='start' align='middle'>

@ -24,17 +24,19 @@ interface Props {
onJobUpdate(jobInstance: any): void;
}
export default function JobListComponent(props: Props) {
const { jobs } = props.taskInstance;
export default function JobListComponent(props: Props): JSX.Element {
const {
taskInstance,
registeredUsers,
onJobUpdate,
} = props;
const { jobs } = taskInstance;
const columns = [{
title: 'Job',
dataIndex: 'job',
key: 'job',
render: (id: number) => {
return (
<a href={`${baseURL}/?id=${id}`}>{ `Job #${id++}` }</a>
);
}
render: (id: number): JSX.Element => (<a href={`${baseURL}/?id=${id}`}>{ `Job #${id}` }</a>),
}, {
title: 'Frames',
dataIndex: 'frames',
@ -44,14 +46,20 @@ export default function JobListComponent(props: Props) {
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status: string) => {
const progressColor = status === 'completed' ? 'cvat-job-completed-color':
status === 'validation' ? 'cvat-job-validation-color' : 'cvat-job-annotation-color';
render: (status: string): JSX.Element => {
let progressColor = null;
if (status === 'completed') {
progressColor = 'cvat-job-completed-color';
} else if (status === 'validation') {
progressColor = 'cvat-job-validation-color';
} else {
progressColor = 'cvat-job-annotation-color';
}
return (
<Text strong className={progressColor}>{ status }</Text>
);
}
},
}, {
title: 'Started on',
dataIndex: 'started',
@ -66,22 +74,24 @@ export default function JobListComponent(props: Props) {
title: 'Assignee',
dataIndex: 'assignee',
key: 'assignee',
render: (jobInstance: any) => {
const assignee = jobInstance.assignee ? jobInstance.assignee.username : null
render: (jobInstance: any): JSX.Element => {
const assignee = jobInstance.assignee ? jobInstance.assignee.username : null;
return (
<UserSelector
users={props.registeredUsers}
users={registeredUsers}
value={assignee}
onChange={(value: string) => {
let [userInstance] = props.registeredUsers
.filter((user: any) => user.username === value);
onChange={(value: string): void => {
let [userInstance] = [...registeredUsers]
.filter((user: any) => user.username === value);
if (userInstance === undefined) {
userInstance = null;
}
// eslint-disable-next-line
jobInstance.assignee = userInstance;
props.onJobUpdate(jobInstance);
onJobUpdate(jobInstance);
}}
/>
);
@ -129,4 +139,4 @@ export default function JobListComponent(props: Props) {
/>
</div>
);
}
}

@ -25,52 +25,64 @@ interface TaskPageComponentProps {
type Props = TaskPageComponentProps & RouteComponentProps<{id: string}>;
class TaskPageComponent extends React.PureComponent<Props> {
private attempts: number = 0;
private attempts = 0;
public componentDidUpdate() {
if (this.props.deleteActivity) {
this.props.history.replace('/tasks');
public componentDidUpdate(): void {
const {
deleteActivity,
history,
} = this.props;
if (deleteActivity) {
history.replace('/tasks');
}
if (this.attempts == 2) {
if (this.attempts === 2) {
notification.warning({
message: 'Something wrong with the task. It cannot be fetched from the server',
});
}
}
public render() {
const { id } = this.props.match.params;
const fetchTask = !this.props.task;
public render(): JSX.Element {
const {
match,
task,
fetching,
onFetchTask,
} = this.props;
const { id } = match.params;
const fetchTask = !task;
if (fetchTask) {
if (!this.props.fetching) {
if (!fetching) {
if (!this.attempts) {
this.attempts ++;
this.props.onFetchTask(+id);
this.attempts++;
onFetchTask(+id);
} else {
this.attempts ++;
this.attempts++;
}
}
return (
<Spin size='large' style={{margin: '25% 50%'}}/>
<Spin size='large' style={{ margin: '25% 50%' }} />
);
} else if (typeof(this.props.task) === 'undefined') {
}
if (typeof (task) === 'undefined') {
return (
<div> </div>
)
} else {
const task = this.props.task as Task;
return (
<Row type='flex' justify='center' align='top' className='cvat-task-details-wrapper'>
<Col md={22} lg={18} xl={16} xxl={14}>
<TopBarComponent taskInstance={task.instance}/>
<DetailsContainer task={task}/>
<JobListContainer task={task}/>
</Col>
</Row>
);
}
return (
<Row type='flex' justify='center' align='top' className='cvat-task-details-wrapper'>
<Col md={22} lg={18} xl={16} xxl={14}>
<TopBarComponent taskInstance={(task as Task).instance} />
<DetailsContainer task={(task as Task)} />
<JobListContainer task={(task as Task)} />
</Col>
</Row>
);
}
}

@ -16,10 +16,11 @@ interface DetailsComponentProps {
taskInstance: any;
}
export default function DetailsComponent(props: DetailsComponentProps) {
const subMenuIcon = () => (<img src='/assets/icon-sub-menu.svg'/>);
export default function DetailsComponent(props: DetailsComponentProps): JSX.Element {
const subMenuIcon = (): JSX.Element => (<img alt='' src='/assets/icon-sub-menu.svg' />);
const { id } = props.taskInstance;
const { taskInstance } = props;
const { id } = taskInstance;
return (
<Row className='cvat-task-top-bar' type='flex' justify='space-between' align='middle'>
@ -28,16 +29,18 @@ export default function DetailsComponent(props: DetailsComponentProps) {
</Col>
<Col>
<Dropdown overlay={
<ActionsMenuContainer
taskInstance={props.taskInstance}
/>
}>
(
<ActionsMenuContainer
taskInstance={taskInstance}
/>
)}
>
<Button size='large' className='cvat-flex cvat-flex-center'>
<Text className='cvat-black-color'>Actions</Text>
<Icon className='cvat-task-item-menu-icon' component={subMenuIcon}/>
<Icon className='cvat-task-item-menu-icon' component={subMenuIcon} />
</Button>
</Dropdown>
</Col>
</Row>
);
}
}

@ -1,7 +1,6 @@
import React from 'react';
import {
Icon,
Select,
} from 'antd';
@ -11,23 +10,27 @@ interface Props {
onChange: (user: string) => void;
}
export default function UserSelector(props: Props) {
export default function UserSelector(props: Props): JSX.Element {
const {
value,
users,
onChange,
} = props;
return (
<Select
defaultValue={props.value ? props.value : '—'}
size='small'
showSearch
className='cvat-user-selector'
onChange={props.onChange}
>
<Select.Option key='-1' value='—'>{'—'}</Select.Option>
{ props.users.map((user) => {
return (
<Select.Option key={user.id} value={user.username}>
{user.username}
</Select.Option>
);
})}
</Select>
defaultValue={value || '—'}
size='small'
showSearch
className='cvat-user-selector'
onChange={onChange}
>
<Select.Option key='-1' value='—'></Select.Option>
{ users.map((user): JSX.Element => (
<Select.Option key={user.id} value={user.username}>
{user.username}
</Select.Option>
))}
</Select>
);
}
}

@ -8,32 +8,31 @@ import {
Icon,
} from 'antd';
export default function EmptyListComponent() {
const emptyTasksIcon = () => (<img src='/assets/empty-tasks-icon.svg'/>);
export default function EmptyListComponent(): JSX.Element {
const emptyTasksIcon = (): JSX.Element => (<img alt='' 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}/>
<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>
<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>
<Text type='secondary'>To get started with your annotation project</Text>
</Col>
</Row>
<Row type='flex' justify='center' align='middle'>
<Row type='flex' justify='center' align='middle'>
<Col>
<Link to='/tasks/create'>{'create a new task'}</Link>
<Link to='/tasks/create'>create a new task</Link>
</Col>
</Row>
</div>
)
}
);
}

@ -25,71 +25,80 @@ export interface TaskItemProps {
}
class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteComponentProps> {
constructor(props: TaskItemProps & RouteComponentProps) {
super(props);
}
private renderPreview() {
private renderPreview(): JSX.Element {
const { previewImage } = this.props;
return (
<Col span={4}>
<div className='cvat-task-item-preview-wrapper'>
<img alt='Preview' className='cvat-task-item-preview' src={this.props.previewImage}/>
<img alt='Preview' className='cvat-task-item-preview' src={previewImage} />
</div>
</Col>
)
);
}
private renderDescription() {
private renderDescription(): JSX.Element {
// Task info
const task = this.props.taskInstance;
const { id } = task;
const owner = task.owner ? task.owner.username : null;
const updated = moment(task.updatedDate).fromNow();
const created = moment(task.createdDate).format('MMMM Do YYYY');
const { taskInstance } = this.props;
const { id } = taskInstance;
const owner = taskInstance.owner ? taskInstance.owner.username : null;
const updated = moment(taskInstance.updatedDate).fromNow();
const created = moment(taskInstance.createdDate).format('MMMM Do YYYY');
// Get and truncate a task name
const name = `${task.name.substring(0, 70)}${task.name.length > 70 ? '...' : ''}`;
const name = `${taskInstance.name.substring(0, 70)}${taskInstance.name.length > 70 ? '...' : ''}`;
return (
<Col span={10}>
<Text strong>{`${id} ${name}`}</Text> <br/>
{ owner ?
<>
<Text type='secondary'>
Created { owner ? 'by ' + owner : '' } on {created}
</Text> <br/>
</> : null
<Text strong>{`${id} ${name}`}</Text>
<br />
{ owner
&& (
<>
<Text type='secondary'>
{`Created ${owner ? `by ${owner}` : ''} on ${created}`}
</Text>
<br />
</>
)
}
<Text type='secondary'>{`Last updated ${updated}`}</Text>
</Col>
)
);
}
private renderProgress() {
const task = this.props.taskInstance;
private renderProgress(): JSX.Element {
const {
taskInstance,
activeInference,
} = this.props;
// Count number of jobs and performed jobs
const numOfJobs = task.jobs.length;
const numOfCompleted = task.jobs.filter(
(job: any) => job.status === 'completed'
const numOfJobs = taskInstance.jobs.length;
const numOfCompleted = taskInstance.jobs.filter(
(job: any): boolean => job.status === 'completed',
).length;
// Progress appearence depends on number of jobs
const progressColor = numOfCompleted === numOfJobs ? 'cvat-task-completed-progress':
numOfCompleted ? 'cvat-task-progress-progress' : 'cvat-task-pending-progress';
let progressColor = null;
let progressText = null;
if (numOfCompleted === numOfJobs) {
progressColor = 'cvat-task-completed-progress';
progressText = <Text strong className={progressColor}>Completed</Text>;
} else if (numOfCompleted) {
progressColor = 'cvat-task-progress-progress';
progressText = <Text strong className={progressColor}>In Progress</Text>;
} else {
progressColor = 'cvat-task-pending-progress';
progressText = <Text strong className={progressColor}>Pending</Text>;
}
return (
<Col span={6}>
<Row type='flex' justify='space-between' align='top'>
<Col>
<svg height='8' width='8' className={progressColor}>
<circle cx='4' cy='4' r='4' strokeWidth='0'/>
<circle cx='4' cy='4' r='4' strokeWidth='0' />
</svg>
{ numOfCompleted === numOfJobs ?
<Text strong className={progressColor}>{'Completed'}</Text>
: numOfCompleted ?
<Text strong className={progressColor}>{'In Progress'}</Text>
: <Text strong className={progressColor}>{'Pending'}</Text>
}
{ progressText }
</Col>
<Col>
<Text type='secondary'>{`${numOfCompleted} of ${numOfJobs} jobs`}</Text>
@ -107,78 +116,86 @@ class TaskItemComponent extends React.PureComponent<TaskItemProps & RouteCompone
/>
</Col>
</Row>
{ this.props.activeInference ?
<>
<Row>
<Col>
<Text strong>Automatic annotation</Text>
</Col>
</Row>
<Row>
<Col>
<Progress
percent={Math.floor(this.props.activeInference.progress)}
strokeColor={{
from: '#108ee9',
to: '#87d068',
}}
showInfo={false}
strokeWidth={5}
size='small'
/>
</Col>
</Row>
</> : null
{ activeInference
&& (
<>
<Row>
<Col>
<Text strong>Automatic annotation</Text>
</Col>
</Row>
<Row>
<Col>
<Progress
percent={Math.floor(activeInference.progress)}
strokeColor={{
from: '#108ee9',
to: '#87d068',
}}
showInfo={false}
strokeWidth={5}
size='small'
/>
</Col>
</Row>
</>
)
}
</Col>
)
);
}
private renderNavigation() {
const subMenuIcon = () => (<img src='/assets/icon-sub-menu.svg'/>);
const { id } = this.props.taskInstance;
private renderNavigation(): JSX.Element {
const subMenuIcon = (): JSX.Element => (<img alt='' src='/assets/icon-sub-menu.svg' />);
const {
taskInstance,
history,
} = this.props;
const { id } = taskInstance;
return (
<Col span={4}>
<Row type='flex' justify='end'>
<Col>
<Button type='primary' size='large' ghost onClick={
() => this.props.history.push(`/tasks/${id}`)
}> Open </Button>
<Button
type='primary'
size='large'
ghost
onClick={(): void => history.push(`/tasks/${id}`)}
>
Open
</Button>
</Col>
</Row>
<Row type='flex' justify='end'>
<Col>
<Text className='cvat-black-color'>Actions</Text>
<Dropdown overlay={
<ActionsMenuContainer
taskInstance={this.props.taskInstance}
/>
}>
<Icon className='cvat-task-item-menu-icon' component={subMenuIcon}/>
<Dropdown overlay={<ActionsMenuContainer taskInstance={taskInstance} />}>
<Icon className='cvat-task-item-menu-icon' component={subMenuIcon} />
</Dropdown>
</Col>
</Row>
</Col>
)
);
}
public render() {
public render(): JSX.Element {
const { deleted } = this.props;
const style = {};
if (this.props.deleted) {
if (deleted) {
(style as any).pointerEvents = 'none';
(style as any).opacity = 0.5;
}
return (
<Row className='cvat-tasks-list-item' type='flex' justify='center' align='top' style={{...style}}>
<Row className='cvat-tasks-list-item' type='flex' justify='center' align='top' style={{ ...style }}>
{this.renderPreview()}
{this.renderDescription()}
{this.renderProgress()}
{this.renderNavigation()}
</Row>
)
};
);
}
}
export default withRouter(TaskItemComponent);

@ -15,10 +15,15 @@ export interface ContentListProps {
numberOfTasks: number;
}
export default function TaskListComponent(props: ContentListProps) {
const tasks = props.currentTasksIndexes;
const taskViews = tasks.map(
(tid, id) => <TaskItem idx={id} taskID={tid} key={tid}/>
export default function TaskListComponent(props: ContentListProps): JSX.Element {
const {
currentTasksIndexes,
numberOfTasks,
currentPage,
onSwitchPage,
} = props;
const taskViews = currentTasksIndexes.map(
(tid, id): JSX.Element => <TaskItem idx={id} taskID={tid} key={tid} />,
);
return (
@ -32,14 +37,14 @@ export default function TaskListComponent(props: ContentListProps) {
<Col md={22} lg={18} xl={16} xxl={14}>
<Pagination
className='cvat-tasks-pagination'
onChange={props.onSwitchPage}
total={props.numberOfTasks}
onChange={onSwitchPage}
total={numberOfTasks}
pageSize={10}
current={props.currentPage}
current={currentPage}
showQuickJumper
/>
</Col>
</Row>
</>
)
);
}

@ -22,135 +22,155 @@ interface TasksPageProps {
onGetTasks: (gettingQuery: TasksQuery) => void;
}
class TasksPageComponent extends React.PureComponent<TasksPageProps & RouteComponentProps> {
constructor(props: any) {
super(props);
}
function getSearchField(gettingQuery: TasksQuery): string {
let searchString = '';
for (const field of Object.keys(gettingQuery)) {
if (gettingQuery[field] !== null && field !== 'page') {
if (field === 'search') {
return (gettingQuery[field] as any) as string;
}
private updateURL(gettingQuery: TasksQuery) {
let queryString = '?';
for (const field of Object.keys(gettingQuery)) {
if (gettingQuery[field] !== null) {
queryString += `${field}=${gettingQuery[field]}&`;
// not constant condition
// eslint-disable-next-line
if (typeof (gettingQuery[field] === 'number')) {
searchString += `${field}:${gettingQuery[field]} AND `;
} else {
searchString += `${field}:"${gettingQuery[field]}" AND `;
}
}
this.props.history.replace({
search: queryString.slice(0, -1),
});
}
private getSearchField(gettingQuery: TasksQuery): string {
let searchString = '';
for (const field of Object.keys(gettingQuery)) {
if (gettingQuery[field] !== null && field !== 'page') {
if (field === 'search') {
return (gettingQuery[field] as any) as string;
} else {
if (typeof (gettingQuery[field] === 'number')) {
searchString += `${field}:${gettingQuery[field]} AND `;
return searchString.slice(0, -5);
}
class TasksPageComponent extends React.PureComponent<TasksPageProps & RouteComponentProps> {
public componentDidMount(): void {
const {
gettingQuery,
location,
onGetTasks,
} = this.props;
const params = new URLSearchParams(location.search);
const query = { ...gettingQuery };
for (const field of Object.keys(query)) {
if (params.has(field)) {
const value = params.get(field);
if (value) {
if (field === 'id' || field === 'page') {
if (Number.isInteger(+value)) {
query[field] = +value;
}
} else {
searchString += `${field}:"${gettingQuery[field]}" AND `;
query[field] = value;
}
}
} else if (field === 'page') {
query[field] = 1;
} else {
query[field] = null;
}
}
return searchString.slice(0, -5);
this.updateURL(query);
onGetTasks(query);
}
private handleSearch = (value: string): void => {
const gettingQuery = { ...this.props.gettingQuery };
const {
gettingQuery,
onGetTasks,
} = this.props;
const query = { ...gettingQuery };
const search = value.replace(/\s+/g, ' ').replace(/\s*:+\s*/g, ':').trim();
const fields = ['name', 'mode', 'owner', 'assignee', 'status', 'id'];
for (const field of fields) {
gettingQuery[field] = null;
query[field] = null;
}
gettingQuery.search = null;
query.search = null;
let specificRequest = false;
for (const param of search.split(/[\s]+and[\s]+|[\s]+AND[\s]+/)) {
if (param.includes(':')) {
const [name, value] = param.split(':');
if (fields.includes(name) && !!value) {
const [field, fieldValue] = param.split(':');
if (fields.includes(field) && !!fieldValue) {
specificRequest = true;
if (name === 'id') {
if (Number.isInteger(+value)) {
gettingQuery[name] = +value;
if (field === 'id') {
if (Number.isInteger(+fieldValue)) {
query[field] = +fieldValue;
}
} else {
gettingQuery[name] = value;
query[field] = fieldValue;
}
}
}
}
gettingQuery.page = 1;
query.page = 1;
if (!specificRequest && value) { // only id
gettingQuery.search = value;
query.search = value;
}
this.updateURL(gettingQuery);
this.props.onGetTasks(gettingQuery);
}
this.updateURL(query);
onGetTasks(query);
};
private handlePagination = (page: number): void => {
const gettingQuery = { ...this.props.gettingQuery };
gettingQuery.page = page;
this.updateURL(gettingQuery);
this.props.onGetTasks(gettingQuery);
}
public componentDidMount() {
const gettingQuery = { ...this.props.gettingQuery };
const params = new URLSearchParams(this.props.location.search);
const {
gettingQuery,
onGetTasks,
} = this.props;
const query = { ...gettingQuery };
query.page = page;
this.updateURL(query);
onGetTasks(query);
};
private updateURL(gettingQuery: TasksQuery): void {
const { history } = this.props;
let queryString = '?';
for (const field of Object.keys(gettingQuery)) {
if (params.has(field)) {
const value = params.get(field);
if (value) {
if (field === 'id' || field === 'page') {
if (Number.isInteger(+value)) {
gettingQuery[field] = +value;
}
} else {
gettingQuery[field] = value;
}
}
} else {
if (field === 'page') {
gettingQuery[field] = 1;
} else {
gettingQuery[field] = null;
}
if (gettingQuery[field] !== null) {
queryString += `${field}=${gettingQuery[field]}&`;
}
}
this.updateURL(gettingQuery);
this.props.onGetTasks(gettingQuery);
history.replace({
search: queryString.slice(0, -1),
});
}
public render() {
if (this.props.tasksFetching) {
public render(): JSX.Element {
const {
tasksFetching,
gettingQuery,
numberOfVisibleTasks,
} = this.props;
if (tasksFetching) {
return (
<Spin size='large' style={{margin: '25% 45%'}}/>
<Spin size='large' style={{ margin: '25% 45%' }} />
);
} else {
return (
<div className='cvat-tasks-page'>
<TopBar
onSearch={this.handleSearch}
searchValue={this.getSearchField(this.props.gettingQuery)}
/>
{this.props.numberOfVisibleTasks ?
}
return (
<div className='cvat-tasks-page'>
<TopBar
onSearch={this.handleSearch}
searchValue={getSearchField(gettingQuery)}
/>
{numberOfVisibleTasks
? (
<TaskListContainer
onSwitchPage={this.handlePagination}
/> : <EmptyListComponent/>}
</div>
)
}
/>
) : <EmptyListComponent />
}
</div>
);
}
}
export default withRouter(TasksPageComponent);
export default withRouter(TasksPageComponent);

@ -16,35 +16,50 @@ interface VisibleTopBarProps {
searchValue: string;
}
function TopBarComponent(props: VisibleTopBarProps & RouteComponentProps) {
function TopBarComponent(props: VisibleTopBarProps & RouteComponentProps): JSX.Element {
const {
searchValue,
history,
onSearch,
} = props;
return (
<>
<Row type='flex' justify='center' align='middle'>
<Col md={22} lg={18} xl={16} xxl={14}>
<Text strong>{'Default project'}</Text>
<Text strong>Default project</Text>
</Col>
</Row>
<Row type='flex' justify='center' align='middle'>
<Col md={11} lg={9} xl={8} xxl={7}>
<Text className='cvat-title'>Tasks</Text>
<Input.Search
defaultValue={props.searchValue}
onSearch={props.onSearch}
size='large' placeholder='Search'
defaultValue={searchValue}
onSearch={onSearch}
size='large'
placeholder='Search'
/>
</Col>
<Col
md={{span: 11}}
lg={{span: 9}}
xl={{span: 8}}
xxl={{span: 7}}>
<Button size='large' id='cvat-create-task-button' type='primary' onClick={
() => props.history.push('/tasks/create')
}> Create new task </Button>
md={{ span: 11 }}
lg={{ span: 9 }}
xl={{ span: 8 }}
xxl={{ span: 7 }}
>
<Button
size='large'
id='cvat-create-task-button'
type='primary'
onClick={
(): void => history.push('/tasks/create')
}
>
Create new task
</Button>
</Col>
</Row>
</>
)
);
}
export default withRouter(TopBarComponent);

@ -4,7 +4,6 @@ import { connect } from 'react-redux';
import ActionsMenuComponent from '../../components/actions-menu/actions-menu';
import {
CombinedState,
ActiveInference,
} from '../../reducers/interfaces';
import { showRunModelDialog } from '../../actions/models-actions';
@ -30,7 +29,7 @@ interface StateToProps {
installedTFSegmentation: boolean;
installedAutoAnnotation: boolean;
inferenceIsActive: boolean;
};
}
interface DispatchToProps {
onLoadAnnotation: (taskInstance: any, loader: any, file: File) => void;
@ -45,16 +44,16 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const { activities } = state.tasks;
const { dumps } = activities;
const { loads } = activities;
const _exports = activities.exports;
const activeExports = activities.exports;
const { plugins } = state.plugins;
const id = own.taskInstance.id;
const { id } = own.taskInstance;
return {
installedTFAnnotation: plugins.TF_ANNOTATION,
installedTFSegmentation: plugins.TF_SEGMENTATION,
installedAutoAnnotation: plugins.AUTO_ANNOTATION,
dumpActivities: dumps.byTask[id] ? dumps.byTask[id] : null,
exportActivities: _exports.byTask[id] ? _exports.byTask[id] : null,
exportActivities: activeExports.byTask[id] ? activeExports.byTask[id] : null,
loadActivity: loads.byTask[id] ? loads.byTask[id] : null,
loaders: formats.annotationFormats
.map((format: any): any[] => format.loaders).flat(),
@ -62,49 +61,32 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
.map((format: any): any[] => format.dumpers).flat(),
exporters: formats.datasetFormats,
inferenceIsActive: id in state.models.inferences,
};
};
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onLoadAnnotation: (taskInstance: any, loader: any, file: File) => {
onLoadAnnotation: (taskInstance: any, loader: any, file: File): void => {
dispatch(loadAnnotationsAsync(taskInstance, loader, file));
},
onDumpAnnotation: (taskInstance: any, dumper: any) => {
onDumpAnnotation: (taskInstance: any, dumper: any): void => {
dispatch(dumpAnnotationsAsync(taskInstance, dumper));
},
onExportDataset: (taskInstance: any, exporter: any) => {
onExportDataset: (taskInstance: any, exporter: any): void => {
dispatch(exportDatasetAsync(taskInstance, exporter));
},
onDeleteTask: (taskInstance: any) => {
onDeleteTask: (taskInstance: any): void => {
dispatch(deleteTaskAsync(taskInstance));
},
onOpenRunWindow: (taskInstance: any) => {
onOpenRunWindow: (taskInstance: any): void => {
dispatch(showRunModelDialog(taskInstance));
}
},
};
}
function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps) {
function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps): JSX.Element {
return (
<ActionsMenuComponent
taskInstance={props.taskInstance}
loaders={props.loaders}
dumpers={props.dumpers}
exporters={props.exporters}
loadActivity={props.loadActivity}
dumpActivities={props.dumpActivities}
exportActivities={props.exportActivities}
installedTFAnnotation={props.installedTFAnnotation}
installedTFSegmentation={props.installedTFSegmentation}
installedAutoAnnotation={props.installedAutoAnnotation}
onLoadAnnotation={props.onLoadAnnotation}
onDumpAnnotation={props.onDumpAnnotation}
onExportDataset={props.onExportDataset}
onDeleteTask={props.onDeleteTask}
onOpenRunWindow={props.onOpenRunWindow}
inferenceIsActive={props.inferenceIsActive}
/>
<ActionsMenuComponent {...props} />
);
}

@ -1,15 +1,11 @@
import React from 'react';
export default class AnnotationPageContainer extends React.PureComponent {
constructor(props: any) {
super(props);
}
public render() {
public render(): JSX.Element {
return (
<div>
"AnnotationPage"
AnnotationPage
</div>
);
}
}
}

@ -18,7 +18,7 @@ interface DispatchToProps {
}
function mapStateToProps(state: CombinedState): StateToProps {
const { models} = state;
const { models } = state;
return {
isAdmin: state.auth.user.isAdmin,
@ -28,19 +28,15 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
createModel(name: string, files: ModelFiles, global: boolean) {
createModel(name: string, files: ModelFiles, global: boolean): void {
dispatch(createModelAsync(name, files, global));
},
};
}
function CreateModelPageContainer(props: StateToProps & DispatchToProps) {
function CreateModelPageContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<CreateModelPageComponent
isAdmin={props.isAdmin}
modelCreatingStatus={props.modelCreatingStatus}
createModel={props.createModel}
/>
<CreateModelPageComponent {...props} />
);
}

@ -12,12 +12,12 @@ interface StateToProps {
}
interface DispatchToProps {
create: (data: CreateTaskData) => void;
onCreate: (data: CreateTaskData) => void;
}
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
create: (data: CreateTaskData) => dispatch(createTaskAsync(data)),
onCreate: (data: CreateTaskData): void => dispatch(createTaskAsync(data)),
};
}
@ -29,13 +29,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
};
}
function CreateTaskPageContainer(props: StateToProps & DispatchToProps) {
function CreateTaskPageContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<CreateTaskComponent
status={props.status}
onCreate={props.create}
installedGit={props.installedGit}
/>
<CreateTaskComponent {...props} />
);
}

@ -1,7 +1,7 @@
import React from 'react';
import { connect } from 'react-redux';
import { TreeNodeNormal } from 'antd/lib/tree/Tree'
import { TreeNodeNormal } from 'antd/lib/tree/Tree';
import FileManagerComponent, { Files } from '../../components/file-manager/file-manager';
import { loadShareDataAsync } from '../../actions/share-actions';
@ -44,9 +44,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
getTreeData: (key: string, success: () => void, failure: () => void) => {
getTreeData: (key: string, success: () => void, failure: () => void): void => {
dispatch(loadShareDataAsync(key, success, failure));
}
},
};
}
@ -63,13 +63,21 @@ export class FileManagerContainer extends React.PureComponent<Props> {
return this.managerComponentRef.reset();
}
public render() {
public render(): JSX.Element {
const {
treeData,
getTreeData,
withRemote,
} = this.props;
return (
<FileManagerComponent
treeData={this.props.treeData}
onLoadData={this.props.getTreeData}
withRemote={this.props.withRemote}
ref={(component) => this.managerComponentRef = component}
treeData={treeData}
onLoadData={getTreeData}
withRemote={withRemote}
ref={(component): void => {
this.managerComponentRef = component;
}}
/>
);
}

@ -19,7 +19,7 @@ interface StateToProps {
}
interface DispatchToProps {
logout(): void;
onLogout(): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -37,21 +37,13 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
logout: () => dispatch(logoutAsync()),
}
onLogout: (): void => dispatch(logoutAsync()),
};
}
function HeaderContainer(props: StateToProps & DispatchToProps) {
function HeaderContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<HeaderComponent
logoutFetching={props.logoutFetching}
installedAnalytics={props.installedAnalytics}
installedTFAnnotation={props.installedTFAnnotation}
installedTFSegmentation={props.installedTFSegmentation}
installedAutoAnnotation={props.installedAutoAnnotation}
onLogout={props.logout}
username={props.username}
/>
<HeaderComponent {...props} />
);
}

@ -9,7 +9,7 @@ interface StateToProps {
}
interface DispatchToProps {
login(username: string, password: string): void;
onLogin(username: string, password: string): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -20,16 +20,13 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
login: (...args) => dispatch(loginAsync(...args)),
onLogin: (...args): void => dispatch(loginAsync(...args)),
};
}
function LoginPageContainer(props: DispatchToProps & StateToProps) {
function LoginPageContainer(props: DispatchToProps & StateToProps): JSX.Element {
return (
<LoginPageComponent
fetching={props.fetching}
onLogin={props.login}
/>
<LoginPageComponent {...props} />
);
}

@ -18,18 +18,18 @@ interface StateToProps {
modelsInitialized: boolean;
models: Model[];
activeProcesses: {
[index: string]: string
[index: string]: string;
};
taskInstance: any;
visible: boolean;
}
interface DispatchToProps {
inferModelAsync(
runInference(
taskInstance: any,
model: Model,
mapping: {
[index: string]: string
[index: string]: string;
},
cleanOut: boolean,
): void;
@ -52,42 +52,33 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps {
return ({
inferModelAsync(
runInference(
taskInstance: any,
model: Model,
mapping: {
[index: string]: string
[index: string]: string;
},
cleanOut: boolean): void {
dispatch(inferModelAsync(taskInstance, model, mapping, cleanOut));
cleanOut: boolean,
): void {
dispatch(inferModelAsync(taskInstance, model, mapping, cleanOut));
},
getModels(): void {
dispatch(getModelsAsync());
},
closeDialog(): void {
dispatch(closeRunModelDialog());
}
},
});
}
function ModelRunnerModalContainer(props: StateToProps & DispatchToProps) {
function ModelRunnerModalContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<ModelRunnerModalComponent
modelsFetching={props.modelsFetching}
modelsInitialized={props.modelsInitialized}
models={props.models}
activeProcesses={props.activeProcesses}
visible={props.visible}
taskInstance={props.taskInstance}
getModels={props.getModels}
closeDialog={props.closeDialog}
runInference={props.inferModelAsync}
/>
<ModelRunnerModalComponent {...props} />
);
}
export default connect(
mapStateToProps,
mapDispatchToProps,
) (ModelRunnerModalContainer);
)(ModelRunnerModalContainer);

@ -43,33 +43,28 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
getModels() {
getModels(): void {
dispatch(getModelsAsync());
},
deleteModel(id: number) {
deleteModel(id: number): void {
dispatch(deleteModelAsync(id));
},
};
}
function ModelsPageContainer(props: DispatchToProps & StateToProps) {
const render = props.installedAutoAnnotation
|| props.installedTFAnnotation
|| props.installedTFSegmentation;
function ModelsPageContainer(props: DispatchToProps & StateToProps): JSX.Element | null {
const {
installedAutoAnnotation,
installedTFSegmentation,
installedTFAnnotation,
} = props;
const render = installedAutoAnnotation
|| installedTFAnnotation
|| installedTFSegmentation;
return (
render ?
<ModelsPageComponent
installedAutoAnnotation={props.installedAutoAnnotation}
installedTFSegmentation={props.installedTFSegmentation}
installedTFAnnotation={props.installedTFAnnotation}
modelsInitialized={props.modelsInitialized}
modelsFetching={props.modelsFetching}
registeredUsers={props.registeredUsers}
models={props.models}
getModels={props.getModels}
deleteModel={props.deleteModel}
/> : null
render ? <ModelsPageComponent {...props} /> : null
);
}

@ -9,7 +9,7 @@ interface StateToProps {
}
interface DispatchToProps {
register: (username: string, firstName: string,
onRegister: (username: string, firstName: string,
lastName: string, email: string,
password1: string, password2: string) => void;
}
@ -22,16 +22,13 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
register: (...args) => dispatch(registerAsync(...args))
}
onRegister: (...args): void => dispatch(registerAsync(...args)),
};
}
function RegisterPageContainer(props: StateToProps & DispatchToProps) {
function RegisterPageContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<RegisterPageComponent
fetching={props.fetching}
onRegister={props.register}
/>
<RegisterPageComponent {...props} />
);
}

@ -21,7 +21,7 @@ interface DispatchToProps {
onTaskUpdate: (taskInstance: any) => void;
}
function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
function mapStateToProps(state: CombinedState): StateToProps {
const { plugins } = state.plugins;
return {
@ -33,20 +33,26 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onTaskUpdate: (taskInstance: any) =>
dispatch(updateTaskAsync(taskInstance))
}
onTaskUpdate: (taskInstance: any): void => dispatch(updateTaskAsync(taskInstance)),
};
}
function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps) {
function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps): JSX.Element {
const {
task,
installedGit,
registeredUsers,
onTaskUpdate,
} = props;
return (
<DetailsComponent
previewImage={props.task.preview}
taskInstance={props.task.instance}
installedGit={props.installedGit}
onTaskUpdate={props.onTaskUpdate}
registeredUsers={props.registeredUsers}
previewImage={task.preview}
taskInstance={task.instance}
installedGit={installedGit}
onTaskUpdate={onTaskUpdate}
registeredUsers={registeredUsers}
/>
);
}
@ -54,4 +60,4 @@ function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps) {
export default connect(
mapStateToProps,
mapDispatchToProps,
)(TaskPageContainer);
)(TaskPageContainer);

@ -28,16 +28,22 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
onJobUpdate: (jobInstance: any) => dispatch(updateJobAsync(jobInstance)),
onJobUpdate: (jobInstance: any): void => dispatch(updateJobAsync(jobInstance)),
};
}
function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps) {
function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps): JSX.Element {
const {
task,
registeredUsers,
onJobUpdate,
} = props;
return (
<JobListComponent
taskInstance={props.task.instance}
registeredUsers={props.registeredUsers}
onJobUpdate={props.onJobUpdate}
taskInstance={task.instance}
registeredUsers={registeredUsers}
onJobUpdate={onJobUpdate}
/>
);
}
@ -45,4 +51,4 @@ function TaskPageContainer(props: StateToProps & DispatchToProps & OwnProps) {
export default connect(
mapStateToProps,
mapDispatchToProps,
)(TaskPageContainer);
)(TaskPageContainer);

@ -21,7 +21,7 @@ interface StateToProps {
}
interface DispatchToProps {
fetchTask: (tid: number) => void;
onFetchTask: (tid: number) => void;
}
function mapStateToProps(state: CombinedState, own: Props): StateToProps {
@ -47,7 +47,7 @@ function mapStateToProps(state: CombinedState, own: Props): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
fetchTask: (tid: number) => {
onFetchTask: (tid: number): void => {
dispatch(getTasksAsync({
id: tid,
page: 1,
@ -62,15 +62,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
};
}
function TaskPageContainer(props: StateToProps & DispatchToProps) {
function TaskPageContainer(props: StateToProps & DispatchToProps): JSX.Element {
return (
<TaskPageComponent
task={props.task}
fetching={props.fetching}
deleteActivity={props.deleteActivity}
installedGit={props.installedGit}
onFetchTask={props.fetchTask}
/>
<TaskPageComponent {...props} />
);
}

@ -7,14 +7,14 @@ import {
ActiveInference,
} from '../../reducers/interfaces';
import TaskItemComponent from '../../components/tasks-page/task-item'
import TaskItemComponent from '../../components/tasks-page/task-item';
import {
getTasksAsync,
} from '../../actions/tasks-actions';
interface StateToProps {
deleteActivity: boolean | null;
deleted: boolean;
previewImage: string;
taskInstance: any;
activeInference: ActiveInference | null;
@ -35,7 +35,7 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
const id = own.taskID;
return {
deleteActivity: deletes.byTask[id] ? deletes.byTask[id] : null,
deleted: deletes.byTask[id] ? deletes.byTask[id] === true : false,
previewImage: task.preview,
taskInstance: task.instance,
activeInference: state.models.inferences[id] || null,
@ -47,19 +47,14 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
getTasks: (query: TasksQuery): void => {
dispatch(getTasksAsync(query));
},
}
};
}
type TasksItemContainerProps = StateToProps & DispatchToProps & OwnProps;
function TaskItemContainer(props: TasksItemContainerProps) {
function TaskItemContainer(props: TasksItemContainerProps): JSX.Element {
return (
<TaskItemComponent
deleted={props.deleteActivity === true}
taskInstance={props.taskInstance}
previewImage={props.previewImage}
activeInference={props.activeInference}
/>
<TaskItemComponent {...props} />
);
}

@ -33,19 +33,26 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
getTasks: (query: TasksQuery) => {dispatch(getTasksAsync(query))}
}
getTasks: (query: TasksQuery): void => {
dispatch(getTasksAsync(query));
},
};
}
type TasksListContainerProps = StateToProps & DispatchToProps & OwnProps;
function TasksListContainer(props: TasksListContainerProps) {
function TasksListContainer(props: TasksListContainerProps): JSX.Element {
const {
tasks,
onSwitchPage,
} = props;
return (
<TasksListComponent
onSwitchPage={props.onSwitchPage}
currentTasksIndexes={props.tasks.current.map((task) => task.instance.id)}
currentPage={props.tasks.gettingQuery.page}
numberOfTasks={props.tasks.count}
onSwitchPage={onSwitchPage}
currentTasksIndexes={tasks.current.map((task): number => task.instance.id)}
currentPage={tasks.gettingQuery.page}
numberOfTasks={tasks.count}
/>
);
}

@ -3,7 +3,7 @@ import { connect } from 'react-redux';
import {
TasksQuery,
CombinedState
CombinedState,
} from '../../reducers/interfaces';
import TasksPageComponent from '../../components/tasks-page/tasks-page';
@ -18,7 +18,7 @@ interface StateToProps {
}
interface DispatchToProps {
getTasks: (gettingQuery: TasksQuery) => void;
onGetTasks: (gettingQuery: TasksQuery) => void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -34,21 +34,17 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps {
return {
getTasks: (query: TasksQuery) => {dispatch(getTasksAsync(query))}
}
onGetTasks: (query: TasksQuery): void => {
dispatch(getTasksAsync(query));
},
};
}
type TasksPageContainerProps = StateToProps & DispatchToProps;
function TasksPageContainer(props: TasksPageContainerProps) {
function TasksPageContainer(props: TasksPageContainerProps): JSX.Element {
return (
<TasksPageComponent
tasksFetching={props.tasksFetching}
gettingQuery={props.gettingQuery}
numberOfTasks={props.numberOfTasks}
numberOfVisibleTasks={props.numberOfVisibleTasks}
onGetTasks={props.getTasks}
/>
<TasksPageComponent {...props} />
);
}

@ -19,7 +19,7 @@ import {
import {
CombinedState,
NotificationsState,
} from './reducers/interfaces';
} from './reducers/interfaces';
createCVATStore(createRootReducer);
const cvatStore = getCVATStore();
@ -65,7 +65,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
installedAutoAnnotation: plugins.plugins.AUTO_ANNOTATION,
installedTFSegmentation: plugins.plugins.TF_SEGMENTATION,
installedTFAnnotation: plugins.plugins.TF_ANNOTATION,
notifications: {...state.notifications},
notifications: { ...state.notifications },
user: auth.user,
};
}
@ -81,29 +81,10 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
};
}
function reduxAppWrapper(props: StateToProps & DispatchToProps) {
function reduxAppWrapper(props: StateToProps & DispatchToProps): JSX.Element {
return (
<CVATApplication
initPlugins={props.initPlugins}
loadFormats={props.loadFormats}
loadUsers={props.loadUsers}
verifyAuthorized={props.verifyAuthorized}
resetErrors={props.resetErrors}
resetMessages={props.resetMessages}
userInitialized={props.userInitialized}
pluginsInitialized={props.pluginsInitialized}
pluginsFetching={props.pluginsFetching}
usersInitialized={props.usersInitialized}
usersFetching={props.usersFetching}
formatsInitialized={props.formatsInitialized}
formatsFetching={props.formatsFetching}
installedAutoAnnotation={props.installedAutoAnnotation}
installedTFSegmentation={props.installedTFSegmentation}
installedTFAnnotation={props.installedTFAnnotation}
notifications={props.notifications}
user={props.user}
/>
)
<CVATApplication {...props} />
);
}
const ReduxAppWrapper = connect(
@ -114,8 +95,8 @@ const ReduxAppWrapper = connect(
ReactDOM.render(
(
<Provider store={cvatStore}>
<ReduxAppWrapper/>
<ReduxAppWrapper />
</Provider>
),
document.getElementById('root')
)
document.getElementById('root'),
);

@ -18,15 +18,15 @@ export default (state = defaultState, action: AnyAction): AuthState => {
user: action.payload.user,
};
case AuthActionTypes.AUTHORIZED_FAILED:
return {
...state,
initialized: true,
};
return {
...state,
initialized: true,
};
case AuthActionTypes.LOGIN:
return {
...state,
fetching: true,
}
};
case AuthActionTypes.LOGIN_SUCCESS:
return {
...state,
@ -49,11 +49,6 @@ export default (state = defaultState, action: AnyAction): AuthState => {
fetching: false,
user: null,
};
case AuthActionTypes.LOGIN_FAILED:
return {
...state,
fetching: false,
};
case AuthActionTypes.REGISTER:
return {
...state,

@ -37,7 +37,7 @@ export default (state = defaultState, action: AnyAction): FormatsState => {
case AuthActionTypes.LOGOUT_SUCCESS: {
return {
...defaultState,
}
};
}
default:
return state;

@ -147,8 +147,8 @@ export interface ModelFiles {
}
export interface ErrorState {
message: string,
reason: string,
message: string;
reason: string;
}
export interface NotificationsState {

@ -110,7 +110,7 @@ export default function (state = defaultState, action: AnyAction): ModelsState {
case AuthActionTypes.LOGOUT_SUCCESS: {
return {
...defaultState,
}
};
}
default: {
return {

@ -67,7 +67,7 @@ export default function (state = defaultState, action: AnyAction): Notifications
authorized: {
message: 'Could not check authorization on the server',
reason: action.payload.error.toString(),
}
},
},
},
};
@ -82,7 +82,7 @@ export default function (state = defaultState, action: AnyAction): Notifications
login: {
message: 'Could not login on the server',
reason: action.payload.error.toString(),
}
},
},
},
};
@ -323,7 +323,7 @@ export default function (state = defaultState, action: AnyAction): Notifications
}
case ModelsActionTypes.GET_INFERENCE_STATUS_SUCCESS: {
if (action.payload.activeInference.status === 'finished') {
const taskID = action.payload.taskID;
const { taskID } = action.payload;
return {
...state,
messages: {
@ -351,7 +351,7 @@ export default function (state = defaultState, action: AnyAction): Notifications
metaFetching: {
message: 'Could not fetch models meta information',
reason: action.payload.error.toString(),
}
},
},
},
};
@ -368,7 +368,7 @@ export default function (state = defaultState, action: AnyAction): Notifications
message: 'Could not fetch inference status for the '
+ `<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
reason: action.payload.error.toString(),
}
},
},
},
};
@ -400,7 +400,7 @@ export default function (state = defaultState, action: AnyAction): Notifications
message: 'Could not infer model for the '
+ `<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
reason: action.payload.error.toString(),
}
},
},
},
};
@ -424,7 +424,7 @@ export default function (state = defaultState, action: AnyAction): Notifications
case AuthActionTypes.LOGOUT_SUCCESS: {
return {
...defaultState,
}
};
}
default: {
return {

@ -45,7 +45,7 @@ export default function (state = defaultState, action: AnyAction): PluginsState
case AuthActionTypes.LOGOUT_SUCCESS: {
return {
...defaultState,
}
};
}
default:
return { ...state };

@ -46,7 +46,7 @@ export default function (state = defaultState, action: AnyAction): ShareState {
case AuthActionTypes.LOGOUT_SUCCESS: {
return {
...defaultState,
}
};
}
default:
return {

@ -408,7 +408,7 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
case AuthActionTypes.LOGOUT_SUCCESS: {
return {
...defaultState,
}
};
}
default:
return state;

@ -35,7 +35,7 @@ export default function (state: UsersState = defaultState, action: AnyAction): U
case AuthActionTypes.LOGOUT_SUCCESS: {
return {
...defaultState,
}
};
}
default:
return {

@ -275,6 +275,10 @@
border: 1px solid #40a9ff;
}
.cvat-tasks-list-item > div:nth-child(2) {
word-break: break-all;
}
.cvat-tasks-list-item > div:nth-child(4) > div {
margin-right: 20px;
}

Loading…
Cancel
Save