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 (
+
+ );
+};
+
+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 (
+
+ );
+};
+
+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={
+
+ }
+ >
+