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.enable": true,
"eslint.validate": [ "eslint.validate": [
"javascript", "javascript",
"typescript" "typescript",
"typescriptreact",
], ],
"eslint.workingDirectories": [ "eslint.workingDirectories": [
{ {

@ -19,16 +19,23 @@ module.exports = {
], ],
'extends': [ 'extends': [
'plugin:@typescript-eslint/recommended', 'plugin:@typescript-eslint/recommended',
'airbnb-typescript/base', 'airbnb-typescript',
'plugin:import/errors', 'plugin:import/errors',
'plugin:import/warnings', 'plugin:import/warnings',
'plugin:import/typescript', 'plugin:import/typescript',
], ],
'rules': { 'rules': {
'@typescript-eslint/indent': ['warn', 4], '@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], '@typescript-eslint/no-explicit-any': [0],
'no-restricted-syntax': [0, {'selector': 'ForOfStatement'}], 'no-restricted-syntax': [0, {'selector': 'ForOfStatement'}],
'no-plusplus': [0], 'no-plusplus': [0],
'lines-between-class-members': 0,
'react/no-did-update-set-state': 0, // https://github.com/airbnb/javascript/issues/1875
}, },
'settings': { 'settings': {
'import/resolver': { '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": { "@babel/template": {
"version": "7.6.0", "version": "7.6.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz",
@ -1059,27 +1077,39 @@
} }
}, },
"@typescript-eslint/eslint-plugin": { "@typescript-eslint/eslint-plugin": {
"version": "1.13.0", "version": "2.10.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.13.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.10.0.tgz",
"integrity": "sha512-WQHCozMnuNADiqMtsNzp96FNox5sOVpU8Xt4meaT4em8lOG1SrOv92/mUbEHQVh90sldKSfcOc/I0FOb/14G1g==", "integrity": "sha512-rT51fNLW0u3fnDGnAHVC5nu+Das+y2CpW10yqvf6/j5xbuUV3FxA3mBaIbM24CXODXjbgUznNb4Kg9XZOUxKAw==",
"dev": true, "dev": true,
"requires": { "requires": {
"@typescript-eslint/experimental-utils": "1.13.0", "@typescript-eslint/experimental-utils": "2.10.0",
"eslint-utils": "^1.3.1", "eslint-utils": "^1.4.3",
"functional-red-black-tree": "^1.0.1", "functional-red-black-tree": "^1.0.1",
"regexpp": "^2.0.1", "regexpp": "^3.0.0",
"tsutils": "^3.7.0" "tsutils": "^3.17.1"
} }
}, },
"@typescript-eslint/experimental-utils": { "@typescript-eslint/experimental-utils": {
"version": "1.13.0", "version": "2.10.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz", "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.10.0.tgz",
"integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==", "integrity": "sha512-FZhWq6hWWZBP76aZ7bkrfzTMP31CCefVIImrwP3giPLcoXocmLTmr92NLZxuIcTL4GTEOE33jQMWy9PwelL+yQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/json-schema": "^7.0.3", "@types/json-schema": "^7.0.3",
"@typescript-eslint/typescript-estree": "1.13.0", "@typescript-eslint/typescript-estree": "2.10.0",
"eslint-scope": "^4.0.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": { "@typescript-eslint/parser": {
@ -1092,6 +1122,17 @@
"@typescript-eslint/experimental-utils": "1.13.0", "@typescript-eslint/experimental-utils": "1.13.0",
"@typescript-eslint/typescript-estree": "1.13.0", "@typescript-eslint/typescript-estree": "1.13.0",
"eslint-visitor-keys": "^1.0.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": { "@typescript-eslint/typescript-estree": {
@ -1102,8 +1143,8 @@
"requires": { "requires": {
"lodash.unescape": "4.0.1", "lodash.unescape": "4.0.1",
"semver": "5.5.0" "semver": "5.5.0"
}
}, },
"dependencies": {
"semver": { "semver": {
"version": "5.5.0", "version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
@ -1112,6 +1153,43 @@
} }
} }
}, },
"@typescript-eslint/typescript-estree": {
"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": "^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": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
},
"@webassemblyjs/ast": { "@webassemblyjs/ast": {
"version": "1.8.5", "version": "1.8.5",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.8.5.tgz",
@ -1634,12 +1712,30 @@
"dev": true "dev": true
}, },
"axobject-query": { "axobject-query": {
"version": "2.0.2", "version": "2.1.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.1.1.tgz",
"integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==", "integrity": "sha512-lF98xa/yvy6j3fBHAgQXIYl+J4eZadOSqsPojemUqClzNbBV38wWGpUbQbVEyf4eUF5yF7eHmGgGA2JiHyjeqw==",
"dev": true, "dev": true,
"requires": { "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": { "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": { "core-util-is": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", "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": { "eslint-plugin-import": {
"version": "2.18.2", "version": "2.18.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz",
@ -3538,20 +3646,21 @@
} }
}, },
"eslint-plugin-react": { "eslint-plugin-react": {
"version": "7.16.0", "version": "7.17.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.16.0.tgz", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.17.0.tgz",
"integrity": "sha512-GacBAATewhhptbK3/vTP09CbFrgUJmBSaaRcWdbQLFvUZy9yVcQxigBNHGPU/KE2AyHpzj3AWXpxoMTsIDiHug==", "integrity": "sha512-ODB7yg6lxhBVMeiH1c7E95FLD4E/TwmFjltiU+ethv7KPdCwgiFuOZg9zNRHyufStTDLl/dEFqI2Q1VPmCd78A==",
"dev": true, "dev": true,
"requires": { "requires": {
"array-includes": "^3.0.3", "array-includes": "^3.0.3",
"doctrine": "^2.1.0", "doctrine": "^2.1.0",
"eslint-plugin-eslint-plugin": "^2.1.0",
"has": "^1.0.3", "has": "^1.0.3",
"jsx-ast-utils": "^2.2.1", "jsx-ast-utils": "^2.2.3",
"object.entries": "^1.1.0", "object.entries": "^1.1.0",
"object.fromentries": "^2.0.0", "object.fromentries": "^2.0.1",
"object.values": "^1.1.0", "object.values": "^1.1.0",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"resolve": "^1.12.0" "resolve": "^1.13.1"
}, },
"dependencies": { "dependencies": {
"doctrine": { "doctrine": {
@ -3562,9 +3671,24 @@
"requires": { "requires": {
"esutils": "^2.0.2" "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": { "eslint-scope": {
"version": "4.0.3", "version": "4.0.3",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
@ -8141,9 +8265,9 @@
} }
}, },
"regexpp": { "regexpp": {
"version": "2.0.1", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz",
"integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==", "integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==",
"dev": true "dev": true
}, },
"regexpu-core": { "regexpu-core": {
@ -9444,9 +9568,9 @@
"dev": true "dev": true
}, },
"typescript": { "typescript": {
"version": "3.6.4", "version": "3.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz",
"integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==", "integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==",
"dev": true "dev": true
}, },
"ua-parser-js": { "ua-parser-js": {

@ -14,7 +14,7 @@
"@babel/preset-env": "^7.6.0", "@babel/preset-env": "^7.6.0",
"@babel/preset-react": "^7.0.0", "@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.6.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": "^6.23.0",
"babel-loader": "^8.0.6", "babel-loader": "^8.0.6",
"babel-plugin-import": "^1.12.2", "babel-plugin-import": "^1.12.2",
@ -22,11 +22,12 @@
"eslint-config-airbnb-typescript": "^4.0.1", "eslint-config-airbnb-typescript": "^4.0.1",
"eslint-plugin-import": "^2.18.2", "eslint-plugin-import": "^2.18.2",
"eslint-plugin-jsx-a11y": "^6.2.3", "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", "html-webpack-plugin": "^3.2.0",
"nodemon": "^1.19.2", "nodemon": "^1.19.2",
"style-loader": "^1.0.0", "style-loader": "^1.0.0",
"typescript": "^3.6.3", "typescript": "^3.7.3",
"webpack": "^4.41.2", "webpack": "^4.41.2",
"webpack-cli": "^3.3.8", "webpack-cli": "^3.3.8",
"webpack-dev-server": "^3.8.0" "webpack-dev-server": "^3.8.0"

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

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

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

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

@ -14,10 +14,10 @@ import {
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import CreateModelForm, { import CreateModelForm, {
CreateModelForm as WrappedCreateModelForm CreateModelForm as WrappedCreateModelForm,
} from './create-model-form'; } from './create-model-form';
import ConnectedFileManager, { import ConnectedFileManager, {
FileManagerContainer FileManagerContainer,
} from '../../containers/file-manager/file-manager'; } from '../../containers/file-manager/file-manager';
import { ModelFiles } from '../../reducers/interfaces'; import { ModelFiles } from '../../reducers/interfaces';
@ -30,13 +30,26 @@ interface Props {
export default class CreateModelContent extends React.PureComponent<Props> { export default class CreateModelContent extends React.PureComponent<Props> {
private modelForm: WrappedCreateModelForm; private modelForm: WrappedCreateModelForm;
private fileManagerContainer: FileManagerContainer; private fileManagerContainer: FileManagerContainer;
public constructor(props: Props) { public constructor(props: Props) {
super(props); super(props);
this.modelForm = null as any as WrappedCreateModelForm; this.modelForm = null as any as WrappedCreateModelForm;
this.fileManagerContainer = null as any as FileManagerContainer; 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() this.modelForm.submit()
.then((data) => { .then((data) => {
const { const {
@ -70,45 +83,46 @@ export default class CreateModelContent extends React.PureComponent<Props> {
description: 'Please, specify correct files', description: 'Please, specify correct files',
}); });
} else { } else {
this.props.createModel(data.name, grouppedFiles, data.global); createModel(data.name, grouppedFiles, data.global);
} }
}).catch(() => { }).catch(() => {
notification.error({ notification.error({
message: 'Could not upload a model', message: 'Could not upload a model',
description: 'Please, check input fields', 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() { public render(): JSX.Element {
const loading = !!this.props.modelCreatingStatus const {
&& this.props.modelCreatingStatus !== 'CREATED'; modelCreatingStatus,
const status = this.props.modelCreatingStatus } = this.props;
&& this.props.modelCreatingStatus !== 'CREATED' ? this.props.modelCreatingStatus : ''; 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'; const guideLink = 'https://github.com/opencv/cvat/blob/develop/cvat/apps/auto_annotation/README.md';
return ( return (
<Row type='flex' justify='start' align='middle' className='cvat-create-model-content'> <Row type='flex' justify='start' align='middle' className='cvat-create-model-content'>
<Col span={24}> <Col span={24}>
<Tooltip overlay='Click to open guide'> <Tooltip overlay='Click to open guide'>
<Icon onClick={() => { <Icon
window.open(guideLink, '_blank') onClick={(): void => {
}} type='question-circle'/> // false positive
// eslint-disable-next-line
window.open(guideLink, '_blank');
}}
type='question-circle'
/>
</Tooltip> </Tooltip>
</Col> </Col>
<Col span={24}> <Col span={24}>
<CreateModelForm <CreateModelForm
wrappedComponentRef={ wrappedComponentRef={
(ref: WrappedCreateModelForm) => this.modelForm = ref (ref: WrappedCreateModelForm): void => {
this.modelForm = ref;
}
} }
/> />
</Col> </Col>
@ -117,13 +131,17 @@ export default class CreateModelContent extends React.PureComponent<Props> {
<Text className='cvat-black-color'>Select files:</Text> <Text className='cvat-black-color'>Select files:</Text>
</Col> </Col>
<Col span={24}> <Col span={24}>
<ConnectedFileManager ref={ <ConnectedFileManager
(container: FileManagerContainer) => ref={
this.fileManagerContainer = container (container: FileManagerContainer): void => {
} withRemote={true}/> this.fileManagerContainer = container;
}
}
withRemote={false}
/>
</Col> </Col>
<Col span={18}> <Col span={18}>
{status && <Alert message={`${status}`}/>} {status && <Alert message={`${status}`} />}
</Col> </Col>
<Col span={6}> <Col span={6}>
<Button <Button
@ -131,7 +149,9 @@ export default class CreateModelContent extends React.PureComponent<Props> {
disabled={loading} disabled={loading}
loading={loading} loading={loading}
onClick={this.handleSubmitClick} onClick={this.handleSubmitClick}
> Submit </Button> >
Submit
</Button>
</Col> </Col>
</Row> </Row>
); );

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -12,12 +12,12 @@ interface StateToProps {
} }
interface DispatchToProps { interface DispatchToProps {
create: (data: CreateTaskData) => void; onCreate: (data: CreateTaskData) => void;
} }
function mapDispatchToProps(dispatch: any): DispatchToProps { function mapDispatchToProps(dispatch: any): DispatchToProps {
return { 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 ( return (
<CreateTaskComponent <CreateTaskComponent {...props} />
status={props.status}
onCreate={props.create}
installedGit={props.installedGit}
/>
); );
} }

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

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

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

@ -18,18 +18,18 @@ interface StateToProps {
modelsInitialized: boolean; modelsInitialized: boolean;
models: Model[]; models: Model[];
activeProcesses: { activeProcesses: {
[index: string]: string [index: string]: string;
}; };
taskInstance: any; taskInstance: any;
visible: boolean; visible: boolean;
} }
interface DispatchToProps { interface DispatchToProps {
inferModelAsync( runInference(
taskInstance: any, taskInstance: any,
model: Model, model: Model,
mapping: { mapping: {
[index: string]: string [index: string]: string;
}, },
cleanOut: boolean, cleanOut: boolean,
): void; ): void;
@ -52,13 +52,14 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps { function mapDispatchToProps(dispatch: any): DispatchToProps {
return ({ return ({
inferModelAsync( runInference(
taskInstance: any, taskInstance: any,
model: Model, model: Model,
mapping: { mapping: {
[index: string]: string [index: string]: string;
}, },
cleanOut: boolean): void { cleanOut: boolean,
): void {
dispatch(inferModelAsync(taskInstance, model, mapping, cleanOut)); dispatch(inferModelAsync(taskInstance, model, mapping, cleanOut));
}, },
getModels(): void { getModels(): void {
@ -66,28 +67,18 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
}, },
closeDialog(): void { closeDialog(): void {
dispatch(closeRunModelDialog()); dispatch(closeRunModelDialog());
} },
}); });
} }
function ModelRunnerModalContainer(props: StateToProps & DispatchToProps) { function ModelRunnerModalContainer(props: StateToProps & DispatchToProps): JSX.Element {
return ( return (
<ModelRunnerModalComponent <ModelRunnerModalComponent {...props} />
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}
/>
); );
} }
export default connect( export default connect(
mapStateToProps, mapStateToProps,
mapDispatchToProps, mapDispatchToProps,
) (ModelRunnerModalContainer); )(ModelRunnerModalContainer);

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

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

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

@ -28,16 +28,22 @@ function mapStateToProps(state: CombinedState): StateToProps {
function mapDispatchToProps(dispatch: any): DispatchToProps { function mapDispatchToProps(dispatch: any): DispatchToProps {
return { 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 ( return (
<JobListComponent <JobListComponent
taskInstance={props.task.instance} taskInstance={task.instance}
registeredUsers={props.registeredUsers} registeredUsers={registeredUsers}
onJobUpdate={props.onJobUpdate} onJobUpdate={onJobUpdate}
/> />
); );
} }

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

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

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

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

@ -19,7 +19,7 @@ import {
import { import {
CombinedState, CombinedState,
NotificationsState, NotificationsState,
} from './reducers/interfaces'; } from './reducers/interfaces';
createCVATStore(createRootReducer); createCVATStore(createRootReducer);
const cvatStore = getCVATStore(); const cvatStore = getCVATStore();
@ -65,7 +65,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
installedAutoAnnotation: plugins.plugins.AUTO_ANNOTATION, installedAutoAnnotation: plugins.plugins.AUTO_ANNOTATION,
installedTFSegmentation: plugins.plugins.TF_SEGMENTATION, installedTFSegmentation: plugins.plugins.TF_SEGMENTATION,
installedTFAnnotation: plugins.plugins.TF_ANNOTATION, installedTFAnnotation: plugins.plugins.TF_ANNOTATION,
notifications: {...state.notifications}, notifications: { ...state.notifications },
user: auth.user, 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 ( return (
<CVATApplication <CVATApplication {...props} />
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}
/>
)
} }
const ReduxAppWrapper = connect( const ReduxAppWrapper = connect(
@ -114,8 +95,8 @@ const ReduxAppWrapper = connect(
ReactDOM.render( ReactDOM.render(
( (
<Provider store={cvatStore}> <Provider store={cvatStore}>
<ReduxAppWrapper/> <ReduxAppWrapper />
</Provider> </Provider>
), ),
document.getElementById('root') document.getElementById('root'),
) );

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

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

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

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

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

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

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

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

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

@ -275,6 +275,10 @@
border: 1px solid #40a9ff; 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 { .cvat-tasks-list-item > div:nth-child(4) > div {
margin-right: 20px; margin-right: 20px;
} }

Loading…
Cancel
Save