mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major
This commit is contained in:
@@ -69,7 +69,8 @@
|
||||
"with": "projects/ui/src/environments/environment.prod.ts"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
"outputHashing": "all",
|
||||
"extractLicenses": false
|
||||
},
|
||||
"development": {
|
||||
"buildOptimizer": false,
|
||||
@@ -261,6 +262,8 @@
|
||||
],
|
||||
"styles": [
|
||||
"node_modules/@taiga-ui/core/styles/taiga-ui-theme.less",
|
||||
"projects/shared/styles/taiga.scss",
|
||||
"projects/shared/styles/shared.scss",
|
||||
"projects/setup-wizard/src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
|
||||
411
web/package-lock.json
generated
411
web/package-lock.json
generated
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"name": "startos-ui",
|
||||
"version": "0.3.5.2",
|
||||
"version": "0.3.6-alpha.3",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "startos-ui",
|
||||
"version": "0.3.5.2",
|
||||
"version": "0.3.6-alpha.3",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^17.3.1",
|
||||
"@angular/cdk": "^17.3.1",
|
||||
@@ -20,8 +21,9 @@
|
||||
"@angular/router": "^17.3.1",
|
||||
"@angular/service-worker": "^17.3.1",
|
||||
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
||||
"@start9labs/argon2": "^0.1.0",
|
||||
"@start9labs/emver": "^0.1.5",
|
||||
"@noble/curves": "^1.4.0",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@start9labs/argon2": "^0.2.2",
|
||||
"@start9labs/start-sdk": "file:../sdk/dist",
|
||||
"@taiga-ui/addon-charts": "4.0.0-rc.7",
|
||||
"@taiga-ui/addon-commerce": "4.0.0-rc.7",
|
||||
@@ -38,6 +40,7 @@
|
||||
"@tinkoff/ng-dompurify": "4.0.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"base64-js": "^1.5.1",
|
||||
"buffer": "^6.0.3",
|
||||
"cbor": "npm:@jprochazk/cbor@^0.4.9",
|
||||
"cbor-web": "^8.1.0",
|
||||
"core-js": "^3.21.1",
|
||||
@@ -49,6 +52,7 @@
|
||||
"jose": "^4.9.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"marked": "^4.0.0",
|
||||
"mime": "^4.0.3",
|
||||
"monaco-editor": "^0.33.0",
|
||||
"mustache": "^4.2.0",
|
||||
"ng-qrcode": "^17.0.0",
|
||||
@@ -57,7 +61,7 @@
|
||||
"pbkdf2": "^3.1.2",
|
||||
"rxjs": "^7.5.6",
|
||||
"swiper": "^8.2.4",
|
||||
"ts-matches": "^5.2.1",
|
||||
"ts-matches": "^5.5.1",
|
||||
"tslib": "^2.6.3",
|
||||
"uuid": "^8.3.2",
|
||||
"zone.js": "^0.14.2"
|
||||
@@ -374,12 +378,11 @@
|
||||
}
|
||||
},
|
||||
"../patch-db/client/node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"version": "3.0.2",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
"fill-range": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -612,10 +615,9 @@
|
||||
}
|
||||
},
|
||||
"../patch-db/client/node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"version": "7.0.1",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
@@ -814,9 +816,8 @@
|
||||
},
|
||||
"../patch-db/client/node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
@@ -1496,10 +1497,9 @@
|
||||
}
|
||||
},
|
||||
"../patch-db/client/node_modules/semver": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"version": "5.7.1",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
@@ -1665,9 +1665,8 @@
|
||||
},
|
||||
"../patch-db/client/node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
@@ -1984,20 +1983,29 @@
|
||||
},
|
||||
"../sdk/dist": {
|
||||
"name": "@start9labs/start-sdk",
|
||||
"version": "0.4.0-rev0.lib0.rc8.beta10",
|
||||
"version": "0.3.6-alpha6",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@noble/curves": "^1.4.0",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"isomorphic-fetch": "^3.0.0",
|
||||
"ts-matches": "^5.4.1"
|
||||
"lodash.merge": "^4.6.2",
|
||||
"mime": "^4.0.3",
|
||||
"ts-matches": "^5.5.1",
|
||||
"yaml": "^2.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@iarna/toml": "^2.2.5",
|
||||
"@types/jest": "^29.4.0",
|
||||
"@types/lodash.merge": "^4.6.2",
|
||||
"copyfiles": "^2.4.1",
|
||||
"jest": "^29.4.3",
|
||||
"peggy": "^3.0.2",
|
||||
"prettier": "^3.2.5",
|
||||
"ts-jest": "^29.0.5",
|
||||
"ts-node": "^10.9.1",
|
||||
"ts-pegjs": "^4.2.1",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.0.4",
|
||||
"yaml": "^2.2.2"
|
||||
@@ -2277,9 +2285,8 @@
|
||||
},
|
||||
"node_modules/@angular-devkit/build-angular/node_modules/tslib": {
|
||||
"version": "2.6.2",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
|
||||
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==",
|
||||
"dev": true
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/@angular-devkit/build-angular/node_modules/webpack": {
|
||||
"version": "5.90.3",
|
||||
@@ -4559,8 +4566,7 @@
|
||||
},
|
||||
"node_modules/@maskito/angular": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@maskito/angular/-/angular-3.0.0.tgz",
|
||||
"integrity": "sha512-OqVWCItRhSzbHBfEh79Rg5eHEru6b5p7xzUlZ19JY93Wo/4xhA+d4G8P0j+QJIDl+TIuD+BbB0h6DHtS41GzUw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "2.6.3"
|
||||
@@ -4573,14 +4579,12 @@
|
||||
},
|
||||
"node_modules/@maskito/core": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@maskito/core/-/core-3.0.0.tgz",
|
||||
"integrity": "sha512-g7zeYPMlpMczrq4Huf+Bpdm3Emy/GO0NUXXnQnUiCjlAoKQl+86cLyP5Hbf4HGcNl/J9SoEGEA4uoW6uUc/yLw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@maskito/kit": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-3.0.0.tgz",
|
||||
"integrity": "sha512-aXRlDBjeNox/+D7hbXtnM9INGml1QUIXhrnScrCsbqgg7550mt/ivh4PrxL7oazq/BH7HhvS4olJCF5TPEti1g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@maskito/core": "^3.0.0"
|
||||
@@ -4588,8 +4592,7 @@
|
||||
},
|
||||
"node_modules/@maskito/phone": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@maskito/phone/-/phone-3.0.0.tgz",
|
||||
"integrity": "sha512-v5ky83geF2PReW8h6MfkeT1Zx02iUlib/KJdmdS2ojMeawbnVqKdNKffL+JNlZJdZtglLR8eCxnGLjp+E75TIQ==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"peerDependencies": {
|
||||
"@maskito/core": "^3.0.0",
|
||||
@@ -4610,8 +4613,7 @@
|
||||
},
|
||||
"node_modules/@ng-web-apis/common": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ng-web-apis/common/-/common-4.2.0.tgz",
|
||||
"integrity": "sha512-xOadrY1yhmFl5aOB1AyQlwfa7DIXMATdRCld36DKyg+ebPrWaFBeGueAgwspxGaDGoAHcC8xM/erRq0B/qkmbw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
@@ -4624,8 +4626,7 @@
|
||||
},
|
||||
"node_modules/@ng-web-apis/intersection-observer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ng-web-apis/intersection-observer/-/intersection-observer-4.2.0.tgz",
|
||||
"integrity": "sha512-OAaMtrRUbqvtIu4eFvbo4DIIG6Yw1q+2wirW+4HXqoM84/szhQq/W4iFTTjpDXRy2VNg+PJmMSHUAYWpHSIP5g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
@@ -4637,8 +4638,7 @@
|
||||
},
|
||||
"node_modules/@ng-web-apis/mutation-observer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ng-web-apis/mutation-observer/-/mutation-observer-4.2.0.tgz",
|
||||
"integrity": "sha512-kz5zYI7UcDGs5DFrlmKsDoCuWaCDbh7kJlztsXdM26H+frYP893jSj8GSSeAang2G7ljv+silIv+QDTX2pJ3dA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
@@ -4650,8 +4650,7 @@
|
||||
},
|
||||
"node_modules/@ng-web-apis/resize-observer": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@ng-web-apis/resize-observer/-/resize-observer-4.2.0.tgz",
|
||||
"integrity": "sha512-WVupnbN/41a9fO0O8VuqXc7iT5OAUrayP5y/cLXpuAvymKocJQDys/glqVzuT4I7Dvi7eTWU6CfVePJusDD0Tg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
@@ -4676,9 +4675,31 @@
|
||||
"webpack": "^5.54.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/curves": {
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.5.0.tgz",
|
||||
"integrity": "sha512-J5EKamIHnKPyClwVrzmaf5wSdQXgdHcPZIZLu3bwnbeCx8/7NPK5q2ZBWF+5FvYGByjiQQsJYX6jfgB2wDPn3A==",
|
||||
"dependencies": {
|
||||
"@noble/hashes": "1.4.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@noble/hashes": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.4.0.tgz",
|
||||
"integrity": "sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==",
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "2.0.5",
|
||||
@@ -4690,7 +4711,7 @@
|
||||
},
|
||||
"node_modules/@nodelib/fs.stat": {
|
||||
"version": "2.0.5",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@@ -4698,7 +4719,7 @@
|
||||
},
|
||||
"node_modules/@nodelib/fs.walk": {
|
||||
"version": "1.2.8",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.scandir": "2.1.5",
|
||||
@@ -5110,11 +5131,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@start9labs/argon2": {
|
||||
"version": "0.1.0"
|
||||
},
|
||||
"node_modules/@start9labs/emver": {
|
||||
"version": "0.1.5",
|
||||
"license": "MIT"
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@start9labs/argon2/-/argon2-0.2.2.tgz",
|
||||
"integrity": "sha512-OEJYDIicwwWg0NgG3d2GSO2Qs65B0LY9dIrlXFIJZJ1mo9vcDIU0kC2Yp8dg4XMt2U16ncsgru98s9I+y5Yuaw=="
|
||||
},
|
||||
"node_modules/@start9labs/start-sdk": {
|
||||
"resolved": "../sdk/dist",
|
||||
@@ -5122,8 +5141,7 @@
|
||||
},
|
||||
"node_modules/@taiga-ui/addon-charts": {
|
||||
"version": "4.0.0-rc.7",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.0.0-rc.7.tgz",
|
||||
"integrity": "sha512-0saSw6lFsHTPcRa/G9jAYI4M9CgtkG9eo4ZGPiQqapAILRDs0beF7/3Vrhi+sTaJUAx4hhzRJYdO/W3Iqa+irA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.3"
|
||||
},
|
||||
@@ -5138,8 +5156,7 @@
|
||||
},
|
||||
"node_modules/@taiga-ui/addon-commerce": {
|
||||
"version": "4.0.0-rc.7",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.0.0-rc.7.tgz",
|
||||
"integrity": "sha512-N4gotOyFdvLUBIYkJw/7dmCAq4uqYAl7AenkH8vcouYGfz0jPzW1hhuDxZtmB3dIyTcJdZFdLYkK3574gh2qcA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.3"
|
||||
},
|
||||
@@ -5161,8 +5178,7 @@
|
||||
},
|
||||
"node_modules/@taiga-ui/addon-mobile": {
|
||||
"version": "4.0.0-rc.7",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.0.0-rc.7.tgz",
|
||||
"integrity": "sha512-M9FvZ/DWCR0czA+eHxTigfJYuQTWBuW7zqR8rl7TSW7ibluS6GgYBlrLcqUK/qozSDwmx3vA6iaEt1xkbqLLkg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.3"
|
||||
},
|
||||
@@ -5180,8 +5196,7 @@
|
||||
},
|
||||
"node_modules/@taiga-ui/cdk": {
|
||||
"version": "4.0.0-rc.7",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.0.0-rc.7.tgz",
|
||||
"integrity": "sha512-09d5ePA1kTxL8T3o0HTQouSnqM+fuBqPfL7Fckdmebo6Dqm1MWeO6QRa+aFoYmiz1d/kvmDXDBaKJZp51S54FA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "2.6.3"
|
||||
},
|
||||
@@ -5208,8 +5223,7 @@
|
||||
},
|
||||
"node_modules/@taiga-ui/core": {
|
||||
"version": "4.0.0-rc.7",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.0.0-rc.7.tgz",
|
||||
"integrity": "sha512-59F0U6ZGl7aGA4UQfL8KE8BAFCk3lD9XRshdSk6EatEZITxp7a1XrnKKZOuYYuWMYkKEchHJxPR8g71PlChgAA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.3"
|
||||
},
|
||||
@@ -5231,8 +5245,7 @@
|
||||
},
|
||||
"node_modules/@taiga-ui/event-plugins": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/event-plugins/-/event-plugins-4.0.1.tgz",
|
||||
"integrity": "sha512-qy9AMUVakgZ1e2H7G6dSHtBccveHCi/SG5EhNNjMjhMGkSywIDn+HLJ9kCtDmrJDRtFAPgZ3J0C/UwrsEzCvdQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
@@ -5244,8 +5257,7 @@
|
||||
},
|
||||
"node_modules/@taiga-ui/experimental": {
|
||||
"version": "4.0.0-rc.7",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-4.0.0-rc.7.tgz",
|
||||
"integrity": "sha512-lbefiQz/zyAamjd4HhRH/AhobCGbFRW2YhmwTEjcBDJOupCm0wkZuV3WO1EiSwpPyu4rh82Z4kcL8X9xhQQWvQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.3"
|
||||
},
|
||||
@@ -5262,8 +5274,7 @@
|
||||
},
|
||||
"node_modules/@taiga-ui/i18n": {
|
||||
"version": "4.0.0-rc.7",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.0.0-rc.7.tgz",
|
||||
"integrity": "sha512-ma/iL0izpM9oO4kD8ATiJUSRzc7iPy2QKRfAOKpP02Wkd1DuDeTFd/Wc+JdMJQBb1adPh73LeQDyatkqxHsOPw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.3"
|
||||
@@ -5276,16 +5287,14 @@
|
||||
},
|
||||
"node_modules/@taiga-ui/icons": {
|
||||
"version": "4.0.0-rc.7",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.0.0-rc.7.tgz",
|
||||
"integrity": "sha512-sT6tRJJgmEYzI0JJ/Xmje7kQx6TnQ7qJPdfRX68deEnn7sq/GuSEDZoDvCOAyNiPUmUn2OIIYTI54yMtZQv2zQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/kit": {
|
||||
"version": "4.0.0-rc.7",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.0.0-rc.7.tgz",
|
||||
"integrity": "sha512-sHlXxVixmFm79Z3WPjfNN4nzs8YtLArXHXIruI/cRgppuei9yrRWNZ5lue7kV5jByuyhu7k7aiBzpqRLZ0h+TQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.3"
|
||||
},
|
||||
@@ -5311,8 +5320,7 @@
|
||||
},
|
||||
"node_modules/@taiga-ui/layout": {
|
||||
"version": "4.0.0-rc.7",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.0.0-rc.7.tgz",
|
||||
"integrity": "sha512-GkMDBZmJK0uVY1F1BSkcZjzYdmJngjGxJYNsB8jEVkxqCQsXgjYP07/dJTx/G0TtdVft1y8f/zK3CpeFYHvy1w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.3"
|
||||
},
|
||||
@@ -5328,8 +5336,7 @@
|
||||
},
|
||||
"node_modules/@taiga-ui/legacy": {
|
||||
"version": "4.0.0-rc.7",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/legacy/-/legacy-4.0.0-rc.7.tgz",
|
||||
"integrity": "sha512-/aVVdWQCcHqhTMZqhOT5T9odSaY6yv1VPy3yPp5n2+ScBmbrSjNqUS0q7Mqwb6OtyeiqOgOmJCrBUup3sQu6Yw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.3"
|
||||
},
|
||||
@@ -5339,8 +5346,7 @@
|
||||
},
|
||||
"node_modules/@taiga-ui/polymorpheus": {
|
||||
"version": "4.6.4",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/polymorpheus/-/polymorpheus-4.6.4.tgz",
|
||||
"integrity": "sha512-rlMNWfhImLaMEDWXU1TG7a+YYkVA528poq7lNQ8d+61HyXvbjgs/WAeJQwdabjeLxTUyDozSBJVwSwU69tYprg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"tslib": "^2.6.3"
|
||||
@@ -5352,8 +5358,6 @@
|
||||
},
|
||||
"node_modules/@taiga-ui/styles": {
|
||||
"version": "4.0.0-rc.7",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/styles/-/styles-4.0.0-rc.7.tgz",
|
||||
"integrity": "sha512-00Dp/cE7DJrNwRBpsSd5n4p99Sw1id4dfc36JoKtEV94KaohVdzsm1gRAqeEIu3aJ7pTl5nzCzB4MYOGE0kc6g==",
|
||||
"peerDependencies": {
|
||||
"@taiga-ui/cdk": "^4.0.0-rc.7",
|
||||
"@taiga-ui/core": "^4.0.0-rc.7",
|
||||
@@ -5373,33 +5377,6 @@
|
||||
"dompurify": ">= 2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-morph/common": {
|
||||
"version": "0.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@ts-morph/common/-/common-0.24.0.tgz",
|
||||
"integrity": "sha512-c1xMmNHWpNselmpIqursHeOHHBTIsJLbB+NuovbTTRCNiTLEr/U9dbJ8qy0jd/O2x5pc3seWuOUN5R2IoOTp8A==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"fast-glob": "^3.3.2",
|
||||
"minimatch": "^9.0.4",
|
||||
"mkdirp": "^3.0.1",
|
||||
"path-browserify": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@ts-morph/common/node_modules/mkdirp": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz",
|
||||
"integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==",
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"mkdirp": "dist/cjs/src/bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@tsconfig/node10": {
|
||||
"version": "1.0.9",
|
||||
"dev": true,
|
||||
@@ -5563,12 +5540,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/minimatch": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.5.tgz",
|
||||
"integrity": "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/@types/mustache": {
|
||||
"version": "4.2.5",
|
||||
"dev": true,
|
||||
@@ -6071,38 +6042,11 @@
|
||||
"version": "2.0.1",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/array-differ": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/array-differ/-/array-differ-3.0.0.tgz",
|
||||
"integrity": "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/array-flatten": {
|
||||
"version": "1.1.1",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/array-union": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/arrify": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz",
|
||||
"integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/autoprefixer": {
|
||||
"version": "10.4.19",
|
||||
"dev": true,
|
||||
@@ -6403,9 +6347,8 @@
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz",
|
||||
"integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fill-range": "^7.1.1"
|
||||
},
|
||||
@@ -6840,12 +6783,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/code-block-writer": {
|
||||
"version": "13.0.1",
|
||||
"resolved": "https://registry.npmjs.org/code-block-writer/-/code-block-writer-13.0.1.tgz",
|
||||
"integrity": "sha512-c5or4P6erEA69TxaxTNcHUNcIn+oyxSRTOWV+pSYF+z4epXqNvwvJ70XPGjPNgue83oAFAPBRQYwpAJ/Hpe/Sg==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "1.9.3",
|
||||
"dev": true,
|
||||
@@ -6943,7 +6880,7 @@
|
||||
},
|
||||
"node_modules/concat-map": {
|
||||
"version": "0.0.1",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/connect-history-api-fallback": {
|
||||
@@ -7911,9 +7848,8 @@
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
@@ -7997,7 +7933,7 @@
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.2",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@nodelib/fs.stat": "^2.0.2",
|
||||
@@ -8021,7 +7957,7 @@
|
||||
},
|
||||
"node_modules/fastq": {
|
||||
"version": "1.17.1",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"reusify": "^1.0.4"
|
||||
@@ -8054,9 +7990,8 @@
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz",
|
||||
"integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
@@ -9201,9 +9136,8 @@
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
@@ -9523,6 +9457,19 @@
|
||||
"webpack": "^5.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/less/node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/less/node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"dev": true,
|
||||
@@ -9534,8 +9481,7 @@
|
||||
},
|
||||
"node_modules/libphonenumber-js": {
|
||||
"version": "1.11.5",
|
||||
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.11.5.tgz",
|
||||
"integrity": "sha512-TwHR5BZxGRODtAfz03szucAkjT5OArXr+94SMtAM2pYXIlQNVMrxvb6uSCbnaJJV6QXEyICk7+l6QPgn72WHhg==",
|
||||
"license": "MIT",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/license-webpack-plugin": {
|
||||
@@ -10141,7 +10087,7 @@
|
||||
},
|
||||
"node_modules/merge2": {
|
||||
"version": "1.4.1",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
@@ -10157,7 +10103,7 @@
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.5",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"braces": "^3.0.2",
|
||||
@@ -10169,7 +10115,7 @@
|
||||
},
|
||||
"node_modules/micromatch/node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -10179,14 +10125,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz",
|
||||
"integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==",
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa"
|
||||
],
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
"mime": "bin/cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/mime-db": {
|
||||
@@ -10245,9 +10194,8 @@
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "9.0.5",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
|
||||
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
@@ -10475,47 +10423,6 @@
|
||||
"multicast-dns": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/multimatch": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz",
|
||||
"integrity": "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@types/minimatch": "^3.0.3",
|
||||
"array-differ": "^3.0.0",
|
||||
"array-union": "^2.1.0",
|
||||
"arrify": "^2.0.1",
|
||||
"minimatch": "^3.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/multimatch/node_modules/brace-expansion": {
|
||||
"version": "1.1.11",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
|
||||
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0",
|
||||
"concat-map": "0.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/multimatch/node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^1.1.7"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/mustache": {
|
||||
"version": "4.2.0",
|
||||
"license": "MIT",
|
||||
@@ -10595,44 +10502,6 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ng-morph": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/ng-morph/-/ng-morph-4.5.4.tgz",
|
||||
"integrity": "sha512-g7asHMUZR05bOw/kX5nIVEFs8SDcuH0t5WjdMuVBlJEvl0mY1gDlz1LoIhGlzcbRleMg9n1J0PdrrmYJchWJ9g==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"jsonc-parser": "3.3.1",
|
||||
"minimatch": "10.0.1",
|
||||
"multimatch": "5.0.0",
|
||||
"ts-morph": "23.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular-devkit/core": ">=16.0.0",
|
||||
"@angular-devkit/schematics": ">=16.0.0",
|
||||
"tslib": "^2.6.3"
|
||||
}
|
||||
},
|
||||
"node_modules/ng-morph/node_modules/jsonc-parser": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz",
|
||||
"integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/ng-morph/node_modules/minimatch": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz",
|
||||
"integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"brace-expansion": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/ng-packagr": {
|
||||
"version": "17.3.0",
|
||||
"dev": true,
|
||||
@@ -11478,12 +11347,6 @@
|
||||
"resolved": "../patch-db/client",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/path-browserify": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz",
|
||||
"integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/path-exists": {
|
||||
"version": "4.0.0",
|
||||
"license": "MIT",
|
||||
@@ -12054,7 +11917,7 @@
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -12435,7 +12298,7 @@
|
||||
},
|
||||
"node_modules/reusify": {
|
||||
"version": "1.0.4",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"iojs": ">=1.0.0",
|
||||
@@ -12554,7 +12417,7 @@
|
||||
},
|
||||
"node_modules/run-parallel": {
|
||||
"version": "1.2.0",
|
||||
"devOptional": true,
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
@@ -12776,6 +12639,18 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/send/node_modules/mime": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz",
|
||||
"integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/send/node_modules/ms": {
|
||||
"version": "2.1.3",
|
||||
"dev": true,
|
||||
@@ -13335,9 +13210,8 @@
|
||||
},
|
||||
"node_modules/tar": {
|
||||
"version": "6.2.1",
|
||||
"resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz",
|
||||
"integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==",
|
||||
"devOptional": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"chownr": "^2.0.0",
|
||||
"fs-minipass": "^2.0.0",
|
||||
@@ -13563,9 +13437,8 @@
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
@@ -13598,18 +13471,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ts-matches": {
|
||||
"version": "v5.4.1",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ts-morph": {
|
||||
"version": "23.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ts-morph/-/ts-morph-23.0.0.tgz",
|
||||
"integrity": "sha512-FcvFx7a9E8TUe6T3ShihXJLiJOiqyafzFKUO4aqIHDUCIvADdGNShcbc2W5PMr3LerXRv7mafvFZ9lRENxJmug==",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@ts-morph/common": "~0.24.0",
|
||||
"code-block-writer": "^13.0.1"
|
||||
}
|
||||
"version": "5.5.1",
|
||||
"resolved": "https://registry.npmjs.org/ts-matches/-/ts-matches-5.5.1.tgz",
|
||||
"integrity": "sha512-UFYaKgfqlg9FROK7bdpYqFwG1CJvP4kOJdjXuWoqxo9jCmANoDw1GxkSCpJgoTeIiSTaTH5Qr1klSspb8c+ydg=="
|
||||
},
|
||||
"node_modules/ts-node": {
|
||||
"version": "10.9.2",
|
||||
@@ -13655,8 +13519,7 @@
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
|
||||
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tslint": {
|
||||
"version": "6.1.3",
|
||||
@@ -14358,9 +14221,8 @@
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/ws": {
|
||||
"version": "8.18.0",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz",
|
||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
@@ -14644,9 +14506,8 @@
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "7.5.10",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz",
|
||||
"integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.3.0"
|
||||
},
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
"name": "startos-ui",
|
||||
"version": "0.3.5.2",
|
||||
"version": "0.3.6-alpha.3",
|
||||
"author": "Start9 Labs, Inc",
|
||||
"homepage": "https://start9.com/",
|
||||
"license": "MIT",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"check": "npm run check:shared && npm run check:marketplace && npm run check:ui && npm run check:install && npm run check:setup",
|
||||
@@ -11,7 +12,7 @@
|
||||
"check:install": "tsc --project projects/install-wizard/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck",
|
||||
"build:deps": "npx rimraf .angular/cache && (cd ../sdk && npm ci && npm run build) && (cd ../patch-db/client && npm ci && npm run build)",
|
||||
"build:deps": "rm -rf .angular/cache && (cd ../patch-db/client && npm ci && npm run build) && (cd ../sdk && make bundle)",
|
||||
"build:install": "ng run install-wizard:build",
|
||||
"build:setup": "ng run setup-wizard:build",
|
||||
"build:ui": "ng run ui:build",
|
||||
@@ -42,8 +43,7 @@
|
||||
"@angular/router": "^17.3.1",
|
||||
"@angular/service-worker": "^17.3.1",
|
||||
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
||||
"@start9labs/argon2": "^0.1.0",
|
||||
"@start9labs/emver": "^0.1.5",
|
||||
"@start9labs/argon2": "^0.2.2",
|
||||
"@start9labs/start-sdk": "file:../sdk/dist",
|
||||
"@taiga-ui/addon-charts": "4.0.0-rc.7",
|
||||
"@taiga-ui/addon-commerce": "4.0.0-rc.7",
|
||||
@@ -58,8 +58,11 @@
|
||||
"@taiga-ui/legacy": "4.0.0-rc.7",
|
||||
"@taiga-ui/styles": "4.0.0-rc.7",
|
||||
"@tinkoff/ng-dompurify": "4.0.0",
|
||||
"@noble/curves": "^1.4.0",
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"base64-js": "^1.5.1",
|
||||
"buffer": "^6.0.3",
|
||||
"cbor": "npm:@jprochazk/cbor@^0.4.9",
|
||||
"cbor-web": "^8.1.0",
|
||||
"core-js": "^3.21.1",
|
||||
@@ -71,6 +74,7 @@
|
||||
"jose": "^4.9.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"marked": "^4.0.0",
|
||||
"mime": "^4.0.3",
|
||||
"monaco-editor": "^0.33.0",
|
||||
"mustache": "^4.2.0",
|
||||
"ng-qrcode": "^17.0.0",
|
||||
@@ -79,7 +83,7 @@
|
||||
"pbkdf2": "^3.1.2",
|
||||
"rxjs": "^7.5.6",
|
||||
"swiper": "^8.2.4",
|
||||
"ts-matches": "^5.2.1",
|
||||
"ts-matches": "^5.5.1",
|
||||
"tslib": "^2.6.3",
|
||||
"uuid": "^8.3.2",
|
||||
"zone.js": "^0.14.2"
|
||||
|
||||
@@ -1,11 +1,15 @@
|
||||
{
|
||||
"name": null,
|
||||
"ackWelcome": "0.3.6.1",
|
||||
"ackWelcome": "0.0.0",
|
||||
"marketplace": {
|
||||
"selectedUrl": "https://registry.start9.com/",
|
||||
"knownHosts": {
|
||||
"https://registry.start9.com/": {},
|
||||
"https://community-registry.start9.com/": {}
|
||||
"https://registry.start9.com/": {
|
||||
"name": "Start9 Registry"
|
||||
},
|
||||
"https://community-registry.start9.com/": {
|
||||
"name": "Community Registry"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dev": {},
|
||||
@@ -16,5 +20,6 @@
|
||||
},
|
||||
"ackInstructions": {},
|
||||
"theme": "Dark",
|
||||
"widgets": []
|
||||
"widgets": [],
|
||||
"ack-welcome": "0.3.6-alpha.3"
|
||||
}
|
||||
|
||||
@@ -18,11 +18,14 @@ export class MockApiService implements ApiService {
|
||||
capacity: 73264762332,
|
||||
used: null,
|
||||
startOs: {
|
||||
version: '0.2.17',
|
||||
full: true,
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: null,
|
||||
'1234-5678-9876-5432': {
|
||||
hostname: 'adjective-noun',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '0.2.17',
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: null,
|
||||
},
|
||||
},
|
||||
guid: null,
|
||||
},
|
||||
@@ -41,11 +44,14 @@ export class MockApiService implements ApiService {
|
||||
capacity: 73264762332,
|
||||
used: null,
|
||||
startOs: {
|
||||
version: '0.3.3',
|
||||
full: true,
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: null,
|
||||
'1234-5678-9876-5432': {
|
||||
hostname: 'adjective-noun',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '0.2.17',
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: null,
|
||||
},
|
||||
},
|
||||
guid: null,
|
||||
},
|
||||
@@ -64,11 +70,14 @@ export class MockApiService implements ApiService {
|
||||
capacity: 73264762332,
|
||||
used: null,
|
||||
startOs: {
|
||||
version: '0.3.2',
|
||||
full: true,
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: null,
|
||||
'1234-5678-9876-5432': {
|
||||
hostname: 'adjective-noun',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: '0.2.17',
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: null,
|
||||
},
|
||||
},
|
||||
guid: 'guid-guid-guid-guid',
|
||||
},
|
||||
|
||||
@@ -41,4 +41,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
ElementRef,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { AbstractMarketplaceService } from '../../services/marketplace.service'
|
||||
import { MarketplacePkg } from '../../types'
|
||||
import { map } from 'rxjs'
|
||||
import { Exver } from '@start9labs/shared'
|
||||
// @TODO Alex use Taiga modal
|
||||
import { ModalController } from '@ionic/angular'
|
||||
|
||||
@Component({
|
||||
selector: 'release-notes',
|
||||
templateUrl: './release-notes.component.html',
|
||||
styleUrls: ['./release-notes.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ReleaseNotesComponent {
|
||||
@Input() pkg!: MarketplacePkg
|
||||
|
||||
private selected: string | null = null
|
||||
|
||||
readonly notes$ = this.marketplaceService.getSelectedStore$().pipe(
|
||||
map(s => {
|
||||
return Object.entries(this.pkg.otherVersions)
|
||||
.filter(
|
||||
([v, _]) =>
|
||||
this.exver.getFlavor(v) === this.pkg.flavor &&
|
||||
this.exver.compareExver(this.pkg.version, v) === 1,
|
||||
)
|
||||
.reduce(
|
||||
(obj, [version, info]) => ({
|
||||
...obj,
|
||||
[version]: info.releaseNotes,
|
||||
}),
|
||||
{
|
||||
[`${this.pkg.version} (current)`]: this.pkg.releaseNotes,
|
||||
},
|
||||
)
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly marketplaceService: AbstractMarketplaceService,
|
||||
private readonly exver: Exver,
|
||||
private readonly modalCtrl: ModalController,
|
||||
) {}
|
||||
|
||||
async dismiss() {
|
||||
return this.modalCtrl.dismiss()
|
||||
}
|
||||
|
||||
isSelected(key: string): boolean {
|
||||
return this.selected === key
|
||||
}
|
||||
|
||||
setSelected(selected: string) {
|
||||
this.selected = this.isSelected(selected) ? null : selected
|
||||
}
|
||||
|
||||
getDocSize(key: string, { nativeElement }: ElementRef<HTMLElement>) {
|
||||
return this.isSelected(key) ? nativeElement.scrollHeight : 0
|
||||
}
|
||||
|
||||
asIsOrder(a: any, b: any) {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { TuiAccordion } from '@taiga-ui/kit'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
EmverPipesModule,
|
||||
ExverPipesModule,
|
||||
MarkdownPipeModule,
|
||||
SafeLinksDirective,
|
||||
} from '@start9labs/shared'
|
||||
@@ -12,11 +12,12 @@ import {
|
||||
FilterVersionsPipe,
|
||||
ReleaseNotesComponent,
|
||||
} from './release-notes.component'
|
||||
import { ReleaseNotesComponent } from './release-notes.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
EmverPipesModule,
|
||||
ExverPipesModule,
|
||||
MarkdownPipeModule,
|
||||
NgDompurifyModule,
|
||||
SafeLinksDirective,
|
||||
@@ -28,4 +29,4 @@ import {
|
||||
declarations: [ReleaseNotesComponent],
|
||||
exports: [ReleaseNotesComponent],
|
||||
})
|
||||
export class ReleaseNotesModule {}
|
||||
export class ReleaseNotesComponentModule {}
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-categories',
|
||||
@@ -14,7 +15,7 @@ import {
|
||||
})
|
||||
export class CategoriesComponent {
|
||||
@Input()
|
||||
categories?: string[]
|
||||
categories!: Map<string, T.Category>
|
||||
|
||||
@Input()
|
||||
category = ''
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<div class="background">
|
||||
<img
|
||||
[src]="determineIcon(marketplace)"
|
||||
alt="{{ pkg.manifest.title }} Icon"
|
||||
alt="{{ pkg.title }} Icon"
|
||||
/>
|
||||
</div>
|
||||
<!-- darkening overlay -->
|
||||
@@ -15,14 +15,32 @@
|
||||
<img
|
||||
[src]="determineIcon(marketplace)"
|
||||
class="icon"
|
||||
alt="{{ pkg.manifest.title }} Icon"
|
||||
alt="{{ pkg.title }} Icon"
|
||||
/>
|
||||
<div class="detail">
|
||||
<span class="detail-title" ticker>
|
||||
{{ pkg.manifest.title }}
|
||||
{{ pkg.title }}
|
||||
</span>
|
||||
<span class="detail-description">
|
||||
{{ pkg.manifest.description.short }}
|
||||
{{ pkg.description.short }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- @TODO Alex -->
|
||||
<!-- <ion-item
|
||||
class="service-card"
|
||||
[routerLink]="['/marketplace', pkg.id]"
|
||||
[queryParams]="{ flavor: pkg.flavor, version: pkg.version }"
|
||||
>
|
||||
<ion-thumbnail slot="start">
|
||||
<img alt="" [src]="pkg.icon | trustUrl" />
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2 class="montserrat">
|
||||
<strong>{{ pkg.title }}</strong>
|
||||
</h2>
|
||||
<h3>{{ pkg.description.short }}</h3>
|
||||
<ng-content></ng-content>
|
||||
</ion-label>
|
||||
</ion-item> -->
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Inject,
|
||||
Input,
|
||||
Pipe,
|
||||
PipeTransform,
|
||||
} from '@angular/core'
|
||||
import { AbstractMarketplaceService } from '../../services/marketplace.service'
|
||||
import { PolymorpheusContent } from '@taiga-ui/polymorpheus'
|
||||
import { TuiDialogContext, TuiDialogService } from '@taiga-ui/core'
|
||||
import { MarketplacePkg } from '../../types'
|
||||
import { Observable } from 'rxjs'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { KeyValue } from '@angular/common'
|
||||
|
||||
@Component({
|
||||
selector: 'release-notes',
|
||||
templateUrl: './release-notes.component.html',
|
||||
styleUrls: ['./release-notes.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ReleaseNotesComponent {
|
||||
constructor(
|
||||
private readonly emver: Emver,
|
||||
private readonly marketplaceService: AbstractMarketplaceService,
|
||||
@Inject(TuiDialogService) private readonly dialogs: TuiDialogService,
|
||||
) {}
|
||||
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
|
||||
notes$!: Observable<Record<string, string>>
|
||||
|
||||
ngOnChanges() {
|
||||
this.notes$ = this.marketplaceService.fetchReleaseNotes$(
|
||||
this.pkg.manifest.id,
|
||||
)
|
||||
}
|
||||
|
||||
asIsOrder(a: KeyValue<string, string>, b: KeyValue<string, string>) {
|
||||
const a1 = a.key.split('.')
|
||||
const b1 = b.key.split('.')
|
||||
// contingency in case there's a 4th or 5th version
|
||||
const len = Math.min(a1.length, b1.length)
|
||||
// look through each version number and compare.
|
||||
for (let i = 0; i < len; i++) {
|
||||
const a2 = +a1[i] || 0
|
||||
const b2 = +b1[i] || 0
|
||||
|
||||
if (a2 !== b2) {
|
||||
// sort descending
|
||||
return a2 > b2 ? -1 : 1
|
||||
}
|
||||
}
|
||||
return a1.length - b1.length
|
||||
}
|
||||
|
||||
async showReleaseNotes(content: PolymorpheusContent<TuiDialogContext>) {
|
||||
this.dialogs
|
||||
.open(content, {
|
||||
label: 'Previous Release Notes',
|
||||
})
|
||||
.subscribe()
|
||||
}
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'filterVersions',
|
||||
standalone: true,
|
||||
})
|
||||
export class FilterVersionsPipe implements PipeTransform {
|
||||
transform(
|
||||
notes: Record<string, string>,
|
||||
pkgVersion: string,
|
||||
): Record<string, string> {
|
||||
delete notes[pkgVersion]
|
||||
return notes
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { ReleaseNotesComponent } from '../../../modals/release-notes/release-notes.component'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-about',
|
||||
@@ -10,4 +12,15 @@ import { MarketplacePkg } from '../../../types'
|
||||
export class AboutComponent {
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
|
||||
constructor(private readonly modalCtrl: ModalController) {}
|
||||
|
||||
async presentModalNotes() {
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: { pkg: this.pkg },
|
||||
component: ReleaseNotesComponent,
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
></marketplace-additional-item>
|
||||
<!-- git hash -->
|
||||
<marketplace-additional-item
|
||||
*ngIf="pkg.manifest.gitHash as gitHash; else noHash"
|
||||
*ngIf="pkg.gitHash as gitHash; else noHash"
|
||||
(click)="copyService.copy(gitHash)"
|
||||
[data]="gitHash"
|
||||
label="Git Hash"
|
||||
@@ -29,7 +29,7 @@
|
||||
<!-- license -->
|
||||
<marketplace-additional-item
|
||||
(click)="presentModalMd('License')"
|
||||
[data]="pkg.manifest.license"
|
||||
[data]="pkg.license"
|
||||
label="License"
|
||||
icon="@tui.chevron-right"
|
||||
class="item-pointer"
|
||||
@@ -46,29 +46,29 @@
|
||||
<ng-content />
|
||||
<!-- links -->
|
||||
<marketplace-additional-link
|
||||
*ngIf="pkg.manifest.marketingSite"
|
||||
[url]="pkg.manifest.marketingSite"
|
||||
*ngIf="pkg.marketingSite"
|
||||
[url]="pkg.marketingSite"
|
||||
label="Marketing Site"
|
||||
icon="@tui.external-link"
|
||||
class="item-pointer"
|
||||
></marketplace-additional-link>
|
||||
<marketplace-additional-link
|
||||
*ngIf="pkg.manifest.upstreamRepo"
|
||||
[url]="pkg.manifest.upstreamRepo"
|
||||
*ngIf="pkg.upstreamRepo"
|
||||
[url]="pkg.upstreamRepo"
|
||||
label="Source Repository"
|
||||
icon="@tui.external-link"
|
||||
class="item-pointer"
|
||||
></marketplace-additional-link>
|
||||
<marketplace-additional-link
|
||||
*ngIf="pkg.manifest.wrapperRepo"
|
||||
[url]="pkg.manifest.wrapperRepo"
|
||||
*ngIf="pkg.wrapperRepo"
|
||||
[url]="pkg.wrapperRepo"
|
||||
label="Wrapper Repository"
|
||||
icon="@tui.external-link"
|
||||
class="item-pointer"
|
||||
></marketplace-additional-link>
|
||||
<marketplace-additional-link
|
||||
*ngIf="pkg.manifest.supportSite"
|
||||
[url]="pkg.manifest.supportSite"
|
||||
*ngIf="pkg.supportSite"
|
||||
[url]="pkg.supportSite"
|
||||
label="Support Site"
|
||||
icon="@tui.external-link"
|
||||
class="item-pointer"
|
||||
@@ -76,3 +76,98 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- <ion-item-divider>Additional Info</ion-item-divider>
|
||||
<ion-grid *ngIf="pkg">
|
||||
<ion-row>
|
||||
<ion-col responsiveCol sizeXs="12" sizeMd="6">
|
||||
<ion-item-group>
|
||||
<ion-item
|
||||
*ngIf="pkg.gitHash as gitHash; else noHash"
|
||||
button
|
||||
detail="false"
|
||||
(click)="copy(gitHash)"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Git Hash</h2>
|
||||
<p>{{ gitHash }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="copy-outline"></ion-icon>
|
||||
</ion-item>
|
||||
<ng-template #noHash>
|
||||
<ion-item>
|
||||
<ion-label>
|
||||
<h2>Git Hash</h2>
|
||||
<p>Unknown</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ng-template>
|
||||
<ion-item button detail="false" (click)="presentAlertVersions()">
|
||||
<ion-label>
|
||||
<h2>Other Versions</h2>
|
||||
<p>Click to view other versions</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="chevron-forward"></ion-icon>
|
||||
</ion-item>
|
||||
<ion-item button detail="false" (click)="presentModalMd('license')">
|
||||
<ion-label>
|
||||
<h2>License</h2>
|
||||
<p>{{ pkg.license }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="chevron-forward"></ion-icon>
|
||||
</ion-item>
|
||||
<ion-item
|
||||
button
|
||||
detail="false"
|
||||
(click)="presentModalMd('instructions')"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Instructions</h2>
|
||||
<p>Click to view instructions</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="chevron-forward"></ion-icon>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-col>
|
||||
<ion-col responsiveCol sizeXs="12" sizeMd="6">
|
||||
<ion-item-group>
|
||||
<ion-item
|
||||
[href]="pkg.upstreamRepo"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
detail="false"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Source Repository</h2>
|
||||
<p>{{ pkg.upstreamRepo }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-item>
|
||||
<ion-item
|
||||
[href]="pkg.wrapperRepo"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
detail="false"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Wrapper Repository</h2>
|
||||
<p>{{ pkg.wrapperRepo }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-item>
|
||||
<ion-item
|
||||
[href]="pkg.supportSite"
|
||||
[disabled]="!pkg.supportSite"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
detail="false"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>Support Site</h2>
|
||||
<p>{{ pkg.supportSite || 'Not provided' }}</p>
|
||||
</ion-label>
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid> -->
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
|
||||
import { CopyService, MarkdownComponent } from '@start9labs/shared'
|
||||
import { CopyService, Exver, MarkdownComponent } from '@start9labs/shared'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
import { AbstractMarketplaceService } from '../../../services/marketplace.service'
|
||||
|
||||
@@ -38,7 +38,7 @@ export class AdditionalComponent {
|
||||
size: 'l',
|
||||
data: {
|
||||
content: this.marketplaceService.fetchStatic$(
|
||||
this.pkg.manifest.id,
|
||||
this.pkg.id,
|
||||
label.toLowerCase(),
|
||||
this.url,
|
||||
),
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
<ion-item-divider>Alternative Implementations</ion-item-divider>
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col *ngFor="let pkg of pkgs" responsiveCol sizeSm="12" sizeMd="6">
|
||||
<ion-item
|
||||
[routerLink]="['/marketplace', pkg.id]"
|
||||
[queryParams]="{ flavor: pkg.flavor }"
|
||||
>
|
||||
<ion-thumbnail slot="start">
|
||||
<img alt="" style="border-radius: 100%" [src]="pkg.icon | trustUrl" />
|
||||
</ion-thumbnail>
|
||||
<ion-label>
|
||||
<h2>
|
||||
{{ pkg.title }}
|
||||
</h2>
|
||||
<p>{{ pkg.version }}</p>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-flavors',
|
||||
templateUrl: 'flavors.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FlavorsComponent {
|
||||
@Input()
|
||||
pkgs!: MarketplacePkg[]
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { ResponsiveColModule, SharedPipesModule } from '@start9labs/shared'
|
||||
import { FlavorsComponent } from './flavors.component'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterModule,
|
||||
IonicModule,
|
||||
SharedPipesModule,
|
||||
ResponsiveColModule,
|
||||
],
|
||||
declarations: [FlavorsComponent],
|
||||
exports: [FlavorsComponent],
|
||||
})
|
||||
export class FlavorsModule {}
|
||||
@@ -26,11 +26,11 @@ export class FilterPackagesPipe implements PipeTransform {
|
||||
distance: 16,
|
||||
keys: [
|
||||
{
|
||||
name: 'manifest.title',
|
||||
name: 'title',
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
name: 'manifest.id',
|
||||
name: 'id',
|
||||
weight: 0.5,
|
||||
},
|
||||
],
|
||||
@@ -42,19 +42,19 @@ export class FilterPackagesPipe implements PipeTransform {
|
||||
useExtendedSearch: true,
|
||||
keys: [
|
||||
{
|
||||
name: 'manifest.title',
|
||||
name: 'title',
|
||||
weight: 1,
|
||||
},
|
||||
{
|
||||
name: 'manifest.id',
|
||||
name: 'id',
|
||||
weight: 0.5,
|
||||
},
|
||||
{
|
||||
name: 'manifest.description.short',
|
||||
name: 'description.short',
|
||||
weight: 0.4,
|
||||
},
|
||||
{
|
||||
name: 'manifest.description.long',
|
||||
name: 'description.long',
|
||||
weight: 0.1,
|
||||
},
|
||||
],
|
||||
@@ -71,7 +71,8 @@ export class FilterPackagesPipe implements PipeTransform {
|
||||
.filter(p => category === 'all' || p.categories.includes(category!))
|
||||
.sort((a, b) => {
|
||||
return (
|
||||
new Date(b.publishedAt).valueOf() - new Date(a.publishedAt).valueOf()
|
||||
new Date(b.s9pk.publishedAt).valueOf() -
|
||||
new Date(a.s9pk.publishedAt).valueOf()
|
||||
)
|
||||
})
|
||||
.map(a => ({ ...a }))
|
||||
|
||||
@@ -8,8 +8,6 @@ export * from './pages/list/item/item.component'
|
||||
export * from './pages/list/item/item.module'
|
||||
export * from './pages/list/search/search.component'
|
||||
export * from './pages/list/search/search.module'
|
||||
export * from './pages/release-notes/release-notes.component'
|
||||
export * from './pages/release-notes/release-notes.module'
|
||||
export * from './pages/show/about/about.component'
|
||||
export * from './pages/show/about/about.module'
|
||||
export * from './pages/show/additional/additional-link.component'
|
||||
@@ -20,6 +18,8 @@ export * from './pages/show/dependencies/dependencies.component'
|
||||
export * from './pages/show/dependencies/dependency-item.component'
|
||||
export * from './pages/show/screenshots/screenshots.component'
|
||||
export * from './pages/show/hero/hero.component'
|
||||
export * from './pages/show/flavors/flavors.component'
|
||||
export * from './pages/show/flavors/flavors.module'
|
||||
|
||||
export * from './pipes/filter-packages.pipe'
|
||||
|
||||
|
||||
@@ -20,18 +20,13 @@ export abstract class AbstractMarketplaceService {
|
||||
|
||||
abstract getPackage$(
|
||||
id: string,
|
||||
version: string,
|
||||
version: string | null,
|
||||
flavor: string | null,
|
||||
url?: string,
|
||||
): Observable<MarketplacePkg> // could be {} so need to check in show page
|
||||
|
||||
abstract fetchReleaseNotes$(
|
||||
id: string,
|
||||
url?: string,
|
||||
): Observable<Record<string, string>>
|
||||
): Observable<MarketplacePkg>
|
||||
|
||||
abstract fetchStatic$(
|
||||
id: string,
|
||||
type: string,
|
||||
url?: string,
|
||||
pkg: MarketplacePkg,
|
||||
type: 'LICENSE.md' | 'instructions.md',
|
||||
): Observable<string>
|
||||
}
|
||||
|
||||
@@ -1,49 +1,39 @@
|
||||
import { Url } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
export type StoreURL = string
|
||||
export type StoreName = string
|
||||
|
||||
export interface StoreIdentity {
|
||||
url: StoreURL
|
||||
name?: StoreName
|
||||
export type GetPackageReq = {
|
||||
id: string
|
||||
version: string | null
|
||||
otherVersions: 'short'
|
||||
}
|
||||
export type GetPackageRes = T.GetPackageResponse & {
|
||||
otherVersions: { [version: string]: T.PackageInfoShort }
|
||||
}
|
||||
export type Marketplace = Record<StoreURL, StoreData | null>
|
||||
|
||||
export interface StoreData {
|
||||
info: StoreInfo
|
||||
export type GetPackagesReq = {
|
||||
id: null
|
||||
version: null
|
||||
otherVersions: 'short'
|
||||
}
|
||||
|
||||
export type GetPackagesRes = {
|
||||
[id: T.PackageId]: GetPackageRes
|
||||
}
|
||||
|
||||
export type StoreIdentity = {
|
||||
url: string
|
||||
name?: string
|
||||
}
|
||||
|
||||
export type Marketplace = Record<string, StoreData | null>
|
||||
|
||||
export type StoreData = {
|
||||
info: T.RegistryInfo
|
||||
packages: MarketplacePkg[]
|
||||
}
|
||||
|
||||
export interface StoreInfo {
|
||||
name: StoreName
|
||||
categories: string[]
|
||||
}
|
||||
|
||||
export type StoreIdentityWithData = StoreData & StoreIdentity
|
||||
|
||||
export interface MarketplacePkg {
|
||||
icon: Url
|
||||
license: Url
|
||||
screenshots?: string[]
|
||||
instructions: Url
|
||||
manifest: T.Manifest
|
||||
categories: string[]
|
||||
versions: string[]
|
||||
dependencyMetadata: {
|
||||
[id: string]: DependencyMetadata
|
||||
export type MarketplacePkg = T.PackageVersionInfo &
|
||||
Omit<GetPackageRes, 'best'> & {
|
||||
id: T.PackageId
|
||||
version: string
|
||||
flavor: string | null
|
||||
}
|
||||
publishedAt: string
|
||||
}
|
||||
|
||||
export interface DependencyMetadata {
|
||||
title: string
|
||||
icon: Url
|
||||
optional: boolean
|
||||
hidden: boolean
|
||||
}
|
||||
|
||||
export interface Dependency {
|
||||
description: string | null
|
||||
optional: boolean
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export class AppComponent {
|
||||
let route = 'home'
|
||||
|
||||
if (inProgress) {
|
||||
route = inProgress.complete ? 'success' : 'loading'
|
||||
route = inProgress.status === 'complete' ? '/success' : '/loading'
|
||||
}
|
||||
|
||||
await this.router.navigate([route])
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { ServerBackupSelectModal } from './server-backup-select.page'
|
||||
import { PasswordPageModule } from '../password/password.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [ServerBackupSelectModal],
|
||||
imports: [CommonModule, FormsModule, IonicModule, PasswordPageModule],
|
||||
exports: [ServerBackupSelectModal],
|
||||
})
|
||||
export class ServerBackupSelectModule {}
|
||||
@@ -0,0 +1,24 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Select Server to Restore</ion-title>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-item *ngFor="let server of servers" button (click)="select(server)">
|
||||
<ion-label>
|
||||
<h2>
|
||||
<b>Local Hostname</b>
|
||||
: {{ server.hostname }}.local
|
||||
</h2>
|
||||
<h2>
|
||||
<b>StartOS Version</b>
|
||||
: {{ server.version }}
|
||||
</h2>
|
||||
<h2>
|
||||
<b>Created</b>
|
||||
: {{ server.timestamp | date : 'medium' }}
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,44 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController } from '@ionic/angular'
|
||||
import { StartOSDiskInfoWithId } from 'src/app/services/api/api.service'
|
||||
import { PasswordPage } from '../password/password.page'
|
||||
|
||||
@Component({
|
||||
selector: 'server-backup-select',
|
||||
templateUrl: 'server-backup-select.page.html',
|
||||
styleUrls: ['server-backup-select.page.scss'],
|
||||
})
|
||||
export class ServerBackupSelectModal {
|
||||
@Input() servers: StartOSDiskInfoWithId[] = []
|
||||
|
||||
constructor(private readonly modalController: ModalController) {}
|
||||
|
||||
cancel() {
|
||||
this.modalController.dismiss()
|
||||
}
|
||||
|
||||
async select(server: StartOSDiskInfoWithId): Promise<void> {
|
||||
this.presentModalPassword(server)
|
||||
}
|
||||
|
||||
private async presentModalPassword(
|
||||
server: StartOSDiskInfoWithId,
|
||||
): Promise<void> {
|
||||
const modal = await this.modalController.create({
|
||||
component: PasswordPage,
|
||||
componentProps: { passwordHash: server.passwordHash },
|
||||
})
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.role === 'success') {
|
||||
this.modalController.dismiss(
|
||||
{
|
||||
serverId: server.id,
|
||||
recoveryPassword: res.data.password,
|
||||
},
|
||||
'success',
|
||||
)
|
||||
}
|
||||
})
|
||||
await modal.present()
|
||||
}
|
||||
}
|
||||
@@ -1,32 +1,30 @@
|
||||
import * as jose from 'node-jose'
|
||||
import {
|
||||
DiskInfo,
|
||||
DiskListResponse,
|
||||
PartitionInfo,
|
||||
StartOSDiskInfo,
|
||||
Log,
|
||||
SetupStatus,
|
||||
FollowLogsRes,
|
||||
FollowLogsReq,
|
||||
} from '@start9labs/shared'
|
||||
import { Observable } from 'rxjs'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
export abstract class ApiService {
|
||||
pubkey?: jose.JWK.Key
|
||||
|
||||
abstract getSetupStatus(): Promise<SetupStatus | null> // setup.status
|
||||
abstract getStatus(): Promise<T.SetupStatusRes | null> // setup.status
|
||||
abstract getPubKey(): Promise<void> // setup.get-pubkey
|
||||
abstract getDrives(): Promise<DiskListResponse> // setup.disk.list
|
||||
abstract verifyCifs(cifs: CifsRecoverySource): Promise<StartOSDiskInfo> // setup.cifs.verify
|
||||
abstract attach(importInfo: AttachReq): Promise<void> // setup.attach
|
||||
abstract execute(setupInfo: ExecuteReq): Promise<void> // setup.execute
|
||||
abstract complete(): Promise<CompleteRes> // setup.complete
|
||||
abstract verifyCifs(
|
||||
cifs: T.VerifyCifsParams,
|
||||
): Promise<Record<string, StartOSDiskInfo>> // setup.cifs.verify
|
||||
abstract attach(importInfo: T.AttachParams): Promise<T.SetupProgress> // setup.attach
|
||||
abstract execute(setupInfo: T.SetupExecuteParams): Promise<T.SetupProgress> // setup.execute
|
||||
abstract complete(): Promise<T.SetupResult> // setup.complete
|
||||
abstract exit(): Promise<void> // setup.exit
|
||||
abstract followServerLogs(params: FollowLogsReq): Promise<FollowLogsRes> // setup.logs.follow
|
||||
abstract openLogsWebsocket$(
|
||||
config: WebSocketSubjectConfig<Log>,
|
||||
): Observable<Log>
|
||||
abstract openProgressWebsocket$(guid: string): Observable<T.FullProgress>
|
||||
|
||||
async encrypt(toEncrypt: string): Promise<Encrypted> {
|
||||
async encrypt(toEncrypt: string): Promise<T.EncryptedWire> {
|
||||
if (!this.pubkey) throw new Error('No pubkey found!')
|
||||
const encrypted = await jose.JWE.createEncrypt(this.pubkey!)
|
||||
.update(toEncrypt)
|
||||
@@ -37,66 +35,13 @@ export abstract class ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
type Encrypted = {
|
||||
encrypted: string
|
||||
export type WebsocketConfig<T> = Omit<WebSocketSubjectConfig<T>, 'url'>
|
||||
|
||||
export type StartOSDiskInfoWithId = StartOSDiskInfo & {
|
||||
id: string
|
||||
}
|
||||
|
||||
export type AttachReq = {
|
||||
guid: string
|
||||
startOsPassword: Encrypted
|
||||
}
|
||||
|
||||
export type ExecuteReq = {
|
||||
startOsLogicalname: string
|
||||
startOsPassword: Encrypted
|
||||
recoverySource: RecoverySource | null
|
||||
recoveryPassword: Encrypted | null
|
||||
}
|
||||
|
||||
export type CompleteRes = {
|
||||
torAddress: string
|
||||
lanAddress: string
|
||||
rootCa: string
|
||||
}
|
||||
|
||||
export type DiskBackupTarget = {
|
||||
vendor: string | null
|
||||
model: string | null
|
||||
logicalname: string | null
|
||||
label: string | null
|
||||
capacity: number
|
||||
used: number | null
|
||||
startOs: StartOSDiskInfo | null
|
||||
}
|
||||
|
||||
export type CifsBackupTarget = {
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
mountable: boolean
|
||||
startOs: StartOSDiskInfo | null
|
||||
}
|
||||
|
||||
export type DiskRecoverySource = {
|
||||
type: 'disk'
|
||||
logicalname: string // partition logicalname
|
||||
}
|
||||
|
||||
export type BackupRecoverySource = {
|
||||
type: 'backup'
|
||||
target: CifsRecoverySource | DiskRecoverySource
|
||||
}
|
||||
export type RecoverySource = BackupRecoverySource | DiskMigrateSource
|
||||
|
||||
export type DiskMigrateSource = {
|
||||
type: 'migrate'
|
||||
guid: string
|
||||
}
|
||||
|
||||
export type CifsRecoverySource = {
|
||||
type: 'cifs'
|
||||
hostname: string
|
||||
path: string
|
||||
username: string
|
||||
password: Encrypted | null
|
||||
export type StartOSDiskInfoFull = StartOSDiskInfoWithId & {
|
||||
partition: PartitionInfo
|
||||
drive: DiskInfo
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import {
|
||||
DiskListResponse,
|
||||
StartOSDiskInfo,
|
||||
@@ -12,26 +12,35 @@ import {
|
||||
FollowLogsRes,
|
||||
FollowLogsReq,
|
||||
} from '@start9labs/shared'
|
||||
import {
|
||||
ApiService,
|
||||
CifsRecoverySource,
|
||||
DiskRecoverySource,
|
||||
AttachReq,
|
||||
ExecuteReq,
|
||||
CompleteRes,
|
||||
} from './api.service'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { ApiService } from './api.service'
|
||||
import * as jose from 'node-jose'
|
||||
import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import { Observable } from 'rxjs'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { webSocket } from 'rxjs/webSocket'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class LiveApiService extends ApiService {
|
||||
private readonly http = inject(HttpService)
|
||||
constructor(
|
||||
private readonly http: HttpService,
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
) {
|
||||
super()
|
||||
}
|
||||
|
||||
async getSetupStatus() {
|
||||
return this.rpcRequest<SetupStatus | null>({
|
||||
openProgressWebsocket$(guid: string): Observable<T.FullProgress> {
|
||||
const { location } = this.document.defaultView!
|
||||
const host = location.host
|
||||
|
||||
return webSocket({
|
||||
url: `ws://${host}/ws/rpc/${guid}`,
|
||||
})
|
||||
}
|
||||
|
||||
async getStatus(): Promise<T.SetupStatusRes | null> {
|
||||
return this.rpcRequest<T.SetupStatusRes | null>({
|
||||
method: 'setup.status',
|
||||
params: {},
|
||||
})
|
||||
@@ -44,7 +53,7 @@ export class LiveApiService extends ApiService {
|
||||
* this wil all public/private key, which means that there is no information loss
|
||||
* through the network.
|
||||
*/
|
||||
async getPubKey() {
|
||||
async getPubKey(): Promise<void> {
|
||||
const response: jose.JWK.Key = await this.rpcRequest({
|
||||
method: 'setup.get-pubkey',
|
||||
params: {},
|
||||
@@ -53,29 +62,31 @@ export class LiveApiService extends ApiService {
|
||||
this.pubkey = response
|
||||
}
|
||||
|
||||
async getDrives() {
|
||||
async getDrives(): Promise<DiskListResponse> {
|
||||
return this.rpcRequest<DiskListResponse>({
|
||||
method: 'setup.disk.list',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async verifyCifs(source: CifsRecoverySource) {
|
||||
async verifyCifs(
|
||||
source: T.VerifyCifsParams,
|
||||
): Promise<Record<string, StartOSDiskInfo>> {
|
||||
source.path = source.path.replace('/\\/g', '/')
|
||||
return this.rpcRequest<StartOSDiskInfo>({
|
||||
return this.rpcRequest<Record<string, StartOSDiskInfo>>({
|
||||
method: 'setup.cifs.verify',
|
||||
params: source,
|
||||
})
|
||||
}
|
||||
|
||||
async attach(params: AttachReq) {
|
||||
await this.rpcRequest<void>({
|
||||
async attach(params: T.AttachParams): Promise<T.SetupProgress> {
|
||||
return this.rpcRequest<T.SetupProgress>({
|
||||
method: 'setup.attach',
|
||||
params,
|
||||
})
|
||||
}
|
||||
|
||||
async execute(setupInfo: ExecuteReq) {
|
||||
async execute(setupInfo: T.SetupExecuteParams): Promise<T.SetupProgress> {
|
||||
if (setupInfo.recoverySource?.type === 'backup') {
|
||||
if (isCifsSource(setupInfo.recoverySource.target)) {
|
||||
setupInfo.recoverySource.target.path =
|
||||
@@ -83,22 +94,14 @@ export class LiveApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
await this.rpcRequest<void>({
|
||||
return this.rpcRequest<T.SetupProgress>({
|
||||
method: 'setup.execute',
|
||||
params: setupInfo,
|
||||
})
|
||||
}
|
||||
|
||||
async followServerLogs(params: FollowLogsReq): Promise<FollowLogsRes> {
|
||||
return this.rpcRequest({ method: 'setup.logs.follow', params })
|
||||
}
|
||||
|
||||
openLogsWebsocket$({ url }: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||
return webSocket(`http://start.local/ws/${url}`)
|
||||
}
|
||||
|
||||
async complete() {
|
||||
const res = await this.rpcRequest<CompleteRes>({
|
||||
async complete(): Promise<T.SetupResult> {
|
||||
const res = await this.rpcRequest<T.SetupResult>({
|
||||
method: 'setup.complete',
|
||||
params: {},
|
||||
})
|
||||
@@ -109,7 +112,7 @@ export class LiveApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async exit() {
|
||||
async exit(): Promise<void> {
|
||||
await this.rpcRequest<void>({
|
||||
method: 'setup.exit',
|
||||
params: {},
|
||||
@@ -130,7 +133,7 @@ export class LiveApiService extends ApiService {
|
||||
}
|
||||
|
||||
function isCifsSource(
|
||||
source: CifsRecoverySource | DiskRecoverySource | null,
|
||||
): source is CifsRecoverySource {
|
||||
return !!(source as CifsRecoverySource)?.hostname
|
||||
source: T.BackupTargetFS | null,
|
||||
): source is T.Cifs & { type: 'cifs' } {
|
||||
return !!(source as T.Cifs)?.hostname
|
||||
}
|
||||
|
||||
@@ -1,32 +1,151 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import {
|
||||
DiskListResponse,
|
||||
StartOSDiskInfo,
|
||||
encodeBase64,
|
||||
FollowLogsReq,
|
||||
FollowLogsRes,
|
||||
getSetupStatusMock,
|
||||
Log,
|
||||
pauseFor,
|
||||
} from '@start9labs/shared'
|
||||
import {
|
||||
ApiService,
|
||||
AttachReq,
|
||||
CifsRecoverySource,
|
||||
CompleteRes,
|
||||
ExecuteReq,
|
||||
} from './api.service'
|
||||
import { ApiService } from './api.service'
|
||||
import * as jose from 'node-jose'
|
||||
import { interval, map, Observable } from 'rxjs'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
Observable,
|
||||
concatMap,
|
||||
delay,
|
||||
from,
|
||||
interval,
|
||||
map,
|
||||
mergeScan,
|
||||
of,
|
||||
startWith,
|
||||
switchMap,
|
||||
switchScan,
|
||||
takeWhile,
|
||||
} from 'rxjs'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MockApiService extends ApiService {
|
||||
async getSetupStatus() {
|
||||
return getSetupStatusMock()
|
||||
// fullProgress$(): Observable<T.FullProgress> {
|
||||
// const phases = [
|
||||
// {
|
||||
// name: 'Preparing Data',
|
||||
// progress: null,
|
||||
// },
|
||||
// {
|
||||
// name: 'Transferring Data',
|
||||
// progress: null,
|
||||
// },
|
||||
// {
|
||||
// name: 'Finalizing Setup',
|
||||
// progress: null,
|
||||
// },
|
||||
// ]
|
||||
|
||||
// return from(phases).pipe(
|
||||
// switchScan((acc, val, i) => {}, { overall: null, phases }),
|
||||
// )
|
||||
// }
|
||||
|
||||
// namedProgress$(namedProgress: T.NamedProgress): Observable<T.NamedProgress> {
|
||||
// return of(namedProgress).pipe(startWith(namedProgress))
|
||||
// }
|
||||
|
||||
// progress$(progress: T.Progress): Observable<T.Progress> {}
|
||||
|
||||
// websocket
|
||||
|
||||
openProgressWebsocket$(guid: string): Observable<T.FullProgress> {
|
||||
return of(PROGRESS)
|
||||
// const numPhases = PROGRESS.phases.length
|
||||
|
||||
// return of(PROGRESS).pipe(
|
||||
// switchMap(full =>
|
||||
// from(PROGRESS.phases).pipe(
|
||||
// mergeScan((full, phase, i) => {
|
||||
// if (
|
||||
// !phase.progress ||
|
||||
// typeof phase.progress !== 'object' ||
|
||||
// !phase.progress.total
|
||||
// ) {
|
||||
// full.phases[i].progress = true
|
||||
|
||||
// if (
|
||||
// full.overall &&
|
||||
// typeof full.overall === 'object' &&
|
||||
// full.overall.total
|
||||
// ) {
|
||||
// const step = full.overall.total / numPhases
|
||||
// full.overall.done += step
|
||||
// }
|
||||
|
||||
// return of(full).pipe(delay(2000))
|
||||
// } else {
|
||||
// const total = phase.progress.total
|
||||
// const step = total / 4
|
||||
// let done = phase.progress.done
|
||||
|
||||
// return interval(1000).pipe(
|
||||
// takeWhile(() => done < total),
|
||||
// map(() => {
|
||||
// done += step
|
||||
|
||||
// console.error(done)
|
||||
|
||||
// if (
|
||||
// full.overall &&
|
||||
// typeof full.overall === 'object' &&
|
||||
// full.overall.total
|
||||
// ) {
|
||||
// const step = full.overall.total / numPhases / 4
|
||||
|
||||
// full.overall.done += step
|
||||
// }
|
||||
|
||||
// if (done === total) {
|
||||
// full.phases[i].progress = true
|
||||
|
||||
// if (i === numPhases - 1) {
|
||||
// full.overall = true
|
||||
// }
|
||||
// }
|
||||
// return full
|
||||
// }),
|
||||
// )
|
||||
// }
|
||||
// }, full),
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
}
|
||||
|
||||
async getPubKey() {
|
||||
private statusIndex = 0
|
||||
async getStatus(): Promise<T.SetupStatusRes | null> {
|
||||
await pauseFor(1000)
|
||||
|
||||
this.statusIndex++
|
||||
|
||||
switch (this.statusIndex) {
|
||||
case 2:
|
||||
return {
|
||||
status: 'running',
|
||||
progress: PROGRESS,
|
||||
guid: 'progress-guid',
|
||||
}
|
||||
case 3:
|
||||
return {
|
||||
status: 'complete',
|
||||
torAddress: 'https://asdafsadasdasasdasdfasdfasdf.onion',
|
||||
lanAddress: 'https://adjective-noun.local',
|
||||
rootCa: encodeBase64(rootCA),
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
async getPubKey(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
|
||||
// randomly generated
|
||||
@@ -42,7 +161,7 @@ export class MockApiService extends ApiService {
|
||||
})
|
||||
}
|
||||
|
||||
async getDrives() {
|
||||
async getDrives(): Promise<DiskListResponse> {
|
||||
await pauseFor(1000)
|
||||
return [
|
||||
{
|
||||
@@ -56,11 +175,14 @@ export class MockApiService extends ApiService {
|
||||
capacity: 1979120929996,
|
||||
used: null,
|
||||
startOs: {
|
||||
version: '0.2.17',
|
||||
full: true,
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: null,
|
||||
'1234-5678-9876-5432': {
|
||||
hostname: 'adjective-noun',
|
||||
version: '0.2.17',
|
||||
timestamp: new Date().toISOString(),
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: null,
|
||||
},
|
||||
},
|
||||
guid: null,
|
||||
},
|
||||
@@ -79,11 +201,14 @@ export class MockApiService extends ApiService {
|
||||
capacity: 73264762332,
|
||||
used: null,
|
||||
startOs: {
|
||||
version: '0.3.3',
|
||||
full: true,
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: null,
|
||||
'1234-5678-9876-5432': {
|
||||
hostname: 'adjective-noun',
|
||||
version: '0.2.17',
|
||||
timestamp: new Date().toISOString(),
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: null,
|
||||
},
|
||||
},
|
||||
guid: null,
|
||||
},
|
||||
@@ -102,11 +227,14 @@ export class MockApiService extends ApiService {
|
||||
capacity: 73264762332,
|
||||
used: null,
|
||||
startOs: {
|
||||
version: '0.3.2',
|
||||
full: true,
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: null,
|
||||
'1234-5678-9876-5432': {
|
||||
hostname: 'adjective-noun',
|
||||
version: '0.2.17',
|
||||
timestamp: new Date().toISOString(),
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: null,
|
||||
},
|
||||
},
|
||||
guid: 'guid-guid-guid-guid',
|
||||
},
|
||||
@@ -117,43 +245,41 @@ export class MockApiService extends ApiService {
|
||||
]
|
||||
}
|
||||
|
||||
async verifyCifs(params: CifsRecoverySource) {
|
||||
async verifyCifs(
|
||||
params: T.VerifyCifsParams,
|
||||
): Promise<Record<string, StartOSDiskInfo>> {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
version: '0.3.0',
|
||||
full: true,
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: '',
|
||||
}
|
||||
}
|
||||
|
||||
async attach(params: AttachReq) {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async execute(setupInfo: ExecuteReq) {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
|
||||
async followServerLogs(params: FollowLogsReq): Promise<FollowLogsRes> {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
startCursor: 'fakestartcursor',
|
||||
guid: 'fake-guid',
|
||||
}
|
||||
}
|
||||
|
||||
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||
return interval(500).pipe(
|
||||
map(() => ({
|
||||
'9876-5432-1234-5678': {
|
||||
hostname: 'adjective-noun',
|
||||
version: '0.3.6',
|
||||
timestamp: new Date().toISOString(),
|
||||
message: 'fake log entry',
|
||||
})),
|
||||
)
|
||||
passwordHash:
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
wrappedKey: '',
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
async complete(): Promise<CompleteRes> {
|
||||
async attach(params: T.AttachParams): Promise<T.SetupProgress> {
|
||||
await pauseFor(1000)
|
||||
|
||||
return {
|
||||
progress: PROGRESS,
|
||||
guid: 'progress-guid',
|
||||
}
|
||||
}
|
||||
|
||||
async execute(setupInfo: T.SetupExecuteParams): Promise<T.SetupProgress> {
|
||||
await pauseFor(1000)
|
||||
|
||||
return {
|
||||
progress: PROGRESS,
|
||||
guid: 'progress-guid',
|
||||
}
|
||||
}
|
||||
|
||||
async complete(): Promise<T.SetupResult> {
|
||||
await pauseFor(1000)
|
||||
return {
|
||||
torAddress: 'https://asdafsadasdasasdasdfasdfasdf.onion',
|
||||
@@ -162,7 +288,7 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
}
|
||||
|
||||
async exit() {
|
||||
async exit(): Promise<void> {
|
||||
await pauseFor(1000)
|
||||
}
|
||||
}
|
||||
@@ -189,3 +315,8 @@ Rf3ZOPm9QP92YpWyYDkfAU04xdDo1vR0MYjKPkl4LjRqSU/tcCJnPMbJiwq+bWpX
|
||||
2WJoEBXB/p15Kn6JxjI0ze2SnSI48JZ8it4fvxrhOo0VoLNIuCuNXJOwU17Rdl1W
|
||||
YJidaq7je6k18AdgPA0Kh8y1XtfUH3fTaVw4
|
||||
-----END CERTIFICATE-----`
|
||||
|
||||
const PROGRESS = {
|
||||
overall: null,
|
||||
phases: [],
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { ApiService, RecoverySource } from './api.service'
|
||||
import { ApiService } from './api.service'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
@@ -8,8 +9,7 @@ export class StateService {
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
setupType?: 'fresh' | 'restore' | 'attach' | 'transfer'
|
||||
recoverySource?: RecoverySource
|
||||
recoveryPassword?: string
|
||||
recoverySource?: T.RecoverySource<string>
|
||||
|
||||
async importDrive(guid: string, password: string): Promise<void> {
|
||||
await this.api.attach({
|
||||
@@ -25,9 +25,13 @@ export class StateService {
|
||||
await this.api.execute({
|
||||
startOsLogicalname: storageLogicalname,
|
||||
startOsPassword: await this.api.encrypt(password),
|
||||
recoverySource: this.recoverySource || null,
|
||||
recoveryPassword: this.recoveryPassword
|
||||
? await this.api.encrypt(this.recoveryPassword)
|
||||
recoverySource: this.recoverySource
|
||||
? this.recoverySource.type === 'migrate'
|
||||
? this.recoverySource
|
||||
: {
|
||||
...this.recoverySource,
|
||||
password: await this.api.encrypt(this.recoverySource.password),
|
||||
}
|
||||
: null,
|
||||
})
|
||||
}
|
||||
|
||||
BIN
web/projects/shared/assets/img/service-icons/fallback.png
Normal file
BIN
web/projects/shared/assets/img/service-icons/fallback.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
@@ -0,0 +1,10 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" height="1.5em" viewBox="0 0 24 24" width="1.5em">
|
||||
<path
|
||||
vector-effect="non-scaling-stroke"
|
||||
clip-rule="evenodd"
|
||||
d="M7.2395 3.45152L4.01497 3.49988C3.46275 3.50816 3.00837 3.06721 3.00008 2.51499C2.9918 1.96277 3.43275 1.50839 3.98497 1.50011L9.98497 1.41012C10.0214 1.40957 10.0575 1.41098 10.093 1.41425C10.6089 1.4111 11.1258 1.60635 11.5194 2L16 6.48053C16.781 7.26158 16.781 8.52791 16 9.30895L12.5492 12.7597L10.3089 15C9.52787 15.7811 8.26154 15.781 7.48049 15L5.24023 12.7597L2.99997 10.5195C2.21892 9.73843 2.21892 8.4721 2.99997 7.69105L7.2395 3.45152ZM10.1052 3.41422L4.41418 9.10527L5.80892 10.5H11.9805L14.5858 7.89474L10.1052 3.41422ZM20.4206 13.5445L20.4223 13.5453C20.6348 10.9945 18.7416 9.37791 17.9715 8.83085C17.9293 8.80059 17.8726 8.79856 17.8281 8.82545C17.7836 8.85234 17.7676 8.8632 17.7761 8.91357C17.9704 10.0886 16.2673 11.5428 16.2673 11.5428C15.8623 11.9726 15.5886 12.3895 15.4811 12.9331C15.2176 14.2662 16.0981 15.5842 17.4524 15.8519C18.8068 16.1196 20.1167 15.2659 20.3802 13.9329C20.4067 13.7992 20.4166 13.6763 20.4206 13.5445ZM3 18C2.44772 18 2 18.4477 2 19V21C2 21.5523 2.44772 22 3 22H21C21.5523 22 22 21.5523 22 21V19C22 18.4477 21.5523 18 21 18H3Z"
|
||||
fill="currentColor"
|
||||
stroke="none"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
13
web/projects/shared/src/components/loading/loading.module.ts
Normal file
13
web/projects/shared/src/components/loading/loading.module.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { TuiLoaderModule } from '@taiga-ui/core'
|
||||
import { tuiAsDialog } from '@taiga-ui/cdk'
|
||||
import { LoadingComponent } from './loading.component'
|
||||
import { LoadingService } from './loading.service'
|
||||
|
||||
@NgModule({
|
||||
imports: [TuiLoaderModule],
|
||||
declarations: [LoadingComponent],
|
||||
exports: [LoadingComponent],
|
||||
providers: [tuiAsDialog(LoadingService)],
|
||||
})
|
||||
export class LoadingModule {}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import {
|
||||
EmverComparesPipe,
|
||||
EmverDisplayPipe,
|
||||
EmverSatisfiesPipe,
|
||||
} from './emver.pipe'
|
||||
|
||||
@NgModule({
|
||||
declarations: [EmverComparesPipe, EmverDisplayPipe, EmverSatisfiesPipe],
|
||||
exports: [EmverComparesPipe, EmverDisplayPipe, EmverSatisfiesPipe],
|
||||
})
|
||||
export class EmverPipesModule {}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { Emver } from '../../services/emver.service'
|
||||
|
||||
@Pipe({
|
||||
name: 'satisfiesEmver',
|
||||
})
|
||||
export class EmverSatisfiesPipe implements PipeTransform {
|
||||
constructor(private readonly emver: Emver) {}
|
||||
|
||||
transform(versionUnderTest?: string, range?: string): boolean {
|
||||
return (
|
||||
!!versionUnderTest &&
|
||||
!!range &&
|
||||
this.emver.satisfies(versionUnderTest, range)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'compareEmver',
|
||||
})
|
||||
export class EmverComparesPipe implements PipeTransform {
|
||||
constructor(private readonly emver: Emver) {}
|
||||
|
||||
transform(first: string, second: string): SemverResult {
|
||||
try {
|
||||
return this.emver.compare(first, second) as SemverResult
|
||||
} catch (e) {
|
||||
console.error(`emver comparison failed`, e, first, second)
|
||||
return 'comparison-impossible'
|
||||
}
|
||||
}
|
||||
}
|
||||
// left compared to right - if 1, version on left is higher; if 0, values the same; if -1, version on left is lower
|
||||
type SemverResult = 0 | 1 | -1 | 'comparison-impossible'
|
||||
|
||||
@Pipe({
|
||||
name: 'displayEmver',
|
||||
})
|
||||
export class EmverDisplayPipe implements PipeTransform {
|
||||
constructor() {}
|
||||
|
||||
transform(version: string): string {
|
||||
return displayEmver(version)
|
||||
}
|
||||
}
|
||||
|
||||
export function displayEmver(version: string): string {
|
||||
const vs = version.split('.')
|
||||
if (vs.length === 4) return `${vs[0]}.${vs[1]}.${vs[2]}~${vs[3]}`
|
||||
return version
|
||||
}
|
||||
8
web/projects/shared/src/pipes/exver/exver.module.ts
Normal file
8
web/projects/shared/src/pipes/exver/exver.module.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { ExverComparesPipe, ExverSatisfiesPipe } from './exver.pipe'
|
||||
|
||||
@NgModule({
|
||||
declarations: [ExverComparesPipe, ExverSatisfiesPipe],
|
||||
exports: [ExverComparesPipe, ExverSatisfiesPipe],
|
||||
})
|
||||
export class ExverPipesModule {}
|
||||
35
web/projects/shared/src/pipes/exver/exver.pipe.ts
Normal file
35
web/projects/shared/src/pipes/exver/exver.pipe.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { Exver } from '../../services/exver.service'
|
||||
|
||||
@Pipe({
|
||||
name: 'satisfiesExver',
|
||||
})
|
||||
export class ExverSatisfiesPipe implements PipeTransform {
|
||||
constructor(private readonly exver: Exver) {}
|
||||
|
||||
transform(versionUnderTest?: string, range?: string): boolean {
|
||||
return (
|
||||
!!versionUnderTest &&
|
||||
!!range &&
|
||||
this.exver.satisfies(versionUnderTest, range)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'compareExver',
|
||||
})
|
||||
export class ExverComparesPipe implements PipeTransform {
|
||||
constructor(private readonly exver: Exver) {}
|
||||
|
||||
transform(first: string, second: string): SemverResult {
|
||||
try {
|
||||
return this.exver.compareExver(first, second) as SemverResult
|
||||
} catch (e) {
|
||||
console.error(`exver comparison failed`, e, first, second)
|
||||
return 'comparison-impossible'
|
||||
}
|
||||
}
|
||||
}
|
||||
// left compared to right - if 1, version on left is higher; if 0, values the same; if -1, version on left is lower
|
||||
type SemverResult = 0 | 1 | -1 | 'comparison-impossible'
|
||||
@@ -8,6 +8,8 @@ export * from './classes/rpc-error'
|
||||
export * from './components/initializing/logs-window.component'
|
||||
export * from './components/initializing/initializing.component'
|
||||
export * from './components/loading/loading.component'
|
||||
export * from './components/loading/loading.component'
|
||||
export * from './components/loading/loading.module'
|
||||
export * from './components/loading/loading.service'
|
||||
export * from './components/markdown/markdown.component'
|
||||
export * from './components/markdown/markdown.component.module'
|
||||
@@ -20,8 +22,8 @@ export * from './directives/safe-links.directive'
|
||||
|
||||
export * from './mocks/get-setup-status'
|
||||
|
||||
export * from './pipes/emver/emver.module'
|
||||
export * from './pipes/emver/emver.pipe'
|
||||
export * from './pipes/exver/exver.module'
|
||||
export * from './pipes/exver/exver.pipe'
|
||||
export * from './pipes/markdown/markdown.module'
|
||||
export * from './pipes/markdown/markdown.pipe'
|
||||
export * from './pipes/shared/shared.module'
|
||||
@@ -33,7 +35,7 @@ export * from './pipes/unit-conversion/unit-conversion.pipe'
|
||||
|
||||
export * from './services/copy.service'
|
||||
export * from './services/download-html.service'
|
||||
export * from './services/emver.service'
|
||||
export * from './services/exver.service'
|
||||
export * from './services/error.service'
|
||||
export * from './services/http.service'
|
||||
export * from './services/setup.service'
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import * as emver from '@start9labs/emver'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class Emver {
|
||||
constructor() {}
|
||||
|
||||
compare(lhs?: string, rhs?: string): number | null {
|
||||
if (!lhs || !rhs) return null
|
||||
return emver.compare(lhs, rhs)
|
||||
}
|
||||
|
||||
satisfies(version: string, range: string): boolean {
|
||||
return emver.satisfies(version, range)
|
||||
}
|
||||
}
|
||||
43
web/projects/shared/src/services/exver.service.ts
Normal file
43
web/projects/shared/src/services/exver.service.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { VersionRange, ExtendedVersion, Version } from '@start9labs/start-sdk'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class Exver {
|
||||
constructor() {}
|
||||
|
||||
compareExver(lhs: string, rhs: string): number | null {
|
||||
if (!lhs || !rhs) return null
|
||||
try {
|
||||
return ExtendedVersion.parse(lhs).compareForSort(
|
||||
ExtendedVersion.parse(rhs),
|
||||
)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
greaterThanOrEqual(lhs: string, rhs: string): boolean | null {
|
||||
if (!lhs || !rhs) return null
|
||||
try {
|
||||
return ExtendedVersion.parse(lhs).greaterThanOrEqual(
|
||||
ExtendedVersion.parse(rhs),
|
||||
)
|
||||
} catch (e) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
compareOsVersion(current: string, other: string) {
|
||||
return Version.parse(current).compare(Version.parse(other))
|
||||
}
|
||||
|
||||
satisfies(version: string, range: string): boolean {
|
||||
return VersionRange.parse(range).satisfiedBy(ExtendedVersion.parse(version))
|
||||
}
|
||||
|
||||
getFlavor(version: string): string | null {
|
||||
return ExtendedVersion.parse(version).flavor
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ export type FetchLogsRes = {
|
||||
export interface Log {
|
||||
timestamp: string
|
||||
message: string
|
||||
bootId: string
|
||||
}
|
||||
|
||||
export type DiskListResponse = DiskInfo[]
|
||||
@@ -37,13 +38,14 @@ export interface PartitionInfo {
|
||||
label: string | null
|
||||
capacity: number
|
||||
used: number | null
|
||||
startOs: StartOSDiskInfo | null
|
||||
startOs: Record<string, StartOSDiskInfo>
|
||||
guid: string | null
|
||||
}
|
||||
|
||||
export type StartOSDiskInfo = {
|
||||
hostname: string
|
||||
version: string
|
||||
full: boolean
|
||||
timestamp: string
|
||||
passwordHash: string | null
|
||||
wrappedKey: string | null
|
||||
}
|
||||
|
||||
@@ -5,6 +5,8 @@ export enum Method {
|
||||
POST = 'POST',
|
||||
}
|
||||
|
||||
type ParamPrimitive = string | number | boolean
|
||||
|
||||
export interface HttpOptions {
|
||||
method: Method
|
||||
url: string
|
||||
@@ -12,7 +14,7 @@ export interface HttpOptions {
|
||||
[header: string]: string | string[]
|
||||
}
|
||||
params?: {
|
||||
[param: string]: string | string[]
|
||||
[param: string]: ParamPrimitive | ParamPrimitive[]
|
||||
}
|
||||
responseType?: 'json' | 'text' | 'arrayBuffer'
|
||||
body?: any
|
||||
@@ -28,7 +30,7 @@ export interface HttpAngularOptions {
|
||||
[header: string]: string | string[]
|
||||
}
|
||||
params?: {
|
||||
[param: string]: string | string[]
|
||||
[param: string]: ParamPrimitive | ParamPrimitive[]
|
||||
}
|
||||
responseType?: 'json' | 'text' | 'arrayBuffer'
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { APP_INITIALIZER, Provider } from '@angular/core'
|
||||
import { APP_INITIALIZER, inject, Provider } from '@angular/core'
|
||||
import { UntypedFormBuilder } from '@angular/forms'
|
||||
import { Router } from '@angular/router'
|
||||
import {
|
||||
@@ -30,6 +30,7 @@ import { DateTransformerService } from './services/date-transformer.service'
|
||||
import { DatetimeTransformerService } from './services/datetime-transformer.service'
|
||||
import { MarketplaceService } from './services/marketplace.service'
|
||||
import { ThemeSwitcherService } from './services/theme-switcher.service'
|
||||
import { StorageService } from './services/storage.service'
|
||||
|
||||
const {
|
||||
useMocks,
|
||||
@@ -64,9 +65,14 @@ export const APP_PROVIDERS: Provider[] = [
|
||||
provide: ApiService,
|
||||
useClass: useMocks ? MockApiService : LiveApiService,
|
||||
},
|
||||
{
|
||||
provide: PatchDB,
|
||||
deps: [PatchDbSource, PATCH_CACHE],
|
||||
useClass: PatchDB,
|
||||
},
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
deps: [AuthService, ClientStorageService, Router],
|
||||
deps: [StorageService, AuthService, ClientStorageService, Router],
|
||||
useFactory: appInitializer,
|
||||
multi: true,
|
||||
},
|
||||
@@ -86,14 +92,27 @@ export const APP_PROVIDERS: Provider[] = [
|
||||
provide: AbstractCategoryService,
|
||||
useClass: CategoryService,
|
||||
},
|
||||
{
|
||||
provide: TUI_DIALOGS_CLOSE,
|
||||
useFactory: () =>
|
||||
inject(StateService).pipe(
|
||||
pairwise(),
|
||||
filter(
|
||||
([prev, curr]) =>
|
||||
prev === 'running' && (curr === 'error' || curr === 'initializing'),
|
||||
),
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
export function appInitializer(
|
||||
storage: StorageService,
|
||||
auth: AuthService,
|
||||
localStorage: ClientStorageService,
|
||||
router: Router,
|
||||
): () => void {
|
||||
return () => {
|
||||
storage.migrate036()
|
||||
auth.init()
|
||||
localStorage.init()
|
||||
router.initialNavigation()
|
||||
|
||||
167
web/projects/ui/src/app/components/form.component.ts
Normal file
167
web/projects/ui/src/app/components/form.component.ts
Normal file
@@ -0,0 +1,167 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
OnInit,
|
||||
} from '@angular/core'
|
||||
import { FormGroup, ReactiveFormsModule } from '@angular/forms'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
|
||||
import {
|
||||
tuiMarkControlAsTouchedAndValidate,
|
||||
TuiValueChangesModule,
|
||||
} from '@taiga-ui/cdk'
|
||||
import { TuiDialogContext, TuiModeModule } from '@taiga-ui/core'
|
||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||
import { TuiDialogFormService } from '@taiga-ui/kit'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
|
||||
import { compare, Operation } from 'fast-json-patch'
|
||||
import { FormModule } from 'src/app/components/form/form.module'
|
||||
import { InvalidService } from 'src/app/components/form/invalid.service'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
|
||||
export interface ActionButton<T> {
|
||||
text: string
|
||||
handler?: (value: T) => Promise<boolean | void> | void
|
||||
link?: string
|
||||
}
|
||||
|
||||
export interface FormContext<T> {
|
||||
spec: CT.InputSpec
|
||||
buttons: ActionButton<T>[]
|
||||
value?: T
|
||||
patch?: Operation[]
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'app-form',
|
||||
template: `
|
||||
<form
|
||||
[formGroup]="form"
|
||||
(submit.capture.prevent)="(0)"
|
||||
(reset.capture.prevent.stop)="onReset()"
|
||||
(tuiValueChanges)="markAsDirty()"
|
||||
>
|
||||
<form-group [spec]="spec"></form-group>
|
||||
<footer tuiMode="onDark">
|
||||
<ng-content></ng-content>
|
||||
<ng-container *ngFor="let button of buttons; let last = last">
|
||||
<button
|
||||
*ngIf="button.handler; else link"
|
||||
tuiButton
|
||||
[appearance]="last ? 'primary' : 'flat'"
|
||||
[type]="last ? 'submit' : 'button'"
|
||||
(click)="onClick(button.handler)"
|
||||
>
|
||||
{{ button.text }}
|
||||
</button>
|
||||
<ng-template #link>
|
||||
<a
|
||||
tuiButton
|
||||
appearance="flat"
|
||||
[routerLink]="button.link"
|
||||
(click)="close()"
|
||||
>
|
||||
{{ button.text }}
|
||||
</a>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
</footer>
|
||||
</form>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 1rem 0;
|
||||
margin: 1rem 0 -1rem;
|
||||
gap: 1rem;
|
||||
background: var(--tui-elevation-01);
|
||||
border-top: 1px solid var(--tui-base-02);
|
||||
}
|
||||
`,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
ReactiveFormsModule,
|
||||
RouterModule,
|
||||
TuiValueChangesModule,
|
||||
TuiButtonModule,
|
||||
TuiModeModule,
|
||||
FormModule,
|
||||
],
|
||||
providers: [InvalidService],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FormComponent<T extends Record<string, any>> implements OnInit {
|
||||
private readonly dialogFormService = inject(TuiDialogFormService)
|
||||
private readonly formService = inject(FormService)
|
||||
private readonly invalidService = inject(InvalidService)
|
||||
private readonly context = inject<TuiDialogContext<void, FormContext<T>>>(
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
{ optional: true },
|
||||
)
|
||||
|
||||
@Input() spec = this.context?.data.spec || {}
|
||||
@Input() buttons = this.context?.data.buttons || []
|
||||
@Input() patch = this.context?.data.patch || []
|
||||
@Input() value?: T = this.context?.data.value
|
||||
|
||||
form = new FormGroup({})
|
||||
|
||||
ngOnInit() {
|
||||
this.dialogFormService.markAsPristine()
|
||||
this.form = this.formService.createForm(this.spec, this.value)
|
||||
this.process(this.patch)
|
||||
}
|
||||
|
||||
onReset() {
|
||||
const { value } = this.form
|
||||
|
||||
this.form = this.formService.createForm(this.spec)
|
||||
this.process(compare(this.form.value, value))
|
||||
tuiMarkControlAsTouchedAndValidate(this.form)
|
||||
this.markAsDirty()
|
||||
}
|
||||
|
||||
async onClick(handler: Required<ActionButton<T>>['handler']) {
|
||||
tuiMarkControlAsTouchedAndValidate(this.form)
|
||||
this.invalidService.scrollIntoView()
|
||||
|
||||
if (this.form.valid && (await handler(this.form.value as T))) {
|
||||
this.close()
|
||||
}
|
||||
}
|
||||
|
||||
markAsDirty() {
|
||||
this.dialogFormService.markAsDirty()
|
||||
}
|
||||
|
||||
close() {
|
||||
this.context?.$implicit.complete()
|
||||
}
|
||||
|
||||
private process(patch: Operation[]) {
|
||||
patch.forEach(({ op, path }) => {
|
||||
const control = this.form.get(path.substring(1).split('/'))
|
||||
|
||||
if (!control || !control.parent) return
|
||||
|
||||
if (op !== 'remove') {
|
||||
control.markAsDirty()
|
||||
control.markAsTouched()
|
||||
}
|
||||
|
||||
control.parent.markAsDirty()
|
||||
control.parent.markAsTouched()
|
||||
})
|
||||
}
|
||||
}
|
||||
30
web/projects/ui/src/app/components/form/control.directive.ts
Normal file
30
web/projects/ui/src/app/components/form/control.directive.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Directive, ElementRef, inject, OnDestroy, OnInit } from '@angular/core'
|
||||
import { ControlContainer, NgControl } from '@angular/forms'
|
||||
import { InvalidService } from './invalid.service'
|
||||
|
||||
@Directive({
|
||||
selector: 'form-control, form-array, form-object',
|
||||
})
|
||||
export class ControlDirective implements OnInit, OnDestroy {
|
||||
private readonly invalidService = inject(InvalidService, { optional: true })
|
||||
private readonly element: ElementRef<HTMLElement> = inject(ElementRef)
|
||||
private readonly control =
|
||||
inject(NgControl, { optional: true }) ||
|
||||
inject(ControlContainer, { optional: true })
|
||||
|
||||
get invalid(): boolean {
|
||||
return !!this.control?.invalid
|
||||
}
|
||||
|
||||
scrollIntoView() {
|
||||
this.element.nativeElement.scrollIntoView({ behavior: 'smooth' })
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.invalidService?.add(this)
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
this.invalidService?.remove(this)
|
||||
}
|
||||
}
|
||||
35
web/projects/ui/src/app/components/form/control.ts
Normal file
35
web/projects/ui/src/app/components/form/control.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { inject } from '@angular/core'
|
||||
import { FormControlComponent } from './form-control/form-control.component'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
|
||||
export abstract class Control<Spec extends CT.ValueSpec, Value> {
|
||||
private readonly control: FormControlComponent<Spec, Value> =
|
||||
inject(FormControlComponent)
|
||||
|
||||
get invalid(): boolean {
|
||||
return this.control.touched && this.control.invalid
|
||||
}
|
||||
|
||||
get spec(): Spec {
|
||||
return this.control.spec
|
||||
}
|
||||
|
||||
// TODO: Properly handle already set immutable value
|
||||
get readOnly(): boolean {
|
||||
return (
|
||||
!!this.value && !!this.control.control?.pristine && this.control.immutable
|
||||
)
|
||||
}
|
||||
|
||||
get value(): Value | null {
|
||||
return this.control.value
|
||||
}
|
||||
|
||||
set value(value: Value | null) {
|
||||
this.control.onInput(value)
|
||||
}
|
||||
|
||||
onFocus(focused: boolean) {
|
||||
this.control.onFocus(focused)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<div class="label">
|
||||
{{ spec.name }}
|
||||
<tui-tooltip
|
||||
*ngIf="spec.description || spec.disabled"
|
||||
[content]="spec | hint"
|
||||
></tui-tooltip>
|
||||
<button
|
||||
tuiLink
|
||||
type="button"
|
||||
class="add"
|
||||
[disabled]="!canAdd"
|
||||
(click)="add()"
|
||||
>
|
||||
+ Add
|
||||
</button>
|
||||
</div>
|
||||
<tui-error [error]="order | tuiFieldError | async"></tui-error>
|
||||
|
||||
<ng-container *ngFor="let item of array.control.controls; let index = index">
|
||||
<form-object
|
||||
*ngIf="spec.spec.type === 'object'; else control"
|
||||
class="object"
|
||||
[class.object_open]="!!open.get(item)"
|
||||
[formGroup]="$any(item)"
|
||||
[spec]="$any(spec.spec)"
|
||||
[@tuiHeightCollapse]="animation"
|
||||
[@tuiFadeIn]="animation"
|
||||
[open]="!!open.get(item)"
|
||||
(openChange)="open.set(item, $event)"
|
||||
>
|
||||
{{ item.value | mustache : $any(spec.spec).displayAs }}
|
||||
<ng-container *ngTemplateOutlet="remove"></ng-container>
|
||||
</form-object>
|
||||
<ng-template #control>
|
||||
<form-control
|
||||
class="control"
|
||||
tuiTextfieldSize="m"
|
||||
[tuiTextfieldLabelOutside]="true"
|
||||
[tuiTextfieldIcon]="remove"
|
||||
[formControl]="$any(item)"
|
||||
[spec]="$any(spec.spec)"
|
||||
[@tuiHeightCollapse]="animation"
|
||||
[@tuiFadeIn]="animation"
|
||||
></form-control>
|
||||
</ng-template>
|
||||
<ng-template #remove>
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
class="remove"
|
||||
iconLeft="tuiIconTrash"
|
||||
appearance="icon"
|
||||
size="m"
|
||||
title="Remove"
|
||||
(click.stop)="removeAt(index)"
|
||||
></button>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,50 @@
|
||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
margin: 2rem 0;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.add {
|
||||
font-size: 1rem;
|
||||
padding: 0 1rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.object {
|
||||
display: block;
|
||||
position: relative;
|
||||
|
||||
&_open::after,
|
||||
&:last-child::after {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:after {
|
||||
@include transition(opacity);
|
||||
|
||||
content: '';
|
||||
position: absolute;
|
||||
bottom: -0.5rem;
|
||||
height: 1px;
|
||||
left: 3rem;
|
||||
right: 1rem;
|
||||
background: var(--tui-clear);
|
||||
}
|
||||
}
|
||||
|
||||
.remove {
|
||||
margin-left: auto;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.control {
|
||||
display: block;
|
||||
margin: 0.5rem 0;
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { Component, HostBinding, inject, Input } from '@angular/core'
|
||||
import { AbstractControl, FormArrayName } from '@angular/forms'
|
||||
import { TUI_PARENT_ANIMATION, TuiDestroyService } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TUI_ANIMATION_OPTIONS,
|
||||
TuiDialogService,
|
||||
tuiFadeIn,
|
||||
tuiHeightCollapse,
|
||||
} from '@taiga-ui/core'
|
||||
import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { filter, takeUntil } from 'rxjs'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
import { ERRORS } from '../form-group/form-group.component'
|
||||
|
||||
@Component({
|
||||
selector: 'form-array',
|
||||
templateUrl: './form-array.component.html',
|
||||
styleUrls: ['./form-array.component.scss'],
|
||||
animations: [tuiFadeIn, tuiHeightCollapse, TUI_PARENT_ANIMATION],
|
||||
providers: [TuiDestroyService],
|
||||
})
|
||||
export class FormArrayComponent {
|
||||
@Input()
|
||||
spec!: CT.ValueSpecList
|
||||
|
||||
@HostBinding('@tuiParentAnimation')
|
||||
readonly animation = { value: '', ...inject(TUI_ANIMATION_OPTIONS) }
|
||||
readonly order = ERRORS
|
||||
readonly array = inject(FormArrayName)
|
||||
readonly open = new Map<AbstractControl, boolean>()
|
||||
|
||||
private warned = false
|
||||
private readonly formService = inject(FormService)
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly destroy$ = inject(TuiDestroyService)
|
||||
|
||||
get canAdd(): boolean {
|
||||
return (
|
||||
!this.spec.disabled &&
|
||||
(!this.spec.maxLength ||
|
||||
this.spec.maxLength >= this.array.control.controls.length)
|
||||
)
|
||||
}
|
||||
|
||||
add() {
|
||||
if (!this.warned && this.spec.warning) {
|
||||
this.dialogs
|
||||
.open<boolean>(TUI_PROMPT, {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: { content: this.spec.warning, yes: 'Ok', no: 'Cancel' },
|
||||
})
|
||||
.pipe(filter(Boolean), takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
this.addItem()
|
||||
})
|
||||
} else {
|
||||
this.addItem()
|
||||
}
|
||||
|
||||
this.warned = true
|
||||
}
|
||||
|
||||
removeAt(index: number) {
|
||||
this.dialogs
|
||||
.open<boolean>(TUI_PROMPT, {
|
||||
label: 'Confirm',
|
||||
size: 's',
|
||||
data: {
|
||||
content: 'Are you sure you want to delete this entry?',
|
||||
yes: 'Delete',
|
||||
no: 'Cancel',
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean), takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
this.removeItem(index)
|
||||
})
|
||||
}
|
||||
|
||||
private removeItem(index: number) {
|
||||
this.open.delete(this.array.control.at(index))
|
||||
this.array.control.removeAt(index)
|
||||
}
|
||||
|
||||
private addItem() {
|
||||
this.array.control.insert(0, this.formService.getListItem(this.spec))
|
||||
this.open.set(this.array.control.at(0), true)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<tui-input
|
||||
[maskito]="mask"
|
||||
[tuiTextfieldCustomContent]="color"
|
||||
[tuiTextfieldCleaner]="false"
|
||||
[tuiHintContent]="spec | hint"
|
||||
[readOnly]="readOnly"
|
||||
[disabled]="!!spec.disabled"
|
||||
[pseudoInvalid]="invalid"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
</tui-input>
|
||||
<ng-template #color>
|
||||
<div class="wrapper" [style.color]="value">
|
||||
<input
|
||||
*ngIf="!readOnly && !spec.disabled"
|
||||
type="color"
|
||||
class="color"
|
||||
tabindex="-1"
|
||||
[(ngModel)]="value"
|
||||
(click.stop)="(0)"
|
||||
/>
|
||||
<tui-icon
|
||||
icon="tuiIconPaintLarge"
|
||||
tuiAppearance="icon"
|
||||
class="icon"
|
||||
></tui-icon>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,33 @@
|
||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||
|
||||
.wrapper {
|
||||
position: relative;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
pointer-events: auto;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
height: 0.3rem;
|
||||
width: 1.4rem;
|
||||
bottom: 0.125rem;
|
||||
background: currentColor;
|
||||
border-radius: 0.125rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.color {
|
||||
@include fullsize();
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
@include fullsize();
|
||||
pointer-events: none;
|
||||
|
||||
input:hover + & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
import { MaskitoOptions } from '@maskito/core'
|
||||
|
||||
@Component({
|
||||
selector: 'form-color',
|
||||
templateUrl: './form-color.component.html',
|
||||
styleUrls: ['./form-color.component.scss'],
|
||||
})
|
||||
export class FormColorComponent extends Control<CT.ValueSpecColor, string> {
|
||||
readonly mask: MaskitoOptions = {
|
||||
mask: ['#', ...Array(6).fill(/[0-9a-f]/i)],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<ng-container [ngSwitch]="spec.type">
|
||||
<form-color *ngSwitchCase="'color'"></form-color>
|
||||
<form-datetime *ngSwitchCase="'datetime'"></form-datetime>
|
||||
<form-multiselect *ngSwitchCase="'multiselect'"></form-multiselect>
|
||||
<form-number *ngSwitchCase="'number'"></form-number>
|
||||
<form-select *ngSwitchCase="'select'"></form-select>
|
||||
<form-text *ngSwitchCase="'text'"></form-text>
|
||||
<form-textarea *ngSwitchCase="'textarea'"></form-textarea>
|
||||
<form-toggle *ngSwitchCase="'toggle'"></form-toggle>
|
||||
</ng-container>
|
||||
<tui-error [error]="order | tuiFieldError | async"></tui-error>
|
||||
<ng-template
|
||||
*ngIf="spec.warning || immutable"
|
||||
#warning
|
||||
let-completeWith="completeWith"
|
||||
>
|
||||
{{ spec.warning }}
|
||||
<p *ngIf="immutable">This value cannot be changed once set!</p>
|
||||
<div class="buttons">
|
||||
<button
|
||||
tuiButton
|
||||
type="button"
|
||||
appearance="secondary"
|
||||
size="s"
|
||||
(click)="completeWith(true)"
|
||||
>
|
||||
Rollback
|
||||
</button>
|
||||
<button
|
||||
tuiButton
|
||||
type="button"
|
||||
appearance="flat"
|
||||
size="s"
|
||||
(click)="completeWith(false)"
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,11 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.buttons {
|
||||
margin-top: 0.5rem;
|
||||
|
||||
:first-child {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
TemplateRef,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { AbstractTuiNullableControl } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiAlertService,
|
||||
TuiDialogContext,
|
||||
TuiNotification,
|
||||
} from '@taiga-ui/core'
|
||||
import { filter, takeUntil } from 'rxjs'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { ERRORS } from '../form-group/form-group.component'
|
||||
import { FORM_CONTROL_PROVIDERS } from './form-control.providers'
|
||||
|
||||
@Component({
|
||||
selector: 'form-control',
|
||||
templateUrl: './form-control.component.html',
|
||||
styleUrls: ['./form-control.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
providers: FORM_CONTROL_PROVIDERS,
|
||||
})
|
||||
export class FormControlComponent<
|
||||
T extends CT.ValueSpec,
|
||||
V,
|
||||
> extends AbstractTuiNullableControl<V> {
|
||||
@Input()
|
||||
spec!: T
|
||||
|
||||
@ViewChild('warning')
|
||||
warning?: TemplateRef<TuiDialogContext<boolean>>
|
||||
|
||||
warned = false
|
||||
focused = false
|
||||
readonly order = ERRORS
|
||||
private readonly alerts = inject(TuiAlertService)
|
||||
|
||||
get immutable(): boolean {
|
||||
return 'immutable' in this.spec && this.spec.immutable
|
||||
}
|
||||
|
||||
onFocus(focused: boolean) {
|
||||
this.focused = focused
|
||||
this.updateFocused(focused)
|
||||
}
|
||||
|
||||
onInput(value: V | null) {
|
||||
const previous = this.value
|
||||
|
||||
if (!this.warned && this.warning) {
|
||||
this.alerts
|
||||
.open<boolean>(this.warning, {
|
||||
label: 'Warning',
|
||||
status: TuiNotification.Warning,
|
||||
hasCloseButton: false,
|
||||
autoClose: false,
|
||||
})
|
||||
.pipe(filter(Boolean), takeUntil(this.destroy$))
|
||||
.subscribe(() => {
|
||||
this.value = previous
|
||||
})
|
||||
}
|
||||
|
||||
this.warned = true
|
||||
this.value = value === '' ? null : value
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { forwardRef, Provider } from '@angular/core'
|
||||
import { TUI_VALIDATION_ERRORS } from '@taiga-ui/kit'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { FormControlComponent } from './form-control.component'
|
||||
|
||||
interface ValidatorsPatternError {
|
||||
actualValue: string
|
||||
requiredPattern: string | RegExp
|
||||
}
|
||||
|
||||
export const FORM_CONTROL_PROVIDERS: Provider[] = [
|
||||
{
|
||||
provide: TUI_VALIDATION_ERRORS,
|
||||
deps: [forwardRef(() => FormControlComponent)],
|
||||
useFactory: (control: FormControlComponent<CT.ValueSpec, string>) => ({
|
||||
required: 'Required',
|
||||
pattern: ({ requiredPattern }: ValidatorsPatternError) =>
|
||||
('patterns' in control.spec &&
|
||||
control.spec.patterns.find(
|
||||
({ regex }) => String(regex) === String(requiredPattern),
|
||||
)?.description) ||
|
||||
'Invalid format',
|
||||
}),
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,43 @@
|
||||
<ng-container [ngSwitch]="spec.inputmode" [tuiHintContent]="spec.description">
|
||||
<tui-input-time
|
||||
*ngSwitchCase="'time'"
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[ngModel]="getTime(value)"
|
||||
(ngModelChange)="value = $event?.toString() || null"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
</tui-input-time>
|
||||
<tui-input-date
|
||||
*ngSwitchCase="'date'"
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[min]="spec.min ? (spec.min | tuiMapper : getLimit)[0] : min"
|
||||
[max]="spec.max ? (spec.max | tuiMapper : getLimit)[0] : max"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
</tui-input-date>
|
||||
<tui-input-date-time
|
||||
*ngSwitchCase="'datetime-local'"
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[min]="spec.min ? (spec.min | tuiMapper : getLimit) : min"
|
||||
[max]="spec.max ? (spec.max | tuiMapper : getLimit) : max"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
</tui-input-date-time>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,36 @@
|
||||
import { Component } from '@angular/core'
|
||||
import {
|
||||
TUI_FIRST_DAY,
|
||||
TUI_LAST_DAY,
|
||||
TuiDay,
|
||||
tuiPure,
|
||||
TuiTime,
|
||||
} from '@taiga-ui/cdk'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
selector: 'form-datetime',
|
||||
templateUrl: './form-datetime.component.html',
|
||||
})
|
||||
export class FormDatetimeComponent extends Control<
|
||||
CT.ValueSpecDatetime,
|
||||
string
|
||||
> {
|
||||
readonly min = TUI_FIRST_DAY
|
||||
readonly max = TUI_LAST_DAY
|
||||
|
||||
@tuiPure
|
||||
getTime(value: string | null) {
|
||||
return value ? TuiTime.fromString(value) : null
|
||||
}
|
||||
|
||||
getLimit(limit: string): [TuiDay, TuiTime] {
|
||||
return [
|
||||
TuiDay.jsonParse(limit.slice(0, 10)),
|
||||
limit.length === 10
|
||||
? new TuiTime(0, 0)
|
||||
: TuiTime.fromString(limit.slice(-5)),
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
<ng-container
|
||||
*ngFor="let entry of spec | keyvalue : asIsOrder"
|
||||
tuiMode="onDark"
|
||||
[ngSwitch]="entry.value.type"
|
||||
[tuiTextfieldCleaner]="true"
|
||||
>
|
||||
<form-object
|
||||
*ngSwitchCase="'object'"
|
||||
class="g-form-control"
|
||||
[formGroupName]="entry.key"
|
||||
[spec]="$any(entry.value)"
|
||||
></form-object>
|
||||
<form-union
|
||||
*ngSwitchCase="'union'"
|
||||
class="g-form-control"
|
||||
[formGroupName]="entry.key"
|
||||
[spec]="$any(entry.value)"
|
||||
></form-union>
|
||||
<form-array
|
||||
*ngSwitchCase="'list'"
|
||||
[formArrayName]="entry.key"
|
||||
[spec]="$any(entry.value)"
|
||||
></form-array>
|
||||
<form-control
|
||||
*ngSwitchDefault
|
||||
class="g-form-control"
|
||||
[formControlName]="entry.key"
|
||||
[spec]="entry.value"
|
||||
></form-control>
|
||||
</ng-container>
|
||||
@@ -0,0 +1,35 @@
|
||||
form-group .g-form-control:not(:first-child) {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
form-group .g-form-group {
|
||||
position: relative;
|
||||
padding-left: var(--tui-height-m);
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
background: var(--tui-clear);
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 0;
|
||||
left: calc(1rem - 1px);
|
||||
bottom: 0.5rem;
|
||||
width: 2px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
left: 0.75rem;
|
||||
bottom: 0;
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
border-radius: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
form-group tui-tooltip {
|
||||
z-index: 1;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { FORM_GROUP_PROVIDERS } from './form-group.providers'
|
||||
|
||||
export const ERRORS = [
|
||||
'required',
|
||||
'pattern',
|
||||
'notNumber',
|
||||
'numberNotInteger',
|
||||
'numberNotInRange',
|
||||
'listNotUnique',
|
||||
'listNotInRange',
|
||||
'listItemIssue',
|
||||
]
|
||||
|
||||
@Component({
|
||||
selector: 'form-group',
|
||||
templateUrl: './form-group.component.html',
|
||||
styleUrls: ['./form-group.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
viewProviders: [FORM_GROUP_PROVIDERS],
|
||||
})
|
||||
export class FormGroupComponent {
|
||||
@Input() spec: CT.InputSpec = {}
|
||||
|
||||
asIsOrder() {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Provider, SkipSelf } from '@angular/core'
|
||||
import {
|
||||
TUI_ARROW_MODE,
|
||||
tuiInputDateOptionsProvider,
|
||||
tuiInputTimeOptionsProvider,
|
||||
} from '@taiga-ui/kit'
|
||||
import { TUI_DEFAULT_ERROR_MESSAGE } from '@taiga-ui/core'
|
||||
import { ControlContainer } from '@angular/forms'
|
||||
import { identity, of } from 'rxjs'
|
||||
|
||||
export const FORM_GROUP_PROVIDERS: Provider[] = [
|
||||
{
|
||||
provide: TUI_DEFAULT_ERROR_MESSAGE,
|
||||
useValue: of('Unknown error'),
|
||||
},
|
||||
{
|
||||
provide: ControlContainer,
|
||||
deps: [[new SkipSelf(), ControlContainer]],
|
||||
useFactory: identity,
|
||||
},
|
||||
{
|
||||
provide: TUI_ARROW_MODE,
|
||||
useValue: {
|
||||
interactive: null,
|
||||
disabled: null,
|
||||
},
|
||||
},
|
||||
tuiInputDateOptionsProvider({
|
||||
nativePicker: true,
|
||||
}),
|
||||
tuiInputTimeOptionsProvider({
|
||||
nativePicker: true,
|
||||
}),
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
<tui-multi-select
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[editable]="false"
|
||||
[disabledItemHandler]="disabledItemHandler"
|
||||
[(ngModel)]="selected"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<select
|
||||
tuiSelect
|
||||
multiple
|
||||
[items]="items"
|
||||
[disabledItemHandler]="disabledItemHandler"
|
||||
></select>
|
||||
</tui-multi-select>
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
import { invert } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
selector: 'form-multiselect',
|
||||
templateUrl: './form-multiselect.component.html',
|
||||
})
|
||||
export class FormMultiselectComponent extends Control<
|
||||
CT.ValueSpecMultiselect,
|
||||
readonly string[]
|
||||
> {
|
||||
private readonly inverted = invert(this.spec.values)
|
||||
|
||||
private readonly isDisabled = (item: string) =>
|
||||
Array.isArray(this.spec.disabled) &&
|
||||
this.spec.disabled.includes(this.inverted[item])
|
||||
|
||||
private readonly isExceedingLimit = (item: string) =>
|
||||
!!this.spec.maxLength &&
|
||||
this.selected.length >= this.spec.maxLength &&
|
||||
!this.selected.includes(item)
|
||||
|
||||
readonly disabledItemHandler = (item: string): boolean =>
|
||||
this.isDisabled(item) || this.isExceedingLimit(item)
|
||||
|
||||
readonly items = Object.values(this.spec.values)
|
||||
|
||||
get disabled(): boolean {
|
||||
return typeof this.spec.disabled === 'string'
|
||||
}
|
||||
|
||||
get selected(): string[] {
|
||||
return this.memoize(this.value)
|
||||
}
|
||||
|
||||
set selected(value: string[]) {
|
||||
this.value = Object.entries(this.spec.values)
|
||||
.filter(([_, v]) => value.includes(v))
|
||||
.map(([k]) => k)
|
||||
}
|
||||
|
||||
@tuiPure
|
||||
private memoize(value: null | readonly string[]): string[] {
|
||||
return value?.map(key => this.spec.values[key]) || []
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<tui-input-number
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[tuiTextfieldPostfix]="spec.units || ''"
|
||||
[pseudoInvalid]="invalid"
|
||||
[precision]="Infinity"
|
||||
[decimal]="spec.integer ? 'never' : 'not-zero'"
|
||||
[min]="spec.min ?? -Infinity"
|
||||
[max]="spec.max ?? Infinity"
|
||||
[step]="spec.step || 0"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
<input tuiTextfield [placeholder]="spec.placeholder || ''" />
|
||||
</tui-input-number>
|
||||
@@ -0,0 +1,11 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
selector: 'form-number',
|
||||
templateUrl: './form-number.component.html',
|
||||
})
|
||||
export class FormNumberComponent extends Control<CT.ValueSpecNumber, number> {
|
||||
protected readonly Infinity = Infinity
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<h3 class="title" (click)="toggle()">
|
||||
<button
|
||||
tuiIconButton
|
||||
size="s"
|
||||
iconLeft="tuiIconChevronDown"
|
||||
type="button"
|
||||
class="button"
|
||||
[class.button_open]="open"
|
||||
[style.border-radius.%]="100"
|
||||
[appearance]="invalid ? 'destructive' : 'secondary'"
|
||||
></button>
|
||||
<ng-content></ng-content>
|
||||
{{ spec.name }}
|
||||
<tui-tooltip
|
||||
*ngIf="spec.description"
|
||||
[content]="spec.description"
|
||||
(click.stop)="(0)"
|
||||
></tui-tooltip>
|
||||
</h3>
|
||||
|
||||
<tui-expand class="expand" [expanded]="open">
|
||||
<div class="g-form-group" [class.g-form-group_invalid]="invalid">
|
||||
<form-group [spec]="spec.spec"></form-group>
|
||||
</div>
|
||||
</tui-expand>
|
||||
@@ -0,0 +1,41 @@
|
||||
@import '@taiga-ui/core/styles/taiga-ui-local';
|
||||
|
||||
:host {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.title {
|
||||
position: relative;
|
||||
height: var(--tui-height-l);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
font: var(--tui-font-text-l);
|
||||
font-weight: bold;
|
||||
margin: 0 0 -0.75rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
@include transition(transform);
|
||||
|
||||
margin-right: 1rem;
|
||||
|
||||
&_open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.expand {
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.g-form-group {
|
||||
padding-top: 0.75rem;
|
||||
|
||||
&_invalid::before,
|
||||
&_invalid::after {
|
||||
background: var(--tui-error-bg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
EventEmitter,
|
||||
inject,
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import { ControlContainer } from '@angular/forms'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
|
||||
@Component({
|
||||
selector: 'form-object',
|
||||
templateUrl: './form-object.component.html',
|
||||
styleUrls: ['./form-object.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class FormObjectComponent {
|
||||
@Input()
|
||||
spec!: CT.ValueSpecObject
|
||||
|
||||
@Input()
|
||||
open = false
|
||||
|
||||
@Output()
|
||||
readonly openChange = new EventEmitter<boolean>()
|
||||
|
||||
private readonly container = inject(ControlContainer)
|
||||
|
||||
get invalid() {
|
||||
return !this.container.valid && this.container.touched
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.open = !this.open
|
||||
this.openChange.emit(this.open)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<tui-select
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="disabled"
|
||||
[readOnly]="readOnly"
|
||||
[tuiTextfieldCleaner]="!spec.required"
|
||||
[pseudoInvalid]="invalid"
|
||||
[(ngModel)]="selected"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
<select
|
||||
tuiSelect
|
||||
[placeholder]="spec.name"
|
||||
[items]="items"
|
||||
[disabledItemHandler]="disabledItemHandler"
|
||||
></select>
|
||||
</tui-select>
|
||||
@@ -0,0 +1,30 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { invert } from '@start9labs/shared'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
selector: 'form-select',
|
||||
templateUrl: './form-select.component.html',
|
||||
})
|
||||
export class FormSelectComponent extends Control<CT.ValueSpecSelect, string> {
|
||||
private readonly inverted = invert(this.spec.values)
|
||||
|
||||
readonly items = Object.values(this.spec.values)
|
||||
|
||||
readonly disabledItemHandler = (item: string) =>
|
||||
Array.isArray(this.spec.disabled) &&
|
||||
this.spec.disabled.includes(this.inverted[item])
|
||||
|
||||
get disabled(): boolean {
|
||||
return typeof this.spec.disabled === 'string'
|
||||
}
|
||||
|
||||
get selected(): string | null {
|
||||
return (this.value && this.spec.values[this.value]) || null
|
||||
}
|
||||
|
||||
set selected(value: string | null) {
|
||||
this.value = (value && this.inverted[value]) || null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<tui-input
|
||||
[tuiTextfieldCustomContent]="spec.masked || spec.generate ? toggle : ''"
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
<input
|
||||
tuiTextfield
|
||||
[class.masked]="spec.masked && masked"
|
||||
[placeholder]="spec.placeholder || ''"
|
||||
[attr.minLength]="spec.minLength"
|
||||
[attr.maxLength]="spec.maxLength"
|
||||
[attr.inputmode]="spec.inputmode"
|
||||
/>
|
||||
</tui-input>
|
||||
<ng-template #toggle>
|
||||
<button
|
||||
*ngIf="spec.generate"
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
title="Generate"
|
||||
size="xs"
|
||||
class="button"
|
||||
iconLeft="tuiIconRefreshCcw"
|
||||
(click)="generate()"
|
||||
></button>
|
||||
<button
|
||||
*ngIf="spec.masked"
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
title="Toggle masking"
|
||||
size="xs"
|
||||
class="button"
|
||||
[iconLeft]="masked ? 'tuiIconEye' : 'tuiIconEyeOff'"
|
||||
(click)="masked = !masked"
|
||||
></button>
|
||||
</ng-template>
|
||||
@@ -0,0 +1,8 @@
|
||||
.button {
|
||||
pointer-events: auto;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.masked {
|
||||
-webkit-text-security: disc;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT, utils } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
selector: 'form-text',
|
||||
templateUrl: './form-text.component.html',
|
||||
styleUrls: ['./form-text.component.scss'],
|
||||
})
|
||||
export class FormTextComponent extends Control<CT.ValueSpecText, string> {
|
||||
masked = true
|
||||
|
||||
generate() {
|
||||
this.value = utils.getDefaultString(this.spec.generate || '')
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<tui-textarea
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="!!spec.disabled"
|
||||
[readOnly]="readOnly"
|
||||
[pseudoInvalid]="invalid"
|
||||
[expandable]="true"
|
||||
[rows]="6"
|
||||
[maxLength]="spec.maxLength"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
<textarea tuiTextfield [placeholder]="spec.placeholder || ''"></textarea>
|
||||
</tui-textarea>
|
||||
@@ -0,0 +1,12 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
selector: 'form-textarea',
|
||||
templateUrl: './form-textarea.component.html',
|
||||
})
|
||||
export class FormTextareaComponent extends Control<
|
||||
CT.ValueSpecTextarea,
|
||||
string
|
||||
> {}
|
||||
@@ -0,0 +1,11 @@
|
||||
{{ spec.name }}
|
||||
<tui-tooltip
|
||||
*ngIf="spec.description || spec.disabled"
|
||||
[tuiHintContent]="spec | hint"
|
||||
></tui-tooltip>
|
||||
<tui-toggle
|
||||
size="l"
|
||||
[disabled]="!!spec.disabled || readOnly"
|
||||
[(ngModel)]="value"
|
||||
(focusedChange)="onFocus($event)"
|
||||
></tui-toggle>
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
selector: 'form-toggle',
|
||||
templateUrl: './form-toggle.component.html',
|
||||
host: { class: 'g-toggle' },
|
||||
})
|
||||
export class FormToggleComponent extends Control<CT.ValueSpecToggle, boolean> {}
|
||||
@@ -0,0 +1,11 @@
|
||||
<form-control
|
||||
[spec]="selectSpec"
|
||||
formControlName="selection"
|
||||
(tuiValueChanges)="onUnion($event)"
|
||||
></form-control>
|
||||
<tui-elastic-container class="g-form-group" formGroupName="value">
|
||||
<form-group
|
||||
class="group"
|
||||
[spec]="(union && spec.variants[union].spec) || {}"
|
||||
></form-group>
|
||||
</tui-elastic-container>
|
||||
@@ -0,0 +1,8 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.group {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
OnChanges,
|
||||
} from '@angular/core'
|
||||
import { ControlContainer, FormGroupName } from '@angular/forms'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
|
||||
@Component({
|
||||
selector: 'form-union',
|
||||
templateUrl: './form-union.component.html',
|
||||
styleUrls: ['./form-union.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
viewProviders: [
|
||||
{
|
||||
provide: ControlContainer,
|
||||
useExisting: FormGroupName,
|
||||
},
|
||||
],
|
||||
})
|
||||
export class FormUnionComponent implements OnChanges {
|
||||
@Input()
|
||||
spec!: CT.ValueSpecUnion
|
||||
|
||||
selectSpec!: CT.ValueSpecSelect
|
||||
|
||||
private readonly form = inject(FormGroupName)
|
||||
private readonly formService = inject(FormService)
|
||||
|
||||
get union(): string {
|
||||
return this.form.value.selection
|
||||
}
|
||||
|
||||
@tuiPure
|
||||
onUnion(union: string) {
|
||||
this.form.control.setControl(
|
||||
'value',
|
||||
this.formService.getFormGroup(
|
||||
union ? this.spec.variants[union].spec : {},
|
||||
),
|
||||
{
|
||||
emitEvent: false,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
ngOnChanges() {
|
||||
this.selectSpec = this.formService.getUnionSelectSpec(this.spec, this.union)
|
||||
if (this.union) this.onUnion(this.union)
|
||||
}
|
||||
}
|
||||
107
web/projects/ui/src/app/components/form/form.module.ts
Normal file
107
web/projects/ui/src/app/components/form/form.module.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
|
||||
import { MaskitoModule } from '@maskito/angular'
|
||||
import { TuiMapperPipeModule, TuiValueChangesModule } from '@taiga-ui/cdk'
|
||||
import {
|
||||
TuiErrorModule,
|
||||
TuiExpandModule,
|
||||
TuiHintModule,
|
||||
TuiLinkModule,
|
||||
TuiModeModule,
|
||||
TuiTextfieldControllerModule,
|
||||
TuiTooltipModule,
|
||||
} from '@taiga-ui/core'
|
||||
import {
|
||||
TuiAppearanceModule,
|
||||
TuiButtonModule,
|
||||
TuiIconModule,
|
||||
} from '@taiga-ui/experimental'
|
||||
import {
|
||||
TuiElasticContainerModule,
|
||||
TuiFieldErrorPipeModule,
|
||||
TuiInputDateModule,
|
||||
TuiInputDateTimeModule,
|
||||
TuiInputFilesModule,
|
||||
TuiInputModule,
|
||||
TuiInputNumberModule,
|
||||
TuiInputTimeModule,
|
||||
TuiMultiSelectModule,
|
||||
TuiPromptModule,
|
||||
TuiSelectModule,
|
||||
TuiTagModule,
|
||||
TuiTextareaModule,
|
||||
TuiToggleModule,
|
||||
} from '@taiga-ui/kit'
|
||||
|
||||
import { FormGroupComponent } from './form-group/form-group.component'
|
||||
import { FormTextComponent } from './form-text/form-text.component'
|
||||
import { FormToggleComponent } from './form-toggle/form-toggle.component'
|
||||
import { FormTextareaComponent } from './form-textarea/form-textarea.component'
|
||||
import { FormNumberComponent } from './form-number/form-number.component'
|
||||
import { FormSelectComponent } from './form-select/form-select.component'
|
||||
import { FormMultiselectComponent } from './form-multiselect/form-multiselect.component'
|
||||
import { FormUnionComponent } from './form-union/form-union.component'
|
||||
import { FormObjectComponent } from './form-object/form-object.component'
|
||||
import { FormArrayComponent } from './form-array/form-array.component'
|
||||
import { FormControlComponent } from './form-control/form-control.component'
|
||||
import { MustachePipe } from './mustache.pipe'
|
||||
import { ControlDirective } from './control.directive'
|
||||
import { FormColorComponent } from './form-color/form-color.component'
|
||||
import { FormDatetimeComponent } from './form-datetime/form-datetime.component'
|
||||
import { HintPipe } from './hint.pipe'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
ReactiveFormsModule,
|
||||
TuiInputModule,
|
||||
TuiInputNumberModule,
|
||||
TuiInputFilesModule,
|
||||
TuiTextareaModule,
|
||||
TuiSelectModule,
|
||||
TuiMultiSelectModule,
|
||||
TuiToggleModule,
|
||||
TuiTooltipModule,
|
||||
TuiHintModule,
|
||||
TuiModeModule,
|
||||
TuiTagModule,
|
||||
TuiButtonModule,
|
||||
TuiExpandModule,
|
||||
TuiTextfieldControllerModule,
|
||||
TuiLinkModule,
|
||||
TuiPromptModule,
|
||||
TuiErrorModule,
|
||||
TuiFieldErrorPipeModule,
|
||||
TuiValueChangesModule,
|
||||
TuiElasticContainerModule,
|
||||
MaskitoModule,
|
||||
TuiIconModule,
|
||||
TuiAppearanceModule,
|
||||
TuiInputDateModule,
|
||||
TuiInputTimeModule,
|
||||
TuiInputDateTimeModule,
|
||||
TuiMapperPipeModule,
|
||||
],
|
||||
declarations: [
|
||||
FormGroupComponent,
|
||||
FormControlComponent,
|
||||
FormColorComponent,
|
||||
FormDatetimeComponent,
|
||||
FormTextComponent,
|
||||
FormToggleComponent,
|
||||
FormTextareaComponent,
|
||||
FormNumberComponent,
|
||||
FormSelectComponent,
|
||||
FormMultiselectComponent,
|
||||
FormUnionComponent,
|
||||
FormObjectComponent,
|
||||
FormArrayComponent,
|
||||
MustachePipe,
|
||||
HintPipe,
|
||||
ControlDirective,
|
||||
],
|
||||
exports: [FormGroupComponent],
|
||||
})
|
||||
export class FormModule {}
|
||||
21
web/projects/ui/src/app/components/form/hint.pipe.ts
Normal file
21
web/projects/ui/src/app/components/form/hint.pipe.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
|
||||
@Pipe({
|
||||
name: 'hint',
|
||||
})
|
||||
export class HintPipe implements PipeTransform {
|
||||
transform(spec: CT.ValueSpec): string {
|
||||
const hint = []
|
||||
|
||||
if (spec.description) {
|
||||
hint.push(spec.description)
|
||||
}
|
||||
|
||||
if ('disabled' in spec && typeof spec.disabled === 'string') {
|
||||
hint.push(`Disabled: ${spec.disabled}`)
|
||||
}
|
||||
|
||||
return hint.join('\n\n')
|
||||
}
|
||||
}
|
||||
19
web/projects/ui/src/app/components/form/invalid.service.ts
Normal file
19
web/projects/ui/src/app/components/form/invalid.service.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { ControlDirective } from './control.directive'
|
||||
|
||||
@Injectable()
|
||||
export class InvalidService {
|
||||
private readonly controls: ControlDirective[] = []
|
||||
|
||||
scrollIntoView() {
|
||||
this.controls.find(({ invalid }) => invalid)?.scrollIntoView()
|
||||
}
|
||||
|
||||
add(control: ControlDirective) {
|
||||
this.controls.push(control)
|
||||
}
|
||||
|
||||
remove(control: ControlDirective) {
|
||||
this.controls.splice(this.controls.indexOf(control), 1)
|
||||
}
|
||||
}
|
||||
12
web/projects/ui/src/app/components/form/mustache.pipe.ts
Normal file
12
web/projects/ui/src/app/components/form/mustache.pipe.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
|
||||
const Mustache = require('mustache')
|
||||
|
||||
@Pipe({
|
||||
name: 'mustache',
|
||||
})
|
||||
export class MustachePipe implements PipeTransform {
|
||||
transform(value: any, displayAs: string): string {
|
||||
return displayAs && Mustache.render(displayAs, value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { BackupServerSelectModal } from './backup-server-select.page'
|
||||
import { AppRecoverSelectPageModule } from 'src/app/modals/app-recover-select/app-recover-select.module'
|
||||
|
||||
@NgModule({
|
||||
declarations: [BackupServerSelectModal],
|
||||
imports: [CommonModule, FormsModule, IonicModule, AppRecoverSelectPageModule],
|
||||
exports: [BackupServerSelectModal],
|
||||
})
|
||||
export class BackupServerSelectModule {}
|
||||
@@ -0,0 +1,35 @@
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>Select Server Backup</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="dismiss()">
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<ion-item-group>
|
||||
<ion-item
|
||||
*ngFor="let server of target.entry.startOs | keyvalue"
|
||||
button
|
||||
(click)="presentModalPassword(server.key, server.value)"
|
||||
>
|
||||
<ion-label>
|
||||
<h2>
|
||||
<b>Local Hostname</b>
|
||||
: {{ server.value.hostname }}.local
|
||||
</h2>
|
||||
<h2>
|
||||
<b>StartOS Version</b>
|
||||
: {{ server.value.version }}
|
||||
</h2>
|
||||
<h2>
|
||||
<b>Created</b>
|
||||
: {{ server.value.timestamp | date : 'medium' }}
|
||||
</h2>
|
||||
</ion-label>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
@@ -0,0 +1,118 @@
|
||||
import { Component, Input } from '@angular/core'
|
||||
import { ModalController, NavController } from '@ionic/angular'
|
||||
import * as argon2 from '@start9labs/argon2'
|
||||
import {
|
||||
ErrorService,
|
||||
LoadingService,
|
||||
StartOSDiskInfo,
|
||||
} from '@start9labs/shared'
|
||||
import {
|
||||
PasswordPromptComponent,
|
||||
PromptOptions,
|
||||
} from 'src/app/modals/password-prompt.component'
|
||||
import {
|
||||
BackupInfo,
|
||||
CifsBackupTarget,
|
||||
DiskBackupTarget,
|
||||
} from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { MappedBackupTarget } from 'src/app/types/mapped-backup-target'
|
||||
import { AppRecoverSelectPage } from '../app-recover-select/app-recover-select.page'
|
||||
|
||||
@Component({
|
||||
selector: 'backup-server-select',
|
||||
templateUrl: 'backup-server-select.page.html',
|
||||
styleUrls: ['backup-server-select.page.scss'],
|
||||
})
|
||||
export class BackupServerSelectModal {
|
||||
@Input() target!: MappedBackupTarget<CifsBackupTarget | DiskBackupTarget>
|
||||
|
||||
constructor(
|
||||
private readonly modalCtrl: ModalController,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly api: ApiService,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly errorService: ErrorService,
|
||||
) {}
|
||||
|
||||
dismiss() {
|
||||
this.modalCtrl.dismiss()
|
||||
}
|
||||
|
||||
async presentModalPassword(
|
||||
serverId: string,
|
||||
{ passwordHash }: StartOSDiskInfo,
|
||||
): Promise<void> {
|
||||
const options: PromptOptions = {
|
||||
title: 'Password Required',
|
||||
message:
|
||||
'Enter the password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.',
|
||||
label: 'Decrypt Backup',
|
||||
placeholder: 'Enter password',
|
||||
buttonText: 'Next',
|
||||
}
|
||||
const modal = await this.modalCtrl.create({
|
||||
component: PasswordPromptComponent,
|
||||
componentProps: { options },
|
||||
canDismiss: async password => {
|
||||
if (password === null) {
|
||||
return true
|
||||
}
|
||||
|
||||
try {
|
||||
argon2.verify(passwordHash!, password)
|
||||
await this.restoreFromBackup(serverId, password)
|
||||
return true
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
return false
|
||||
}
|
||||
},
|
||||
})
|
||||
modal.present()
|
||||
}
|
||||
|
||||
private async restoreFromBackup(
|
||||
serverId: string,
|
||||
password: string,
|
||||
): Promise<void> {
|
||||
const loader = this.loader.open('Decrypting drive...').subscribe()
|
||||
|
||||
try {
|
||||
const backupInfo = await this.api.getBackupInfo({
|
||||
targetId: this.target.id,
|
||||
serverId,
|
||||
password,
|
||||
})
|
||||
this.presentModalSelect(serverId, backupInfo, password)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async presentModalSelect(
|
||||
serverId: string,
|
||||
backupInfo: BackupInfo,
|
||||
password: string,
|
||||
): Promise<void> {
|
||||
const modal = await this.modalCtrl.create({
|
||||
componentProps: {
|
||||
targetId: this.target.id,
|
||||
serverId,
|
||||
backupInfo,
|
||||
password,
|
||||
},
|
||||
presentingElement: await this.modalCtrl.getTop(),
|
||||
component: AppRecoverSelectPage,
|
||||
})
|
||||
|
||||
modal.onDidDismiss().then(res => {
|
||||
if (res.role === 'success') {
|
||||
this.modalCtrl.dismiss(undefined, 'success')
|
||||
this.navCtrl.navigateRoot('/services')
|
||||
}
|
||||
})
|
||||
|
||||
await modal.present()
|
||||
}
|
||||
}
|
||||
104
web/projects/ui/src/app/modals/config-dep.component.ts
Normal file
104
web/projects/ui/src/app/modals/config-dep.component.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
} from '@angular/core'
|
||||
import { compare, getValueByPointer, Operation } from 'fast-json-patch'
|
||||
import { isObject } from '@start9labs/shared'
|
||||
import { tuiIsNumber } from '@taiga-ui/cdk'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { TuiNotificationModule } from '@taiga-ui/core'
|
||||
|
||||
@Component({
|
||||
selector: 'config-dep',
|
||||
template: `
|
||||
<tui-notification>
|
||||
<h3 style="margin: 0 0 0.5rem; font-size: 1.25rem;">
|
||||
{{ package }}
|
||||
</h3>
|
||||
The following modifications have been made to {{ package }} to satisfy
|
||||
{{ dep }}:
|
||||
<ul>
|
||||
<li *ngFor="let d of diff" [innerHTML]="d"></li>
|
||||
</ul>
|
||||
To accept these modifications, click "Save".
|
||||
</tui-notification>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [CommonModule, TuiNotificationModule],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ConfigDepComponent implements OnChanges {
|
||||
@Input()
|
||||
package = ''
|
||||
|
||||
@Input()
|
||||
dep = ''
|
||||
|
||||
@Input()
|
||||
original: object = {}
|
||||
|
||||
@Input()
|
||||
value: object = {}
|
||||
|
||||
diff: string[] = []
|
||||
|
||||
ngOnChanges() {
|
||||
this.diff = compare(this.original, this.value).map(
|
||||
op => `${this.getPath(op)}: ${this.getMessage(op)}`,
|
||||
)
|
||||
}
|
||||
|
||||
private getPath(operation: Operation): string {
|
||||
const path = operation.path
|
||||
.substring(1)
|
||||
.split('/')
|
||||
.map(node => {
|
||||
const num = Number(node)
|
||||
return isNaN(num) ? node : num
|
||||
})
|
||||
|
||||
if (tuiIsNumber(path[path.length - 1])) {
|
||||
path.pop()
|
||||
}
|
||||
|
||||
return path.join(' → ')
|
||||
}
|
||||
|
||||
private getMessage(operation: Operation): string {
|
||||
switch (operation.op) {
|
||||
case 'add':
|
||||
return `Added ${this.getNewValue(operation.value)}`
|
||||
case 'remove':
|
||||
return `Removed ${this.getOldValue(operation.path)}`
|
||||
case 'replace':
|
||||
return `Changed from ${this.getOldValue(
|
||||
operation.path,
|
||||
)} to ${this.getNewValue(operation.value)}`
|
||||
default:
|
||||
return `Unknown operation`
|
||||
}
|
||||
}
|
||||
|
||||
private getOldValue(path: any): string {
|
||||
const val = getValueByPointer(this.original, path)
|
||||
if (['string', 'number', 'boolean'].includes(typeof val)) {
|
||||
return val
|
||||
} else if (isObject(val)) {
|
||||
return 'entry'
|
||||
} else {
|
||||
return 'list'
|
||||
}
|
||||
}
|
||||
|
||||
private getNewValue(val: any): string {
|
||||
if (['string', 'number', 'boolean'].includes(typeof val)) {
|
||||
return val
|
||||
} else if (isObject(val)) {
|
||||
return 'new entry'
|
||||
} else {
|
||||
return 'new list'
|
||||
}
|
||||
}
|
||||
}
|
||||
261
web/projects/ui/src/app/modals/config.component.ts
Normal file
261
web/projects/ui/src/app/modals/config.component.ts
Normal file
@@ -0,0 +1,261 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, Inject, ViewChild } from '@angular/core'
|
||||
import {
|
||||
ErrorService,
|
||||
getErrorMessage,
|
||||
isEmptyObject,
|
||||
LoadingService,
|
||||
} from '@start9labs/shared'
|
||||
import { CT, T } from '@start9labs/start-sdk'
|
||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||
import {
|
||||
TuiDialogContext,
|
||||
TuiDialogService,
|
||||
TuiLoaderModule,
|
||||
TuiModeModule,
|
||||
TuiNotificationModule,
|
||||
} from '@taiga-ui/core'
|
||||
import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
|
||||
import { compare, Operation } from 'fast-json-patch'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { endWith, firstValueFrom, Subscription } from 'rxjs'
|
||||
import { ActionButton, FormComponent } from 'src/app/components/form.component'
|
||||
import { InvalidService } from 'src/app/components/form/invalid.service'
|
||||
import { ConfigDepComponent } from 'src/app/modals/config-dep.component'
|
||||
import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
getAllPackages,
|
||||
getManifest,
|
||||
getPackage,
|
||||
} from 'src/app/util/get-package-data'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
import { Breakages } from 'src/app/services/api/api.types'
|
||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||
|
||||
export interface PackageConfigData {
|
||||
readonly pkgId: string
|
||||
readonly dependentInfo?: DependentInfo
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<tui-loader
|
||||
*ngIf="loadingText"
|
||||
size="l"
|
||||
[textContent]="loadingText"
|
||||
></tui-loader>
|
||||
|
||||
<tui-notification
|
||||
*ngIf="!loadingText && (loadingError || !pkg)"
|
||||
status="error"
|
||||
>
|
||||
<div [innerHTML]="loadingError"></div>
|
||||
</tui-notification>
|
||||
|
||||
<ng-container
|
||||
*ngIf="
|
||||
!loadingText && !loadingError && pkg && (pkg | toManifest) as manifest
|
||||
"
|
||||
>
|
||||
<tui-notification *ngIf="success" status="success">
|
||||
{{ manifest.title }} has been automatically configured with recommended
|
||||
defaults. Make whatever changes you want, then click "Save".
|
||||
</tui-notification>
|
||||
|
||||
<config-dep
|
||||
*ngIf="dependentInfo && value && original"
|
||||
[package]="manifest.title"
|
||||
[dep]="dependentInfo.title"
|
||||
[original]="original"
|
||||
[value]="value"
|
||||
></config-dep>
|
||||
|
||||
<tui-notification *ngIf="!manifest.hasConfig" status="warning">
|
||||
No config options for {{ manifest.title }} {{ manifest.version }}.
|
||||
</tui-notification>
|
||||
|
||||
<app-form
|
||||
tuiMode="onDark"
|
||||
[spec]="spec"
|
||||
[value]="value || {}"
|
||||
[buttons]="buttons"
|
||||
[patch]="patch"
|
||||
>
|
||||
<button
|
||||
tuiButton
|
||||
appearance="flat"
|
||||
type="reset"
|
||||
[style.margin-right]="'auto'"
|
||||
>
|
||||
Reset Defaults
|
||||
</button>
|
||||
</app-form>
|
||||
</ng-container>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
tui-notification {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
`,
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormComponent,
|
||||
TuiLoaderModule,
|
||||
TuiNotificationModule,
|
||||
TuiButtonModule,
|
||||
TuiModeModule,
|
||||
ConfigDepComponent,
|
||||
UiPipeModule,
|
||||
],
|
||||
providers: [InvalidService],
|
||||
})
|
||||
export class ConfigModal {
|
||||
@ViewChild(FormComponent)
|
||||
private readonly form?: FormComponent<Record<string, any>>
|
||||
|
||||
readonly pkgId = this.context.data.pkgId
|
||||
readonly dependentInfo = this.context.data.dependentInfo
|
||||
|
||||
loadingError = ''
|
||||
loadingText = this.dependentInfo
|
||||
? `Setting properties to accommodate ${this.dependentInfo.title}`
|
||||
: 'Loading Config'
|
||||
|
||||
pkg?: PackageDataEntry
|
||||
spec: CT.InputSpec = {}
|
||||
patch: Operation[] = []
|
||||
buttons: ActionButton<any>[] = [
|
||||
{
|
||||
text: 'Save',
|
||||
handler: value => this.save(value),
|
||||
},
|
||||
]
|
||||
|
||||
original: object | null = null
|
||||
value: object | null = null
|
||||
|
||||
constructor(
|
||||
@Inject(POLYMORPHEUS_CONTEXT)
|
||||
private readonly context: TuiDialogContext<void, PackageConfigData>,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly patchDb: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
get success(): boolean {
|
||||
return (
|
||||
!!this.form &&
|
||||
!this.form.form.dirty &&
|
||||
!this.original &&
|
||||
!this.pkg?.status?.configured
|
||||
)
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
this.pkg = await getPackage(this.patchDb, this.pkgId)
|
||||
|
||||
if (!this.pkg) {
|
||||
this.loadingError = 'This service does not exist'
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (this.dependentInfo) {
|
||||
const depConfig = await this.embassyApi.dryConfigureDependency({
|
||||
dependencyId: this.pkgId,
|
||||
dependentId: this.dependentInfo.id,
|
||||
})
|
||||
|
||||
this.original = depConfig.oldConfig
|
||||
this.value = depConfig.newConfig || this.original
|
||||
this.spec = depConfig.spec
|
||||
this.patch = compare(this.original, this.value)
|
||||
} else {
|
||||
const { config, spec } = await this.embassyApi.getPackageConfig({
|
||||
id: this.pkgId,
|
||||
})
|
||||
|
||||
this.original = config
|
||||
this.value = config
|
||||
this.spec = spec
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.loadingError = String(getErrorMessage(e))
|
||||
} finally {
|
||||
this.loadingText = ''
|
||||
}
|
||||
}
|
||||
|
||||
private async save(config: any) {
|
||||
const loader = new Subscription()
|
||||
|
||||
try {
|
||||
if (hasCurrentDeps(this.pkgId, await getAllPackages(this.patchDb))) {
|
||||
await this.configureDeps(config, loader)
|
||||
} else {
|
||||
await this.configure(config, loader)
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async configureDeps(
|
||||
config: Record<string, any>,
|
||||
loader: Subscription,
|
||||
) {
|
||||
loader.unsubscribe()
|
||||
loader.closed = false
|
||||
loader.add(this.loader.open('Checking dependent services...').subscribe())
|
||||
|
||||
const breakages = await this.embassyApi.drySetPackageConfig({
|
||||
id: this.pkgId,
|
||||
config,
|
||||
})
|
||||
|
||||
loader.unsubscribe()
|
||||
loader.closed = false
|
||||
|
||||
if (isEmptyObject(breakages) || (await this.approveBreakages(breakages))) {
|
||||
await this.configure(config, loader)
|
||||
}
|
||||
}
|
||||
|
||||
private async configure(config: Record<string, any>, loader: Subscription) {
|
||||
loader.unsubscribe()
|
||||
loader.closed = false
|
||||
loader.add(this.loader.open('Saving...').subscribe())
|
||||
|
||||
await this.embassyApi.setPackageConfig({ id: this.pkgId, config })
|
||||
this.context.$implicit.complete()
|
||||
}
|
||||
|
||||
private async approveBreakages(breakages: T.PackageId[]): Promise<boolean> {
|
||||
const packages = await getAllPackages(this.patchDb)
|
||||
const message =
|
||||
'As a result of this change, the following services will no longer work properly and may crash:<ul>'
|
||||
const content = `${message}${breakages.map(
|
||||
id => `<li><b>${getManifest(packages[id]).title}</b></li>`,
|
||||
)}</ul>`
|
||||
const data: TuiPromptData = { content, yes: 'Continue', no: 'Cancel' }
|
||||
|
||||
return firstValueFrom(
|
||||
this.dialogs.open<boolean>(TUI_PROMPT, { data }).pipe(endWith(false)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import { NgIf } from '@angular/common'
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { TuiIconModule, TuiTitleModule } from '@taiga-ui/experimental'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
|
||||
import { StoreIconComponent } from './store-icon.component'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: '[registry]',
|
||||
template: `
|
||||
<store-icon
|
||||
[url]="registry.url"
|
||||
[marketplace]="marketplace"
|
||||
size="40px"
|
||||
></store-icon>
|
||||
<div tuiTitle>
|
||||
{{ registry.name }}
|
||||
<div tuiSubtitle>{{ registry.url }}</div>
|
||||
</div>
|
||||
<tui-icon
|
||||
*ngIf="registry.selected; else content"
|
||||
icon="tuiIconCheck"
|
||||
[style.color]="'var(--tui-positive)'"
|
||||
></tui-icon>
|
||||
<ng-template #content><ng-content></ng-content></ng-template>
|
||||
`,
|
||||
styles: [':host { border-radius: 0.25rem; width: stretch; }'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgIf, StoreIconComponent, TuiIconModule, TuiTitleModule],
|
||||
})
|
||||
export class MarketplaceRegistryComponent {
|
||||
readonly marketplace = inject(ConfigService).marketplace
|
||||
|
||||
@Input()
|
||||
registry!: { url: string; selected: boolean; name?: string }
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
import { NgIf } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { sameUrl } from '@start9labs/shared'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'store-icon',
|
||||
template: `
|
||||
<img
|
||||
*ngIf="icon; else noIcon"
|
||||
[style.border-radius.%]="100"
|
||||
[style.max-width]="size || '100%'"
|
||||
[src]="icon"
|
||||
alt="Marketplace Icon"
|
||||
/>
|
||||
<ng-template #noIcon>
|
||||
<img
|
||||
[style.max-width]="size || '100%'"
|
||||
[style.border-radius]="0"
|
||||
src="assets/img/storefront-outline.png"
|
||||
alt="Marketplace Icon"
|
||||
/>
|
||||
</ng-template>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [NgIf],
|
||||
})
|
||||
export class StoreIconComponent {
|
||||
@Input()
|
||||
url = ''
|
||||
@Input()
|
||||
size?: string
|
||||
@Input()
|
||||
marketplace!: any
|
||||
|
||||
get icon() {
|
||||
const { start9, community } = this.marketplace
|
||||
|
||||
if (sameUrl(this.url, start9)) {
|
||||
return 'assets/img/icon_transparent.png'
|
||||
} else if (sameUrl(this.url, community)) {
|
||||
return 'assets/img/community-store.png'
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
94
web/projects/ui/src/app/modals/password-prompt.component.ts
Normal file
94
web/projects/ui/src/app/modals/password-prompt.component.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
AfterViewInit,
|
||||
Component,
|
||||
ElementRef,
|
||||
Input,
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { IonicModule, ModalController } from '@ionic/angular'
|
||||
import { TuiTextfieldComponent } from '@taiga-ui/core'
|
||||
import { TuiInputPasswordModule } from '@taiga-ui/kit'
|
||||
|
||||
export interface PromptOptions {
|
||||
title: string
|
||||
message: string
|
||||
label: string
|
||||
placeholder: string
|
||||
buttonText: string
|
||||
}
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: `
|
||||
<ion-header>
|
||||
<ion-toolbar>
|
||||
<ion-title>{{ options.title }}</ion-title>
|
||||
<ion-buttons slot="end">
|
||||
<ion-button (click)="cancel()">
|
||||
<ion-icon slot="icon-only" name="close"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-buttons>
|
||||
</ion-toolbar>
|
||||
</ion-header>
|
||||
|
||||
<ion-content class="ion-padding">
|
||||
<p>{{ options.message }}</p>
|
||||
<p>
|
||||
<tui-input-password [(ngModel)]="password" (keydown.enter)="confirm()">
|
||||
{{ options.label }}
|
||||
<input tuiTextfield [placeholder]="options.placeholder" />
|
||||
</tui-input-password>
|
||||
</p>
|
||||
</ion-content>
|
||||
|
||||
<ion-footer>
|
||||
<ion-toolbar>
|
||||
<ion-button
|
||||
class="ion-padding-end"
|
||||
slot="end"
|
||||
color="dark"
|
||||
(click)="cancel()"
|
||||
>
|
||||
Cancel
|
||||
</ion-button>
|
||||
<ion-button
|
||||
class="ion-padding-end"
|
||||
slot="end"
|
||||
color="primary"
|
||||
strong="true"
|
||||
[disabled]="!password"
|
||||
(click)="confirm()"
|
||||
>
|
||||
{{ options.buttonText }}
|
||||
</ion-button>
|
||||
</ion-toolbar>
|
||||
</ion-footer>
|
||||
`,
|
||||
imports: [IonicModule, FormsModule, TuiInputPasswordModule],
|
||||
})
|
||||
export class PasswordPromptComponent implements AfterViewInit {
|
||||
@ViewChild(TuiTextfieldComponent, { read: ElementRef })
|
||||
input?: ElementRef<HTMLInputElement>
|
||||
|
||||
@Input()
|
||||
options!: PromptOptions
|
||||
|
||||
password = ''
|
||||
|
||||
constructor(private modalCtrl: ModalController) {}
|
||||
|
||||
ngAfterViewInit() {
|
||||
setTimeout(() => {
|
||||
this.input?.nativeElement.focus({ preventScroll: true })
|
||||
}, 300)
|
||||
}
|
||||
|
||||
cancel() {
|
||||
return this.modalCtrl.dismiss(null, 'cancel')
|
||||
}
|
||||
|
||||
confirm() {
|
||||
return this.modalCtrl.dismiss(this.password, 'confirm')
|
||||
}
|
||||
}
|
||||
123
web/projects/ui/src/app/modals/prompt.component.ts
Normal file
123
web/projects/ui/src/app/modals/prompt.component.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { TuiAutoFocusModule } from '@taiga-ui/cdk'
|
||||
import { TuiDialogContext, TuiTextfieldControllerModule } from '@taiga-ui/core'
|
||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||
import { TuiInputModule } from '@taiga-ui/kit'
|
||||
import {
|
||||
POLYMORPHEUS_CONTEXT,
|
||||
PolymorpheusComponent,
|
||||
} from '@tinkoff/ng-polymorpheus'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
template: `
|
||||
<p>{{ options.message }}</p>
|
||||
<p *ngIf="options.warning" class="warning">{{ options.warning }}</p>
|
||||
<form (ngSubmit)="submit(value.trim())">
|
||||
<tui-input
|
||||
tuiAutoFocus
|
||||
[tuiTextfieldLabelOutside]="!options.label"
|
||||
[tuiTextfieldCustomContent]="options.useMask ? toggle : ''"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
[(ngModel)]="value"
|
||||
>
|
||||
{{ options.label }}
|
||||
<span *ngIf="options.required !== false && options.label">*</span>
|
||||
<input
|
||||
tuiTextfield
|
||||
[class.masked]="options.useMask && masked && value"
|
||||
[placeholder]="options.placeholder || ''"
|
||||
/>
|
||||
</tui-input>
|
||||
<footer class="g-buttons">
|
||||
<button
|
||||
tuiButton
|
||||
type="button"
|
||||
appearance="secondary"
|
||||
(click)="cancel()"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button tuiButton [disabled]="!value && options.required !== false">
|
||||
{{ options.buttonText || 'Submit' }}
|
||||
</button>
|
||||
</footer>
|
||||
</form>
|
||||
|
||||
<ng-template #toggle>
|
||||
<button
|
||||
tuiIconButton
|
||||
type="button"
|
||||
appearance="icon"
|
||||
title="Toggle masking"
|
||||
size="xs"
|
||||
class="button"
|
||||
[iconLeft]="masked ? 'tuiIconEye' : 'tuiIconEyeOff'"
|
||||
(click)="masked = !masked"
|
||||
></button>
|
||||
</ng-template>
|
||||
`,
|
||||
styles: [
|
||||
`
|
||||
.warning {
|
||||
color: var(--tui-warning-fill);
|
||||
}
|
||||
|
||||
.button {
|
||||
pointer-events: auto;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.masked {
|
||||
-webkit-text-security: disc;
|
||||
}
|
||||
`,
|
||||
],
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
TuiInputModule,
|
||||
TuiButtonModule,
|
||||
TuiTextfieldControllerModule,
|
||||
TuiAutoFocusModule,
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class PromptModal {
|
||||
masked = this.options.useMask
|
||||
value = this.options.initialValue || ''
|
||||
|
||||
constructor(
|
||||
@Inject(POLYMORPHEUS_CONTEXT)
|
||||
private readonly context: TuiDialogContext<string, PromptOptions>,
|
||||
) {}
|
||||
|
||||
get options(): PromptOptions {
|
||||
return this.context.data
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.context.$implicit.complete()
|
||||
}
|
||||
|
||||
submit(value: string) {
|
||||
if (value || !this.options.required) {
|
||||
this.context.$implicit.next(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const PROMPT = new PolymorpheusComponent(PromptModal)
|
||||
|
||||
export interface PromptOptions {
|
||||
message: string
|
||||
label?: string
|
||||
warning?: string
|
||||
buttonText?: string
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
useMask?: boolean
|
||||
initialValue?: string | null
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
|
||||
const ROUTES: Routes = [
|
||||
{
|
||||
path: '',
|
||||
loadChildren: () =>
|
||||
import('./home/home.module').then(m => m.HomePageModule),
|
||||
},
|
||||
{
|
||||
path: 'logs',
|
||||
loadChildren: () =>
|
||||
import('./logs/logs.module').then(m => m.LogsPageModule),
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [RouterModule.forChild(ROUTES)],
|
||||
exports: [RouterModule],
|
||||
})
|
||||
export class DiagnosticModule {}
|
||||
@@ -0,0 +1,222 @@
|
||||
<<<<<<<< HEAD:web/projects/ui/src/app/routes/diagnostic/home/home.page.ts
|
||||
import { TUI_CONFIRM } from '@taiga-ui/kit'
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { WINDOW } from '@ng-web-apis/common'
|
||||
import { LoadingService } from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { filter } from 'rxjs'
|
||||
import { DiagnosticService } from '../services/diagnostic.service'
|
||||
========
|
||||
import { Component } from '@angular/core'
|
||||
import { AlertController } from '@ionic/angular'
|
||||
import { LoadingService } from '@start9labs/shared'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
>>>>>>>> 94a5075b6daa1375433420abf5d121171dae72cb:web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts
|
||||
|
||||
@Component({
|
||||
selector: 'diagnostic-home',
|
||||
templateUrl: 'home.page.html',
|
||||
styleUrls: ['home.page.scss'],
|
||||
})
|
||||
export class HomePage {
|
||||
restarted = false
|
||||
error?: {
|
||||
code: number
|
||||
problem: string
|
||||
solution: string
|
||||
details?: string
|
||||
}
|
||||
|
||||
constructor(
|
||||
private readonly loader: LoadingService,
|
||||
<<<<<<<< HEAD:web/projects/ui/src/app/routes/diagnostic/home/home.page.ts
|
||||
private readonly api: DiagnosticService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
@Inject(WINDOW) private readonly window: Window,
|
||||
========
|
||||
private readonly api: ApiService,
|
||||
private readonly alertCtrl: AlertController,
|
||||
>>>>>>>> 94a5075b6daa1375433420abf5d121171dae72cb:web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
try {
|
||||
const error = await this.api.diagnosticGetError()
|
||||
// incorrect drive
|
||||
if (error.code === 15) {
|
||||
this.error = {
|
||||
code: 15,
|
||||
problem: 'Unknown storage drive detected',
|
||||
solution:
|
||||
'To use a different storage drive, replace the current one and click RESTART SERVER below. To use the current storage drive, click USE CURRENT DRIVE below, then follow instructions. No data will be erased during this process.',
|
||||
details: error.data?.details,
|
||||
}
|
||||
// no drive
|
||||
} else if (error.code === 20) {
|
||||
this.error = {
|
||||
code: 20,
|
||||
problem: 'Storage drive not found',
|
||||
solution:
|
||||
'Insert your StartOS storage drive and click RESTART SERVER below.',
|
||||
details: error.data?.details,
|
||||
}
|
||||
// drive corrupted
|
||||
} else if (error.code === 25) {
|
||||
this.error = {
|
||||
code: 25,
|
||||
problem:
|
||||
'Storage drive corrupted. This could be the result of data corruption or physical damage.',
|
||||
solution:
|
||||
'It may or may not be possible to re-use this drive by reformatting and recovering from backup. To enter recovery mode, click ENTER RECOVERY MODE below, then follow instructions. No data will be erased during this step.',
|
||||
details: error.data?.details,
|
||||
}
|
||||
// filesystem I/O error - disk needs repair
|
||||
} else if (error.code === 2) {
|
||||
this.error = {
|
||||
code: 2,
|
||||
problem: 'Filesystem I/O error.',
|
||||
solution:
|
||||
'Repairing the disk could help resolve this issue. Please DO NOT unplug the drive or server during this time or the situation will become worse.',
|
||||
details: error.data?.details,
|
||||
}
|
||||
// disk management error - disk needs repair
|
||||
} else if (error.code === 48) {
|
||||
this.error = {
|
||||
code: 48,
|
||||
problem: 'Disk management error.',
|
||||
solution:
|
||||
'Repairing the disk could help resolve this issue. Please DO NOT unplug the drive or server during this time or the situation will become worse.',
|
||||
details: error.data?.details,
|
||||
}
|
||||
} else {
|
||||
this.error = {
|
||||
code: error.code,
|
||||
problem: error.message,
|
||||
solution: 'Please contact support.',
|
||||
details: error.data?.details,
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
|
||||
async restart(): Promise<void> {
|
||||
<<<<<<<< HEAD:web/projects/ui/src/app/routes/diagnostic/home/home.page.ts
|
||||
const loader = this.loader.open('').subscribe()
|
||||
========
|
||||
const loader = this.loader.open('Loading...').subscribe()
|
||||
>>>>>>>> 94a5075b6daa1375433420abf5d121171dae72cb:web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts
|
||||
|
||||
try {
|
||||
await this.api.diagnosticRestart()
|
||||
this.restarted = true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
async forgetDrive(): Promise<void> {
|
||||
<<<<<<<< HEAD:web/projects/ui/src/app/routes/diagnostic/home/home.page.ts
|
||||
const loader = this.loader.open('').subscribe()
|
||||
========
|
||||
const loader = this.loader.open('Loading...').subscribe()
|
||||
>>>>>>>> 94a5075b6daa1375433420abf5d121171dae72cb:web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts
|
||||
|
||||
try {
|
||||
await this.api.diagnosticForgetDrive()
|
||||
await this.api.diagnosticRestart()
|
||||
this.restarted = true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
<<<<<<<< HEAD:web/projects/ui/src/app/routes/diagnostic/home/home.page.ts
|
||||
async presentAlertSystemRebuild() {
|
||||
this.dialogs
|
||||
.open(TUI_CONFIRM, {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
no: 'Cancel',
|
||||
yes: 'Rebuild',
|
||||
content:
|
||||
'<p>This action will tear down all service containers and rebuild them from scratch. No data will be deleted.</p><p>A system rebuild can be useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues.</p><p>It may take up to an hour to complete. During this time, you will lose all connectivity to your Start9 server.</p>',
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => {
|
||||
try {
|
||||
this.systemRebuild()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
========
|
||||
>>>>>>>> 94a5075b6daa1375433420abf5d121171dae72cb:web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts
|
||||
async presentAlertRepairDisk() {
|
||||
this.dialogs
|
||||
.open(TUI_CONFIRM, {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
no: 'Cancel',
|
||||
yes: 'Repair',
|
||||
content:
|
||||
'<p>This action should only be executed if directed by a Start9 support specialist.</p><p>If anything happens to the device during the reboot, such as losing power or unplugging the drive, the filesystem <i>will</i> be in an unrecoverable state. Please proceed with caution.</p>',
|
||||
},
|
||||
})
|
||||
.pipe(filter(Boolean))
|
||||
.subscribe(() => {
|
||||
try {
|
||||
this.repairDisk()
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
refreshPage(): void {
|
||||
this.window.location.reload()
|
||||
}
|
||||
|
||||
<<<<<<<< HEAD:web/projects/ui/src/app/routes/diagnostic/home/home.page.ts
|
||||
private async systemRebuild(): Promise<void> {
|
||||
const loader = this.loader.open('').subscribe()
|
||||
|
||||
try {
|
||||
await this.api.systemRebuild()
|
||||
await this.api.restart()
|
||||
this.restarted = true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async repairDisk(): Promise<void> {
|
||||
const loader = this.loader.open('').subscribe()
|
||||
========
|
||||
private async repairDisk(): Promise<void> {
|
||||
const loader = this.loader.open('Loading...').subscribe()
|
||||
>>>>>>>> 94a5075b6daa1375433420abf5d121171dae72cb:web/projects/ui/src/app/pages/diagnostic-routes/home/home.page.ts
|
||||
|
||||
try {
|
||||
await this.api.diagnosticRepairDisk()
|
||||
await this.api.diagnosticRestart()
|
||||
this.restarted = true
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
}
|
||||
24
web/projects/ui/src/app/pages/init/init.module.ts
Normal file
24
web/projects/ui/src/app/pages/init/init.module.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { NgModule } from '@angular/core'
|
||||
import { RouterModule, Routes } from '@angular/router'
|
||||
import { TuiProgressModule } from '@taiga-ui/kit'
|
||||
import { LogsModule } from 'src/app/pages/init/logs/logs.module'
|
||||
import { InitPage } from './init.page'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
path: '',
|
||||
component: InitPage,
|
||||
},
|
||||
]
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
LogsModule,
|
||||
TuiProgressModule,
|
||||
RouterModule.forChild(routes),
|
||||
],
|
||||
declarations: [InitPage],
|
||||
})
|
||||
export class InitPageModule {}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user