Merge branch 'release-1.6.0'

main
Nikita Manovich 4 years ago
commit 8d03ed7959

@ -5,8 +5,7 @@ on:
- 'master'
- 'develop'
pull_request:
branches:
- '*'
jobs:
Unit_testing:
runs-on: ubuntu-latest

@ -5,7 +5,7 @@
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"type": "pwa-chrome",
"request": "launch",
"preLaunchTask": "npm: start - cvat-ui",
"name": "ui.js: debug",
@ -59,7 +59,7 @@
},
{
"name": "server: chrome",
"type": "chrome",
"type": "pwa-chrome",
"request": "launch",
"url": "http://localhost:7000/",
"disableNetworkCache":true,

@ -5,6 +5,63 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## \[1.7.0] - Unreleased
### Added
- TDB
### Changed
- TDB
### Deprecated
- TDB
### Removed
- TDB
### Fixed
- TDB
### Security
- TDB
## \[1.6.0] - 2021-09-17
### Added
- Added ability to import data from share with cli without copying the data (<https://github.com/openvinotoolkit/cvat/issues/2862>)
- Notification if the browser does not support nesassary API
- Added ability to export project as a dataset (<https://github.com/openvinotoolkit/cvat/pull/3365>)
and project with 3D tasks (<https://github.com/openvinotoolkit/cvat/pull/3502>)
- Additional inline tips in interactors with demo gifs (<https://github.com/openvinotoolkit/cvat/pull/3473>)
- Added intelligent scissors blocking feature (<https://github.com/openvinotoolkit/cvat/pull/3510>)
- Support cloud storage status (<https://github.com/openvinotoolkit/cvat/pull/3386>)
- Support cloud storage preview (<https://github.com/openvinotoolkit/cvat/pull/3386>)
- cvat-core: support cloud storages (<https://github.com/openvinotoolkit/cvat/pull/3313>)
### Changed
- Non-blocking UI when using interactors (<https://github.com/openvinotoolkit/cvat/pull/3473>)
- "Selected opacity" slider now defines opacity level for shapes being drawnSelected opacity (<https://github.com/openvinotoolkit/cvat/pull/3473>)
- Cloud storage creating and updating (<https://github.com/openvinotoolkit/cvat/pull/3386>)
- Way of working with cloud storage content (<https://github.com/openvinotoolkit/cvat/pull/3386>)
### Removed
- Support TEMP_KEY_SECRET_KEY_TOKEN_SET for AWS S3 cloud storage (<https://github.com/openvinotoolkit/cvat/pull/3386>)
### Fixed
- Fixed multiple tasks moving (<https://github.com/openvinotoolkit/cvat/pull/3517>)
- Fixed task creating CLI parameter (<https://github.com/openvinotoolkit/cvat/pull/3519>)
- Fixed import for MOTS format (<https://github.com/openvinotoolkit/cvat/pull/3612>)
## \[1.5.0] - 2021-08-02
### Added
@ -17,6 +74,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Explicit "Done" button when drawing any polyshapes (<https://github.com/openvinotoolkit/cvat/pull/3417>)
- Histogram equalization with OpenCV javascript (<https://github.com/openvinotoolkit/cvat/pull/3447>)
- Client-side polyshapes approximation when using semi-automatic interactors & scissors (<https://github.com/openvinotoolkit/cvat/pull/3450>)
- Support of Google Cloud Storage for cloud storage (<https://github.com/openvinotoolkit/cvat/pull/3561>)
### Changed
@ -49,6 +107,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Context image disappears after undo/redo (<https://github.com/openvinotoolkit/cvat/pull/3416>)
- Using combined data sources (directory and image) when create a task (<https://github.com/openvinotoolkit/cvat/pull/3424>)
- Creating task with labels in project (<https://github.com/openvinotoolkit/cvat/pull/3454>)
- Move task and autoannotation modals were invisible from project page (<https://github.com/openvinotoolkit/cvat/pull/3475>)
## \[1.4.0] - 2021-05-18
@ -844,22 +903,22 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Template
```
## [Unreleased]
## \[Unreleased]
### Added
-
- TDB
### Changed
-
- TDB
### Deprecated
-
- TDB
### Removed
-
- TDB
### Fixed
-
- TDB
### Security
-
- TDB
```

@ -50,7 +50,7 @@ COPY cvat-canvas/ /tmp/cvat-canvas/
COPY cvat-ui/ /tmp/cvat-ui/
RUN npm run build
FROM nginx:stable-alpine
FROM nginx:mainline-alpine
# Replace default.conf configuration to remove unnecessary rules
RUN sed -i "s/}/application\/wasm wasm;\n}/g" /etc/nginx/mime.types
COPY cvat-ui/react_nginx.conf /etc/nginx/conf.d/default.conf

@ -124,7 +124,7 @@ output {
if [type] == "client" {
elasticsearch {
hosts => ["${LOGSTASH_OUTPUT_HOST}"]
index => "cvat.client"
index => "%{[@metadata][target_index_client]}"
user => "${LOGSTASH_OUTPUT_USER:}"
password => "${LOGSTASH_OUTPUT_PASS:}"
manage_template => false
@ -132,7 +132,7 @@ output {
} else if [type] == "server" {
elasticsearch {
hosts => ["${LOGSTASH_OUTPUT_HOST}"]
index => "cvat.server"
index => "%{[@metadata][target_index_server]}"
user => "${LOGSTASH_OUTPUT_USER:}"
password => "${LOGSTASH_OUTPUT_PASS:}"
manage_template => false

@ -1,6 +1,6 @@
{
"name": "cvat-canvas",
"version": "2.5.0",
"version": "2.7.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -3513,14 +3513,26 @@
}
},
"enhanced-resolve": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz",
"integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz",
"integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"memory-fs": "^0.4.0",
"memory-fs": "^0.5.0",
"tapable": "^1.0.0"
},
"dependencies": {
"memory-fs": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz",
"integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==",
"dev": true,
"requires": {
"errno": "^0.1.3",
"readable-stream": "^2.0.1"
}
}
}
},
"errno": {
@ -5605,9 +5617,9 @@
}
},
"interpret": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz",
"integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
"dev": true
},
"invariant": {
@ -5619,12 +5631,6 @@
"loose-envify": "^1.0.0"
}
},
"invert-kv": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
"integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==",
"dev": true
},
"ip": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
@ -6165,15 +6171,6 @@
"package-json": "^6.3.0"
}
},
"lcid": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
"integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
"dev": true,
"requires": {
"invert-kv": "^2.0.0"
}
},
"levn": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
@ -6309,15 +6306,6 @@
"semver": "^5.6.0"
}
},
"map-age-cleaner": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
"integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==",
"dev": true,
"requires": {
"p-defer": "^1.0.0"
}
},
"map-cache": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
@ -6345,17 +6333,6 @@
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
"dev": true
},
"mem": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz",
"integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==",
"dev": true,
"requires": {
"map-age-cleaner": "^0.1.1",
"mimic-fn": "^2.0.0",
"p-is-promise": "^2.0.0"
}
},
"memory-fs": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@ -6525,12 +6502,6 @@
"mime-db": "1.40.0"
}
},
"mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true
},
"mimic-response": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
@ -7276,17 +7247,6 @@
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"dev": true
},
"os-locale": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
"integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
"dev": true,
"requires": {
"execa": "^1.0.0",
"lcid": "^2.0.0",
"mem": "^4.0.0"
}
},
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
@ -7309,24 +7269,12 @@
"integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==",
"dev": true
},
"p-defer": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
"integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=",
"dev": true
},
"p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
"dev": true
},
"p-is-promise": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz",
"integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==",
"dev": true
},
"p-limit": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz",
@ -7453,9 +7401,9 @@
"dev": true
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"path-to-regexp": {
@ -10275,9 +10223,9 @@
}
},
"url-parse": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz",
"integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==",
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
"integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
"dev": true,
"requires": {
"querystringify": "^2.1.1",
@ -10535,112 +10483,46 @@
}
},
"webpack-cli": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.6.tgz",
"integrity": "sha512-0vEa83M7kJtxK/jUhlpZ27WHIOndz5mghWL2O53kiDoA9DIxSKnfqB92LoqEn77cT4f3H2cZm1BMEat/6AZz3A==",
"dev": true,
"requires": {
"chalk": "2.4.2",
"cross-spawn": "6.0.5",
"enhanced-resolve": "4.1.0",
"findup-sync": "3.0.0",
"global-modules": "2.0.0",
"import-local": "2.0.0",
"interpret": "1.2.0",
"loader-utils": "1.2.3",
"supports-color": "6.1.0",
"v8-compile-cache": "2.0.3",
"yargs": "13.2.4"
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.12.tgz",
"integrity": "sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
"cross-spawn": "^6.0.5",
"enhanced-resolve": "^4.1.1",
"findup-sync": "^3.0.0",
"global-modules": "^2.0.0",
"import-local": "^2.0.0",
"interpret": "^1.4.0",
"loader-utils": "^1.4.0",
"supports-color": "^6.1.0",
"v8-compile-cache": "^2.1.1",
"yargs": "^13.3.2"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"dev": true
},
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"loader-utils": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"dev": true,
"requires": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^1.0.1"
}
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true
},
"require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"v8-compile-cache": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
"dev": true
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
}
},
"yargs": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz",
"integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==",
"dev": true,
"requires": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"os-locale": "^3.1.0",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.0"
}
},
"yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},

@ -1,6 +1,6 @@
{
"name": "cvat-canvas",
"version": "2.5.0",
"version": "2.7.0",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts",
"scripts": {
@ -9,6 +9,12 @@
},
"author": "Intel",
"license": "MIT",
"browserslist": [
"Chrome >= 63",
"Firefox > 58",
"not IE 11",
"> 2%"
],
"dependencies": {
"svg.draggable.js": "2.2.2",
"svg.draw.js": "^2.0.4",
@ -41,7 +47,7 @@
"style-loader": "^1.0.0",
"typescript": "^3.5.3",
"webpack": "^5.20.2",
"webpack-cli": "^3.3.6",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
}
}

@ -1,12 +0,0 @@
// Copyright (C) 2019-2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
module.exports = {
parser: false,
plugins: {
'postcss-preset-env': {
browsers: '> 2.5%', // https://github.com/browserslist/browserslist
},
},
};

@ -24,7 +24,6 @@ polyline.cvat_shape_action_opacity {
}
.cvat_shape_drawing_opacity {
fill-opacity: 0.2;
stroke-opacity: 1;
}
@ -161,9 +160,8 @@ polyline.cvat_canvas_shape_splitting {
.cvat_canvas_removable_interaction_point {
cursor:
url(
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K'
) 10 10,
url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K')
10 10,
auto;
}

@ -59,6 +59,7 @@ export interface Configuration {
forceDisableEditing?: boolean;
intelligentPolygonCrop?: boolean;
forceFrameUpdate?: boolean;
creationOpacity?: number;
}
export interface DrawData {
@ -86,6 +87,7 @@ export interface InteractionData {
shapeType: string;
points: number[];
};
onChangeToolsBlockerState?: (event: string) => void;
}
export interface InteractionResult {
@ -547,6 +549,16 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
}
}
// install default values for drawing method
if (drawData.enabled) {
if (drawData.shapeType === 'rectangle') {
this.data.drawData.rectDrawingMethod = drawData.rectDrawingMethod || RectDrawingMethod.CLASSIC;
}
if (drawData.shapeType === 'cuboid') {
this.data.drawData.cuboidDrawingMethod = drawData.cuboidDrawingMethod || CuboidDrawingMethod.CLASSIC;
}
}
this.notify(UpdateReasons.DRAW);
}
@ -554,15 +566,14 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
if (![Mode.IDLE, Mode.INTERACT].includes(this.data.mode)) {
throw Error(`Canvas is busy. Action: ${this.data.mode}`);
}
if (interactionData.enabled && !interactionData.intermediateShape) {
const thresholdChanged = this.data.interactionData.enableThreshold !== interactionData.enableThreshold;
if (interactionData.enabled && !interactionData.intermediateShape && !thresholdChanged) {
if (this.data.interactionData.enabled) {
throw new Error('Interaction has been already started');
} else if (!interactionData.shapeType) {
throw new Error('A shape type was not specified');
}
}
this.data.interactionData = interactionData;
if (typeof this.data.interactionData.crosshair !== 'boolean') {
this.data.interactionData.crosshair = true;
@ -656,6 +667,10 @@ export class CanvasModelImpl extends MasterImpl implements CanvasModel {
this.data.configuration.forceFrameUpdate = configuration.forceFrameUpdate;
}
if (typeof configuration.creationOpacity === 'number') {
this.data.configuration.creationOpacity = configuration.creationOpacity;
}
this.notify(UpdateReasons.CONFIG_UPDATED);
}

@ -998,6 +998,8 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.adoptedContent,
this.adoptedText,
this.autoborderHandler,
this.geometry,
this.configuration,
);
this.editHandler = new EditHandlerImpl(this.onEditDone.bind(this), this.adoptedContent, this.autoborderHandler);
this.mergeHandler = new MergeHandlerImpl(
@ -1026,6 +1028,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.onInteraction.bind(this),
this.adoptedContent,
this.geometry,
this.configuration,
);
// Setup event handlers
@ -1117,6 +1120,7 @@ export class CanvasViewImpl implements CanvasView, Listener {
this.activate(activeElement);
this.editHandler.configurate(this.configuration);
this.drawHandler.configurate(this.configuration);
this.interactionHandler.configurate(this.configuration);
// remove if exist and not enabled
// this.setupObjects([]);
@ -1275,7 +1279,9 @@ export class CanvasViewImpl implements CanvasView, Listener {
}
this.interactionHandler.interact(data);
} else {
this.canvas.style.cursor = '';
if (!data.enabled) {
this.canvas.style.cursor = '';
}
if (this.mode !== Mode.IDLE) {
this.interactionHandler.interact(data);
}
@ -1565,7 +1571,6 @@ export class CanvasViewImpl implements CanvasView, Listener {
private addObjects(states: any[]): void {
const { displayAllText } = this.configuration;
for (const state of states) {
const points: number[] = state.points as number[];
const translatedPoints: number[] = this.translateToCanvas(points);

@ -50,6 +50,7 @@ export class DrawHandlerImpl implements DrawHandler {
private crosshair: Crosshair;
private drawData: DrawData;
private geometry: Geometry;
private configuration: Configuration;
private autoborderHandler: AutoborderHandler;
private autobordersEnabled: boolean;
@ -371,6 +372,7 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
});
}
@ -527,6 +529,7 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
});
this.drawPolyshape();
@ -597,6 +600,7 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
});
}
@ -654,6 +658,7 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
});
this.pasteShape();
@ -686,6 +691,7 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
});
this.pasteShape();
this.pastePolyshape();
@ -709,6 +715,7 @@ export class DrawHandlerImpl implements DrawHandler {
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'face-stroke': 'black',
'fill-opacity': this.configuration.creationOpacity,
});
this.pasteShape();
this.pastePolyshape();
@ -845,6 +852,8 @@ export class DrawHandlerImpl implements DrawHandler {
canvas: SVG.Container,
text: SVG.Container,
autoborderHandler: AutoborderHandler,
geometry: Geometry,
configuration: Configuration,
) {
this.autoborderHandler = autoborderHandler;
this.autobordersEnabled = false;
@ -855,7 +864,8 @@ export class DrawHandlerImpl implements DrawHandler {
this.initialized = false;
this.canceled = false;
this.drawData = null;
this.geometry = null;
this.geometry = geometry;
this.configuration = configuration;
this.crosshair = new Crosshair();
this.drawInstance = null;
this.pointsGroup = null;
@ -874,6 +884,20 @@ export class DrawHandlerImpl implements DrawHandler {
}
public configurate(configuration: Configuration): void {
this.configuration = configuration;
const isFillableRect = this.drawData
&& this.drawData.shapeType === 'rectangle'
&& (this.drawData.rectDrawingMethod === RectDrawingMethod.CLASSIC || this.drawData.initialState);
const isFillableCuboid = this.drawData
&& this.drawData.shapeType === 'cuboid'
&& (this.drawData.cuboidDrawingMethod === CuboidDrawingMethod.CLASSIC || this.drawData.initialState);
const isFilalblePolygon = this.drawData && this.drawData.shapeType === 'polygon';
if (this.drawInstance && (isFillableRect || isFillableCuboid || isFilalblePolygon)) {
this.drawInstance.fill({ opacity: configuration.creationOpacity });
}
if (typeof configuration.autoborders === 'boolean') {
this.autobordersEnabled = configuration.autoborders;
if (this.drawInstance) {

@ -8,16 +8,21 @@ import Crosshair from './crosshair';
import {
translateToSVG, PropType, stringifyPoints, translateToCanvas,
} from './shared';
import { InteractionData, InteractionResult, Geometry } from './canvasModel';
import {
InteractionData, InteractionResult, Geometry, Configuration,
} from './canvasModel';
export interface InteractionHandler {
transform(geometry: Geometry): void;
interact(interactData: InteractionData): void;
configurate(config: Configuration): void;
cancel(): void;
}
export class InteractionHandlerImpl implements InteractionHandler {
private onInteraction: (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean) => void;
private configuration: Configuration;
private geometry: Geometry;
private canvas: SVG.Container;
private interactionData: InteractionData;
@ -30,6 +35,7 @@ export class InteractionHandlerImpl implements InteractionHandler {
private thresholdRectSize: number;
private intermediateShape: PropType<InteractionData, 'intermediateShape'>;
private drawnIntermediateShape: SVG.Shape;
private thresholdWasModified: boolean;
private prepareResult(): InteractionResult[] {
return this.interactionShapes.map(
@ -137,14 +143,15 @@ export class InteractionHandlerImpl implements InteractionHandler {
_e.preventDefault();
_e.stopPropagation();
self.remove();
this.shapesWereUpdated = true;
const shouldRaiseEvent = this.shouldRaiseEvent(_e.ctrlKey);
this.interactionShapes = this.interactionShapes.filter(
(shape: SVG.Shape): boolean => shape !== self,
);
if (this.interactionData.startWithBox && this.interactionShapes.length === 1) {
this.interactionShapes[0].style({ visibility: '' });
}
this.shapesWereUpdated = true;
if (this.shouldRaiseEvent(_e.ctrlKey)) {
if (shouldRaiseEvent) {
this.onInteraction(this.prepareResult(), true, false);
}
});
@ -196,16 +203,21 @@ export class InteractionHandlerImpl implements InteractionHandler {
.addClass('cvat_canvas_shape_drawing')
.attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
});
})
.fill({ opacity: this.configuration.creationOpacity, color: 'white' });
}
private initInteraction(): void {
if (this.interactionData.crosshair) {
this.addCrosshair();
} else if (this.crosshair) {
this.removeCrosshair();
}
if (this.interactionData.enableThreshold) {
this.addThreshold();
} else if (this.threshold) {
this.threshold.remove();
this.threshold = null;
}
}
@ -286,8 +298,8 @@ export class InteractionHandlerImpl implements InteractionHandler {
'shape-rendering': 'geometricprecision',
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
stroke: erroredShape ? 'red' : 'black',
fill: 'none',
})
.fill({ opacity: this.configuration.creationOpacity, color: 'white' })
.addClass('cvat_canvas_interact_intermediate_shape');
this.selectize(true, this.drawnIntermediateShape, erroredShape);
} else {
@ -327,7 +339,25 @@ export class InteractionHandlerImpl implements InteractionHandler {
const handler = shape.remember('_selectHandler');
if (handler && handler.nested) {
handler.nested.fill(shape.attr('fill'));
// move green circle group(anchors) and polygon(lastChild) to the top of svg to make anchors hoverable
handler.parent.node.prepend(handler.nested.node);
handler.parent.node.prepend(handler.parent.node.lastChild);
}
}
private visualComponentsChanged(interactionData: InteractionData): boolean {
const allowedKeys = ['enabled', 'crosshair', 'enableThreshold', 'onChangeToolsBlockerState'];
if (Object.keys(interactionData).every((key: string): boolean => allowedKeys.includes(key))) {
if (this.interactionData.enableThreshold !== undefined && interactionData.enableThreshold !== undefined
&& this.interactionData.enableThreshold !== interactionData.enableThreshold) {
return true;
}
if (this.interactionData.crosshair !== undefined && interactionData.crosshair !== undefined
&& this.interactionData.crosshair !== interactionData.crosshair) {
return true;
}
}
return false;
}
public constructor(
@ -339,12 +369,14 @@ export class InteractionHandlerImpl implements InteractionHandler {
) => void,
canvas: SVG.Container,
geometry: Geometry,
configuration: Configuration,
) {
this.onInteraction = (shapes: InteractionResult[] | null, shapesUpdated?: boolean, isDone?: boolean): void => {
this.shapesWereUpdated = false;
onInteraction(shapes, shapesUpdated, isDone, this.threshold ? this.thresholdRectSize / 2 : null);
};
this.canvas = canvas;
this.configuration = configuration;
this.geometry = geometry;
this.shapesWereUpdated = false;
this.interactionShapes = [];
@ -369,7 +401,6 @@ export class InteractionHandlerImpl implements InteractionHandler {
if (this.threshold) {
this.threshold.center(x, y);
}
if (this.interactionData.enableSliding && this.interactionShapes.length) {
if (this.isWithinFrame(x, y)) {
if (this.interactionData.enableThreshold && !this.isWithinThreshold(x, y)) return;
@ -392,6 +423,7 @@ export class InteractionHandlerImpl implements InteractionHandler {
this.canvas.on('wheel.interaction', (e: WheelEvent): void => {
if (e.ctrlKey) {
if (this.threshold) {
this.thresholdWasModified = true;
const { x, y } = this.cursorPosition;
e.preventDefault();
if (e.deltaY > 0) {
@ -405,10 +437,24 @@ export class InteractionHandlerImpl implements InteractionHandler {
}
});
document.body.addEventListener('keyup', (e: KeyboardEvent): void => {
if (e.keyCode === 17 && this.shouldRaiseEvent(false)) {
// 17 is ctrl
this.onInteraction(this.prepareResult(), true, false);
window.addEventListener('keyup', (e: KeyboardEvent): void => {
if (this.interactionData.enabled && e.keyCode === 17) {
if (this.interactionData.onChangeToolsBlockerState && !this.thresholdWasModified) {
this.interactionData.onChangeToolsBlockerState('keyup');
}
if (this.shouldRaiseEvent(false)) {
// 17 is ctrl
this.onInteraction(this.prepareResult(), true, false);
}
}
});
window.addEventListener('keydown', (e: KeyboardEvent): void => {
if (this.interactionData.enabled && e.keyCode === 17) {
if (this.interactionData.onChangeToolsBlockerState && !this.thresholdWasModified) {
this.interactionData.onChangeToolsBlockerState('keydown');
}
this.thresholdWasModified = false;
}
});
}
@ -454,6 +500,9 @@ export class InteractionHandlerImpl implements InteractionHandler {
if (this.interactionData.startWithBox) {
this.interactionShapes[0].style({ visibility: 'hidden' });
}
} else if (interactionData.enabled && this.visualComponentsChanged(interactionData)) {
this.interactionData = { ...this.interactionData, ...interactionData };
this.initInteraction();
} else if (interactionData.enabled) {
this.interactionData = interactionData;
this.initInteraction();
@ -465,6 +514,25 @@ export class InteractionHandlerImpl implements InteractionHandler {
}
}
public configurate(configuration: Configuration): void {
this.configuration = configuration;
if (this.drawnIntermediateShape) {
this.drawnIntermediateShape.fill({
opacity: configuration.creationOpacity,
});
}
// when interactRectangle
if (this.currentInteractionShape && this.currentInteractionShape.type === 'rect') {
this.currentInteractionShape.fill({ opacity: configuration.creationOpacity });
}
// when interactPoints with startwithbbox
if (this.interactionShapes[0] && this.interactionShapes[0].type === 'rect') {
this.interactionShapes[0].fill({ opacity: configuration.creationOpacity });
}
}
public cancel(): void {
this.release();
this.onInteraction(null);

@ -10,8 +10,8 @@ import 'svg.select.js';
import 'svg.draw.js';
import consts from './consts';
import { Point, Equation, CuboidModel, Orientation, Edge } from './cuboid';
import { parsePoints, clamp } from './shared';
import { Equation, CuboidModel, Orientation, Edge } from './cuboid';
import { Point, parsePoints, clamp } from './shared';
// Update constructor
const originalDraw = SVG.Element.prototype.draw;
@ -958,8 +958,12 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
},
paintOrientationLines() {
const fillColor = this.attr('fill');
const strokeColor = this.attr('stroke');
// style has higher priority than attr, so then try to fetch it if exists
// https://stackoverflow.com/questions/47088409/svg-attributes-beaten-by-cssstyle-in-priority]
// we use getComputedStyle to get actual, not-inlined css property (come from the corresponding css class)
const computedStyles = getComputedStyle(this.node);
const fillColor = computedStyles['fill'] || this.attr('fill');
const strokeColor = computedStyles['stroke'] || this.attr('stroke');
const selectedColor = this.attr('face-stroke') || '#b0bec5';
this.frontTopEdge.stroke({ color: selectedColor });
this.frontLeftEdge.stroke({ color: selectedColor });

@ -8,6 +8,23 @@ const path = require('path');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const DtsBundleWebpack = require('dts-bundle-webpack');
const styleLoaders = [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
{
loader: 'postcss-loader',
options: {
plugins: [require('postcss-preset-env')],
},
},
'sass-loader',
];
const nodeConfig = {
target: 'node',
mode: 'production',
@ -34,7 +51,7 @@ const nodeConfig = {
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-optional-chaining',
],
presets: [['@babel/preset-env'], ['@babel/typescript']],
presets: [['@babel/preset-env', { targets: 'node > 10' }], '@babel/typescript'],
sourceType: 'unambiguous',
},
},
@ -42,17 +59,7 @@ const nodeConfig = {
{
test: /\.(css|scss)$/,
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
'sass-loader',
],
use: styleLoaders,
},
],
},
@ -96,15 +103,7 @@ const webConfig = {
loader: 'babel-loader',
options: {
plugins: ['@babel/plugin-proposal-class-properties'],
presets: [
[
'@babel/preset-env',
{
targets: '> 2.5%', // https://github.com/browserslist/browserslist
},
],
['@babel/typescript'],
],
presets: ['@babel/preset-env', '@babel/typescript'],
sourceType: 'unambiguous',
},
},
@ -112,17 +111,7 @@ const webConfig = {
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
'sass-loader',
],
use: styleLoaders,
},
],
},

@ -3585,14 +3585,26 @@
}
},
"enhanced-resolve": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz",
"integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==",
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz",
"integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==",
"dev": true,
"requires": {
"graceful-fs": "^4.1.2",
"memory-fs": "^0.4.0",
"memory-fs": "^0.5.0",
"tapable": "^1.0.0"
},
"dependencies": {
"memory-fs": {
"version": "0.5.0",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz",
"integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==",
"dev": true,
"requires": {
"errno": "^0.1.3",
"readable-stream": "^2.0.1"
}
}
}
},
"errno": {
@ -5726,9 +5738,9 @@
}
},
"interpret": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz",
"integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==",
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
"integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
"dev": true
},
"invariant": {
@ -5740,12 +5752,6 @@
"loose-envify": "^1.0.0"
}
},
"invert-kv": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-2.0.0.tgz",
"integrity": "sha512-wPVv/y/QQ/Uiirj/vh3oP+1Ww+AWehmi1g5fFWGPF6IpCBCDVrhgHRMvrLfdYcwDh3QJbGXDW4JAuzxElLSqKA==",
"dev": true
},
"ip": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
@ -6210,15 +6216,6 @@
"package-json": "^4.0.0"
}
},
"lcid": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/lcid/-/lcid-2.0.0.tgz",
"integrity": "sha512-avPEb8P8EGnwXKClwsNUgryVjllcRqtMYa49NTsbQagYuT1DcXnl1915oxWjoyGrXR6zH/Y0Zc96xWsPcoDKeA==",
"dev": true,
"requires": {
"invert-kv": "^2.0.0"
}
},
"levn": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
@ -6363,15 +6360,6 @@
"semver": "^5.6.0"
}
},
"map-age-cleaner": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/map-age-cleaner/-/map-age-cleaner-0.1.3.tgz",
"integrity": "sha512-bJzx6nMoP6PDLPBFmg7+xRKeFZvFboMrGlxmNj9ClvX53KrmvM5bXFXEWjbz4cz1AFn+jWJ9z/DJSz7hrs0w3w==",
"dev": true,
"requires": {
"p-defer": "^1.0.0"
}
},
"map-cache": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
@ -6410,17 +6398,6 @@
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
"dev": true
},
"mem": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/mem/-/mem-4.3.0.tgz",
"integrity": "sha512-qX2bG48pTqYRVmDB37rn/6PT7LcR8T7oAX3bf99u1Tt1nzxYfxkgqDwUwolPlXweM0XzBOBFzSx4kfp7KP1s/w==",
"dev": true,
"requires": {
"map-age-cleaner": "^0.1.1",
"mimic-fn": "^2.0.0",
"p-is-promise": "^2.0.0"
}
},
"memory-fs": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@ -6602,12 +6579,6 @@
"mime-db": "1.40.0"
}
},
"mimic-fn": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==",
"dev": true
},
"minimalistic-assert": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@ -7348,17 +7319,6 @@
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"dev": true
},
"os-locale": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz",
"integrity": "sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q==",
"dev": true,
"requires": {
"execa": "^1.0.0",
"lcid": "^2.0.0",
"mem": "^4.0.0"
}
},
"os-tmpdir": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
@ -7375,24 +7335,12 @@
"os-tmpdir": "^1.0.0"
}
},
"p-defer": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-defer/-/p-defer-1.0.0.tgz",
"integrity": "sha1-n26xgvbJqozXQwBKfU+WsZaw+ww=",
"dev": true
},
"p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
"dev": true
},
"p-is-promise": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-2.1.0.tgz",
"integrity": "sha512-Y3W0wlRPK8ZMRbNq97l4M5otioeA5lm1z7bkNkxCka8HSPjR0xRWmpCmc9utiaLP9Jb1eD8BgeIxTW4AIF45Pg==",
"dev": true
},
"p-limit": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz",
@ -10772,112 +10720,46 @@
}
},
"webpack-cli": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.6.tgz",
"integrity": "sha512-0vEa83M7kJtxK/jUhlpZ27WHIOndz5mghWL2O53kiDoA9DIxSKnfqB92LoqEn77cT4f3H2cZm1BMEat/6AZz3A==",
"dev": true,
"requires": {
"chalk": "2.4.2",
"cross-spawn": "6.0.5",
"enhanced-resolve": "4.1.0",
"findup-sync": "3.0.0",
"global-modules": "2.0.0",
"import-local": "2.0.0",
"interpret": "1.2.0",
"loader-utils": "1.2.3",
"supports-color": "6.1.0",
"v8-compile-cache": "2.0.3",
"yargs": "13.2.4"
"version": "3.3.12",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.12.tgz",
"integrity": "sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==",
"dev": true,
"requires": {
"chalk": "^2.4.2",
"cross-spawn": "^6.0.5",
"enhanced-resolve": "^4.1.1",
"findup-sync": "^3.0.0",
"global-modules": "^2.0.0",
"import-local": "^2.0.0",
"interpret": "^1.4.0",
"loader-utils": "^1.4.0",
"supports-color": "^6.1.0",
"v8-compile-cache": "^2.1.1",
"yargs": "^13.3.2"
},
"dependencies": {
"ansi-regex": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==",
"emojis-list": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"dev": true
},
"cliui": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==",
"loader-utils": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"dev": true,
"requires": {
"string-width": "^3.1.0",
"strip-ansi": "^5.2.0",
"wrap-ansi": "^5.1.0"
"big.js": "^5.2.2",
"emojis-list": "^3.0.0",
"json5": "^1.0.1"
}
},
"get-caller-file": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
"dev": true
},
"require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
"integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==",
"v8-compile-cache": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
"integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
"dev": true
},
"string-width": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz",
"integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==",
"dev": true,
"requires": {
"emoji-regex": "^7.0.1",
"is-fullwidth-code-point": "^2.0.0",
"strip-ansi": "^5.1.0"
}
},
"strip-ansi": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz",
"integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==",
"dev": true,
"requires": {
"ansi-regex": "^4.1.0"
}
},
"wrap-ansi": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz",
"integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==",
"dev": true,
"requires": {
"ansi-styles": "^3.2.0",
"string-width": "^3.0.0",
"strip-ansi": "^5.0.0"
}
},
"yargs": {
"version": "13.2.4",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-13.2.4.tgz",
"integrity": "sha512-HG/DWAJa1PAnHT9JAhNa8AbAv3FPaiLzioSjCcmuXXhP8MlpHO5vwls4g4j6n30Z74GVQj8Xa62dWVx1QCGklg==",
"dev": true,
"requires": {
"cliui": "^5.0.0",
"find-up": "^3.0.0",
"get-caller-file": "^2.0.1",
"os-locale": "^3.1.0",
"require-directory": "^2.1.1",
"require-main-filename": "^2.0.0",
"set-blocking": "^2.0.0",
"string-width": "^3.0.0",
"which-module": "^2.0.0",
"y18n": "^4.0.0",
"yargs-parser": "^13.1.0"
}
},
"yargs-parser": {
"version": "13.1.2",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz",
"integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==",
"dev": true,
"requires": {
"camelcase": "^5.0.0",
"decamelize": "^1.2.0"
}
}
}
},

@ -9,6 +9,12 @@
},
"author": "Intel",
"license": "MIT",
"browserslist": [
"Chrome >= 63",
"Firefox > 58",
"not IE 11",
"> 2%"
],
"devDependencies": {
"@babel/cli": "^7.13.16",
"@babel/core": "^7.5.5",
@ -34,7 +40,7 @@
"style-loader": "^1.0.0",
"typescript": "^3.5.3",
"webpack": "^4.44.2",
"webpack-cli": "^3.3.6",
"webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0"
},
"dependencies": {

@ -1,12 +0,0 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
module.exports = {
parser: false,
plugins: {
'postcss-preset-env': {
browsers: '> 2.5%', // https://github.com/browserslist/browserslist
},
},
};

@ -8,6 +8,23 @@ const path = require('path');
// eslint-disable-next-line @typescript-eslint/no-var-requires
const DtsBundleWebpack = require('dts-bundle-webpack');
const styleLoaders = [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
{
loader: 'postcss-loader',
options: {
plugins: [require('postcss-preset-env')],
},
},
'sass-loader',
];
const nodeConfig = {
target: 'node',
mode: 'production',
@ -34,7 +51,7 @@ const nodeConfig = {
'@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-optional-chaining',
],
presets: [['@babel/preset-env'], ['@babel/typescript']],
presets: [['@babel/preset-env', { targets: 'node > 10' }], '@babel/typescript'],
sourceType: 'unambiguous',
},
},
@ -42,17 +59,7 @@ const nodeConfig = {
{
test: /\.(css|scss)$/,
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
'sass-loader',
],
use: styleLoaders,
},
],
},
@ -96,15 +103,7 @@ const webConfig = {
loader: 'babel-loader',
options: {
plugins: ['@babel/plugin-proposal-class-properties'],
presets: [
[
'@babel/preset-env',
{
targets: '> 2.5%', // https://github.com/browserslist/browserslist
},
],
['@babel/typescript'],
],
presets: ['@babel/preset-env', '@babel/typescript'],
sourceType: 'unambiguous',
},
},
@ -112,17 +111,7 @@ const webConfig = {
{
test: /\.scss$/,
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
'sass-loader',
],
use: styleLoaders,
},
],
},

@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "3.13.3",
"version": "3.16.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -3785,11 +3785,11 @@
"integrity": "sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA=="
},
"axios": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz",
"integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==",
"version": "0.21.3",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.3.tgz",
"integrity": "sha512-JtoZ3Ndke/+Iwt5n+BgSli/3idTvpt5OjKyoCmz4LX5+lPiY5l7C1colYezhlxThjNa/NhngCUWZSZFypIFuaA==",
"requires": {
"follow-redirects": "^1.10.0"
"follow-redirects": "^1.14.0"
}
},
"babel-code-frame": {
@ -13562,9 +13562,9 @@
}
},
"follow-redirects": {
"version": "1.13.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz",
"integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg=="
"version": "1.14.3",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.3.tgz",
"integrity": "sha512-3MkHxknWMUtb23apkgz/83fDoe+y+qr0TdgacGIA7bew+QLBo3vdgEN2xEsuXNivpFy4CyDhBBZnNZOtalmenw=="
},
"for-in": {
"version": "1.0.2",

@ -1,6 +1,6 @@
{
"name": "cvat-core",
"version": "3.13.3",
"version": "3.16.0",
"description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js",
"scripts": {
@ -11,6 +11,12 @@
},
"author": "Intel",
"license": "MIT",
"browserslist": [
"Chrome >= 63",
"Firefox > 58",
"not IE 11",
"> 2%"
],
"devDependencies": {
"@babel/cli": "^7.4.4",
"@babel/core": "^7.4.4",
@ -33,7 +39,7 @@
"webpack-cli": "^3.3.2"
},
"dependencies": {
"axios": "^0.21.1",
"axios": "^0.21.3",
"browser-or-node": "^1.2.1",
"cvat-data": "../cvat-data",
"detect-browser": "^5.2.0",

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -8,8 +8,9 @@
const AnnotationsSaver = require('./annotations-saver');
const AnnotationsHistory = require('./annotations-history');
const { checkObjectType } = require('./common');
const { Task } = require('./session');
const { Loader, Dumper } = require('./annotation-formats');
const { Project } = require('./project');
const { Task, Job } = require('./session');
const { Loader } = require('./annotation-formats');
const { ScriptingError, DataError, ArgumentError } = require('./exceptions');
const jobCache = new WeakMap();
@ -50,6 +51,7 @@
stopFrame,
frameMeta,
});
// eslint-disable-next-line no-unsanitized/method
collection.import(rawAnnotations);
const saver = new AnnotationsSaver(rawAnnotations.version, collection, session);
@ -232,27 +234,12 @@
await serverProxy.annotations.uploadAnnotations(sessionType, session.id, file, loader.name);
}
async function dumpAnnotations(session, name, dumper) {
if (!(dumper instanceof Dumper)) {
throw new ArgumentError('A dumper must be instance of Dumper class');
}
let result = null;
const sessionType = session instanceof Task ? 'task' : 'job';
if (sessionType === 'job') {
result = await serverProxy.annotations.dumpAnnotations(session.task.id, name, dumper.name);
} else {
result = await serverProxy.annotations.dumpAnnotations(session.id, name, dumper.name);
}
return result;
}
function importAnnotations(session, data) {
const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType);
if (cache.has(session)) {
// eslint-disable-next-line no-unsanitized/method
return cache.get(session).collection.import(data);
}
@ -274,16 +261,25 @@
);
}
async function exportDataset(session, format) {
async function exportDataset(instance, format, name, saveImages = false) {
if (!(format instanceof String || typeof format === 'string')) {
throw new ArgumentError('Format must be a string');
}
if (!(session instanceof Task)) {
throw new ArgumentError('A dataset can only be created from a task');
if (!(instance instanceof Task || instance instanceof Project || instance instanceof Job)) {
throw new ArgumentError('A dataset can only be created from a job, task or project');
}
if (typeof saveImages !== 'boolean') {
throw new ArgumentError('Save images parameter must be a boolean');
}
let result = null;
result = await serverProxy.tasks.exportDataset(session.id, format);
if (instance instanceof Task) {
result = await serverProxy.tasks.exportDataset(instance.id, format, name, saveImages);
} else if (instance instanceof Job) {
result = await serverProxy.tasks.exportDataset(instance.task.id, format, name, saveImages);
} else {
result = await serverProxy.projects.exportDataset(instance.id, format, name, saveImages);
}
return result;
}
@ -367,7 +363,6 @@
annotationsStatistics,
selectObject,
uploadAnnotations,
dumpAnnotations,
importAnnotations,
exportAnnotations,
exportDataset,

@ -16,13 +16,20 @@
camelToSnake,
} = require('./common');
const { TaskStatus, TaskMode, DimensionType } = require('./enums');
const {
TaskStatus,
TaskMode,
DimensionType,
CloudStorageProviderType,
CloudStorageCredentialsType,
} = require('./enums');
const User = require('./user');
const { AnnotationFormats } = require('./annotation-formats');
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
const { Project } = require('./project');
const { CloudStorage } = require('./cloud-storage');
function implementAPI(cvat) {
cvat.plugins.list.implementation = PluginRegistry.list;
@ -262,6 +269,49 @@
cvat.projects.searchNames.implementation = async (search, limit) => serverProxy.projects.searchNames(search, limit);
cvat.cloudStorages.get.implementation = async (filter) => {
checkFilter(filter, {
page: isInteger,
displayName: isString,
resourceName: isString,
description: isString,
id: isInteger,
owner: isString,
search: isString,
providerType: isEnum.bind(CloudStorageProviderType),
credentialsType: isEnum.bind(CloudStorageCredentialsType),
});
checkExclusiveFields(filter, ['id', 'search'], ['page']);
const searchParams = new URLSearchParams();
for (const field of [
'displayName',
'credentialsType',
'providerType',
'owner',
'search',
'id',
'page',
'description',
]) {
if (Object.prototype.hasOwnProperty.call(filter, field)) {
searchParams.set(camelToSnake(field), filter[field]);
}
}
if (Object.prototype.hasOwnProperty.call(filter, 'resourceName')) {
searchParams.set('resource', filter.resourceName);
}
const cloudStoragesData = await serverProxy.cloudStorages.get(searchParams.toString());
const cloudStorages = cloudStoragesData.map((cloudStorage) => new CloudStorage(cloudStorage));
cloudStorages.count = cloudStoragesData.count;
return cloudStorages;
};
return cvat;
}

@ -18,9 +18,12 @@ function build() {
const Review = require('./review');
const { Job, Task } = require('./session');
const { Project } = require('./project');
const implementProject = require('./project-implementation');
const { Attribute, Label } = require('./labels');
const MLModel = require('./ml-model');
const { FrameData } = require('./frames');
const { CloudStorage } = require('./cloud-storage');
const enums = require('./enums');
@ -747,6 +750,41 @@ function build() {
PluginError,
ServerError,
},
/**
* Namespace is used for getting cloud storages
* @namespace cloudStorages
* @memberof module:API.cvat
*/
cloudStorages: {
/**
* @typedef {Object} CloudStorageFilter
* @property {string} displayName Check if displayName contains this value
* @property {string} resourceName Check if resourceName contains this value
* @property {module:API.cvat.enums.ProviderType} providerType Check if providerType equal this value
* @property {integer} id Check if id equals this value
* @property {integer} page Get specific page
* (default REST API returns 20 clouds storages per request.
* In order to get more, it is need to specify next page)
* @property {string} owner Check if an owner name contains this value
* @property {string} search Combined search of contains among all the fields
* @global
*/
/**
* Method returns a list of cloud storages corresponding to a filter
* @method get
* @async
* @memberof module:API.cvat.cloudStorages
* @param {CloudStorageFilter} [filter={}] cloud storage filter
* @returns {module:API.cvat.classes.CloudStorage[]}
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
*/
async get(filter = {}) {
const result = await PluginRegistry.apiWrapper(cvat.cloudStorages.get, filter);
return result;
},
},
/**
* Namespace is used for access to classes
* @namespace classes
@ -754,7 +792,7 @@ function build() {
*/
classes: {
User,
Project,
Project: implementProject(Project),
Task,
Job,
Log,
@ -767,6 +805,7 @@ function build() {
Issue,
Review,
FrameData,
CloudStorage,
},
};
@ -779,6 +818,7 @@ function build() {
cvat.lambda = Object.freeze(cvat.lambda);
cvat.client = Object.freeze(cvat.client);
cvat.enums = Object.freeze(cvat.enums);
cvat.cloudStorages = Object.freeze(cvat.cloudStorages);
const implementAPI = require('./api-implementation');

@ -0,0 +1,520 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
const { isBrowser, isNode } = require('browser-or-node');
const { ArgumentError } = require('./exceptions');
const { CloudStorageCredentialsType, CloudStorageProviderType } = require('./enums');
/**
* Class representing a cloud storage
* @memberof module:API.cvat.classes
*/
class CloudStorage {
// TODO: add storage availability status (avaliable/unavaliable)
constructor(initialData) {
const data = {
id: undefined,
display_name: undefined,
description: undefined,
credentials_type: undefined,
provider_type: undefined,
resource: undefined,
account_name: undefined,
key: undefined,
secret_key: undefined,
session_token: undefined,
specific_attributes: undefined,
owner: undefined,
created_date: undefined,
updated_date: undefined,
manifest_path: undefined,
manifests: undefined,
};
for (const property in data) {
if (Object.prototype.hasOwnProperty.call(data, property) && property in initialData) {
data[property] = initialData[property];
}
}
Object.defineProperties(
this,
Object.freeze({
/**
* @name id
* @type {integer}
* @memberof module:API.cvat.classes.CloudStorage
* @readonly
* @instance
*/
id: {
get: () => data.id,
},
/**
* Storage name
* @name displayName
* @type {string}
* @memberof module:API.cvat.classes.CloudStorage
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
displayName: {
get: () => data.display_name,
set: (value) => {
if (typeof value !== 'string') {
throw new ArgumentError(`Value must be string. ${typeof value} was found`);
} else if (!value.trim().length) {
throw new ArgumentError('Value must not be empty string');
}
data.display_name = value;
},
},
/**
* Storage description
* @name description
* @type {string}
* @memberof module:API.cvat.classes.CloudStorage
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
description: {
get: () => data.description,
set: (value) => {
if (typeof value !== 'string') {
throw new ArgumentError('Value must be string');
}
data.description = value;
},
},
/**
* Azure account name
* @name accountName
* @type {string}
* @memberof module:API.cvat.classes.CloudStorage
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
accountName: {
get: () => data.account_name,
set: (value) => {
if (typeof value === 'string') {
if (value.trim().length) {
data.account_name = value;
} else {
throw new ArgumentError('Value must not be empty');
}
} else {
throw new ArgumentError(`Value must be a string. ${typeof value} was found`);
}
},
},
/**
* AWS access key id
* @name accessKey
* @type {string}
* @memberof module:API.cvat.classes.CloudStorage
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
accessKey: {
get: () => data.key,
set: (value) => {
if (typeof value === 'string') {
if (value.trim().length) {
data.key = value;
} else {
throw new ArgumentError('Value must not be empty');
}
} else {
throw new ArgumentError(`Value must be a string. ${typeof value} was found`);
}
},
},
/**
* AWS secret key
* @name secretKey
* @type {string}
* @memberof module:API.cvat.classes.CloudStorage
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
secretKey: {
get: () => data.secret_key,
set: (value) => {
if (typeof value === 'string') {
if (value.trim().length) {
data.secret_key = value;
} else {
throw new ArgumentError('Value must not be empty');
}
} else {
throw new ArgumentError(`Value must be a string. ${typeof value} was found`);
}
},
},
/**
* Session token
* @name token
* @type {string}
* @memberof module:API.cvat.classes.CloudStorage
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
token: {
get: () => data.session_token,
set: (value) => {
if (typeof value === 'string') {
if (value.trim().length) {
data.session_token = value;
} else {
throw new ArgumentError('Value must not be empty');
}
} else {
throw new ArgumentError(`Value must be a string. ${typeof value} was found`);
}
},
},
/**
* Unique resource name
* @name resourceName
* @type {string}
* @memberof module:API.cvat.classes.CloudStorage
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
resourceName: {
get: () => data.resource,
set: (value) => {
if (typeof value !== 'string') {
throw new ArgumentError(`Value must be string. ${typeof value} was found`);
} else if (!value.trim().length) {
throw new ArgumentError('Value must not be empty');
}
data.resource = value;
},
},
/**
* @name manifestPath
* @type {string}
* @memberof module:API.cvat.classes.CloudStorage
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
manifestPath: {
get: () => data.manifest_path,
set: (value) => {
if (typeof value === 'string') {
data.manifest_path = value;
} else {
throw new ArgumentError('Value must be a string');
}
},
},
/**
* @name providerType
* @type {module:API.cvat.enums.ProviderType}
* @memberof module:API.cvat.classes.CloudStorage
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
providerType: {
get: () => data.provider_type,
set: (key) => {
if (key !== undefined && !!CloudStorageProviderType[key]) {
data.provider_type = CloudStorageProviderType[key];
} else {
throw new ArgumentError('Value must be one CloudStorageProviderType keys');
}
},
},
/**
* @name credentialsType
* @type {module:API.cvat.enums.CloudStorageCredentialsType}
* @memberof module:API.cvat.classes.CloudStorage
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
credentialsType: {
get: () => data.credentials_type,
set: (key) => {
if (key !== undefined && !!CloudStorageCredentialsType[key]) {
data.credentials_type = CloudStorageCredentialsType[key];
} else {
throw new ArgumentError('Value must be one CloudStorageCredentialsType keys');
}
},
},
/**
* @name specificAttributes
* @type {string}
* @memberof module:API.cvat.classes.CloudStorage
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
specificAttributes: {
get: () => data.specific_attributes,
set: (attributesValue) => {
if (typeof attributesValue === 'string') {
const attrValues = new URLSearchParams(
Array.from(new URLSearchParams(attributesValue).entries()).filter(
([key, value]) => !!key && !!value,
),
).toString();
if (!attrValues) {
throw new ArgumentError('Value must match the key1=value1&key2=value2');
}
data.specific_attributes = attributesValue;
} else {
throw new ArgumentError('Value must be a string');
}
},
},
/**
* Instance of a user who has created the cloud storage
* @name owner
* @type {module:API.cvat.classes.User}
* @memberof module:API.cvat.classes.CloudStorage
* @readonly
* @instance
*/
owner: {
get: () => data.owner,
},
/**
* @name createdDate
* @type {string}
* @memberof module:API.cvat.classes.CloudStorage
* @readonly
* @instance
*/
createdDate: {
get: () => data.created_date,
},
/**
* @name updatedDate
* @type {string}
* @memberof module:API.cvat.classes.CloudStorage
* @readonly
* @instance
*/
updatedDate: {
get: () => data.updated_date,
},
/**
* @name manifests
* @type {string[]}
* @memberof module:API.cvat.classes.CloudStorage
* @instance
* @throws {module:API.cvat.exceptions.ArgumentError}
*/
manifests: {
get: () => data.manifests,
set: (manifests) => {
if (Array.isArray(manifests)) {
for (const elem of manifests) {
if (typeof elem !== 'string') {
throw new ArgumentError('Each element of the manifests array must be a string');
}
}
data.manifests = manifests;
} else {
throw new ArgumentError('Value must be an array');
}
},
},
}),
);
}
/**
* Method updates data of a created cloud storage or creates new cloud storage
* @method save
* @returns {module:API.cvat.classes.CloudStorage}
* @memberof module:API.cvat.classes.CloudStorage
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async save() {
const result = await PluginRegistry.apiWrapper.call(this, CloudStorage.prototype.save);
return result;
}
/**
* Method deletes a cloud storage from a server
* @method delete
* @memberof module:API.cvat.classes.CloudStorage
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async delete() {
const result = await PluginRegistry.apiWrapper.call(this, CloudStorage.prototype.delete);
return result;
}
/**
* Method returns cloud storage content
* @method getContent
* @memberof module:API.cvat.classes.CloudStorage
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async getContent() {
const result = await PluginRegistry.apiWrapper.call(this, CloudStorage.prototype.getContent);
return result;
}
/**
* Method returns the cloud storage preview
* @method getPreview
* @memberof module:API.cvat.classes.CloudStorage
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async getPreview() {
const result = await PluginRegistry.apiWrapper.call(this, CloudStorage.prototype.getPreview);
return result;
}
/**
* Method returns cloud storage status
* @method getStatus
* @memberof module:API.cvat.classes.CloudStorage
* @readonly
* @instance
* @async
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.PluginError}
*/
async getStatus() {
const result = await PluginRegistry.apiWrapper.call(this, CloudStorage.prototype.getStatus);
return result;
}
}
CloudStorage.prototype.save.implementation = async function () {
function prepareOptionalFields(cloudStorageInstance) {
const data = {};
if (cloudStorageInstance.description) {
data.description = cloudStorageInstance.description;
}
if (cloudStorageInstance.accountName) {
data.account_name = cloudStorageInstance.accountName;
}
if (cloudStorageInstance.accessKey) {
data.key = cloudStorageInstance.accessKey;
}
if (cloudStorageInstance.secretKey) {
data.secret_key = cloudStorageInstance.secretKey;
}
if (cloudStorageInstance.token) {
data.session_token = cloudStorageInstance.token;
}
if (cloudStorageInstance.specificAttributes) {
data.specific_attributes = cloudStorageInstance.specificAttributes;
}
return data;
}
// update
if (typeof this.id !== 'undefined') {
// providr_type and recource should not change;
// send to the server only the values that have changed
const initialData = {};
if (this.displayName) {
initialData.display_name = this.displayName;
}
if (this.credentialsType) {
initialData.credentials_type = this.credentialsType;
}
if (this.manifests) {
initialData.manifests = this.manifests;
}
const cloudStorageData = {
...initialData,
...prepareOptionalFields(this),
};
await serverProxy.cloudStorages.update(this.id, cloudStorageData);
return this;
}
// create
const initialData = {
display_name: this.displayName,
credentials_type: this.credentialsType,
provider_type: this.providerType,
resource: this.resourceName,
manifests: this.manifests,
};
const cloudStorageData = {
...initialData,
...prepareOptionalFields(this),
};
const cloudStorage = await serverProxy.cloudStorages.create(cloudStorageData);
return new CloudStorage(cloudStorage);
};
CloudStorage.prototype.delete.implementation = async function () {
const result = await serverProxy.cloudStorages.delete(this.id);
return result;
};
CloudStorage.prototype.getContent.implementation = async function () {
const result = await serverProxy.cloudStorages.getContent(this.id, this.manifestPath);
return result;
};
CloudStorage.prototype.getPreview.implementation = async function getPreview() {
return new Promise((resolve, reject) => {
serverProxy.cloudStorages
.getPreview(this.id)
.then((result) => {
if (isNode) {
resolve(global.Buffer.from(result, 'binary').toString('base64'));
} else if (isBrowser) {
const reader = new FileReader();
reader.onload = () => {
resolve(reader.result);
};
reader.readAsDataURL(result);
}
})
.catch((error) => {
reject(error);
});
});
};
CloudStorage.prototype.getStatus.implementation = async function () {
const result = await serverProxy.cloudStorages.getStatus(this.id);
return result;
};
module.exports = {
CloudStorage,
};
})();

@ -1,4 +1,4 @@
// Copyright (C) 2019-2020 Intel Corporation
// Copyright (C) 2019-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -333,6 +333,36 @@
'#733380',
];
/**
* Types of cloud storage providers
* @enum {string}
* @name CloudStorageProviderType
* @memberof module:API.cvat.enums
* @property {string} AWS_S3 'AWS_S3_BUCKET'
* @property {string} AZURE 'AZURE_CONTAINER'
* @readonly
*/
const CloudStorageProviderType = Object.freeze({
AWS_S3_BUCKET: 'AWS_S3_BUCKET',
AZURE_CONTAINER: 'AZURE_CONTAINER',
});
/**
* Types of cloud storage credentials
* @enum {string}
* @name CloudStorageCredentialsType
* @memberof module:API.cvat.enums
* @property {string} KEY_SECRET_KEY_PAIR 'KEY_SECRET_KEY_PAIR'
* @property {string} ACCOUNT_NAME_TOKEN_PAIR 'ACCOUNT_NAME_TOKEN_PAIR'
* @property {string} ANONYMOUS_ACCESS 'ANONYMOUS_ACCESS'
* @readonly
*/
const CloudStorageCredentialsType = Object.freeze({
KEY_SECRET_KEY_PAIR: 'KEY_SECRET_KEY_PAIR',
ACCOUNT_NAME_TOKEN_PAIR: 'ACCOUNT_NAME_TOKEN_PAIR',
ANONYMOUS_ACCESS: 'ANONYMOUS_ACCESS',
});
module.exports = {
ShareFileType,
TaskStatus,
@ -348,5 +378,7 @@
colors,
Source,
DimensionType,
CloudStorageProviderType,
CloudStorageCredentialsType,
};
})();

@ -14,6 +14,10 @@ class MLModel {
this._framework = data.framework;
this._description = data.description;
this._type = data.type;
this._tip = {
message: data.help_message,
gif: data.animated_gif,
};
this._params = {
canvas: {
minPosVertices: data.min_pos_points,
@ -84,6 +88,25 @@ class MLModel {
canvas: { ...this._params.canvas },
};
}
/**
* @typedef {Object} MlModelTip
* @property {string} message A short message for a user about the model
* @property {string} gif A gif URL to be shawn to a user as an example
* @returns {MlModelTip}
* @readonly
*/
get tip() {
return { ...this._tip };
}
/**
* @param {(event:string)=>void} onChangeToolsBlockerState Set canvas onChangeToolsBlockerState callback
* @returns {void}
*/
set onChangeToolsBlockerState(onChangeToolsBlockerState) {
this._params.canvas.onChangeToolsBlockerState = onChangeToolsBlockerState;
}
}
module.exports = MLModel;

@ -0,0 +1,74 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
(() => {
const serverProxy = require('./server-proxy');
const { getPreview } = require('./frames');
const { Project } = require('./project');
const { exportDataset } = require('./annotations');
function implementProject(projectClass) {
projectClass.prototype.save.implementation = async function () {
const trainingProjectCopy = this.trainingProject;
if (typeof this.id !== 'undefined') {
// project has been already created, need to update some data
const projectData = {
name: this.name,
assignee_id: this.assignee ? this.assignee.id : null,
bug_tracker: this.bugTracker,
labels: [...this._internalData.labels.map((el) => el.toJSON())],
};
if (trainingProjectCopy) {
projectData.training_project = trainingProjectCopy;
}
await serverProxy.projects.save(this.id, projectData);
return this;
}
// initial creating
const projectSpec = {
name: this.name,
labels: [...this.labels.map((el) => el.toJSON())],
};
if (this.bugTracker) {
projectSpec.bug_tracker = this.bugTracker;
}
if (trainingProjectCopy) {
projectSpec.training_project = trainingProjectCopy;
}
const project = await serverProxy.projects.create(projectSpec);
return new Project(project);
};
projectClass.prototype.delete.implementation = async function () {
const result = await serverProxy.projects.delete(this.id);
return result;
};
projectClass.prototype.preview.implementation = async function () {
if (!this._internalData.task_ids.length) {
return '';
}
const frameData = await getPreview(this._internalData.task_ids[0]);
return frameData;
};
projectClass.prototype.annotations.exportDataset.implementation = async function (
format, saveImages, customName,
) {
const result = exportDataset(this, format, customName, saveImages);
return result;
};
return projectClass;
}
module.exports = implementProject;
})();

@ -4,11 +4,9 @@
(() => {
const PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
const { ArgumentError } = require('./exceptions');
const { Task } = require('./session');
const { Label } = require('./labels');
const { getPreview } = require('./frames');
const User = require('./user');
/**
@ -36,6 +34,7 @@
task_subsets: undefined,
training_project: undefined,
task_ids: undefined,
dimension: undefined,
};
for (const property in data) {
@ -155,7 +154,7 @@
/**
* @name createdDate
* @type {string}
* @memberof module:API.cvat.classes.Task
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
@ -165,13 +164,24 @@
/**
* @name updatedDate
* @type {string}
* @memberof module:API.cvat.classes.Task
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
updatedDate: {
get: () => data.updated_date,
},
/**
* Dimesion of the tasks in the project, if no task dimension is null
* @name dimension
* @type {string}
* @memberof module:API.cvat.classes.Project
* @readonly
* @instance
*/
dimension: {
get: () => data.dimension,
},
/**
* After project has been created value can be appended only.
* @name labels
@ -203,7 +213,7 @@
},
},
/**
* Tasks linked with the project
* Tasks related with the project
* @name tasks
* @type {module:API.cvat.classes.Task[]}
* @memberof module:API.cvat.classes.Project
@ -214,7 +224,7 @@
get: () => [...data.tasks],
},
/**
* Subsets array for linked tasks
* Subsets array for related tasks
* @name subsets
* @type {string[]}
* @memberof module:API.cvat.classes.Project
@ -254,6 +264,13 @@
},
}),
);
// When we call a function, for example: project.annotations.get()
// In the method get we lose the project context
// So, we need return it
this.annotations = {
exportDataset: Object.getPrototypeOf(this).annotations.exportDataset.bind(this),
};
}
/**
@ -289,7 +306,7 @@
}
/**
* Method deletes a task from a server
* Method deletes a project from a server
* @method delete
* @memberof module:API.cvat.classes.Project
* @readonly
@ -304,57 +321,28 @@
}
}
Object.defineProperties(
Project.prototype,
Object.freeze({
annotations: Object.freeze({
value: {
async exportDataset(format, saveImages, customName = '') {
const result = await PluginRegistry.apiWrapper.call(
this,
Project.prototype.annotations.exportDataset,
format,
saveImages,
customName,
);
return result;
},
},
writable: true,
}),
}),
);
module.exports = {
Project,
};
Project.prototype.save.implementation = async function () {
const trainingProjectCopy = this.trainingProject;
if (typeof this.id !== 'undefined') {
// project has been already created, need to update some data
const projectData = {
name: this.name,
assignee_id: this.assignee ? this.assignee.id : null,
bug_tracker: this.bugTracker,
labels: [...this._internalData.labels.map((el) => el.toJSON())],
};
if (trainingProjectCopy) {
projectData.training_project = trainingProjectCopy;
}
await serverProxy.projects.save(this.id, projectData);
return this;
}
// initial creating
const projectSpec = {
name: this.name,
labels: [...this.labels.map((el) => el.toJSON())],
};
if (this.bugTracker) {
projectSpec.bug_tracker = this.bugTracker;
}
if (trainingProjectCopy) {
projectSpec.training_project = trainingProjectCopy;
}
const project = await serverProxy.projects.create(projectSpec);
return new Project(project);
};
Project.prototype.delete.implementation = async function () {
const result = await serverProxy.projects.delete(this.id);
return result;
};
Project.prototype.preview.implementation = async function () {
if (!this._internalData.task_ids.length) {
return '';
}
const frameData = await getPreview(this._internalData.task_ids[0]);
return frameData;
};
})();

@ -465,29 +465,39 @@
}
}
async function exportDataset(id, format) {
const { backendAPI } = config;
let url = `${backendAPI}/tasks/${id}/dataset?format=${format}`;
function exportDataset(instanceType) {
return async function (id, format, name, saveImages) {
const { backendAPI } = config;
const baseURL = `${backendAPI}/${instanceType}/${id}/${saveImages ? 'dataset' : 'annotations'}`;
let query = `format=${encodeURIComponent(format)}`;
if (name) {
const filename = name.replace(/\//g, '_');
query += `&filename=${encodeURIComponent(filename)}`;
}
let url = `${baseURL}?${query}`;
return new Promise((resolve, reject) => {
async function request() {
try {
const response = await Axios.get(`${url}`, {
return new Promise((resolve, reject) => {
async function request() {
Axios.get(`${url}`, {
proxy: config.proxy,
});
if (response.status === 202) {
setTimeout(request, 3000);
} else {
url = `${url}&action=download`;
resolve(url);
}
} catch (errorData) {
reject(generateError(errorData));
})
.then((response) => {
if (response.status === 202) {
setTimeout(request, 3000);
} else {
query = `${query}&action=download`;
url = `${baseURL}?${query}`;
resolve(url);
}
})
.catch((errorData) => {
reject(generateError(errorData));
});
}
}
setTimeout(request);
});
setTimeout(request);
});
};
}
async function exportTask(id) {
@ -1169,6 +1179,121 @@
}
}
async function createCloudStorage(storageDetail) {
const { backendAPI } = config;
try {
const response = await Axios.post(`${backendAPI}/cloudstorages`, JSON.stringify(storageDetail), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
return response.data;
} catch (errorData) {
throw generateError(errorData);
}
}
async function updateCloudStorage(id, cloudStorageData) {
const { backendAPI } = config;
try {
await Axios.patch(`${backendAPI}/cloudstorages/${id}`, JSON.stringify(cloudStorageData), {
proxy: config.proxy,
headers: {
'Content-Type': 'application/json',
},
});
} catch (errorData) {
throw generateError(errorData);
}
}
async function getCloudStorages(filter = '') {
const { backendAPI } = config;
let response = null;
try {
response = await Axios.get(`${backendAPI}/cloudstorages?page_size=12&${filter}`, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
response.data.results.count = response.data.count;
return response.data.results;
}
async function getCloudStorageContent(id, manifestPath) {
const { backendAPI } = config;
let response = null;
try {
const url = `${backendAPI}/cloudstorages/${id}/content${
manifestPath ? `?manifest_path=${manifestPath}` : ''
}`;
response = await Axios.get(url, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function getCloudStoragePreview(id) {
const { backendAPI } = config;
let response = null;
try {
const url = `${backendAPI}/cloudstorages/${id}/preview`;
response = await workerAxios.get(url, {
proxy: config.proxy,
responseType: 'arraybuffer',
});
} catch (errorData) {
throw generateError({
...errorData,
message: '',
response: {
...errorData.response,
data: String.fromCharCode.apply(null, new Uint8Array(errorData.response.data)),
},
});
}
return new Blob([new Uint8Array(response)]);
}
async function getCloudStorageStatus(id) {
const { backendAPI } = config;
let response = null;
try {
const url = `${backendAPI}/cloudstorages/${id}/status`;
response = await Axios.get(url, {
proxy: config.proxy,
});
} catch (errorData) {
throw generateError(errorData);
}
return response.data;
}
async function deleteCloudStorage(id) {
const { backendAPI } = config;
try {
await Axios.delete(`${backendAPI}/cloudstorages/${id}`);
} catch (errorData) {
throw generateError(errorData);
}
}
Object.defineProperties(
this,
Object.freeze({
@ -1199,6 +1324,7 @@
save: saveProject,
create: createProject,
delete: deleteProject,
exportDataset: exportDataset('projects'),
}),
writable: false,
},
@ -1209,7 +1335,7 @@
saveTask,
createTask,
deleteTask,
exportDataset,
exportDataset: exportDataset('tasks'),
exportTask,
importTask,
}),
@ -1297,6 +1423,19 @@
}),
writable: false,
},
cloudStorages: {
value: Object.freeze({
get: getCloudStorages,
getContent: getCloudStorageContent,
getPreview: getCloudStoragePreview,
getStatus: getCloudStorageStatus,
create: createCloudStorage,
delete: deleteCloudStorage,
update: updateCloudStorage,
}),
writable: false,
},
}),
);
}

@ -42,16 +42,6 @@
return result;
},
async dump(dumper, name = null) {
const result = await PluginRegistry.apiWrapper.call(
this,
prototype.annotations.dump,
dumper,
name,
);
return result;
},
async statistics() {
const result = await PluginRegistry.apiWrapper.call(this, prototype.annotations.statistics);
return result;
@ -148,11 +138,13 @@
return result;
},
async exportDataset(format) {
async exportDataset(format, saveImages, customName = '') {
const result = await PluginRegistry.apiWrapper.call(
this,
prototype.annotations.exportDataset,
format,
saveImages,
customName,
);
return result;
},
@ -329,21 +321,6 @@
* @instance
* @async
*/
/**
* Dump of annotations to a file.
* Method always dumps annotations for a whole task.
* @method dump
* @memberof Session.annotations
* @param {module:API.cvat.classes.Dumper} dumper - a dumper
* @param {string} [name = null] - a name of a file with annotations
* which will be used to dump
* @returns {string} URL which can be used in order to get a dump file
* @throws {module:API.cvat.exceptions.PluginError}
* @throws {module:API.cvat.exceptions.ServerError}
* @throws {module:API.cvat.exceptions.ArgumentError}
* @instance
* @async
*/
/**
* Collect short statistics about a task or a job.
* @method statistics
@ -877,7 +854,6 @@
get: Object.getPrototypeOf(this).annotations.get.bind(this),
put: Object.getPrototypeOf(this).annotations.put.bind(this),
save: Object.getPrototypeOf(this).annotations.save.bind(this),
dump: Object.getPrototypeOf(this).annotations.dump.bind(this),
merge: Object.getPrototypeOf(this).annotations.merge.bind(this),
split: Object.getPrototypeOf(this).annotations.split.bind(this),
group: Object.getPrototypeOf(this).annotations.group.bind(this),
@ -1036,6 +1012,7 @@
use_cache: undefined,
copy_data: undefined,
dimension: undefined,
cloud_storage_id: undefined,
};
const updatedFields = new FieldUpdateTrigger({
@ -1397,7 +1374,7 @@
get: () => [...data.jobs],
},
/**
* List of files from shared resource
* List of files from shared resource or list of cloud storage files
* @name serverFiles
* @type {string[]}
* @memberof module:API.cvat.classes.Task
@ -1559,6 +1536,15 @@
*/
get: () => data.dimension,
},
/**
* @name cloudStorageId
* @type {integer|null}
* @memberof module:API.cvat.classes.Task
* @instance
*/
cloudStorageId: {
get: () => data.cloud_storage_id,
},
_internalData: {
get: () => data,
},
@ -1575,7 +1561,6 @@
get: Object.getPrototypeOf(this).annotations.get.bind(this),
put: Object.getPrototypeOf(this).annotations.put.bind(this),
save: Object.getPrototypeOf(this).annotations.save.bind(this),
dump: Object.getPrototypeOf(this).annotations.dump.bind(this),
merge: Object.getPrototypeOf(this).annotations.merge.bind(this),
split: Object.getPrototypeOf(this).annotations.split.bind(this),
group: Object.getPrototypeOf(this).annotations.group.bind(this),
@ -1715,7 +1700,6 @@
selectObject,
annotationsStatistics,
uploadAnnotations,
dumpAnnotations,
importAnnotations,
exportAnnotations,
exportDataset,
@ -1948,13 +1932,8 @@
return result;
};
Job.prototype.annotations.dump.implementation = async function (dumper, name) {
const result = await dumpAnnotations(this, name, dumper);
return result;
};
Job.prototype.annotations.exportDataset.implementation = async function (format) {
const result = await exportDataset(this.task, format);
Job.prototype.annotations.exportDataset.implementation = async function (format, saveImages, customName) {
const result = await exportDataset(this.task, format, customName, saveImages);
return result;
};
@ -2093,6 +2072,9 @@
if (typeof this.copyData !== 'undefined') {
taskDataSpec.copy_data = this.copyData;
}
if (typeof this.cloudStorageId !== 'undefined') {
taskDataSpec.cloud_storage_id = this.cloudStorageId;
}
const task = await serverProxy.tasks.createTask(taskSpec, taskDataSpec, onUpdate);
return new Task(task);
@ -2252,11 +2234,6 @@
return result;
};
Task.prototype.annotations.dump.implementation = async function (dumper, name) {
const result = await dumpAnnotations(this, name, dumper);
return result;
};
Task.prototype.annotations.import.implementation = function (data) {
const result = importAnnotations(this, data);
return result;
@ -2267,8 +2244,8 @@
return result;
};
Task.prototype.annotations.exportDataset.implementation = async function (format) {
const result = await exportDataset(this, format);
Task.prototype.annotations.exportDataset.implementation = async function (format, saveImages, customName) {
const result = await exportDataset(this, format, customName, saveImages);
return result;
};

@ -0,0 +1,178 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
// Setup mock for a server
jest.mock('../../src/server-proxy', () => {
const mock = require('../mocks/server-proxy.mock');
return mock;
});
// Initialize api
window.cvat = require('../../src/api');
const { CloudStorage } = require('../../src/cloud-storage');
const { cloudStoragesDummyData } = require('../mocks/dummy-data.mock');
describe('Feature: get cloud storages', () => {
test('get all cloud storages', async () => {
const result = await window.cvat.cloudStorages.get();
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(cloudStoragesDummyData.count);
for (const item of result) {
expect(item).toBeInstanceOf(CloudStorage);
}
});
test('get cloud storage by id', async () => {
const result = await window.cvat.cloudStorages.get({
id: 1,
});
const cloudStorage = result[0];
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(1);
expect(cloudStorage).toBeInstanceOf(CloudStorage);
expect(cloudStorage.id).toBe(1);
expect(cloudStorage.providerType).toBe('AWS_S3_BUCKET');
expect(cloudStorage.credentialsType).toBe('KEY_SECRET_KEY_PAIR');
expect(cloudStorage.resourceName).toBe('bucket');
expect(cloudStorage.displayName).toBe('Demonstration bucket');
expect(cloudStorage.manifests).toHaveLength(1);
expect(cloudStorage.manifests[0]).toBe('manifest.jsonl');
expect(cloudStorage.specificAttributes).toBe('');
expect(cloudStorage.description).toBe('It is first bucket');
});
test('get a cloud storage by an unknown id', async () => {
const result = await window.cvat.cloudStorages.get({
id: 10,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(0);
});
test('get a cloud storage by an invalid id', async () => {
expect(
window.cvat.cloudStorages.get({
id: '1',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
test('get cloud storages by filters', async () => {
const filters = new Map([
['providerType', 'AWS_S3_BUCKET'],
['resourceName', 'bucket'],
['displayName', 'Demonstration bucket'],
['credentialsType', 'KEY_SECRET_KEY_PAIR'],
['description', 'It is first bucket'],
]);
const result = await window.cvat.cloudStorages.get(Object.fromEntries(filters));
const [cloudStorage] = result;
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(1);
expect(cloudStorage).toBeInstanceOf(CloudStorage);
expect(cloudStorage.id).toBe(1);
filters.forEach((value, key) => {
expect(cloudStorage[key]).toBe(value);
});
});
test('get cloud storage by invalid filters', async () => {
expect(
window.cvat.cloudStorages.get({
unknown: '5',
}),
).rejects.toThrow(window.cvat.exceptions.ArgumentError);
});
});
describe('Feature: create a cloud storage', () => {
test('create new cloud storage without an id', async () => {
const cloudStorage = new window.cvat.classes.CloudStorage({
display_name: 'new cloud storage',
provider_type: 'AZURE_CONTAINER',
resource: 'newcontainer',
credentials_type: 'ACCOUNT_NAME_TOKEN_PAIR',
account_name: 'accountname',
session_token: 'x'.repeat(135),
manifests: ['manifest.jsonl'],
});
const result = await cloudStorage.save();
expect(typeof result.id).toBe('number');
});
});
describe('Feature: update a cloud storage', () => {
test('update cloud storage with some new field values', async () => {
const newValues = new Map([
['displayName', 'new display name'],
['credentialsType', 'ANONYMOUS_ACCESS'],
['description', 'new description'],
['specificAttributes', 'region=eu-west-1'],
]);
let result = await window.cvat.cloudStorages.get({
id: 1,
});
let [cloudStorage] = result;
for (const [key, value] of newValues) {
cloudStorage[key] = value;
}
cloudStorage.save();
result = await window.cvat.cloudStorages.get({
id: 1,
});
[cloudStorage] = result;
newValues.forEach((value, key) => {
expect(cloudStorage[key]).toBe(value);
});
});
test('Update manifests in a cloud storage', async () => {
const newManifests = [
'sub1/manifest.jsonl',
'sub2/manifest.jsonl',
];
let result = await window.cvat.cloudStorages.get({
id: 1,
});
let [cloudStorage] = result;
cloudStorage.manifests = newManifests;
cloudStorage.save();
result = await window.cvat.cloudStorages.get({
id: 1,
});
[cloudStorage] = result;
expect(cloudStorage.manifests).toEqual(newManifests);
});
});
describe('Feature: delete a cloud storage', () => {
test('delete a cloud storage', async () => {
let result = await window.cvat.cloudStorages.get({
id: 2,
});
await result[0].delete();
result = await window.cvat.cloudStorages.get({
id: 2,
});
expect(Array.isArray(result)).toBeTruthy();
expect(result).toHaveLength(0);
});
});

@ -2547,6 +2547,56 @@ const frameMetaDummyData = {
},
};
const cloudStoragesDummyData = {
count: 2,
next: null,
previous: null,
results: [
{
id: 2,
owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'maya',
first_name: '',
last_name: ''
},
manifests: [
'manifest.jsonl'
],
provider_type: 'AZURE_CONTAINER',
resource: 'container',
display_name: 'Demonstration container',
created_date: '2021-09-01T09:29:47.094244Z',
updated_date: '2021-09-01T09:29:47.103264Z',
credentials_type: 'ACCOUNT_NAME_TOKEN_PAIR',
specific_attributes: '',
description: 'It is first container'
},
{
id: 1,
owner: {
url: 'http://localhost:7000/api/v1/users/1',
id: 1,
username: 'maya',
first_name: '',
last_name: ''
},
manifests: [
'manifest.jsonl'
],
provider_type: 'AWS_S3_BUCKET',
resource: 'bucket',
display_name: 'Demonstration bucket',
created_date: '2021-08-31T09:03:09.350817Z',
updated_date: '2021-08-31T15:16:21.394773Z',
credentials_type: 'KEY_SECRET_KEY_PAIR',
specific_attributes: '',
description: 'It is first bucket'
}
]
};
module.exports = {
tasksDummyData,
projectsDummyData,
@ -2557,4 +2607,5 @@ module.exports = {
jobAnnotationsDummyData,
frameMetaDummyData,
formatsDummyData,
cloudStoragesDummyData,
};

@ -12,6 +12,7 @@ const {
taskAnnotationsDummyData,
jobAnnotationsDummyData,
frameMetaDummyData,
cloudStoragesDummyData,
} = require('./dummy-data.mock');
function QueryStringToJSON(query, ignoreList = []) {
@ -318,6 +319,63 @@ class ServerProxy {
return null;
}
async function getCloudStorages(filter = '') {
const queries = QueryStringToJSON(filter);
const result = cloudStoragesDummyData.results.filter((item) => {
for (const key in queries) {
if (Object.prototype.hasOwnProperty.call(queries, key)) {
if (queries[key] !== item[key]) {
return false;
}
}
}
return true;
});
return result;
}
async function updateCloudStorage(id, cloudStorageData) {
const cloudStorage = cloudStoragesDummyData.results.find((item) => item.id === id);
if (cloudStorage) {
for (const prop in cloudStorageData) {
if (
Object.prototype.hasOwnProperty.call(cloudStorageData, prop)
&& Object.prototype.hasOwnProperty.call(cloudStorage, prop)
) {
cloudStorage[prop] = cloudStorageData[prop];
}
}
}
}
async function createCloudStorage(cloudStorageData) {
const id = Math.max(...cloudStoragesDummyData.results.map((item) => item.id)) + 1;
cloudStoragesDummyData.results.push({
id,
provider_type: cloudStorageData.provider_type,
resource: cloudStorageData.resource,
display_name: cloudStorageData.display_name,
credentials_type: cloudStorageData.credentials_type,
specific_attributes: cloudStorageData.specific_attributes,
description: cloudStorageData.description,
owner: 1,
created_date: '2021-09-01T09:29:47.094244+03:00',
updated_date: '2021-09-01T09:29:47.103264+03:00',
});
const result = await getCloudStorages(`?id=${id}`);
return result[0];
}
async function deleteCloudStorage(id) {
const cloudStorages = cloudStoragesDummyData.results;
const cloudStorageId = cloudStorages.findIndex((item) => item.id === id);
if (cloudStorageId !== -1) {
cloudStorages.splice(cloudStorageId);
}
}
Object.defineProperties(
this,
Object.freeze({
@ -384,6 +442,16 @@ class ServerProxy {
getAnnotations,
},
},
cloudStorages: {
value: Object.freeze({
get: getCloudStorages,
update: updateCloudStorage,
create: createCloudStorage,
delete: deleteCloudStorage,
}),
writable: false,
},
}),
);
}

@ -52,14 +52,7 @@ const webConfig = {
use: {
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
targets: '> 2.5%',
},
],
],
presets: ['@babel/preset-env'],
sourceType: 'unambiguous',
},
},

@ -4832,9 +4832,9 @@
"dev": true
},
"jszip": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.0.tgz",
"integrity": "sha512-Y2OlFIzrDOPWUnpU0LORIcDn2xN7rC9yKffFM/7pGhQuhO+SUhfm2trkJ/S5amjFvem0Y+1EALz/MEPkvHXVNw==",
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.7.1.tgz",
"integrity": "sha512-ghL0tz1XG9ZEmRMcEN2vt7xabrDdqHHeykgARpmZ0BiIctWxM47Vt63ZO2dnp4QYt/xJVLLy5Zv1l/xRdh2byg==",
"requires": {
"lie": "~3.3.0",
"pako": "~1.0.2",

@ -3,6 +3,19 @@
"version": "1.0.2",
"description": "",
"main": "src/js/cvat-data.js",
"scripts": {
"patch": "cd src/js && patch --dry-run --forward -p0 < 3rdparty_patch.diff >> /dev/null && patch -p0 < 3rdparty_patch.diff; true",
"build": "npm run patch; webpack --config ./webpack.config.js",
"server": "npm run patch; nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --mode=development --open'"
},
"author": "Intel",
"license": "MIT",
"browserslist": [
"Chrome >= 63",
"Firefox > 58",
"not IE 11",
"> 2%"
],
"devDependencies": {
"@babel/cli": "^7.13.16",
"@babel/core": "^7.4.4",
@ -22,13 +35,6 @@
},
"dependencies": {
"async-mutex": "^0.3.1",
"jszip": "3.7.0"
},
"scripts": {
"patch": "cd src/js && patch --dry-run --forward -p0 < 3rdparty_patch.diff >> /dev/null && patch -p0 < 3rdparty_patch.diff; true",
"build": "npm run patch; webpack --config ./webpack.config.js",
"server": "npm run patch; nodemon --watch config --exec 'webpack-dev-server --config ./webpack.config.js --mode=development --open'"
},
"author": "Intel",
"license": "MIT"
"jszip": "3.7.1"
}
}

@ -29,14 +29,7 @@ const cvatData = {
use: {
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
targets: '> 2.5%', // https://github.com/browserslist/browserslist
},
],
],
presets: ['@babel/preset-env'],
sourceType: 'unambiguous',
},
},

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.21.1",
"version": "1.23.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -1244,9 +1244,9 @@
"dev": true
},
"@types/lodash": {
"version": "4.14.170",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.170.tgz",
"integrity": "sha512-bpcvu/MKHHeYX+qeEN8GE7DIravODWdACVA1ctevD8CN24RhPZIKMn9ntfAsrvLfSX3cR5RrBKAbYm9bGs0A+Q=="
"version": "4.14.172",
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.172.tgz",
"integrity": "sha512-/BHF5HAx3em7/KkzVKm3LrsD6HZAXuXO1AJZQ3cRRBZj4oHZDviWPYu0aEplAqDFNHZPW6d3G7KN+ONcCCC7pw=="
},
"@types/minimatch": {
"version": "3.0.3",
@ -1267,9 +1267,9 @@
"dev": true
},
"@types/platform": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/@types/platform/-/platform-1.3.3.tgz",
"integrity": "sha512-1fuOulBHWIxAPLBtLms+UtbeRDt6rL7gP5R+Yugfzdg+poCLxXqXTE8i+FpYeiytGRLUEtnFkjsY/j+usbQBqw=="
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/@types/platform/-/platform-1.3.4.tgz",
"integrity": "sha512-U0o4K+GNiK0PNxoDwd8xRnvLVe4kzei6opn3/FCjAriqaP+rfrDdSl1kP/hLL6Y3/Y3hhGnBwD4dCkkAqs1W/Q=="
},
"@types/prop-types": {
"version": "15.7.4",
@ -1283,9 +1283,9 @@
"dev": true
},
"@types/react": {
"version": "16.14.10",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.10.tgz",
"integrity": "sha512-QadBsMyF6ldjEAXEhsmEW/L0uBDJT8yw7Qoe5sRnEKVrzMkiYoJwqoL5TKJOlArsn/wvIJM/XdVzkdL6+AS64Q==",
"version": "16.14.12",
"resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.12.tgz",
"integrity": "sha512-7nOJgNsRbARhZhvwPm7cnzahtzEi5VJ9OvcQk8ExEEb1t+zaFklwLVkJz7G1kfxX4X/mDa/icTmzE0vTmqsqBg==",
"requires": {
"@types/prop-types": "*",
"@types/scheduler": "*",
@ -1293,26 +1293,26 @@
}
},
"@types/react-color": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.4.tgz",
"integrity": "sha512-EswbYJDF1kkrx93/YU+BbBtb46CCtDMvTiGmcOa/c5PETnwTiSWoseJ1oSWeRl/4rUXkhME9bVURvvPg0W5YQw==",
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@types/react-color/-/react-color-3.0.5.tgz",
"integrity": "sha512-0VZy8Uq5x04cW5QFz24Qw8MMMlsMi8Bb+XG5h59ATqPnWVq6OheHtrwv5LeakdTRDaECQnExJNSFOsSe4Eo/zQ==",
"requires": {
"@types/react": "*",
"@types/reactcss": "*"
}
},
"@types/react-dom": {
"version": "16.9.13",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.13.tgz",
"integrity": "sha512-34Hr3XnmUSJbUVDxIw/e7dhQn2BJZhJmlAaPyPwfTQyuVS9mV/CeyghFcXyvkJXxI7notQJz8mF8FeCVvloJrA==",
"version": "16.9.14",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.14.tgz",
"integrity": "sha512-FIX2AVmPTGP30OUJ+0vadeIFJJ07Mh1m+U0rxfgyW34p3rTlXI+nlenvAxNn4BP36YyI9IJ/+UJ7Wu22N1pI7A==",
"requires": {
"@types/react": "^16"
}
},
"@types/react-redux": {
"version": "7.1.16",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.16.tgz",
"integrity": "sha512-f/FKzIrZwZk7YEO9E1yoxIuDNRiDducxkFlkw/GNMGEnK9n4K8wJzlJBghpSuOVDgEUHoDkDF7Gi9lHNQR4siw==",
"version": "7.1.18",
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.18.tgz",
"integrity": "sha512-9iwAsPyJ9DLTRH+OFeIrm9cAbIj1i2ANL3sKQFATqnPWRbg+jEFXyZOKHiQK/N86pNRXbb4HRxAxo0SIX1XwzQ==",
"requires": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
@ -1321,26 +1321,26 @@
}
},
"@types/react-resizable": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@types/react-resizable/-/react-resizable-1.7.2.tgz",
"integrity": "sha512-6c6L94+VOksr9838LDrlYeucic2+0qkGnwolGE77YJztYHCWSucQV0e9+Qyl+uHpJTBRS95A5JESBg5NgCAC3A==",
"version": "1.7.3",
"resolved": "https://registry.npmjs.org/@types/react-resizable/-/react-resizable-1.7.3.tgz",
"integrity": "sha512-DAx+hdnHFMJHgl8geiKo3jLt1GCT838SwQixjCtbRRfqCBawAKriVLCZ1nvp7B/2Pxd94MWod8NyJEnAAmNHNA==",
"requires": {
"@types/react": "*"
}
},
"@types/react-router": {
"version": "5.1.15",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.15.tgz",
"integrity": "sha512-z3UlMG/x91SFEVmmvykk9FLTliDvfdIUky4k2rCfXWQ0NKbrP8o9BTCaCTPuYsB8gDkUnUmkcA2vYlm2DR+HAA==",
"version": "5.1.16",
"resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.16.tgz",
"integrity": "sha512-8d7nR/fNSqlTFGHti0R3F9WwIertOaaA1UEB8/jr5l5mDMOs4CidEgvvYMw4ivqrBK+vtVLxyTj2P+Pr/dtgzg==",
"requires": {
"@types/history": "*",
"@types/react": "*"
}
},
"@types/react-router-dom": {
"version": "5.1.7",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.7.tgz",
"integrity": "sha512-D5mHD6TbdV/DNHYsnwBTv+y73ei+mMjrkGrla86HthE4/PVvL1J94Bu3qABU+COXzpL23T1EZapVVpwHuBXiUg==",
"version": "5.1.8",
"resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.1.8.tgz",
"integrity": "sha512-03xHyncBzG0PmDmf8pf3rehtjY0NpUj7TIN46FrT5n1ZWHPZvXz32gUyNboJ+xsL8cpg8bQVLcllptcQHvocrw==",
"requires": {
"@types/history": "*",
"@types/react": "*",
@ -1356,25 +1356,25 @@
}
},
"@types/reactcss": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.3.tgz",
"integrity": "sha512-d2gQQ0IL6hXLnoRfVYZukQNWHuVsE75DzFTLPUuyyEhJS8G2VvlE+qfQQ91SJjaMqlURRCNIsX7Jcsw6cEuJlA==",
"version": "1.2.4",
"resolved": "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.4.tgz",
"integrity": "sha512-1rhVqteMSD6KQjO+dPBObE1OkKadw00HVJkG5WCYsyvMwGgdTZ530wF7Bkrg/4TWxB2AtINIzFotjW51eViw+w==",
"requires": {
"@types/react": "*"
}
},
"@types/redux-logger": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.8.tgz",
"integrity": "sha512-zM+cxiSw6nZtRbxpVp9SE3x/X77Z7e7YAfHD1NkxJyJbAGSXJGF0E9aqajZfPOa/sTYnuwutmlCldveExuCeLw==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/@types/redux-logger/-/redux-logger-3.0.9.tgz",
"integrity": "sha512-cwYhVbYNgH01aepeMwhd0ABX6fhVB2rcQ9m80u8Fl50ZODhsZ8RhQArnLTkE7/Zrfq4Sz/taNoF7DQy9pCZSKg==",
"requires": {
"redux": "^4.0.0"
}
},
"@types/resize-observer-browser": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.5.tgz",
"integrity": "sha512-8k/67Z95Goa6Lznuykxkfhq9YU3l1Qe6LNZmwde1u7802a3x8v44oq0j91DICclxatTr0rNnhXx7+VTIetSrSQ=="
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/@types/resize-observer-browser/-/resize-observer-browser-0.1.6.tgz",
"integrity": "sha512-61IfTac0s9jvNtBCpyo86QeaN8qqpMGHdK0uGKCCIy2dt5/Yk84VduHIdWAcmkC5QvdkPL0p5eWYgUZtHKKUVg=="
},
"@types/scheduler": {
"version": "0.16.2",
@ -7664,8 +7664,7 @@
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
"integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
"requires": {
"is-core-module": "^2.2.0",
"path-parse": "^1.0.6"
"is-core-module": "^2.2.0"
}
}
}
@ -7759,8 +7758,7 @@
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz",
"integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==",
"requires": {
"is-core-module": "^2.2.0",
"path-parse": "^1.0.6"
"is-core-module": "^2.2.0"
}
}
}
@ -11289,11 +11287,6 @@
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@ -12296,10 +12289,7 @@
"resolve": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz",
"integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==",
"requires": {
"path-parse": "^1.0.6"
}
"integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw=="
},
"resolve-cwd": {
"version": "2.0.0",
@ -21051,11 +21041,6 @@
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
},
"path-to-regexp": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
@ -22054,10 +22039,7 @@
"resolve": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.11.1.tgz",
"integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw==",
"requires": {
"path-parse": "^1.0.6"
}
"integrity": "sha512-vIpgF6wfuJOZI7KKKSP+HmiKggadPQAdsp5HiC1mvqnfp0gF1vdwgBWZIdrVft9pgqoMFQN+R7BSWZiBxx+BBw=="
},
"resolve-cwd": {
"version": "2.0.0",
@ -27255,8 +27237,7 @@
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
"integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
"requires": {
"is-core-module": "^2.1.0",
"path-parse": "^1.0.6"
"is-core-module": "^2.1.0"
}
},
"semver": {
@ -35054,11 +35035,6 @@
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
},
"path-type": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz",
@ -35512,10 +35488,7 @@
"resolve": {
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.12.0.tgz",
"integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w==",
"requires": {
"path-parse": "^1.0.6"
}
"integrity": "sha512-B/dOmuoAik5bKcD6s6nXDCjzUKnaDvdkRyAk6rsmsKLipWj4797iothd7jmmUhWTfinVMU+wc56rYKsit2Qy4w=="
},
"resolve-cwd": {
"version": "2.0.0",
@ -40741,8 +40714,7 @@
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
"integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
"requires": {
"is-core-module": "^2.1.0",
"path-parse": "^1.0.6"
"is-core-module": "^2.1.0"
}
},
"semver": {
@ -42211,8 +42183,7 @@
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz",
"integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==",
"requires": {
"is-core-module": "^2.1.0",
"path-parse": "^1.0.6"
"is-core-module": "^2.1.0"
}
},
"slash": {
@ -44212,11 +44183,6 @@
"resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz",
"integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A="
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw=="
},
"pbkdf2": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz",
@ -44769,10 +44735,7 @@
"resolve": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz",
"integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==",
"requires": {
"path-parse": "^1.0.6"
}
"integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w=="
},
"resolve-cwd": {
"version": "2.0.0",
@ -52297,9 +52260,9 @@
"dev": true
},
"path-parse": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==",
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
"path-to-regexp": {
@ -54169,9 +54132,9 @@
}
},
"react-cookie": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.0.3.tgz",
"integrity": "sha512-cmi6IpdVgTSvjqssqIEvo779Gfqc4uPGHRrKMEdHcqkmGtPmxolGfsyKj95bhdLEKqMdbX8MLBCwezlnhkHK0g==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/react-cookie/-/react-cookie-4.1.1.tgz",
"integrity": "sha512-ffn7Y7G4bXiFbnE+dKhHhbP+b8I34mH9jqnm8Llhj89zF4nPxPutxHT1suUqMeCEhLDBI7InYwf1tpaSoK5w8A==",
"requires": {
"@types/hoist-non-react-statics": "^3.0.1",
"hoist-non-react-statics": "^3.0.0",
@ -54481,25 +54444,25 @@
}
},
"redux": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.1.0.tgz",
"integrity": "sha512-uI2dQN43zqLWCt6B/BMGRMY6db7TTY4qeHHfGeKb3EOhmOKjU3KdWvNLJyqaHRksv/ErdNH7cFZWg9jXtewy4g==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz",
"integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==",
"requires": {
"@babel/runtime": "^7.9.2"
},
"dependencies": {
"@babel/runtime": {
"version": "7.14.0",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.0.tgz",
"integrity": "sha512-JELkvo/DlpNdJ7dlyw/eY7E0suy5i5GQH+Vlxaq1nsNJ+H7f4Vtv3jMeCEgRhZZQFXTjldYfQgv2qmM6M1v5wA==",
"version": "7.15.3",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.3.tgz",
"integrity": "sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA==",
"requires": {
"regenerator-runtime": "^0.13.4"
}
},
"regenerator-runtime": {
"version": "0.13.7",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz",
"integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew=="
"version": "0.13.9",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz",
"integrity": "sha512-p3VT+cOEgxFsRRA9X4lkI1E+k2/CtnKtU4gcxyaCUreilL/vqI6CdZ3wxVUx3UOUg+gnUOQQcRI7BmSI656MYA=="
}
}
},

@ -1,6 +1,6 @@
{
"name": "cvat-ui",
"version": "1.21.1",
"version": "1.23.1",
"description": "CVAT single-page application",
"main": "src/index.tsx",
"scripts": {
@ -11,6 +11,12 @@
"lint": "eslint './src/**/*.{ts,tsx}'",
"lint:fix": "eslint './src/**/*.{ts,tsx}' --fix"
},
"browserslist": [
"Chrome >= 63",
"Firefox > 58",
"not IE 11",
"> 2%"
],
"author": "Intel",
"license": "MIT",
"devDependencies": {
@ -49,18 +55,18 @@
},
"dependencies": {
"@ant-design/icons": "^4.6.2",
"@types/lodash": "^4.14.170",
"@types/platform": "^1.3.3",
"@types/react": "^16.14.10",
"@types/react-color": "^3.0.4",
"@types/react-dom": "^16.9.13",
"@types/react-redux": "^7.1.16",
"@types/react-resizable": "^1.7.2",
"@types/react-router": "^5.1.15",
"@types/react-router-dom": "^5.1.7",
"@types/lodash": "^4.14.172",
"@types/platform": "^1.3.4",
"@types/react": "^16.14.12",
"@types/react-color": "^3.0.5",
"@types/react-dom": "^16.9.14",
"@types/react-redux": "^7.1.18",
"@types/react-resizable": "^1.7.3",
"@types/react-router": "^5.1.16",
"@types/react-router-dom": "^5.1.8",
"@types/react-share": "^3.0.3",
"@types/redux-logger": "^3.0.8",
"@types/resize-observer-browser": "^0.1.5",
"@types/redux-logger": "^3.0.9",
"@types/resize-observer-browser": "^0.1.6",
"antd": "^4.13.0",
"copy-to-clipboard": "^3.3.1",
"cvat-canvas": "file:../cvat-canvas",
@ -77,7 +83,7 @@
"react": "^16.14.0",
"react-awesome-query-builder": "^3.0.0",
"react-color": "^2.19.3",
"react-cookie": "^4.0.3",
"react-cookie": "^4.1.1",
"react-dom": "^16.14.0",
"react-moment": "^1.1.1",
"react-redux": "^7.2.4",
@ -85,7 +91,7 @@
"react-router": "^5.1.0",
"react-router-dom": "^5.1.0",
"react-share": "^3.0.1",
"redux": "^4.1.0",
"redux": "^4.1.1",
"redux-devtools-extension": "^2.13.9",
"redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0"

@ -1,13 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
/* eslint-disable */
module.exports = {
parser: false,
plugins: {
'postcss-preset-env': {
browsers: '> 2.5%', // https://github.com/browserslist/browserslist
},
},
};

@ -0,0 +1,49 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { ActionUnion, createAction, ThunkAction } from 'utils/redux';
export enum ExportActionTypes {
OPEN_EXPORT_MODAL = 'OPEN_EXPORT_MODAL',
CLOSE_EXPORT_MODAL = 'CLOSE_EXPORT_MODAL',
EXPORT_DATASET = 'EXPORT_DATASET',
EXPORT_DATASET_SUCCESS = 'EXPORT_DATASET_SUCCESS',
EXPORT_DATASET_FAILED = 'EXPORT_DATASET_FAILED',
}
export const exportActions = {
openExportModal: (instance: any) => createAction(ExportActionTypes.OPEN_EXPORT_MODAL, { instance }),
closeExportModal: () => createAction(ExportActionTypes.CLOSE_EXPORT_MODAL),
exportDataset: (instance: any, format: string) =>
createAction(ExportActionTypes.EXPORT_DATASET, { instance, format }),
exportDatasetSuccess: (instance: any, format: string) =>
createAction(ExportActionTypes.EXPORT_DATASET_SUCCESS, { instance, format }),
exportDatasetFailed: (instance: any, format: string, error: any) =>
createAction(ExportActionTypes.EXPORT_DATASET_FAILED, {
instance,
format,
error,
}),
};
export const exportDatasetAsync = (
instance: any,
format: string,
name: string,
saveImages: boolean,
): ThunkAction => async (dispatch) => {
dispatch(exportActions.exportDataset(instance, format));
try {
const url = await instance.annotations.exportDataset(format, saveImages, name);
const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement;
downloadAnchor.href = url;
downloadAnchor.click();
dispatch(exportActions.exportDatasetSuccess(instance, format));
} catch (error) {
dispatch(exportActions.exportDatasetFailed(instance, format, error));
}
};
export type ExportActions = ActionUnion<typeof exportActions>;

@ -3,7 +3,9 @@
// SPDX-License-Identifier: MIT
import { AnyAction } from 'redux';
import { GridColor, ColorBy, SettingsState } from 'reducers/interfaces';
import {
GridColor, ColorBy, SettingsState, ToolsBlockerState,
} from 'reducers/interfaces';
export enum SettingsActionTypes {
SWITCH_ROTATE_ALL = 'SWITCH_ROTATE_ALL',
@ -34,6 +36,7 @@ export enum SettingsActionTypes {
CHANGE_CANVAS_BACKGROUND_COLOR = 'CHANGE_CANVAS_BACKGROUND_COLOR',
SWITCH_SETTINGS_DIALOG = 'SWITCH_SETTINGS_DIALOG',
SET_SETTINGS = 'SET_SETTINGS',
SWITCH_TOOLS_BLOCKER_STATE = 'SWITCH_TOOLS_BLOCKER_STATE',
}
export function changeShapesOpacity(opacity: number): AnyAction {
@ -280,6 +283,15 @@ export function changeDefaultApproxPolyAccuracy(approxPolyAccuracy: number): Any
};
}
export function switchToolsBlockerState(toolsBlockerState: ToolsBlockerState): AnyAction {
return {
type: SettingsActionTypes.SWITCH_TOOLS_BLOCKER_STATE,
payload: {
toolsBlockerState,
},
};
}
export function setSettings(settings: Partial<SettingsState>): AnyAction {
return {
type: SettingsActionTypes.SET_SETTINGS,

@ -18,12 +18,6 @@ export enum TasksActionTypes {
LOAD_ANNOTATIONS = 'LOAD_ANNOTATIONS',
LOAD_ANNOTATIONS_SUCCESS = 'LOAD_ANNOTATIONS_SUCCESS',
LOAD_ANNOTATIONS_FAILED = 'LOAD_ANNOTATIONS_FAILED',
DUMP_ANNOTATIONS = 'DUMP_ANNOTATIONS',
DUMP_ANNOTATIONS_SUCCESS = 'DUMP_ANNOTATIONS_SUCCESS',
DUMP_ANNOTATIONS_FAILED = 'DUMP_ANNOTATIONS_FAILED',
EXPORT_DATASET = 'EXPORT_DATASET',
EXPORT_DATASET_SUCCESS = 'EXPORT_DATASET_SUCCESS',
EXPORT_DATASET_FAILED = 'EXPORT_DATASET_FAILED',
DELETE_TASK = 'DELETE_TASK',
DELETE_TASK_SUCCESS = 'DELETE_TASK_SUCCESS',
DELETE_TASK_FAILED = 'DELETE_TASK_FAILED',
@ -108,60 +102,6 @@ export function getTasksAsync(query: TasksQuery): ThunkAction<Promise<void>, {},
};
}
function dumpAnnotation(task: any, dumper: any): AnyAction {
const action = {
type: TasksActionTypes.DUMP_ANNOTATIONS,
payload: {
task,
dumper,
},
};
return action;
}
function dumpAnnotationSuccess(task: any, dumper: any): AnyAction {
const action = {
type: TasksActionTypes.DUMP_ANNOTATIONS_SUCCESS,
payload: {
task,
dumper,
},
};
return action;
}
function dumpAnnotationFailed(task: any, dumper: any, error: any): AnyAction {
const action = {
type: TasksActionTypes.DUMP_ANNOTATIONS_FAILED,
payload: {
task,
dumper,
error,
},
};
return action;
}
export function dumpAnnotationsAsync(task: any, dumper: any): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
try {
dispatch(dumpAnnotation(task, dumper));
const url = await task.annotations.dump(dumper);
const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement;
downloadAnchor.href = url;
downloadAnchor.click();
} catch (error) {
dispatch(dumpAnnotationFailed(task, dumper, error));
return;
}
dispatch(dumpAnnotationSuccess(task, dumper));
};
}
function loadAnnotations(task: any, loader: any): AnyAction {
const action = {
type: TasksActionTypes.LOAD_ANNOTATIONS,
@ -263,60 +203,6 @@ export function importTaskAsync(file: File): ThunkAction<Promise<void>, {}, {},
};
}
function exportDataset(task: any, exporter: any): AnyAction {
const action = {
type: TasksActionTypes.EXPORT_DATASET,
payload: {
task,
exporter,
},
};
return action;
}
function exportDatasetSuccess(task: any, exporter: any): AnyAction {
const action = {
type: TasksActionTypes.EXPORT_DATASET_SUCCESS,
payload: {
task,
exporter,
},
};
return action;
}
function exportDatasetFailed(task: any, exporter: any, error: any): AnyAction {
const action = {
type: TasksActionTypes.EXPORT_DATASET_FAILED,
payload: {
task,
exporter,
error,
},
};
return action;
}
export function exportDatasetAsync(task: any, exporter: any): ThunkAction<Promise<void>, {}, {}, AnyAction> {
return async (dispatch: ActionCreator<Dispatch>): Promise<void> => {
dispatch(exportDataset(task, exporter));
try {
const url = await task.annotations.exportDataset(exporter.name);
const downloadAnchor = window.document.getElementById('downloadAnchor') as HTMLAnchorElement;
downloadAnchor.href = url;
downloadAnchor.click();
} catch (error) {
dispatch(exportDatasetFailed(task, exporter, error));
}
dispatch(exportDatasetSuccess(task, exporter));
};
}
function exportTask(taskID: number): AnyAction {
const action = {
type: TasksActionTypes.EXPORT_TASK,

@ -9,9 +9,7 @@ import Modal from 'antd/lib/modal';
import { LoadingOutlined } from '@ant-design/icons';
// eslint-disable-next-line import/no-extraneous-dependencies
import { MenuInfo } from 'rc-menu/lib/interface';
import DumpSubmenu from './dump-submenu';
import LoadSubmenu from './load-submenu';
import ExportSubmenu from './export-submenu';
import { DimensionType } from '../../reducers/interfaces';
interface Props {
@ -21,8 +19,6 @@ interface Props {
loaders: any[];
dumpers: any[];
loadActivity: string | null;
dumpActivities: string[] | null;
exportActivities: string[] | null;
inferenceIsActive: boolean;
taskDimension: DimensionType;
onClickMenu: (params: MenuInfo, file?: File) => void;
@ -30,7 +26,6 @@ interface Props {
}
export enum Actions {
DUMP_TASK_ANNO = 'dump_task_anno',
LOAD_TASK_ANNO = 'load_task_anno',
EXPORT_TASK_DATASET = 'export_task_dataset',
DELETE_TASK = 'delete_task',
@ -43,14 +38,10 @@ export enum Actions {
export default function ActionsMenuComponent(props: Props): JSX.Element {
const {
taskID,
taskMode,
bugTracker,
inferenceIsActive,
dumpers,
loaders,
onClickMenu,
dumpActivities,
exportActivities,
loadActivity,
taskDimension,
exportIsActive,
@ -106,13 +97,6 @@ export default function ActionsMenuComponent(props: Props): JSX.Element {
return (
<Menu selectable={false} className='cvat-actions-menu' onClick={onClickMenuWrapper}>
{DumpSubmenu({
taskMode,
dumpers,
dumpActivities,
menuKey: Actions.DUMP_TASK_ANNO,
taskDimension,
})}
{LoadSubmenu({
loaders,
loadActivity,
@ -122,19 +106,14 @@ export default function ActionsMenuComponent(props: Props): JSX.Element {
menuKey: Actions.LOAD_TASK_ANNO,
taskDimension,
})}
{ExportSubmenu({
exporters: dumpers,
exportActivities,
menuKey: Actions.EXPORT_TASK_DATASET,
taskDimension,
})}
<Menu.Item key={Actions.EXPORT_TASK_DATASET}>Export task dataset</Menu.Item>
{!!bugTracker && <Menu.Item key={Actions.OPEN_BUG_TRACKER}>Open bug tracker</Menu.Item>}
<Menu.Item disabled={inferenceIsActive} key={Actions.RUN_AUTO_ANNOTATION}>
Automatic annotation
</Menu.Item>
<Menu.Item key={Actions.EXPORT_TASK} disabled={exportIsActive}>
{exportIsActive && <LoadingOutlined id='cvat-export-task-loading' />}
Export Task
Export task
</Menu.Item>
<hr />
<Menu.Item key={Actions.MOVE_TASK_TO_PROJECT}>Move to project</Menu.Item>

@ -1,54 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Menu from 'antd/lib/menu';
import { DownloadOutlined, LoadingOutlined } from '@ant-design/icons';
import Text from 'antd/lib/typography/Text';
import { DimensionType } from '../../reducers/interfaces';
function isDefaultFormat(dumperName: string, taskMode: string): boolean {
return (
(dumperName === 'CVAT for video 1.1' && taskMode === 'interpolation') ||
(dumperName === 'CVAT for images 1.1' && taskMode === 'annotation')
);
}
interface Props {
taskMode: string;
menuKey: string;
dumpers: any[];
dumpActivities: string[] | null;
taskDimension: DimensionType;
}
export default function DumpSubmenu(props: Props): JSX.Element {
const {
taskMode, menuKey, dumpers, dumpActivities, taskDimension,
} = props;
return (
<Menu.SubMenu key={menuKey} title='Dump annotations'>
{dumpers
.sort((a: any, b: any) => a.name.localeCompare(b.name))
.filter((dumper: any): boolean => dumper.dimension === taskDimension)
.map(
(dumper: any): JSX.Element => {
const pending = (dumpActivities || []).includes(dumper.name);
const disabled = !dumper.enabled || pending;
const isDefault = isDefaultFormat(dumper.name, taskMode);
return (
<Menu.Item key={dumper.name} disabled={disabled} className='cvat-menu-dump-submenu-item'>
<DownloadOutlined />
<Text strong={isDefault} disabled={disabled}>
{dumper.name}
</Text>
{pending && <LoadingOutlined style={{ marginLeft: 10 }} />}
</Menu.Item>
);
},
)}
</Menu.SubMenu>
);
}

@ -1,47 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Menu from 'antd/lib/menu';
import Text from 'antd/lib/typography/Text';
import { ExportOutlined, LoadingOutlined } from '@ant-design/icons';
import { DimensionType } from '../../reducers/interfaces';
interface Props {
menuKey: string;
exporters: any[];
exportActivities: string[] | null;
taskDimension: DimensionType;
}
export default function ExportSubmenu(props: Props): JSX.Element {
const {
menuKey, exporters, exportActivities, taskDimension,
} = props;
return (
<Menu.SubMenu key={menuKey} title='Export as a dataset'>
{exporters
.sort((a: any, b: any) => a.name.localeCompare(b.name))
.filter((exporter: any): boolean => exporter.dimension === taskDimension)
.map(
(exporter: any): JSX.Element => {
const pending = (exportActivities || []).includes(exporter.name);
const disabled = !exporter.enabled || pending;
return (
<Menu.Item
key={exporter.name}
disabled={disabled}
className='cvat-menu-export-submenu-item'
>
<ExportOutlined />
<Text disabled={disabled}>{exporter.name}</Text>
{pending && <LoadingOutlined style={{ marginLeft: 10 }} />}
</Menu.Item>
);
},
)}
</Menu.SubMenu>
);
}

@ -44,7 +44,7 @@ export default function AnnotationPageComponent(props: Props): JSX.Element {
saveLogs();
const root = window.document.getElementById('root');
if (root) {
root.style.minHeight = '600px';
root.style.minHeight = '720px';
}
return () => {

@ -106,6 +106,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
showObjectsTextAlways,
workspace,
showProjections,
selectedOpacity,
} = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas };
@ -121,6 +122,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
forceDisableEditing: workspace === Workspace.REVIEW_WORKSPACE,
intelligentPolygonCrop,
showProjections,
creationOpacity: selectedOpacity,
});
this.initialSetup();
@ -166,7 +168,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
prevProps.showObjectsTextAlways !== showObjectsTextAlways ||
prevProps.automaticBordering !== automaticBordering ||
prevProps.showProjections !== showProjections ||
prevProps.intelligentPolygonCrop !== intelligentPolygonCrop
prevProps.intelligentPolygonCrop !== intelligentPolygonCrop ||
prevProps.selectedOpacity !== selectedOpacity
) {
canvasInstance.configure({
undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE,
@ -174,6 +177,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
autoborders: automaticBordering,
showProjections,
intelligentPolygonCrop,
creationOpacity: selectedOpacity,
});
}
@ -198,7 +202,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.activate(null);
const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`);
if (el) {
(el as any).instance.fill({ opacity: opacity / 100 });
(el as any).instance.fill({ opacity });
}
}
@ -214,7 +218,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
if (gridPattern) {
gridPattern.style.stroke = gridColor.toLowerCase();
gridPattern.style.opacity = `${gridOpacity / 100}`;
gridPattern.style.opacity = `${gridOpacity}`;
}
}
@ -225,10 +229,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
) {
const backgroundElement = window.document.getElementById('cvat_canvas_background');
if (backgroundElement) {
backgroundElement.style.filter =
`brightness(${brightnessLevel / 100})` +
`contrast(${contrastLevel / 100})` +
`saturate(${saturationLevel / 100})`;
const filter = `brightness(${brightnessLevel}) contrast(${contrastLevel}) saturate(${saturationLevel})`;
backgroundElement.style.filter = filter;
}
}
@ -619,7 +621,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`);
if (el) {
((el as any) as SVGElement).setAttribute('fill-opacity', `${selectedOpacity / 100}`);
((el as any) as SVGElement).setAttribute('fill-opacity', `${selectedOpacity}`);
}
}
}
@ -648,7 +650,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
handler.nested.fill({ color: shapeColor });
}
(shapeView as any).instance.fill({ color: shapeColor, opacity: opacity / 100 });
(shapeView as any).instance.fill({ color: shapeColor, opacity });
(shapeView as any).instance.stroke({ color: outlined ? outlineColor : shapeColor });
}
}
@ -710,17 +712,15 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
}
if (gridPattern) {
gridPattern.style.stroke = gridColor.toLowerCase();
gridPattern.style.opacity = `${gridOpacity / 100}`;
gridPattern.style.opacity = `${gridOpacity}`;
}
canvasInstance.grid(gridSize, gridSize);
// Filters
const backgroundElement = window.document.getElementById('cvat_canvas_background');
if (backgroundElement) {
backgroundElement.style.filter =
`brightness(${brightnessLevel / 100})` +
`contrast(${contrastLevel / 100})` +
`saturate(${saturationLevel / 100})`;
const filter = `brightness(${brightnessLevel}) contrast(${contrastLevel}) saturate(${saturationLevel})`;
backgroundElement.style.filter = filter;
}
const canvasWrapperElement = window.document
@ -823,7 +823,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
<ContextImage />
<Dropdown trigger='click' placement='topCenter' overlay={<ImageSetupsContent />}>
<Dropdown trigger={['click']} placement='topCenter' overlay={<ImageSetupsContent />}>
<UpOutlined className='cvat-canvas-image-setups-trigger' />
</Dropdown>

@ -0,0 +1,48 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import Image from 'antd/lib/image';
import Paragraph from 'antd/lib/typography/Paragraph';
import Text from 'antd/lib/typography/Text';
interface Props {
name?: string;
gif?: string;
message?: string;
withNegativePoints?: boolean;
}
function InteractorTooltips(props: Props): JSX.Element {
const {
name, gif, message, withNegativePoints,
} = props;
const UNKNOWN_MESSAGE = 'Selected interactor does not have a help message';
const desc = message || UNKNOWN_MESSAGE;
return (
<div className='cvat-interactor-tip-container'>
{name ? (
<>
<Paragraph>{desc}</Paragraph>
<Paragraph>
<Text>You can prevent server requests holding</Text>
<Text strong>{' Ctrl '}</Text>
<Text>key</Text>
</Paragraph>
<Paragraph>
<Text>Positive points can be added by left-clicking the image. </Text>
{withNegativePoints ? (
<Text>Negative points can be added by right-clicking the image. </Text>
) : null}
</Paragraph>
{gif ? <Image className='cvat-interactor-tip-image' alt='Example gif' src={gif} /> : null}
</>
) : (
<Text>Select an interactor to see help message</Text>
)}
</div>
);
}
export default React.memo(InteractorTooltips);

@ -19,7 +19,7 @@ import getCore from 'cvat-core-wrapper';
import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper';
import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors';
import {
CombinedState, ActiveControl, OpenCVTool, ObjectType, ShapeType,
CombinedState, ActiveControl, OpenCVTool, ObjectType, ShapeType, ToolsBlockerState,
} from 'reducers/interfaces';
import {
interactWithCanvas,
@ -34,6 +34,7 @@ import ApproximationAccuracy, {
thresholdFromAccuracy,
} from 'components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy';
import { ImageProcessing } from 'utils/opencv-wrapper/opencv-interfaces';
import { switchToolsBlockerState } from 'actions/settings-actions';
import withVisibilityHandling from './handle-popover-visibility';
interface Props {
@ -46,6 +47,8 @@ interface Props {
curZOrder: number;
defaultApproxPolyAccuracy: number;
frameData: any;
toolsBlockerState: ToolsBlockerState;
activeControl: ActiveControl;
}
interface DispatchToProps {
@ -54,6 +57,7 @@ interface DispatchToProps {
createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void;
fetchAnnotations(): void;
changeFrame(toFrame: number, fillBuffer?: boolean, frameStep?: number, forceUpdate?: boolean):void;
onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState):void;
}
interface State {
@ -87,12 +91,13 @@ function mapStateToProps(state: CombinedState): Props {
},
},
settings: {
workspace: { defaultApproxPolyAccuracy },
workspace: { defaultApproxPolyAccuracy, toolsBlockerState },
},
} = state;
return {
isActivated: activeControl === ActiveControl.OPENCV_TOOLS,
activeControl,
canvasInstance: canvasInstance as Canvas,
defaultApproxPolyAccuracy,
jobInstance,
@ -101,6 +106,7 @@ function mapStateToProps(state: CombinedState): Props {
states,
frame,
frameData,
toolsBlockerState,
};
}
@ -110,6 +116,7 @@ const mapDispatchToProps = {
fetchAnnotations: fetchAnnotationsAsync,
createAnnotations: createAnnotationsAsync,
changeFrame: changeFrameAsync,
onSwitchToolsBlockerState: switchToolsBlockerState,
};
class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps, State> {
@ -142,7 +149,10 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
public componentDidUpdate(prevProps: Props, prevState: State): void {
const { approxPolyAccuracy } = this.state;
const { isActivated, defaultApproxPolyAccuracy, canvasInstance } = this.props;
const {
isActivated, defaultApproxPolyAccuracy, canvasInstance, toolsBlockerState,
} = this.props;
if (!prevProps.isActivated && isActivated) {
// reset flags & states before using a tool
this.latestPoints = [];
@ -150,6 +160,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
approxPolyAccuracy: defaultApproxPolyAccuracy,
});
if (this.activeTool) {
this.activeTool.switchBlockMode(toolsBlockerState.algorithmsLocked);
this.activeTool.reset();
}
}
@ -169,6 +180,10 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
});
}
}
if (prevProps.toolsBlockerState.algorithmsLocked !== toolsBlockerState.algorithmsLocked &&
!!this.activeTool?.switchBlockMode) {
this.activeTool.switchBlockMode(toolsBlockerState.algorithmsLocked);
}
}
public componentWillUnmount(): void {
@ -180,7 +195,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
private interactionListener = async (e: Event): Promise<void> => {
const { approxPolyAccuracy } = this.state;
const {
createAnnotations, isActivated, jobInstance, frame, labels, curZOrder, canvasInstance,
createAnnotations, isActivated, jobInstance, frame, labels, curZOrder, canvasInstance, toolsBlockerState,
} = this.props;
const { activeLabelID } = this.state;
if (!isActivated || !this.activeTool) {
@ -191,27 +206,41 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
shapesUpdated, isDone, threshold, shapes,
} = (e as CustomEvent).detail;
const pressedPoints = convertShapesForInteractor(shapes, 0).flat();
try {
if (shapesUpdated) {
this.latestPoints = await this.runCVAlgorithm(pressedPoints, threshold);
const approx = openCVWrapper.contours.approxPoly(
this.latestPoints,
thresholdFromAccuracy(approxPolyAccuracy),
false,
);
this.latestPoints = await this.runCVAlgorithm(pressedPoints,
toolsBlockerState.algorithmsLocked ? 0 : threshold);
let points = [];
if (toolsBlockerState.algorithmsLocked && this.latestPoints.length > 2) {
// disable approximation for lastest two points to disable fickering
const [x, y] = this.latestPoints.slice(-2);
this.latestPoints.splice(this.latestPoints.length - 2, 2);
points = openCVWrapper.contours.approxPoly(
this.latestPoints,
thresholdFromAccuracy(approxPolyAccuracy),
false,
);
points.push([x, y]);
} else {
points = openCVWrapper.contours.approxPoly(
this.latestPoints,
thresholdFromAccuracy(approxPolyAccuracy),
false,
);
}
canvasInstance.interact({
enabled: true,
intermediateShape: {
shapeType: ShapeType.POLYGON,
points: approx.flat(),
points: points.flat(),
},
});
}
if (isDone) {
// need to recalculate without the latest sliding point
const finalPoints = await this.runCVAlgorithm(pressedPoints, threshold);
const finalPoints = await this.runCVAlgorithm(pressedPoints,
toolsBlockerState.algorithmsLocked ? 0 : threshold);
const finalObject = new core.classes.ObjectState({
frame,
objectType: ObjectType.SHAPE,
@ -229,6 +258,22 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
notification.error({
description: error.toString(),
message: 'OpenCV.js processing error occured',
className: 'cvat-notification-notice-opencv-processing-error',
});
}
};
private onChangeToolsBlockerState = (event:string):void => {
const {
isActivated, toolsBlockerState, onSwitchToolsBlockerState, canvasInstance,
} = this.props;
if (isActivated && event === 'keyup') {
onSwitchToolsBlockerState({ algorithmsLocked: !toolsBlockerState.algorithmsLocked });
canvasInstance.interact({
enabled: true,
crosshair: toolsBlockerState.algorithmsLocked,
enableThreshold: toolsBlockerState.algorithmsLocked,
onChangeToolsBlockerState: this.onChangeToolsBlockerState,
});
}
};
@ -263,6 +308,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
notification.error({
description: error.toString(),
message: 'OpenCV.js processing error occured',
className: 'cvat-notification-notice-opencv-processing-error',
});
} finally {
this.disableCanvasForceUpdate();
@ -277,22 +323,24 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
if (!canvas) {
throw new Error('Element #cvat_canvas_background was not found');
}
if (!this.activeTool || pressedPoints.length === 0) return [];
const { width, height } = canvas;
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Canvas context is empty');
}
let imageData;
const [x, y] = pressedPoints.slice(-2);
const startX = Math.round(Math.max(0, x - threshold));
const startY = Math.round(Math.max(0, y - threshold));
const segmentWidth = Math.min(2 * threshold, width - startX);
const segmentHeight = Math.min(2 * threshold, height - startY);
const imageData = context.getImageData(startX, startY, segmentWidth, segmentHeight);
if (!this.activeTool) return [];
if (threshold !== 0) {
const segmentWidth = Math.min(2 * threshold, width - startX);
const segmentHeight = Math.min(2 * threshold, height - startY);
imageData = context.getImageData(startX, startY, segmentWidth, segmentHeight);
} else {
imageData = context.getImageData(0, 0, width, height);
}
// Handling via OpenCV.js
const points = await this.activeTool.run(pressedPoints, imageData, startX, startY);
return points;
@ -358,7 +406,8 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
<CVATTooltip title='Intelligent scissors' className='cvat-opencv-drawing-tool'>
<Button
onClick={() => {
this.activeTool = openCVWrapper.segmentation.intelligentScissorsFactory();
this.activeTool = openCVWrapper.segmentation
.intelligentScissorsFactory(this.onChangeToolsBlockerState);
canvasInstance.cancel();
onInteractionStart(this.activeTool, activeLabelID);
canvasInstance.interact({

@ -4,7 +4,7 @@
import React, { MutableRefObject } from 'react';
import { connect } from 'react-redux';
import Icon, { LoadingOutlined } from '@ant-design/icons';
import Icon, { LoadingOutlined, QuestionCircleOutlined } from '@ant-design/icons';
import Popover from 'antd/lib/popover';
import Select from 'antd/lib/select';
import Button from 'antd/lib/button';
@ -16,6 +16,8 @@ import notification from 'antd/lib/notification';
import message from 'antd/lib/message';
import Progress from 'antd/lib/progress';
import InputNumber from 'antd/lib/input-number';
import Dropdown from 'antd/lib/dropdown';
import lodash from 'lodash';
import { AIToolsIcon } from 'icons';
import { Canvas, convertShapesForInteractor } from 'cvat-canvas-wrapper';
@ -23,7 +25,7 @@ import range from 'utils/range';
import getCore from 'cvat-core-wrapper';
import openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper';
import {
CombinedState, ActiveControl, Model, ObjectType, ShapeType,
CombinedState, ActiveControl, Model, ObjectType, ShapeType, ToolsBlockerState,
} from 'reducers/interfaces';
import {
interactWithCanvas,
@ -36,7 +38,9 @@ import LabelSelector from 'components/label-selector/label-selector';
import ApproximationAccuracy, {
thresholdFromAccuracy,
} from 'components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy';
import { switchToolsBlockerState } from 'actions/settings-actions';
import withVisibilityHandling from './handle-popover-visibility';
import ToolsTooltips from './interactor-tooltips';
interface StateToProps {
canvasInstance: Canvas;
@ -52,6 +56,7 @@ interface StateToProps {
curZOrder: number;
aiToolsRef: MutableRefObject<any>;
defaultApproxPolyAccuracy: number;
toolsBlockerState: ToolsBlockerState;
}
interface DispatchToProps {
@ -59,6 +64,7 @@ interface DispatchToProps {
updateAnnotations(statesToUpdate: any[]): void;
createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void;
fetchAnnotations(): void;
onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState):void;
}
const core = getCore();
@ -72,6 +78,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
const { instance: canvasInstance, activeControl } = annotation.canvas;
const { models } = state;
const { interactors, detectors, trackers } = models;
const { toolsBlockerState } = state.settings.workspace;
return {
interactors,
@ -87,6 +94,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
curZOrder: annotation.annotations.zLayer.cur,
aiToolsRef: annotation.aiToolsRef,
defaultApproxPolyAccuracy: settings.workspace.defaultApproxPolyAccuracy,
toolsBlockerState,
};
}
@ -95,6 +103,7 @@ const mapDispatchToProps = {
updateAnnotations: updateAnnotationsAsync,
createAnnotations: createAnnotationsAsync,
fetchAnnotations: fetchAnnotationsAsync,
onSwitchToolsBlockerState: switchToolsBlockerState,
};
type Props = StateToProps & DispatchToProps;
@ -111,10 +120,21 @@ interface State {
}
export class ToolsControlComponent extends React.PureComponent<Props, State> {
private interactionIsAborted: boolean;
private interactionIsDone: boolean;
private latestResponseResult: number[][];
private latestResult: number[][];
private interaction: {
id: string | null;
isAborted: boolean;
latestResponse: number[][];
latestResult: number[][];
latestRequest: null | {
interactor: Model;
data: {
frame: number;
neg_points: number[][];
pos_points: number[][];
};
} | null;
hideMessage: (() => void) | null;
};
public constructor(props: Props) {
super(props);
@ -130,10 +150,14 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
mode: 'interaction',
};
this.latestResponseResult = [];
this.latestResult = [];
this.interactionIsAborted = false;
this.interactionIsDone = false;
this.interaction = {
id: null,
isAborted: false,
latestResponse: [],
latestResult: [],
latestRequest: null,
hideMessage: null,
};
}
public componentDidMount(): void {
@ -145,33 +169,44 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
public componentDidUpdate(prevProps: Props, prevState: State): void {
const { isActivated, defaultApproxPolyAccuracy, canvasInstance } = this.props;
const { approxPolyAccuracy, activeInteractor } = this.state;
const { approxPolyAccuracy, mode } = this.state;
if (prevProps.isActivated && !isActivated) {
window.removeEventListener('contextmenu', this.contextmenuDisabler);
// hide interaction message if exists
if (this.interaction.hideMessage) {
this.interaction.hideMessage();
this.interaction.hideMessage = null;
}
} else if (!prevProps.isActivated && isActivated) {
// reset flags when start interaction/tracking
this.interaction = {
id: null,
isAborted: false,
latestResponse: [],
latestResult: [],
latestRequest: null,
hideMessage: null,
};
this.setState({
approxPolyAccuracy: defaultApproxPolyAccuracy,
pointsRecieved: false,
});
this.latestResult = [];
this.latestResponseResult = [];
this.interactionIsDone = false;
this.interactionIsAborted = false;
window.addEventListener('contextmenu', this.contextmenuDisabler);
}
if (prevState.approxPolyAccuracy !== approxPolyAccuracy) {
if (isActivated && activeInteractor !== null && this.latestResponseResult.length) {
this.approximateResponsePoints(this.latestResponseResult).then((points: number[][]) => {
this.latestResult = points;
if (isActivated && mode === 'interaction' && this.interaction.latestResponse.length) {
this.approximateResponsePoints(this.interaction.latestResponse).then((points: number[][]) => {
this.interaction.latestResult = points;
canvasInstance.interact({
enabled: true,
intermediateShape: {
shapeType: ShapeType.POLYGON,
points: this.latestResult.flat(),
points: this.interaction.latestResult.flat(),
},
onChangeToolsBlockerState: this.onChangeToolsBlockerState,
});
});
}
@ -196,91 +231,68 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
};
private cancelListener = async (): Promise<void> => {
const { isActivated } = this.props;
const { fetching } = this.state;
this.latestResult = [];
if (isActivated) {
if (fetching && !this.interactionIsDone) {
// user pressed ESC
this.setState({ fetching: false });
this.interactionIsAborted = true;
}
if (fetching) {
// user pressed ESC
this.setState({ fetching: false });
this.interaction.isAborted = true;
}
};
private onInteraction = async (e: Event): Promise<void> => {
const {
frame,
labels,
curZOrder,
jobInstance,
isActivated,
activeLabelID,
canvasInstance,
createAnnotations,
} = this.props;
private runInteractionRequest = async (interactionId: string): Promise<void> => {
const { jobInstance, canvasInstance } = this.props;
const { activeInteractor, fetching } = this.state;
if (!isActivated) {
const { id, latestRequest } = this.interaction;
if (id !== interactionId || !latestRequest || fetching) {
// current interaction request is not relevant (new interaction session has started)
// or a user didn't add more points
// or one server request is on processing
return;
}
try {
this.interactionIsDone = (e as CustomEvent).detail.isDone;
const interactor = activeInteractor as Model;
const { interactor, data } = latestRequest;
this.interaction.latestRequest = null;
if ((e as CustomEvent).detail.shapesUpdated) {
try {
this.interaction.hideMessage = message.loading(`Waiting a response from ${activeInteractor?.name}..`, 0);
try {
// run server request
this.setState({ fetching: true });
try {
this.latestResponseResult = await core.lambda.call(jobInstance.task, interactor, {
frame,
pos_points: convertShapesForInteractor((e as CustomEvent).detail.shapes, 0),
neg_points: convertShapesForInteractor((e as CustomEvent).detail.shapes, 2),
});
const response = await core.lambda.call(jobInstance.task, interactor, data);
// approximation with cv.approxPolyDP
const approximated = await this.approximateResponsePoints(response);
this.latestResult = this.latestResponseResult;
if (this.interaction.id !== interactionId || this.interaction.isAborted) {
// new interaction session or the session is aborted
return;
}
if (this.interactionIsAborted) {
// while the server request
// user has cancelled interaction (for example pressed ESC)
// need to clean variables that have been just set
this.latestResult = [];
this.latestResponseResult = [];
return;
}
this.interaction.latestResponse = response;
this.interaction.latestResult = approximated;
this.latestResult = await this.approximateResponsePoints(this.latestResponseResult);
} finally {
this.setState({ fetching: false, pointsRecieved: !!this.latestResult.length });
this.setState({ pointsRecieved: !!response.length });
} finally {
if (this.interaction.id === interactionId && this.interaction.hideMessage) {
this.interaction.hideMessage();
this.interaction.hideMessage = null;
}
}
if (!this.latestResult.length) {
return;
this.setState({ fetching: false });
}
if (this.interactionIsDone && !fetching) {
const object = new core.classes.ObjectState({
frame,
objectType: ObjectType.SHAPE,
label: labels.length ? labels.filter((label: any) => label.id === activeLabelID)[0] : null,
shapeType: ShapeType.POLYGON,
points: this.latestResult.flat(),
occluded: false,
zOrder: curZOrder,
});
createAnnotations(jobInstance, frame, [object]);
} else {
if (this.interaction.latestResult.length) {
canvasInstance.interact({
enabled: true,
intermediateShape: {
shapeType: ShapeType.POLYGON,
points: this.latestResult.flat(),
points: this.interaction.latestResult.flat(),
},
onChangeToolsBlockerState: this.onChangeToolsBlockerState,
});
}
setTimeout(() => this.runInteractionRequest(interactionId));
} catch (err) {
notification.error({
description: err.toString(),
@ -289,6 +301,43 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
}
};
private onInteraction = (e: Event): void => {
const { frame, isActivated } = this.props;
const { activeInteractor } = this.state;
if (!isActivated) {
return;
}
if (!this.interaction.id) {
this.interaction.id = lodash.uniqueId('interaction_');
}
const { shapesUpdated, isDone, shapes } = (e as CustomEvent).detail;
if (isDone) {
// make an object from current result
// do not make one more request
// prevent future requests if possible
this.interaction.isAborted = true;
this.interaction.latestRequest = null;
if (this.interaction.latestResult.length) {
this.constructFromPoints(this.interaction.latestResult);
}
} else if (shapesUpdated) {
const interactor = activeInteractor as Model;
this.interaction.latestRequest = {
interactor,
data: {
frame,
pos_points: convertShapesForInteractor(shapes, 0),
neg_points: convertShapesForInteractor(shapes, 2),
},
};
this.runInteractionRequest(this.interaction.id);
}
};
private onTracking = async (e: Event): Promise<void> => {
const {
isActivated, jobInstance, frame, curZOrder, fetchAnnotations,
@ -306,7 +355,6 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
return;
}
this.interactionIsDone = true;
try {
const { points } = (e as CustomEvent).detail.shapes[0];
const state = new core.classes.ObjectState({
@ -363,11 +411,38 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
});
};
private onChangeToolsBlockerState = (event:string):void => {
const { isActivated, onSwitchToolsBlockerState } = this.props;
if (isActivated && event === 'keydown') {
onSwitchToolsBlockerState({ algorithmsLocked: true });
} else if (isActivated && event === 'keyup') {
onSwitchToolsBlockerState({ algorithmsLocked: false });
}
};
private constructFromPoints(points: number[][]): void {
const {
frame, labels, curZOrder, jobInstance, activeLabelID, createAnnotations,
} = this.props;
const object = new core.classes.ObjectState({
frame,
objectType: ObjectType.SHAPE,
label: labels.length ? labels.filter((label: any) => label.id === activeLabelID)[0] : null,
shapeType: ShapeType.POLYGON,
points: points.flat(),
occluded: false,
zOrder: curZOrder,
});
createAnnotations(jobInstance, frame, [object]);
}
private async approximateResponsePoints(points: number[][]): Promise<number[][]> {
const { approxPolyAccuracy } = this.state;
if (points.length > 3) {
if (!openCVWrapper.isInitialized) {
const hide = message.loading('OpenCV.js initialization..');
const hide = message.loading('OpenCV.js initialization..', 0);
try {
await openCVWrapper.initialize(() => {});
} finally {
@ -553,6 +628,8 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
});
onInteractionStart(activeTracker, activeLabelID);
const { onSwitchToolsBlockerState } = this.props;
onSwitchToolsBlockerState({ buttonVisible: false });
}
}}
>
@ -580,6 +657,8 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
);
}
const minNegVertices = activeInteractor ? (activeInteractor.params.canvas.minNegVertices as number) : -1;
return (
<>
<Row justify='start'>
@ -587,8 +666,8 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
<Text className='cvat-text-color'>Interactor</Text>
</Col>
</Row>
<Row align='middle' justify='center'>
<Col span={24}>
<Row align='middle' justify='space-between'>
<Col span={22}>
<Select
style={{ width: '100%' }}
defaultValue={interactors[0].name}
@ -607,6 +686,19 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
)}
</Select>
</Col>
<Col span={2} className='cvat-interactors-tips-icon-container'>
<Dropdown
overlay={(
<ToolsTooltips
name={activeInteractor?.name}
withNegativePoints={minNegVertices >= 0}
{...(activeInteractor?.tip || {})}
/>
)}
>
<QuestionCircleOutlined />
</Dropdown>
</Col>
</Row>
<Row align='middle' justify='end'>
<Col>
@ -620,12 +712,12 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
if (activeInteractor) {
canvasInstance.cancel();
activeInteractor.onChangeToolsBlockerState = this.onChangeToolsBlockerState;
canvasInstance.interact({
shapeType: 'points',
enabled: true,
...activeInteractor.params.canvas,
});
onInteractionStart(activeInteractor, activeLabelID);
}
}}
@ -680,6 +772,8 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
);
createAnnotations(jobInstance, frame, states);
const { onSwitchToolsBlockerState } = this.props;
onSwitchToolsBlockerState({ buttonVisible: false });
} catch (error) {
notification.error({
description: error.toString(),
@ -703,7 +797,10 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
</Text>
</Col>
</Row>
<Tabs type='card' tabBarGutter={8}>
<Tabs
type='card'
tabBarGutter={8}
>
<Tabs.TabPane key='interactors' tab='Interactors'>
{this.renderLabelBlock()}
{this.renderInteractorBlock()}
@ -725,7 +822,7 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
interactors, detectors, trackers, isActivated, canvasInstance, labels,
} = this.props;
const {
fetching, trackingProgress, approxPolyAccuracy, activeInteractor, pointsRecieved,
fetching, trackingProgress, approxPolyAccuracy, pointsRecieved, mode,
} = this.state;
if (![...interactors, ...detectors, ...trackers].length) return null;
@ -749,35 +846,51 @@ export class ToolsControlComponent extends React.PureComponent<Props, State> {
className: 'cvat-tools-control',
};
return !labels.length ? (
<Icon className=' cvat-tools-control cvat-disabled-canvas-control' component={AIToolsIcon} />
) : (
const showAnyContent = !!labels.length;
const showInteractionContent = isActivated && mode === 'interaction' && pointsRecieved;
const showDetectionContent = fetching && mode === 'detection';
const showTrackingContent = fetching && mode === 'tracking' && trackingProgress !== null;
const formattedTrackingProgress = showTrackingContent ? +((trackingProgress as number) * 100).toFixed(0) : null;
const interactionContent: JSX.Element | null = showInteractionContent ? (
<>
<ApproximationAccuracy
approxPolyAccuracy={approxPolyAccuracy}
onChange={(value: number) => {
this.setState({ approxPolyAccuracy: value });
}}
/>
</>
) : null;
const trackOrDetectModal: JSX.Element | null =
showDetectionContent || showTrackingContent ? (
<Modal
title='Making a server request'
zIndex={Number.MAX_SAFE_INTEGER}
visible={fetching}
visible
destroyOnClose
closable={false}
footer={[]}
>
<Text>Waiting for a server response..</Text>
<LoadingOutlined style={{ marginLeft: '10px' }} />
{trackingProgress !== null && (
<Progress percent={+(trackingProgress * 100).toFixed(0)} status='active' />
)}
{showTrackingContent ? (
<Progress percent={formattedTrackingProgress as number} status='active' />
) : null}
</Modal>
{isActivated && activeInteractor !== null && pointsRecieved ? (
<ApproximationAccuracy
approxPolyAccuracy={approxPolyAccuracy}
onChange={(value: number) => {
this.setState({ approxPolyAccuracy: value });
}}
/>
) : null}
) : null;
return showAnyContent ? (
<>
<CustomPopover {...dynamcPopoverPros} placement='right' content={this.renderPopoverContent()}>
<Icon {...dynamicIconProps} component={AIToolsIcon} />
</CustomPopover>
{interactionContent}
{trackOrDetectModal}
</>
) : (
<Icon className=' cvat-tools-control cvat-disabled-canvas-control' component={AIToolsIcon} />
);
}
}

@ -60,11 +60,7 @@ function mapDispatchToProps(dispatch: Dispatch<AnyAction>): DispatchToProps {
function ObjectsSideBar(props: StateToProps & DispatchToProps & OwnProps): JSX.Element {
const {
sidebarCollapsed,
canvasInstance,
collapseSidebar,
objectsList,
jobInstance,
sidebarCollapsed, canvasInstance, collapseSidebar, objectsList, jobInstance,
} = props;
const collapse = (): void => {
@ -119,13 +115,11 @@ function ObjectsSideBar(props: StateToProps & DispatchToProps & OwnProps): JSX.E
<LabelsList />
</Tabs.TabPane>
{is2D ?
(
<Tabs.TabPane tab={<Text strong>Issues</Text>} key='issues'>
<IssuesListComponent />
</Tabs.TabPane>
) : null}
{is2D ? (
<Tabs.TabPane tab={<Text strong>Issues</Text>} key='issues'>
<IssuesListComponent />
</Tabs.TabPane>
) : null}
</Tabs>
{!sidebarCollapsed && <AppearanceBlock />}

@ -192,6 +192,24 @@
margin-top: 10px;
}
.cvat-interactors-tips-icon-container {
text-align: center;
font-size: 20px;
}
.cvat-interactor-tip-container {
background: $background-color-2;
padding: $grid-unit-size;
box-shadow: $box-shadow-base;
width: $grid-unit-size * 40;
text-align: center;
border-radius: 4px;
}
.cvat-interactor-tip-image {
width: $grid-unit-size * 37;
}
.cvat-draw-shape-popover-points-selector {
width: 100%;
}

@ -128,6 +128,10 @@ button.cvat-predictor-button {
pointer-events: none;
}
.cvat-button-active.ant-btn {
color: $border-color-hover;
}
.cvat-annotation-header-player-group > div {
height: 48px;
line-height: 0;

@ -8,9 +8,8 @@ import Modal from 'antd/lib/modal';
// eslint-disable-next-line import/no-extraneous-dependencies
import { MenuInfo } from 'rc-menu/lib/interface';
import DumpSubmenu from 'components/actions-menu/dump-submenu';
import ExportDatasetModal from 'components/export-dataset/export-dataset-modal';
import LoadSubmenu from 'components/actions-menu/load-submenu';
import ExportSubmenu from 'components/actions-menu/export-submenu';
import { DimensionType } from '../../../reducers/interfaces';
interface Props {
@ -18,8 +17,6 @@ interface Props {
loaders: any[];
dumpers: any[];
loadActivity: string | null;
dumpActivities: string[] | null;
exportActivities: string[] | null;
isReviewer: boolean;
jobInstance: any;
onClickMenu(params: MenuInfo, file?: File): void;
@ -28,7 +25,6 @@ interface Props {
}
export enum Actions {
DUMP_TASK_ANNO = 'dump_task_anno',
LOAD_JOB_ANNO = 'load_job_anno',
EXPORT_TASK_DATASET = 'export_task_dataset',
REMOVE_ANNO = 'remove_anno',
@ -41,12 +37,8 @@ export enum Actions {
export default function AnnotationMenuComponent(props: Props): JSX.Element {
const {
taskMode,
loaders,
dumpers,
loadActivity,
dumpActivities,
exportActivities,
isReviewer,
jobInstance,
onClickMenu,
@ -163,13 +155,6 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element {
return (
<Menu onClick={onClickMenuWrapper} className='cvat-annotation-menu' selectable={false}>
{DumpSubmenu({
taskMode,
dumpers,
dumpActivities,
menuKey: Actions.DUMP_TASK_ANNO,
taskDimension: jobInstance.task.dimension,
})}
{LoadSubmenu({
loaders,
loadActivity,
@ -179,13 +164,7 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element {
menuKey: Actions.LOAD_JOB_ANNO,
taskDimension: jobInstance.task.dimension,
})}
{ExportSubmenu({
exporters: dumpers,
exportActivities,
menuKey: Actions.EXPORT_TASK_DATASET,
taskDimension: jobInstance.task.dimension,
})}
<Menu.Item key={Actions.EXPORT_TASK_DATASET}>Export task dataset</Menu.Item>
<Menu.Item key={Actions.REMOVE_ANNO}>Remove annotations</Menu.Item>
<Menu.Item key={Actions.OPEN_TASK}>
<a href={`/tasks/${taskID}`} onClick={(e: React.MouseEvent) => e.preventDefault()}>
@ -198,6 +177,7 @@ export default function AnnotationMenuComponent(props: Props): JSX.Element {
<Menu.Item key={Actions.SUBMIT_REVIEW}>Submit the review</Menu.Item>
)}
{jobStatus === 'completed' && <Menu.Item key={Actions.RENEW_JOB}>Renew the job</Menu.Item>}
<ExportDatasetModal />
</Menu>
);
}

@ -4,7 +4,7 @@
import React from 'react';
import { Col } from 'antd/lib/grid';
import Icon, { CheckOutlined } from '@ant-design/icons';
import Icon, { StopOutlined, CheckOutlined } from '@ant-design/icons';
import Modal from 'antd/lib/modal';
import Button from 'antd/lib/button';
import Timeline from 'antd/lib/timeline';
@ -14,7 +14,7 @@ import AnnotationMenuContainer from 'containers/annotation-page/top-bar/annotati
import {
MainMenuIcon, SaveIcon, UndoIcon, RedoIcon,
} from 'icons';
import { ActiveControl } from 'reducers/interfaces';
import { ActiveControl, ToolsBlockerState } from 'reducers/interfaces';
import CVATTooltip from 'components/common/cvat-tooltip';
interface Props {
@ -26,11 +26,14 @@ interface Props {
undoShortcut: string;
redoShortcut: string;
drawShortcut: string;
switchToolsBlockerShortcut: string;
toolsBlockerState: ToolsBlockerState;
activeControl: ActiveControl;
onSaveAnnotation(): void;
onUndoClick(): void;
onRedoClick(): void;
onFinishDraw(): void;
onSwitchToolsBlockerState(): void;
}
function LeftGroup(props: Props): JSX.Element {
@ -43,11 +46,14 @@ function LeftGroup(props: Props): JSX.Element {
undoShortcut,
redoShortcut,
drawShortcut,
switchToolsBlockerShortcut,
activeControl,
toolsBlockerState,
onSaveAnnotation,
onUndoClick,
onRedoClick,
onFinishDraw,
onSwitchToolsBlockerState,
} = props;
const includesDoneButton = [
@ -58,6 +64,15 @@ function LeftGroup(props: Props): JSX.Element {
ActiveControl.OPENCV_TOOLS,
].includes(activeControl);
const includesToolsBlockerButton = [
ActiveControl.OPENCV_TOOLS,
ActiveControl.AI_TOOLS,
].includes(activeControl) && toolsBlockerState.buttonVisible;
const shouldEnableToolsBlockerOnClick = [
ActiveControl.OPENCV_TOOLS,
].includes(activeControl);
return (
<Col className='cvat-annotation-header-left-group'>
<Dropdown overlay={<AnnotationMenuContainer />}>
@ -113,6 +128,18 @@ function LeftGroup(props: Props): JSX.Element {
</Button>
</CVATTooltip>
) : null}
{includesToolsBlockerButton ? (
<CVATTooltip overlay={`Press "${switchToolsBlockerShortcut}" to postpone running the algorithm `}>
<Button
type='link'
className={`cvat-annotation-header-button ${toolsBlockerState.algorithmsLocked ? 'cvat-button-active' : ''}`}
onClick={shouldEnableToolsBlockerOnClick ? onSwitchToolsBlockerState : undefined}
>
<StopOutlined />
Block
</Button>
</CVATTooltip>
) : null}
</Col>
);
}

@ -6,7 +6,9 @@ import React from 'react';
import Input from 'antd/lib/input';
import { Col, Row } from 'antd/lib/grid';
import { ActiveControl, PredictorState, Workspace } from 'reducers/interfaces';
import {
ActiveControl, PredictorState, ToolsBlockerState, Workspace,
} from 'reducers/interfaces';
import LeftGroup from './left-group';
import PlayerButtons from './player-buttons';
import PlayerNavigation from './player-navigation';
@ -28,6 +30,7 @@ interface Props {
undoShortcut: string;
redoShortcut: string;
drawShortcut: string;
switchToolsBlockerShortcut: string;
playPauseShortcut: string;
nextFrameShortcut: string;
previousFrameShortcut: string;
@ -39,6 +42,7 @@ interface Props {
predictor: PredictorState;
isTrainingActive: boolean;
activeControl: ActiveControl;
toolsBlockerState: ToolsBlockerState;
changeWorkspace(workspace: Workspace): void;
switchPredictor(predictorEnabled: boolean): void;
showStatistics(): void;
@ -59,6 +63,7 @@ interface Props {
onUndoClick(): void;
onRedoClick(): void;
onFinishDraw(): void;
onSwitchToolsBlockerState(): void;
jobInstance: any;
}
@ -79,6 +84,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
undoShortcut,
redoShortcut,
drawShortcut,
switchToolsBlockerShortcut,
playPauseShortcut,
nextFrameShortcut,
previousFrameShortcut,
@ -89,6 +95,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
predictor,
focusFrameInputShortcut,
activeControl,
toolsBlockerState,
showStatistics,
switchPredictor,
showFilters,
@ -109,6 +116,7 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
onUndoClick,
onRedoClick,
onFinishDraw,
onSwitchToolsBlockerState,
jobInstance,
isTrainingActive,
} = props;
@ -125,10 +133,13 @@ export default function AnnotationTopBarComponent(props: Props): JSX.Element {
redoShortcut={redoShortcut}
activeControl={activeControl}
drawShortcut={drawShortcut}
switchToolsBlockerShortcut={switchToolsBlockerShortcut}
toolsBlockerState={toolsBlockerState}
onSaveAnnotation={onSaveAnnotation}
onUndoClick={onUndoClick}
onRedoClick={onRedoClick}
onFinishDraw={onFinishDraw}
onSwitchToolsBlockerState={onSwitchToolsBlockerState}
/>
<Col className='cvat-annotation-header-player-group'>
<Row align='middle'>

@ -33,7 +33,11 @@ import getCore from 'cvat-core-wrapper';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import { NotificationsState } from 'reducers/interfaces';
import { customWaViewHit } from 'utils/enviroment';
import showPlatformNotification, { platformInfo, stopNotifications } from 'utils/platform-checker';
import showPlatformNotification, {
platformInfo,
stopNotifications,
showUnsupportedNotification,
} from 'utils/platform-checker';
import '../styles.scss';
interface CVATAppProps {
@ -87,6 +91,50 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
});
verifyAuthorized();
const {
name, version, engine, os,
} = platformInfo();
if (showPlatformNotification()) {
stopNotifications(false);
Modal.warning({
title: 'Unsupported platform detected',
className: 'cvat-modal-unsupported-platform-warning',
content: (
<>
<Row>
<Col>
<Text>
{`The browser you are using is ${name} ${version} based on ${engine}.` +
' CVAT was tested in the latest versions of Chrome and Firefox.' +
' We recommend to use Chrome (or another Chromium based browser)'}
</Text>
</Col>
</Row>
<Row>
<Col>
<Text type='secondary'>{`The operating system is ${os}`}</Text>
</Col>
</Row>
</>
),
onOk: () => stopNotifications(true),
});
} else if (showUnsupportedNotification()) {
stopNotifications(false);
Modal.warning({
title: 'Unsupported features detected',
className: 'cvat-modal-unsupported-features-warning',
content: (
<Text>
{`${name} v${version} does not support API, which is used by CVAT. `}
It is strongly recommended to update your browser.
</Text>
),
onOk: () => stopNotifications(true),
});
}
}
public componentDidUpdate(): void {
@ -269,37 +317,6 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
},
};
if (showPlatformNotification()) {
stopNotifications(false);
const {
name, version, engine, os,
} = platformInfo();
Modal.warning({
title: 'Unsupported platform detected',
className: 'cvat-modal-unsupported-platform-warning',
content: (
<>
<Row>
<Col>
<Text>
{`The browser you are using is ${name} ${version} based on ${engine}.` +
' CVAT was tested in the latest versions of Chrome and Firefox.' +
' We recommend to use Chrome (or another Chromium based browser)'}
</Text>
</Col>
</Row>
<Row>
<Col>
<Text type='secondary'>{`The operating system is ${os}`}</Text>
</Col>
</Row>
</>
),
onOk: () => stopNotifications(true),
});
}
if (readyForRender) {
if (user && user.isVerified) {
return (

@ -0,0 +1,150 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import './styles.scss';
import React, { useState, useEffect, useCallback } from 'react';
import Modal from 'antd/lib/modal';
import Notification from 'antd/lib/notification';
import { useSelector, useDispatch } from 'react-redux';
import { DownloadOutlined, LoadingOutlined } from '@ant-design/icons';
import Text from 'antd/lib/typography/Text';
import Select from 'antd/lib/select';
import Checkbox from 'antd/lib/checkbox';
import Input from 'antd/lib/input';
import Form from 'antd/lib/form';
import { CombinedState } from 'reducers/interfaces';
import { exportActions, exportDatasetAsync } from 'actions/export-actions';
import getCore from 'cvat-core-wrapper';
const core = getCore();
type FormValues = {
selectedFormat: string | undefined;
saveImages: boolean;
customName: string | undefined;
};
function ExportDatasetModal(): JSX.Element {
const [instanceType, setInstanceType] = useState('');
const [activities, setActivities] = useState<string[]>([]);
const [form] = Form.useForm();
const dispatch = useDispatch();
const instance = useSelector((state: CombinedState) => state.export.instance);
const modalVisible = useSelector((state: CombinedState) => state.export.modalVisible);
const dumpers = useSelector((state: CombinedState) => state.formats.annotationFormats.dumpers);
const { tasks: taskExportActivities, projects: projectExportActivities } = useSelector(
(state: CombinedState) => state.export,
);
const initActivities = (): void => {
if (instance instanceof core.classes.Project) {
setInstanceType('project');
setActivities(projectExportActivities[instance.id] || []);
} else if (instance instanceof core.classes.Task) {
setInstanceType('task');
setActivities(taskExportActivities[instance.id] || []);
if (instance.mode === 'interpolation' && instance.dimension === '2d') {
form.setFieldsValue({ selectedFormat: 'CVAT for video 1.1' });
} else if (instance.mode === 'annotation' && instance.dimension === '2d') {
form.setFieldsValue({ selectedFormat: 'CVAT for images 1.1' });
}
}
};
useEffect(() => {
initActivities();
}, [instance?.id, instance instanceof core.classes.Project]);
const closeModal = (): void => {
form.resetFields();
dispatch(exportActions.closeExportModal());
};
const handleExport = useCallback(
(values: FormValues): void => {
// have to validate format before so it would not be undefined
dispatch(
exportDatasetAsync(
instance,
values.selectedFormat as string,
values.customName ? `${values.customName}.zip` : '',
values.saveImages,
),
);
closeModal();
Notification.info({
message: 'Dataset export started',
description:
`Dataset export was started for ${instanceType} #${instance?.id}. ` +
'Download will start automaticly as soon as the dataset is ready.',
className: `cvat-notification-notice-export-${instanceType}-start`,
});
},
[instance?.id, instance instanceof core.classes.Project, instanceType],
);
return (
<Modal
title={`Export ${instanceType} #${instance?.id} as a dataset`}
visible={modalVisible}
onCancel={closeModal}
onOk={() => form.submit()}
className={`cvat-modal-export-${instanceType}`}
>
<Form
name='Export dataset'
form={form}
labelCol={{ span: 8 }}
wrapperCol={{ span: 16 }}
initialValues={
{
selectedFormat: undefined,
saveImages: false,
customName: undefined,
} as FormValues
}
onFinish={handleExport}
>
<Form.Item
name='selectedFormat'
label='Export format'
rules={[{ required: true, message: 'Format must be selected' }]}
>
<Select placeholder='Select dataset format' className='cvat-modal-export-select'>
{dumpers
.sort((a: any, b: any) => a.name.localeCompare(b.name))
.filter((dumper: any): boolean => dumper.dimension === instance?.dimension)
.map(
(dumper: any): JSX.Element => {
const pending = (activities || []).includes(dumper.name);
const disabled = !dumper.enabled || pending;
return (
<Select.Option
value={dumper.name}
key={dumper.name}
disabled={disabled}
className='cvat-modal-export-option-item'
>
<DownloadOutlined />
<Text disabled={disabled}>{dumper.name}</Text>
{pending && <LoadingOutlined style={{ marginLeft: 10 }} />}
</Select.Option>
);
},
)}
</Select>
</Form.Item>
<Form.Item name='saveImages' valuePropName='checked' wrapperCol={{ offset: 8, span: 16 }}>
<Checkbox>Save images</Checkbox>
</Form.Item>
<Form.Item label='Custom name' name='customName'>
<Input placeholder='Custom name for a dataset' suffix='.zip' className='cvat-modal-export-filename-input' />
</Form.Item>
</Form>
</Modal>
);
}
export default React.memo(ExportDatasetModal);

@ -0,0 +1,13 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@import '../../base.scss';
.cvat-modal-export-option-item > .ant-select-item-option-content,
.cvat-modal-export-select .ant-select-selection-item {
> span[role='img'] {
color: $info-icon-color;
margin-right: $grid-unit-size;
}
}

@ -1,49 +0,0 @@
// Copyright (C) 2020 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React, { useState, useEffect } from 'react';
import Drawer from 'antd/lib/drawer';
import Paragraph from 'antd/lib/typography/Paragraph';
import Button from 'antd/lib/button/button';
import { isPublic } from 'utils/enviroment';
function CookieDrawer(): JSX.Element {
const [drawerVisible, setDrawerVisible] = useState(false);
useEffect(() => {
const cookiePolicyAccepted = localStorage.getItem('cookiePolicyAccepted');
if (cookiePolicyAccepted === null && isPublic()) {
setDrawerVisible(true);
}
}, []);
const onClose = (): void => {
localStorage.setItem('cookiePolicyAccepted', 'true');
setDrawerVisible(false);
};
return (
<Drawer
title='About Cookies on this site:'
placement='bottom'
closable={false}
visible={drawerVisible}
height={200}
destroyOnClose
>
<Paragraph>
This site uses cookies for functionality, analytics, and advertising purposes as described in our Cookie
and Similar Technologies Notice. To see what cookies we serve and set your preferences, please visit our
<a href='https://www.intel.com/cookies'> Cookie Consent Tool</a>. By continuing to use our website, you
agree to our use of cookies.
</Paragraph>
<Button onClick={onClose} size='large' type='primary'>
Accept
</Button>
</Drawer>
);
}
export default CookieDrawer;

@ -0,0 +1,27 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import React from 'react';
import { Layout } from 'antd';
import { isPublic } from 'utils/enviroment';
import consts from 'consts';
function FooterDrawer(): JSX.Element | null {
const { Footer } = Layout;
const { INTEL_TERMS_OF_USE_URL, INTEL_COOKIES_URL, INTEL_PRIVACY_URL } = consts;
return isPublic() ? (
<Footer style={{ textAlign: 'center', borderTop: '1px solid #e8e8e8' }}>
© Intel Corporation |
<a target='_blank' rel='noopener noreferrer' href={INTEL_TERMS_OF_USE_URL}> Terms of Use </a>
|
<a target='_blank' rel='noopener noreferrer' data-cookie-notice='true' href={INTEL_COOKIES_URL}> Cookies </a>
|
<a target='_blank' rel='noopener noreferrer' href={INTEL_PRIVACY_URL}> Privacy </a>
</Footer>
) : null;
}
export default React.memo(FooterDrawer);

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -8,9 +8,11 @@ import { Link, withRouter } from 'react-router-dom';
import Title from 'antd/lib/typography/Title';
import Text from 'antd/lib/typography/Text';
import { Row, Col } from 'antd/lib/grid';
import { Layout } from 'antd';
import FooterDrawer from 'components/login-page/intel-footer-drawer';
import LoginForm, { LoginData } from './login-form';
import CookieDrawer from './cookie-policy-drawer';
interface LoginPageComponentProps {
fetching: boolean;
@ -27,40 +29,44 @@ function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps
xl: { span: 4 },
};
const { Content } = Layout;
const { fetching, onLogin, renderResetPassword } = props;
return (
<>
<Row justify='center' align='middle'>
<Col {...sizes}>
<Title level={2}> Login </Title>
<LoginForm
fetching={fetching}
onSubmit={(loginData: LoginData): void => {
onLogin(loginData.username, loginData.password);
}}
/>
<Row justify='start' align='top'>
<Col>
<Text strong>
New to CVAT? Create
<Link to='/auth/register'> an account</Link>
</Text>
</Col>
</Row>
{renderResetPassword && (
<Layout>
<Content>
<Row justify='center' align='middle' style={{ height: '100%' }}>
<Col {...sizes}>
<Title level={2}> Login </Title>
<LoginForm
fetching={fetching}
onSubmit={(loginData: LoginData): void => {
onLogin(loginData.username, loginData.password);
}}
/>
<Row justify='start' align='top'>
<Col>
<Text strong>
<Link to='/auth/password/reset'>Forgot your password?</Link>
New to CVAT? Create
<Link to='/auth/register'> an account</Link>
</Text>
</Col>
</Row>
)}
</Col>
</Row>
<CookieDrawer />
</>
{renderResetPassword && (
<Row justify='start' align='top'>
<Col>
<Text strong>
<Link to='/auth/password/reset'>Forgot your password?</Link>
</Text>
</Col>
</Row>
)}
</Col>
</Row>
</Content>
<FooterDrawer />
</Layout>
);
}

@ -5,7 +5,8 @@
@import '../../base.scss';
.cvat-models-page {
padding-top: 30px;
padding-top: $grid-unit-size * 2;
padding-bottom: $grid-unit-size;
height: 90%;
overflow: auto;
position: fixed;

@ -34,8 +34,8 @@ export default function MoveTaskModal(): JSX.Element {
const [labelMap, setLabelMap] = useState<{ [key: string]: LabelMapperItemValue }>({});
const initValues = (): void => {
const labelValues: { [key: string]: LabelMapperItemValue } = {};
if (task) {
const labelValues: { [key: string]: LabelMapperItemValue } = {};
task.labels.forEach((label: any) => {
labelValues[label.id] = {
labelId: label.id,
@ -43,8 +43,8 @@ export default function MoveTaskModal(): JSX.Element {
clearAttributes: true,
};
});
setLabelMap(labelValues);
}
setLabelMap(labelValues);
};
const onCancel = (): void => {

@ -16,7 +16,10 @@ import { PlusOutlined } from '@ant-design/icons';
import { CombinedState, Task } from 'reducers/interfaces';
import { getProjectsAsync } from 'actions/projects-actions';
import { cancelInferenceAsync } from 'actions/models-actions';
import ExportDatasetModal from 'components/export-dataset/export-dataset-modal';
import TaskItem from 'components/tasks-page/task-item';
import MoveTaskModal from 'components/move-task-modal/move-task-modal';
import ModelRunnerDialog from 'components/model-runner-modal/model-runner-dialog';
import DetailsComponent from './details';
import ProjectTopBar from './top-bar';
@ -109,6 +112,9 @@ export default function ProjectPageComponent(): JSX.Element {
</React.Fragment>
))}
</Col>
<ExportDatasetModal />
<MoveTaskModal />
<ModelRunnerDialog />
</Row>
);
}

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -8,6 +8,7 @@ import Modal from 'antd/lib/modal';
import Menu from 'antd/lib/menu';
import { deleteProjectAsync } from 'actions/projects-actions';
import { exportActions } from 'actions/export-actions';
interface Props {
projectInstance: any;
@ -36,6 +37,12 @@ export default function ProjectActionsMenuComponent(props: Props): JSX.Element {
return (
<Menu className='cvat-project-actions-menu'>
<Menu.Item
onClick={() => dispatch(exportActions.openExportModal(projectInstance))}
>
Export project dataset
</Menu.Item>
<hr />
<Menu.Item onClick={onDeleteProject}>Delete</Menu.Item>
</Menu>
);

@ -15,18 +15,27 @@ import Button from 'antd/lib/button';
import { MoreOutlined } from '@ant-design/icons';
import { CombinedState, Project } from 'reducers/interfaces';
import { useCardHeightHOC } from 'utils/hooks';
import ProjectActionsMenuComponent from './actions-menu';
interface Props {
projectInstance: Project;
}
const useCardHeight = useCardHeightHOC({
containerClassName: 'cvat-projects-page',
siblingClassNames: ['cvat-projects-pagination', 'cvat-projects-top-bar'],
paddings: 40,
numberOfRows: 3,
});
export default function ProjectItemComponent(props: Props): JSX.Element {
const {
projectInstance: { instance, preview },
} = props;
const history = useHistory();
const height = useCardHeight();
const ownerName = instance.owner ? instance.owner.username : null;
const updated = moment(instance.updatedDate).fromNow();
const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes);
@ -36,8 +45,7 @@ export default function ProjectItemComponent(props: Props): JSX.Element {
history.push(`/projects/${instance.id}`);
};
const style: React.CSSProperties = {};
const style: React.CSSProperties = { height };
if (deleted) {
style.pointerEvents = 'none';
style.opacity = 0.5;

@ -15,7 +15,7 @@ export default function ProjectListComponent(): JSX.Element {
const dispatch = useDispatch();
const projectsCount = useSelector((state: CombinedState) => state.projects.count);
const { page } = useSelector((state: CombinedState) => state.projects.gettingQuery);
const projectInstances = useSelector((state: CombinedState) => state.projects.current);
const projects = useSelector((state: CombinedState) => state.projects.current);
const gettingQuery = useSelector((state: CombinedState) => state.projects.gettingQuery);
function changePage(p: number): void {
@ -27,28 +27,13 @@ export default function ProjectListComponent(): JSX.Element {
);
}
const projects = projectInstances.reduce<Project[][]>((rows, key, index) => {
if (index % 4 === 0) {
rows.push([key]);
} else {
rows[rows.length - 1].push(key);
}
return rows;
}, []);
return (
<>
<Row justify='center' align='middle' className='cvat-project-list-content'>
<Col className='cvat-projects-list' md={22} lg={18} xl={16} xxl={14}>
{projects.map(
(row: Project[]): JSX.Element => (
<Row key={row[0].instance.id} gutter={[8, 8]}>
{row.map((project: Project) => (
<Col span={6} key={project.instance.id}>
<ProjectItem projectInstance={project} />
</Col>
))}
</Row>
(project: Project): JSX.Element => (
<ProjectItem key={project.instance.id} projectInstance={project} />
),
)}
</Col>

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -8,9 +8,10 @@ import { useDispatch, useSelector } from 'react-redux';
import { useLocation, useHistory } from 'react-router';
import Spin from 'antd/lib/spin';
import FeedbackComponent from 'components/feedback/feedback';
import { CombinedState, ProjectsQuery } from 'reducers/interfaces';
import { getProjectsAsync } from 'actions/projects-actions';
import FeedbackComponent from 'components/feedback/feedback';
import ExportDatasetModal from 'components/export-dataset/export-dataset-modal';
import EmptyListComponent from './empty-list';
import TopBarComponent from './top-bar';
import ProjectListComponent from './project-list';
@ -55,6 +56,7 @@ export default function ProjectsPageComponent(): JSX.Element {
<TopBarComponent />
{projectsCount ? <ProjectListComponent /> : <EmptyListComponent notFound={anySearchQuery} />}
<FeedbackComponent />
<ExportDatasetModal />
</div>
);
}

@ -6,9 +6,8 @@
.cvat-projects-page {
padding-top: $grid-unit-size * 2;
padding-bottom: $grid-unit-size * 5;
padding-bottom: $grid-unit-size;
height: 100%;
position: fixed;
width: 100%;
> div:nth-child(1) {
@ -71,28 +70,6 @@
justify-content: center;
}
.cvat-projects-project-item-title,
.cvat-projects-project-item-card-preview {
cursor: pointer;
}
.cvat-porjects-project-item-description {
display: flex;
justify-content: space-between;
// actions button
> div:nth-child(2) {
display: flex;
align-self: flex-end;
justify-content: center;
> button {
color: $text-color;
width: inherit;
}
}
}
.ant-menu.cvat-project-actions-menu {
box-shadow: 0 0 17px rgba(0, 0, 0, 0.2);
@ -107,17 +84,62 @@
}
.cvat-projects-project-item-card {
.ant-empty {
margin: $grid-unit-size;
height: $grid-unit-size * 16;
}
.cvat-projects-project-item-card-preview {
.ant-empty {
margin: $grid-unit-size;
height: inherit;
display: grid;
> div:first-child {
margin: auto;
}
}
img {
height: $grid-unit-size * 18;
height: 100%;
display: flex;
align-items: center;
justify-content: space-around;
object-fit: cover;
cursor: pointer;
}
.cvat-projects-project-item-title {
cursor: pointer;
}
.cvat-porjects-project-item-description {
display: flex;
justify-content: space-between;
// actions button
> div:nth-child(2) {
display: flex;
align-self: flex-end;
justify-content: center;
> button {
color: $text-color;
width: inherit;
}
}
}
.ant-card-cover {
flex: 1;
height: 0;
}
width: 25%;
border-width: 4px;
display: flex;
flex-direction: column;
}
.cvat-project-list-content {
padding-bottom: $grid-unit-size;
}
.cvat-projects-list {
display: flex;
flex-wrap: wrap;
}

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -9,9 +9,10 @@ import { Link, withRouter } from 'react-router-dom';
import Title from 'antd/lib/typography/Title';
import Text from 'antd/lib/typography/Text';
import { Row, Col } from 'antd/lib/grid';
import { Layout } from 'antd';
import { UserAgreement } from 'reducers/interfaces';
import CookieDrawer from 'components/login-page/cookie-policy-drawer';
import FooterDrawer from 'components/login-page/intel-footer-drawer';
import RegisterForm, { RegisterData, UserConfirmation } from './register-form';
interface RegisterPageComponentProps {
@ -38,39 +39,42 @@ function RegisterPageComponent(props: RegisterPageComponentProps & RouteComponen
};
const { fetching, userAgreements, onRegister } = props;
const { Content } = Layout;
return (
<>
<Row justify='center' align='middle'>
<Col {...sizes}>
<Title level={2}> Create an account </Title>
<RegisterForm
fetching={fetching}
userAgreements={userAgreements}
onSubmit={(registerData: RegisterData): void => {
onRegister(
registerData.username,
registerData.firstName,
registerData.lastName,
registerData.email,
registerData.password1,
registerData.password2,
registerData.confirmations,
);
}}
/>
<Row justify='start' align='top'>
<Col>
<Text strong>
Already have an account?
<Link to='/auth/login'> Login </Link>
</Text>
</Col>
</Row>
</Col>
</Row>
<CookieDrawer />
</>
<Layout>
<Content>
<Row justify='center' align='middle' style={{ height: '100%' }}>
<Col {...sizes}>
<Title level={2}> Create an account </Title>
<RegisterForm
fetching={fetching}
userAgreements={userAgreements}
onSubmit={(registerData: RegisterData): void => {
onRegister(
registerData.username,
registerData.firstName,
registerData.lastName,
registerData.email,
registerData.password1,
registerData.password2,
registerData.confirmations,
);
}}
/>
<Row justify='start' align='top'>
<Col>
<Text strong>
Already have an account?
<Link to='/auth/login'> Login </Link>
</Text>
</Col>
</Row>
</Col>
</Row>
</Content>
<FooterDrawer />
</Layout>
);
}

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -6,10 +6,12 @@ import React from 'react';
import { connect } from 'react-redux';
import Title from 'antd/lib/typography/Title';
import { Row, Col } from 'antd/lib/grid';
import { Layout } from 'antd';
import { CombinedState } from 'reducers/interfaces';
import { resetPasswordAsync } from 'actions/auth-actions';
import FooterDrawer from 'components/login-page/intel-footer-drawer';
import ResetPasswordConfirmForm, { ResetPasswordConfirmData } from './reset-password-confirm-form';
interface StateToProps {
@ -46,23 +48,30 @@ function ResetPasswordPagePageComponent(props: ResetPasswordConfirmPageComponent
const { fetching, onResetPasswordConfirm } = props;
const { Content } = Layout;
return (
<Row justify='center' align='middle'>
<Col {...sizes}>
<Title level={2}> Change password </Title>
<ResetPasswordConfirmForm
fetching={fetching}
onSubmit={(resetPasswordConfirmData: ResetPasswordConfirmData): void => {
onResetPasswordConfirm(
resetPasswordConfirmData.newPassword1,
resetPasswordConfirmData.newPassword2,
resetPasswordConfirmData.uid,
resetPasswordConfirmData.token,
);
}}
/>
</Col>
</Row>
<Layout>
<Content>
<Row justify='center' align='middle' style={{ height: '100%' }}>
<Col {...sizes}>
<Title level={2}> Change password </Title>
<ResetPasswordConfirmForm
fetching={fetching}
onSubmit={(resetPasswordConfirmData: ResetPasswordConfirmData): void => {
onResetPasswordConfirm(
resetPasswordConfirmData.newPassword1,
resetPasswordConfirmData.newPassword2,
resetPasswordConfirmData.uid,
resetPasswordConfirmData.token,
);
}}
/>
</Col>
</Row>
</Content>
<FooterDrawer />
</Layout>
);
}

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -8,9 +8,11 @@ import { connect } from 'react-redux';
import Title from 'antd/lib/typography/Title';
import Text from 'antd/lib/typography/Text';
import { Row, Col } from 'antd/lib/grid';
import { Layout } from 'antd';
import { requestPasswordResetAsync } from 'actions/auth-actions';
import { CombinedState } from 'reducers/interfaces';
import FooterDrawer from 'components/login-page/intel-footer-drawer';
import ResetPasswordForm, { ResetPasswordData } from './reset-password-form';
interface StateToProps {
@ -46,27 +48,33 @@ function ResetPasswordPagePageComponent(props: ResetPasswordPageComponentProps):
};
const { fetching, onResetPassword } = props;
const { Content } = Layout;
return (
<Row justify='center' align='middle'>
<Col {...sizes}>
<Title level={2}> Reset password </Title>
<ResetPasswordForm
fetching={fetching}
onSubmit={(resetPasswordData: ResetPasswordData): void => {
onResetPassword(resetPasswordData.email);
}}
/>
<Row justify='start' align='top'>
<Col>
<Text strong>
Go to
<Link to='/auth/login'> login page </Link>
</Text>
<Layout>
<Content>
<Row justify='center' align='middle' style={{ height: '100%' }}>
<Col {...sizes}>
<Title level={2}> Reset password </Title>
<ResetPasswordForm
fetching={fetching}
onSubmit={(resetPasswordData: ResetPasswordData): void => {
onResetPassword(resetPasswordData.email);
}}
/>
<Row justify='start' align='top'>
<Col>
<Text strong>
Go to
<Link to='/auth/login'> login page </Link>
</Text>
</Col>
</Row>
</Col>
</Row>
</Col>
</Row>
</Content>
<FooterDrawer />
</Layout>
);
}

@ -88,9 +88,7 @@
.cvat-task-preview-wrapper {
overflow: hidden;
margin-bottom: 20px;
width: $grid-unit-size * 32;
height: $grid-unit-size * 18;
display: table-cell;
text-align: center;
vertical-align: middle;
background-color: $background-color-2;
@ -98,7 +96,6 @@
.cvat-task-parameters {
margin-top: $grid-unit-size * 2;
width: $grid-unit-size * 32;
}
.cvat-open-bug-tracker-button {

@ -14,6 +14,7 @@ import DetailsContainer from 'containers/task-page/details';
import JobListContainer from 'containers/task-page/job-list';
import ModelRunnerModal from 'components/model-runner-modal/model-runner-dialog';
import MoveTaskModal from 'components/move-task-modal/move-task-modal';
import ExportDatasetModal from 'components/export-dataset/export-dataset-modal';
import { Task } from 'reducers/interfaces';
import TopBarComponent from './top-bar';
@ -85,6 +86,7 @@ class TaskPageComponent extends React.PureComponent<Props> {
</Row>
<ModelRunnerModal />
<MoveTaskModal />
<ExportDatasetModal />
{updating && <Spin size='large' className='cvat-spinner' />}
</>
);

@ -6,8 +6,8 @@
@import '../../styles.scss';
.cvat-tasks-page {
padding-top: 15px;
padding-bottom: 40px;
padding-top: $grid-unit-size * 2;
padding-bottom: $grid-unit-size;
height: 100%;
width: 100%;
@ -33,6 +33,18 @@
padding-top: 10px;
}
@media screen and (min-height: 900px) {
> div:nth-child(2) {
height: 88%;
}
}
@media screen and (min-height: 1200px) {
> div:nth-child(2) {
height: 93%;
}
}
> div:nth-child(3) {
padding-top: 10px;
}

@ -14,6 +14,7 @@ import Text from 'antd/lib/typography/Text';
import { TasksQuery } from 'reducers/interfaces';
import FeedbackComponent from 'components/feedback/feedback';
import TaskListContainer from 'containers/tasks-page/tasks-list';
import ExportDatasetModal from 'components/export-dataset/export-dataset-modal';
import TopBar from './top-bar';
import EmptyListComponent from './empty-list';
@ -221,6 +222,7 @@ class TasksPageComponent extends React.PureComponent<TasksPageProps & RouteCompo
<EmptyListComponent />
)}
<FeedbackComponent />
<ExportDatasetModal />
</div>
);
}

@ -22,6 +22,9 @@ const LATEST_COMMENTS_SHOWN_QUICK_ISSUE = 3;
const QUICK_ISSUE_INCORRECT_POSITION_TEXT = 'Wrong position';
const QUICK_ISSUE_INCORRECT_ATTRIBUTE_TEXT = 'Wrong attribute';
const DEFAULT_PROJECT_SUBSETS = ['Train', 'Test', 'Validation'];
const INTEL_TERMS_OF_USE_URL = 'https://www.intel.com/content/www/us/en/legal/terms-of-use.html';
const INTEL_COOKIES_URL = 'https://www.intel.com/content/www/us/en/privacy/intel-cookie-notice.html';
const INTEL_PRIVACY_URL = 'https://www.intel.com/content/www/us/en/privacy/intel-privacy-notice.html';
export default {
UNDEFINED_ATTRIBUTE_VALUE,
@ -41,4 +44,7 @@ export default {
QUICK_ISSUE_INCORRECT_POSITION_TEXT,
QUICK_ISSUE_INCORRECT_ATTRIBUTE_TEXT,
DEFAULT_PROJECT_SUBSETS,
INTEL_TERMS_OF_USE_URL,
INTEL_COOKIES_URL,
INTEL_PRIVACY_URL,
};

@ -12,13 +12,12 @@ import { CombinedState } from 'reducers/interfaces';
import { modelsActions } from 'actions/models-actions';
import {
dumpAnnotationsAsync,
loadAnnotationsAsync,
exportDatasetAsync,
deleteTaskAsync,
exportTaskAsync,
switchMoveTaskModalVisible,
} from 'actions/tasks-actions';
import { exportActions } from 'actions/export-actions';
interface OwnProps {
taskInstance: any;
@ -27,16 +26,13 @@ interface OwnProps {
interface StateToProps {
annotationFormats: any;
loadActivity: string | null;
dumpActivities: string[] | null;
exportActivities: string[] | null;
inferenceIsActive: boolean;
exportIsActive: boolean;
}
interface DispatchToProps {
loadAnnotations: (taskInstance: any, loader: any, file: File) => void;
dumpAnnotations: (taskInstance: any, dumper: any) => void;
exportDataset: (taskInstance: any, exporter: any) => void;
showExportModal: (taskInstance: any) => void;
deleteTask: (taskInstance: any) => void;
openRunModelWindow: (taskInstance: any) => void;
exportTask: (taskInstance: any) => void;
@ -52,14 +48,12 @@ function mapStateToProps(state: CombinedState, own: OwnProps): StateToProps {
formats: { annotationFormats },
tasks: {
activities: {
dumps, loads, exports: activeExports, backups,
loads, backups,
},
},
} = state;
return {
dumpActivities: tid in dumps ? dumps[tid] : null,
exportActivities: tid in activeExports ? activeExports[tid] : null,
loadActivity: tid in loads ? loads[tid] : null,
annotationFormats,
inferenceIsActive: tid in state.models.inferences,
@ -72,11 +66,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
loadAnnotations: (taskInstance: any, loader: any, file: File): void => {
dispatch(loadAnnotationsAsync(taskInstance, loader, file));
},
dumpAnnotations: (taskInstance: any, dumper: any): void => {
dispatch(dumpAnnotationsAsync(taskInstance, dumper));
},
exportDataset: (taskInstance: any, exporter: any): void => {
dispatch(exportDatasetAsync(taskInstance, exporter));
showExportModal: (taskInstance: any): void => {
dispatch(exportActions.openExportModal(taskInstance));
},
deleteTask: (taskInstance: any): void => {
dispatch(deleteTaskAsync(taskInstance));
@ -98,14 +89,11 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps):
taskInstance,
annotationFormats: { loaders, dumpers },
loadActivity,
dumpActivities,
exportActivities,
inferenceIsActive,
exportIsActive,
loadAnnotations,
dumpAnnotations,
exportDataset,
showExportModal,
deleteTask,
openRunModelWindow,
exportTask,
@ -115,28 +103,18 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps):
function onClickMenu(params: MenuInfo, file?: File): void {
if (params.keyPath.length > 1) {
const [additionalKey, action] = params.keyPath;
if (action === Actions.DUMP_TASK_ANNO) {
const format = additionalKey;
const [dumper] = dumpers.filter((_dumper: any): boolean => _dumper.name === format);
if (dumper) {
dumpAnnotations(taskInstance, dumper);
}
} else if (action === Actions.LOAD_TASK_ANNO) {
if (action === Actions.LOAD_TASK_ANNO) {
const format = additionalKey;
const [loader] = loaders.filter((_loader: any): boolean => _loader.name === format);
if (loader && file) {
loadAnnotations(taskInstance, loader, file);
}
} else if (action === Actions.EXPORT_TASK_DATASET) {
const format = additionalKey;
const [exporter] = dumpers.filter((_exporter: any): boolean => _exporter.name === format);
if (exporter) {
exportDataset(taskInstance, exporter);
}
}
} else {
const [action] = params.keyPath;
if (action === Actions.DELETE_TASK) {
if (action === Actions.EXPORT_TASK_DATASET) {
showExportModal(taskInstance);
} else if (action === Actions.DELETE_TASK) {
deleteTask(taskInstance);
} else if (action === Actions.OPEN_BUG_TRACKER) {
window.open(`${taskInstance.bugTracker}`, '_blank');
@ -158,8 +136,6 @@ function ActionsMenuContainer(props: OwnProps & StateToProps & DispatchToProps):
loaders={loaders}
dumpers={dumpers}
loadActivity={loadActivity}
dumpActivities={dumpActivities}
exportActivities={exportActivities}
inferenceIsActive={inferenceIsActive}
onClickMenu={onClickMenu}
taskDimension={taskInstance.dimension}

@ -90,6 +90,7 @@ interface StateToProps {
maxZLayer: number;
curZLayer: number;
automaticBordering: boolean;
intelligentPolygonCrop: boolean;
switchableAutomaticBordering: boolean;
keyMap: KeyMap;
canvasBackgroundColor: string;
@ -188,9 +189,9 @@ function mapStateToProps(state: CombinedState): StateToProps {
activatedAttributeID,
selectedStatesID,
annotations,
opacity,
opacity: opacity / 100,
colorBy,
selectedOpacity,
selectedOpacity: selectedOpacity / 100,
outlined,
outlineColor,
showBitmap,
@ -198,12 +199,12 @@ function mapStateToProps(state: CombinedState): StateToProps {
grid,
gridSize,
gridColor,
gridOpacity,
gridOpacity: gridOpacity / 100,
activeLabelID,
activeObjectType,
brightnessLevel,
contrastLevel,
saturationLevel,
brightnessLevel: brightnessLevel / 100,
contrastLevel: contrastLevel / 100,
saturationLevel: saturationLevel / 100,
resetZoom,
aamZoomMargin,
showObjectsTextAlways,

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -10,7 +10,7 @@ import { MenuInfo } from 'rc-menu/lib/interface';
import { CombinedState, TaskStatus } from 'reducers/interfaces';
import AnnotationMenuComponent, { Actions } from 'components/annotation-page/top-bar/annotation-menu';
import { dumpAnnotationsAsync, exportDatasetAsync, updateJobAsync } from 'actions/tasks-actions';
import { updateJobAsync } from 'actions/tasks-actions';
import {
uploadJobAnnotationsAsync,
removeAnnotationsAsync,
@ -19,20 +19,18 @@ import {
switchSubmitReviewDialog as switchSubmitReviewDialogAction,
setForceExitAnnotationFlag as setForceExitAnnotationFlagAction,
} from 'actions/annotation-actions';
import { exportActions } from 'actions/export-actions';
interface StateToProps {
annotationFormats: any;
jobInstance: any;
loadActivity: string | null;
dumpActivities: string[] | null;
exportActivities: string[] | null;
user: any;
}
interface DispatchToProps {
loadAnnotations(job: any, loader: any, file: File): void;
dumpAnnotations(task: any, dumper: any): void;
exportDataset(task: any, exporter: any): void;
showExportModal(task: any): void;
removeAnnotations(sessionInstance: any): void;
switchRequestReviewDialog(visible: boolean): void;
switchSubmitReviewDialog(visible: boolean): void;
@ -49,7 +47,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
},
formats: { annotationFormats },
tasks: {
activities: { dumps, loads, exports: activeExports },
activities: { loads },
},
auth: { user },
} = state;
@ -58,8 +56,6 @@ function mapStateToProps(state: CombinedState): StateToProps {
const jobID = jobInstance.id;
return {
dumpActivities: taskID in dumps ? dumps[taskID] : null,
exportActivities: taskID in activeExports ? activeExports[taskID] : null,
loadActivity: taskID in loads || jobID in jobLoads ? loads[taskID] || jobLoads[jobID] : null,
jobInstance,
annotationFormats,
@ -72,11 +68,8 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
loadAnnotations(job: any, loader: any, file: File): void {
dispatch(uploadJobAnnotationsAsync(job, loader, file));
},
dumpAnnotations(task: any, dumper: any): void {
dispatch(dumpAnnotationsAsync(task, dumper));
},
exportDataset(task: any, exporter: any): void {
dispatch(exportDatasetAsync(task, exporter));
showExportModal(task: any): void {
dispatch(exportActions.openExportModal(task));
},
removeAnnotations(sessionInstance: any): void {
dispatch(removeAnnotationsAsync(sessionInstance));
@ -108,11 +101,8 @@ function AnnotationMenuContainer(props: Props): JSX.Element {
annotationFormats: { loaders, dumpers },
history,
loadActivity,
dumpActivities,
exportActivities,
loadAnnotations,
dumpAnnotations,
exportDataset,
showExportModal,
removeAnnotations,
switchRequestReviewDialog,
switchSubmitReviewDialog,
@ -124,28 +114,18 @@ function AnnotationMenuContainer(props: Props): JSX.Element {
const onClickMenu = (params: MenuInfo, file?: File): void => {
if (params.keyPath.length > 1) {
const [additionalKey, action] = params.keyPath;
if (action === Actions.DUMP_TASK_ANNO) {
const format = additionalKey;
const [dumper] = dumpers.filter((_dumper: any): boolean => _dumper.name === format);
if (dumper) {
dumpAnnotations(jobInstance.task, dumper);
}
} else if (action === Actions.LOAD_JOB_ANNO) {
if (action === Actions.LOAD_JOB_ANNO) {
const format = additionalKey;
const [loader] = loaders.filter((_loader: any): boolean => _loader.name === format);
if (loader && file) {
loadAnnotations(jobInstance, loader, file);
}
} else if (action === Actions.EXPORT_TASK_DATASET) {
const format = additionalKey;
const [exporter] = dumpers.filter((_exporter: any): boolean => _exporter.name === format);
if (exporter) {
exportDataset(jobInstance.task, exporter);
}
}
} else {
const [action] = params.keyPath;
if (action === Actions.REMOVE_ANNO) {
if (action === Actions.EXPORT_TASK_DATASET) {
showExportModal(jobInstance.task);
} else if (action === Actions.REMOVE_ANNO) {
removeAnnotations(jobInstance);
} else if (action === Actions.REQUEST_REVIEW) {
switchRequestReviewDialog(true);
@ -173,8 +153,6 @@ function AnnotationMenuContainer(props: Props): JSX.Element {
loaders={loaders}
dumpers={dumpers}
loadActivity={loadActivity}
dumpActivities={dumpActivities}
exportActivities={exportActivities}
onClickMenu={onClickMenu}
setForceExitAnnotationFlag={setForceExitAnnotationFlag}
saveAnnotations={saveAnnotations}

@ -30,9 +30,10 @@ import AnnotationTopBarComponent from 'components/annotation-page/top-bar/top-ba
import { Canvas } from 'cvat-canvas-wrapper';
import { Canvas3d } from 'cvat-canvas3d-wrapper';
import {
CombinedState, FrameSpeed, Workspace, PredictorState, DimensionType, ActiveControl,
CombinedState, FrameSpeed, Workspace, PredictorState, DimensionType, ActiveControl, ToolsBlockerState,
} from 'reducers/interfaces';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import { switchToolsBlockerState } from 'actions/settings-actions';
interface StateToProps {
jobInstance: any;
@ -49,6 +50,7 @@ interface StateToProps {
redoAction?: string;
autoSave: boolean;
autoSaveInterval: number;
toolsBlockerState: ToolsBlockerState;
workspace: Workspace;
keyMap: KeyMap;
normalizedKeyMap: Record<string, string>;
@ -72,6 +74,7 @@ interface DispatchToProps {
setForceExitAnnotationFlag(forceExit: boolean): void;
changeWorkspace(workspace: Workspace): void;
switchPredictor(predictorEnabled: boolean): void;
onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState): void;
}
function mapStateToProps(state: CombinedState): StateToProps {
@ -92,7 +95,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
},
settings: {
player: { frameSpeed, frameStep },
workspace: { autoSave, autoSaveInterval },
workspace: { autoSave, autoSaveInterval, toolsBlockerState },
},
shortcuts: { keyMap, normalizedKeyMap },
plugins: { list },
@ -113,6 +116,7 @@ function mapStateToProps(state: CombinedState): StateToProps {
redoAction: history.redo.length ? history.redo[history.redo.length - 1][0] : undefined,
autoSave,
autoSaveInterval,
toolsBlockerState,
workspace,
keyMap,
normalizedKeyMap,
@ -167,6 +171,9 @@ function mapDispatchToProps(dispatch: any): DispatchToProps {
dispatch(getPredictionsAsync());
}
},
onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState):void{
dispatch(switchToolsBlockerState(toolsBlockerState));
},
};
}
@ -431,6 +438,22 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
canvasInstance.draw({ enabled: false });
};
private onSwitchToolsBlockerState = (): void => {
const {
toolsBlockerState, onSwitchToolsBlockerState, canvasInstance, activeControl,
} = this.props;
if (canvasInstance instanceof Canvas) {
if (activeControl.includes(ActiveControl.OPENCV_TOOLS)) {
canvasInstance.interact({
enabled: true,
crosshair: toolsBlockerState.algorithmsLocked,
enableThreshold: toolsBlockerState.algorithmsLocked,
});
}
}
onSwitchToolsBlockerState({ algorithmsLocked: !toolsBlockerState.algorithmsLocked });
};
private onURLIconClick = (): void => {
const { frameNumber } = this.props;
const { origin, pathname } = window.location;
@ -546,6 +569,7 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
searchAnnotations,
changeWorkspace,
switchPredictor,
toolsBlockerState,
} = this.props;
const preventDefault = (event: KeyboardEvent | undefined): void => {
@ -672,6 +696,8 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
undoShortcut={normalizedKeyMap.UNDO}
redoShortcut={normalizedKeyMap.REDO}
drawShortcut={normalizedKeyMap.SWITCH_DRAW_MODE}
// this shortcut is handled in interactionHandler.ts separatelly
switchToolsBlockerShortcut={normalizedKeyMap.SWITCH_TOOLS_BLOCKER_STATE}
playPauseShortcut={normalizedKeyMap.PLAY_PAUSE}
nextFrameShortcut={normalizedKeyMap.NEXT_FRAME}
previousFrameShortcut={normalizedKeyMap.PREV_FRAME}
@ -683,6 +709,8 @@ class AnnotationTopBarContainer extends React.PureComponent<Props, State> {
onUndoClick={this.undo}
onRedoClick={this.redo}
onFinishDraw={this.onFinishDraw}
onSwitchToolsBlockerState={this.onSwitchToolsBlockerState}
toolsBlockerState={toolsBlockerState}
jobInstance={jobInstance}
isTrainingActive={isTrainingActive}
activeControl={activeControl}

@ -0,0 +1,67 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { ExportActions, ExportActionTypes } from 'actions/export-actions';
import getCore from 'cvat-core-wrapper';
import deepCopy from 'utils/deep-copy';
import { ExportState } from './interfaces';
const core = getCore();
const defaultState: ExportState = {
tasks: {},
projects: {},
instance: null,
modalVisible: false,
};
export default (state: ExportState = defaultState, action: ExportActions): ExportState => {
switch (action.type) {
case ExportActionTypes.OPEN_EXPORT_MODAL:
return {
...state,
modalVisible: true,
instance: action.payload.instance,
};
case ExportActionTypes.CLOSE_EXPORT_MODAL:
return {
...state,
modalVisible: false,
instance: null,
};
case ExportActionTypes.EXPORT_DATASET: {
const { instance, format } = action.payload;
const activities = deepCopy(instance instanceof core.classes.Project ? state.projects : state.tasks);
activities[instance.id] =
instance.id in activities && !activities[instance.id].includes(format) ?
[...activities[instance.id], format] :
activities[instance.id] || [format];
return {
...state,
tasks: instance instanceof core.classes.Task ? activities : state.tasks,
projects: instance instanceof core.classes.Project ? activities : state.projects,
};
}
case ExportActionTypes.EXPORT_DATASET_FAILED:
case ExportActionTypes.EXPORT_DATASET_SUCCESS: {
const { instance, format } = action.payload;
const activities = deepCopy(instance instanceof core.classes.Project ? state.projects : state.tasks);
activities[instance.id] = activities[instance.id].filter(
(exporterName: string): boolean => exporterName !== format,
);
return {
...state,
tasks: instance instanceof core.classes.Task ? activities : state.tasks,
projects: instance instanceof core.classes.Project ? activities : state.projects,
};
}
default:
return state;
}
};

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT

@ -86,14 +86,6 @@ export interface TasksState {
count: number;
current: Task[];
activities: {
dumps: {
// dumps in different formats at the same time
[tid: number]: string[]; // dumper names
};
exports: {
// exports in different formats at the same time
[tid: number]: string[]; // dumper names
};
loads: {
// only one loading simultaneously
[tid: number]: string; // loader name
@ -112,6 +104,17 @@ export interface TasksState {
};
}
export interface ExportState {
tasks: {
[tid: number]: string[];
};
projects: {
[pid: number]: string[];
};
instance: any;
modalVisible: boolean;
}
export interface FormatsState {
annotationFormats: any;
fetching: boolean;
@ -182,12 +185,23 @@ export interface Model {
framework: string;
description: string;
type: string;
onChangeToolsBlockerState: (event:string) => void;
tip: {
message: string;
gif: string;
};
params: {
canvas: Record<string, unknown>;
canvas: Record<string, number | boolean>;
};
}
export type OpenCVTool = IntelligentScissors;
export interface ToolsBlockerState {
algorithmsLocked?: boolean;
buttonVisible?: boolean;
}
export enum TaskStatus {
ANNOTATION = 'annotation',
REVIEW = 'validation',
@ -557,6 +571,7 @@ export interface WorkspaceSettingsState {
showAllInterpolationTracks: boolean;
intelligentPolygonCrop: boolean;
defaultApproxPolyAccuracy: number;
toolsBlockerState: ToolsBlockerState;
}
export interface ShapesSettingsState {
@ -617,6 +632,7 @@ export interface CombinedState {
settings: SettingsState;
shortcuts: ShortcutsState;
review: ReviewState;
export: ExportState;
}
export enum DimensionType {

@ -16,9 +16,13 @@ import { NotificationsActionType } from 'actions/notification-actions';
import { BoundariesActionTypes } from 'actions/boundaries-actions';
import { UserAgreementsActionTypes } from 'actions/useragreements-actions';
import { ReviewActionTypes } from 'actions/review-actions';
import { ExportActionTypes } from 'actions/export-actions';
import getCore from 'cvat-core-wrapper';
import { NotificationsState } from './interfaces';
const core = getCore();
const defaultState: NotificationsState = {
errors: {
auth: {
@ -308,8 +312,9 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case TasksActionTypes.EXPORT_DATASET_FAILED: {
const taskID = action.payload.task.id;
case ExportActionTypes.EXPORT_DATASET_FAILED: {
const instanceID = action.payload.instance.id;
const instanceType = action.payload.instance instanceof core.classes.Project ? 'project' : 'task';
return {
...state,
errors: {
@ -319,7 +324,8 @@ export default function (state = defaultState, action: AnyAction): Notifications
exportingAsDataset: {
message:
'Could not export dataset for the ' +
`<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
`<a href="/${instanceType}s/${instanceID}" target="_blank">` +
`${instanceType} ${instanceID}</a>`,
reason: action.payload.error.toString(),
},
},
@ -392,24 +398,6 @@ export default function (state = defaultState, action: AnyAction): Notifications
},
};
}
case TasksActionTypes.DUMP_ANNOTATIONS_FAILED: {
const taskID = action.payload.task.id;
return {
...state,
errors: {
...state.errors,
tasks: {
...state.errors.tasks,
dumping: {
message:
'Could not dump annotations for the ' +
`<a href="/tasks/${taskID}" target="_blank">task ${taskID}</a>`,
reason: action.payload.error.toString(),
},
},
},
};
}
case TasksActionTypes.DELETE_TASK_FAILED: {
const { taskID } = action.payload;
return {

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
@ -17,6 +17,7 @@ import settingsReducer from './settings-reducer';
import shortcutsReducer from './shortcuts-reducer';
import userAgreementsReducer from './useragreements-reducer';
import reviewReducer from './review-reducer';
import exportReducer from './export-reducer';
export default function createRootReducer(): Reducer {
return combineReducers({
@ -34,5 +35,6 @@ export default function createRootReducer(): Reducer {
shortcuts: shortcutsReducer,
userAgreements: userAgreementsReducer,
review: reviewReducer,
export: exportReducer,
});
}

@ -32,6 +32,10 @@ const defaultState: SettingsState = {
showAllInterpolationTracks: false,
intelligentPolygonCrop: true,
defaultApproxPolyAccuracy: 9,
toolsBlockerState: {
algorithmsLocked: false,
buttonVisible: false,
},
},
player: {
canvasBackgroundColor: '#ffffff',
@ -287,6 +291,15 @@ export default (state = defaultState, action: AnyAction): SettingsState => {
},
};
}
case SettingsActionTypes.SWITCH_TOOLS_BLOCKER_STATE: {
return {
...state,
workspace: {
...state.workspace,
toolsBlockerState: { ...state.workspace.toolsBlockerState, ...action.payload.toolsBlockerState },
},
};
}
case SettingsActionTypes.SWITCH_SETTINGS_DIALOG: {
return {
...state,
@ -320,6 +333,18 @@ export default (state = defaultState, action: AnyAction): SettingsState => {
},
};
}
case AnnotationActionTypes.INTERACT_WITH_CANVAS: {
return {
...state,
workspace: {
...state.workspace,
toolsBlockerState: {
buttonVisible: true,
algorithmsLocked: false,
},
},
};
}
case AuthActionTypes.LOGOUT_SUCCESS: {
return { ...defaultState };
}

@ -322,6 +322,13 @@ const defaultKeyMap = ({
action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D],
},
SWITCH_TOOLS_BLOCKER_STATE: {
name: 'Switch algorithm blocker',
description: 'Postpone running the algorithm for interaction tools',
sequences: ['сtrl'],
action: 'keydown',
applicable: [DimensionType.DIM_2D],
},
CHANGE_OBJECT_COLOR: {
name: 'Change color',
description: 'Set the next color for an activated shape',

@ -32,8 +32,6 @@ const defaultState: TasksState = {
mode: null,
},
activities: {
dumps: {},
exports: {},
loads: {},
deletes: {},
creates: {
@ -85,84 +83,6 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
initialized: true,
fetching: false,
};
case TasksActionTypes.DUMP_ANNOTATIONS: {
const { task } = action.payload;
const { dumper } = action.payload;
const { dumps } = state.activities;
dumps[task.id] =
task.id in dumps && !dumps[task.id].includes(dumper.name) ?
[...dumps[task.id], dumper.name] :
dumps[task.id] || [dumper.name];
return {
...state,
activities: {
...state.activities,
dumps: {
...dumps,
},
},
};
}
case TasksActionTypes.DUMP_ANNOTATIONS_FAILED:
case TasksActionTypes.DUMP_ANNOTATIONS_SUCCESS: {
const { task } = action.payload;
const { dumper } = action.payload;
const { dumps } = state.activities;
dumps[task.id] = dumps[task.id].filter((dumperName: string): boolean => dumperName !== dumper.name);
return {
...state,
activities: {
...state.activities,
dumps: {
...dumps,
},
},
};
}
case TasksActionTypes.EXPORT_DATASET: {
const { task } = action.payload;
const { exporter } = action.payload;
const { exports: activeExports } = state.activities;
activeExports[task.id] =
task.id in activeExports && !activeExports[task.id].includes(exporter.name) ?
[...activeExports[task.id], exporter.name] :
activeExports[task.id] || [exporter.name];
return {
...state,
activities: {
...state.activities,
exports: {
...activeExports,
},
},
};
}
case TasksActionTypes.EXPORT_DATASET_FAILED:
case TasksActionTypes.EXPORT_DATASET_SUCCESS: {
const { task } = action.payload;
const { exporter } = action.payload;
const { exports: activeExports } = state.activities;
activeExports[task.id] = activeExports[task.id].filter(
(exporterName: string): boolean => exporterName !== exporter.name,
);
return {
...state,
activities: {
...state.activities,
exports: {
...activeExports,
},
},
};
}
case TasksActionTypes.LOAD_ANNOTATIONS: {
const { task } = action.payload;
const { loader } = action.payload;

@ -57,7 +57,7 @@ hr {
width: 100%;
height: 100%;
display: grid;
min-width: 1280px;
min-width: 1024px;
}
#layout-grid {

@ -0,0 +1,21 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
function deepCopy<T>(obj: T): T {
if (typeof obj !== 'object') {
return obj;
}
if (!obj) {
return obj;
}
const container: any = (obj instanceof Array) ? [] : {};
for (const i in obj) {
if (Object.prototype.hasOwnProperty.call(obj, i)) {
container[i] = deepCopy(obj[i]);
}
}
return container;
}
export default deepCopy;

@ -1,4 +1,4 @@
// Copyright (C) 2020 Intel Corporation
// Copyright (C) 2020-2021 Intel Corporation
//
// SPDX-License-Identifier: MIT

@ -1,7 +1,7 @@
// Copyright (C) 2021 Intel Corporation
//
// SPDX-License-Identifier: MIT
import { useRef, useEffect } from 'react';
import { useRef, useEffect, useState } from 'react';
// eslint-disable-next-line import/prefer-default-export
export function usePrevious<T>(value: T): T | undefined {
@ -11,3 +11,51 @@ export function usePrevious<T>(value: T): T | undefined {
});
return ref.current;
}
export interface ICardHeightHOC {
numberOfRows: number;
paddings: number;
containerClassName: string;
siblingClassNames: string[];
}
export function useCardHeightHOC(params: ICardHeightHOC): () => string {
const {
numberOfRows, paddings, containerClassName, siblingClassNames,
} = params;
return (): string => {
const [height, setHeight] = useState('auto');
useEffect(() => {
const resize = (): void => {
const container = window.document.getElementsByClassName(containerClassName)[0];
const siblings = siblingClassNames.map(
(classname: string): Element | undefined => window.document.getElementsByClassName(classname)[0],
);
if (container) {
const { clientHeight: containerHeight } = container;
const othersHeight = siblings.reduce<number>((acc: number, el: Element | undefined): number => {
if (el) {
return acc + el.clientHeight;
}
return acc;
}, 0);
const cardHeight = (containerHeight - (othersHeight + paddings)) / numberOfRows;
setHeight(`${Math.round(cardHeight)}px`);
}
};
resize();
window.addEventListener('resize', resize);
return () => {
window.removeEventListener('resize', resize);
};
}, []);
return height;
};
}

@ -60,8 +60,7 @@ export default class HistogramEqualizationImplementation implements HistogramEqu
this.hashFrame(imgData, frameNumber);
return imgData;
} catch (e) {
console.log('Histogram equalization error', e);
return src;
throw new Error(e.toString());
} finally {
if (matImage) matImage.delete();
if (channels) channels.delete();

@ -14,6 +14,7 @@ export interface IntelligentScissorsParams {
enableSliding: boolean;
allowRemoveOnlyLast: boolean;
minPosVertices: number;
onChangeToolsBlockerState: (event:string)=>void;
};
}
@ -21,6 +22,7 @@ export interface IntelligentScissors {
reset(): void;
run(points: number[], image: ImageData, offsetX: number, offsetY: number): number[];
params: IntelligentScissorsParams;
switchBlockMode(mode?:boolean):void;
}
function applyOffset(points: Point[], offsetX: number, offsetY: number): Point[] {
@ -34,6 +36,7 @@ function applyOffset(points: Point[], offsetX: number, offsetY: number): Point[]
export default class IntelligentScissorsImplementation implements IntelligentScissors {
private cv: any;
private onChangeToolsBlockerState: (event:string)=>void;
private scissors: {
tool: any;
state: {
@ -46,14 +49,20 @@ export default class IntelligentScissorsImplementation implements IntelligentSci
}
>; // point index : start index in path
image: any | null;
blocked: boolean;
};
};
public constructor(cv: any) {
public constructor(cv: any, onChangeToolsBlockerState:(event:string)=>void) {
this.cv = cv;
this.onChangeToolsBlockerState = onChangeToolsBlockerState;
this.reset();
}
public switchBlockMode(mode:boolean): void {
this.scissors.state.blocked = mode;
}
public reset(): void {
if (this.scissors && this.scissors.tool) {
this.scissors.tool.delete();
@ -66,6 +75,7 @@ export default class IntelligentScissorsImplementation implements IntelligentSci
path: [],
anchors: {},
image: null,
blocked: false,
},
};
@ -88,7 +98,6 @@ export default class IntelligentScissorsImplementation implements IntelligentSci
const { tool, state } = scissors;
const points = applyOffset(numberArrayToPoints(coordinates), offsetX, offsetY);
if (points.length > 1) {
let matImage = null;
const contour = new cv.Mat();
@ -108,7 +117,6 @@ export default class IntelligentScissorsImplementation implements IntelligentSci
delete state.anchors[+i];
}
}
return [...state.path];
}
@ -118,14 +126,17 @@ export default class IntelligentScissorsImplementation implements IntelligentSci
state.path = state.path.slice(0, state.anchors[points.length - 1].start);
delete state.anchors[points.length - 1];
}
tool.applyImage(matImage);
tool.buildMap(new cv.Point(prevX, prevY));
tool.getContour(new cv.Point(curX, curY), contour);
const pathSegment = [];
for (let row = 0; row < contour.rows; row++) {
pathSegment.push(contour.intAt(row, 0) + offsetX, contour.intAt(row, 1) + offsetY);
if (!state.blocked) {
tool.applyImage(matImage);
tool.buildMap(new cv.Point(prevX, prevY));
tool.getContour(new cv.Point(curX, curY), contour);
for (let row = 0; row < contour.rows; row++) {
pathSegment.push(contour.intAt(row, 0) + offsetX, contour.intAt(row, 1) + offsetY);
}
} else {
pathSegment.push(curX + offsetX, curY + offsetY);
}
state.anchors[points.length - 1] = {
point: cur,
@ -140,13 +151,13 @@ export default class IntelligentScissorsImplementation implements IntelligentSci
contour.delete();
}
} else {
state.path = [];
state.path.push(...pointsToNumberArray(applyOffset(points.slice(-1), -offsetX, -offsetY)));
state.anchors[0] = {
point: points[0],
start: 0,
};
}
return [...state.path];
}
@ -167,6 +178,7 @@ export default class IntelligentScissorsImplementation implements IntelligentSci
enableSliding: true,
allowRemoveOnlyLast: true,
minPosVertices: 1,
onChangeToolsBlockerState: this.onChangeToolsBlockerState,
},
};
}

@ -12,7 +12,7 @@ const core = getCore();
const baseURL = core.config.backendAPI.slice(0, -7);
export interface Segmentation {
intelligentScissorsFactory: () => IntelligentScissors;
intelligentScissorsFactory: (onChangeToolsBlockerState:(event:string)=>void) => IntelligentScissors;
}
export interface Contours {
@ -20,7 +20,7 @@ export interface Contours {
}
export interface ImgProc {
hist: () => HistogramEqualization
hist: () => HistogramEqualization;
}
export class OpenCVWrapper {
@ -41,10 +41,6 @@ export class OpenCVWrapper {
const contentLength = response.headers.get('Content-Length');
const { body } = response;
if (contentLength === null) {
throw new Error('Content length is null, but necessary');
}
if (body === null) {
throw new Error('Response body is null, but necessary');
}
@ -64,7 +60,9 @@ export class OpenCVWrapper {
if (value instanceof Uint8Array) {
decodedScript += decoder.decode(value);
receivedLength += value.length;
const percentage = (receivedLength * 100) / +(contentLength as string);
// Cypress workaround: content-length is always zero in cypress, it is done optional here
// Just progress bar will be disabled
const percentage = contentLength ? (receivedLength * 100) / +(contentLength as string) : 0;
onProgress(+percentage.toFixed(0));
}
}
@ -128,7 +126,8 @@ export class OpenCVWrapper {
}
return {
intelligentScissorsFactory: () => new IntelligentScissorsImplementation(this.cv),
intelligentScissorsFactory: (onChangeToolsBlockerState:(event:string)=>void) =>
new IntelligentScissorsImplementation(this.cv, onChangeToolsBlockerState),
};
}

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

Loading…
Cancel
Save