diff --git a/CHANGELOG.md b/CHANGELOG.md index cb6161fff..428ef6232 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,18 +6,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added -- Add support for device tags ([#191](https://github.com/edgehog-device-manager/edgehog/pull/191), [#212](https://github.com/edgehog-device-manager/edgehog/pull/212)) +- Add support for device tags ([#191](https://github.com/edgehog-device-manager/edgehog/pull/191), [#212](https://github.com/edgehog-device-manager/edgehog/pull/212)). - Add support for device custom attributes - ([#205](https://github.com/edgehog-device-manager/edgehog/pull/205)) + ([#205](https://github.com/edgehog-device-manager/edgehog/pull/205)). - Add `MAX_UPLOAD_SIZE_BYTES` env variable to define the maximum dimension for uploads (particularly relevant for OTA updates). Defaults to 4 GB. - Allow creating and managing groups based on selectors. - Add support for device's `network_interfaces` ([#231](https://github.com/edgehog-device-manager/edgehog/pull/231), [#232](https://github.com/edgehog-device-manager/edgehog/pull/232)). - Add support for base image collections ([#229](https://github.com/edgehog-device-manager/edgehog/pull/229), [#230](https://github.com/edgehog-device-manager/edgehog/pull/230)). -- Add support for base images ([#240](https://github.com/edgehog-device-manager/edgehog/pull/240)) +- Add support for base images ([#240](https://github.com/edgehog-device-manager/edgehog/pull/240), [#244](https://github.com/edgehog-device-manager/edgehog/pull/244)). ### Changed -- Handle Device part numbers for nonexistent system models +- Handle Device part numbers for nonexistent system models. - BREAKING: The `Description` field in the `SystemModel` object is now a `String` instead of a `LocalizedText`. @@ -35,7 +35,7 @@ available. ## [0.5.1] - 2022-06-01 ### Added - Add `connected` field to wifi scan result and highlight the latest connected network - ([#193](https://github.com/edgehog-device-manager/edgehog/pull/193)) + ([#193](https://github.com/edgehog-device-manager/edgehog/pull/193)). ### Changed - Change Geo IP provider from FreeGeoIP to IPBase @@ -44,8 +44,8 @@ available. ### Fixed - Add a workaround to correctly parse Astarte datastreams even if AppEngine API shows them with a - inconsistent format ([#194](https://github.com/edgehog-device-manager/edgehog/pull/194)) + inconsistent format ([#194](https://github.com/edgehog-device-manager/edgehog/pull/194)). ## [0.5.0] - 2022-03-22 ### Added -- Initial Edgehog release +- Initial Edgehog release. diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a2df2d230..d5c4ff47c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -31,6 +31,7 @@ "@types/react-table": "^7.7.7", "@types/relay-runtime": "^13.0.2", "@types/relay-test-utils": "^6.0.5", + "@types/semver": "^7.3.13", "babel-plugin-relay": "^12.0.0", "bootstrap": "^5.1.3", "dayjs": "^1.10.7", @@ -60,6 +61,7 @@ "relay-compiler-language-typescript": "^15.0.1", "relay-runtime": "^13.1.1", "relay-test-utils": "^13.1.1", + "semver": "^7.3.8", "typescript": "^4.4.4", "yup": "^0.32.11" } @@ -123,6 +125,14 @@ "url": "https://opencollective.com/babel" } }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/generator": { "version": "7.17.3", "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.17.3.tgz", @@ -176,6 +186,14 @@ "@babel/core": "^7.0.0" } }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-create-class-features-plugin": { "version": "7.17.6", "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.17.6.tgz", @@ -229,6 +247,14 @@ "@babel/core": "^7.4.0-0" } }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/helper-environment-visitor": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.16.7.tgz", @@ -1478,6 +1504,14 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/plugin-transform-shorthand-properties": { "version": "7.16.7", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.16.7.tgz", @@ -1681,6 +1715,14 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@babel/preset-modules": { "version": "0.1.5", "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", @@ -1841,20 +1883,6 @@ "react-scripts": "^4.0.0" } }, - "node_modules/@craco/craco/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@cspotcode/source-map-consumer": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-consumer/-/source-map-consumer-0.8.0.tgz", @@ -2789,6 +2817,14 @@ "node": ">= 10.13.0" } }, + "node_modules/@jest/reporters/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/@jest/reporters/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -3097,20 +3133,6 @@ "semver": "^7.3.5" } }, - "node_modules/@npmcli/fs/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@npmcli/move-file": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", @@ -4149,6 +4171,11 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, + "node_modules/@types/semver": { + "version": "7.3.13", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==" + }, "node_modules/@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -4276,20 +4303,6 @@ } } }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/experimental-utils": { "version": "4.33.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.33.0.tgz", @@ -4393,20 +4406,6 @@ } } }, - "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/@typescript-eslint/visitor-keys": { "version": "4.33.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.33.0.tgz", @@ -5552,6 +5551,14 @@ "@babel/core": "^7.0.0-0" } }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/babel-plugin-polyfill-corejs3": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.5.2.tgz", @@ -7150,20 +7157,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/css-loader/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/css-prefers-color-scheme": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-3.1.1.tgz", @@ -8882,6 +8875,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/eslint-plugin-relay": { "version": "1.8.3", "resolved": "https://registry.npmjs.org/eslint-plugin-relay/-/eslint-plugin-relay-1.8.3.tgz", @@ -9003,20 +9004,6 @@ "node": ">=4" } }, - "node_modules/eslint-plugin-testing-library/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint-scope": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", @@ -9227,20 +9214,6 @@ "node": ">= 4" } }, - "node_modules/eslint/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/eslint/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -12024,6 +11997,14 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/istanbul-lib-report": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", @@ -12059,6 +12040,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/istanbul-lib-report/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/istanbul-lib-report/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -13610,20 +13599,6 @@ "node": ">= 10" } }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/jest-snapshot/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -15082,20 +15057,6 @@ "node": ">= 10.12.0" } }, - "node_modules/node-gyp/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -15150,21 +15111,6 @@ "which": "^2.0.2" } }, - "node_modules/node-notifier/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "optional": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/node-releases": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.2.tgz", @@ -15277,20 +15223,6 @@ "node": ">=10" } }, - "node_modules/normalize-package-data/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -19676,20 +19608,6 @@ "url": "https://opencollective.com/webpack" } }, - "node_modules/sass-loader/node_modules/semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", @@ -19766,11 +19684,17 @@ } }, "node_modules/semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "dependencies": { + "lru-cache": "^6.0.0" + }, "bin": { "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, "node_modules/send": { @@ -21319,6 +21243,14 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/terser-webpack-plugin/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/terser-webpack-plugin/node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -23028,6 +22960,14 @@ "node": ">= 4" } }, + "node_modules/webpack-dev-server/node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/webpack-dev-server/node_modules/string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", @@ -24173,6 +24113,13 @@ "gensync": "^1.0.0-beta.2", "json5": "^2.1.2", "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "@babel/generator": { @@ -24211,6 +24158,13 @@ "@babel/helper-validator-option": "^7.16.7", "browserslist": "^4.17.5", "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "@babel/helper-create-class-features-plugin": { @@ -24249,6 +24203,13 @@ "lodash.debounce": "^4.0.8", "resolve": "^1.14.2", "semver": "^6.1.2" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "@babel/helper-environment-visitor": { @@ -25051,6 +25012,13 @@ "babel-plugin-polyfill-corejs3": "^0.5.0", "babel-plugin-polyfill-regenerator": "^0.3.0", "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "@babel/plugin-transform-shorthand-properties": { @@ -25200,6 +25168,13 @@ "babel-plugin-polyfill-regenerator": "^0.3.0", "core-js-compat": "^3.20.2", "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "@babel/preset-modules": { @@ -25315,16 +25290,6 @@ "lodash": "^4.17.15", "semver": "^7.3.2", "webpack-merge": "^4.2.2" - }, - "dependencies": { - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "@cspotcode/source-map-consumer": { @@ -26054,6 +26019,11 @@ "supports-color": "^7.0.0" } }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -26287,16 +26257,6 @@ "requires": { "@gar/promisify": "^1.0.1", "semver": "^7.3.5" - }, - "dependencies": { - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "@npmcli/move-file": { @@ -27099,6 +27059,11 @@ "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" }, + "@types/semver": { + "version": "7.3.13", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.3.13.tgz", + "integrity": "sha512-21cFJr9z3g5dW8B0CVI9g2O9beqaThGQ6ZFBqHfwhzLDKUxaqTIy3vnfah/UPkfOiF2pLq+tGz+W8RyCskuslw==" + }, "@types/source-list-map": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", @@ -27205,16 +27170,6 @@ "regexpp": "^3.1.0", "semver": "^7.3.5", "tsutils": "^3.21.0" - }, - "dependencies": { - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "@typescript-eslint/experimental-utils": { @@ -27267,16 +27222,6 @@ "is-glob": "^4.0.1", "semver": "^7.3.5", "tsutils": "^3.21.0" - }, - "dependencies": { - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "@typescript-eslint/visitor-keys": { @@ -28204,6 +28149,13 @@ "@babel/compat-data": "^7.13.11", "@babel/helper-define-polyfill-provider": "^0.3.1", "semver": "^6.1.1" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "babel-plugin-polyfill-corejs3": { @@ -29478,14 +29430,6 @@ "version": "6.3.0", "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } } } }, @@ -30663,14 +30607,6 @@ "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==" }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -30908,6 +30844,11 @@ "is-core-module": "^2.2.0", "path-parse": "^1.0.6" } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" } } }, @@ -30985,14 +30926,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==" - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } } } }, @@ -33139,6 +33072,13 @@ "@istanbuljs/schema": "^0.1.2", "istanbul-lib-coverage": "^3.2.0", "semver": "^6.3.0" + }, + "dependencies": { + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + } } }, "istanbul-lib-report": { @@ -33164,6 +33104,11 @@ "semver": "^6.0.0" } }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -34323,14 +34268,6 @@ "react-is": "^17.0.1" } }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -35453,16 +35390,6 @@ "semver": "^7.3.2", "tar": "^6.0.2", "which": "^2.0.2" - }, - "dependencies": { - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "node-int64": { @@ -35519,17 +35446,6 @@ "shellwords": "^0.1.1", "uuid": "^8.3.0", "which": "^2.0.2" - }, - "dependencies": { - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "optional": true, - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "node-releases": { @@ -35613,16 +35529,6 @@ "is-core-module": "^2.5.0", "semver": "^7.3.4", "validate-npm-package-license": "^3.0.1" - }, - "dependencies": { - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } - } } }, "normalize-path": { @@ -39023,14 +38929,6 @@ "ajv": "^6.12.5", "ajv-keywords": "^3.5.2" } - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "requires": { - "lru-cache": "^6.0.0" - } } } }, @@ -39099,9 +38997,12 @@ } }, "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", + "requires": { + "lru-cache": "^6.0.0" + } }, "send": { "version": "0.17.2", @@ -40355,6 +40256,11 @@ "ajv-keywords": "^3.5.2" } }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -41921,6 +41827,11 @@ "ajv-keywords": "^3.1.0" } }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, "string-width": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index e40755e43..30ed5eab0 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -32,6 +32,7 @@ "@types/react-table": "^7.7.7", "@types/relay-runtime": "^13.0.2", "@types/relay-test-utils": "^6.0.5", + "@types/semver": "^7.3.13", "babel-plugin-relay": "^12.0.0", "bootstrap": "^5.1.3", "dayjs": "^1.10.7", @@ -61,6 +62,7 @@ "relay-compiler-language-typescript": "^15.0.1", "relay-runtime": "^13.1.1", "relay-test-utils": "^13.1.1", + "semver": "^7.3.8", "typescript": "^4.4.4", "yup": "^0.32.11" }, diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f0447e3d2..e58f1e3d2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -39,6 +39,8 @@ import HardwareTypes from "pages/HardwareTypes"; import BaseImageCollection from "pages/BaseImageCollection"; import BaseImageCollectionCreate from "pages/BaseImageCollectionCreate"; import BaseImageCollections from "pages/BaseImageCollections"; +import BaseImage from "pages/BaseImage"; +import BaseImageCreate from "pages/BaseImageCreate"; import Login from "pages/Login"; import Logout from "pages/Logout"; @@ -72,6 +74,8 @@ const authenticatedRoutes: RouterRule[] = [ path: Route.baseImageCollectionsNew, element: , }, + { path: Route.baseImagesEdit, element: }, + { path: Route.baseImagesNew, element: }, { path: Route.logout, element: }, { path: "*", element: }, ]; diff --git a/frontend/src/Navigation.tsx b/frontend/src/Navigation.tsx index 6181b8238..33cdc4a14 100644 --- a/frontend/src/Navigation.tsx +++ b/frontend/src/Navigation.tsx @@ -39,6 +39,8 @@ enum Route { baseImageCollections = "/base-image-collections", baseImageCollectionsNew = "/base-image-collections/new", baseImageCollectionsEdit = "/base-image-collections/:baseImageCollectionId/edit", + baseImagesNew = "/base-image-collections/:baseImageCollectionId/base-images/new", + baseImagesEdit = "/base-image-collections/:baseImageCollectionId/base-images/:baseImageId/edit", login = "/login", logout = "/logout", } @@ -66,6 +68,14 @@ type ParametricRoute = route: Route.baseImageCollectionsEdit; params: { baseImageCollectionId: string }; } + | { + route: Route.baseImagesEdit; + params: { baseImageCollectionId: string; baseImageId: string }; + } + | { + route: Route.baseImagesNew; + params: { baseImageCollectionId: string }; + } | { route: Route.login } | { route: Route.logout }; diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts index 19a0d56f8..b2fc95aac 100644 --- a/frontend/src/api/index.ts +++ b/frontend/src/api/index.ts @@ -1,7 +1,7 @@ /* This file is part of Edgehog. - Copyright 2021-2022 SECO Mind Srl + Copyright 2021-2023 SECO Mind Srl Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -26,6 +26,8 @@ import { Store, UploadableMap, } from "relay-runtime"; +import type { TaskScheduler } from "relay-runtime"; +import ReactDOM from "react-dom"; import { AuthConfig, loadAuthConfig } from "contexts/Auth"; @@ -132,9 +134,20 @@ const fetchRelay: FetchFunction = async ( : fetchGraphQL(operation.text, variables, authConfig); }; +// TODO: remove custom scheduler when Relay starts to use React's batched updates +// learn more: https://github.com/facebook/relay/issues/3514#issuecomment-988303222 +const relayScheduler: TaskScheduler = { + cancel: () => {}, + schedule: (task) => { + ReactDOM.unstable_batchedUpdates(task); + return ""; + }, +}; + const relayEnvironment = new Environment({ network: Network.create(fetchRelay), store: new Store(new RecordSource()), + scheduler: relayScheduler, }); export { fetchGraphQL, relayEnvironment }; diff --git a/frontend/src/api/schema.graphql b/frontend/src/api/schema.graphql index 02c958f62..eef51c092 100644 --- a/frontend/src/api/schema.graphql +++ b/frontend/src/api/schema.graphql @@ -173,6 +173,9 @@ type BaseImageCollection implements Node { "The System Model associated with the Base Image Collection" systemModel: SystemModel + + "The Base Images associated with the Base Image Collection" + baseImages: [BaseImage!]! } "Describes a modem of a device." @@ -313,6 +316,11 @@ input DeleteBaseImageCollectionInput { baseImageCollectionId: ID! } +type UpdateBaseImagePayload { + "The updated base image." + baseImage: BaseImage! +} + "Describes a battery slot of a device." type BatterySlot { "The identifier of the battery slot." @@ -356,6 +364,9 @@ type RootQueryType { id: ID! ): BaseImageCollection + "Fetches a single base image." + baseImage("The ID of the base image." id: ID!): BaseImage + "Fetches the list of all devices." devices( "An optional set of filters to apply when fetching the devices." @@ -465,7 +476,7 @@ type Device implements Node { batteryStatus: [BatterySlot!] "Information about the operating system's base image for the device." - baseImage: BaseImage + baseImage: BaseImageInfo "Information about the operating system of the device." osInfo: OsInfo @@ -483,6 +494,16 @@ type Device implements Node { networkInterfaces: [NetworkInterface!] } +type CreateBaseImagePayload { + "The created base image." + baseImage: BaseImage! +} + +type DeleteBaseImagePayload { + "The deleted base image." + baseImage: BaseImage! +} + """ Denotes a type of hardware that devices can have. @@ -744,19 +765,38 @@ enum OtaOperationStatusCode { WRONG_PARTITION } -"Describes an operating system's base image for a device." -type BaseImage { - "The name of the image." - name: String +""" +Represents an uploaded Base Image. - "The version of the image." - version: String +A base image represents a downloadable base image that can be installed on a device +""" +type BaseImage implements Node { + "The ID of an object" + id: ID! - "Human readable build identifier of the image." - buildId: String + "The base image version" + version: String! - "A unique string that identifies the release, usually the image hash." - fingerprint: String + "The url where the base image can be downloaded" + url: String! + + "The starting version requirement for the base image" + startingVersionRequirement: String + + """ + The localized description of the base image + The language of the description can be controlled passing an Accept-Language header in the request. If no such header is present, the default tenant language is returned. + """ + description: String + + """ + The localized release display name of the base image + The language of the description can be controlled passing an Accept-Language header in the request. If no such header is present, the default tenant language is returned. + """ + releaseDisplayName: String + + "The Base Image Collection the Base Image belongs to" + baseImageCollection: BaseImageCollection! } input CreateDeviceGroupInput { @@ -783,6 +823,26 @@ input DeleteDeviceGroupInput { deviceGroupId: ID! } +input DeleteBaseImageInput { + "The ID of the base image to be deleted." + baseImageId: ID! +} + +"Describes the information on the system's base image for a device." +type BaseImageInfo { + "The name of the image." + name: String + + "The version of the image." + version: String + + "Human readable build identifier of the image." + buildId: String + + "A unique string that identifies the release, usually the image hash." + fingerprint: String +} + "Describes the list of WiFi Access Points found by the device." type WifiScanResult { "The channel used by the Access Point." @@ -969,6 +1029,15 @@ type RootMutationType { input: DeleteBaseImageCollectionInput! ): DeleteBaseImageCollectionPayload + "Create a new base image in a base image collection." + createBaseImage(input: CreateBaseImageInput!): CreateBaseImagePayload + + "Updates a base image." + updateBaseImage(input: UpdateBaseImageInput!): UpdateBaseImagePayload + + "Deletes a base image." + deleteBaseImage(input: DeleteBaseImageInput!): DeleteBaseImagePayload + "Updates a device." updateDevice(input: UpdateDeviceInput!): UpdateDevicePayload @@ -1137,3 +1206,37 @@ type DeleteHardwareTypePayload { "The deleted hardware type." hardwareType: HardwareType! } + +input CreateBaseImageInput { + "The ID of the Base Image Collection this Base Image will belong to" + baseImageCollectionId: ID! + + "The base image version" + version: String! + + "The base image file, which will be uploaded to the storage" + file: Upload! + + "An optional starting version requirement for the base image" + startingVersionRequirement: String + + "An optional localized description. This description can currently only use the default tenant locale." + description: LocalizedTextInput + + "An optional relase display name. This can currently only use the default tenant locale." + releaseDisplayName: LocalizedTextInput +} + +input UpdateBaseImageInput { + "The ID of the base image to be updated." + baseImageId: ID! + + "The starting version requirement for the base image" + startingVersionRequirement: String + + "The localized description. This description can currently only use the default tenant locale." + description: LocalizedTextInput + + "The localized relase display name. This can currently only use the default tenant locale." + releaseDisplayName: LocalizedTextInput +} diff --git a/frontend/src/components/BaseImagesTable.tsx b/frontend/src/components/BaseImagesTable.tsx new file mode 100644 index 000000000..53767c0f9 --- /dev/null +++ b/frontend/src/components/BaseImagesTable.tsx @@ -0,0 +1,125 @@ +/* + This file is part of Edgehog. + + Copyright 2023 SECO Mind Srl + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache-2.0 +*/ + +import { useMemo } from "react"; +import { FormattedMessage } from "react-intl"; +import { graphql, useFragment } from "react-relay"; + +import type { + BaseImagesTable_BaseImagesFragment$data, + BaseImagesTable_BaseImagesFragment$key, +} from "api/__generated__/BaseImagesTable_BaseImagesFragment.graphql"; + +import Table from "components/Table"; +import type { Column } from "components/Table"; +import { Link, Route } from "Navigation"; + +// We use graphql fields below in columns configuration +/* eslint-disable relay/unused-fields */ +const BASE_IMAGES_TABLE_FRAGMENT = graphql` + fragment BaseImagesTable_BaseImagesFragment on BaseImageCollection { + id + baseImages { + id + version + startingVersionRequirement + releaseDisplayName + } + } +`; + +type TableRecord = + BaseImagesTable_BaseImagesFragment$data["baseImages"][number]; + +const getColumnsDefinition = ( + baseImageCollectionId: string +): Column[] => [ + { + accessor: "version", + Header: ( + + ), + Cell: ({ row, value }) => ( + + {value} + + ), + }, + { + accessor: "releaseDisplayName", + Header: ( + + ), + }, + { + accessor: "startingVersionRequirement", + Header: ( + + ), + }, +]; + +interface Props { + className?: string; + baseImageCollectionRef: BaseImagesTable_BaseImagesFragment$key; + hideSearch?: boolean; +} + +const BaseImagesTable = ({ + className, + baseImageCollectionRef, + hideSearch = false, +}: Props) => { + const baseImageCollection = useFragment( + BASE_IMAGES_TABLE_FRAGMENT, + baseImageCollectionRef + ); + + const columns = useMemo( + () => getColumnsDefinition(baseImageCollection.id), + [baseImageCollection.id] + ); + + return ( + + ); +}; + +export default BaseImagesTable; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index e85da2d19..9ddb630b7 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -166,6 +166,8 @@ const Sidebar = () => ( Route.baseImageCollections, Route.baseImageCollectionsNew, Route.baseImageCollectionsEdit, + Route.baseImagesNew, + Route.baseImagesEdit, ]} /> diff --git a/frontend/src/components/Table.tsx b/frontend/src/components/Table.tsx index a8a942ca7..cc75406ef 100644 --- a/frontend/src/components/Table.tsx +++ b/frontend/src/components/Table.tsx @@ -1,7 +1,7 @@ /* This file is part of Edgehog. - Copyright 2021,2022 SECO Mind Srl + Copyright 2021-2023 SECO Mind Srl Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -100,7 +100,7 @@ const SortDirectionIndicator = ({ type TableProps = { columns: Column[]; - data: T[]; + data: readonly T[]; className?: string; maxPageRows?: number; hiddenColumns?: string[]; diff --git a/frontend/src/forms/CreateBaseImage.tsx b/frontend/src/forms/CreateBaseImage.tsx new file mode 100644 index 000000000..63b9c8f9f --- /dev/null +++ b/frontend/src/forms/CreateBaseImage.tsx @@ -0,0 +1,282 @@ +/* + This file is part of Edgehog. + + Copyright 2023 SECO Mind Srl + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache-2.0 +*/ + +import React from "react"; +import { useForm } from "react-hook-form"; +import { FormattedMessage } from "react-intl"; +import { yupResolver } from "@hookform/resolvers/yup"; + +import Button from "components/Button"; +import Col from "components/Col"; +import Form from "components/Form"; +import Row from "components/Row"; +import Spinner from "components/Spinner"; +import Stack from "components/Stack"; +import { + baseImageFileSchema, + baseImageVersionSchema, + baseImageStartingVersionRequirementSchema, + yup, +} from "forms"; + +const FormRow = ({ + id, + label, + children, +}: { + id: string; + label: React.ReactNode; + children: React.ReactNode; +}) => ( + + + {label} + + {children} + +); + +type BaseImageData = { + baseImageCollectionId: string; + file: File; + version: string; + startingVersionRequirement: string; + releaseDisplayName: { + locale: string; + text: string; + }; + description: { + locale: string; + text: string; + }; +}; + +type FormData = { + baseImageCollection: string; + file: FileList | null; + version: string; + startingVersionRequirement: string; + releaseDisplayName: string; + description: string; +}; + +const baseImageSchema = yup + .object({ + baseImageCollection: yup.string().required(), + file: baseImageFileSchema.required(), + version: baseImageVersionSchema.required(), + startingVersionRequirement: baseImageStartingVersionRequirementSchema, + releaseDisplayName: yup.string(), + description: yup.string(), + }) + .required(); + +const transformInputData = ( + baseImageCollection: BaseImageCollection +): FormData => ({ + baseImageCollection: baseImageCollection.name, + file: null, + version: "", + startingVersionRequirement: "", + description: "", + releaseDisplayName: "", +}); + +type FormOutput = FormData & { + file: FileList; +}; + +const transformOutputData = ( + baseImageCollection: BaseImageCollection, + locale: string, + data: FormOutput +): BaseImageData => ({ + baseImageCollectionId: baseImageCollection.id, + file: data.file[0], + version: data.version, + startingVersionRequirement: data.startingVersionRequirement, + releaseDisplayName: { + locale, + text: data.releaseDisplayName, + }, + description: { + locale, + text: data.description, + }, +}); + +type BaseImageCollection = { + id: string; + name: string; +}; + +type Props = { + baseImageCollection: BaseImageCollection; + locale: string; + isLoading?: boolean; + onSubmit: (data: BaseImageData) => void; +}; + +const CreateBaseImageForm = ({ + baseImageCollection, + locale, + isLoading = false, + onSubmit, +}: Props) => { + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ + mode: "onTouched", + defaultValues: transformInputData(baseImageCollection), + resolver: yupResolver(baseImageSchema), + }); + + const onFormSubmit = (data: FormData) => { + if (data.file instanceof FileList && data.file[0]) { + const baseImageData = { + ...data, + file: data.file, + }; + onSubmit(transformOutputData(baseImageCollection, locale, baseImageData)); + } + }; + + return ( +
+ + + } + > + + + + } + > + + + {errors.file?.message && ( + + )} + + + + } + > + + + {errors.version?.message && ( + + )} + + + + } + > + + + {errors.startingVersionRequirement?.message && ( + + )} + + + + + ({locale}) + + } + > + + + + + ({locale}) + + } + > + + +
+ +
+
+ + ); +}; + +export type { BaseImageData }; + +export default CreateBaseImageForm; diff --git a/frontend/src/forms/UpdateBaseImage.tsx b/frontend/src/forms/UpdateBaseImage.tsx new file mode 100644 index 000000000..e9be81bae --- /dev/null +++ b/frontend/src/forms/UpdateBaseImage.tsx @@ -0,0 +1,275 @@ +/* + This file is part of Edgehog. + + Copyright 2023 SECO Mind Srl + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache-2.0 +*/ + +import React, { useEffect, useMemo } from "react"; +import { useForm } from "react-hook-form"; +import { FormattedMessage } from "react-intl"; +import { yupResolver } from "@hookform/resolvers/yup"; + +import Button from "components/Button"; +import Col from "components/Col"; +import Form from "components/Form"; +import Row from "components/Row"; +import Spinner from "components/Spinner"; +import Stack from "components/Stack"; +import { baseImageStartingVersionRequirementSchema, yup } from "forms"; + +const FormRow = ({ + id, + label, + children, +}: { + id: string; + label: React.ReactNode; + children: React.ReactNode; +}) => ( + + + {label} + +
{children} + +); + +type BaseImageData = { + baseImageCollection: { + name: string; + }; + version: string; + url: string; + startingVersionRequirement: string | null; + releaseDisplayName: string | null; + description: string | null; +}; + +type FormData = { + baseImageCollection: string; + version: string; + startingVersionRequirement: string; + releaseDisplayName: string; + description: string; +}; + +type BaseImageChanges = { + startingVersionRequirement: string; + releaseDisplayName: { + locale: string; + text: string; + }; + description: { + locale: string; + text: string; + }; +}; + +const baseImageSchema = yup + .object({ + startingVersionRequirement: baseImageStartingVersionRequirementSchema, + releaseDisplayName: yup.string(), + description: yup.string(), + }) + .required(); + +const transformOutputData = ( + locale: string, + data: FormData +): BaseImageChanges => ({ + startingVersionRequirement: data.startingVersionRequirement, + releaseDisplayName: { + locale, + text: data.releaseDisplayName, + }, + description: { + locale, + text: data.description, + }, +}); + +type UpdateBaseImageProps = { + baseImage: BaseImageData; + locale: string; + isLoading?: boolean; + onSubmit: (data: BaseImageChanges) => void; + onDelete: () => void; +}; + +const UpdateBaseImage = ({ + baseImage, + locale, + isLoading = false, + onSubmit, + onDelete, +}: UpdateBaseImageProps) => { + const defaultValues = useMemo( + () => ({ + baseImageCollection: baseImage.baseImageCollection.name, + version: baseImage.version, + startingVersionRequirement: baseImage.startingVersionRequirement || "", + releaseDisplayName: baseImage.releaseDisplayName || "", + description: baseImage.description || "", + }), + [baseImage] + ); + + const { + register, + handleSubmit, + formState: { errors, isDirty }, + reset, + } = useForm({ + mode: "onTouched", + defaultValues, + resolver: yupResolver(baseImageSchema), + }); + + useEffect(() => { + reset(defaultValues); + }, [reset, defaultValues]); + + const onFormSubmit = (data: FormData) => + onSubmit(transformOutputData(locale, data)); + + const canSubmit = !isLoading && isDirty; + + return ( +
+ + + } + > + + + + } + > + ( + + {chunks} + + ), + baseImageName: baseImage.url.split("/").pop(), + }} + /> + + + } + > + + + + } + > + + + {errors.startingVersionRequirement?.message && ( + + )} + + + + + ({locale}) + + } + > + + + + + ({locale}) + + } + > + + +
+ + + + +
+
+ + ); +}; + +export type { BaseImageData, BaseImageChanges }; + +export default UpdateBaseImage; diff --git a/frontend/src/forms/index.ts b/frontend/src/forms/index.ts index f5f53fda8..74a0a5afc 100644 --- a/frontend/src/forms/index.ts +++ b/frontend/src/forms/index.ts @@ -20,6 +20,8 @@ import * as yup from "yup"; import { defineMessages } from "react-intl"; +import semverValid from "semver/functions/valid"; +import semverValidRange from "semver/ranges/valid"; const messages = defineMessages({ required: { @@ -39,6 +41,19 @@ const messages = defineMessages({ defaultMessage: "The handle must start with a letter and only contain lower case characters, numbers or the hyphen symbol -", }, + baseImageFileSchema: { + id: "validation.baseImageFile.required", + defaultMessage: "Required.", + }, + baseImageVersionFormat: { + id: "validation.baseImageVersion.format", + defaultMessage: "The version must follow the Semantic Versioning spec", + }, + baseImageStartingVersionRequirementFormat: { + id: "validation.baseImageStartingVersionRequirement.format", + defaultMessage: + "The supported starting versions must be a valid version range", + }, }); yup.setLocale({ @@ -66,11 +81,32 @@ const baseImageCollectionHandleSchema = yup .string() .matches(/^[a-z][a-z\d-]*$/, messages.handleFormat.id); +const baseImageFileSchema = yup.mixed().test({ + name: "fileRequired", + message: messages.baseImageFileSchema.id, + test: (value) => value instanceof FileList && value.length > 0, +}); + +const baseImageVersionSchema = yup.string().test({ + name: "versionFormat", + message: messages.baseImageVersionFormat.id, + test: (value) => semverValid(value) !== null, +}); + +const baseImageStartingVersionRequirementSchema = yup.string().test({ + name: "startingVersionRequirementFormat", + message: messages.baseImageStartingVersionRequirementFormat.id, + test: (value) => semverValidRange(value) !== null, +}); + export { deviceGroupHandleSchema, systemModelHandleSchema, hardwareTypeHandleSchema, baseImageCollectionHandleSchema, + baseImageFileSchema, + baseImageVersionSchema, + baseImageStartingVersionRequirementSchema, messages, yup, }; diff --git a/frontend/src/i18n/langs/en.json b/frontend/src/i18n/langs/en.json index 995cc7538..408bcb239 100644 --- a/frontend/src/i18n/langs/en.json +++ b/frontend/src/i18n/langs/en.json @@ -98,6 +98,18 @@ "components.BaseImageForm.update": { "defaultMessage": "Update" }, + "components.BaseImagesTable.releaseDisplayNameTitle": { + "defaultMessage": "Release Name", + "description": "Title for the Release Name column of the base images table" + }, + "components.BaseImagesTable.startingVersionRequirementTitle": { + "defaultMessage": "Supported Starting Versions", + "description": "Title for the Supported Starting Versions column of the base images table" + }, + "components.BaseImagesTable.versionTitle": { + "defaultMessage": "Base Image Version", + "description": "Title for the Version column of the base images table" + }, "components.BatteryTable.chargeLevelTitle": { "defaultMessage": "Charge Level" }, @@ -506,6 +518,54 @@ "device.otaOperationStatus.Unknown": { "defaultMessage": "Unknown" }, + "forms.CreateBaseImage.baseImageCollectionLabel": { + "defaultMessage": "Base Image Collection" + }, + "forms.CreateBaseImage.descriptionLabel": { + "defaultMessage": "Description" + }, + "forms.CreateBaseImage.fileLabel": { + "defaultMessage": "Base Image File" + }, + "forms.CreateBaseImage.releaseDisplayNameLabel": { + "defaultMessage": "Release Display Name" + }, + "forms.CreateBaseImage.startingVersionRequirementLabel": { + "defaultMessage": "Supported Starting Versions" + }, + "forms.CreateBaseImage.submitButton": { + "defaultMessage": "Create" + }, + "forms.CreateBaseImage.versionLabel": { + "defaultMessage": "Version" + }, + "forms.UpdateBaseImage.baseImageCollectionLabel": { + "defaultMessage": "Base Image Collection" + }, + "forms.UpdateBaseImage.deleteButton": { + "defaultMessage": "Delete" + }, + "forms.UpdateBaseImage.descriptionLabel": { + "defaultMessage": "Description" + }, + "forms.UpdateBaseImage.file": { + "defaultMessage": "{baseImageName}" + }, + "forms.UpdateBaseImage.fileLabel": { + "defaultMessage": "Base Image file" + }, + "forms.UpdateBaseImage.releaseDisplayNameLabel": { + "defaultMessage": "Release Display Name" + }, + "forms.UpdateBaseImage.starting-version-requirementLabel": { + "defaultMessage": "Supported starting versions" + }, + "forms.UpdateBaseImage.submitButton": { + "defaultMessage": "Update" + }, + "forms.UpdateBaseImage.versionLabel": { + "defaultMessage": "Version" + }, "modem.RegistrationStatus.NotRegistered": { "defaultMessage": "Not Registered" }, @@ -551,12 +611,41 @@ "modem.technology.Unknown": { "defaultMessage": "Unknown" }, + "pages.BaseImage.baseImageNotFound.message": { + "defaultMessage": "Return to the Base Image Collection." + }, + "pages.BaseImage.baseImageNotFound.title": { + "defaultMessage": "Base Image not found." + }, + "pages.BaseImage.creationErrorFeedback": { + "defaultMessage": "Could not update the Base Image, please try again." + }, + "pages.BaseImage.deleteModal.description": { + "defaultMessage": "This action cannot be undone. This will permanently delete the Base Image version {baseImageVersion}.", + "description": "Description for the confirmation modal to delete a Base Image" + }, + "pages.BaseImage.deleteModal.title": { + "defaultMessage": "Delete Base Image", + "description": "Title for the confirmation modal to delete a Base Image" + }, + "pages.BaseImage.deletionErrorFeedback": { + "defaultMessage": "Could not delete the Base Image, please try again." + }, + "pages.BaseImage.title": { + "defaultMessage": "Base Image" + }, "pages.BaseImageCollection.baseImageCollectionNotFound.message": { "defaultMessage": "Return to the Base Image Collection list." }, "pages.BaseImageCollection.baseImageCollectionNotFound.title": { "defaultMessage": "Base Image Collection not found." }, + "pages.BaseImageCollection.baseImagesLabel": { + "defaultMessage": "Base Images" + }, + "pages.BaseImageCollection.createBaseImageButton": { + "defaultMessage": "Create Base Image" + }, "pages.BaseImageCollection.deleteModal.description": { "defaultMessage": "This action cannot be undone. This will permanently delete the Base Image Collection {baseImageCollection}.", "description": "Description for the confirmation modal to delete a Base Image Collection" @@ -592,6 +681,18 @@ "pages.BaseImageCollections.title": { "defaultMessage": "Base Image Collections" }, + "pages.BaseImageCreate.baseImageCollectionNotFound.message": { + "defaultMessage": "Return to the Base Image Collection list." + }, + "pages.BaseImageCreate.baseImageCollectionNotFound.title": { + "defaultMessage": "Base Image Collection not found." + }, + "pages.BaseImageCreate.creationErrorFeedback": { + "defaultMessage": "Could not create the Base Image, please try again." + }, + "pages.BaseImageCreate.title": { + "defaultMessage": "Create Base Image" + }, "pages.Device.BatteryStatusTab": { "defaultMessage": "Battery" }, @@ -830,6 +931,15 @@ "validation.array.min": { "defaultMessage": "Does not have enough values." }, + "validation.baseImageFile.required": { + "defaultMessage": "Required." + }, + "validation.baseImageStartingVersionRequirement.format": { + "defaultMessage": "The supported starting versions must be a valid version range" + }, + "validation.baseImageVersion.format": { + "defaultMessage": "The version must follow the Semantic Versioning spec" + }, "validation.handle.format": { "defaultMessage": "The handle must start with a letter and only contain lower case characters, numbers or the hyphen symbol -" }, diff --git a/frontend/src/mocks/relay.ts b/frontend/src/mocks/relay.ts index f6f386ec5..cf64efdc0 100644 --- a/frontend/src/mocks/relay.ts +++ b/frontend/src/mocks/relay.ts @@ -31,6 +31,16 @@ const relayMockResolvers: MockPayloadGenerator.MockResolvers = { pictureUrl: assets.images.brand, }; }, + BaseImage() { + return { + id: btoa("BaseImage:1"), + description: "Base Image Description", + releaseDisplayName: "release-1", + startingVersionRequirement: null, + url: "https://sample-storage.com/bucket/base_images/1.0.0.bin", + version: "1.0.0", + }; + }, BaseImageCollection(_, generateId) { const id = generateId(); const handle = `base-image-collection-id-${id}`; @@ -103,7 +113,7 @@ const relayMockResolvers: MockPayloadGenerator.MockResolvers = { timestamp: "2021-11-11T09:43:54.437Z", }; }, - BaseImage() { + BaseImageInfo() { return { name: "FreeRTOS", version: "10.4.3", diff --git a/frontend/src/pages/BaseImage.tsx b/frontend/src/pages/BaseImage.tsx new file mode 100644 index 000000000..54bb70f40 --- /dev/null +++ b/frontend/src/pages/BaseImage.tsx @@ -0,0 +1,321 @@ +/* + This file is part of Edgehog. + + Copyright 2023 SECO Mind Srl + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + SPDX-License-Identifier: Apache-2.0 +*/ + +import { Suspense, useCallback, useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { FormattedMessage } from "react-intl"; +import { ErrorBoundary } from "react-error-boundary"; +import graphql from "babel-plugin-relay/macro"; +import { + useMutation, + usePreloadedQuery, + useQueryLoader, + PreloadedQuery, +} from "react-relay/hooks"; + +import type { + BaseImage_getBaseImage_Query, + BaseImage_getBaseImage_Query$data, +} from "api/__generated__/BaseImage_getBaseImage_Query.graphql"; +import type { BaseImage_updateBaseImage_Mutation } from "api/__generated__/BaseImage_updateBaseImage_Mutation.graphql"; +import type { BaseImage_deleteBaseImage_Mutation } from "api/__generated__/BaseImage_deleteBaseImage_Mutation.graphql"; +import Alert from "components/Alert"; +import Center from "components/Center"; +import DeleteModal from "components/DeleteModal"; +import Page from "components/Page"; +import Result from "components/Result"; +import Spinner from "components/Spinner"; +import UpdateBaseImageForm from "forms/UpdateBaseImage"; +import type { BaseImageChanges } from "forms/UpdateBaseImage"; +import { Link, Route, useNavigate } from "Navigation"; + +const GET_BASE_IMAGE_QUERY = graphql` + query BaseImage_getBaseImage_Query($id: ID!) { + baseImage(id: $id) { + id + version + url + startingVersionRequirement + releaseDisplayName + description + baseImageCollection { + id + name + } + } + tenantInfo { + defaultLocale + } + } +`; + +const UPDATE_BASE_IMAGE_MUTATION = graphql` + mutation BaseImage_updateBaseImage_Mutation($input: UpdateBaseImageInput!) { + updateBaseImage(input: $input) { + baseImage { + id + startingVersionRequirement + description + releaseDisplayName + } + } + } +`; + +const DELETE_BASE_IMAGE_MUTATION = graphql` + mutation BaseImage_deleteBaseImage_Mutation($input: DeleteBaseImageInput!) { + deleteBaseImage(input: $input) { + baseImage { + id + } + } + } +`; + +type BaseImageContentProps = { + baseImage: NonNullable; + locale: BaseImage_getBaseImage_Query$data["tenantInfo"]["defaultLocale"]; +}; + +const BaseImageContent = ({ baseImage, locale }: BaseImageContentProps) => { + const baseImageId = baseImage.id; + const baseImageCollectionId = baseImage.baseImageCollection.id; + const navigate = useNavigate(); + + const [showDeleteModal, setShowDeleteModal] = useState(false); + const [errorFeedback, setErrorFeedback] = useState(null); + + const handleShowDeleteModal = useCallback(() => { + setShowDeleteModal(true); + }, [setShowDeleteModal]); + + const [deleteBaseImage, isDeletingBaseImage] = + useMutation(DELETE_BASE_IMAGE_MUTATION); + + const handleDeleteBaseImage = useCallback(() => { + const input = { + baseImageId, + }; + deleteBaseImage({ + variables: { input }, + onCompleted(data, errors) { + if (errors) { + const errorFeedback = errors + .map((error) => error.message) + .join(". \n"); + setErrorFeedback(errorFeedback); + return setShowDeleteModal(false); + } + navigate({ + route: Route.baseImageCollectionsEdit, + params: { baseImageCollectionId }, + }); + }, + onError() { + setErrorFeedback( + + ); + setShowDeleteModal(false); + }, + updater(store, data) { + const baseImageId = data.deleteBaseImage?.baseImage.id; + if (!baseImageId) { + return; + } + + store.delete(baseImageId); + store + .getRoot() + .getLinkedRecord("baseImageCollection", { id: baseImageCollectionId }) + ?.invalidateRecord(); + }, + }); + }, [deleteBaseImage, baseImageId, baseImageCollectionId, navigate]); + + const [updateBaseImage, isUpdatingBaseImage] = + useMutation(UPDATE_BASE_IMAGE_MUTATION); + + const handleUpdateBaseImage = useCallback( + (baseImageChanges: BaseImageChanges) => { + const input = { + baseImageId: baseImage.id, + ...baseImageChanges, + }; + updateBaseImage({ + variables: { input }, + onCompleted(data, errors) { + if (errors) { + const errorFeedback = errors + .map((error) => error.message) + .join(". \n"); + return setErrorFeedback(errorFeedback); + } + }, + onError() { + setErrorFeedback( + + ); + }, + }); + }, + [updateBaseImage, baseImage] + ); + + return ( + + + } + /> + + setErrorFeedback(null)} + dismissible + > + {errorFeedback} + + + {showDeleteModal && ( + setShowDeleteModal(false)} + onConfirm={handleDeleteBaseImage} + isDeleting={isDeletingBaseImage} + title={ + + } + > +

+ {chunks}, + }} + /> +

+
+ )} +
+
+ ); +}; + +type BaseImageWrapperProps = { + getBaseImageQuery: PreloadedQuery; +}; + +const BaseImageWrapper = ({ getBaseImageQuery }: BaseImageWrapperProps) => { + const { baseImageCollectionId = "" } = useParams(); + + const { baseImage, tenantInfo } = usePreloadedQuery( + GET_BASE_IMAGE_QUERY, + getBaseImageQuery + ); + + if (!baseImage) { + return ( + + } + > + + + + + ); + } + + return ( + + ); +}; + +const BaseImagePage = () => { + const { baseImageId = "" } = useParams(); + + const [getBaseImageQuery, getBaseImage] = + useQueryLoader(GET_BASE_IMAGE_QUERY); + + const fetchBaseImage = useCallback(() => { + getBaseImage({ id: baseImageId }, { fetchPolicy: "network-only" }); + }, [getBaseImage, baseImageId]); + + useEffect(fetchBaseImage, [fetchBaseImage]); + + return ( + + + + } + > + ( +
+ +
+ )} + onReset={fetchBaseImage} + > + {getBaseImageQuery && ( + + )} +
+
+ ); +}; + +export default BaseImagePage; diff --git a/frontend/src/pages/BaseImageCollection.tsx b/frontend/src/pages/BaseImageCollection.tsx index 8763fd7d2..bac5a4f4c 100644 --- a/frontend/src/pages/BaseImageCollection.tsx +++ b/frontend/src/pages/BaseImageCollection.tsx @@ -39,6 +39,8 @@ import type { BaseImageCollection_updateBaseImageCollection_Mutation } from "api import type { BaseImageCollection_deleteBaseImageCollection_Mutation } from "api/__generated__/BaseImageCollection_deleteBaseImageCollection_Mutation.graphql"; import { Link, Route, useNavigate } from "Navigation"; import Alert from "components/Alert"; +import BaseImagesTable from "components/BaseImagesTable"; +import Button from "components/Button"; import Center from "components/Center"; import DeleteModal from "components/DeleteModal"; import Page from "components/Page"; @@ -56,6 +58,7 @@ const GET_BASE_IMAGE_COLLECTION_QUERY = graphql` systemModel { name } + ...BaseImagesTable_BaseImagesFragment } } `; @@ -86,11 +89,11 @@ const DELETE_BASE_IMAGE_COLLECTION_MUTATION = graphql` } `; -interface BaseImageCollectionContentProps { +type BaseImageCollectionContentProps = { baseImageCollection: NonNullable< BaseImageCollection_getBaseImageCollection_Query$data["baseImageCollection"] >; -} +}; const BaseImageCollectionContent = ({ baseImageCollection, @@ -218,6 +221,30 @@ const BaseImageCollectionContent = ({ isLoading={isUpdatingBaseImageCollection} /> +
+
+

+ +

+ +
+ {showDeleteModal && ( ; -} +}; const BaseImageCollectionWrapper = ({ getBaseImageCollectionQuery, @@ -295,10 +322,15 @@ const BaseImageCollectionPage = () => { GET_BASE_IMAGE_COLLECTION_QUERY ); - useEffect(() => { - getBaseImageCollection({ id: baseImageCollectionId }); + const fetchBaseImageCollection = useCallback(() => { + getBaseImageCollection( + { id: baseImageCollectionId }, + { fetchPolicy: "network-only" } + ); }, [getBaseImageCollection, baseImageCollectionId]); + useEffect(fetchBaseImageCollection, [fetchBaseImageCollection]); + return ( { )} - onReset={() => { - getBaseImageCollection({ id: baseImageCollectionId }); - }} + onReset={fetchBaseImageCollection} > {getBaseImageCollectionQuery && ( ; + locale: BaseImageCreate_getBaseImageCollection_Query$data["tenantInfo"]["defaultLocale"]; +}; + +const BaseImageCreateContent = ({ + baseImageCollection, + locale, +}: BaseImageCreateContentProps) => { + const [errorFeedback, setErrorFeedback] = useState(null); + const navigate = useNavigate(); + + const [createBaseImage, isCreatingBaseImage] = + useMutation( + CREATE_BASE_IMAGE_MUTATION + ); + + const handleCreateBaseImage = useCallback( + (baseImage: BaseImageData) => { + createBaseImage({ + variables: { input: baseImage }, + onCompleted(data, errors) { + if (data.createBaseImage) { + return navigate({ + route: Route.baseImageCollectionsEdit, + params: { + baseImageCollectionId: baseImage.baseImageCollectionId, + }, + }); + } + + if (errors) { + const errorFeedback = errors + .map((error) => error.message) + .join(". \n"); + return setErrorFeedback(errorFeedback); + } + }, + onError() { + setErrorFeedback( + + ); + }, + updater(store, data) { + if (!data.createBaseImage) { + return; + } + store + .getRoot() + .getLinkedRecord("baseImageCollection", { + id: baseImage.baseImageCollectionId, + }) + ?.invalidateRecord(); + }, + }); + }, + [createBaseImage, navigate] + ); + + return ( + + + } + /> + + setErrorFeedback(null)} + dismissible + > + {errorFeedback} + + + + + ); +}; + +type BaseImageCreateWrapperProps = { + getBaseImageCollectionQuery: PreloadedQuery; +}; + +const BaseImageCreateWrapper = ({ + getBaseImageCollectionQuery, +}: BaseImageCreateWrapperProps) => { + const { baseImageCollection, tenantInfo } = usePreloadedQuery( + GET_BASE_IMAGE_COLLECTION_QUERY, + getBaseImageCollectionQuery + ); + + if (!baseImageCollection) { + return ( + + } + > + + + + + ); + } + + return ( + + ); +}; + +const BaseImageCreatePage = () => { + const { baseImageCollectionId = "" } = useParams(); + + const [getBaseImageCollectionQuery, getBaseImageCollection] = + useQueryLoader( + GET_BASE_IMAGE_COLLECTION_QUERY + ); + + const fetchBaseImageCollection = useCallback(() => { + getBaseImageCollection( + { id: baseImageCollectionId }, + { fetchPolicy: "network-only" } + ); + }, [getBaseImageCollection, baseImageCollectionId]); + + useEffect(fetchBaseImageCollection, [fetchBaseImageCollection]); + + return ( + + + + } + > + ( +
+ +
+ )} + onReset={fetchBaseImageCollection} + > + {getBaseImageCollectionQuery && ( + + )} +
+
+ ); +}; + +export default BaseImageCreatePage;