提交 32027ce8 编写于 作者: B Boris Sekachev 提交者: Nikita Manovich

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
上级 c0f1854f
......@@ -3,7 +3,8 @@
"eslint.enable": true,
"eslint.validate": [
"javascript",
"typescript"
"typescript",
"typescriptreact",
],
"eslint.workingDirectories": [
{
......
......@@ -19,16 +19,23 @@ module.exports = {
],
'extends': [
'plugin:@typescript-eslint/recommended',
'airbnb-typescript/base',
'airbnb-typescript',
'plugin:import/errors',
'plugin:import/warnings',
'plugin:import/typescript',
],
'rules': {
'@typescript-eslint/indent': ['warn', 4],
'react/jsx-indent': ['warn', 4],
'react/jsx-indent-props': ['warn', 4],
'react/jsx-props-no-spreading': 0,
'jsx-quotes': ['error', 'prefer-single'],
'arrow-parens': ['error', 'always'],
'@typescript-eslint/no-explicit-any': [0],
'no-restricted-syntax': [0, {'selector': 'ForOfStatement'}],
'no-plusplus': [0],
'lines-between-class-members': 0,
'react/no-did-update-set-state': 0, // https://github.com/airbnb/javascript/issues/1875
},
'settings': {
'import/resolver': {
......
......@@ -899,6 +899,24 @@
}
}
},
"@babel/runtime-corejs3": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.7.4.tgz",
"integrity": "sha512-BBIEhzk8McXDcB3IbOi8zQPzzINUp4zcLesVlBSOcyGhzPUU8Xezk5GAG7Sy5GVhGmAO0zGd2qRSeY2g4Obqxw==",
"dev": true,
"requires": {
"core-js-pure": "^3.0.0",
"regenerator-runtime": "^0.13.2"
},
"dependencies": {
"regenerator-runtime": {
"version": "0.13.3",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==",
"dev": true
}
}
},
"@babel/template": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.6.0.tgz",
......@@ -1059,27 +1077,39 @@
}
},
"@typescript-eslint/eslint-plugin": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-1.13.0.tgz",
"integrity": "sha512-WQHCozMnuNADiqMtsNzp96FNox5sOVpU8Xt4meaT4em8lOG1SrOv92/mUbEHQVh90sldKSfcOc/I0FOb/14G1g==",
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-2.10.0.tgz",
"integrity": "sha512-rT51fNLW0u3fnDGnAHVC5nu+Das+y2CpW10yqvf6/j5xbuUV3FxA3mBaIbM24CXODXjbgUznNb4Kg9XZOUxKAw==",
"dev": true,
"requires": {
"@typescript-eslint/experimental-utils": "1.13.0",
"eslint-utils": "^1.3.1",
"@typescript-eslint/experimental-utils": "2.10.0",
"eslint-utils": "^1.4.3",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^2.0.1",
"tsutils": "^3.7.0"
"regexpp": "^3.0.0",
"tsutils": "^3.17.1"
}
},
"@typescript-eslint/experimental-utils": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz",
"integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==",
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-2.10.0.tgz",
"integrity": "sha512-FZhWq6hWWZBP76aZ7bkrfzTMP31CCefVIImrwP3giPLcoXocmLTmr92NLZxuIcTL4GTEOE33jQMWy9PwelL+yQ==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/typescript-estree": "1.13.0",
"eslint-scope": "^4.0.0"
"@typescript-eslint/typescript-estree": "2.10.0",
"eslint-scope": "^5.0.0"
},
"dependencies": {
"eslint-scope": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.0.0.tgz",
"integrity": "sha512-oYrhJW7S0bxAFDvWqzvMPRm6pcgcnWc4QnofCAqRTRfQC0JcwenzGglTtsLyIuuWFfkqDG9vz67cnttSd53djw==",
"dev": true,
"requires": {
"esrecurse": "^4.1.0",
"estraverse": "^4.1.1"
}
}
}
},
"@typescript-eslint/parser": {
......@@ -1092,22 +1122,70 @@
"@typescript-eslint/experimental-utils": "1.13.0",
"@typescript-eslint/typescript-estree": "1.13.0",
"eslint-visitor-keys": "^1.0.0"
},
"dependencies": {
"@typescript-eslint/experimental-utils": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-1.13.0.tgz",
"integrity": "sha512-zmpS6SyqG4ZF64ffaJ6uah6tWWWgZ8m+c54XXgwFtUv0jNz8aJAVx8chMCvnk7yl6xwn8d+d96+tWp7fXzTuDg==",
"dev": true,
"requires": {
"@types/json-schema": "^7.0.3",
"@typescript-eslint/typescript-estree": "1.13.0",
"eslint-scope": "^4.0.0"
}
},
"@typescript-eslint/typescript-estree": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz",
"integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==",
"dev": true,
"requires": {
"lodash.unescape": "4.0.1",
"semver": "5.5.0"
}
},
"semver": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
"dev": true
}
}
},
"@typescript-eslint/typescript-estree": {
"version": "1.13.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-1.13.0.tgz",
"integrity": "sha512-b5rCmd2e6DCC6tCTN9GSUAuxdYwCM/k/2wdjHGrIRGPSJotWMCe/dGpi66u42bhuh8q3QBzqM4TMA1GUUCJvdw==",
"version": "2.10.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-2.10.0.tgz",
"integrity": "sha512-oOYnplddQNm/LGVkqbkAwx4TIBuuZ36cAQq9v3nFIU9FmhemHuVzAesMSXNQDdAzCa5bFgCrfD3JWhYVKlRN2g==",
"dev": true,
"requires": {
"debug": "^4.1.1",
"eslint-visitor-keys": "^1.1.0",
"glob": "^7.1.6",
"is-glob": "^4.0.1",
"lodash.unescape": "4.0.1",
"semver": "5.5.0"
"semver": "^6.3.0",
"tsutils": "^3.17.1"
},
"dependencies": {
"glob": {
"version": "7.1.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz",
"integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==",
"dev": true,
"requires": {
"fs.realpath": "^1.0.0",
"inflight": "^1.0.4",
"inherits": "2",
"minimatch": "^3.0.4",
"once": "^1.3.0",
"path-is-absolute": "^1.0.0"
}
},
"semver": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.5.0.tgz",
"integrity": "sha512-4SJ3dm0WAwWy/NVeioZh5AntkdJoWKxHxcmyP622fOkgHa4z3R0TdBJICINyaSDE6uNwVc8gZr+ZinwZAH4xIA==",
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz",
"integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==",
"dev": true
}
}
......@@ -1634,12 +1712,30 @@
"dev": true
},
"axobject-query": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz",
"integrity": "sha512-MCeek8ZH7hKyO1rWUbKNQBbl4l2eY0ntk7OGi+q0RlafrCnfPxC06WZA+uebCfmYp4mNU9jRBP1AhGyf8+W3ww==",
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.1.1.tgz",
"integrity": "sha512-lF98xa/yvy6j3fBHAgQXIYl+J4eZadOSqsPojemUqClzNbBV38wWGpUbQbVEyf4eUF5yF7eHmGgGA2JiHyjeqw==",
"dev": true,
"requires": {
"ast-types-flow": "0.0.7"
"@babel/runtime": "^7.7.4",
"@babel/runtime-corejs3": "^7.7.4"
},
"dependencies": {
"@babel/runtime": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.4.tgz",
"integrity": "sha512-r24eVUUr0QqNZa+qrImUk8fn5SPhHq+IfYvIoIMg0do3GdK9sMdiLKP3GYVVaxpPKORgm8KRKaNTEhAjgIpLMw==",
"dev": true,
"requires": {
"regenerator-runtime": "^0.13.2"
}
},
"regenerator-runtime": {
"version": "0.13.3",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz",
"integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==",
"dev": true
}
}
},
"babel": {
......@@ -2580,6 +2676,12 @@
}
}
},
"core-js-pure": {
"version": "3.4.7",
"resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.4.7.tgz",
"integrity": "sha512-Am3uRS8WCdTFA3lP7LtKR0PxgqYzjAMGKXaZKSNSC/8sqU0Wfq8R/YzoRs2rqtOVEunfgH+0q3O0BKOg0AvjPw==",
"dev": true
},
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
......@@ -3484,6 +3586,12 @@
}
}
},
"eslint-plugin-eslint-plugin": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-eslint-plugin/-/eslint-plugin-eslint-plugin-2.1.0.tgz",
"integrity": "sha512-kT3A/ZJftt28gbl/Cv04qezb/NQ1dwYIbi8lyf806XMxkus7DvOVCLIfTXMrorp322Pnoez7+zabXH29tADIDg==",
"dev": true
},
"eslint-plugin-import": {
"version": "2.18.2",
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.18.2.tgz",
......@@ -3538,20 +3646,21 @@
}
},
"eslint-plugin-react": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.16.0.tgz",
"integrity": "sha512-GacBAATewhhptbK3/vTP09CbFrgUJmBSaaRcWdbQLFvUZy9yVcQxigBNHGPU/KE2AyHpzj3AWXpxoMTsIDiHug==",
"version": "7.17.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.17.0.tgz",
"integrity": "sha512-ODB7yg6lxhBVMeiH1c7E95FLD4E/TwmFjltiU+ethv7KPdCwgiFuOZg9zNRHyufStTDLl/dEFqI2Q1VPmCd78A==",
"dev": true,
"requires": {
"array-includes": "^3.0.3",
"doctrine": "^2.1.0",
"eslint-plugin-eslint-plugin": "^2.1.0",
"has": "^1.0.3",
"jsx-ast-utils": "^2.2.1",
"jsx-ast-utils": "^2.2.3",
"object.entries": "^1.1.0",
"object.fromentries": "^2.0.0",
"object.fromentries": "^2.0.1",
"object.values": "^1.1.0",
"prop-types": "^15.7.2",
"resolve": "^1.12.0"
"resolve": "^1.13.1"
},
"dependencies": {
"doctrine": {
......@@ -3562,9 +3671,24 @@
"requires": {
"esutils": "^2.0.2"
}
},
"resolve": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.13.1.tgz",
"integrity": "sha512-CxqObCX8K8YtAhOBRg+lrcdn+LK+WYOS8tSjqSFbjtrI5PnS63QPhZl4+yKfrU9tdsbMu9Anr/amegT87M9Z6w==",
"dev": true,
"requires": {
"path-parse": "^1.0.6"
}
}
}
},
"eslint-plugin-react-hooks": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-1.7.0.tgz",
"integrity": "sha512-iXTCFcOmlWvw4+TOE8CLWj6yX1GwzT0Y6cUfHHZqWnSk144VmVIRcVGtUAzrLES7C798lmvnt02C7rxaOX1HNA==",
"dev": true
},
"eslint-scope": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz",
......@@ -8141,9 +8265,9 @@
}
},
"regexpp": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-2.0.1.tgz",
"integrity": "sha512-lv0M6+TkDVniA3aD1Eg0DVpfU/booSu7Eev3TDO/mZKHBfVjgCGTV4t4buppESEYDtkArYFOxTJWv6S5C+iaNw==",
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.0.0.tgz",
"integrity": "sha512-Z+hNr7RAVWxznLPuA7DIh8UNX1j9CDrUQxskw9IrBE1Dxue2lyXT+shqEIeLUjrokxIP8CMy1WkjgG3rTsd5/g==",
"dev": true
},
"regexpu-core": {
......@@ -9444,9 +9568,9 @@
"dev": true
},
"typescript": {
"version": "3.6.4",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.4.tgz",
"integrity": "sha512-unoCll1+l+YK4i4F8f22TaNVPRHcD9PA3yCuZ8g5e0qGqlVlJ/8FSateOLLSagn+Yg5+ZwuPkL8LFUc0Jcvksg==",
"version": "3.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.7.3.tgz",
"integrity": "sha512-Mcr/Qk7hXqFBXMN7p7Lusj1ktCBydylfQM/FZCk5glCNQJrCUKPkMHdo9R0MTFWsC/4kPFvDS0fDPvukfCkFsw==",
"dev": true
},
"ua-parser-js": {
......
......@@ -14,7 +14,7 @@
"@babel/preset-env": "^7.6.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.6.0",
"@typescript-eslint/eslint-plugin": "^1.13.0",
"@typescript-eslint/eslint-plugin": "^2.10.0",
"babel": "^6.23.0",
"babel-loader": "^8.0.6",
"babel-plugin-import": "^1.12.2",
......@@ -22,11 +22,12 @@
"eslint-config-airbnb-typescript": "^4.0.1",
"eslint-plugin-import": "^2.18.2",
"eslint-plugin-jsx-a11y": "^6.2.3",
"eslint-plugin-react": "^7.14.3",
"eslint-plugin-react": "^7.17.0",
"eslint-plugin-react-hooks": "^1.7.0",
"html-webpack-plugin": "^3.2.0",
"nodemon": "^1.19.2",
"style-loader": "^1.0.0",
"typescript": "^3.6.3",
"typescript": "^3.7.3",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.8",
"webpack-dev-server": "^3.8.0"
......
......@@ -431,7 +431,7 @@ async function timeoutCallback(
dispatch(getInferenceStatusSuccess(taskID, activeInference));
} catch (error) {
dispatch(getInferenceStatusFailed(taskID, new Error(
`Server request for the task ${taskID} was failed`
`Server request for the task ${taskID} was failed`,
)));
}
}
......
......@@ -36,14 +36,16 @@ interface MinActionsMenuProps {
onOpenRunWindow: (taskInstance: any) => void;
}
export function handleMenuClick(props: MinActionsMenuProps, params: ClickParam) {
export function handleMenuClick(props: MinActionsMenuProps, params: ClickParam): void {
const { taskInstance } = props;
const tracker = taskInstance.bugTracker;
if (params.keyPath.length !== 2) {
switch (params.key) {
case 'tracker': {
window.open(`${tracker}`, '_blank')
// false positive eslint(security/detect-non-literal-fs-filename)
// eslint-disable-next-line
window.open(`${tracker}`, '_blank');
return;
} case 'auto_annotation': {
props.onOpenRunWindow(taskInstance);
......@@ -57,36 +59,51 @@ export function handleMenuClick(props: MinActionsMenuProps, params: ClickParam)
props.onDeleteTask(taskInstance);
},
});
return;
break;
} default: {
return;
// do nothing
}
}
}
}
export default function ActionsMenuComponent(props: ActionsMenuComponentProps) {
const tracker = props.taskInstance.bugTracker;
const renderModelRunner = props.installedAutoAnnotation ||
props.installedTFAnnotation || props.installedTFSegmentation;
export default function ActionsMenuComponent(props: ActionsMenuComponentProps): JSX.Element {
const {
taskInstance,
installedAutoAnnotation,
installedTFAnnotation,
installedTFSegmentation,
dumpers,
loaders,
exporters,
inferenceIsActive,
} = props;
const tracker = taskInstance.bugTracker;
const renderModelRunner = installedAutoAnnotation
|| installedTFAnnotation || installedTFSegmentation;
return (
<Menu selectable={false} className='cvat-actions-menu' onClick={
(params: ClickParam) => handleMenuClick(props, params)
}>
<Menu
selectable={false}
className='cvat-actions-menu'
onClick={
(params: ClickParam): void => handleMenuClick(props, params)
}
>
<Menu.SubMenu key='dump' title='Dump annotations'>
{
props.dumpers.map((dumper) => DumperItemComponent({
dumpers.map((dumper): JSX.Element => DumperItemComponent({
dumper,
taskInstance: props.taskInstance,
dumpActivity: (props.dumpActivities || [])
.filter((_dumper: string) => _dumper === dumper.name)[0] || null,
onDumpAnnotation: props.onDumpAnnotation,
} ))}
}))
}
</Menu.SubMenu>
<Menu.SubMenu key='load' title='Upload annotations'>
{
props.loaders.map((loader) => LoaderItemComponent({
loaders.map((loader): JSX.Element => LoaderItemComponent({
loader,
taskInstance: props.taskInstance,
loadActivity: props.loadActivity,
......@@ -96,7 +113,7 @@ export default function ActionsMenuComponent(props: ActionsMenuComponentProps) {
</Menu.SubMenu>
<Menu.SubMenu key='export' title='Export as a dataset'>
{
props.exporters.map((exporter) => ExportItemComponent({
exporters.map((exporter): JSX.Element => ExportItemComponent({
exporter,
taskInstance: props.taskInstance,
exportActivity: (props.exportActivities || [])
......@@ -107,10 +124,17 @@ export default function ActionsMenuComponent(props: ActionsMenuComponentProps) {
</Menu.SubMenu>
{tracker && <Menu.Item key='tracker'>Open bug tracker</Menu.Item>}
{
renderModelRunner &&
<Menu.Item disabled={props.inferenceIsActive} key='auto_annotation'>Automatic annotation</Menu.Item>
renderModelRunner
&& (
<Menu.Item
disabled={inferenceIsActive}
key='auto_annotation'
>
Automatic annotation
</Menu.Item>
)
}
<hr/>
<hr />
<Menu.Item key='delete'>Delete</Menu.Item>
</Menu>
);
......
......@@ -20,25 +20,31 @@ function isDefaultFormat(dumperName: string, taskMode: string): boolean {
|| (dumperName === 'CVAT XML 1.1 for images' && taskMode === 'annotation');
}
export default function DumperItemComponent(props: DumperItemComponentProps) {
const task = props.taskInstance;
const { mode } = task;
export default function DumperItemComponent(props: DumperItemComponentProps): JSX.Element {
const {
taskInstance,
dumpActivity,
} = props;
const { mode } = taskInstance;
const { dumper } = props;
const pending = !!props.dumpActivity;
const pending = !!dumpActivity;
return (
<Menu.Item className='cvat-actions-menu-dump-submenu-item' key={dumper.name}>
<Button block={true} type='link' disabled={pending}
onClick={() => {
props.onDumpAnnotation(task, dumper);
}}>
<Icon type='download'/>
<Button
block
type='link'
disabled={pending}
onClick={(): void => {
props.onDumpAnnotation(taskInstance, dumper);
}}
>
<Icon type='download' />
<Text strong={isDefaultFormat(dumper.name, mode)}>
{dumper.name}
</Text>
{pending && <Icon type='loading'/>}
{pending && <Icon type='loading' />}
</Button>
</Menu.Item>
);
}
......@@ -15,24 +15,31 @@ interface DumperItemComponentProps {
onExportDataset: (task: any, exporter: any) => void;
}
export default function DumperItemComponent(props: DumperItemComponentProps) {
const task = props.taskInstance;
const { exporter } = props;
const pending = !!props.exportActivity;
export default function DumperItemComponent(props: DumperItemComponentProps): JSX.Element {
const {
taskInstance,
exporter,
exportActivity,
} = props;
const pending = !!exportActivity;
return (
<Menu.Item className='cvat-actions-menu-export-submenu-item' key={exporter.name}>
<Button block={true} type='link' disabled={pending}
onClick={() => {
props.onExportDataset(task, exporter);
}}>
<Icon type='export'/>
<Text strong={props.exporter.is_default}>
<Button
block
type='link'
disabled={pending}
onClick={(): void => {
props.onExportDataset(taskInstance, exporter);
}}
>
<Icon type='export' />
<Text strong={exporter.is_default}>
{exporter.name}
</Text>
{pending && <Icon type='loading'/>}
{pending && <Icon type='loading' />}
</Button>
</Menu.Item>
);
}
......@@ -17,12 +17,15 @@ interface LoaderItemComponentProps {
onLoadAnnotation: (taskInstance: any, loader: any, file: File) => void;
}
export default function LoaderItemComponent(props: LoaderItemComponentProps) {
const { loader } = props;
export default function LoaderItemComponent(props: LoaderItemComponentProps): JSX.Element {
const {
loader,
loadActivity,
} = props;
const loadingWithThisLoader = props.loadActivity
&& props.loadActivity === loader.name
? props.loadActivity : null;
const loadingWithThisLoader = loadActivity
&& loadActivity === loader.name
? loadActivity : null;
const pending = !!loadingWithThisLoader;
......@@ -31,8 +34,8 @@ export default function LoaderItemComponent(props: LoaderItemComponentProps) {
<Upload
accept={`.${loader.format}`}
multiple={false}
showUploadList={ false }
beforeUpload={(file: RcFile) => {
showUploadList={false}
beforeUpload={(file: RcFile): boolean => {
props.onLoadAnnotation(
props.taskInstance,
loader,
......@@ -40,13 +43,14 @@ export default function LoaderItemComponent(props: LoaderItemComponentProps) {
);
return false;
}}>
<Button block={true} type='link' disabled={!!props.loadActivity}>
<Icon type='upload'/>
}}
>
<Button block type='link' disabled={!!loadActivity}>
<Icon type='upload' />
<Text>{loader.name}</Text>
{pending && <Icon type='loading'/>}
{pending && <Icon type='loading' />}
</Button>
</Upload>
</Menu.Item>
);
}
\ No newline at end of file
}
......@@ -14,10 +14,10 @@ import {
import Text from 'antd/lib/typography/Text';
import CreateModelForm, {
CreateModelForm as WrappedCreateModelForm
CreateModelForm as WrappedCreateModelForm,
} from './create-model-form';
import ConnectedFileManager, {
FileManagerContainer
FileManagerContainer,
} from '../../containers/file-manager/file-manager';
import { ModelFiles } from '../../reducers/interfaces';
......@@ -30,19 +30,32 @@ interface Props {
export default class CreateModelContent extends React.PureComponent<Props> {
private modelForm: WrappedCreateModelForm;
private fileManagerContainer: FileManagerContainer;
public constructor(props: Props) {
super(props);
this.modelForm = null as any as WrappedCreateModelForm;
this.fileManagerContainer = null as any as FileManagerContainer;
}
private handleSubmitClick = () => {
public componentDidUpdate(prevProps: Props): void {
const { modelCreatingStatus } = this.props;
if (prevProps.modelCreatingStatus !== 'CREATED'
&& modelCreatingStatus === 'CREATED') {
message.success('The model has been uploaded');
this.modelForm.resetFields();
this.fileManagerContainer.reset();
}
}
private handleSubmitClick = (): void => {
const { createModel } = this.props;
this.modelForm.submit()
.then((data) => {
const {
local,
share,
} = this.fileManagerContainer.getFiles();
} = this.fileManagerContainer.getFiles();
const files = local.length ? local : share;
const grouppedFiles: ModelFiles = {
......@@ -65,50 +78,51 @@ export default class CreateModelContent extends React.PureComponent<Props> {
if (Object.keys(grouppedFiles)
.map((key: string) => grouppedFiles[key])
.filter((val) => !!val).length !== 4) {
notification.error({
message: 'Could not upload a model',
description: 'Please, specify correct files',
});
} else {
this.props.createModel(data.name, grouppedFiles, data.global);
}
notification.error({
message: 'Could not upload a model',
description: 'Please, specify correct files',
});
} else {
createModel(data.name, grouppedFiles, data.global);
}
}).catch(() => {
notification.error({
message: 'Could not upload a model',
description: 'Please, check input fields',
});
})
}
});
};
public componentDidUpdate(prevProps: Props) {
if (prevProps.modelCreatingStatus !== 'CREATED'
&& this.props.modelCreatingStatus === 'CREATED') {
message.success('The model has been uploaded');
this.modelForm.resetFields();
this.fileManagerContainer.reset();
}
}
public render() {
const loading = !!this.props.modelCreatingStatus
&& this.props.modelCreatingStatus !== 'CREATED';
const status = this.props.modelCreatingStatus
&& this.props.modelCreatingStatus !== 'CREATED' ? this.props.modelCreatingStatus : '';
public render(): JSX.Element {
const {
modelCreatingStatus,
} = this.props;
const loading = !!modelCreatingStatus
&& modelCreatingStatus !== 'CREATED';
const status = modelCreatingStatus
&& modelCreatingStatus !== 'CREATED' ? modelCreatingStatus : '';
const guideLink = 'https://github.com/opencv/cvat/blob/develop/cvat/apps/auto_annotation/README.md';
return (
<Row type='flex' justify='start' align='middle' className='cvat-create-model-content'>
<Col span={24}>
<Tooltip overlay='Click to open guide'>
<Icon onClick={() => {
window.open(guideLink, '_blank')
}} type='question-circle'/>
<Icon
onClick={(): void => {
// false positive
// eslint-disable-next-line
window.open(guideLink, '_blank');
}}
type='question-circle'
/>
</Tooltip>
</Col>
<Col span={24}>
<CreateModelForm
wrappedComponentRef={
(ref: WrappedCreateModelForm) => this.modelForm = ref
(ref: WrappedCreateModelForm): void => {
this.modelForm = ref;
}
}
/>
</Col>
......@@ -117,13 +131,17 @@ export default class CreateModelContent extends React.PureComponent<Props> {
<Text className='cvat-black-color'>Select files:</Text>
</Col>
<Col span={24}>
<ConnectedFileManager ref={
(container: FileManagerContainer) =>
this.fileManagerContainer = container
} withRemote={true}/>
<ConnectedFileManager
ref={
(container: FileManagerContainer): void => {
this.fileManagerContainer = container;
}
}
withRemote={false}
/>
</Col>
<Col span={18}>
{status && <Alert message={`${status}`}/>}
{status && <Alert message={`${status}`} />}
</Col>
<Col span={6}>
<Button
......@@ -131,9 +149,11 @@ export default class CreateModelContent extends React.PureComponent<Props> {
disabled={loading}
loading={loading}
onClick={this.handleSubmitClick}
> Submit </Button>
>
Submit
</Button>
</Col>
</Row>
);
}
}
\ No newline at end of file
}
......@@ -15,13 +15,10 @@ import Text from 'antd/lib/typography/Text';
type Props = FormComponentProps;
export class CreateModelForm extends React.PureComponent<Props> {
public constructor(props: Props) {
super(props);
}
public submit(): Promise<{name: string, global: boolean}> {
public submit(): Promise<{name: string; global: boolean}> {
const { form } = this.props;
return new Promise((resolve, reject) => {
this.props.form.validateFields((errors, values) => {
form.validateFields((errors, values): void => {
if (!errors) {
resolve({
name: values.name,
......@@ -34,15 +31,17 @@ export class CreateModelForm extends React.PureComponent<Props> {
});
}
public resetFields() {
this.props.form.resetFields();
public resetFields(): void {
const { form } = this.props;
form.resetFields();
}
public render() {
const { getFieldDecorator } = this.props.form;
public render(): JSX.Element {
const { form } = this.props;
const { getFieldDecorator } = form;
return (
<Form onSubmit={(e: React.FormEvent) => e.preventDefault()}>
<Form onSubmit={(e: React.FormEvent): void => e.preventDefault()}>
<Row>
<Col span={24}>
<Text type='danger'>* </Text>
......@@ -55,7 +54,7 @@ export class CreateModelForm extends React.PureComponent<Props> {
required: true,
message: 'Please, specify a model name',
}],
})(<Input placeholder='Model name'/>)}
})(<Input placeholder='Model name' />)}
</Form.Item>
</Col>
<Col span={8} offset={2}>
......@@ -64,11 +63,13 @@ export class CreateModelForm extends React.PureComponent<Props> {
{ getFieldDecorator('global', {
initialValue: false,
valuePropName: 'checked',
})(<Checkbox>
<Text className='cvat-black-color'>
Load globally
</Text>
</Checkbox>)}
})(
<Checkbox>
<Text className='cvat-black-color'>
Load globally
</Text>
</Checkbox>,
)}
</Tooltip>
</Form.Item>
</Col>
......
......@@ -16,17 +16,23 @@ interface Props {
modelCreatingStatus: string;
}
export default function CreateModelPageComponent(props: Props) {
export default function CreateModelPageComponent(props: Props): JSX.Element {
const {
isAdmin,
modelCreatingStatus,
createModel,
} = props;
return (
<Row type='flex' justify='center' align='top' className='cvat-create-model-form-wrapper'>
<Col md={20} lg={16} xl={14} xxl={9}>
<Text className='cvat-title'>Upload a new model</Text>
<CreateModelContent
isAdmin={props.isAdmin}
modelCreatingStatus={props.modelCreatingStatus}
createModel={props.createModel}
isAdmin={isAdmin}
modelCreatingStatus={modelCreatingStatus}
createModel={createModel}
/>
</Col>
</Row>
);
}
\ No newline at end of file
}
......@@ -28,19 +28,24 @@ export interface AdvancedConfiguration {
}
type Props = FormComponentProps & {
onSubmit(values: AdvancedConfiguration): void
onSubmit(values: AdvancedConfiguration): void;
installedGit: boolean;
};
class AdvancedConfigurationForm extends React.PureComponent<Props> {
public submit() {
public submit(): Promise<void> {
return new Promise((resolve, reject) => {
this.props.form.validateFields((error, values) => {
const {
form,
onSubmit,
} = this.props;
form.validateFields((error, values): void => {
if (!error) {
const filteredValues = { ...values };
delete filteredValues.frameStep;
this.props.onSubmit({
onSubmit({
...values,
frameFilter: values.frameStep ? `step=${values.frameStep}` : undefined,
});
......@@ -49,17 +54,19 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
reject();
}
});
})
});
}
public resetFields() {
this.props.form.resetFields();
public resetFields(): void {
const { form } = this.props;
form.resetFields();
}
private renderZOrder() {
private renderZOrder(): JSX.Element {
const { form } = this.props;
return (
<Form.Item help='Enables order for shapes. Useful for segmentation tasks'>
{this.props.form.getFieldDecorator('zOrder', {
{form.getFieldDecorator('zOrder', {
initialValue: false,
valuePropName: 'checked',
})(
......@@ -67,21 +74,23 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
<Text className='cvat-black-color'>
Z-order
</Text>
</Checkbox>
</Checkbox>,
)}
</Form.Item>
);
}
private renderImageQuality() {
private renderImageQuality(): JSX.Element {
const { form } = this.props;
return (
<Form.Item label='Image quality'>
<Tooltip overlay='Defines image compression level'>
{this.props.form.getFieldDecorator('imageQuality', {
{form.getFieldDecorator('imageQuality', {
initialValue: 70,
rules: [{
required: true,
message: 'This field is required'
message: 'This field is required',
}],
})(
<Input
......@@ -89,87 +98,99 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
type='number'
min={5}
max={100}
suffix={<Icon type='percentage'/>}
/>
suffix={<Icon type='percentage' />}
/>,
)}
</Tooltip>
</Form.Item>
);
}
private renderOverlap() {
private renderOverlap(): JSX.Element {
const { form } = this.props;
return (
<Form.Item label='Overlap size'>
<Tooltip overlay='Defines a number of intersected frames between different segments'>
{this.props.form.getFieldDecorator('overlapSize')(
<Input size='large' type='number'/>
{form.getFieldDecorator('overlapSize')(
<Input size='large' type='number' />,
)}
</Tooltip>
</Form.Item>
);
}
private renderSegmentSize() {
private renderSegmentSize(): JSX.Element {
const { form } = this.props;
return (
<Form.Item label='Segment size'>
<Tooltip overlay='Defines a number of frames in a segment'>
{this.props.form.getFieldDecorator('segmentSize')(
<Input size='large' type='number'/>
{form.getFieldDecorator('segmentSize')(
<Input size='large' type='number' />,
)}
</Tooltip>
</Form.Item>
);
}
private renderStartFrame() {
private renderStartFrame(): JSX.Element {
const { form } = this.props;
return (
<Form.Item label='Start frame'>
{this.props.form.getFieldDecorator('startFrame')(
{form.getFieldDecorator('startFrame')(
<Input
size='large'
type='number'
min={0}
step={1}
/>
/>,
)}
</Form.Item>
);
}
private renderStopFrame() {
private renderStopFrame(): JSX.Element {
const { form } = this.props;
return (
<Form.Item label='Stop frame'>
{this.props.form.getFieldDecorator('stopFrame')(
{form.getFieldDecorator('stopFrame')(
<Input
size='large'
type='number'
min={0}
step={1}
/>
/>,
)}
</Form.Item>
);
}
private renderFrameStep() {
private renderFrameStep(): JSX.Element {
const { form } = this.props;
return (
<Form.Item label='Frame step'>
{this.props.form.getFieldDecorator('frameStep')(
{form.getFieldDecorator('frameStep')(
<Input
size='large'
type='number'
min={1}
step={1}
/>
/>,
)}
</Form.Item>
);
}
private renderGitLFSBox() {
private renderGitLFSBox(): JSX.Element {
const { form } = this.props;
return (
<Form.Item help='If annotation files are large, you can use git LFS feature'>
{this.props.form.getFieldDecorator('lfs', {
{form.getFieldDecorator('lfs', {
valuePropName: 'checked',
initialValue: false,
})(
......@@ -177,22 +198,24 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
<Text className='cvat-black-color'>
Use LFS (Large File Support):
</Text>
</Checkbox>
</Checkbox>,
)}
</Form.Item>
);
}
private renderGitRepositoryURL() {
private renderGitRepositoryURL(): JSX.Element {
const { form } = this.props;
return (
<Form.Item
hasFeedback
label='Dataset repository URL'
extra='Attach a repository to store annotations there'
>
{this.props.form.getFieldDecorator('repository', {
{form.getFieldDecorator('repository', {
rules: [{
validator: (_, value, callback) => {
validator: (_, value, callback): void => {
if (!value) {
callback();
} else {
......@@ -207,18 +230,19 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
callback();
}
}
}]
},
}],
})(
<Input size='large'
<Input
size='large'
placeholder='e.g. https//github.com/user/repos [annotation/<anno_file_name>.zip]'
/>
/>,
)}
</Form.Item>
);
}
private renderGit() {
private renderGit(): JSX.Element {
return (
<>
<Row>
......@@ -235,36 +259,42 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
);
}
private renderBugTracker() {
private renderBugTracker(): JSX.Element {
const { form } = this.props;
return (
<Form.Item
hasFeedback
label='Issue tracker'
extra='Attach issue tracker where the task is described'
>
{this.props.form.getFieldDecorator('bugTracker', {
{form.getFieldDecorator('bugTracker', {
rules: [{
validator: (_, value, callback) => {
validator: (_, value, callback): void => {
if (value && !patterns.validateURL.pattern.test(value)) {
callback('Issue tracker must be URL');
} else {
callback();
}
}
}]
},
}],
})(
<Input size='large'/>
<Input size='large' />,
)}
</Form.Item>
)
);
}
public render() {
public render(): JSX.Element {
const { installedGit } = this.props;
return (
<Form>
<Row><Col>
{this.renderZOrder()}
</Col></Row>
<Row>
<Col>
{this.renderZOrder()}
</Col>
</Row>
<Row type='flex' justify='start'>
<Col span={7}>
......@@ -290,11 +320,11 @@ class AdvancedConfigurationForm extends React.PureComponent<Props> {
</Col>
</Row>
{ this.props.installedGit ? this.renderGit() : null}
{ installedGit ? this.renderGit() : null}
<Row>
<Col>
{this.renderBugTracker()}
{this.renderBugTracker()}
</Col>
</Row>
</Form>
......
......@@ -4,7 +4,6 @@ import {
Input,
} from 'antd';
import Text from 'antd/lib/typography/Text';
import Form, { FormComponentProps } from 'antd/lib/form/Form';
export interface BaseConfiguration {
......@@ -16,11 +15,16 @@ type Props = FormComponentProps & {
};
class BasicConfigurationForm extends React.PureComponent<Props> {
public submit() {
public submit(): Promise<void> {
return new Promise((resolve, reject) => {
this.props.form.validateFields((error, values) => {
const {
form,
onSubmit,
} = this.props;
form.validateFields((error, values): void => {
if (!error) {
this.props.onSubmit({
onSubmit({
name: values.name,
});
resolve();
......@@ -28,25 +32,28 @@ class BasicConfigurationForm extends React.PureComponent<Props> {
reject();
}
});
})
});
}
public resetFields() {
this.props.form.resetFields();
public resetFields(): void {
const { form } = this.props;
form.resetFields();
}
public render() {
const { getFieldDecorator } = this.props.form;
public render(): JSX.Element {
const { form } = this.props;
const { getFieldDecorator } = form;
return (
<Form onSubmit={(e: React.FormEvent) => e.preventDefault()}>
<Form onSubmit={(e: React.FormEvent): void => e.preventDefault()}>
<Form.Item hasFeedback label='Name'>
{ getFieldDecorator('name', {
rules: [{
required: true,
message: 'Please, specify a name',
}]
}],
})(
<Input/>
<Input />,
) }
</Form.Item>
</Form>
......
......@@ -21,7 +21,7 @@ export interface CreateTaskData {
basic: BaseConfiguration;
advanced: AdvancedConfiguration;
labels: any[];
files: Files,
files: Files;
}
interface Props {
......@@ -58,11 +58,33 @@ export default class CreateTaskContent extends React.PureComponent<Props, State>
this.state = { ...defaultState };
}
private validateLabels = () => {
return !!this.state.labels.length;
public componentDidUpdate(prevProps: Props): void {
const { status } = this.props;
if (status === 'CREATED' && prevProps.status !== 'CREATED') {
notification.info({
message: 'The task has been created',
});
this.basicConfigurationComponent.resetFields();
if (this.advancedConfigurationComponent) {
this.advancedConfigurationComponent.resetFields();
}
this.fileManagerContainer.reset();
this.setState({
...defaultState,
});
}
}
private validateFiles = () => {
private validateLabels = (): boolean => {
const { labels } = this.state;
return !!labels.length;
};
private validateFiles = (): boolean => {
const files = this.fileManagerContainer.getFiles();
this.setState({
files,
......@@ -72,21 +94,21 @@ export default class CreateTaskContent extends React.PureComponent<Props, State>
);
return !!totalLen;
}
};
private handleSubmitBasicConfiguration = (values: BaseConfiguration) => {
private handleSubmitBasicConfiguration = (values: BaseConfiguration): void => {
this.setState({
basic: {...values},
basic: { ...values },
});
};
private handleSubmitAdvancedConfiguration = (values: AdvancedConfiguration) => {
private handleSubmitAdvancedConfiguration = (values: AdvancedConfiguration): void => {
this.setState({
advanced: {...values},
advanced: { ...values },
});
};
private handleSubmitClick = () => {
private handleSubmitClick = (): void => {
if (!this.validateLabels()) {
notification.error({
message: 'Could not create a task',
......@@ -105,44 +127,50 @@ export default class CreateTaskContent extends React.PureComponent<Props, State>
this.basicConfigurationComponent.submit()
.then(() => {
return this.advancedConfigurationComponent ?
this.advancedConfigurationComponent.submit() :
new Promise((resolve) => {
resolve();
})
})
.then(() => {
this.props.onCreate(this.state);
})
.catch((_: any) => {
if (this.advancedConfigurationComponent) {
return this.advancedConfigurationComponent.submit();
}
return new Promise((resolve): void => {
resolve();
});
}).then((): void => {
const { onCreate } = this.props;
onCreate(this.state);
}).catch((): void => {
notification.error({
message: 'Could not create a task',
description: 'Please, check configuration you specified',
});
});
}
};
private renderBasicBlock() {
private renderBasicBlock(): JSX.Element {
return (
<Col span={24}>
<BasicConfigurationForm wrappedComponentRef={
(component: any) => { this.basicConfigurationComponent = component }
} onSubmit={this.handleSubmitBasicConfiguration}/>
<BasicConfigurationForm
wrappedComponentRef={
(component: any): void => { this.basicConfigurationComponent = component; }
}
onSubmit={this.handleSubmitBasicConfiguration}
/>
</Col>
);
}
private renderLabelsBlock() {
private renderLabelsBlock(): JSX.Element {
const { labels } = this.state;
return (
<Col span={24}>
<Text type='danger'>* </Text>
<Text className='cvat-black-color'>Labels:</Text>
<LabelsEditor
labels={this.state.labels}
labels={labels}
onSubmit={
(labels) => {
(newLabels): void => {
this.setState({
labels,
labels: newLabels,
});
}
}
......@@ -151,32 +179,37 @@ export default class CreateTaskContent extends React.PureComponent<Props, State>
);
}
private renderFilesBlock() {
private renderFilesBlock(): JSX.Element {
return (
<Col span={24}>
<Text type='danger'>* </Text>
<Text className='cvat-black-color'>Select files:</Text>
<FileManagerContainer ref={
(container: any) =>
this.fileManagerContainer = container
} withRemote={true}/>
<FileManagerContainer
ref={
(container: any): void => { this.fileManagerContainer = container; }
}
withRemote
/>
</Col>
);
}
private renderAdvancedBlock() {
private renderAdvancedBlock(): JSX.Element {
const { installedGit } = this.props;
return (
<Col span={24}>
<Collapse>
<Collapse.Panel
key='1'
header={
<Text className='cvat-title'>Advanced configuration</Text>
} key='1'>
}
>
<AdvancedConfigurationForm
installedGit={this.props.installedGit}
installedGit={installedGit}
wrappedComponentRef={
(component: any) => {
this.advancedConfigurationComponent = component
(component: any): void => {
this.advancedConfigurationComponent = component;
}
}
onSubmit={this.handleSubmitAdvancedConfiguration}
......@@ -187,29 +220,9 @@ export default class CreateTaskContent extends React.PureComponent<Props, State>
);
}
public componentDidUpdate(prevProps: Props) {
if (this.props.status === 'CREATED' && prevProps.status !== 'CREATED') {
notification.info({
message: 'The task has been created',
});
this.basicConfigurationComponent.resetFields();
if (this.advancedConfigurationComponent) {
this.advancedConfigurationComponent.resetFields();
}
this.fileManagerContainer.reset();
this.setState({
...defaultState,
});
}
}
public render() {
const loading = !!this.props.status
&& this.props.status !== 'CREATED'
&& this.props.status !== 'FAILED';
public render(): JSX.Element {
const { status } = this.props;
const loading = !!status && status !== 'CREATED' && status !== 'FAILED';
return (
<Row type='flex' justify='start' align='middle' className='cvat-create-task-content'>
......@@ -223,7 +236,7 @@ export default class CreateTaskContent extends React.PureComponent<Props, State>
{ this.renderAdvancedBlock() }
<Col span={18}>
{loading ? <Alert message={this.props.status}/> : null}
{loading ? <Alert message={status} /> : null}
</Col>
<Col span={6}>
<Button
......@@ -231,7 +244,9 @@ export default class CreateTaskContent extends React.PureComponent<Props, State>
disabled={loading}
type='danger'
onClick={this.handleSubmitClick}
> Submit </Button>
>
Submit
</Button>
</Col>
</Row>
);
......
......@@ -15,15 +15,21 @@ interface Props {
installedGit: boolean;
}
export default function CreateTaskPage(props: Props) {
export default function CreateTaskPage(props: Props): JSX.Element {
const {
status,
onCreate,
installedGit,
} = props;
return (
<Row type='flex' justify='center' align='top' className='cvat-create-task-form-wrapper'>
<Col md={20} lg={16} xl={14} xxl={9}>
<Text className='cvat-title'>Create a new task</Text>
<CreateTaskContent
status={props.status}
onCreate={props.onCreate}
installedGit={props.installedGit}
status={status}
onCreate={onCreate}
installedGit={installedGit}
/>
</Col>
</Row>
......
......@@ -46,28 +46,73 @@ type CVATAppProps = {
installedTFSegmentation: boolean;
notifications: NotificationsState;
user: any;
}
};
export default class CVATApplication extends React.PureComponent<CVATAppProps> {
constructor(props: any) {
super(props);
public componentDidMount(): void {
const { verifyAuthorized } = this.props;
verifyAuthorized();
}
private showMessages() {
function showMessage(title: string) {
public componentDidUpdate(): void {
const {
loadFormats,
loadUsers,
initPlugins,
userInitialized,
formatsInitialized,
formatsFetching,
usersInitialized,
usersFetching,
pluginsInitialized,
pluginsFetching,
user,
} = this.props;
this.showErrors();
this.showMessages();
if (!userInitialized || user == null) {
// not authorized user
return;
}
if (!formatsInitialized && !formatsFetching) {
loadFormats();
}
if (!usersInitialized && !usersFetching) {
loadUsers();
}
if (!pluginsInitialized && !pluginsFetching) {
initPlugins();
}
}
private showMessages(): void {
function showMessage(title: string): void {
notification.info({
message: (
<div dangerouslySetInnerHTML={{
__html: title,
}}/>
<div
// eslint-disable-next-line
dangerouslySetInnerHTML={{
__html: title,
}}
/>
),
duration: null,
});
}
const { tasks } = this.props.notifications.messages;
const { models } = this.props.notifications.messages;
let shown = !!tasks.loadingDone || !!models.inferenceDone;
const {
notifications,
resetMessages,
} = this.props;
const { tasks } = notifications.messages;
const { models } = notifications.messages;
const shown = !!tasks.loadingDone || !!models.inferenceDone;
if (tasks.loadingDone) {
showMessage(tasks.loadingDone);
......@@ -77,18 +122,21 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
}
if (shown) {
this.props.resetMessages();
resetMessages();
}
}
private showErrors() {
function showError(title: string, _error: any) {
private showErrors(): void {
function showError(title: string, _error: any): void {
const error = _error.toString();
notification.error({
message: (
<div dangerouslySetInnerHTML={{
__html: title,
}}/>
<div
// eslint-disable-next-line
dangerouslySetInnerHTML={{
__html: title,
}}
/>
),
duration: null,
description: error.length > 200 ? '' : error,
......@@ -97,14 +145,19 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
console.error(error);
}
const { auth } = this.props.notifications.errors;
const { tasks } = this.props.notifications.errors;
const { formats } = this.props.notifications.errors;
const { users } = this.props.notifications.errors;
const { share } = this.props.notifications.errors;
const { models } = this.props.notifications.errors;
const {
notifications,
resetErrors,
} = this.props;
const { auth } = notifications.errors;
const { tasks } = notifications.errors;
const { formats } = notifications.errors;
const { users } = notifications.errors;
const { share } = notifications.errors;
const { models } = notifications.errors;
let shown = !!auth.authorized || !!auth.login || !!auth.logout || !!auth.register
const shown = !!auth.authorized || !!auth.login || !!auth.logout || !!auth.register
|| !!tasks.fetching || !!tasks.updating || !!tasks.dumping || !!tasks.loading
|| !!tasks.exporting || !!tasks.deleting || !!tasks.creating || !!formats.fetching
|| !!users.fetching || !!share.fetching || !!models.creating || !!models.starting
......@@ -169,90 +222,78 @@ export default class CVATApplication extends React.PureComponent<CVATAppProps> {
showError(models.metaFetching.message, models.metaFetching.reason);
}
if (models.inferenceStatusFetching) {
showError(models.inferenceStatusFetching.message, models.inferenceStatusFetching.reason);
showError(
models.inferenceStatusFetching.message,
models.inferenceStatusFetching.reason,
);
}
if (shown) {
this.props.resetErrors();
}
}
public componentDidMount() {
this.props.verifyAuthorized();
}
public componentDidUpdate() {
this.showErrors();
this.showMessages();
if (!this.props.userInitialized || this.props.user == null) {
// not authorized user
return;
}
if (!this.props.formatsInitialized && !this.props.formatsFetching) {
this.props.loadFormats();
}
if (!this.props.usersInitialized && !this.props.usersFetching) {
this.props.loadUsers();
}
if (!this.props.pluginsInitialized && !this.props.pluginsFetching) {
this.props.initPlugins();
resetErrors();
}
}
// Where you go depends on your URL
public render() {
const readyForRender =
(this.props.userInitialized && this.props.user == null) ||
(this.props.userInitialized && this.props.formatsInitialized &&
this.props.pluginsInitialized && this.props.usersInitialized);
public render(): JSX.Element {
const {
userInitialized,
usersInitialized,
pluginsInitialized,
formatsInitialized,
installedAutoAnnotation,
installedTFSegmentation,
installedTFAnnotation,
user,
} = this.props;
const withModels = this.props.installedAutoAnnotation
|| this.props.installedTFAnnotation || this.props.installedTFSegmentation;
const readyForRender = (userInitialized && user == null)
|| (userInitialized && formatsInitialized
&& pluginsInitialized && usersInitialized);
const withModels = installedAutoAnnotation
|| installedTFAnnotation || installedTFSegmentation;
if (readyForRender) {
if (this.props.user) {
if (user) {
return (
<BrowserRouter>
<Layout>
<HeaderContainer> </HeaderContainer>
<Layout.Content>
<Switch>
<Route exact path='/tasks' component={TasksPageContainer}/>
<Route path='/tasks/create' component={CreateTaskPageContainer}/>
<Route exact path='/tasks/:id' component={TaskPageContainer}/>
<Route path='/tasks/:id/jobs/:id' component={AnnotationPageContainer}/>
{ withModels &&
<Route exact path='/models' component={ModelsPageContainer}/> }
{ this.props.installedAutoAnnotation &&
<Route path='/models/create' component={CreateModelPageContainer}/> }
<Redirect push to='/tasks'/>
<Route exact path='/tasks' component={TasksPageContainer} />
<Route path='/tasks/create' component={CreateTaskPageContainer} />
<Route exact path='/tasks/:id' component={TaskPageContainer} />
<Route path='/tasks/:id/jobs/:id' component={AnnotationPageContainer} />
{ withModels
&& <Route exact path='/models' component={ModelsPageContainer} /> }
{ installedAutoAnnotation
&& <Route path='/models/create' component={CreateModelPageContainer} /> }
<Redirect push to='/tasks' />
</Switch>
<FeedbackComponent/>
<ModelRunnerModalContainer/>
<FeedbackComponent />
<ModelRunnerModalContainer />
{/* eslint-disable-next-line */}
<a id='downloadAnchor' style={{ display: 'none' }} download/>
</Layout.Content>
</Layout>
</BrowserRouter>
);
} else {
return (
<BrowserRouter>
<Switch>
<Route exact path='/auth/register' component={RegisterPageContainer}/>
<Route exact path='/auth/login' component={LoginPageContainer}/>
<Redirect to='/auth/login'/>
</Switch>
</BrowserRouter>
);
}
} else {
return (
<Spin size='large' style={{margin: '25% 50%'}}/>
<BrowserRouter>
<Switch>
<Route exact path='/auth/register' component={RegisterPageContainer} />
<Route exact path='/auth/login' component={LoginPageContainer} />
<Redirect to='/auth/login' />
</Switch>
</BrowserRouter>
);
}
return (
<Spin size='large' style={{ margin: '25% 50%' }} />
);
}
}
......@@ -31,88 +31,87 @@ interface State {
active: boolean;
}
export default class Feedback extends React.PureComponent<{}, State> {
public constructor(props: {}) {
super(props);
this.state = {
active: false,
}
}
function renderContent(): JSX.Element {
const githubURL = 'https://github.com/opencv/cvat';
const githubImage = 'https://raw.githubusercontent.com/opencv/'
+ 'cvat/develop/cvat/apps/documentation/static/documentation/images/cvat.jpg';
const questionsURL = 'https://gitter.im/opencv-cvat/public';
const feedbackURL = 'https://gitter.im/opencv-cvat/public';
private renderContent() {
const githubURL = 'https://github.com/opencv/cvat';
const githubImage = 'https://raw.githubusercontent.com/opencv/'
+ 'cvat/develop/cvat/apps/documentation/static/documentation/images/cvat.jpg';
const questionsURL = 'https://gitter.im/opencv-cvat/public';
const feedbackURL = 'https://gitter.im/opencv-cvat/public';
return (
<>
<Icon type='star' />
<Text style={{ marginLeft: '10px' }}>
Star us on
<a target='_blank' rel='noopener noreferrer' href={githubURL}> GitHub</a>
</Text>
<br />
<Icon type='like' />
<Text style={{ marginLeft: '10px' }}>
Leave a
<a target='_blank' rel='noopener noreferrer' href={feedbackURL}> feedback</a>
</Text>
<hr />
<div style={{ display: 'flex' }}>
<FacebookShareButton url={githubURL} quote='Computer Vision Annotation Tool'>
<FacebookIcon size={32} round />
</FacebookShareButton>
<VKShareButton url={githubURL} title='Computer Vision Annotation Tool' image={githubImage} description='CVAT'>
<VKIcon size={32} round />
</VKShareButton>
<TwitterShareButton url={githubURL} title='Computer Vision Annotation Tool' hashtags={['CVAT']}>
<TwitterIcon size={32} round />
</TwitterShareButton>
<RedditShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<RedditIcon size={32} round />
</RedditShareButton>
<LinkedinShareButton url={githubURL}>
<LinkedinIcon size={32} round />
</LinkedinShareButton>
<TelegramShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<TelegramIcon size={32} round />
</TelegramShareButton>
<WhatsappShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<WhatsappIcon size={32} round />
</WhatsappShareButton>
<ViberShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<ViberIcon size={32} round />
</ViberShareButton>
</div>
<hr />
<Text style={{ marginTop: '50px' }}>
Do you need help? Contact us on
<a target='_blank' rel='noopener noreferrer' href={questionsURL}> gitter</a>
</Text>
</>
);
}
return (
<>
<Icon type='star'/>
<Text style={{marginLeft: '10px'}}>
Star us on <a target='_blank' href={githubURL}>GitHub</a>
</Text>
<br/>
<Icon type='like'/>
<Text style={{marginLeft: '10px'}}>
Leave a <a target='_blank' href={feedbackURL}>feedback</a>
</Text>
<hr/>
<div style={{display: 'flex'}}>
<FacebookShareButton url={githubURL} quote='Computer Vision Annotation Tool'>
<FacebookIcon size={32} round={true} />
</FacebookShareButton>
<VKShareButton url={githubURL} title='Computer Vision Annotation Tool' image={githubImage} description='CVAT'>
<VKIcon size={32} round={true} />
</VKShareButton>
<TwitterShareButton url={githubURL} title='Computer Vision Annotation Tool' hashtags={['CVAT']}>
<TwitterIcon size={32} round={true} />
</TwitterShareButton>
<RedditShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<RedditIcon size={32} round={true} />
</RedditShareButton>
<LinkedinShareButton url={githubURL}>
<LinkedinIcon size={32} round={true} />
</LinkedinShareButton>
<TelegramShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<TelegramIcon size={32} round={true} />
</TelegramShareButton>
<WhatsappShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<WhatsappIcon size={32} round={true} />
</WhatsappShareButton>
<ViberShareButton url={githubURL} title='Computer Vision Annotation Tool'>
<ViberIcon size={32} round={true} />
</ViberShareButton>
</div>
<hr/>
<Text style={{marginTop: '50px'}}>
Do you need help? Contact us on <a href={questionsURL}>gitter</a>
</Text>
</>
);
}
export default function Feedback(): JSX.Element {
const [visible, setVisible] = React.useState(false);
public render() {
return (
<>
<Popover
placement='leftTop'
title={
<Text className='cvat-black-color'>Help to make CVAT better</Text>
}
content={this.renderContent()}
visible={this.state.active}
return (
<>
<Popover
placement='leftTop'
title={
<Text className='cvat-black-color'>Help to make CVAT better</Text>
}
content={renderContent()}
visible={visible}
>
<Button
style={{ color: '#ff4d4f' }}
className='cvat-feedback-button'
type='link'
onClick={(): void => {
setVisible(!visible);
}}
>
<Button style={{color: '#ff4d4f'}} className='cvat-feedback-button' type='link' onClick={() => {
this.setState({
active: !this.state.active,
});
}}>
{ this.state.active ? <Icon type='close-circle' theme='filled'/> :
<Icon type='message' theme='twoTone'/> }
</Button>
</Popover>
</>
);
}
}
\ No newline at end of file
{ visible ? <Icon type='close-circle' theme='filled' />
: <Icon type='message' theme='twoTone' /> }
</Button>
</Popover>
</>
);
}
......@@ -44,35 +44,61 @@ export default class FileManager extends React.PureComponent<Props, State> {
};
this.loadData('/');
};
}
private loadData = (key: string) => {
const promise = new Promise<void>((resolve, reject) => {
const success = () => resolve();
const failure = () => reject();
this.props.onLoadData(key, success, failure);
});
public getFiles(): Files {
const {
active,
files,
} = this.state;
return {
local: active === 'local' ? files.local : [],
share: active === 'share' ? files.share : [],
remote: active === 'remote' ? files.remote : [],
};
}
private loadData = (key: string): Promise<void> => new Promise<void>(
(resolve, reject): void => {
const { onLoadData } = this.props;
const success = (): void => resolve();
const failure = (): void => reject();
onLoadData(key, success, failure);
},
);
return promise;
public reset(): void {
this.setState({
expandedKeys: [],
active: 'local',
files: {
local: [],
share: [],
remote: [],
},
});
}
private renderLocalSelector() {
private renderLocalSelector(): JSX.Element {
const { files } = this.state;
return (
<Tabs.TabPane key='local' tab='My computer'>
<Upload.Dragger
multiple
fileList={this.state.files.local as any[]}
fileList={files.local as any[]}
showUploadList={false}
beforeUpload={(_: RcFile, files: RcFile[]) => {
beforeUpload={(_: RcFile, newLocalFiles: RcFile[]): boolean => {
this.setState({
files: {
...this.state.files,
local: files
...files,
local: newLocalFiles,
},
});
return false;
}
}>
}}
>
<p className='ant-upload-drag-icon'>
<Icon type='inbox' />
</p>
......@@ -81,115 +107,126 @@ export default class FileManager extends React.PureComponent<Props, State> {
Support for a bulk images or a single video
</p>
</Upload.Dragger>
{ !!this.state.files.local.length &&
<>
<br/>
<Text className='cvat-black-color'>
{`${this.state.files.local.length} file(s) selected`}
</Text>
</>
{ !!files.local.length
&& (
<>
<br />
<Text className='cvat-black-color'>
{`${files.local.length} file(s) selected`}
</Text>
</>
)
}
</Tabs.TabPane>
);
}
private renderShareSelector() {
function renderTreeNodes(data: TreeNodeNormal[]) {
private renderShareSelector(): JSX.Element {
function renderTreeNodes(data: TreeNodeNormal[]): JSX.Element[] {
return data.map((item: TreeNodeNormal) => {
if (item.children) {
return (
<Tree.TreeNode title={item.title} key={item.key} dataRef={item} isLeaf={item.isLeaf}>
{renderTreeNodes(item.children)}
</Tree.TreeNode>
);
return (
<Tree.TreeNode
title={item.title}
key={item.key}
dataRef={item}
isLeaf={item.isLeaf}
>
{renderTreeNodes(item.children)}
</Tree.TreeNode>
);
}
return <Tree.TreeNode key={item.key} {...item} dataRef={item} />;
});
}
const { treeData } = this.props;
const {
expandedKeys,
files,
} = this.state;
return (
<Tabs.TabPane key='share' tab='Connected file share'>
{ this.props.treeData.length ?
<Tree
className='cvat-share-tree'
checkable
showLine
checkStrictly={false}
expandedKeys={this.state.expandedKeys}
checkedKeys={this.state.files.share}
loadData={(node: AntTreeNode) => {
return this.loadData(node.props.dataRef.key);
}}
onExpand={(expandedKeys: string[]) => {
this.setState({
expandedKeys,
});
}}
onCheck={(checkedKeys: string[] | {checked: string[], halfChecked: string[]}) => {
const keys = checkedKeys as string[];
this.setState({
files: {
...this.state.files,
share: keys,
},
});
}}>
{ renderTreeNodes(this.props.treeData) }
</Tree> : <Text className='cvat-black-color'>{'No data found'}</Text>
{ treeData.length
? (
<Tree
className='cvat-share-tree'
checkable
showLine
checkStrictly={false}
expandedKeys={expandedKeys}
checkedKeys={files.share}
loadData={(node: AntTreeNode): Promise<void> => this.loadData(
node.props.dataRef.key,
)}
onExpand={(newExpandedKeys: string[]): void => {
this.setState({
expandedKeys: newExpandedKeys,
});
}}
onCheck={
(checkedKeys: string[] | {
checked: string[];
halfChecked: string[];
}): void => {
const keys = checkedKeys as string[];
this.setState({
files: {
...files,
share: keys,
},
});
}}
>
{ renderTreeNodes(treeData) }
</Tree>
) : <Text className='cvat-black-color'>No data found</Text>
}
</Tabs.TabPane>
);
}
private renderRemoteSelector() {
private renderRemoteSelector(): JSX.Element {
const { files } = this.state;
return (
<Tabs.TabPane key='remote' tab='Remote sources'>
<Input.TextArea
placeholder='Enter one URL per line'
rows={6}
value={[...this.state.files.remote].join('\n')}
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>) => {
value={[...files.remote].join('\n')}
onChange={(event: React.ChangeEvent<HTMLTextAreaElement>): void => {
this.setState({
files: {
...this.state.files,
...files,
remote: event.target.value.split('\n'),
},
});
}}/>
}}
/>
</Tabs.TabPane>
);
}
public getFiles(): Files {
return {
local: this.state.active === 'local' ? this.state.files.local : [],
share: this.state.active === 'share' ? this.state.files.share : [],
remote: this.state.active === 'remote' ? this.state.files.remote : [],
};
}
public reset() {
this.setState({
expandedKeys: [],
active: 'local',
files: {
local: [],
share: [],
remote: [],
},
});
}
public render(): JSX.Element {
const { withRemote } = this.props;
public render() {
return (
<>
<Tabs type='card' tabBarGutter={5} onChange={(activeKey: string) => this.setState({
active: activeKey as any,
})}>
<Tabs
type='card'
tabBarGutter={5}
onChange={
(activeKey: string): void => this.setState({
active: activeKey as any,
})
}
>
{ this.renderLocalSelector() }
{ this.renderShareSelector() }
{ this.props.withRemote && this.renderRemoteSelector() }
{ withRemote && this.renderRemoteSelector() }
</Tabs>
</>
);
......
......@@ -15,6 +15,7 @@ import Text from 'antd/lib/typography/Text';
import getCore from '../../core';
const core = getCore();
const serverHost = core.config.backendAPI.slice(0, -7);
interface HeaderContainerProps {
onLogout: () => void;
......@@ -28,66 +29,121 @@ interface HeaderContainerProps {
type Props = HeaderContainerProps & RouteComponentProps;
const cvatLogo = () => <img src='/assets/cvat-logo.svg'/>;
const userLogo = () => <img src='/assets/icon-account.svg'/>;
const cvatLogo = (): JSX.Element => <img alt='' src='/assets/cvat-logo.svg' />;
const userLogo = (): JSX.Element => <img alt='' src='/assets/icon-account.svg' />;
function HeaderContainer(props: Props): JSX.Element {
const {
installedTFSegmentation,
installedAutoAnnotation,
installedTFAnnotation,
installedAnalytics,
username,
onLogout,
logoutFetching,
} = props;
const renderModels = installedAutoAnnotation
|| installedTFAnnotation
|| installedTFSegmentation;
function HeaderContainer(props: Props) {
const renderModels = props.installedAutoAnnotation
|| props.installedTFAnnotation
|| props.installedTFSegmentation;
return (
<Layout.Header className='cvat-header'>
<div className='cvat-left-header'>
<Icon className='cvat-logo-icon' component={cvatLogo}/>
<Icon className='cvat-logo-icon' component={cvatLogo} />
<Button className='cvat-header-button' type='link' value='tasks' onClick={
() => props.history.push('/tasks')
}> Tasks </Button>
{ renderModels ?
<Button className='cvat-header-button' type='link' value='models' onClick={
() => props.history.push('/models')
}> Models </Button> : null
<Button
className='cvat-header-button'
type='link'
value='tasks'
onClick={
(): void => props.history.push('/tasks')
}
>
Tasks
</Button>
{ renderModels
&& (
<Button
className='cvat-header-button'
type='link'
value='models'
onClick={
(): void => props.history.push('/models')
}
>
Models
</Button>
)
}
{ props.installedAnalytics ?
<Button className='cvat-header-button' type='link' onClick={
() => {
const serverHost = core.config.backendAPI.slice(0, -7);
window.open(`${serverHost}/analytics/app/kibana`, '_blank');
}
}> Analytics </Button> : null
{ installedAnalytics
&& (
<Button
className='cvat-header-button'
type='link'
onClick={
(): void => {
// false positive
// eslint-disable-next-line
window.open(`${serverHost}/analytics/app/kibana`, '_blank');
}
}
>
Analytics
</Button>
)
}
</div>
<div className='cvat-right-header'>
<Button className='cvat-header-button' type='link' onClick={
() => window.open('https://github.com/opencv/cvat', '_blank')
}>
<Icon type='github'/>
<Button
className='cvat-header-button'
type='link'
onClick={
(): void => {
window.open('https://github.com/opencv/cvat', '_blank');
}
}
>
<Icon type='github' />
<Text className='cvat-black-color'>GitHub</Text>
</Button>
<Button className='cvat-header-button' type='link' onClick={
() => {
const serverHost = core.config.backendAPI.slice(0, -7);
<Button
className='cvat-header-button'
type='link'
onClick={
(): void => {
// false positive
// eslint-disable-next-line
window.open(`${serverHost}/documentation/user_guide.html`, '_blank')
}
}> <Icon type='question-circle'/> Help </Button>
}
>
<Icon type='question-circle' />
Help
</Button>
<Menu className='cvat-header-menu' subMenuCloseDelay={0.1} mode='horizontal'>
<Menu.SubMenu title={
<span>
<Icon className='cvat-header-user-icon' component={userLogo} />
<span>
<Text strong>
{props.username.length > 14 ? `${props.username.slice(0, 10)} ...` : props.username}
</Text>
<Icon className='cvat-header-menu-icon' type='caret-down'/>
</span>
</span>
}>
<Menu.SubMenu
title={
(
<span>
<Icon className='cvat-header-user-icon' component={userLogo} />
<span>
<Text strong>
{username.length > 14 ? `${username.slice(0, 10)} ...` : username}
</Text>
<Icon className='cvat-header-menu-icon' type='caret-down' />
</span>
</span>
)
}
>
<Menu.Item
onClick={props.onLogout}
disabled={props.logoutFetching}
onClick={onLogout}
disabled={logoutFetching}
className='cvat-header-button'
>
{props.logoutFetching && <Icon type='loading'/>} Logout
{logoutFetching && <Icon type='loading' />}
Logout
</Menu.Item>
</Menu.SubMenu>
</Menu>
......
import React from 'react';
import LabelForm from './label-form';
import { Label, Attribute } from './common';
import LabelForm from './label-form';
import { Label } from './common';
interface Props {
onCreate: (label: Label | null) => void;
}
interface State {
attributes: Attribute[];
}
export default class ConstructorCreator extends React.PureComponent<Props, State> {
public constructor(props: Props) {
super(props);
}
public render() {
return (
<div className='cvat-label-constructor-creator'>
<LabelForm label={null} onSubmit={this.props.onCreate}/>
</div>
);
}
export default function ConstructorCreator(props: Props): JSX.Element {
const { onCreate } = props;
return (
<div className='cvat-label-constructor-creator'>
<LabelForm label={null} onSubmit={onCreate} />
</div>
);
}
import React from 'react';
import LabelForm from './label-form';
import { Label, Attribute } from './common';
import { Label } from './common';
interface Props {
label: Label;
onUpdate: (label: Label | null) => void;
}
interface State {
savedAttributes: Attribute[];
unsavedAttributes: Attribute[];
}
export default class ConstructorUpdater extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
}
export default function ConstructorUpdater(props: Props): JSX.Element {
const {
label,
onUpdate,
} = props;
public render() {
return (
<div className='cvat-label-constructor-updater'>
<LabelForm label={this.props.label} onSubmit={this.props.onUpdate}/>
</div>
);
}
return (
<div className='cvat-label-constructor-updater'>
<LabelForm label={label} onSubmit={onUpdate} />
</div>
);
}
......@@ -16,21 +16,40 @@ interface ConstructorViewerItemProps {
onDelete: (label: Label) => void;
}
export default function ConstructorViewerItem(props: ConstructorViewerItemProps) {
export default function ConstructorViewerItem(props: ConstructorViewerItemProps): JSX.Element {
const {
color,
label,
onUpdate,
onDelete,
} = props;
return (
<div style={{background: props.color}} className='cvat-constructor-viewer-item'>
<Text>{ props.label.name }</Text>
<div style={{ background: color }} className='cvat-constructor-viewer-item'>
<Text>{label.name}</Text>
<Tooltip title='Update attributes'>
<span onClick={() => props.onUpdate(props.label)}>
<Icon theme='filled' type='edit'/>
<span
role='button'
tabIndex={0}
onClick={(): void => onUpdate(label)}
onKeyPress={(): boolean => false}
>
<Icon theme='filled' type='edit' />
</span>
</Tooltip>
{ props.label.id >= 0 ? null :
<Tooltip title='Delete label'>
<span onClick={() => props.onDelete(props.label)}>
<Icon type='close'></Icon>
</span>
</Tooltip>
{ label.id < 0
&& (
<Tooltip title='Delete label'>
<span
role='button'
tabIndex={0}
onClick={(): void => onDelete(label)}
onKeyPress={(): boolean => false}
>
<Icon type='close' />
</span>
</Tooltip>
)
}
</div>
);
......
......@@ -23,7 +23,7 @@ const colors = [
let currentColor = 0;
function nextColor() {
function nextColor(): string {
const color = colors[currentColor];
currentColor += 1;
if (currentColor >= colors.length) {
......@@ -32,12 +32,14 @@ function nextColor() {
return color;
}
export default function ConstructorViewer(props: ConstructorViewerProps) {
export default function ConstructorViewer(props: ConstructorViewerProps): JSX.Element {
const { onCreate } = props;
currentColor = 0;
const list = [
<Button key='create' type='ghost' onClick={props.onCreate} className='cvat-constructor-viewer-new-item'>
Add label <Icon type='plus-circle'/>
<Button key='create' type='ghost' onClick={onCreate} className='cvat-constructor-viewer-new-item'>
Add label
<Icon type='plus-circle' />
</Button>];
for (const label of props.labels) {
list.push(
......@@ -47,8 +49,8 @@ export default function ConstructorViewer(props: ConstructorViewerProps) {
label={label}
key={label.id}
color={nextColor()}
/>
)
/>,
);
}
return (
......@@ -56,4 +58,4 @@ export default function ConstructorViewer(props: ConstructorViewerProps) {
{ list }
</div>
);
}
\ No newline at end of file
}
......@@ -39,7 +39,6 @@ interface LabelsEditorState {
export default class LabelsEditor
extends React.PureComponent<LabelsEditortProps, LabelsEditorState> {
public constructor(props: LabelsEditortProps) {
super(props);
......@@ -51,37 +50,46 @@ export default class LabelsEditor
};
}
private handleSubmit(savedLabels: Label[], unsavedLabels: Label[]) {
function transformLabel(label: Label): any {
public componentDidMount(): void {
// just need performe the same code
this.componentDidUpdate(null as any as LabelsEditortProps);
}
public componentDidUpdate(prevProps: LabelsEditortProps): void {
function transformLabel(label: any): Label {
return {
name: label.name,
id: label.id < 0 ? undefined : label.id,
attributes: label.attributes.map((attr: Attribute): any => {
return {
id: label.id || idGenerator(),
attributes: label.attributes.map((attr: any): Attribute => (
{
id: attr.id || idGenerator(),
name: attr.name,
id: attr.id < 0 ? undefined : attr.id,
input_type: attr.type.toLowerCase(),
default_value: attr.values[0],
type: attr.input_type,
mutable: attr.mutable,
values: [...attr.values],
};
}),
}
}
)),
};
}
const output = [];
for (const label of savedLabels.concat(unsavedLabels)) {
output.push(transformLabel(label));
}
const { labels } = this.props;
this.props.onSubmit(output);
if (!prevProps || prevProps.labels !== labels) {
const transformedLabels = labels.map(transformLabel);
this.setState({
savedLabels: transformedLabels
.filter((label: Label) => label.id >= 0),
unsavedLabels: transformedLabels
.filter((label: Label) => label.id < 0),
});
}
}
private handleRawSubmit = (labels: Label[]) => {
private handleRawSubmit = (labels: Label[]): void => {
const unsavedLabels = [];
const savedLabels = [];
for (let label of labels) {
for (const label of labels) {
if (label.id >= 0) {
savedLabels.push(label);
} else {
......@@ -95,29 +103,60 @@ export default class LabelsEditor
});
this.handleSubmit(savedLabels, unsavedLabels);
}
};
private handleCreate = (label: Label | null): void => {
if (label === null) {
this.setState({
constructorMode: ConstructorMode.SHOW,
});
} else {
const {
unsavedLabels,
savedLabels,
} = this.state;
const newUnsavedLabels = [
...unsavedLabels,
{
...label,
id: idGenerator(),
},
];
this.setState({
unsavedLabels: newUnsavedLabels,
});
this.handleSubmit(savedLabels, newUnsavedLabels);
}
};
private handleUpdate = (label: Label | null): void => {
const {
savedLabels,
unsavedLabels,
} = this.state;
private handleUpdate = (label: Label | null) => {
if (label) {
const savedLabels = this.state.savedLabels
const filteredSavedLabels = savedLabels
.filter((_label: Label) => _label.id !== label.id);
const unsavedLabels = this.state.unsavedLabels
const filteredUnsavedLabels = unsavedLabels
.filter((_label: Label) => _label.id !== label.id);
if (label.id >= 0) {
savedLabels.push(label);
filteredSavedLabels.push(label);
this.setState({
savedLabels,
savedLabels: filteredSavedLabels,
constructorMode: ConstructorMode.SHOW,
});
} else {
unsavedLabels.push(label);
filteredUnsavedLabels.push(label);
this.setState({
unsavedLabels,
unsavedLabels: filteredUnsavedLabels,
constructorMode: ConstructorMode.SHOW,
});
}
this.handleSubmit(savedLabels, unsavedLabels);
this.handleSubmit(filteredSavedLabels, filteredUnsavedLabels);
} else {
this.setState({
constructorMode: ConstructorMode.SHOW,
......@@ -125,128 +164,132 @@ export default class LabelsEditor
}
};
private handleDelete = (label: Label) => {
private handleDelete = (label: Label): void => {
// the label is saved on the server, cannot delete it
if (typeof(label.id) !== 'undefined' && label.id >= 0) {
if (typeof (label.id) !== 'undefined' && label.id >= 0) {
notification.error({
message: 'Could not delete the label',
description: 'It has been already saved on the server',
});
}
const unsavedLabels = this.state.unsavedLabels.filter(
(_label: Label) => _label.id !== label.id
const {
unsavedLabels,
savedLabels,
} = this.state;
const filteredUnsavedLabels = unsavedLabels.filter(
(_label: Label): boolean => _label.id !== label.id,
);
this.setState({
unsavedLabels: [...unsavedLabels],
unsavedLabels: filteredUnsavedLabels,
});
this.handleSubmit(this.state.savedLabels, unsavedLabels);
};
private handleCreate = (label: Label | null) => {
if (label === null) {
this.setState({
constructorMode: ConstructorMode.SHOW,
});
} else {
const unsavedLabels = [...this.state.unsavedLabels,
{
...label,
id: idGenerator()
}
];
this.setState({
unsavedLabels,
});
this.handleSubmit(this.state.savedLabels, unsavedLabels);
}
this.handleSubmit(savedLabels, filteredUnsavedLabels);
};
public componentDidMount() {
this.componentDidUpdate(null as any as LabelsEditortProps);
}
public componentDidUpdate(prevProps: LabelsEditortProps) {
function transformLabel(label: any): Label {
private handleSubmit(savedLabels: Label[], unsavedLabels: Label[]): void {
function transformLabel(label: Label): any {
return {
name: label.name,
id: label.id || idGenerator(),
attributes: label.attributes.map((attr: any): Attribute => {
return {
id: attr.id || idGenerator(),
id: label.id < 0 ? undefined : label.id,
attributes: label.attributes.map((attr: Attribute): any => (
{
name: attr.name,
type: attr.input_type,
id: attr.id < 0 ? undefined : attr.id,
input_type: attr.type.toLowerCase(),
default_value: attr.values[0],
mutable: attr.mutable,
values: [...attr.values],
};
}),
}
}
)),
};
}
if (!prevProps || prevProps.labels !== this.props.labels) {
const transformedLabels = this.props.labels.map(transformLabel);
this.setState({
savedLabels: transformedLabels
.filter((label: Label) => label.id >= 0),
unsavedLabels: transformedLabels
.filter((label: Label) => label.id < 0),
});
const { onSubmit } = this.props;
const output = [];
for (const label of savedLabels.concat(unsavedLabels)) {
output.push(transformLabel(label));
}
onSubmit(output);
}
public render() {
public render(): JSX.Element {
const {
savedLabels,
unsavedLabels,
constructorMode,
labelForUpdate,
} = this.state;
return (
<Tabs defaultActiveKey='2' type='card' tabBarStyle={{marginBottom: '0px'}}>
<Tabs.TabPane tab={
<span>
<Icon type='edit'/>
<Text>Raw</Text>
</span>
} key='1'>
<Tabs defaultActiveKey='2' type='card' tabBarStyle={{ marginBottom: '0px' }}>
<Tabs.TabPane
tab={
(
<span>
<Icon type='edit' />
<Text>Raw</Text>
</span>
)
}
key='1'
>
<RawViewer
labels={[...this.state.savedLabels, ...this.state.unsavedLabels]}
labels={[...savedLabels, ...unsavedLabels]}
onSubmit={this.handleRawSubmit}
/>
</Tabs.TabPane>
<Tabs.TabPane tab={
<span>
<Icon type='build'/>
<Text>Constructor</Text>
</span>
} key='2'>
<Tabs.TabPane
tab={
(
<span>
<Icon type='build' />
<Text>Constructor</Text>
</span>
)
}
key='2'
>
{
constructorMode === ConstructorMode.SHOW
&& (
<ConstructorViewer
labels={[...savedLabels, ...unsavedLabels]}
onUpdate={(label: Label): void => {
this.setState({
constructorMode: ConstructorMode.UPDATE,
labelForUpdate: label,
});
}}
onDelete={this.handleDelete}
onCreate={(): void => {
this.setState({
constructorMode: ConstructorMode.CREATE,
});
}}
/>
)
}
{
constructorMode === ConstructorMode.UPDATE
&& labelForUpdate !== null && (
<ConstructorUpdater
label={labelForUpdate}
onUpdate={this.handleUpdate}
/>
)
}
{
this.state.constructorMode === ConstructorMode.SHOW ?
<ConstructorViewer
labels={[...this.state.savedLabels, ...this.state.unsavedLabels]}
onUpdate={(label: Label) => {
this.setState({
constructorMode: ConstructorMode.UPDATE,
labelForUpdate: label,
});
}}
onDelete={this.handleDelete}
onCreate={() => {
this.setState({
constructorMode: ConstructorMode.CREATE,
})
}}
/> :
this.state.constructorMode === ConstructorMode.UPDATE
&& this.state.labelForUpdate !== null ?
<ConstructorUpdater
label={this.state.labelForUpdate}
onUpdate={this.handleUpdate}
/> :
<ConstructorCreator
onCreate={this.handleCreate}
/>
constructorMode === ConstructorMode.CREATE
&& (
<ConstructorCreator
onCreate={this.handleCreate}
/>
)
}
</Tabs.TabPane>
</Tabs>
......
......@@ -13,27 +13,16 @@ import { FormComponentProps } from 'antd/lib/form/Form';
import {
Label,
Attribute,
} from './common';
type Props = FormComponentProps & {
labels: Label[];
onSubmit: (labels: Label[]) => void;
}
interface State {
valid: boolean;
}
class RawViewer extends React.PureComponent<Props, State> {
public constructor(props: Props) {
super(props);
this.state = {
valid: true,
};
}
};
private validateLabels = (_: any, value: string, callback: any) => {
class RawViewer extends React.PureComponent<Props> {
private validateLabels = (_: any, value: string, callback: any): void => {
try {
JSON.parse(value);
} catch (error) {
......@@ -41,62 +30,73 @@ class RawViewer extends React.PureComponent<Props, State> {
}
callback();
}
};
private handleSubmit = (e: React.FormEvent): void => {
const {
form,
onSubmit,
} = this.props;
private handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
this.props.form.validateFields((error, values) => {
form.validateFields((error, values): void => {
if (!error) {
this.props.onSubmit(JSON.parse(values.labels));
onSubmit(JSON.parse(values.labels));
}
});
}
};
public render() {
const labels = this.props.labels.map((label: any) => {
return {
public render(): JSX.Element {
const { labels } = this.props;
const convertedLabels = labels.map((label: any): Label => (
{
...label,
id: label.id < 0 ? undefined : label.id,
attributes: label.attributes.map((attribute: any) => {
return {
attributes: label.attributes.map((attribute: any): Attribute => (
{
...attribute,
id: attribute.id < 0 ? undefined : attribute.id,
};
}),
};
});
}
)),
}
));
const textLabels = JSON.stringify(labels, null, 2);
const textLabels = JSON.stringify(convertedLabels, null, 2);
const { form } = this.props;
return (
<Form onSubmit={this.handleSubmit}>
<Form.Item> {
this.props.form.getFieldDecorator('labels', {
<Form.Item>
{form.getFieldDecorator('labels', {
initialValue: textLabels,
rules: [{
validator: this.validateLabels,
}]
})( <Input.TextArea rows={5} className='cvat-raw-labels-viewer'/> )
} </Form.Item>
}],
})(<Input.TextArea rows={5} className='cvat-raw-labels-viewer' />)}
</Form.Item>
<Row type='flex' justify='start' align='middle'>
<Col>
<Tooltip overlay='Save labels and return'>
<Button
style={{width: '150px'}}
style={{ width: '150px' }}
type='primary'
htmlType='submit'
> Done </Button>
>
Done
</Button>
</Tooltip>
</Col>
<Col offset={1}>
<Tooltip overlay='Do not save the label and return'>
<Button
style={{width: '150px'}}
style={{ width: '150px' }}
type='danger'
onClick={() => {
this.props.form.resetFields();
onClick={(): void => {
form.resetFields();
}}
> Reset </Button>
>
Reset
</Button>
</Tooltip>
</Col>
</Row>
......
......@@ -18,21 +18,23 @@ type LoginFormProps = {
} & FormComponentProps;
class LoginFormComponent extends React.PureComponent<LoginFormProps> {
constructor(props: LoginFormProps) {
super(props);
}
private handleSubmit = (e: React.FormEvent) => {
private handleSubmit = (e: React.FormEvent): void => {
e.preventDefault();
this.props.form.validateFields((error, values) => {
const {
form,
onSubmit,
} = this.props;
form.validateFields((error, values): void => {
if (!error) {
this.props.onSubmit(values);
onSubmit(values);
}
});
}
};
private renderUsernameField() {
const { getFieldDecorator } = this.props.form;
private renderUsernameField(): JSX.Element {
const { form } = this.props;
const { getFieldDecorator } = form;
return (
<Form.Item hasFeedback>
......@@ -44,16 +46,17 @@ class LoginFormComponent extends React.PureComponent<LoginFormProps> {
})(
<Input
autoComplete='username'
prefix={<Icon type='user' style={{ color: 'rgba(0,0,0,.25)'}} />}
prefix={<Icon type='user' style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder='Username'
/>
/>,
)}
</Form.Item>
)
);
}
private renderPasswordField() {
const { getFieldDecorator } = this.props.form;
private renderPasswordField(): JSX.Element {
const { form } = this.props;
const { getFieldDecorator } = form;
return (
<Form.Item hasFeedback>
......@@ -65,16 +68,17 @@ class LoginFormComponent extends React.PureComponent<LoginFormProps> {
})(
<Input
autoComplete='current-password'
prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)'}} />}
prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder='Password'
type='password'
/>
/>,
)}
</Form.Item>
)
);
}
public render() {
public render(): JSX.Element {
const { fetching } = this.props;
return (
<Form onSubmit={this.handleSubmit} className='login-form'>
{this.renderUsernameField()}
......@@ -83,8 +87,8 @@ class LoginFormComponent extends React.PureComponent<LoginFormProps> {
<Form.Item>
<Button
type='primary'
loading={this.props.fetching}
disabled={this.props.fetching}
loading={fetching}
disabled={fetching}
htmlType='submit'
className='login-form-button'
>
......
......@@ -17,26 +17,35 @@ interface LoginPageComponentProps {
onLogin: (username: string, password: string) => void;
}
function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps) {
function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps): JSX.Element {
const sizes = {
xs: { span: 14 },
sm: { span: 14 },
md: { span: 10 },
lg: { span: 4 },
xl: { span: 4 },
}
};
const {
fetching,
onLogin,
} = props;
return (
<Row type='flex' justify='center' align='middle'>
<Col {...sizes}>
<Title level={2}> Login </Title>
<LoginForm fetching={props.fetching} onSubmit={(loginData: LoginData) => {
props.onLogin(loginData.username, loginData.password);
}}/>
<LoginForm
fetching={fetching}
onSubmit={(loginData: LoginData): void => {
onLogin(loginData.username, loginData.password);
}}
/>
<Row type='flex' justify='start' align='top'>
<Col>
<Text strong>
New to CVAT? Create <Link to="/auth/register">an account</Link>
New to CVAT? Create
<Link to='/auth/register'> an account</Link>
</Text>
</Col>
</Row>
......
......@@ -41,25 +41,31 @@ interface State {
mapping: StringObject;
colors: StringObject;
matching: {
model: string,
task: string,
model: string;
task: string;
};
}
const nextColor = (function *() {
function colorGenerator(): () => string {
const values = [
'magenta', 'green', 'geekblue',
'orange', 'red', 'cyan',
'blue', 'volcano', 'purple',
];
for (let i = 0; i < values.length; i++) {
yield values[i];
if (i === values.length) {
i = 0;
let index = 0;
return (): string => {
const color = values[index++];
if (index >= values.length) {
index = 0;
}
}
})();
return color;
};
}
const nextColor = colorGenerator();
export default class ModelRunnerModalComponent extends React.PureComponent<Props, State> {
public constructor(props: Props) {
......@@ -76,51 +82,115 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
};
}
private renderModelSelector() {
public componentDidUpdate(prevProps: Props, prevState: State): void {
const {
taskInstance,
modelsInitialized,
modelsFetching,
models,
visible,
getModels,
} = this.props;
const {
selectedModel,
} = this.state;
if (!modelsInitialized && !modelsFetching) {
getModels();
}
if (!prevProps.visible && visible) {
this.setState({
selectedModel: null,
mapping: {},
matching: {
model: '',
task: '',
},
cleanOut: false,
});
}
if (selectedModel && prevState.selectedModel !== selectedModel) {
const selectedModelInstance = models
.filter((model) => model.name === selectedModel)[0];
if (!selectedModelInstance.primary) {
let taskLabels: string[] = taskInstance.labels
.map((label: any): string => label.name);
const [defaultMapping, defaultColors]: StringObject[] = selectedModelInstance.labels
.reduce((acc: StringObject[], label): StringObject[] => {
if (taskLabels.includes(label)) {
acc[0][label] = label;
acc[1][label] = nextColor();
taskLabels = taskLabels.filter((_label): boolean => _label !== label);
}
return acc;
}, [{}, {}]);
this.setState({
mapping: defaultMapping,
colors: defaultColors,
});
}
}
}
private renderModelSelector(): JSX.Element {
const { models } = this.props;
return (
<Row type='flex' align='middle'>
<Col span={4}>Model:</Col>
<Col span={19}>
<Select
placeholder='Select a model'
style={{width: '100%'}}
onChange={(value: string) => this.setState({
style={{ width: '100%' }}
onChange={(value: string): void => this.setState({
selectedModel: value,
mapping: {},
})
}>
{this.props.models.map((model) =>
})}
>
{models.map((model): JSX.Element => (
<Select.Option key={model.name}>
{model.name}
</Select.Option>
)}
))}
</Select>
</Col>
</Row>
);
}
private renderMappingTag(modelLabel: string, taskLabel: string) {
private renderMappingTag(modelLabel: string, taskLabel: string): JSX.Element {
const {
colors,
mapping,
} = this.state;
return (
<Row key={`${modelLabel}-${taskLabel}`} type='flex' justify='start' align='middle'>
<Col span={10}>
<Tag color={this.state.colors[modelLabel]}>{modelLabel}</Tag>
<Tag color={colors[modelLabel]}>{modelLabel}</Tag>
</Col>
<Col span={10} offset={1}>
<Tag color={this.state.colors[modelLabel]}>{taskLabel}</Tag>
<Tag color={colors[modelLabel]}>{taskLabel}</Tag>
</Col>
<Col span={1} offset={1}>
<Tooltip overlay='Remove the mapped values'>
<Icon
className='cvat-run-model-dialog-remove-mapping-icon'
type='close-circle'
onClick={() => {
const mapping = {...this.state.mapping};
delete mapping[modelLabel];
this.setState({
mapping,
});
}}/>
onClick={(): void => {
const newMapping = { ...mapping };
delete newMapping[modelLabel];
this.setState({
mapping: newMapping,
});
}}
/>
</Tooltip>
</Col>
</Row>
......@@ -130,106 +200,126 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
private renderMappingInputSelector(
value: string,
current: string,
options: string[]
) {
options: string[],
): JSX.Element {
const {
matching,
mapping,
colors,
} = this.state;
return (
<Select
value={value}
placeholder={`${current} labels`}
style={{width: '100%'}}
onChange={(value: string) => {
const anotherValue = current === 'Model' ?
this.state.matching.task : this.state.matching.model;
style={{ width: '100%' }}
onChange={(selectedValue: string): void => {
const anotherValue = current === 'Model'
? matching.task : matching.model;
if (!anotherValue) {
const matching = { ...this.state.matching };
const newMatching = { ...matching };
if (current === 'Model') {
matching.model = value;
newMatching.model = selectedValue;
} else {
matching.task = value;
newMatching.task = selectedValue;
}
this.setState({
matching,
matching: newMatching,
});
} else {
const colors = {...this.state.colors};
const mapping = {...this.state.mapping};
const newColors = { ...colors };
const newMapping = { ...mapping };
if (current === 'Model') {
colors[value] = nextColor.next().value;
mapping[value] = anotherValue;
newColors[selectedValue] = nextColor();
newMapping[selectedValue] = anotherValue;
} else {
colors[anotherValue] = nextColor.next().value;
mapping[anotherValue] = value;
newColors[anotherValue] = nextColor();
newMapping[anotherValue] = selectedValue;
}
this.setState({
colors,
mapping,
colors: newColors,
mapping: newMapping,
matching: {
task: '',
model: '',
},
})
});
}
}}
>
{options.map((label: string) =>
{options.map((label: string): JSX.Element => (
<Select.Option key={label}>
{label}
</Select.Option>
)}
))}
</Select>
);
}
private renderMappingInput(availableModelLabels: string[], availableTaskLabels: string[]) {
private renderMappingInput(
availableModelLabels: string[],
availableTaskLabels: string[],
): JSX.Element {
const { matching } = this.state;
return (
<Row type='flex' justify='start' align='middle'>
<Col span={10}>
{this.renderMappingInputSelector(
this.state.matching.model,
matching.model,
'Model',
availableModelLabels,
)}
</Col>
<Col span={10} offset={1}>
{this.renderMappingInputSelector(
this.state.matching.task,
matching.task,
'Task',
availableTaskLabels,
)}
</Col>
<Col span={1} offset={1}>
<Tooltip overlay='Specify a label mapping between model labels and task labels'>
<Icon className='cvat-run-model-dialog-info-icon' type='question-circle'/>
<Icon className='cvat-run-model-dialog-info-icon' type='question-circle' />
</Tooltip>
</Col>
</Row>
);
}
private renderContent() {
const model = this.state.selectedModel && this.props.models
.filter((model) => model.name === this.state.selectedModel)[0];
private renderContent(): JSX.Element {
const {
selectedModel,
cleanOut,
mapping,
} = this.state;
const {
models,
taskInstance,
} = this.props;
const model = selectedModel && models
.filter((_model): boolean => _model.name === selectedModel)[0];
const excludedLabels: {
model: string[],
task: string[],
model: string[];
task: string[];
} = {
model: [],
task: [],
};
const withMapping = model && !model.primary;
const tags = withMapping ? Object.keys(this.state.mapping)
const tags = withMapping ? Object.keys(mapping)
.map((modelLabel: string) => {
const taskLabel = this.state.mapping[modelLabel];
const taskLabel = mapping[modelLabel];
excludedLabels.model.push(modelLabel);
excludedLabels.task.push(taskLabel);
return this.renderMappingTag(
modelLabel,
this.state.mapping[modelLabel],
mapping[modelLabel],
);
}) : [];
......@@ -237,10 +327,10 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
.filter(
(label: string) => !excludedLabels.model.includes(label),
) : [];
const availableTaskLabels = this.props.taskInstance.labels
const availableTaskLabels = taskInstance.labels
.map(
(label: any) => label.name,
).filter((label: string) => !excludedLabels.task.includes(label))
).filter((label: string): boolean => !excludedLabels.task.includes(label));
const mappingISAvailable = !!availableModelLabels.length
&& !!availableTaskLabels.length;
......@@ -253,97 +343,73 @@ export default class ModelRunnerModalComponent extends React.PureComponent<Props
&& mappingISAvailable
&& this.renderMappingInput(availableModelLabels, availableTaskLabels)
}
{ withMapping &&
<div>
<Checkbox
checked={this.state.cleanOut}
onChange={(e: any) => this.setState({
cleanOut: e.target.checked,
})}
> Clean old annotations </Checkbox>
</div>
{ withMapping
&& (
<div>
<Checkbox
checked={cleanOut}
onChange={(e: any): void => this.setState({
cleanOut: e.target.checked,
})}
>
Clean old annotations
</Checkbox>
</div>
)
}
</div>
);
}
private renderSpin() {
return (
<Spin size='large' style={{margin: '25% 50%'}}/>
);
}
public componentDidUpdate(prevProps: Props, prevState: State) {
if (!this.props.modelsInitialized && !this.props.modelsFetching) {
this.props.getModels();
}
if (!prevProps.visible && this.props.visible) {
this.setState({
selectedModel: null,
mapping: {},
matching: {
model: '',
task: '',
},
cleanOut: false,
});
}
if (this.state.selectedModel && prevState.selectedModel !== this.state.selectedModel) {
const model = this.props.models
.filter((model) => model.name === this.state.selectedModel)[0];
if (!model.primary) {
let taskLabels: string[] = this.props.taskInstance.labels
.map((label: any) => label.name);
const defaultMapping: StringObject = model.labels
.reduce((acc: StringObject, label) => {
if (taskLabels.includes(label)) {
acc[label] = label;
taskLabels = taskLabels.filter((_label) => _label !== label)
}
return acc;
}, {});
public render(): JSX.Element | false {
const {
selectedModel,
mapping,
cleanOut,
} = this.state;
this.setState({
mapping: defaultMapping,
});
}
}
}
const {
models,
visible,
taskInstance,
modelsInitialized,
runInference,
closeDialog,
} = this.props;
public render() {
const activeModel = this.props.models.filter(
(model) => model.name === this.state.selectedModel
const activeModel = models.filter(
(model): boolean => model.name === selectedModel,
)[0];
let enabledSubmit = !!activeModel
&& activeModel.primary || !!Object.keys(this.state.mapping).length;
const enabledSubmit = (!!activeModel
&& activeModel.primary) || !!Object.keys(mapping).length;
return (
this.props.visible && <Modal
closable={false}
okType='danger'
okText='Submit'
onOk={() => {
this.props.runInference(
this.props.taskInstance,
this.props.models
.filter((model) => model.name === this.state.selectedModel)[0],
this.state.mapping,
this.state.cleanOut,
);
this.props.closeDialog()
}}
onCancel={() => this.props.closeDialog()}
okButtonProps={{disabled: !enabledSubmit}}
title='Automatic annotation'
visible={true}
>
{!this.props.modelsInitialized && this.renderSpin()}
{this.props.modelsInitialized && this.renderContent()}
</Modal>
visible && (
<Modal
closable={false}
okType='danger'
okText='Submit'
onOk={(): void => {
runInference(
taskInstance,
models
.filter((model): boolean => model.name === selectedModel)[0],
mapping,
cleanOut,
);
closeDialog();
}}
onCancel={(): void => closeDialog()}
okButtonProps={{ disabled: !enabledSubmit }}
title='Automatic annotation'
visible
>
{!modelsInitialized
&& <Spin size='large' style={{ margin: '25% 50%' }} />}
{modelsInitialized && this.renderContent()}
</Modal>
)
);
}
}
\ No newline at end of file
}
......@@ -15,7 +15,9 @@ interface Props {
model: Model;
}
export default function BuiltModelItemComponent(props: Props) {
export default function BuiltModelItemComponent(props: Props): JSX.Element {
const { model } = props;
return (
<Row className='cvat-models-list-item' type='flex'>
<Col span={4} xxl={3}>
......@@ -23,24 +25,26 @@ export default function BuiltModelItemComponent(props: Props) {
</Col>
<Col span={6} xxl={7}>
<Text className='cvat-black-color'>
{props.model.name}
{model.name}
</Text>
</Col>
<Col span={5} offset={7}>
<Select
showSearch
placeholder='Supported labels'
style={{width: '90%'}}
style={{ width: '90%' }}
value='Supported labels'
>
{props.model.labels.map(
(label) => <Select.Option key={label}>
{label}
</Select.Option>)
}
{model.labels.map(
(label): JSX.Element => (
<Select.Option key={label}>
{label}
</Select.Option>
),
)}
</Select>
</Col>
<Col span={2}/>
<Col span={2} />
</Row>
);
}
......@@ -14,10 +14,11 @@ interface Props {
models: Model[];
}
export default function IntegratedModelsListComponent(props: Props) {
const items = props.models.map((model) =>
<BuiltModelItemComponent key={model.name} model={model}/>
);
export default function IntegratedModelsListComponent(props: Props): JSX.Element {
const { models } = props;
const items = models.map((model): JSX.Element => (
<BuiltModelItemComponent key={model.name} model={model} />
));
return (
<>
......@@ -28,12 +29,12 @@ export default function IntegratedModelsListComponent(props: Props) {
</Row>
<Row type='flex' justify='center' align='middle'>
<Col md={22} lg={18} xl={16} xxl={14} className='cvat-models-list'>
<Row type='flex' align='middle' style={{padding: '10px'}}>
<Row type='flex' align='middle' style={{ padding: '10px' }}>
<Col span={4} xxl={3}>
<Text strong>{'Framework'}</Text>
<Text strong>Framework</Text>
</Col>
<Col span={6} xxl={7}>
<Text strong>{'Name'}</Text>
<Text strong>Name</Text>
</Col>
<Col span={5} offset={7}>
<Text strong>Labels</Text>
......
......@@ -8,32 +8,31 @@ import {
Icon,
} from 'antd';
export default function EmptyListComponent() {
const emptyTasksIcon = () => (<img src='/assets/empty-tasks-icon.svg'/>);
export default function EmptyListComponent(): JSX.Element {
const emptyTasksIcon = (): JSX.Element => (<img alt='' src='/assets/empty-tasks-icon.svg' />);
return (
<div className='cvat-empty-models-list'>
<Row type='flex' justify='center' align='middle'>
<Col>
<Icon className='cvat-empty-models-icon' component={emptyTasksIcon}/>
<Icon className='cvat-empty-models-icon' component={emptyTasksIcon} />
</Col>
</Row>
<Row type='flex' justify='center' align='middle'>
<Col>
<Text strong>{'No models uploaded yet ...'}</Text>
<Text strong>No models uploaded yet ...</Text>
</Col>
</Row>
<Row type='flex' justify='center' align='middle'>
<Col>
<Text type='secondary'>{'To annotate your tasks automatically'}</Text>
<Text type='secondary'>To annotate your tasks automatically</Text>
</Col>
</Row>
<Row type='flex' justify='center' align='middle'>
<Row type='flex' justify='center' align='middle'>
<Col>
<Link to='/models/create'>{'upload a new model'}</Link>
<Link to='/models/create'>upload a new model</Link>
</Col>
</Row>
</div>
)
}
\ No newline at end of file
);
}
......@@ -22,36 +22,48 @@ interface Props {
deleteModel(id: number): void;
}
export default function ModelsPageComponent(props: Props) {
if (!props.modelsInitialized && !props.modelsFetching) {
props.getModels();
return (
<Spin size='large' style={{margin: '25% 45%'}}/>
);
} else {
const uploadedModels = props.models.filter((model) => model.id !== null);
const integratedModels = props.models.filter((model) => model.id === null);
export default function ModelsPageComponent(props: Props): JSX.Element {
const {
installedAutoAnnotation,
installedTFSegmentation,
installedTFAnnotation,
modelsInitialized,
modelsFetching,
registeredUsers,
models,
deleteModel,
} = props;
if (!modelsInitialized && !modelsFetching) {
props.getModels();
return (
<div className='cvat-models-page'>
<TopBarComponent installedAutoAnnotation={props.installedAutoAnnotation}/>
{ !!integratedModels.length &&
<BuiltModelsList models={integratedModels}/>
}
{ !!uploadedModels.length &&
<UploadedModelsList
registeredUsers={props.registeredUsers}
models={uploadedModels}
deleteModel={props.deleteModel}
/>
}
{ props.installedAutoAnnotation &&
!uploadedModels.length &&
!props.installedTFAnnotation &&
!props.installedTFSegmentation &&
<EmptyListComponent/>
}
</div>
<Spin size='large' style={{ margin: '25% 45%' }} />
);
}
const uploadedModels = models.filter((model): boolean => model.id !== null);
const integratedModels = models.filter((model): boolean => model.id === null);
return (
<div className='cvat-models-page'>
<TopBarComponent installedAutoAnnotation={installedAutoAnnotation} />
{ !!integratedModels.length
&& <BuiltModelsList models={integratedModels} />
}
{ !!uploadedModels.length && (
<UploadedModelsList
registeredUsers={registeredUsers}
models={uploadedModels}
deleteModel={deleteModel}
/>
)}
{ installedAutoAnnotation
&& !uploadedModels.length
&& !installedTFAnnotation
&& !installedTFSegmentation
&& <EmptyListComponent />
}
</div>
);
}
......@@ -14,26 +14,40 @@ type Props = {
installedAutoAnnotation: boolean;
} & RouteComponentProps;
function TopBarComponent(props: Props) {
function TopBarComponent(props: Props): JSX.Element {
const {
installedAutoAnnotation,
history,
} = props;
return (
<Row type='flex' justify='center' align='middle'>
<Col md={11} lg={9} xl={8} xxl={7}>
<Text className='cvat-title'>Models</Text>
</Col>
<Col
md={{span: 11}}
lg={{span: 9}}
xl={{span: 8}}
xxl={{span: 7}}
md={{ span: 11 }}
lg={{ span: 9 }}
xl={{ span: 8 }}
xxl={{ span: 7 }}
>
{ props.installedAutoAnnotation &&
<Button size='large' id='cvat-create-model-button' type='primary' onClick={
() => props.history.push('/models/create')
}> Create new model </Button>
{ installedAutoAnnotation
&& (
<Button
size='large'
id='cvat-create-model-button'
type='primary'
onClick={
(): void => history.push('/models/create')
}
>
Create new model
</Button>
)
}
</Col>
</Row>
)
);
}
export default withRouter(TopBarComponent);
......@@ -21,8 +21,13 @@ interface Props {
onDelete(): void;
}
export default function UploadedModelItem(props: Props) {
const subMenuIcon = () => (<img src='/assets/icon-sub-menu.svg'/>);
export default function UploadedModelItem(props: Props): JSX.Element {
const subMenuIcon = (): JSX.Element => (<img alt='' src='/assets/icon-sub-menu.svg' />);
const {
model,
owner,
onDelete,
} = props;
return (
<Row className='cvat-models-list-item' type='flex'>
......@@ -31,43 +36,52 @@ export default function UploadedModelItem(props: Props) {
</Col>
<Col span={6} xxl={7}>
<Text className='cvat-black-color'>
{props.model.name}
{model.name}
</Text>
</Col>
<Col span={3}>
<Text className='cvat-black-color'>
{props.owner ? props.owner.username : 'undefined'}
{owner ? owner.username : 'undefined'}
</Text>
</Col>
<Col span={4}>
<Text className='cvat-black-color'>
{moment(props.model.uploadDate).format('MMMM Do YYYY')}
{moment(model.uploadDate).format('MMMM Do YYYY')}
</Text>
</Col>
<Col span={5}>
<Select
showSearch
placeholder='Supported labels'
style={{width: '90%'}}
style={{ width: '90%' }}
value='Supported labels'
>
{props.model.labels.map(
(label) => <Select.Option key={label}>
{label}
</Select.Option>)
}
{model.labels.map(
(label): JSX.Element => (
<Select.Option key={label}>
{label}
</Select.Option>
),
)}
</Select>
</Col>
<Col span={2}>
<Text className='cvat-black-color'>Actions</Text>
<Dropdown overlay={
(
<Menu className='cvat-task-item-menu'>
<Menu.Item onClick={() => {
props.onDelete();
}}key='delete'>Delete</Menu.Item>
<Menu.Item
onClick={(): void => {
onDelete();
}}
key='delete'
>
Delete
</Menu.Item>
</Menu>
}>
<Icon className='cvat-task-item-menu-icon' component={subMenuIcon}/>
)}
>
<Icon className='cvat-task-item-menu-icon' component={subMenuIcon} />
</Dropdown>
</Col>
</Row>
......
......@@ -16,15 +16,21 @@ interface Props {
deleteModel(id: number): void;
}
export default function UploadedModelsListComponent(props: Props) {
const items = props.models.map((model) => {
const owner = props.registeredUsers.filter((user) => user.id === model.ownerID)[0];
export default function UploadedModelsListComponent(props: Props): JSX.Element {
const {
models,
registeredUsers,
deleteModel,
} = props;
const items = models.map((model): JSX.Element => {
const owner = registeredUsers.filter((user) => user.id === model.ownerID)[0];
return (
<UploadedModelItem
key={model.id as number}
owner={owner}
model={model}
onDelete={() => props.deleteModel(model.id as number)}
onDelete={(): void => deleteModel(model.id as number)}
/>
);
});
......@@ -33,17 +39,17 @@ export default function UploadedModelsListComponent(props: Props) {
<>
<Row type='flex' justify='center' align='middle'>
<Col md={22} lg={18} xl={16} xxl={14}>
<Text className='cvat-black-color' strong>{'Uploaded by a user'}</Text>
<Text className='cvat-black-color' strong>Uploaded by a user</Text>
</Col>
</Row>
<Row type='flex' justify='center' align='middle'>
<Col md={22} lg={18} xl={16} xxl={14} className='cvat-models-list'>
<Row type='flex' align='middle' style={{padding: '10px'}}>
<Row type='flex' align='middle' style={{ padding: '10px' }}>
<Col span={4} xxl={3}>
<Text strong>{'Framework'}</Text>
<Text strong>Framework</Text>
</Col>
<Col span={6} xxl={7}>
<Text strong>{'Name'}</Text>
<Text strong>Name</Text>
</Col>
<Col span={3}>
<Text strong>Owner</Text>
......@@ -54,7 +60,7 @@ export default function UploadedModelsListComponent(props: Props) {
<Col span={5}>
<Text strong>Labels</Text>
</Col>
<Col span={2}/>
<Col span={2} />
</Row>
{ items }
</Col>
......
......@@ -7,6 +7,8 @@ import {
Form,
} from 'antd';
import patterns from '../../utils/validation-patterns';
export interface RegisterData {
username: string;
firstName: string;
......@@ -16,28 +18,22 @@ export interface RegisterData {
password2: string;
}
import patterns from '../../utils/validation-patterns';
type RegisterFormProps = {
fetching: boolean;
onSubmit(registerData: RegisterData): void;
} & FormComponentProps;
class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
constructor(props: RegisterFormProps) {
super(props);
}
private validateConfirmation = (rule: any, value: any, callback: any) => {
private validateConfirmation = (rule: any, value: any, callback: any): void => {
const { form } = this.props;
if (value && value !== form.getFieldValue('password1')) {
callback('Two passwords that you enter is inconsistent!');
callback('Two passwords that you enter is inconsistent!');
} else {
callback();
callback();
}
};
};
private validatePassword = (_: any, value: any, callback: any) => {
private validatePassword = (_: any, value: any, callback: any): void => {
const { form } = this.props;
if (!patterns.validatePasswordLength.pattern.test(value)) {
callback(patterns.validatePasswordLength.message);
......@@ -56,12 +52,12 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}
if (value) {
form.validateFields(['password2'], { force: true });
form.validateFields(['password2'], { force: true });
}
callback();
};
private validateUsername = (_: any, value: any, callback: any) => {
private validateUsername = (_: any, value: any, callback: any): void => {
if (!patterns.validateUsernameLength.pattern.test(value)) {
callback(patterns.validateUsernameLength.message);
}
......@@ -73,19 +69,26 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
callback();
};
private handleSubmit = (e: React.FormEvent) => {
private handleSubmit = (e: React.FormEvent): void => {
e.preventDefault();
this.props.form.validateFields((error, values) => {
const {
form,
onSubmit,
} = this.props;
form.validateFields((error, values): void => {
if (!error) {
this.props.onSubmit(values);
onSubmit(values);
}
});
}
};
private renderFirstNameField(): JSX.Element {
const { form } = this.props;
private renderFirstNameField() {
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('firstName', {
{form.getFieldDecorator('firstName', {
rules: [{
required: true,
message: 'Please specify a first name',
......@@ -93,18 +96,20 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}],
})(
<Input
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)'}} />}
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder='First name'
/>
/>,
)}
</Form.Item>
)
);
}
private renderLastNameField() {
private renderLastNameField(): JSX.Element {
const { form } = this.props;
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('lastName', {
{form.getFieldDecorator('lastName', {
rules: [{
required: true,
message: 'Please specify a last name',
......@@ -112,18 +117,20 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}],
})(
<Input
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)'}} />}
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder='Last name'
/>
/>,
)}
</Form.Item>
)
);
}
private renderUsernameField() {
private renderUsernameField(): JSX.Element {
const { form } = this.props;
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('username', {
{form.getFieldDecorator('username', {
rules: [{
required: true,
message: 'Please specify a username',
......@@ -132,40 +139,44 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}],
})(
<Input
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)'}} />}
prefix={<Icon type='user-add' style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder='Username'
/>
/>,
)}
</Form.Item>
)
);
}
private renderEmailField() {
private renderEmailField(): JSX.Element {
const { form } = this.props;
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('email', {
{form.getFieldDecorator('email', {
rules: [{
type: 'email',
message: 'The input is not valid E-mail!',
}, {
}, {
required: true,
message: 'Please specify an email address',
}],
})(
<Input
autoComplete='email'
prefix={<Icon type='mail' style={{ color: 'rgba(0,0,0,.25)'}} />}
prefix={<Icon type='mail' style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder='Email address'
/>
/>,
)}
</Form.Item>
)
);
}
private renderPasswordField() {
private renderPasswordField(): JSX.Element {
const { form } = this.props;
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('password1', {
{form.getFieldDecorator('password1', {
rules: [{
required: true,
message: 'Please input your password!',
......@@ -174,17 +185,19 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}],
})(<Input.Password
autoComplete='new-password'
prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)'}} />}
prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder='Password'
/>)}
</Form.Item>
)
);
}
private renderPasswordConfirmationField() {
private renderPasswordConfirmationField(): JSX.Element {
const { form } = this.props;
return (
<Form.Item hasFeedback>
{this.props.form.getFieldDecorator('password2', {
{form.getFieldDecorator('password2', {
rules: [{
required: true,
message: 'Please confirm your password!',
......@@ -193,16 +206,16 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}],
})(<Input.Password
autoComplete='new-password'
prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)'}} />}
prefix={<Icon type='lock' style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder='Confirm password'
/>)}
</Form.Item>
)
);
}
public render(): JSX.Element {
const { fetching } = this.props;
public render() {
return (
<Form onSubmit={this.handleSubmit} className='login-form'>
{this.renderFirstNameField()}
......@@ -217,8 +230,8 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
type='primary'
htmlType='submit'
className='register-form-button'
loading={this.props.fetching}
disabled={this.props.fetching}
loading={fetching}
disabled={fetching}
>
Submit
</Button>
......@@ -228,4 +241,4 @@ class RegisterFormComponent extends React.PureComponent<RegisterFormProps> {
}
}
export default Form.create<RegisterFormProps>()(RegisterFormComponent);
\ No newline at end of file
export default Form.create<RegisterFormProps>()(RegisterFormComponent);
......@@ -10,7 +10,7 @@ import {
Row,
} from 'antd';
import RegisterForm, { RegisterData } from '../../components/register-page/register-form';
import RegisterForm, { RegisterData } from './register-form';
interface RegisterPageComponentProps {
fetching: boolean;
......@@ -19,33 +19,44 @@ interface RegisterPageComponentProps {
password1: string, password2: string) => void;
}
function RegisterPageComponent(props: RegisterPageComponentProps & RouteComponentProps) {
function RegisterPageComponent(
props: RegisterPageComponentProps & RouteComponentProps,
): JSX.Element {
const sizes = {
xs: { span: 14 },
sm: { span: 14 },
md: { span: 10 },
lg: { span: 4 },
xl: { span: 4 },
}
};
const {
fetching,
onRegister,
} = props;
return (
<Row type='flex' justify='center' align='middle'>
<Col {...sizes}>
<Title level={2}> Create an account </Title>
<RegisterForm fetching={props.fetching} onSubmit={(registerData: RegisterData) => {
props.onRegister(
registerData.username,
registerData.firstName,
registerData.lastName,
registerData.email,
registerData.password1,
registerData.password2,
);
}}/>
<RegisterForm
fetching={fetching}
onSubmit={(registerData: RegisterData): void => {
onRegister(
registerData.username,
registerData.firstName,
registerData.lastName,
registerData.email,
registerData.password1,
registerData.password2,
);
}}
/>
<Row type='flex' justify='start' align='top'>
<Col>
<Text strong>
Already have an account? <Link to="/auth/login"> Login </Link>
Already have an account?
<Link to='/auth/login'> Login </Link>
</Text>
</Col>
</Row>
......
......@@ -24,17 +24,19 @@ interface Props {
onJobUpdate(jobInstance: any): void;
}
export default function JobListComponent(props: Props) {
const { jobs } = props.taskInstance;
export default function JobListComponent(props: Props): JSX.Element {
const {
taskInstance,
registeredUsers,
onJobUpdate,
} = props;
const { jobs } = taskInstance;
const columns = [{
title: 'Job',
dataIndex: 'job',
key: 'job',
render: (id: number) => {
return (
<a href={`${baseURL}/?id=${id}`}>{ `Job #${id++}` }</a>
);
}
render: (id: number): JSX.Element => (<a href={`${baseURL}/?id=${id}`}>{ `Job #${id}` }</a>),
}, {
title: 'Frames',
dataIndex: 'frames',
......@@ -44,14 +46,20 @@ export default function JobListComponent(props: Props) {
title: 'Status',
dataIndex: 'status',
key: 'status',
render: (status: string) => {
const progressColor = status === 'completed' ? 'cvat-job-completed-color':
status === 'validation' ? 'cvat-job-validation-color' : 'cvat-job-annotation-color';
render: (status: string): JSX.Element => {
let progressColor = null;
if (status === 'completed') {
progressColor = 'cvat-job-completed-color';
} else if (status === 'validation') {
progressColor = 'cvat-job-validation-color';
} else {
progressColor = 'cvat-job-annotation-color';
}
return (
<Text strong className={progressColor}>{ status }</Text>
);
}
},
}, {
title: 'Started on',
dataIndex: 'started',
......@@ -66,22 +74,24 @@ export default function JobListComponent(props: Props) {
title: 'Assignee',
dataIndex: 'assignee',
key: 'assignee',
render: (jobInstance: any) => {
const assignee = jobInstance.assignee ? jobInstance.assignee.username : null
render: (jobInstance: any): JSX.Element => {
const assignee = jobInstance.assignee ? jobInstance.assignee.username : null;
return (
<UserSelector
users={props.registeredUsers}
users={registeredUsers}
value={assignee}
onChange={(value: string) => {
let [userInstance] = props.registeredUsers
.filter((user: any) => user.username === value);
onChange={(value: string): void => {
let [userInstance] = [...registeredUsers]
.filter((user: any) => user.username === value);
if (userInstance === undefined) {
userInstance = null;
}
// eslint-disable-next-line
jobInstance.assignee = userInstance;
props.onJobUpdate(jobInstance);
onJobUpdate(jobInstance);
}}
/>
);
......@@ -129,4 +139,4 @@ export default function JobListComponent(props: Props) {
/>
</div>
);
}
\ No newline at end of file
}
此差异已折叠。
......@@ -37,7 +37,7 @@ export default (state = defaultState, action: AnyAction): FormatsState => {
case AuthActionTypes.LOGOUT_SUCCESS: {
return {
...defaultState,
}
};
}
default:
return state;
......
此差异已折叠。
此差异已折叠。
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册