Merge branch 'release-1.6.0'

main
Nikita Manovich 4 years ago
commit 8d03ed7959

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

@ -5,7 +5,7 @@
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"type": "chrome", "type": "pwa-chrome",
"request": "launch", "request": "launch",
"preLaunchTask": "npm: start - cvat-ui", "preLaunchTask": "npm: start - cvat-ui",
"name": "ui.js: debug", "name": "ui.js: debug",
@ -59,7 +59,7 @@
}, },
{ {
"name": "server: chrome", "name": "server: chrome",
"type": "chrome", "type": "pwa-chrome",
"request": "launch", "request": "launch",
"url": "http://localhost:7000/", "url": "http://localhost:7000/",
"disableNetworkCache":true, "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/), 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). 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 ## \[1.5.0] - 2021-08-02
### Added ### 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>) - 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>) - 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>) - 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 ### 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>) - 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>) - 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>) - 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 ## \[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 ## Template
``` ```
## [Unreleased] ## \[Unreleased]
### Added ### Added
- - TDB
### Changed ### Changed
- - TDB
### Deprecated ### Deprecated
- - TDB
### Removed ### Removed
- - TDB
### Fixed ### Fixed
- - TDB
### Security ### Security
- - TDB
``` ```

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

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

@ -1,6 +1,6 @@
{ {
"name": "cvat-canvas", "name": "cvat-canvas",
"version": "2.5.0", "version": "2.7.0",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -3513,14 +3513,26 @@
} }
}, },
"enhanced-resolve": { "enhanced-resolve": {
"version": "4.1.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz",
"integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==",
"dev": true, "dev": true,
"requires": { "requires": {
"graceful-fs": "^4.1.2", "graceful-fs": "^4.1.2",
"memory-fs": "^0.4.0", "memory-fs": "^0.5.0",
"tapable": "^1.0.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": { "errno": {
@ -5605,9 +5617,9 @@
} }
}, },
"interpret": { "interpret": {
"version": "1.2.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
"integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
"dev": true "dev": true
}, },
"invariant": { "invariant": {
@ -5619,12 +5631,6 @@
"loose-envify": "^1.0.0" "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": { "ip": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
@ -6165,15 +6171,6 @@
"package-json": "^6.3.0" "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": { "levn": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
@ -6309,15 +6306,6 @@
"semver": "^5.6.0" "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": { "map-cache": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
@ -6345,17 +6333,6 @@
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
"dev": true "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": { "memory-fs": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@ -6525,12 +6502,6 @@
"mime-db": "1.40.0" "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": { "mimic-response": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz",
@ -7276,17 +7247,6 @@
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"dev": true "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": { "os-tmpdir": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
@ -7309,24 +7269,12 @@
"integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==", "integrity": "sha512-s73XxOZ4zpt1edZYZzvhqFa6uvQc1vwUa0K0BdtIZgQMAJj9IbebH+JkgKZc9h+B05PKHLOTl4ajG1BmNrVZlw==",
"dev": true "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": { "p-finally": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
"dev": true "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": { "p-limit": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz",
@ -7453,9 +7401,9 @@
"dev": true "dev": true
}, },
"path-parse": { "path-parse": {
"version": "1.0.6", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
"integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true "dev": true
}, },
"path-to-regexp": { "path-to-regexp": {
@ -10275,9 +10223,9 @@
} }
}, },
"url-parse": { "url-parse": {
"version": "1.5.1", "version": "1.5.3",
"resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.3.tgz",
"integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==", "integrity": "sha512-IIORyIQD9rvj0A4CLWsHkBBJuNqWpFQe224b6j9t/ABmquIS0qDU2pY6kl6AuOrL5OkCXHMCFNe1jBcuAggjvQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"querystringify": "^2.1.1", "querystringify": "^2.1.1",
@ -10535,112 +10483,46 @@
} }
}, },
"webpack-cli": { "webpack-cli": {
"version": "3.3.6", "version": "3.3.12",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.6.tgz", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.12.tgz",
"integrity": "sha512-0vEa83M7kJtxK/jUhlpZ27WHIOndz5mghWL2O53kiDoA9DIxSKnfqB92LoqEn77cT4f3H2cZm1BMEat/6AZz3A==", "integrity": "sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==",
"dev": true, "dev": true,
"requires": { "requires": {
"chalk": "2.4.2", "chalk": "^2.4.2",
"cross-spawn": "6.0.5", "cross-spawn": "^6.0.5",
"enhanced-resolve": "4.1.0", "enhanced-resolve": "^4.1.1",
"findup-sync": "3.0.0", "findup-sync": "^3.0.0",
"global-modules": "2.0.0", "global-modules": "^2.0.0",
"import-local": "2.0.0", "import-local": "^2.0.0",
"interpret": "1.2.0", "interpret": "^1.4.0",
"loader-utils": "1.2.3", "loader-utils": "^1.4.0",
"supports-color": "6.1.0", "supports-color": "^6.1.0",
"v8-compile-cache": "2.0.3", "v8-compile-cache": "^2.1.1",
"yargs": "13.2.4" "yargs": "^13.3.2"
}, },
"dependencies": { "dependencies": {
"ansi-regex": { "emojis-list": {
"version": "4.1.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"dev": true "dev": true
}, },
"cliui": { "loader-utils": {
"version": "5.0.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"dev": true, "dev": true,
"requires": { "requires": {
"string-width": "^3.1.0", "big.js": "^5.2.2",
"strip-ansi": "^5.2.0", "emojis-list": "^3.0.0",
"wrap-ansi": "^5.1.0" "json5": "^1.0.1"
} }
}, },
"get-caller-file": { "v8-compile-cache": {
"version": "2.0.5", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
"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==",
"dev": true "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", "name": "cvat-canvas",
"version": "2.5.0", "version": "2.7.0",
"description": "Part of Computer Vision Annotation Tool which presents its canvas library", "description": "Part of Computer Vision Annotation Tool which presents its canvas library",
"main": "src/canvas.ts", "main": "src/canvas.ts",
"scripts": { "scripts": {
@ -9,6 +9,12 @@
}, },
"author": "Intel", "author": "Intel",
"license": "MIT", "license": "MIT",
"browserslist": [
"Chrome >= 63",
"Firefox > 58",
"not IE 11",
"> 2%"
],
"dependencies": { "dependencies": {
"svg.draggable.js": "2.2.2", "svg.draggable.js": "2.2.2",
"svg.draw.js": "^2.0.4", "svg.draw.js": "^2.0.4",
@ -41,7 +47,7 @@
"style-loader": "^1.0.0", "style-loader": "^1.0.0",
"typescript": "^3.5.3", "typescript": "^3.5.3",
"webpack": "^5.20.2", "webpack": "^5.20.2",
"webpack-cli": "^3.3.6", "webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0" "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 { .cvat_shape_drawing_opacity {
fill-opacity: 0.2;
stroke-opacity: 1; stroke-opacity: 1;
} }
@ -161,9 +160,8 @@ polyline.cvat_canvas_shape_splitting {
.cvat_canvas_removable_interaction_point { .cvat_canvas_removable_interaction_point {
cursor: cursor:
url( url('data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K')
'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHZpZXdCb3g9IjAgMCAxMCAxMCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHBhdGggZD0iTTEgMUw5IDlNMSA5TDkgMSIgc3Ryb2tlPSJibGFjayIvPgo8L3N2Zz4K' 10 10,
) 10 10,
auto; auto;
} }

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

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

@ -50,6 +50,7 @@ export class DrawHandlerImpl implements DrawHandler {
private crosshair: Crosshair; private crosshair: Crosshair;
private drawData: DrawData; private drawData: DrawData;
private geometry: Geometry; private geometry: Geometry;
private configuration: Configuration;
private autoborderHandler: AutoborderHandler; private autoborderHandler: AutoborderHandler;
private autobordersEnabled: boolean; private autobordersEnabled: boolean;
@ -371,6 +372,7 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing') .addClass('cvat_canvas_shape_drawing')
.attr({ .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, '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') .addClass('cvat_canvas_shape_drawing')
.attr({ .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
}); });
this.drawPolyshape(); this.drawPolyshape();
@ -597,6 +600,7 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing') .addClass('cvat_canvas_shape_drawing')
.attr({ .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, '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') .addClass('cvat_canvas_shape_drawing')
.attr({ .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
}); });
this.pasteShape(); this.pasteShape();
@ -686,6 +691,7 @@ export class DrawHandlerImpl implements DrawHandler {
.addClass('cvat_canvas_shape_drawing') .addClass('cvat_canvas_shape_drawing')
.attr({ .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'fill-opacity': this.configuration.creationOpacity,
}); });
this.pasteShape(); this.pasteShape();
this.pastePolyshape(); this.pastePolyshape();
@ -709,6 +715,7 @@ export class DrawHandlerImpl implements DrawHandler {
.attr({ .attr({
'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale, 'stroke-width': consts.BASE_STROKE_WIDTH / this.geometry.scale,
'face-stroke': 'black', 'face-stroke': 'black',
'fill-opacity': this.configuration.creationOpacity,
}); });
this.pasteShape(); this.pasteShape();
this.pastePolyshape(); this.pastePolyshape();
@ -845,6 +852,8 @@ export class DrawHandlerImpl implements DrawHandler {
canvas: SVG.Container, canvas: SVG.Container,
text: SVG.Container, text: SVG.Container,
autoborderHandler: AutoborderHandler, autoborderHandler: AutoborderHandler,
geometry: Geometry,
configuration: Configuration,
) { ) {
this.autoborderHandler = autoborderHandler; this.autoborderHandler = autoborderHandler;
this.autobordersEnabled = false; this.autobordersEnabled = false;
@ -855,7 +864,8 @@ export class DrawHandlerImpl implements DrawHandler {
this.initialized = false; this.initialized = false;
this.canceled = false; this.canceled = false;
this.drawData = null; this.drawData = null;
this.geometry = null; this.geometry = geometry;
this.configuration = configuration;
this.crosshair = new Crosshair(); this.crosshair = new Crosshair();
this.drawInstance = null; this.drawInstance = null;
this.pointsGroup = null; this.pointsGroup = null;
@ -874,6 +884,20 @@ export class DrawHandlerImpl implements DrawHandler {
} }
public configurate(configuration: Configuration): void { 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') { if (typeof configuration.autoborders === 'boolean') {
this.autobordersEnabled = configuration.autoborders; this.autobordersEnabled = configuration.autoborders;
if (this.drawInstance) { if (this.drawInstance) {

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

@ -10,8 +10,8 @@ import 'svg.select.js';
import 'svg.draw.js'; import 'svg.draw.js';
import consts from './consts'; import consts from './consts';
import { Point, Equation, CuboidModel, Orientation, Edge } from './cuboid'; import { Equation, CuboidModel, Orientation, Edge } from './cuboid';
import { parsePoints, clamp } from './shared'; import { Point, parsePoints, clamp } from './shared';
// Update constructor // Update constructor
const originalDraw = SVG.Element.prototype.draw; const originalDraw = SVG.Element.prototype.draw;
@ -958,8 +958,12 @@ function getTopDown(edgeIndex: EdgeIndex): number[] {
}, },
paintOrientationLines() { paintOrientationLines() {
const fillColor = this.attr('fill'); // style has higher priority than attr, so then try to fetch it if exists
const strokeColor = this.attr('stroke'); // 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'; const selectedColor = this.attr('face-stroke') || '#b0bec5';
this.frontTopEdge.stroke({ color: selectedColor }); this.frontTopEdge.stroke({ color: selectedColor });
this.frontLeftEdge.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 // eslint-disable-next-line @typescript-eslint/no-var-requires
const DtsBundleWebpack = require('dts-bundle-webpack'); 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 = { const nodeConfig = {
target: 'node', target: 'node',
mode: 'production', mode: 'production',
@ -34,7 +51,7 @@ const nodeConfig = {
'@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-optional-chaining',
], ],
presets: [['@babel/preset-env'], ['@babel/typescript']], presets: [['@babel/preset-env', { targets: 'node > 10' }], '@babel/typescript'],
sourceType: 'unambiguous', sourceType: 'unambiguous',
}, },
}, },
@ -42,17 +59,7 @@ const nodeConfig = {
{ {
test: /\.(css|scss)$/, test: /\.(css|scss)$/,
exclude: /node_modules/, exclude: /node_modules/,
use: [ use: styleLoaders,
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
'sass-loader',
],
}, },
], ],
}, },
@ -96,15 +103,7 @@ const webConfig = {
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
plugins: ['@babel/plugin-proposal-class-properties'], plugins: ['@babel/plugin-proposal-class-properties'],
presets: [ presets: ['@babel/preset-env', '@babel/typescript'],
[
'@babel/preset-env',
{
targets: '> 2.5%', // https://github.com/browserslist/browserslist
},
],
['@babel/typescript'],
],
sourceType: 'unambiguous', sourceType: 'unambiguous',
}, },
}, },
@ -112,17 +111,7 @@ const webConfig = {
{ {
test: /\.scss$/, test: /\.scss$/,
exclude: /node_modules/, exclude: /node_modules/,
use: [ use: styleLoaders,
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
'sass-loader',
],
}, },
], ],
}, },

@ -3585,14 +3585,26 @@
} }
}, },
"enhanced-resolve": { "enhanced-resolve": {
"version": "4.1.0", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.1.0.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz",
"integrity": "sha512-F/7vkyTtyc/llOIn8oWclcB25KdRaiPBpZYDgJHgh/UHtpgT2p2eldQgtQnLtUvfMKPKxbRaQM/hHkvLHt1Vng==", "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==",
"dev": true, "dev": true,
"requires": { "requires": {
"graceful-fs": "^4.1.2", "graceful-fs": "^4.1.2",
"memory-fs": "^0.4.0", "memory-fs": "^0.5.0",
"tapable": "^1.0.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": { "errno": {
@ -5726,9 +5738,9 @@
} }
}, },
"interpret": { "interpret": {
"version": "1.2.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz",
"integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==",
"dev": true "dev": true
}, },
"invariant": { "invariant": {
@ -5740,12 +5752,6 @@
"loose-envify": "^1.0.0" "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": { "ip": {
"version": "1.1.5", "version": "1.1.5",
"resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz",
@ -6210,15 +6216,6 @@
"package-json": "^4.0.0" "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": { "levn": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz",
@ -6363,15 +6360,6 @@
"semver": "^5.6.0" "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": { "map-cache": {
"version": "0.2.2", "version": "0.2.2",
"resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz",
@ -6410,17 +6398,6 @@
"integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=", "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=",
"dev": true "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": { "memory-fs": {
"version": "0.4.1", "version": "0.4.1",
"resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz",
@ -6602,12 +6579,6 @@
"mime-db": "1.40.0" "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": { "minimalistic-assert": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz",
@ -7348,17 +7319,6 @@
"integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=",
"dev": true "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": { "os-tmpdir": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz",
@ -7375,24 +7335,12 @@
"os-tmpdir": "^1.0.0" "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": { "p-finally": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=",
"dev": true "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": { "p-limit": {
"version": "2.2.0", "version": "2.2.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.2.0.tgz",
@ -10772,112 +10720,46 @@
} }
}, },
"webpack-cli": { "webpack-cli": {
"version": "3.3.6", "version": "3.3.12",
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.6.tgz", "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.12.tgz",
"integrity": "sha512-0vEa83M7kJtxK/jUhlpZ27WHIOndz5mghWL2O53kiDoA9DIxSKnfqB92LoqEn77cT4f3H2cZm1BMEat/6AZz3A==", "integrity": "sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==",
"dev": true, "dev": true,
"requires": { "requires": {
"chalk": "2.4.2", "chalk": "^2.4.2",
"cross-spawn": "6.0.5", "cross-spawn": "^6.0.5",
"enhanced-resolve": "4.1.0", "enhanced-resolve": "^4.1.1",
"findup-sync": "3.0.0", "findup-sync": "^3.0.0",
"global-modules": "2.0.0", "global-modules": "^2.0.0",
"import-local": "2.0.0", "import-local": "^2.0.0",
"interpret": "1.2.0", "interpret": "^1.4.0",
"loader-utils": "1.2.3", "loader-utils": "^1.4.0",
"supports-color": "6.1.0", "supports-color": "^6.1.0",
"v8-compile-cache": "2.0.3", "v8-compile-cache": "^2.1.1",
"yargs": "13.2.4" "yargs": "^13.3.2"
}, },
"dependencies": { "dependencies": {
"ansi-regex": { "emojis-list": {
"version": "4.1.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz",
"integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==", "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==",
"dev": true "dev": true
}, },
"cliui": { "loader-utils": {
"version": "5.0.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz",
"integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==",
"dev": true, "dev": true,
"requires": { "requires": {
"string-width": "^3.1.0", "big.js": "^5.2.2",
"strip-ansi": "^5.2.0", "emojis-list": "^3.0.0",
"wrap-ansi": "^5.1.0" "json5": "^1.0.1"
} }
}, },
"get-caller-file": { "v8-compile-cache": {
"version": "2.0.5", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz",
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==",
"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==",
"dev": true "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", "author": "Intel",
"license": "MIT", "license": "MIT",
"browserslist": [
"Chrome >= 63",
"Firefox > 58",
"not IE 11",
"> 2%"
],
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.13.16", "@babel/cli": "^7.13.16",
"@babel/core": "^7.5.5", "@babel/core": "^7.5.5",
@ -34,7 +40,7 @@
"style-loader": "^1.0.0", "style-loader": "^1.0.0",
"typescript": "^3.5.3", "typescript": "^3.5.3",
"webpack": "^4.44.2", "webpack": "^4.44.2",
"webpack-cli": "^3.3.6", "webpack-cli": "^3.3.12",
"webpack-dev-server": "^3.11.0" "webpack-dev-server": "^3.11.0"
}, },
"dependencies": { "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 // eslint-disable-next-line @typescript-eslint/no-var-requires
const DtsBundleWebpack = require('dts-bundle-webpack'); 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 = { const nodeConfig = {
target: 'node', target: 'node',
mode: 'production', mode: 'production',
@ -34,7 +51,7 @@ const nodeConfig = {
'@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-class-properties',
'@babel/plugin-proposal-optional-chaining', '@babel/plugin-proposal-optional-chaining',
], ],
presets: [['@babel/preset-env'], ['@babel/typescript']], presets: [['@babel/preset-env', { targets: 'node > 10' }], '@babel/typescript'],
sourceType: 'unambiguous', sourceType: 'unambiguous',
}, },
}, },
@ -42,17 +59,7 @@ const nodeConfig = {
{ {
test: /\.(css|scss)$/, test: /\.(css|scss)$/,
exclude: /node_modules/, exclude: /node_modules/,
use: [ use: styleLoaders,
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
'sass-loader',
],
}, },
], ],
}, },
@ -96,15 +103,7 @@ const webConfig = {
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
plugins: ['@babel/plugin-proposal-class-properties'], plugins: ['@babel/plugin-proposal-class-properties'],
presets: [ presets: ['@babel/preset-env', '@babel/typescript'],
[
'@babel/preset-env',
{
targets: '> 2.5%', // https://github.com/browserslist/browserslist
},
],
['@babel/typescript'],
],
sourceType: 'unambiguous', sourceType: 'unambiguous',
}, },
}, },
@ -112,17 +111,7 @@ const webConfig = {
{ {
test: /\.scss$/, test: /\.scss$/,
exclude: /node_modules/, exclude: /node_modules/,
use: [ use: styleLoaders,
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
},
},
'postcss-loader',
'sass-loader',
],
}, },
], ],
}, },

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

@ -1,6 +1,6 @@
{ {
"name": "cvat-core", "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", "description": "Part of Computer Vision Tool which presents an interface for client-side integration",
"main": "babel.config.js", "main": "babel.config.js",
"scripts": { "scripts": {
@ -11,6 +11,12 @@
}, },
"author": "Intel", "author": "Intel",
"license": "MIT", "license": "MIT",
"browserslist": [
"Chrome >= 63",
"Firefox > 58",
"not IE 11",
"> 2%"
],
"devDependencies": { "devDependencies": {
"@babel/cli": "^7.4.4", "@babel/cli": "^7.4.4",
"@babel/core": "^7.4.4", "@babel/core": "^7.4.4",
@ -33,7 +39,7 @@
"webpack-cli": "^3.3.2" "webpack-cli": "^3.3.2"
}, },
"dependencies": { "dependencies": {
"axios": "^0.21.1", "axios": "^0.21.3",
"browser-or-node": "^1.2.1", "browser-or-node": "^1.2.1",
"cvat-data": "../cvat-data", "cvat-data": "../cvat-data",
"detect-browser": "^5.2.0", "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 // SPDX-License-Identifier: MIT
@ -8,8 +8,9 @@
const AnnotationsSaver = require('./annotations-saver'); const AnnotationsSaver = require('./annotations-saver');
const AnnotationsHistory = require('./annotations-history'); const AnnotationsHistory = require('./annotations-history');
const { checkObjectType } = require('./common'); const { checkObjectType } = require('./common');
const { Task } = require('./session'); const { Project } = require('./project');
const { Loader, Dumper } = require('./annotation-formats'); const { Task, Job } = require('./session');
const { Loader } = require('./annotation-formats');
const { ScriptingError, DataError, ArgumentError } = require('./exceptions'); const { ScriptingError, DataError, ArgumentError } = require('./exceptions');
const jobCache = new WeakMap(); const jobCache = new WeakMap();
@ -50,6 +51,7 @@
stopFrame, stopFrame,
frameMeta, frameMeta,
}); });
// eslint-disable-next-line no-unsanitized/method
collection.import(rawAnnotations); collection.import(rawAnnotations);
const saver = new AnnotationsSaver(rawAnnotations.version, collection, session); const saver = new AnnotationsSaver(rawAnnotations.version, collection, session);
@ -232,27 +234,12 @@
await serverProxy.annotations.uploadAnnotations(sessionType, session.id, file, loader.name); 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) { function importAnnotations(session, data) {
const sessionType = session instanceof Task ? 'task' : 'job'; const sessionType = session instanceof Task ? 'task' : 'job';
const cache = getCache(sessionType); const cache = getCache(sessionType);
if (cache.has(session)) { if (cache.has(session)) {
// eslint-disable-next-line no-unsanitized/method
return cache.get(session).collection.import(data); 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')) { if (!(format instanceof String || typeof format === 'string')) {
throw new ArgumentError('Format must be a string'); throw new ArgumentError('Format must be a string');
} }
if (!(session instanceof Task)) { if (!(instance instanceof Task || instance instanceof Project || instance instanceof Job)) {
throw new ArgumentError('A dataset can only be created from a task'); 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; 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; return result;
} }
@ -367,7 +363,6 @@
annotationsStatistics, annotationsStatistics,
selectObject, selectObject,
uploadAnnotations, uploadAnnotations,
dumpAnnotations,
importAnnotations, importAnnotations,
exportAnnotations, exportAnnotations,
exportDataset, exportDataset,

@ -16,13 +16,20 @@
camelToSnake, camelToSnake,
} = require('./common'); } = require('./common');
const { TaskStatus, TaskMode, DimensionType } = require('./enums'); const {
TaskStatus,
TaskMode,
DimensionType,
CloudStorageProviderType,
CloudStorageCredentialsType,
} = require('./enums');
const User = require('./user'); const User = require('./user');
const { AnnotationFormats } = require('./annotation-formats'); const { AnnotationFormats } = require('./annotation-formats');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { Task } = require('./session'); const { Task } = require('./session');
const { Project } = require('./project'); const { Project } = require('./project');
const { CloudStorage } = require('./cloud-storage');
function implementAPI(cvat) { function implementAPI(cvat) {
cvat.plugins.list.implementation = PluginRegistry.list; cvat.plugins.list.implementation = PluginRegistry.list;
@ -262,6 +269,49 @@
cvat.projects.searchNames.implementation = async (search, limit) => serverProxy.projects.searchNames(search, limit); 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; return cvat;
} }

@ -18,9 +18,12 @@ function build() {
const Review = require('./review'); const Review = require('./review');
const { Job, Task } = require('./session'); const { Job, Task } = require('./session');
const { Project } = require('./project'); const { Project } = require('./project');
const implementProject = require('./project-implementation');
const { Attribute, Label } = require('./labels'); const { Attribute, Label } = require('./labels');
const MLModel = require('./ml-model'); const MLModel = require('./ml-model');
const { FrameData } = require('./frames'); const { FrameData } = require('./frames');
const { CloudStorage } = require('./cloud-storage');
const enums = require('./enums'); const enums = require('./enums');
@ -747,6 +750,41 @@ function build() {
PluginError, PluginError,
ServerError, 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 is used for access to classes
* @namespace classes * @namespace classes
@ -754,7 +792,7 @@ function build() {
*/ */
classes: { classes: {
User, User,
Project, Project: implementProject(Project),
Task, Task,
Job, Job,
Log, Log,
@ -767,6 +805,7 @@ function build() {
Issue, Issue,
Review, Review,
FrameData, FrameData,
CloudStorage,
}, },
}; };
@ -779,6 +818,7 @@ function build() {
cvat.lambda = Object.freeze(cvat.lambda); cvat.lambda = Object.freeze(cvat.lambda);
cvat.client = Object.freeze(cvat.client); cvat.client = Object.freeze(cvat.client);
cvat.enums = Object.freeze(cvat.enums); cvat.enums = Object.freeze(cvat.enums);
cvat.cloudStorages = Object.freeze(cvat.cloudStorages);
const implementAPI = require('./api-implementation'); 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 // SPDX-License-Identifier: MIT
@ -333,6 +333,36 @@
'#733380', '#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 = { module.exports = {
ShareFileType, ShareFileType,
TaskStatus, TaskStatus,
@ -348,5 +378,7 @@
colors, colors,
Source, Source,
DimensionType, DimensionType,
CloudStorageProviderType,
CloudStorageCredentialsType,
}; };
})(); })();

@ -14,6 +14,10 @@ class MLModel {
this._framework = data.framework; this._framework = data.framework;
this._description = data.description; this._description = data.description;
this._type = data.type; this._type = data.type;
this._tip = {
message: data.help_message,
gif: data.animated_gif,
};
this._params = { this._params = {
canvas: { canvas: {
minPosVertices: data.min_pos_points, minPosVertices: data.min_pos_points,
@ -84,6 +88,25 @@ class MLModel {
canvas: { ...this._params.canvas }, 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; 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 PluginRegistry = require('./plugins');
const serverProxy = require('./server-proxy');
const { ArgumentError } = require('./exceptions'); const { ArgumentError } = require('./exceptions');
const { Task } = require('./session'); const { Task } = require('./session');
const { Label } = require('./labels'); const { Label } = require('./labels');
const { getPreview } = require('./frames');
const User = require('./user'); const User = require('./user');
/** /**
@ -36,6 +34,7 @@
task_subsets: undefined, task_subsets: undefined,
training_project: undefined, training_project: undefined,
task_ids: undefined, task_ids: undefined,
dimension: undefined,
}; };
for (const property in data) { for (const property in data) {
@ -155,7 +154,7 @@
/** /**
* @name createdDate * @name createdDate
* @type {string} * @type {string}
* @memberof module:API.cvat.classes.Task * @memberof module:API.cvat.classes.Project
* @readonly * @readonly
* @instance * @instance
*/ */
@ -165,13 +164,24 @@
/** /**
* @name updatedDate * @name updatedDate
* @type {string} * @type {string}
* @memberof module:API.cvat.classes.Task * @memberof module:API.cvat.classes.Project
* @readonly * @readonly
* @instance * @instance
*/ */
updatedDate: { updatedDate: {
get: () => data.updated_date, 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. * After project has been created value can be appended only.
* @name labels * @name labels
@ -203,7 +213,7 @@
}, },
}, },
/** /**
* Tasks linked with the project * Tasks related with the project
* @name tasks * @name tasks
* @type {module:API.cvat.classes.Task[]} * @type {module:API.cvat.classes.Task[]}
* @memberof module:API.cvat.classes.Project * @memberof module:API.cvat.classes.Project
@ -214,7 +224,7 @@
get: () => [...data.tasks], get: () => [...data.tasks],
}, },
/** /**
* Subsets array for linked tasks * Subsets array for related tasks
* @name subsets * @name subsets
* @type {string[]} * @type {string[]}
* @memberof module:API.cvat.classes.Project * @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 * @method delete
* @memberof module:API.cvat.classes.Project * @memberof module:API.cvat.classes.Project
* @readonly * @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 = { module.exports = {
Project, 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) { function exportDataset(instanceType) {
const { backendAPI } = config; return async function (id, format, name, saveImages) {
let url = `${backendAPI}/tasks/${id}/dataset?format=${format}`; 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) => { return new Promise((resolve, reject) => {
async function request() { async function request() {
try { Axios.get(`${url}`, {
const response = await Axios.get(`${url}`, {
proxy: config.proxy, proxy: config.proxy,
}); })
if (response.status === 202) { .then((response) => {
setTimeout(request, 3000); if (response.status === 202) {
} else { setTimeout(request, 3000);
url = `${url}&action=download`; } else {
resolve(url); query = `${query}&action=download`;
} url = `${baseURL}?${query}`;
} catch (errorData) { resolve(url);
reject(generateError(errorData)); }
})
.catch((errorData) => {
reject(generateError(errorData));
});
} }
}
setTimeout(request); setTimeout(request);
}); });
};
} }
async function exportTask(id) { 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( Object.defineProperties(
this, this,
Object.freeze({ Object.freeze({
@ -1199,6 +1324,7 @@
save: saveProject, save: saveProject,
create: createProject, create: createProject,
delete: deleteProject, delete: deleteProject,
exportDataset: exportDataset('projects'),
}), }),
writable: false, writable: false,
}, },
@ -1209,7 +1335,7 @@
saveTask, saveTask,
createTask, createTask,
deleteTask, deleteTask,
exportDataset, exportDataset: exportDataset('tasks'),
exportTask, exportTask,
importTask, importTask,
}), }),
@ -1297,6 +1423,19 @@
}), }),
writable: false, 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; return result;
}, },
async dump(dumper, name = null) {
const result = await PluginRegistry.apiWrapper.call(
this,
prototype.annotations.dump,
dumper,
name,
);
return result;
},
async statistics() { async statistics() {
const result = await PluginRegistry.apiWrapper.call(this, prototype.annotations.statistics); const result = await PluginRegistry.apiWrapper.call(this, prototype.annotations.statistics);
return result; return result;
@ -148,11 +138,13 @@
return result; return result;
}, },
async exportDataset(format) { async exportDataset(format, saveImages, customName = '') {
const result = await PluginRegistry.apiWrapper.call( const result = await PluginRegistry.apiWrapper.call(
this, this,
prototype.annotations.exportDataset, prototype.annotations.exportDataset,
format, format,
saveImages,
customName,
); );
return result; return result;
}, },
@ -329,21 +321,6 @@
* @instance * @instance
* @async * @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. * Collect short statistics about a task or a job.
* @method statistics * @method statistics
@ -877,7 +854,6 @@
get: Object.getPrototypeOf(this).annotations.get.bind(this), get: Object.getPrototypeOf(this).annotations.get.bind(this),
put: Object.getPrototypeOf(this).annotations.put.bind(this), put: Object.getPrototypeOf(this).annotations.put.bind(this),
save: Object.getPrototypeOf(this).annotations.save.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), merge: Object.getPrototypeOf(this).annotations.merge.bind(this),
split: Object.getPrototypeOf(this).annotations.split.bind(this), split: Object.getPrototypeOf(this).annotations.split.bind(this),
group: Object.getPrototypeOf(this).annotations.group.bind(this), group: Object.getPrototypeOf(this).annotations.group.bind(this),
@ -1036,6 +1012,7 @@
use_cache: undefined, use_cache: undefined,
copy_data: undefined, copy_data: undefined,
dimension: undefined, dimension: undefined,
cloud_storage_id: undefined,
}; };
const updatedFields = new FieldUpdateTrigger({ const updatedFields = new FieldUpdateTrigger({
@ -1397,7 +1374,7 @@
get: () => [...data.jobs], get: () => [...data.jobs],
}, },
/** /**
* List of files from shared resource * List of files from shared resource or list of cloud storage files
* @name serverFiles * @name serverFiles
* @type {string[]} * @type {string[]}
* @memberof module:API.cvat.classes.Task * @memberof module:API.cvat.classes.Task
@ -1559,6 +1536,15 @@
*/ */
get: () => data.dimension, get: () => data.dimension,
}, },
/**
* @name cloudStorageId
* @type {integer|null}
* @memberof module:API.cvat.classes.Task
* @instance
*/
cloudStorageId: {
get: () => data.cloud_storage_id,
},
_internalData: { _internalData: {
get: () => data, get: () => data,
}, },
@ -1575,7 +1561,6 @@
get: Object.getPrototypeOf(this).annotations.get.bind(this), get: Object.getPrototypeOf(this).annotations.get.bind(this),
put: Object.getPrototypeOf(this).annotations.put.bind(this), put: Object.getPrototypeOf(this).annotations.put.bind(this),
save: Object.getPrototypeOf(this).annotations.save.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), merge: Object.getPrototypeOf(this).annotations.merge.bind(this),
split: Object.getPrototypeOf(this).annotations.split.bind(this), split: Object.getPrototypeOf(this).annotations.split.bind(this),
group: Object.getPrototypeOf(this).annotations.group.bind(this), group: Object.getPrototypeOf(this).annotations.group.bind(this),
@ -1715,7 +1700,6 @@
selectObject, selectObject,
annotationsStatistics, annotationsStatistics,
uploadAnnotations, uploadAnnotations,
dumpAnnotations,
importAnnotations, importAnnotations,
exportAnnotations, exportAnnotations,
exportDataset, exportDataset,
@ -1948,13 +1932,8 @@
return result; return result;
}; };
Job.prototype.annotations.dump.implementation = async function (dumper, name) { Job.prototype.annotations.exportDataset.implementation = async function (format, saveImages, customName) {
const result = await dumpAnnotations(this, name, dumper); const result = await exportDataset(this.task, format, customName, saveImages);
return result;
};
Job.prototype.annotations.exportDataset.implementation = async function (format) {
const result = await exportDataset(this.task, format);
return result; return result;
}; };
@ -2093,6 +2072,9 @@
if (typeof this.copyData !== 'undefined') { if (typeof this.copyData !== 'undefined') {
taskDataSpec.copy_data = this.copyData; 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); const task = await serverProxy.tasks.createTask(taskSpec, taskDataSpec, onUpdate);
return new Task(task); return new Task(task);
@ -2252,11 +2234,6 @@
return result; 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) { Task.prototype.annotations.import.implementation = function (data) {
const result = importAnnotations(this, data); const result = importAnnotations(this, data);
return result; return result;
@ -2267,8 +2244,8 @@
return result; return result;
}; };
Task.prototype.annotations.exportDataset.implementation = async function (format) { Task.prototype.annotations.exportDataset.implementation = async function (format, saveImages, customName) {
const result = await exportDataset(this, format); const result = await exportDataset(this, format, customName, saveImages);
return result; 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 = { module.exports = {
tasksDummyData, tasksDummyData,
projectsDummyData, projectsDummyData,
@ -2557,4 +2607,5 @@ module.exports = {
jobAnnotationsDummyData, jobAnnotationsDummyData,
frameMetaDummyData, frameMetaDummyData,
formatsDummyData, formatsDummyData,
cloudStoragesDummyData,
}; };

@ -12,6 +12,7 @@ const {
taskAnnotationsDummyData, taskAnnotationsDummyData,
jobAnnotationsDummyData, jobAnnotationsDummyData,
frameMetaDummyData, frameMetaDummyData,
cloudStoragesDummyData,
} = require('./dummy-data.mock'); } = require('./dummy-data.mock');
function QueryStringToJSON(query, ignoreList = []) { function QueryStringToJSON(query, ignoreList = []) {
@ -318,6 +319,63 @@ class ServerProxy {
return null; 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( Object.defineProperties(
this, this,
Object.freeze({ Object.freeze({
@ -384,6 +442,16 @@ class ServerProxy {
getAnnotations, getAnnotations,
}, },
}, },
cloudStorages: {
value: Object.freeze({
get: getCloudStorages,
update: updateCloudStorage,
create: createCloudStorage,
delete: deleteCloudStorage,
}),
writable: false,
},
}), }),
); );
} }

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

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

@ -3,6 +3,19 @@
"version": "1.0.2", "version": "1.0.2",
"description": "", "description": "",
"main": "src/js/cvat-data.js", "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": { "devDependencies": {
"@babel/cli": "^7.13.16", "@babel/cli": "^7.13.16",
"@babel/core": "^7.4.4", "@babel/core": "^7.4.4",
@ -22,13 +35,6 @@
}, },
"dependencies": { "dependencies": {
"async-mutex": "^0.3.1", "async-mutex": "^0.3.1",
"jszip": "3.7.0" "jszip": "3.7.1"
}, }
"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"
} }

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

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

@ -1,6 +1,6 @@
{ {
"name": "cvat-ui", "name": "cvat-ui",
"version": "1.21.1", "version": "1.23.1",
"description": "CVAT single-page application", "description": "CVAT single-page application",
"main": "src/index.tsx", "main": "src/index.tsx",
"scripts": { "scripts": {
@ -11,6 +11,12 @@
"lint": "eslint './src/**/*.{ts,tsx}'", "lint": "eslint './src/**/*.{ts,tsx}'",
"lint:fix": "eslint './src/**/*.{ts,tsx}' --fix" "lint:fix": "eslint './src/**/*.{ts,tsx}' --fix"
}, },
"browserslist": [
"Chrome >= 63",
"Firefox > 58",
"not IE 11",
"> 2%"
],
"author": "Intel", "author": "Intel",
"license": "MIT", "license": "MIT",
"devDependencies": { "devDependencies": {
@ -49,18 +55,18 @@
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^4.6.2", "@ant-design/icons": "^4.6.2",
"@types/lodash": "^4.14.170", "@types/lodash": "^4.14.172",
"@types/platform": "^1.3.3", "@types/platform": "^1.3.4",
"@types/react": "^16.14.10", "@types/react": "^16.14.12",
"@types/react-color": "^3.0.4", "@types/react-color": "^3.0.5",
"@types/react-dom": "^16.9.13", "@types/react-dom": "^16.9.14",
"@types/react-redux": "^7.1.16", "@types/react-redux": "^7.1.18",
"@types/react-resizable": "^1.7.2", "@types/react-resizable": "^1.7.3",
"@types/react-router": "^5.1.15", "@types/react-router": "^5.1.16",
"@types/react-router-dom": "^5.1.7", "@types/react-router-dom": "^5.1.8",
"@types/react-share": "^3.0.3", "@types/react-share": "^3.0.3",
"@types/redux-logger": "^3.0.8", "@types/redux-logger": "^3.0.9",
"@types/resize-observer-browser": "^0.1.5", "@types/resize-observer-browser": "^0.1.6",
"antd": "^4.13.0", "antd": "^4.13.0",
"copy-to-clipboard": "^3.3.1", "copy-to-clipboard": "^3.3.1",
"cvat-canvas": "file:../cvat-canvas", "cvat-canvas": "file:../cvat-canvas",
@ -77,7 +83,7 @@
"react": "^16.14.0", "react": "^16.14.0",
"react-awesome-query-builder": "^3.0.0", "react-awesome-query-builder": "^3.0.0",
"react-color": "^2.19.3", "react-color": "^2.19.3",
"react-cookie": "^4.0.3", "react-cookie": "^4.1.1",
"react-dom": "^16.14.0", "react-dom": "^16.14.0",
"react-moment": "^1.1.1", "react-moment": "^1.1.1",
"react-redux": "^7.2.4", "react-redux": "^7.2.4",
@ -85,7 +91,7 @@
"react-router": "^5.1.0", "react-router": "^5.1.0",
"react-router-dom": "^5.1.0", "react-router-dom": "^5.1.0",
"react-share": "^3.0.1", "react-share": "^3.0.1",
"redux": "^4.1.0", "redux": "^4.1.1",
"redux-devtools-extension": "^2.13.9", "redux-devtools-extension": "^2.13.9",
"redux-logger": "^3.0.6", "redux-logger": "^3.0.6",
"redux-thunk": "^2.3.0" "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 // SPDX-License-Identifier: MIT
import { AnyAction } from 'redux'; import { AnyAction } from 'redux';
import { GridColor, ColorBy, SettingsState } from 'reducers/interfaces'; import {
GridColor, ColorBy, SettingsState, ToolsBlockerState,
} from 'reducers/interfaces';
export enum SettingsActionTypes { export enum SettingsActionTypes {
SWITCH_ROTATE_ALL = 'SWITCH_ROTATE_ALL', SWITCH_ROTATE_ALL = 'SWITCH_ROTATE_ALL',
@ -34,6 +36,7 @@ export enum SettingsActionTypes {
CHANGE_CANVAS_BACKGROUND_COLOR = 'CHANGE_CANVAS_BACKGROUND_COLOR', CHANGE_CANVAS_BACKGROUND_COLOR = 'CHANGE_CANVAS_BACKGROUND_COLOR',
SWITCH_SETTINGS_DIALOG = 'SWITCH_SETTINGS_DIALOG', SWITCH_SETTINGS_DIALOG = 'SWITCH_SETTINGS_DIALOG',
SET_SETTINGS = 'SET_SETTINGS', SET_SETTINGS = 'SET_SETTINGS',
SWITCH_TOOLS_BLOCKER_STATE = 'SWITCH_TOOLS_BLOCKER_STATE',
} }
export function changeShapesOpacity(opacity: number): AnyAction { 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 { export function setSettings(settings: Partial<SettingsState>): AnyAction {
return { return {
type: SettingsActionTypes.SET_SETTINGS, type: SettingsActionTypes.SET_SETTINGS,

@ -18,12 +18,6 @@ export enum TasksActionTypes {
LOAD_ANNOTATIONS = 'LOAD_ANNOTATIONS', LOAD_ANNOTATIONS = 'LOAD_ANNOTATIONS',
LOAD_ANNOTATIONS_SUCCESS = 'LOAD_ANNOTATIONS_SUCCESS', LOAD_ANNOTATIONS_SUCCESS = 'LOAD_ANNOTATIONS_SUCCESS',
LOAD_ANNOTATIONS_FAILED = 'LOAD_ANNOTATIONS_FAILED', 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 = 'DELETE_TASK',
DELETE_TASK_SUCCESS = 'DELETE_TASK_SUCCESS', DELETE_TASK_SUCCESS = 'DELETE_TASK_SUCCESS',
DELETE_TASK_FAILED = 'DELETE_TASK_FAILED', 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 { function loadAnnotations(task: any, loader: any): AnyAction {
const action = { const action = {
type: TasksActionTypes.LOAD_ANNOTATIONS, 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 { function exportTask(taskID: number): AnyAction {
const action = { const action = {
type: TasksActionTypes.EXPORT_TASK, type: TasksActionTypes.EXPORT_TASK,

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

@ -106,6 +106,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
showObjectsTextAlways, showObjectsTextAlways,
workspace, workspace,
showProjections, showProjections,
selectedOpacity,
} = this.props; } = this.props;
const { canvasInstance } = this.props as { canvasInstance: Canvas }; const { canvasInstance } = this.props as { canvasInstance: Canvas };
@ -121,6 +122,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
forceDisableEditing: workspace === Workspace.REVIEW_WORKSPACE, forceDisableEditing: workspace === Workspace.REVIEW_WORKSPACE,
intelligentPolygonCrop, intelligentPolygonCrop,
showProjections, showProjections,
creationOpacity: selectedOpacity,
}); });
this.initialSetup(); this.initialSetup();
@ -166,7 +168,8 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
prevProps.showObjectsTextAlways !== showObjectsTextAlways || prevProps.showObjectsTextAlways !== showObjectsTextAlways ||
prevProps.automaticBordering !== automaticBordering || prevProps.automaticBordering !== automaticBordering ||
prevProps.showProjections !== showProjections || prevProps.showProjections !== showProjections ||
prevProps.intelligentPolygonCrop !== intelligentPolygonCrop prevProps.intelligentPolygonCrop !== intelligentPolygonCrop ||
prevProps.selectedOpacity !== selectedOpacity
) { ) {
canvasInstance.configure({ canvasInstance.configure({
undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE, undefinedAttrValue: consts.UNDEFINED_ATTRIBUTE_VALUE,
@ -174,6 +177,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
autoborders: automaticBordering, autoborders: automaticBordering,
showProjections, showProjections,
intelligentPolygonCrop, intelligentPolygonCrop,
creationOpacity: selectedOpacity,
}); });
} }
@ -198,7 +202,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
canvasInstance.activate(null); canvasInstance.activate(null);
const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`); const el = window.document.getElementById(`cvat_canvas_shape_${prevProps.activatedStateID}`);
if (el) { 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) { if (gridPattern) {
gridPattern.style.stroke = gridColor.toLowerCase(); 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'); const backgroundElement = window.document.getElementById('cvat_canvas_background');
if (backgroundElement) { if (backgroundElement) {
backgroundElement.style.filter = const filter = `brightness(${brightnessLevel}) contrast(${contrastLevel}) saturate(${saturationLevel})`;
`brightness(${brightnessLevel / 100})` + backgroundElement.style.filter = filter;
`contrast(${contrastLevel / 100})` +
`saturate(${saturationLevel / 100})`;
} }
} }
@ -619,7 +621,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`); const el = window.document.getElementById(`cvat_canvas_shape_${activatedStateID}`);
if (el) { 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 }); 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 }); (shapeView as any).instance.stroke({ color: outlined ? outlineColor : shapeColor });
} }
} }
@ -710,17 +712,15 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
} }
if (gridPattern) { if (gridPattern) {
gridPattern.style.stroke = gridColor.toLowerCase(); gridPattern.style.stroke = gridColor.toLowerCase();
gridPattern.style.opacity = `${gridOpacity / 100}`; gridPattern.style.opacity = `${gridOpacity}`;
} }
canvasInstance.grid(gridSize, gridSize); canvasInstance.grid(gridSize, gridSize);
// Filters // Filters
const backgroundElement = window.document.getElementById('cvat_canvas_background'); const backgroundElement = window.document.getElementById('cvat_canvas_background');
if (backgroundElement) { if (backgroundElement) {
backgroundElement.style.filter = const filter = `brightness(${brightnessLevel}) contrast(${contrastLevel}) saturate(${saturationLevel})`;
`brightness(${brightnessLevel / 100})` + backgroundElement.style.filter = filter;
`contrast(${contrastLevel / 100})` +
`saturate(${saturationLevel / 100})`;
} }
const canvasWrapperElement = window.document const canvasWrapperElement = window.document
@ -823,7 +823,7 @@ export default class CanvasWrapperComponent extends React.PureComponent<Props> {
<ContextImage /> <ContextImage />
<Dropdown trigger='click' placement='topCenter' overlay={<ImageSetupsContent />}> <Dropdown trigger={['click']} placement='topCenter' overlay={<ImageSetupsContent />}>
<UpOutlined className='cvat-canvas-image-setups-trigger' /> <UpOutlined className='cvat-canvas-image-setups-trigger' />
</Dropdown> </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 openCVWrapper from 'utils/opencv-wrapper/opencv-wrapper';
import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors'; import { IntelligentScissors } from 'utils/opencv-wrapper/intelligent-scissors';
import { import {
CombinedState, ActiveControl, OpenCVTool, ObjectType, ShapeType, CombinedState, ActiveControl, OpenCVTool, ObjectType, ShapeType, ToolsBlockerState,
} from 'reducers/interfaces'; } from 'reducers/interfaces';
import { import {
interactWithCanvas, interactWithCanvas,
@ -34,6 +34,7 @@ import ApproximationAccuracy, {
thresholdFromAccuracy, thresholdFromAccuracy,
} from 'components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy'; } from 'components/annotation-page/standard-workspace/controls-side-bar/approximation-accuracy';
import { ImageProcessing } from 'utils/opencv-wrapper/opencv-interfaces'; import { ImageProcessing } from 'utils/opencv-wrapper/opencv-interfaces';
import { switchToolsBlockerState } from 'actions/settings-actions';
import withVisibilityHandling from './handle-popover-visibility'; import withVisibilityHandling from './handle-popover-visibility';
interface Props { interface Props {
@ -46,6 +47,8 @@ interface Props {
curZOrder: number; curZOrder: number;
defaultApproxPolyAccuracy: number; defaultApproxPolyAccuracy: number;
frameData: any; frameData: any;
toolsBlockerState: ToolsBlockerState;
activeControl: ActiveControl;
} }
interface DispatchToProps { interface DispatchToProps {
@ -54,6 +57,7 @@ interface DispatchToProps {
createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void; createAnnotations(sessionInstance: any, frame: number, statesToCreate: any[]): void;
fetchAnnotations(): void; fetchAnnotations(): void;
changeFrame(toFrame: number, fillBuffer?: boolean, frameStep?: number, forceUpdate?: boolean):void; changeFrame(toFrame: number, fillBuffer?: boolean, frameStep?: number, forceUpdate?: boolean):void;
onSwitchToolsBlockerState(toolsBlockerState: ToolsBlockerState):void;
} }
interface State { interface State {
@ -87,12 +91,13 @@ function mapStateToProps(state: CombinedState): Props {
}, },
}, },
settings: { settings: {
workspace: { defaultApproxPolyAccuracy }, workspace: { defaultApproxPolyAccuracy, toolsBlockerState },
}, },
} = state; } = state;
return { return {
isActivated: activeControl === ActiveControl.OPENCV_TOOLS, isActivated: activeControl === ActiveControl.OPENCV_TOOLS,
activeControl,
canvasInstance: canvasInstance as Canvas, canvasInstance: canvasInstance as Canvas,
defaultApproxPolyAccuracy, defaultApproxPolyAccuracy,
jobInstance, jobInstance,
@ -101,6 +106,7 @@ function mapStateToProps(state: CombinedState): Props {
states, states,
frame, frame,
frameData, frameData,
toolsBlockerState,
}; };
} }
@ -110,6 +116,7 @@ const mapDispatchToProps = {
fetchAnnotations: fetchAnnotationsAsync, fetchAnnotations: fetchAnnotationsAsync,
createAnnotations: createAnnotationsAsync, createAnnotations: createAnnotationsAsync,
changeFrame: changeFrameAsync, changeFrame: changeFrameAsync,
onSwitchToolsBlockerState: switchToolsBlockerState,
}; };
class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps, State> { 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 { public componentDidUpdate(prevProps: Props, prevState: State): void {
const { approxPolyAccuracy } = this.state; const { approxPolyAccuracy } = this.state;
const { isActivated, defaultApproxPolyAccuracy, canvasInstance } = this.props; const {
isActivated, defaultApproxPolyAccuracy, canvasInstance, toolsBlockerState,
} = this.props;
if (!prevProps.isActivated && isActivated) { if (!prevProps.isActivated && isActivated) {
// reset flags & states before using a tool // reset flags & states before using a tool
this.latestPoints = []; this.latestPoints = [];
@ -150,6 +160,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
approxPolyAccuracy: defaultApproxPolyAccuracy, approxPolyAccuracy: defaultApproxPolyAccuracy,
}); });
if (this.activeTool) { if (this.activeTool) {
this.activeTool.switchBlockMode(toolsBlockerState.algorithmsLocked);
this.activeTool.reset(); 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 { public componentWillUnmount(): void {
@ -180,7 +195,7 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
private interactionListener = async (e: Event): Promise<void> => { private interactionListener = async (e: Event): Promise<void> => {
const { approxPolyAccuracy } = this.state; const { approxPolyAccuracy } = this.state;
const { const {
createAnnotations, isActivated, jobInstance, frame, labels, curZOrder, canvasInstance, createAnnotations, isActivated, jobInstance, frame, labels, curZOrder, canvasInstance, toolsBlockerState,
} = this.props; } = this.props;
const { activeLabelID } = this.state; const { activeLabelID } = this.state;
if (!isActivated || !this.activeTool) { if (!isActivated || !this.activeTool) {
@ -191,27 +206,41 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
shapesUpdated, isDone, threshold, shapes, shapesUpdated, isDone, threshold, shapes,
} = (e as CustomEvent).detail; } = (e as CustomEvent).detail;
const pressedPoints = convertShapesForInteractor(shapes, 0).flat(); const pressedPoints = convertShapesForInteractor(shapes, 0).flat();
try { try {
if (shapesUpdated) { if (shapesUpdated) {
this.latestPoints = await this.runCVAlgorithm(pressedPoints, threshold); this.latestPoints = await this.runCVAlgorithm(pressedPoints,
const approx = openCVWrapper.contours.approxPoly( toolsBlockerState.algorithmsLocked ? 0 : threshold);
this.latestPoints, let points = [];
thresholdFromAccuracy(approxPolyAccuracy), if (toolsBlockerState.algorithmsLocked && this.latestPoints.length > 2) {
false, // 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({ canvasInstance.interact({
enabled: true, enabled: true,
intermediateShape: { intermediateShape: {
shapeType: ShapeType.POLYGON, shapeType: ShapeType.POLYGON,
points: approx.flat(), points: points.flat(),
}, },
}); });
} }
if (isDone) { if (isDone) {
// need to recalculate without the latest sliding point // 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({ const finalObject = new core.classes.ObjectState({
frame, frame,
objectType: ObjectType.SHAPE, objectType: ObjectType.SHAPE,
@ -229,6 +258,22 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
notification.error({ notification.error({
description: error.toString(), description: error.toString(),
message: 'OpenCV.js processing error occured', 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({ notification.error({
description: error.toString(), description: error.toString(),
message: 'OpenCV.js processing error occured', message: 'OpenCV.js processing error occured',
className: 'cvat-notification-notice-opencv-processing-error',
}); });
} finally { } finally {
this.disableCanvasForceUpdate(); this.disableCanvasForceUpdate();
@ -277,22 +323,24 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
if (!canvas) { if (!canvas) {
throw new Error('Element #cvat_canvas_background was not found'); throw new Error('Element #cvat_canvas_background was not found');
} }
if (!this.activeTool || pressedPoints.length === 0) return [];
const { width, height } = canvas; const { width, height } = canvas;
const context = canvas.getContext('2d'); const context = canvas.getContext('2d');
if (!context) { if (!context) {
throw new Error('Canvas context is empty'); throw new Error('Canvas context is empty');
} }
let imageData;
const [x, y] = pressedPoints.slice(-2); const [x, y] = pressedPoints.slice(-2);
const startX = Math.round(Math.max(0, x - threshold)); const startX = Math.round(Math.max(0, x - threshold));
const startY = Math.round(Math.max(0, y - threshold)); const startY = Math.round(Math.max(0, y - threshold));
const segmentWidth = Math.min(2 * threshold, width - startX); if (threshold !== 0) {
const segmentHeight = Math.min(2 * threshold, height - startY); const segmentWidth = Math.min(2 * threshold, width - startX);
const imageData = context.getImageData(startX, startY, segmentWidth, segmentHeight); const segmentHeight = Math.min(2 * threshold, height - startY);
imageData = context.getImageData(startX, startY, segmentWidth, segmentHeight);
if (!this.activeTool) return []; } else {
imageData = context.getImageData(0, 0, width, height);
}
// Handling via OpenCV.js // Handling via OpenCV.js
const points = await this.activeTool.run(pressedPoints, imageData, startX, startY); const points = await this.activeTool.run(pressedPoints, imageData, startX, startY);
return points; return points;
@ -358,7 +406,8 @@ class OpenCVControlComponent extends React.PureComponent<Props & DispatchToProps
<CVATTooltip title='Intelligent scissors' className='cvat-opencv-drawing-tool'> <CVATTooltip title='Intelligent scissors' className='cvat-opencv-drawing-tool'>
<Button <Button
onClick={() => { onClick={() => {
this.activeTool = openCVWrapper.segmentation.intelligentScissorsFactory(); this.activeTool = openCVWrapper.segmentation
.intelligentScissorsFactory(this.onChangeToolsBlockerState);
canvasInstance.cancel(); canvasInstance.cancel();
onInteractionStart(this.activeTool, activeLabelID); onInteractionStart(this.activeTool, activeLabelID);
canvasInstance.interact({ canvasInstance.interact({

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

@ -192,6 +192,24 @@
margin-top: 10px; 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 { .cvat-draw-shape-popover-points-selector {
width: 100%; width: 100%;
} }

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

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

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

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

@ -33,7 +33,11 @@ import getCore from 'cvat-core-wrapper';
import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react'; import GlobalHotKeys, { KeyMap } from 'utils/mousetrap-react';
import { NotificationsState } from 'reducers/interfaces'; import { NotificationsState } from 'reducers/interfaces';
import { customWaViewHit } from 'utils/enviroment'; 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'; import '../styles.scss';
interface CVATAppProps { interface CVATAppProps {
@ -87,6 +91,50 @@ class CVATApplication extends React.PureComponent<CVATAppProps & RouteComponentP
}); });
verifyAuthorized(); 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 { 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 (readyForRender) {
if (user && user.isVerified) { if (user && user.isVerified) {
return ( 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 // SPDX-License-Identifier: MIT
@ -8,9 +8,11 @@ import { Link, withRouter } from 'react-router-dom';
import Title from 'antd/lib/typography/Title'; import Title from 'antd/lib/typography/Title';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { Row, Col } from 'antd/lib/grid'; 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 LoginForm, { LoginData } from './login-form';
import CookieDrawer from './cookie-policy-drawer';
interface LoginPageComponentProps { interface LoginPageComponentProps {
fetching: boolean; fetching: boolean;
@ -27,40 +29,44 @@ function LoginPageComponent(props: LoginPageComponentProps & RouteComponentProps
xl: { span: 4 }, xl: { span: 4 },
}; };
const { Content } = Layout;
const { fetching, onLogin, renderResetPassword } = props; const { fetching, onLogin, renderResetPassword } = props;
return ( return (
<> <Layout>
<Row justify='center' align='middle'> <Content>
<Col {...sizes}> <Row justify='center' align='middle' style={{ height: '100%' }}>
<Title level={2}> Login </Title> <Col {...sizes}>
<LoginForm <Title level={2}> Login </Title>
fetching={fetching} <LoginForm
onSubmit={(loginData: LoginData): void => { fetching={fetching}
onLogin(loginData.username, loginData.password); 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 && (
<Row justify='start' align='top'> <Row justify='start' align='top'>
<Col> <Col>
<Text strong> <Text strong>
<Link to='/auth/password/reset'>Forgot your password?</Link> New to CVAT? Create
<Link to='/auth/register'> an account</Link>
</Text> </Text>
</Col> </Col>
</Row> </Row>
)} {renderResetPassword && (
</Col> <Row justify='start' align='top'>
</Row> <Col>
<CookieDrawer /> <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'; @import '../../base.scss';
.cvat-models-page { .cvat-models-page {
padding-top: 30px; padding-top: $grid-unit-size * 2;
padding-bottom: $grid-unit-size;
height: 90%; height: 90%;
overflow: auto; overflow: auto;
position: fixed; position: fixed;

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

@ -16,7 +16,10 @@ import { PlusOutlined } from '@ant-design/icons';
import { CombinedState, Task } from 'reducers/interfaces'; import { CombinedState, Task } from 'reducers/interfaces';
import { getProjectsAsync } from 'actions/projects-actions'; import { getProjectsAsync } from 'actions/projects-actions';
import { cancelInferenceAsync } from 'actions/models-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 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 DetailsComponent from './details';
import ProjectTopBar from './top-bar'; import ProjectTopBar from './top-bar';
@ -109,6 +112,9 @@ export default function ProjectPageComponent(): JSX.Element {
</React.Fragment> </React.Fragment>
))} ))}
</Col> </Col>
<ExportDatasetModal />
<MoveTaskModal />
<ModelRunnerDialog />
</Row> </Row>
); );
} }

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

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

@ -15,18 +15,27 @@ import Button from 'antd/lib/button';
import { MoreOutlined } from '@ant-design/icons'; import { MoreOutlined } from '@ant-design/icons';
import { CombinedState, Project } from 'reducers/interfaces'; import { CombinedState, Project } from 'reducers/interfaces';
import { useCardHeightHOC } from 'utils/hooks';
import ProjectActionsMenuComponent from './actions-menu'; import ProjectActionsMenuComponent from './actions-menu';
interface Props { interface Props {
projectInstance: Project; 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 { export default function ProjectItemComponent(props: Props): JSX.Element {
const { const {
projectInstance: { instance, preview }, projectInstance: { instance, preview },
} = props; } = props;
const history = useHistory(); const history = useHistory();
const height = useCardHeight();
const ownerName = instance.owner ? instance.owner.username : null; const ownerName = instance.owner ? instance.owner.username : null;
const updated = moment(instance.updatedDate).fromNow(); const updated = moment(instance.updatedDate).fromNow();
const deletes = useSelector((state: CombinedState) => state.projects.activities.deletes); 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}`); history.push(`/projects/${instance.id}`);
}; };
const style: React.CSSProperties = {}; const style: React.CSSProperties = { height };
if (deleted) { if (deleted) {
style.pointerEvents = 'none'; style.pointerEvents = 'none';
style.opacity = 0.5; style.opacity = 0.5;

@ -15,7 +15,7 @@ export default function ProjectListComponent(): JSX.Element {
const dispatch = useDispatch(); const dispatch = useDispatch();
const projectsCount = useSelector((state: CombinedState) => state.projects.count); const projectsCount = useSelector((state: CombinedState) => state.projects.count);
const { page } = useSelector((state: CombinedState) => state.projects.gettingQuery); 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); const gettingQuery = useSelector((state: CombinedState) => state.projects.gettingQuery);
function changePage(p: number): void { 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 ( return (
<> <>
<Row justify='center' align='middle' className='cvat-project-list-content'> <Row justify='center' align='middle' className='cvat-project-list-content'>
<Col className='cvat-projects-list' md={22} lg={18} xl={16} xxl={14}> <Col className='cvat-projects-list' md={22} lg={18} xl={16} xxl={14}>
{projects.map( {projects.map(
(row: Project[]): JSX.Element => ( (project: Project): JSX.Element => (
<Row key={row[0].instance.id} gutter={[8, 8]}> <ProjectItem key={project.instance.id} projectInstance={project} />
{row.map((project: Project) => (
<Col span={6} key={project.instance.id}>
<ProjectItem projectInstance={project} />
</Col>
))}
</Row>
), ),
)} )}
</Col> </Col>

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

@ -6,9 +6,8 @@
.cvat-projects-page { .cvat-projects-page {
padding-top: $grid-unit-size * 2; padding-top: $grid-unit-size * 2;
padding-bottom: $grid-unit-size * 5; padding-bottom: $grid-unit-size;
height: 100%; height: 100%;
position: fixed;
width: 100%; width: 100%;
> div:nth-child(1) { > div:nth-child(1) {
@ -71,28 +70,6 @@
justify-content: center; 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 { .ant-menu.cvat-project-actions-menu {
box-shadow: 0 0 17px rgba(0, 0, 0, 0.2); box-shadow: 0 0 17px rgba(0, 0, 0, 0.2);
@ -107,17 +84,62 @@
} }
.cvat-projects-project-item-card { .cvat-projects-project-item-card {
.ant-empty { .cvat-projects-project-item-card-preview {
margin: $grid-unit-size; .ant-empty {
height: $grid-unit-size * 16; margin: $grid-unit-size;
} height: inherit;
display: grid;
> div:first-child {
margin: auto;
}
}
img { height: 100%;
height: $grid-unit-size * 18; display: flex;
align-items: center;
justify-content: space-around;
object-fit: cover; 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 { .cvat-project-list-content {
padding-bottom: $grid-unit-size; 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 // SPDX-License-Identifier: MIT
@ -9,9 +9,10 @@ import { Link, withRouter } from 'react-router-dom';
import Title from 'antd/lib/typography/Title'; import Title from 'antd/lib/typography/Title';
import Text from 'antd/lib/typography/Text'; import Text from 'antd/lib/typography/Text';
import { Row, Col } from 'antd/lib/grid'; import { Row, Col } from 'antd/lib/grid';
import { Layout } from 'antd';
import { UserAgreement } from 'reducers/interfaces'; 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'; import RegisterForm, { RegisterData, UserConfirmation } from './register-form';
interface RegisterPageComponentProps { interface RegisterPageComponentProps {
@ -38,39 +39,42 @@ function RegisterPageComponent(props: RegisterPageComponentProps & RouteComponen
}; };
const { fetching, userAgreements, onRegister } = props; const { fetching, userAgreements, onRegister } = props;
const { Content } = Layout;
return ( return (
<> <Layout>
<Row justify='center' align='middle'> <Content>
<Col {...sizes}> <Row justify='center' align='middle' style={{ height: '100%' }}>
<Title level={2}> Create an account </Title> <Col {...sizes}>
<RegisterForm <Title level={2}> Create an account </Title>
fetching={fetching} <RegisterForm
userAgreements={userAgreements} fetching={fetching}
onSubmit={(registerData: RegisterData): void => { userAgreements={userAgreements}
onRegister( onSubmit={(registerData: RegisterData): void => {
registerData.username, onRegister(
registerData.firstName, registerData.username,
registerData.lastName, registerData.firstName,
registerData.email, registerData.lastName,
registerData.password1, registerData.email,
registerData.password2, registerData.password1,
registerData.confirmations, registerData.password2,
); registerData.confirmations,
}} );
/> }}
<Row justify='start' align='top'> />
<Col> <Row justify='start' align='top'>
<Text strong> <Col>
Already have an account? <Text strong>
<Link to='/auth/login'> Login </Link> Already have an account?
</Text> <Link to='/auth/login'> Login </Link>
</Col> </Text>
</Row> </Col>
</Col> </Row>
</Row> </Col>
<CookieDrawer /> </Row>
</> </Content>
<FooterDrawer />
</Layout>
); );
} }

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

@ -88,9 +88,7 @@
.cvat-task-preview-wrapper { .cvat-task-preview-wrapper {
overflow: hidden; overflow: hidden;
margin-bottom: 20px; margin-bottom: 20px;
width: $grid-unit-size * 32;
height: $grid-unit-size * 18; height: $grid-unit-size * 18;
display: table-cell;
text-align: center; text-align: center;
vertical-align: middle; vertical-align: middle;
background-color: $background-color-2; background-color: $background-color-2;
@ -98,7 +96,6 @@
.cvat-task-parameters { .cvat-task-parameters {
margin-top: $grid-unit-size * 2; margin-top: $grid-unit-size * 2;
width: $grid-unit-size * 32;
} }
.cvat-open-bug-tracker-button { .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 JobListContainer from 'containers/task-page/job-list';
import ModelRunnerModal from 'components/model-runner-modal/model-runner-dialog'; import ModelRunnerModal from 'components/model-runner-modal/model-runner-dialog';
import MoveTaskModal from 'components/move-task-modal/move-task-modal'; 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 { Task } from 'reducers/interfaces';
import TopBarComponent from './top-bar'; import TopBarComponent from './top-bar';
@ -85,6 +86,7 @@ class TaskPageComponent extends React.PureComponent<Props> {
</Row> </Row>
<ModelRunnerModal /> <ModelRunnerModal />
<MoveTaskModal /> <MoveTaskModal />
<ExportDatasetModal />
{updating && <Spin size='large' className='cvat-spinner' />} {updating && <Spin size='large' className='cvat-spinner' />}
</> </>
); );

@ -6,8 +6,8 @@
@import '../../styles.scss'; @import '../../styles.scss';
.cvat-tasks-page { .cvat-tasks-page {
padding-top: 15px; padding-top: $grid-unit-size * 2;
padding-bottom: 40px; padding-bottom: $grid-unit-size;
height: 100%; height: 100%;
width: 100%; width: 100%;
@ -33,6 +33,18 @@
padding-top: 10px; 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) { > div:nth-child(3) {
padding-top: 10px; padding-top: 10px;
} }

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

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

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

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

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

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

@ -16,9 +16,13 @@ import { NotificationsActionType } from 'actions/notification-actions';
import { BoundariesActionTypes } from 'actions/boundaries-actions'; import { BoundariesActionTypes } from 'actions/boundaries-actions';
import { UserAgreementsActionTypes } from 'actions/useragreements-actions'; import { UserAgreementsActionTypes } from 'actions/useragreements-actions';
import { ReviewActionTypes } from 'actions/review-actions'; import { ReviewActionTypes } from 'actions/review-actions';
import { ExportActionTypes } from 'actions/export-actions';
import getCore from 'cvat-core-wrapper';
import { NotificationsState } from './interfaces'; import { NotificationsState } from './interfaces';
const core = getCore();
const defaultState: NotificationsState = { const defaultState: NotificationsState = {
errors: { errors: {
auth: { auth: {
@ -308,8 +312,9 @@ export default function (state = defaultState, action: AnyAction): Notifications
}, },
}; };
} }
case TasksActionTypes.EXPORT_DATASET_FAILED: { case ExportActionTypes.EXPORT_DATASET_FAILED: {
const taskID = action.payload.task.id; const instanceID = action.payload.instance.id;
const instanceType = action.payload.instance instanceof core.classes.Project ? 'project' : 'task';
return { return {
...state, ...state,
errors: { errors: {
@ -319,7 +324,8 @@ export default function (state = defaultState, action: AnyAction): Notifications
exportingAsDataset: { exportingAsDataset: {
message: message:
'Could not export dataset for the ' + '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(), 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: { case TasksActionTypes.DELETE_TASK_FAILED: {
const { taskID } = action.payload; const { taskID } = action.payload;
return { return {

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

@ -32,6 +32,10 @@ const defaultState: SettingsState = {
showAllInterpolationTracks: false, showAllInterpolationTracks: false,
intelligentPolygonCrop: true, intelligentPolygonCrop: true,
defaultApproxPolyAccuracy: 9, defaultApproxPolyAccuracy: 9,
toolsBlockerState: {
algorithmsLocked: false,
buttonVisible: false,
},
}, },
player: { player: {
canvasBackgroundColor: '#ffffff', 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: { case SettingsActionTypes.SWITCH_SETTINGS_DIALOG: {
return { return {
...state, ...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: { case AuthActionTypes.LOGOUT_SUCCESS: {
return { ...defaultState }; return { ...defaultState };
} }

@ -322,6 +322,13 @@ const defaultKeyMap = ({
action: 'keydown', action: 'keydown',
applicable: [DimensionType.DIM_2D, DimensionType.DIM_3D], 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: { CHANGE_OBJECT_COLOR: {
name: 'Change color', name: 'Change color',
description: 'Set the next color for an activated shape', description: 'Set the next color for an activated shape',

@ -32,8 +32,6 @@ const defaultState: TasksState = {
mode: null, mode: null,
}, },
activities: { activities: {
dumps: {},
exports: {},
loads: {}, loads: {},
deletes: {}, deletes: {},
creates: { creates: {
@ -85,84 +83,6 @@ export default (state: TasksState = defaultState, action: AnyAction): TasksState
initialized: true, initialized: true,
fetching: false, 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: { case TasksActionTypes.LOAD_ANNOTATIONS: {
const { task } = action.payload; const { task } = action.payload;
const { loader } = action.payload; const { loader } = action.payload;

@ -57,7 +57,7 @@ hr {
width: 100%; width: 100%;
height: 100%; height: 100%;
display: grid; display: grid;
min-width: 1280px; min-width: 1024px;
} }
#layout-grid { #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 // SPDX-License-Identifier: MIT

@ -1,7 +1,7 @@
// Copyright (C) 2021 Intel Corporation // Copyright (C) 2021 Intel Corporation
// //
// SPDX-License-Identifier: MIT // SPDX-License-Identifier: MIT
import { useRef, useEffect } from 'react'; import { useRef, useEffect, useState } from 'react';
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export function usePrevious<T>(value: T): T | undefined { export function usePrevious<T>(value: T): T | undefined {
@ -11,3 +11,51 @@ export function usePrevious<T>(value: T): T | undefined {
}); });
return ref.current; 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); this.hashFrame(imgData, frameNumber);
return imgData; return imgData;
} catch (e) { } catch (e) {
console.log('Histogram equalization error', e); throw new Error(e.toString());
return src;
} finally { } finally {
if (matImage) matImage.delete(); if (matImage) matImage.delete();
if (channels) channels.delete(); if (channels) channels.delete();

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

@ -12,7 +12,7 @@ const core = getCore();
const baseURL = core.config.backendAPI.slice(0, -7); const baseURL = core.config.backendAPI.slice(0, -7);
export interface Segmentation { export interface Segmentation {
intelligentScissorsFactory: () => IntelligentScissors; intelligentScissorsFactory: (onChangeToolsBlockerState:(event:string)=>void) => IntelligentScissors;
} }
export interface Contours { export interface Contours {
@ -20,7 +20,7 @@ export interface Contours {
} }
export interface ImgProc { export interface ImgProc {
hist: () => HistogramEqualization hist: () => HistogramEqualization;
} }
export class OpenCVWrapper { export class OpenCVWrapper {
@ -41,10 +41,6 @@ export class OpenCVWrapper {
const contentLength = response.headers.get('Content-Length'); const contentLength = response.headers.get('Content-Length');
const { body } = response; const { body } = response;
if (contentLength === null) {
throw new Error('Content length is null, but necessary');
}
if (body === null) { if (body === null) {
throw new Error('Response body is null, but necessary'); throw new Error('Response body is null, but necessary');
} }
@ -64,7 +60,9 @@ export class OpenCVWrapper {
if (value instanceof Uint8Array) { if (value instanceof Uint8Array) {
decodedScript += decoder.decode(value); decodedScript += decoder.decode(value);
receivedLength += value.length; 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)); onProgress(+percentage.toFixed(0));
} }
} }
@ -128,7 +126,8 @@ export class OpenCVWrapper {
} }
return { 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