mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-03-26 10:21:52 +00:00
sideload wip, websockets, styling, multiple todos (#2865)
* sideload wip, websockets, styling, multiple todos * sideload * misc backend updates * chore: comments * prep for license and instructions display * comment for Matt * s9pk updates and 040 sdk * fix dependency error for actions * 0.4.0-beta.1 * beta.2 --------- Co-authored-by: Aiden McClelland <me@drbonez.dev> Co-authored-by: waterplea <alexander@inkin.ru> Co-authored-by: Aiden McClelland <3732071+dr-bonez@users.noreply.github.com>
This commit is contained in:
234
web/package-lock.json
generated
234
web/package-lock.json
generated
@@ -25,18 +25,18 @@
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@start9labs/argon2": "^0.2.2",
|
||||
"@start9labs/start-sdk": "file:../sdk/baseDist",
|
||||
"@taiga-ui/addon-charts": "4.30.0",
|
||||
"@taiga-ui/addon-commerce": "4.30.0",
|
||||
"@taiga-ui/addon-mobile": "4.30.0",
|
||||
"@taiga-ui/addon-table": "4.30.0",
|
||||
"@taiga-ui/cdk": "4.30.0",
|
||||
"@taiga-ui/core": "4.30.0",
|
||||
"@taiga-ui/event-plugins": "4.5.0",
|
||||
"@taiga-ui/experimental": "4.30.0",
|
||||
"@taiga-ui/icons": "4.30.0",
|
||||
"@taiga-ui/kit": "4.30.0",
|
||||
"@taiga-ui/layout": "4.30.0",
|
||||
"@taiga-ui/legacy": "4.30.0",
|
||||
"@taiga-ui/addon-charts": "4.32.0",
|
||||
"@taiga-ui/addon-commerce": "4.32.0",
|
||||
"@taiga-ui/addon-mobile": "4.32.0",
|
||||
"@taiga-ui/addon-table": "4.32.0",
|
||||
"@taiga-ui/cdk": "4.32.0",
|
||||
"@taiga-ui/core": "4.32.0",
|
||||
"@taiga-ui/event-plugins": "4.5.1",
|
||||
"@taiga-ui/experimental": "4.32.0",
|
||||
"@taiga-ui/icons": "4.32.0",
|
||||
"@taiga-ui/kit": "4.32.0",
|
||||
"@taiga-ui/layout": "4.32.0",
|
||||
"@taiga-ui/legacy": "4.32.0",
|
||||
"@taiga-ui/polymorpheus": "4.9.0",
|
||||
"@tinkoff/ng-dompurify": "4.0.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
@@ -3480,9 +3480,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ng-web-apis/common": {
|
||||
"version": "4.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@ng-web-apis/common/-/common-4.11.1.tgz",
|
||||
"integrity": "sha512-fXbcMrd/+L+9j9knbgXbDwYe30H4Wt0hQzvqyhpXTVrc0jYwlk3MJTYrnazKz5HvP9318caEv5n4qt3HMf5uPQ==",
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@ng-web-apis/common/-/common-4.12.0.tgz",
|
||||
"integrity": "sha512-OG4ChsEWQ0IbGJ+WrJAiOY5X4jF8f5YUCss961taPeiyhvwtUo4zAuX3UvtV/iJSt8XZ41jaOYFTyMIBGubv4Q==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -3495,9 +3495,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ng-web-apis/intersection-observer": {
|
||||
"version": "4.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@ng-web-apis/intersection-observer/-/intersection-observer-4.11.1.tgz",
|
||||
"integrity": "sha512-KjODVVx20yG/U5bnPvp5voihL5DSVFuYwZVY9DNRvaFIcQPMy1tL1t9/oJOdxj7zUSFDL8+Z0RoJbsvArezuSg==",
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@ng-web-apis/intersection-observer/-/intersection-observer-4.12.0.tgz",
|
||||
"integrity": "sha512-7MW0y6BrjLKCTUGb5YfsEPYpn17wPmGj8J+2A980ntGNveo9+DILz3KpFHkpS6G6bJGtnD36exU2YvfTUKiyXA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -3505,13 +3505,13 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@ng-web-apis/common": ">=4.11.1"
|
||||
"@ng-web-apis/common": ">=4.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ng-web-apis/mutation-observer": {
|
||||
"version": "4.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@ng-web-apis/mutation-observer/-/mutation-observer-4.11.1.tgz",
|
||||
"integrity": "sha512-YFnkGFE0gd03q4HBJL+WPl3YZRZNq7dVV8yD5uqr0ZCDgmOAMBilrp42FuHBPaYkF73Rs2EpKsKHWz1jASqBbQ==",
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@ng-web-apis/mutation-observer/-/mutation-observer-4.12.0.tgz",
|
||||
"integrity": "sha512-Yyz0jpQoGgWzQEhjRH8xC9Umxr2W0hYktuYDNxrmnr6GnBejcfkE+wCon7FhCt6h7BrqR3+Z4cg7TvnyUVHU6Q==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -3519,13 +3519,13 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@ng-web-apis/common": ">=4.11.1"
|
||||
"@ng-web-apis/common": ">=4.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ng-web-apis/platform": {
|
||||
"version": "4.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@ng-web-apis/platform/-/platform-4.11.1.tgz",
|
||||
"integrity": "sha512-BrhkUIEEAD7wcwR65LSXHYOD6L3IvAb4aV94S8tzxUNeGUPwikX5glQJBT1UwkHWXQjANPKTCNyK1LO+cMPgkw==",
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@ng-web-apis/platform/-/platform-4.12.0.tgz",
|
||||
"integrity": "sha512-OuwV9OERPvQD+QxS2q84pWg60GlL9O66zl0VBLDR8ARMslBfCg1m70LlbLfQI4mlt4QZzTliUVps1JaGyAEKYA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -3533,9 +3533,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@ng-web-apis/resize-observer": {
|
||||
"version": "4.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@ng-web-apis/resize-observer/-/resize-observer-4.11.1.tgz",
|
||||
"integrity": "sha512-q8eJ6sovnMhfqIULN1yyhqT35Y2a60vB42p9CUBWPeeVammU+QHE/imPCMCJgnti9cdPZfnyI/+TeYNIUg7mzg==",
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@ng-web-apis/resize-observer/-/resize-observer-4.12.0.tgz",
|
||||
"integrity": "sha512-ekLcZnqap9OBcoTtOD0/tcOy/STFrxSu3WR0yU9yEM0n7S1mOsKOnXIY4PMAxxWV4LMs91P00wzFHfNNnuOS/g==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -3543,13 +3543,13 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@ng-web-apis/common": ">=4.11.1"
|
||||
"@ng-web-apis/common": ">=4.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@ng-web-apis/screen-orientation": {
|
||||
"version": "4.11.1",
|
||||
"resolved": "https://registry.npmjs.org/@ng-web-apis/screen-orientation/-/screen-orientation-4.11.1.tgz",
|
||||
"integrity": "sha512-HS/kWTgVjXVDqMLcJbl5uty+1sV10m9PeDag74tzktIDAB06diFQJQGfcQaA0o0IBisT3fOysf9gHV5sXxSOFw==",
|
||||
"version": "4.12.0",
|
||||
"resolved": "https://registry.npmjs.org/@ng-web-apis/screen-orientation/-/screen-orientation-4.12.0.tgz",
|
||||
"integrity": "sha512-oqZqc9TTpEBNNinVSFjEl/plhoRxh07zMtdG6VMnuY6Lng2l1jfC7vKru2rjEskR+0sYgN8Y8Ttov03i8I9GHg==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -3557,7 +3557,7 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@ng-web-apis/common": ">=4.11.1",
|
||||
"@ng-web-apis/common": ">=4.12.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
@@ -4418,9 +4418,9 @@
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@taiga-ui/addon-charts": {
|
||||
"version": "4.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.30.0.tgz",
|
||||
"integrity": "sha512-QrM2Oh4hUcg/I0K3KWFkc/dbTCYZn2n5GU2FSpZaK6I7pwjfRoMjBU7vswPLVVdmgeWTJxxoQlbfYnbUbkMAJw==",
|
||||
"version": "4.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.32.0.tgz",
|
||||
"integrity": "sha512-VhGkBxwfra5eijSvZdXhMKOWEnFMESo5TX3OfsahIXWJXivwguvIc63rIhHYq2uC+t5sj1kINveO4yLqOeAm/Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
@@ -4428,16 +4428,16 @@
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=16.0.0",
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@ng-web-apis/common": "^4.11.1",
|
||||
"@taiga-ui/cdk": "^4.30.0",
|
||||
"@taiga-ui/core": "^4.30.0",
|
||||
"@ng-web-apis/common": "^4.12.0",
|
||||
"@taiga-ui/cdk": "^4.32.0",
|
||||
"@taiga-ui/core": "^4.32.0",
|
||||
"@taiga-ui/polymorpheus": "^4.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/addon-commerce": {
|
||||
"version": "4.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.30.0.tgz",
|
||||
"integrity": "sha512-6diktxvxMpWjbEHXThS0pTrURdUiF/47jf2jdBFkMwX3BbbekisM1qkwxY24V7q8fN0IIxfO8CVEjTeLRrCw5g==",
|
||||
"version": "4.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.32.0.tgz",
|
||||
"integrity": "sha512-AC3VU/RVTNapS8ltSAemZPeDb2CopJEj298rI3Vl4qER1oVl0zunmWVy5ncwK1F1zWKU2/QNDjjo8yKYWeU/Nw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
@@ -4449,19 +4449,19 @@
|
||||
"@maskito/angular": "^3.5.0",
|
||||
"@maskito/core": "^3.5.0",
|
||||
"@maskito/kit": "^3.5.0",
|
||||
"@ng-web-apis/common": "^4.11.1",
|
||||
"@taiga-ui/cdk": "^4.30.0",
|
||||
"@taiga-ui/core": "^4.30.0",
|
||||
"@taiga-ui/i18n": "^4.30.0",
|
||||
"@taiga-ui/kit": "^4.30.0",
|
||||
"@ng-web-apis/common": "^4.12.0",
|
||||
"@taiga-ui/cdk": "^4.32.0",
|
||||
"@taiga-ui/core": "^4.32.0",
|
||||
"@taiga-ui/i18n": "^4.32.0",
|
||||
"@taiga-ui/kit": "^4.32.0",
|
||||
"@taiga-ui/polymorpheus": "^4.9.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/addon-mobile": {
|
||||
"version": "4.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.30.0.tgz",
|
||||
"integrity": "sha512-8cYyU0UDLUd74v+Zjs4m9S4AsSWchUojAexDLvaAHzfi0x+tdtA+ZN0h49v8AmOWHK0v69z4FMjyyc52p/jiDw==",
|
||||
"version": "4.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.32.0.tgz",
|
||||
"integrity": "sha512-pUoHWyILPj6KIAhna1JDzz48c2nCjqYb1tb7AL3LQ3qfNwAbg9fvjBIfrgWMhW0LaDeh5+FfrS7oiO/ERcHTLg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
@@ -4470,19 +4470,19 @@
|
||||
"@angular/cdk": ">=16.0.0",
|
||||
"@angular/common": ">=16.0.0",
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@ng-web-apis/common": "^4.11.1",
|
||||
"@taiga-ui/cdk": "^4.30.0",
|
||||
"@taiga-ui/core": "^4.30.0",
|
||||
"@taiga-ui/kit": "^4.30.0",
|
||||
"@taiga-ui/layout": "^4.30.0",
|
||||
"@ng-web-apis/common": "^4.12.0",
|
||||
"@taiga-ui/cdk": "^4.32.0",
|
||||
"@taiga-ui/core": "^4.32.0",
|
||||
"@taiga-ui/kit": "^4.32.0",
|
||||
"@taiga-ui/layout": "^4.32.0",
|
||||
"@taiga-ui/polymorpheus": "^4.9.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/addon-table": {
|
||||
"version": "4.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.30.0.tgz",
|
||||
"integrity": "sha512-OdCEwlrMs42Z2pINK1wvNk7OZmAlkj+mbgHTyMGdrUdA49dFZfYXNpVUCwVOqHAm2PDOeVN4ybZ8FSbzYefJyw==",
|
||||
"version": "4.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.32.0.tgz",
|
||||
"integrity": "sha512-8oXeqLO1wGH8RYHTYWhjCvrKWptPN1we04NRahmFY4AxSJ3u7MqaR4420RRNO4zZG9kGyktLXPjqGocMoymL8Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
@@ -4490,19 +4490,19 @@
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=16.0.0",
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@ng-web-apis/intersection-observer": "^4.11.1",
|
||||
"@taiga-ui/cdk": "^4.30.0",
|
||||
"@taiga-ui/core": "^4.30.0",
|
||||
"@taiga-ui/i18n": "^4.30.0",
|
||||
"@taiga-ui/kit": "^4.30.0",
|
||||
"@ng-web-apis/intersection-observer": "^4.12.0",
|
||||
"@taiga-ui/cdk": "^4.32.0",
|
||||
"@taiga-ui/core": "^4.32.0",
|
||||
"@taiga-ui/i18n": "^4.32.0",
|
||||
"@taiga-ui/kit": "^4.32.0",
|
||||
"@taiga-ui/polymorpheus": "^4.9.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/cdk": {
|
||||
"version": "4.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.30.0.tgz",
|
||||
"integrity": "sha512-ndfnLOnL6vriItm5lq8/0slzj03CatkGVYG8zAT3fx00Vuam5Wf8Sh6h2ObqCFAljT7WJxHqMF9A1cBfLPI/iQ==",
|
||||
"version": "4.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.32.0.tgz",
|
||||
"integrity": "sha512-qvYe79uh6Tw2LJSEGLJYUlAidbZi6JgcuMRqWAB1JhyIGpgnaqar5v+oJJg28zJZZ81PCj59VkFNLL0dNVXRUg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "2.8.1"
|
||||
@@ -4520,20 +4520,20 @@
|
||||
"@angular/common": ">=16.0.0",
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@angular/forms": ">=16.0.0",
|
||||
"@ng-web-apis/common": "^4.11.1",
|
||||
"@ng-web-apis/mutation-observer": "^4.11.1",
|
||||
"@ng-web-apis/platform": "^4.11.1",
|
||||
"@ng-web-apis/resize-observer": "^4.11.1",
|
||||
"@ng-web-apis/screen-orientation": "^4.11.1",
|
||||
"@taiga-ui/event-plugins": "^4.4.1",
|
||||
"@ng-web-apis/common": "^4.12.0",
|
||||
"@ng-web-apis/mutation-observer": "^4.12.0",
|
||||
"@ng-web-apis/platform": "^4.12.0",
|
||||
"@ng-web-apis/resize-observer": "^4.12.0",
|
||||
"@ng-web-apis/screen-orientation": "^4.12.0",
|
||||
"@taiga-ui/event-plugins": "^4.5.1",
|
||||
"@taiga-ui/polymorpheus": "^4.9.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/core": {
|
||||
"version": "4.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.30.0.tgz",
|
||||
"integrity": "sha512-IeZ6QBpSuv7k4bQx2BSDr8N3dDiMDwgnnwkkKqtJ0yJayZ/ZlCMq3nUQA0kg3VjH2spJeUbdqkDqpEuzrWJGkA==",
|
||||
"version": "4.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.32.0.tgz",
|
||||
"integrity": "sha512-e1z7YhhjePMRLTk+s83OclN45wMixCwZWMxM9WuXIyd2KXMPhJvrrgBDjoK66GuFtjZ4qaSF/H2FTIJmJ/6MiQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
@@ -4545,19 +4545,19 @@
|
||||
"@angular/forms": ">=16.0.0",
|
||||
"@angular/platform-browser": ">=16.0.0",
|
||||
"@angular/router": ">=16.0.0",
|
||||
"@ng-web-apis/common": "^4.11.1",
|
||||
"@ng-web-apis/mutation-observer": "^4.11.1",
|
||||
"@taiga-ui/cdk": "^4.30.0",
|
||||
"@taiga-ui/event-plugins": "^4.4.1",
|
||||
"@taiga-ui/i18n": "^4.30.0",
|
||||
"@ng-web-apis/common": "^4.12.0",
|
||||
"@ng-web-apis/mutation-observer": "^4.12.0",
|
||||
"@taiga-ui/cdk": "^4.32.0",
|
||||
"@taiga-ui/event-plugins": "^4.5.1",
|
||||
"@taiga-ui/i18n": "^4.32.0",
|
||||
"@taiga-ui/polymorpheus": "^4.9.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/event-plugins": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/event-plugins/-/event-plugins-4.5.0.tgz",
|
||||
"integrity": "sha512-bMW36eqr4Q+EnUM8ZNjx1Sw8POIAcyALY74xVPq9UHoQ3NqnRkeEDnZdfPhq9IYxtC3sO2BttNjWYcvBAkU2+A==",
|
||||
"version": "4.5.1",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/event-plugins/-/event-plugins-4.5.1.tgz",
|
||||
"integrity": "sha512-p5TAs6ZAJAlyl64OUdvnVnfCvDteJtLl9cjXarMzPRY7sX2d+SC97qnTZF8ACPcx4FgFaiHI6VH5G6UzYHMZ8g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
@@ -4569,9 +4569,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/experimental": {
|
||||
"version": "4.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-4.30.0.tgz",
|
||||
"integrity": "sha512-0GWkBinW+tqQIFkWQbTqMBTkKGZhju3RslKRCYbjal/hfcIuSAsEPZqLqIQqVqJNz6AhaIpT0UQ+I7QXzx1/yw==",
|
||||
"version": "4.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-4.32.0.tgz",
|
||||
"integrity": "sha512-sCOasTF9UlgPOW4vXSeM5M1tgGrjgofa+Qq7hejYW3BXs/4mnmdm5yiYzWfMVZd4jgTSeV5kobkIJ9Fkp2zt6g==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
@@ -4579,18 +4579,18 @@
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=16.0.0",
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@taiga-ui/addon-commerce": "^4.30.0",
|
||||
"@taiga-ui/cdk": "^4.30.0",
|
||||
"@taiga-ui/core": "^4.30.0",
|
||||
"@taiga-ui/kit": "^4.30.0",
|
||||
"@taiga-ui/addon-commerce": "^4.32.0",
|
||||
"@taiga-ui/cdk": "^4.32.0",
|
||||
"@taiga-ui/core": "^4.32.0",
|
||||
"@taiga-ui/kit": "^4.32.0",
|
||||
"@taiga-ui/polymorpheus": "^4.9.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/i18n": {
|
||||
"version": "4.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.30.0.tgz",
|
||||
"integrity": "sha512-OvtUqSRQE988XfiH1MS7Wd3Eg6dE1mkP2sqYRLw0HyE5Oc9hgHMwdPstSaoMN9aeJRVZnKXGsYmX4iaQ3x7drw==",
|
||||
"version": "4.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.32.0.tgz",
|
||||
"integrity": "sha512-PAQv9RxSgvf3RBUps9bXX2erCk9oiSt9ApM3SAIa/OuzET0TJsW6yZ4EQrtLw03bMX3wyA8PnEYva9wzoYAqxA==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
@@ -4598,23 +4598,23 @@
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@ng-web-apis/common": "^4.11.1",
|
||||
"@ng-web-apis/common": "^4.12.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/icons": {
|
||||
"version": "4.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.30.0.tgz",
|
||||
"integrity": "sha512-EAbvw1ii4UVDgt9+5t7NQkV0WBqkVm5SGixH0ux8Vb4qhhLJJwp5xvXOCGt5QPzviT7nFGqXD6EqB23aYcuusg==",
|
||||
"version": "4.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.32.0.tgz",
|
||||
"integrity": "sha512-X2ZSiqeMKigULgX91fBZkFJRUbwzeW934yLEGhq7C1JMcC2+ppLmL/NbkD2kpKZ4OeHnGsItxKauoXu44rXeLA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/kit": {
|
||||
"version": "4.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.30.0.tgz",
|
||||
"integrity": "sha512-tCHZbsiq1u19ariarFuP9iwnNSxJGicQnYvJYy2+QojL65KsC9p8VgZv36rpggpuPEUXRXwmhyz2Qi6fwFcbLg==",
|
||||
"version": "4.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.32.0.tgz",
|
||||
"integrity": "sha512-J8XoqeQHBNbAAuTz0kVACujDOb3zuh4Vps83lYl+msFIaUmkjC37muXF3eRlImH3m4DpT8yI8+ffh/T3+jky7w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
@@ -4628,21 +4628,21 @@
|
||||
"@maskito/core": "^3.5.0",
|
||||
"@maskito/kit": "^3.5.0",
|
||||
"@maskito/phone": "^3.5.0",
|
||||
"@ng-web-apis/common": "^4.11.1",
|
||||
"@ng-web-apis/intersection-observer": "^4.11.1",
|
||||
"@ng-web-apis/mutation-observer": "^4.11.1",
|
||||
"@ng-web-apis/resize-observer": "^4.11.1",
|
||||
"@taiga-ui/cdk": "^4.30.0",
|
||||
"@taiga-ui/core": "^4.30.0",
|
||||
"@taiga-ui/i18n": "^4.30.0",
|
||||
"@ng-web-apis/common": "^4.12.0",
|
||||
"@ng-web-apis/intersection-observer": "^4.12.0",
|
||||
"@ng-web-apis/mutation-observer": "^4.12.0",
|
||||
"@ng-web-apis/resize-observer": "^4.12.0",
|
||||
"@taiga-ui/cdk": "^4.32.0",
|
||||
"@taiga-ui/core": "^4.32.0",
|
||||
"@taiga-ui/i18n": "^4.32.0",
|
||||
"@taiga-ui/polymorpheus": "^4.9.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/layout": {
|
||||
"version": "4.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.30.0.tgz",
|
||||
"integrity": "sha512-DyIqpmXcv/OP4byt7L1f1iBKPysf3L+sj/dBpkeYvAUUnJnXnJsXav0j57d43VkXPn9lpGqz0gEBtzVDt7xxTw==",
|
||||
"version": "4.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.32.0.tgz",
|
||||
"integrity": "sha512-ECaoJ3CbL+eoqL1MleaHvD9/FQ5OCaUMkjOdXId2Jg2MNbuDhtS9hqVZvSWLXRWz3XgC3aADYnPwrNvIsy5Mng==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
@@ -4650,17 +4650,17 @@
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=16.0.0",
|
||||
"@angular/core": ">=16.0.0",
|
||||
"@taiga-ui/cdk": "^4.30.0",
|
||||
"@taiga-ui/core": "^4.30.0",
|
||||
"@taiga-ui/kit": "^4.30.0",
|
||||
"@taiga-ui/cdk": "^4.32.0",
|
||||
"@taiga-ui/core": "^4.32.0",
|
||||
"@taiga-ui/kit": "^4.32.0",
|
||||
"@taiga-ui/polymorpheus": "^4.9.0",
|
||||
"rxjs": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@taiga-ui/legacy": {
|
||||
"version": "4.30.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/legacy/-/legacy-4.30.0.tgz",
|
||||
"integrity": "sha512-ebFJMddzlsq3TUAWxopn5Qju4REkC4bHzoYYx5OEzPq1VW1zmCvNC+X6usMnluhc9aS50UI8ZB7Xd3N4Zdgtfg==",
|
||||
"version": "4.32.0",
|
||||
"resolved": "https://registry.npmjs.org/@taiga-ui/legacy/-/legacy-4.32.0.tgz",
|
||||
"integrity": "sha512-wEsywt6hK2NNpHddqVrL0MTd1QFzmhMdPPgtraNOieQmzrSW2jpA37KJO11cVleuRdDsk98rFtzQ3stlNNFy5Q==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": ">=2.8.1"
|
||||
|
||||
@@ -47,18 +47,18 @@
|
||||
"@noble/hashes": "^1.4.0",
|
||||
"@start9labs/argon2": "^0.2.2",
|
||||
"@start9labs/start-sdk": "file:../sdk/baseDist",
|
||||
"@taiga-ui/addon-charts": "4.30.0",
|
||||
"@taiga-ui/addon-commerce": "4.30.0",
|
||||
"@taiga-ui/addon-mobile": "4.30.0",
|
||||
"@taiga-ui/addon-table": "4.30.0",
|
||||
"@taiga-ui/cdk": "4.30.0",
|
||||
"@taiga-ui/core": "4.30.0",
|
||||
"@taiga-ui/event-plugins": "4.5.0",
|
||||
"@taiga-ui/experimental": "4.30.0",
|
||||
"@taiga-ui/icons": "4.30.0",
|
||||
"@taiga-ui/kit": "4.30.0",
|
||||
"@taiga-ui/layout": "4.30.0",
|
||||
"@taiga-ui/legacy": "4.30.0",
|
||||
"@taiga-ui/addon-charts": "4.32.0",
|
||||
"@taiga-ui/addon-commerce": "4.32.0",
|
||||
"@taiga-ui/addon-mobile": "4.32.0",
|
||||
"@taiga-ui/addon-table": "4.32.0",
|
||||
"@taiga-ui/cdk": "4.32.0",
|
||||
"@taiga-ui/core": "4.32.0",
|
||||
"@taiga-ui/event-plugins": "4.5.1",
|
||||
"@taiga-ui/experimental": "4.32.0",
|
||||
"@taiga-ui/icons": "4.32.0",
|
||||
"@taiga-ui/kit": "4.32.0",
|
||||
"@taiga-ui/layout": "4.32.0",
|
||||
"@taiga-ui/legacy": "4.32.0",
|
||||
"@taiga-ui/polymorpheus": "4.9.0",
|
||||
"@tinkoff/ng-dompurify": "4.0.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
header {
|
||||
@include scrollbar-hidden();
|
||||
|
||||
// TODO: Theme
|
||||
// @TODO Theme
|
||||
background: #2b2b2f;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
|
||||
@@ -16,21 +16,3 @@
|
||||
</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> -->
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
} from '@angular/core'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { RELEASE_NOTES } from '../../../modals/release-notes.component'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
import { MarketplacePkgBase } from '../../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-about',
|
||||
@@ -18,7 +18,7 @@ export class AboutComponent {
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
pkg!: MarketplacePkgBase
|
||||
|
||||
async onPast() {
|
||||
this.dialogs
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiIcon, TuiLabel, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiLineClamp } from '@taiga-ui/kit'
|
||||
|
||||
@Component({
|
||||
@@ -43,7 +43,7 @@ import { TuiLineClamp } from '@taiga-ui/kit'
|
||||
],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [CommonModule, TuiLineClamp, TuiLabel, TuiIcon, TuiTitle],
|
||||
imports: [CommonModule, TuiLineClamp, TuiIcon, TuiTitle],
|
||||
})
|
||||
export class MarketplaceAdditionalItemComponent {
|
||||
@Input({ required: true })
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
<div class="detail-container">
|
||||
<!-- release date -->
|
||||
<marketplace-additional-item
|
||||
*ngIf="pkg.s9pk.publishedAt as published"
|
||||
*ngIf="pkg.s9pk?.publishedAt as published"
|
||||
[data]="(published | date: 'medium')!"
|
||||
label="Released"
|
||||
icon=""
|
||||
@@ -76,98 +76,3 @@
|
||||
</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,8 +7,7 @@ import {
|
||||
} from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { CopyService } from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
import { MarketplacePkgBase } from '../../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-additional',
|
||||
@@ -18,14 +17,13 @@ import { MarketplacePkg } from '../../../types'
|
||||
})
|
||||
export class AdditionalComponent {
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
pkg!: MarketplacePkgBase
|
||||
|
||||
@Output()
|
||||
readonly static = new EventEmitter<string>()
|
||||
readonly static = new EventEmitter<'License' | 'Instructions'>()
|
||||
|
||||
constructor(
|
||||
readonly copyService: CopyService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly route: ActivatedRoute,
|
||||
) {}
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
Input,
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
import { MarketplacePkgBase } from '../../../types'
|
||||
import { MarketplaceDepItemComponent } from './dependency-item.component'
|
||||
|
||||
@Component({
|
||||
@@ -55,7 +55,7 @@ import { MarketplaceDepItemComponent } from './dependency-item.component'
|
||||
})
|
||||
export class MarketplaceDependenciesComponent {
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
pkg!: MarketplacePkgBase
|
||||
|
||||
@Output() open = new EventEmitter<string>()
|
||||
}
|
||||
|
||||
@@ -3,9 +3,8 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { ExverPipesModule } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import { TuiAvatar, TuiLineClamp } from '@taiga-ui/kit'
|
||||
import { MarketplacePkg } from '../../../types'
|
||||
import { MarketplacePkgBase } from '../../../types'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-dep-item',
|
||||
@@ -97,12 +96,11 @@ import { MarketplacePkg } from '../../../types'
|
||||
TuiAvatar,
|
||||
ExverPipesModule,
|
||||
TuiLineClamp,
|
||||
TuiLet,
|
||||
],
|
||||
})
|
||||
export class MarketplaceDepItemComponent {
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
pkg!: MarketplacePkgBase
|
||||
|
||||
@Input({ required: true })
|
||||
dep!: KeyValue<string, T.DependencyMetadata>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { OptionalProperty } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
export type GetPackageReq = {
|
||||
@@ -31,11 +32,17 @@ export type StoreData = {
|
||||
packages: MarketplacePkg[]
|
||||
}
|
||||
|
||||
export type MarketplacePkg = T.PackageVersionInfo &
|
||||
Omit<GetPackageRes, 'best'> & {
|
||||
id: T.PackageId
|
||||
version: string
|
||||
flavor: string | null
|
||||
}
|
||||
export type MarketplacePkgBase = OptionalProperty<
|
||||
T.PackageVersionInfo,
|
||||
's9pk'
|
||||
> & {
|
||||
id: T.PackageId
|
||||
version: string
|
||||
flavor: string | null
|
||||
}
|
||||
|
||||
export type MarketplacePkg = MarketplacePkgBase &
|
||||
GetPackageRes &
|
||||
T.PackageVersionInfo
|
||||
|
||||
export type StoreDataWithUrl = StoreData & { url: string }
|
||||
|
||||
@@ -117,7 +117,7 @@ export class MockApiService extends ApiService {
|
||||
})),
|
||||
) as Observable<T>
|
||||
} else if (guid === 'progress-guid') {
|
||||
// @TODO mock progress
|
||||
// @TODO Matt mock progress
|
||||
return interval(1000).pipe(
|
||||
map(() => ({
|
||||
overall: true,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import { TuiProgress } from '@taiga-ui/kit'
|
||||
import { LogsWindowComponent } from './logs-window.component'
|
||||
|
||||
@@ -31,7 +30,7 @@ import { LogsWindowComponent } from './logs-window.component'
|
||||
padding: 1rem;
|
||||
margin: 1.5rem;
|
||||
text-align: center;
|
||||
/* TODO: Theme */
|
||||
// @TODO Theme
|
||||
background: #e0e0e0;
|
||||
color: #333;
|
||||
--tui-background-neutral-1: rgba(0, 0, 0, 0.1);
|
||||
@@ -46,11 +45,11 @@ import { LogsWindowComponent } from './logs-window.component'
|
||||
text-align: left;
|
||||
overflow: hidden;
|
||||
border-radius: 2rem;
|
||||
/* TODO: Theme */
|
||||
// @TODO Theme
|
||||
background: #181818;
|
||||
}
|
||||
`,
|
||||
imports: [CommonModule, LogsWindowComponent, TuiLet, TuiProgress],
|
||||
imports: [CommonModule, LogsWindowComponent, TuiProgress],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class InitializingComponent {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AfterViewInit, Directive, ElementRef, Inject } from '@angular/core'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
|
||||
// TODO: Refactor to use `MutationObserver` so it works with dynamic content
|
||||
// @TODO Alex: Refactor to use `MutationObserver` so it works with dynamic content
|
||||
@Directive({
|
||||
selector: '[safeLinks]',
|
||||
standalone: true,
|
||||
|
||||
@@ -2,7 +2,7 @@ import { ErrorHandler, inject, Injectable } from '@angular/core'
|
||||
import { TuiAlertService } from '@taiga-ui/core'
|
||||
import { HttpError } from '../classes/http-error'
|
||||
|
||||
// TODO: Enable this as ErrorHandler
|
||||
// @TODO Alex: Enable this as ErrorHandler
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import { StaticClassProvider } from '@angular/core'
|
||||
import { bufferTime, defer, map, Observable, scan, switchMap } from 'rxjs'
|
||||
import {
|
||||
bufferTime,
|
||||
defer,
|
||||
filter,
|
||||
map,
|
||||
Observable,
|
||||
scan,
|
||||
switchMap,
|
||||
} from 'rxjs'
|
||||
import { FollowLogsReq, FollowLogsRes, Log } from '../types/api'
|
||||
import { Constructor } from '../types/constructor'
|
||||
import { convertAnsi } from '../util/convert-ansi'
|
||||
@@ -22,7 +30,8 @@ export function provideSetupLogsService(
|
||||
export class SetupLogsService extends Observable<readonly string[]> {
|
||||
private readonly log$ = defer(() => this.api.followServerLogs({})).pipe(
|
||||
switchMap(({ guid }) => this.api.openWebsocket$(guid)),
|
||||
bufferTime(1000),
|
||||
bufferTime(500),
|
||||
filter(logs => !!logs.length),
|
||||
map(convertAnsi),
|
||||
scan((logs: readonly string[], log) => [...logs, log], []),
|
||||
)
|
||||
|
||||
@@ -1,22 +1,29 @@
|
||||
// @TODO get types from sdk
|
||||
type Progress = null | boolean | { done: number; total: number | null }
|
||||
type NamedProgress = { name: string; progress: Progress }
|
||||
type FullProgress = { overall: Progress; phases: Array<NamedProgress> }
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
export function formatProgress({ phases, overall }: FullProgress): {
|
||||
export function formatProgress({ phases, overall }: T.FullProgress): {
|
||||
total: number
|
||||
message: string
|
||||
} {
|
||||
return {
|
||||
total: getDecimal(overall),
|
||||
message: phases
|
||||
.filter(p => p.progress !== true && p.progress !== null)
|
||||
.map(p => `${p.name}${getPhaseBytes(p.progress)}`)
|
||||
.filter(
|
||||
(
|
||||
p,
|
||||
): p is {
|
||||
name: string
|
||||
progress: {
|
||||
done: number
|
||||
total: number | null
|
||||
}
|
||||
} => p.progress !== true && p.progress !== null,
|
||||
)
|
||||
.map(p => `<b>${p.name}</b>${getPhaseBytes(p.progress)}`)
|
||||
.join(', '),
|
||||
}
|
||||
}
|
||||
|
||||
function getDecimal(progress: Progress): number {
|
||||
function getDecimal(progress: T.Progress): number {
|
||||
if (progress === true) {
|
||||
return 1
|
||||
} else if (!progress || !progress.total) {
|
||||
@@ -26,7 +33,7 @@ function getDecimal(progress: Progress): number {
|
||||
}
|
||||
}
|
||||
|
||||
function getPhaseBytes(progress: Progress): string {
|
||||
function getPhaseBytes(progress: T.Progress): string {
|
||||
return progress === true || !progress
|
||||
? ''
|
||||
: `: (${progress.done}/${progress.total})`
|
||||
|
||||
@@ -55,3 +55,6 @@ export function toUrl(text: string | null | undefined): string {
|
||||
}
|
||||
|
||||
export type WithId<T> = T & { id: string }
|
||||
|
||||
export type OptionalProperty<T, K extends keyof T> = Omit<T, K> &
|
||||
Partial<Pick<T, K>>
|
||||
|
||||
@@ -169,7 +169,7 @@ $wide-modal: 900px;
|
||||
|
||||
--portal-header-height: 56px;
|
||||
|
||||
// @TODO rename when make style lib
|
||||
// @TODO Alex rename when make style lib
|
||||
--tw-color-black: 0 0 0;
|
||||
--tw-color-white: 255 255 255;
|
||||
--tw-color-slate-50: 248 250 252;
|
||||
|
||||
@@ -139,7 +139,7 @@ tui-dropdown[data-appearance='start-os'][data-appearance='start-os'] {
|
||||
background: rgb(34 34 34 / 80%);
|
||||
}
|
||||
|
||||
// TODO: Move to Taiga UI
|
||||
// @TODO Alex: Move to Taiga UI
|
||||
a[tuiIconButton]:not([href]) {
|
||||
pointer-events: none;
|
||||
opacity: var(--tui-disabled-opacity);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { debounceTime, endWith, map, merge, Subject } from 'rxjs'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
// @TODO Alex
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'refresh-alert',
|
||||
|
||||
@@ -6,16 +6,7 @@ import {
|
||||
provideSetupLogsService,
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
catchError,
|
||||
defer,
|
||||
EMPTY,
|
||||
from,
|
||||
map,
|
||||
startWith,
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs'
|
||||
import { catchError, defer, from, map, startWith, switchMap, tap } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { StateService } from 'src/app/services/state.service'
|
||||
|
||||
@@ -36,18 +27,25 @@ export default class InitializingPage {
|
||||
defer(() => from(this.api.initFollowProgress())).pipe(
|
||||
switchMap(({ guid, progress }) =>
|
||||
this.api
|
||||
.openWebsocket$<T.FullProgress>(guid, {})
|
||||
.openWebsocket$<T.FullProgress>(guid, {
|
||||
closeObserver: {
|
||||
next: () => {
|
||||
this.state.syncState()
|
||||
},
|
||||
},
|
||||
})
|
||||
.pipe(startWith(progress)),
|
||||
),
|
||||
map(formatProgress),
|
||||
tap<{ total: number; message: string }>(({ total }) => {
|
||||
tap(({ total }) => {
|
||||
if (total === 1) {
|
||||
this.state.syncState()
|
||||
}
|
||||
}),
|
||||
catchError(e => {
|
||||
catchError((e, caught$) => {
|
||||
console.error(e)
|
||||
return EMPTY
|
||||
this.state.syncState()
|
||||
return caught$
|
||||
}),
|
||||
),
|
||||
{ initialValue: { total: 0, message: '' } },
|
||||
|
||||
@@ -42,7 +42,6 @@ export class LoginPage {
|
||||
}
|
||||
await this.api.login({
|
||||
password: this.password,
|
||||
metadata: { platforms: [] }, // @TODO do we really need platforms now?
|
||||
ephemeral: window.location.host === 'localhost',
|
||||
})
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ export abstract class Control<
|
||||
return this.control.spec
|
||||
}
|
||||
|
||||
// TODO: Properly handle already set immutable value
|
||||
// @TODO Alex: Properly handle already set immutable value
|
||||
get readOnly(): boolean {
|
||||
return (
|
||||
!!this.value && !!this.control.control?.pristine && this.control.immutable
|
||||
|
||||
@@ -91,7 +91,7 @@ import { HeaderStatusComponent } from './status.component'
|
||||
}
|
||||
|
||||
&:has([data-status='success']) {
|
||||
--status: var(--tui-status-positive);
|
||||
--status: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -88,6 +88,7 @@ import { InterfaceComponent } from './interface.component'
|
||||
text-align: right;
|
||||
grid-area: 1 / 2 / 3 / 3;
|
||||
place-content: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.mobile {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { MappedServiceInterface } from './interface.utils'
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
max-width: 56rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
import { TuiBadge } from '@taiga-ui/kit'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
selector: 'interface-status',
|
||||
template: `
|
||||
<tui-badge
|
||||
size="l"
|
||||
[iconStart]="public() ? '@tui.globe' : '@tui.lock'"
|
||||
[style.vertical-align.rem]="-0.125"
|
||||
[style.margin]="'0 0.25rem -0.25rem'"
|
||||
[appearance]="public() ? 'positive' : 'negative'"
|
||||
>
|
||||
{{ public() ? 'Public' : 'Private' }}
|
||||
</tui-badge>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiBadge],
|
||||
})
|
||||
export class InterfaceStatusComponent {
|
||||
readonly public = input(false)
|
||||
}
|
||||
@@ -80,7 +80,7 @@ const FILTER = ['/portal/services', '/portal/system', '/portal/marketplace']
|
||||
:host {
|
||||
display: none;
|
||||
backdrop-filter: blur(1rem);
|
||||
// TODO: Theme
|
||||
// TODO Theme
|
||||
--tui-background-elevation-1: #333;
|
||||
--tui-background-base: #fff;
|
||||
--tui-border-normal: var(--tui-background-neutral-1);
|
||||
|
||||
@@ -24,7 +24,7 @@ import { HeaderComponent } from './components/header/header.component'
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
// TODO: Theme
|
||||
// @TODO Theme
|
||||
background: url(/assets/img/background_dark.jpeg) fixed center/cover;
|
||||
}
|
||||
|
||||
|
||||
@@ -102,7 +102,7 @@ export class BackupsUpcomingComponent {
|
||||
readonly targets = toSignal(from(this.api.getBackupTargets({})))
|
||||
readonly current = toSignal(
|
||||
inject<PatchDB<DataModel>>(PatchDB)
|
||||
// @TODO remove "as any" once this feature is real
|
||||
// @TODO 041 remove "as any" once this feature is real
|
||||
.watch$('serverInfo', 'statusInfo', 'currentBackup' as any, 'job')
|
||||
.pipe(map(job => job || {})),
|
||||
)
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { MarketplacePkg } from '@start9labs/marketplace'
|
||||
import { MarketplacePkgBase } from '@start9labs/marketplace'
|
||||
import {
|
||||
Exver,
|
||||
ErrorService,
|
||||
@@ -103,7 +103,7 @@ export class MarketplaceControlsComponent {
|
||||
private readonly marketplaceService = inject(MarketplaceService)
|
||||
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
pkg!: MarketplacePkgBase
|
||||
|
||||
@Input()
|
||||
localPkg!: PackageDataEntry | null
|
||||
|
||||
@@ -22,10 +22,9 @@ import {
|
||||
TuiButton,
|
||||
TuiDialogContext,
|
||||
TuiDialogService,
|
||||
TuiIcon,
|
||||
TuiLoader,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiRadioList, TuiStringifyContentPipe } from '@taiga-ui/kit'
|
||||
import { TuiRadioList } from '@taiga-ui/kit'
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest,
|
||||
@@ -53,7 +52,7 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
@if (!(pkg.dependencyMetadata | empty)) {
|
||||
<marketplace-dependencies [pkg]="pkg" (open)="open($event)" />
|
||||
}
|
||||
<marketplace-additional [pkg]="pkg">
|
||||
<marketplace-additional [pkg]="pkg" (static)="onStatic($event)">
|
||||
@if (versions$ | async; as versions) {
|
||||
<marketplace-additional-item
|
||||
(click)="versions.length ? selectVersion(pkg, version) : 0"
|
||||
@@ -172,10 +171,8 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
AboutModule,
|
||||
SharedPipesModule,
|
||||
FormsModule,
|
||||
TuiStringifyContentPipe,
|
||||
TuiRadioList,
|
||||
TuiLoader,
|
||||
TuiIcon,
|
||||
FlavorsComponent,
|
||||
],
|
||||
})
|
||||
@@ -228,6 +225,10 @@ export class MarketplacePreviewComponent {
|
||||
this.router.navigate([], { queryParams: { id } })
|
||||
}
|
||||
|
||||
onStatic(type: 'License' | 'Instructions') {
|
||||
// @TODO Alex need to display License or Instructions. This requires an API request, check out next/minor
|
||||
}
|
||||
|
||||
selectVersion(
|
||||
{ version }: MarketplacePkg,
|
||||
template: TemplateRef<TuiDialogContext>,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { TUI_CONFIRM } from '@taiga-ui/kit'
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { MarketplacePkg } from '@start9labs/marketplace'
|
||||
import { MarketplacePkg, MarketplacePkgBase } from '@start9labs/marketplace'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { defaultIfEmpty, firstValueFrom } from 'rxjs'
|
||||
@@ -60,7 +60,7 @@ export class MarketplaceAlertsService {
|
||||
})
|
||||
}
|
||||
|
||||
async alertInstall({ alerts }: MarketplacePkg): Promise<boolean> {
|
||||
async alertInstall({ alerts }: MarketplacePkgBase): Promise<boolean> {
|
||||
const content = alerts.install
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,19 +1,27 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import {
|
||||
catchError,
|
||||
defer,
|
||||
filter,
|
||||
ignoreElements,
|
||||
Observable,
|
||||
repeat,
|
||||
retry,
|
||||
shareReplay,
|
||||
startWith,
|
||||
switchMap,
|
||||
take,
|
||||
tap,
|
||||
} from 'rxjs'
|
||||
import { ServerMetrics } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class MetricsService extends Observable<ServerMetrics> {
|
||||
private readonly connection = inject(ConnectionService)
|
||||
private readonly api = inject(ApiService)
|
||||
|
||||
private readonly metrics$ = defer(() =>
|
||||
@@ -22,8 +30,10 @@ export class MetricsService extends Observable<ServerMetrics> {
|
||||
switchMap(({ guid, metrics }) =>
|
||||
this.api.openWebsocket$<ServerMetrics>(guid).pipe(startWith(metrics)),
|
||||
),
|
||||
// @TODO Alex how to handle failure and reconnection here? Simple retry() will not work. Seems like we need a general solution for reconnecting websockets: patchDB, logs, metrics, progress, and any future. Reconnection should depend on server state, then we need to get a new guid, then reconnect. Similar to how patchDB websocket currently behaves on disconnect/reconnect.
|
||||
retry(),
|
||||
catchError(() =>
|
||||
this.connection.pipe(filter(Boolean), take(1), ignoreElements()),
|
||||
),
|
||||
repeat(),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core'
|
||||
|
||||
:host {
|
||||
height: 100%;
|
||||
min-height: 7.5rem;
|
||||
min-height: 9rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
@@ -16,11 +16,6 @@ import { TimeService } from 'src/app/services/time.service'
|
||||
selector: 'metrics-time',
|
||||
template: `
|
||||
@if (now(); as time) {
|
||||
@if (!time.synced) {
|
||||
<tui-notification appearance="warning">
|
||||
<ng-container *ngTemplateOutlet="hint" />
|
||||
</tui-notification>
|
||||
}
|
||||
<div tuiCell>
|
||||
<div tuiTitle [style.text-align]="'center'">
|
||||
<div tuiSubtitle class="g-secondary">
|
||||
@@ -36,6 +31,11 @@ import { TimeService } from 'src/app/services/time.service'
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
@if (!time.synced) {
|
||||
<tui-notification size="s" appearance="warning">
|
||||
<ng-container *ngTemplateOutlet="hint" />
|
||||
</tui-notification>
|
||||
}
|
||||
} @else {
|
||||
Loading...
|
||||
}
|
||||
@@ -61,10 +61,12 @@ import { TimeService } from 'src/app/services/time.service'
|
||||
styles: `
|
||||
:host {
|
||||
height: 100%;
|
||||
min-height: var(--tui-height-l);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
[tuiCell],
|
||||
@@ -72,6 +74,10 @@ import { TimeService } from 'src/app/services/time.service'
|
||||
[tuiSubtitle] {
|
||||
margin: 0;
|
||||
justify-content: center;
|
||||
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -79,6 +85,10 @@ import { TimeService } from 'src/app/services/time.service'
|
||||
display: none;
|
||||
}
|
||||
|
||||
tui-notification {
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
tui-notification {
|
||||
display: none;
|
||||
|
||||
@@ -30,6 +30,7 @@ import { TimeService } from 'src/app/services/time.service'
|
||||
styles: `
|
||||
:host {
|
||||
height: 100%;
|
||||
min-height: var(--tui-height-l);
|
||||
display: flex;
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiIcon, TuiTitle } from '@taiga-ui/core'
|
||||
import { TuiTitle } from '@taiga-ui/core'
|
||||
|
||||
interface ActionItem {
|
||||
readonly name: string
|
||||
@@ -12,7 +12,6 @@ interface ActionItem {
|
||||
@Component({
|
||||
selector: '[action]',
|
||||
template: `
|
||||
<tui-icon [icon]="action.icon || '@tui.circle-play'" />
|
||||
<div tuiTitle>
|
||||
<strong>{{ action.name }}</strong>
|
||||
<div tuiSubtitle>{{ action.description }}</div>
|
||||
@@ -23,7 +22,7 @@ interface ActionItem {
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [TuiIcon, TuiTitle],
|
||||
imports: [TuiTitle],
|
||||
host: {
|
||||
'[disabled]': '!!disabled',
|
||||
},
|
||||
|
||||
@@ -1,27 +1,27 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { DependencyInfo } from 'src/app/routes/portal/routes/services/types/dependency-info'
|
||||
import { map } from 'rxjs'
|
||||
import { ControlsService } from 'src/app/services/controls.service'
|
||||
import { DepErrorService } from 'src/app/services/dep-error.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { PrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
|
||||
@Component({
|
||||
selector: 'service-actions',
|
||||
selector: 'service-controls',
|
||||
template: `
|
||||
@if (['running', 'starting', 'restarting'].includes(status)) {
|
||||
<button
|
||||
tuiButton
|
||||
appearance="secondary-destructive"
|
||||
iconStart="@tui.square"
|
||||
(click)="actions.stop(manifest)"
|
||||
(click)="controls.stop(manifest())"
|
||||
>
|
||||
Stop
|
||||
</button>
|
||||
@@ -31,7 +31,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
<button
|
||||
tuiButton
|
||||
iconStart="@tui.rotate-cw"
|
||||
(click)="actions.restart(manifest)"
|
||||
(click)="controls.restart(manifest())"
|
||||
>
|
||||
Restart
|
||||
</button>
|
||||
@@ -41,7 +41,7 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
<button
|
||||
tuiButton
|
||||
iconStart="@tui.play"
|
||||
(click)="actions.start(manifest, hasUnmet(dependencies))"
|
||||
(click)="controls.start(manifest(), !!hasUnmet)"
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
@@ -78,24 +78,26 @@ import { getManifest } from 'src/app/utils/get-package-data'
|
||||
standalone: true,
|
||||
imports: [TuiButton],
|
||||
})
|
||||
export class ServiceActionsComponent {
|
||||
export class ServiceControlsComponent {
|
||||
private readonly errors = inject(DepErrorService)
|
||||
|
||||
@Input({ required: true })
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
@Input({ required: true })
|
||||
status!: PrimaryStatus
|
||||
|
||||
// TODO
|
||||
dependencies: readonly DependencyInfo[] = []
|
||||
readonly manifest = computed(() => getManifest(this.pkg))
|
||||
|
||||
readonly actions = inject(ControlsService)
|
||||
readonly controls = inject(ControlsService)
|
||||
|
||||
get manifest(): T.Manifest {
|
||||
return getManifest(this.pkg)
|
||||
}
|
||||
|
||||
@tuiPure
|
||||
hasUnmet(dependencies: readonly DependencyInfo[]): boolean {
|
||||
return dependencies.some(dep => !!dep.errorText)
|
||||
}
|
||||
readonly hasUnmet = computed(() =>
|
||||
this.errors.getPkgDepErrors$(this.manifest().id).pipe(
|
||||
map(errors =>
|
||||
Object.keys(this.pkg.currentDependencies)
|
||||
.map(id => errors[id])
|
||||
.some(Boolean),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -5,11 +5,11 @@ import {
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiButton, TuiLink } from '@taiga-ui/core'
|
||||
import { TuiBadge } from '@taiga-ui/kit'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { MappedInterface } from '../types/mapped-interface'
|
||||
|
||||
@Component({
|
||||
selector: 'tr[serviceInterface]',
|
||||
@@ -113,7 +113,10 @@ export class ServiceInterfaceComponent {
|
||||
private readonly config = inject(ConfigService)
|
||||
|
||||
@Input({ required: true })
|
||||
info!: MappedInterface
|
||||
info!: T.ServiceInterface & {
|
||||
public: boolean
|
||||
routerLink: string
|
||||
}
|
||||
|
||||
@Input({ required: true })
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiLoader } from '@taiga-ui/core'
|
||||
import { getProgressText } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe'
|
||||
import { InstallingInfo } from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
PrimaryRendering,
|
||||
PrimaryStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'service-status',
|
||||
@@ -17,7 +18,7 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
|
||||
<tui-loader size="s" [inheritColor]="true" />
|
||||
Installing
|
||||
<span class="loading-dots"></span>
|
||||
{{ installingInfo.progress.overall | installingProgressString }}
|
||||
{{ getText(installingInfo.progress.overall) }}
|
||||
</h3>
|
||||
} @else {
|
||||
<h3 [class]="class">
|
||||
@@ -84,7 +85,7 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
|
||||
host: { class: 'g-card' },
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [InstallingProgressDisplayPipe, TuiLoader],
|
||||
imports: [TuiLoader],
|
||||
})
|
||||
export class ServiceStatusComponent {
|
||||
@Input({ required: true })
|
||||
@@ -120,4 +121,8 @@ export class ServiceStatusComponent {
|
||||
get rendering() {
|
||||
return PrimaryRendering[this.status]
|
||||
}
|
||||
|
||||
getText(progress: T.Progress): string {
|
||||
return getProgressText(progress)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,14 +21,7 @@ import { ServicesService } from './services.service'
|
||||
<th tuiTh [requiredSort]="true" [sorter]="name">Name</th>
|
||||
<th tuiTh>Version</th>
|
||||
<th tuiTh [requiredSort]="true" [sorter]="uptime">Uptime</th>
|
||||
<th
|
||||
tuiTh
|
||||
[requiredSort]="true"
|
||||
[sorter]="status"
|
||||
[style.width.rem]="13"
|
||||
>
|
||||
Status
|
||||
</th>
|
||||
<th tuiTh [requiredSort]="true" [sorter]="status">Status</th>
|
||||
<th [style.width.rem]="8" [style.text-indent.rem]="1.5">Controls</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
inject,
|
||||
Input,
|
||||
} from '@angular/core'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
import { TuiIcon, TuiLoader } from '@taiga-ui/core'
|
||||
import { getProgressText } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { renderPkgStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
|
||||
|
||||
@Component({
|
||||
standalone: true,
|
||||
@@ -31,6 +26,7 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
height: 3rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
:host-context(tui-root._mobile) {
|
||||
@@ -46,11 +42,8 @@ import { InstallingProgressDisplayPipe } from '../pipes/install-progress.pipe'
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
imports: [TuiIcon, TuiLoader],
|
||||
providers: [InstallingProgressDisplayPipe],
|
||||
})
|
||||
export class StatusComponent {
|
||||
private readonly pipe = inject(InstallingProgressDisplayPipe)
|
||||
|
||||
@Input()
|
||||
pkg!: PackageDataEntry
|
||||
|
||||
@@ -72,7 +65,7 @@ export class StatusComponent {
|
||||
|
||||
get status(): string {
|
||||
if (this.pkg.stateInfo.installingInfo) {
|
||||
return `Installing...${this.pipe.transform(this.pkg.stateInfo.installingInfo.progress.overall)}`
|
||||
return `Installing...${getProgressText(this.pkg.stateInfo.installingInfo.progress.overall)}`
|
||||
}
|
||||
|
||||
switch (this.getStatus(this.pkg).primary) {
|
||||
|
||||
@@ -1,22 +1,6 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
// TODO drop these pipes
|
||||
@Pipe({
|
||||
standalone: true,
|
||||
name: 'installingProgressString',
|
||||
})
|
||||
export class InstallingProgressDisplayPipe implements PipeTransform {
|
||||
transform(progress: T.Progress): string {
|
||||
if (progress === true) return 'finalizing'
|
||||
if (progress === false || progress === null || !progress.total)
|
||||
return 'unknown %'
|
||||
const percentage = Math.round((100 * progress.done) / progress.total)
|
||||
|
||||
return percentage < 99 ? String(percentage) + '%' : 'finalizing'
|
||||
}
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
standalone: true,
|
||||
name: 'installingProgress',
|
||||
@@ -28,3 +12,12 @@ export class InstallingProgressPipe implements PipeTransform {
|
||||
return Math.floor((100 * progress.done) / progress.total)
|
||||
}
|
||||
}
|
||||
|
||||
export function getProgressText(progress: T.Progress): string {
|
||||
if (progress === true) return 'finalizing'
|
||||
if (!progress || !progress.total) return 'unknown %'
|
||||
|
||||
const percentage = Math.round((100 * progress.done) / progress.total)
|
||||
|
||||
return percentage < 99 ? `${percentage}%` : 'finalizing'
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 32rem;
|
||||
max-width: 36rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -50,7 +50,7 @@ const OTHER = 'Other Custom Actions'
|
||||
`,
|
||||
styles: `
|
||||
section {
|
||||
max-width: 54rem;
|
||||
max-width: 42rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 2rem;
|
||||
@@ -74,18 +74,20 @@ export default class ServiceActionsRoute {
|
||||
mainStatus: pkg.status.main,
|
||||
icon: pkg.icon,
|
||||
manifest: getManifest(pkg),
|
||||
actions: Object.keys(pkg.actions).reduce<
|
||||
Record<string, ReadonlyArray<T.ActionMetadata & { id: string }>>
|
||||
>(
|
||||
(acc, id) => {
|
||||
const action = { id, ...pkg.actions[id] }
|
||||
const group = pkg.actions[id].group || OTHER
|
||||
const current = acc[group] || []
|
||||
actions: Object.entries(pkg.actions)
|
||||
.filter(([_, val]) => val.visibility !== 'hidden')
|
||||
.reduce<
|
||||
Record<string, ReadonlyArray<T.ActionMetadata & { id: string }>>
|
||||
>(
|
||||
(acc, [id]) => {
|
||||
const action = { id, ...pkg.actions[id] }
|
||||
const group = pkg.actions[id].group || OTHER
|
||||
const current = acc[group] || []
|
||||
|
||||
return { ...acc, [group]: current.concat(action) }
|
||||
},
|
||||
{ [OTHER]: [] },
|
||||
),
|
||||
return { ...acc, [group]: current.concat(action) }
|
||||
},
|
||||
{ [OTHER]: [] },
|
||||
),
|
||||
})),
|
||||
),
|
||||
)
|
||||
@@ -110,7 +112,7 @@ const REBUILD = {
|
||||
icon: '@tui.wrench',
|
||||
name: 'Rebuild Service',
|
||||
description:
|
||||
'Rebuilds the service container. It is harmless and only takes a few seconds to complete, but it should only be necessary if a StartOS bug is preventing dependencies, interfaces, or actions from synchronizing.',
|
||||
'Rebuilds the service container. Only necessary in there is a bug in StartOS',
|
||||
}
|
||||
|
||||
const UNINSTALL = {
|
||||
|
||||
@@ -15,6 +15,7 @@ import { TuiBadge, TuiBreadcrumbs } from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
|
||||
import { InterfaceStatusComponent } from 'src/app/routes/portal/components/interfaces/status.component'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
@@ -24,7 +25,7 @@ import { TitleDirective } from 'src/app/services/title.service'
|
||||
<ng-container *title>
|
||||
<a routerLink="../.." tuiIconButton iconStart="@tui.arrow-left">Back</a>
|
||||
{{ interface()?.name }}
|
||||
<ng-container *ngTemplateOutlet="badge" />
|
||||
<interface-status [public]="!!interface()?.public" />
|
||||
</ng-container>
|
||||
<tui-breadcrumbs size="l" [style.margin-block-end.rem]="1">
|
||||
<a *tuiItem tuiLink appearance="action-grayscale" routerLink="../..">
|
||||
@@ -32,7 +33,7 @@ import { TitleDirective } from 'src/app/services/title.service'
|
||||
</a>
|
||||
<span *tuiItem class="g-primary">
|
||||
{{ interface()?.name }}
|
||||
<ng-container *ngTemplateOutlet="badge" />
|
||||
<interface-status [public]="!!interface()?.public" />
|
||||
</span>
|
||||
</tui-breadcrumbs>
|
||||
@if (interface(); as serviceInterface) {
|
||||
@@ -41,16 +42,6 @@ import { TitleDirective } from 'src/app/services/title.service'
|
||||
[serviceInterface]="serviceInterface"
|
||||
/>
|
||||
}
|
||||
<ng-template #badge>
|
||||
<tui-badge
|
||||
[iconStart]="interface()?.public ? '@tui.globe' : '@tui.lock'"
|
||||
[style.vertical-align.rem]="-0.125"
|
||||
[style.margin]="'0 0.25rem -0.25rem'"
|
||||
[appearance]="interface()?.public ? 'positive' : 'negative'"
|
||||
>
|
||||
{{ interface()?.public ? 'Public' : 'Private' }}
|
||||
</tui-badge>
|
||||
</ng-template>
|
||||
`,
|
||||
styles: `
|
||||
:host-context(tui-root._mobile) tui-breadcrumbs {
|
||||
@@ -70,6 +61,7 @@ import { TitleDirective } from 'src/app/services/title.service'
|
||||
TuiLink,
|
||||
TuiBadge,
|
||||
NgTemplateOutlet,
|
||||
InterfaceStatusComponent,
|
||||
],
|
||||
})
|
||||
export default class ServiceInterfaceRoute {
|
||||
|
||||
@@ -19,7 +19,7 @@ import {
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { getInstalledPrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { ServiceActionRequestsComponent } from '../components/action-requests.component'
|
||||
import { ServiceActionsComponent } from '../components/actions.component'
|
||||
import { ServiceControlsComponent } from '../components/controls.component'
|
||||
import { ServiceDependenciesComponent } from '../components/dependencies.component'
|
||||
import { ServiceErrorComponent } from '../components/error.component'
|
||||
import { ServiceHealthChecksComponent } from '../components/health-checks.component'
|
||||
@@ -38,7 +38,7 @@ import { ServiceStatusComponent } from '../components/status.component'
|
||||
<p class="g-secondary" [appUptime]="started"></p>
|
||||
}
|
||||
@if (installed() && connected()) {
|
||||
<service-actions [pkg]="pkg()" [status]="status()" />
|
||||
<service-controls [pkg]="pkg()" [status]="status()" />
|
||||
}
|
||||
</service-status>
|
||||
|
||||
@@ -90,7 +90,7 @@ import { ServiceStatusComponent } from '../components/status.component'
|
||||
CommonModule,
|
||||
ServiceProgressComponent,
|
||||
ServiceStatusComponent,
|
||||
ServiceActionsComponent,
|
||||
ServiceControlsComponent,
|
||||
ServiceInterfacesComponent,
|
||||
ServiceHealthChecksComponent,
|
||||
ServiceDependenciesComponent,
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
export interface DependencyInfo {
|
||||
id: string
|
||||
title: string | null
|
||||
icon: string | null
|
||||
version: string
|
||||
errorText: string
|
||||
actionText: string
|
||||
action: () => any
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
|
||||
export type MappedInterface = T.ServiceInterface & {
|
||||
public: boolean
|
||||
// TODO implement addresses
|
||||
addresses: any
|
||||
routerLink: string
|
||||
}
|
||||
|
||||
export type MappedAddress = {
|
||||
name: string
|
||||
url: string
|
||||
isDomain: boolean
|
||||
isOnion: boolean
|
||||
acme: string | null
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { Component, inject, Input } from '@angular/core'
|
||||
import { Router, RouterLink } from '@angular/router'
|
||||
import {
|
||||
AboutModule,
|
||||
AdditionalModule,
|
||||
MarketplaceDependenciesComponent,
|
||||
MarketplacePackageHeroComponent,
|
||||
MarketplacePkgBase,
|
||||
} from '@start9labs/marketplace'
|
||||
import {
|
||||
ErrorService,
|
||||
@@ -13,66 +13,39 @@ import {
|
||||
LoadingService,
|
||||
SharedPipesModule,
|
||||
} from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import { TuiProgressBar } from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest, filter, firstValueFrom, map } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ClientStorageService } from 'src/app/services/client-storage.service'
|
||||
import { MarketplaceControlsComponent } from '../marketplace/components/controls.component'
|
||||
import { filter, first, map } from 'rxjs'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { getManifest } from 'src/app/utils/get-package-data'
|
||||
import { InstallingProgressPipe } from 'src/app/routes/portal/routes/services/pipes/install-progress.pipe'
|
||||
import { SideloadService } from './sideload.service'
|
||||
import { MarketplacePkgSideload } from './sideload.utils'
|
||||
|
||||
@Component({
|
||||
selector: 'sideload-package',
|
||||
template: `
|
||||
<div class="outer-container">
|
||||
<ng-content />
|
||||
@if (progress$ | async; as progress) {
|
||||
@for (phase of progress.phases; track $index) {
|
||||
<p>
|
||||
{{ phase.name }}
|
||||
@if (phase.progress | installingProgress; as progress) {
|
||||
: {{ progress }}%
|
||||
}
|
||||
</p>
|
||||
<progress
|
||||
tuiProgressBar
|
||||
size="xs"
|
||||
[style.color]="
|
||||
phase.progress === true
|
||||
? 'var(--tui-text-positive)'
|
||||
: 'var(--tui-text-action)'
|
||||
"
|
||||
[attr.value]="(phase.progress | installingProgress) / 100 || null"
|
||||
></progress>
|
||||
}
|
||||
} @else {
|
||||
<marketplace-package-hero
|
||||
*tuiLet="button$ | async as button"
|
||||
[pkg]="package"
|
||||
>
|
||||
<div class="inner-container">
|
||||
@if (button !== null && button !== 'Install') {
|
||||
<a tuiButton [routerLink]="'/portal/services/' + package.id">
|
||||
View installed
|
||||
</a>
|
||||
}
|
||||
@if (button) {
|
||||
<button tuiButton (click)="upload()">{{ button }}</button>
|
||||
}
|
||||
</div>
|
||||
</marketplace-package-hero>
|
||||
<!-- @TODO Matt do we want this here? How do we turn s9pk into MarketplacePkg? -->
|
||||
<!-- <marketplace-about [pkg]="package" />-->
|
||||
<!-- @if (!(package.dependencyMetadata | empty)) {-->
|
||||
<!-- <marketplace-dependencies [pkg]="package" (open)="open($event)" />-->
|
||||
<!-- }-->
|
||||
<!-- <marketplace-additional [pkg]="package" />-->
|
||||
}
|
||||
<marketplace-package-hero [pkg]="pkg">
|
||||
<marketplace-controls
|
||||
slot="controls"
|
||||
class="controls-wrapper"
|
||||
[pkg]="pkg"
|
||||
[localPkg]="local$ | async"
|
||||
[localFlavor]="!!(flavor$ | async)"
|
||||
/>
|
||||
</marketplace-package-hero>
|
||||
<div class="package-details">
|
||||
<div class="package-details-main">
|
||||
<marketplace-about [pkg]="pkg" />
|
||||
@if (!(pkg.dependencyMetadata | empty)) {
|
||||
<marketplace-dependencies [pkg]="pkg" />
|
||||
}
|
||||
</div>
|
||||
<div class="package-details-additional">
|
||||
<marketplace-additional [pkg]="pkg" (static)="onStatic($event)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [
|
||||
@@ -83,95 +56,97 @@ import { SideloadService } from './sideload.service'
|
||||
width: 100%;
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
max-width: 80%;
|
||||
margin: auto;
|
||||
padding: 2.5rem 4rem 2rem 4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.inner-container {
|
||||
.package-details {
|
||||
-moz-column-gap: 2rem;
|
||||
column-gap: 2rem;
|
||||
|
||||
&-main {
|
||||
grid-column: span 12 / span 12;
|
||||
}
|
||||
|
||||
&-additional {
|
||||
grid-column: span 12 / span 12;
|
||||
}
|
||||
|
||||
@media (min-width: 1536px) {
|
||||
grid-template-columns: repeat(12, minmax(0, 1fr));
|
||||
&-main {
|
||||
grid-column: span 8 / span 8;
|
||||
}
|
||||
&-additional {
|
||||
grid-column: span 4 / span 4;
|
||||
margin-top: 0px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.controls-wrapper {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin: -0.5rem 0 1.5rem -1px;
|
||||
gap: 0.5rem;
|
||||
height: 4.5rem;
|
||||
}
|
||||
`,
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
RouterLink,
|
||||
SharedPipesModule,
|
||||
AboutModule,
|
||||
AdditionalModule,
|
||||
TuiButton,
|
||||
TuiLet,
|
||||
MarketplacePackageHeroComponent,
|
||||
MarketplaceDependenciesComponent,
|
||||
InstallingProgressPipe,
|
||||
TuiProgressBar,
|
||||
MarketplaceControlsComponent,
|
||||
],
|
||||
})
|
||||
export class SideloadPackageComponent {
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly router = inject(Router)
|
||||
private readonly exver = inject(Exver)
|
||||
private readonly sideloadService = inject(SideloadService)
|
||||
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
|
||||
|
||||
readonly progress$ = this.sideloadService.progress$
|
||||
readonly button$ = combineLatest([
|
||||
inject(ClientStorageService).showDevTools$,
|
||||
inject<PatchDB<DataModel>>(PatchDB)
|
||||
.watch$('packageData')
|
||||
.pipe(
|
||||
map(local =>
|
||||
local[this.package.id]
|
||||
? this.exver.compareExver(
|
||||
getManifest(local[this.package.id]).version,
|
||||
this.package.version,
|
||||
)
|
||||
: null,
|
||||
),
|
||||
),
|
||||
]).pipe(
|
||||
map(([devtools, version]) => {
|
||||
switch (version) {
|
||||
case null:
|
||||
return 'Install'
|
||||
case 1:
|
||||
return 'Update'
|
||||
case -1:
|
||||
return devtools ? 'Downgrade' : ''
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}),
|
||||
)
|
||||
// @Input({ required: true })
|
||||
// pkg!: MarketplacePkgSideload
|
||||
|
||||
// @Alex why do I need to initialize pkg below? I would prefer to do the above, but it's not working
|
||||
@Input({ required: true })
|
||||
package!: T.Manifest & { icon: string }
|
||||
pkg: MarketplacePkgSideload = {} as MarketplacePkgSideload
|
||||
|
||||
@Input({ required: true })
|
||||
file!: File
|
||||
|
||||
readonly local$ = this.patch.watch$('packageData', this.pkg.id).pipe(
|
||||
filter(Boolean),
|
||||
map(pkg =>
|
||||
this.exver.getFlavor(getManifest(pkg).version) === this.pkg.flavor
|
||||
? pkg
|
||||
: null,
|
||||
),
|
||||
first(),
|
||||
)
|
||||
|
||||
readonly flavor$ = this.local$.pipe(map(pkg => !pkg))
|
||||
|
||||
onStatic(type: 'License' | 'Instructions') {
|
||||
// @TODO Matt display License or Instructions
|
||||
}
|
||||
|
||||
async upload() {
|
||||
const loader = this.loader.open('Starting upload').subscribe()
|
||||
|
||||
try {
|
||||
const { upload, progress } = await this.api.sideloadPackage()
|
||||
|
||||
this.sideloadService.followProgress(progress)
|
||||
const { upload } = await this.api.sideloadPackage()
|
||||
this.api.uploadPackage(upload, this.file).catch(console.error)
|
||||
await firstValueFrom(this.progress$.pipe(filter(Boolean)))
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
open(id: string) {
|
||||
this.router.navigate(['/marketplace'], { queryParams: { id } })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
inject,
|
||||
signal,
|
||||
} from '@angular/core'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import { MarketplacePkgBase } from '@start9labs/marketplace'
|
||||
import { tuiIsString } from '@taiga-ui/cdk'
|
||||
import { TuiButton } from '@taiga-ui/core'
|
||||
import {
|
||||
@@ -17,16 +16,16 @@ import {
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
import { SideloadPackageComponent } from './package.component'
|
||||
import { parseS9pk } from './sideload.utils'
|
||||
import { MarketplacePkgSideload, validateS9pk } from './sideload.utils'
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<ng-container *title>Sideload</ng-container>
|
||||
@if (file && package()) {
|
||||
<sideload-package [package]="package()!" [file]="file!">
|
||||
@if (file && package(); as pkg) {
|
||||
<sideload-package [pkg]="pkg" [file]="file!">
|
||||
<button
|
||||
tuiIconButton
|
||||
appearance="secondary"
|
||||
appearance="neutral"
|
||||
iconStart="@tui.x"
|
||||
[style.border-radius.%]="100"
|
||||
[style.justify-self]="'end'"
|
||||
@@ -55,7 +54,10 @@ import { parseS9pk } from './sideload.utils'
|
||||
<tui-avatar appearance="secondary" src="@tui.cloud-upload" />
|
||||
<p>Upload .s9pk package file</p>
|
||||
@if (isTor) {
|
||||
<p class="g-positive">Tip: switch to LAN for faster uploads</p>
|
||||
<p class="g-warning">
|
||||
Warning: package upload will be slow over Tor. Switch to local
|
||||
for a better experience.
|
||||
</p>
|
||||
}
|
||||
<button tuiButton>Upload</button>
|
||||
</div>
|
||||
@@ -69,7 +71,7 @@ import { parseS9pk } from './sideload.utils'
|
||||
`
|
||||
label {
|
||||
height: 100%;
|
||||
max-width: 40rem;
|
||||
max-width: 42rem;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
@@ -91,11 +93,10 @@ import { parseS9pk } from './sideload.utils'
|
||||
],
|
||||
})
|
||||
export default class SideloadComponent {
|
||||
private readonly cdr = inject(ChangeDetectorRef)
|
||||
readonly isTor = inject(ConfigService).isTor()
|
||||
|
||||
file: File | null = null
|
||||
readonly package = signal<(T.Manifest & { icon: string }) | null>(null)
|
||||
readonly package = signal<MarketplacePkgSideload | null>(null)
|
||||
readonly error = signal('')
|
||||
|
||||
clear() {
|
||||
@@ -105,12 +106,11 @@ export default class SideloadComponent {
|
||||
}
|
||||
|
||||
async onFile(file: File | null) {
|
||||
const parsed = file ? await parseS9pk(file) : ''
|
||||
|
||||
this.file = file
|
||||
|
||||
const parsed = file ? await validateS9pk(file) : ''
|
||||
|
||||
this.package.set(tuiIsString(parsed) ? null : parsed)
|
||||
this.error.set(tuiIsString(parsed) ? parsed : '')
|
||||
// @TODO Alex figure out why it is needed even though we use signals
|
||||
this.cdr.markForCheck()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { inject, Injectable } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { ErrorService } from '@start9labs/shared'
|
||||
import { T } from '@start9labs/start-sdk'
|
||||
import {
|
||||
catchError,
|
||||
EMPTY,
|
||||
endWith,
|
||||
shareReplay,
|
||||
Subject,
|
||||
switchMap,
|
||||
tap,
|
||||
} from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class SideloadService {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly guid$ = new Subject<string>()
|
||||
private readonly errorService = inject(ErrorService)
|
||||
private readonly router = inject(Router)
|
||||
|
||||
readonly progress$ = this.guid$.pipe(
|
||||
switchMap(guid =>
|
||||
this.api
|
||||
.openWebsocket$<T.FullProgress>(guid, {
|
||||
closeObserver: {
|
||||
next: event => {
|
||||
if (event.code !== 1000) {
|
||||
this.errorService.handleError(event.reason)
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
.pipe(
|
||||
tap(p => {
|
||||
if (p.overall === true) {
|
||||
this.router.navigate([''], { replaceUrl: true })
|
||||
}
|
||||
}),
|
||||
endWith(null),
|
||||
),
|
||||
),
|
||||
catchError(e => {
|
||||
this.errorService.handleError('Websocket connection broken. Try again.')
|
||||
return EMPTY
|
||||
}),
|
||||
shareReplay(1),
|
||||
)
|
||||
|
||||
followProgress(guid: string) {
|
||||
this.guid$.next(guid)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +1,22 @@
|
||||
import { S9pk, T } from '@start9labs/start-sdk'
|
||||
import cbor from 'cbor'
|
||||
import { MarketplacePkgBase } from '@start9labs/marketplace'
|
||||
import { S9pk, ExtendedVersion } from '@start9labs/start-sdk'
|
||||
|
||||
const MAGIC = new Uint8Array([59, 59])
|
||||
const VERSION_1 = new Uint8Array([1])
|
||||
const VERSION_2 = new Uint8Array([2])
|
||||
|
||||
interface Positions {
|
||||
[key: string]: [bigint, bigint] // [position, length]
|
||||
}
|
||||
|
||||
export async function parseS9pk(
|
||||
export async function validateS9pk(
|
||||
file: File,
|
||||
): Promise<(T.Manifest & { icon: string }) | string> {
|
||||
): Promise<MarketplacePkgSideload | string> {
|
||||
const magic = new Uint8Array(await blobToBuffer(file.slice(0, 2)))
|
||||
const version = new Uint8Array(await blobToBuffer(file.slice(2, 3)))
|
||||
|
||||
if (compare(magic, MAGIC)) {
|
||||
try {
|
||||
if (compare(version, VERSION_1)) {
|
||||
return await parseS9pkV1(file)
|
||||
return 'Version 1 s9pk detected. This package format is deprecated. You can sideload a V1 s9pk via start-cli if necessary.'
|
||||
} else if (compare(version, VERSION_2)) {
|
||||
const s9pk = await S9pk.deserialize(file, null)
|
||||
|
||||
return {
|
||||
...s9pk.manifest,
|
||||
icon: await s9pk.icon(),
|
||||
}
|
||||
return await parseS9pk(file)
|
||||
} else {
|
||||
console.error(version)
|
||||
|
||||
@@ -43,92 +34,21 @@ export async function parseS9pk(
|
||||
return 'Invalid package file'
|
||||
}
|
||||
|
||||
async function parseS9pkV1(file: File) {
|
||||
const positions: Positions = {}
|
||||
// magic=2bytes, version=1bytes, pubkey=32bytes, signature=64bytes, toc_length=4bytes = 103byte is starting point
|
||||
let start = 103
|
||||
let end = start + 1 // 104
|
||||
const tocLength = new DataView(
|
||||
await blobToBuffer(file.slice(99, 103) ?? new Blob()),
|
||||
).getUint32(0, false)
|
||||
await getPositions(start, end, file, positions, tocLength as any)
|
||||
|
||||
const data = await blobToBuffer(
|
||||
file.slice(
|
||||
Number(positions['manifest'][0]),
|
||||
Number(positions['manifest'][0]) + Number(positions['manifest'][1]),
|
||||
),
|
||||
)
|
||||
async function parseS9pk(file: File): Promise<MarketplacePkgSideload> {
|
||||
const s9pk = await S9pk.deserialize(file, null)
|
||||
|
||||
return {
|
||||
...(await cbor.decode(data, true)),
|
||||
icon: await blobToDataURL(
|
||||
file.slice(
|
||||
Number(positions['icon'][0]),
|
||||
Number(positions['icon'][0]) + Number(positions['icon'][1]),
|
||||
'',
|
||||
),
|
||||
),
|
||||
...s9pk.manifest,
|
||||
dependencyMetadata: await s9pk.dependencyMetadata(),
|
||||
gitHash: '',
|
||||
icon: await s9pk.icon(),
|
||||
sourceVersion: s9pk.manifest.canMigrateFrom,
|
||||
flavor: ExtendedVersion.parse(s9pk.manifest.version).flavor,
|
||||
license: await s9pk.license(),
|
||||
instructions: await s9pk.instructions(),
|
||||
}
|
||||
}
|
||||
|
||||
async function getPositions(
|
||||
initialStart: number,
|
||||
initialEnd: number,
|
||||
file: Blob,
|
||||
positions: Positions,
|
||||
tocLength: number,
|
||||
) {
|
||||
let start = initialStart
|
||||
let end = initialEnd
|
||||
const titleLength = new Uint8Array(
|
||||
await blobToBuffer(file.slice(start, end)),
|
||||
)[0]
|
||||
const tocTitle = await file.slice(end, end + titleLength).text()
|
||||
start = end + titleLength
|
||||
end = start + 8
|
||||
const chapterPosition = new DataView(
|
||||
await blobToBuffer(file.slice(start, end)),
|
||||
).getBigUint64(0, false)
|
||||
start = end
|
||||
end = start + 8
|
||||
const chapterLength = new DataView(
|
||||
await blobToBuffer(file.slice(start, end)),
|
||||
).getBigUint64(0, false)
|
||||
|
||||
positions[tocTitle] = [chapterPosition, chapterLength]
|
||||
start = end
|
||||
end = start + 1
|
||||
if (end <= tocLength + (initialStart - 1)) {
|
||||
await getPositions(start, end, file, positions, tocLength)
|
||||
}
|
||||
}
|
||||
|
||||
async function readBlobAsDataURL(
|
||||
f: Blob | File,
|
||||
): Promise<string | ArrayBuffer | null> {
|
||||
const reader = new FileReader()
|
||||
return new Promise((resolve, reject) => {
|
||||
reader.onloadend = () => {
|
||||
resolve(reader.result)
|
||||
}
|
||||
reader.readAsDataURL(f)
|
||||
reader.onerror = _ => reject(new Error('error reading blob'))
|
||||
})
|
||||
}
|
||||
|
||||
async function blobToDataURL(data: Blob | File): Promise<string> {
|
||||
const res = await readBlobAsDataURL(data)
|
||||
if (res instanceof ArrayBuffer) {
|
||||
throw new Error('readBlobAsDataURL response should not be an array buffer')
|
||||
}
|
||||
if (res == null) {
|
||||
throw new Error('readBlobAsDataURL response should not be null')
|
||||
}
|
||||
if (typeof res === 'string') return res
|
||||
throw new Error('no possible blob to data url resolution found')
|
||||
}
|
||||
|
||||
async function blobToBuffer(data: Blob | File): Promise<ArrayBuffer> {
|
||||
const res = await readBlobToArrayBuffer(data)
|
||||
if (res instanceof String) {
|
||||
@@ -158,3 +78,8 @@ async function readBlobToArrayBuffer(
|
||||
function compare(a: Uint8Array, b: Uint8Array) {
|
||||
return a.length === b.length && a.every((value, index) => value === b[index])
|
||||
}
|
||||
|
||||
export type MarketplacePkgSideload = MarketplacePkgBase & {
|
||||
license: string
|
||||
instructions: string
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
max-width: 40rem;
|
||||
max-width: 36rem;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -140,7 +140,7 @@ export default class SystemDomainsComponent {
|
||||
|
||||
this.formDialog.open(FormComponent, options)
|
||||
}
|
||||
// @TODO figure out how to get types here
|
||||
// @TODO 041 figure out how to get types here
|
||||
private getNetworkStrategy(strategy: any) {
|
||||
return strategy.selection === 'local'
|
||||
? { ipStrategy: strategy.value.ipStrategy }
|
||||
@@ -162,7 +162,7 @@ export default class SystemDomainsComponent {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
// @TODO figure out how to get types here
|
||||
// @TODO 041 figure out how to get types here
|
||||
private async claimDomain({ strategy }: any): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
const networkStrategy = this.getNetworkStrategy(strategy)
|
||||
@@ -177,7 +177,7 @@ export default class SystemDomainsComponent {
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
// @TODO figure out how to get types here
|
||||
// @TODO 041 figure out how to get types here
|
||||
private async save({ provider, strategy, hostname }: any): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
const name = provider.selection
|
||||
|
||||
@@ -97,7 +97,7 @@ import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
max-width: 40rem;
|
||||
max-width: 36rem;
|
||||
}
|
||||
|
||||
form header,
|
||||
|
||||
@@ -8,6 +8,7 @@ import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs'
|
||||
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
|
||||
import { getAddresses } from 'src/app/routes/portal/components/interfaces/interface.utils'
|
||||
import { InterfaceStatusComponent } from 'src/app/routes/portal/components/interfaces/status.component'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { TitleDirective } from 'src/app/services/title.service'
|
||||
@@ -34,10 +35,14 @@ const iface: T.ServiceInterface = {
|
||||
<ng-container *title>
|
||||
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
|
||||
StartOS UI
|
||||
<interface-status [public]="!!ui()?.public" />
|
||||
</ng-container>
|
||||
<header tuiHeader>
|
||||
<hgroup tuiTitle>
|
||||
<h3>{{ iface.name }}</h3>
|
||||
<h3>
|
||||
{{ iface.name }}
|
||||
<interface-status [public]="!!ui()?.public" />
|
||||
</h3>
|
||||
<p tuiSubtitle>{{ iface.description }}</p>
|
||||
</hgroup>
|
||||
</header>
|
||||
@@ -54,6 +59,7 @@ const iface: T.ServiceInterface = {
|
||||
TitleDirective,
|
||||
TuiHeader,
|
||||
TuiTitle,
|
||||
InterfaceStatusComponent,
|
||||
],
|
||||
})
|
||||
export default class StartOsUiComponent {
|
||||
|
||||
@@ -62,7 +62,7 @@ export default class SystemProxiesComponent {
|
||||
this.formDialog.open(FormComponent, options)
|
||||
}
|
||||
|
||||
// @TODO fix type to be WireguardSpec
|
||||
// @TODO 041 fix type to be WireguardSpec
|
||||
private async save({ name, config }: any): Promise<boolean> {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
|
||||
|
||||
@@ -1,38 +1,45 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { PlatformType } from 'src/app/services/api/api.types'
|
||||
|
||||
@Pipe({
|
||||
name: 'platformInfo',
|
||||
standalone: true,
|
||||
})
|
||||
export class PlatformInfoPipe implements PipeTransform {
|
||||
transform(platforms: readonly PlatformType[]): {
|
||||
transform(userAgent: string | null): {
|
||||
name: string
|
||||
icon: string
|
||||
} {
|
||||
const info = {
|
||||
name: '',
|
||||
icon: '@tui.smartphone',
|
||||
if (!userAgent) {
|
||||
return {
|
||||
name: 'CLI',
|
||||
icon: '@tui.terminal',
|
||||
}
|
||||
}
|
||||
|
||||
if (platforms.includes('cli')) {
|
||||
info.name = 'CLI'
|
||||
info.icon = '@tui.terminal'
|
||||
} else if (platforms.includes('desktop')) {
|
||||
info.name = 'Desktop/Laptop'
|
||||
info.icon = '@tui.monitor'
|
||||
} else if (platforms.includes('android')) {
|
||||
info.name = 'Android Device'
|
||||
} else if (platforms.includes('iphone')) {
|
||||
info.name = 'iPhone'
|
||||
} else if (platforms.includes('ipad')) {
|
||||
info.name = 'iPad'
|
||||
} else if (platforms.includes('ios')) {
|
||||
info.name = 'iOS Device'
|
||||
} else {
|
||||
info.name = 'Unknown Device'
|
||||
if (/Android/i.test(userAgent)) {
|
||||
return {
|
||||
name: 'Android Device',
|
||||
icon: '@tui.smartphone',
|
||||
}
|
||||
}
|
||||
|
||||
return info
|
||||
if (/iPhone/i.test(userAgent)) {
|
||||
return {
|
||||
name: 'iPhone',
|
||||
icon: '@tui.smartphone',
|
||||
}
|
||||
}
|
||||
|
||||
if (/iPad/i.test(userAgent)) {
|
||||
return {
|
||||
name: 'iPad',
|
||||
icon: '@tui.smartphone',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'Desktop/Laptop',
|
||||
icon: '@tui.monitor',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { RouterLink } from '@angular/router'
|
||||
import { TuiTable } from '@taiga-ui/addon-table'
|
||||
import { TuiLet } from '@taiga-ui/cdk'
|
||||
import { TuiButton, TuiTitle } from '@taiga-ui/core'
|
||||
import { CommonModule } from '@angular/common'
|
||||
|
||||
@@ -33,10 +33,10 @@ import { PlatformInfoPipe } from './platform-info.pipe'
|
||||
<span tuiFade class="agent">{{ session.userAgent }}</span>
|
||||
</label>
|
||||
</td>
|
||||
@if (session.metadata.platforms | platformInfo; as info) {
|
||||
@if (session.userAgent | platformInfo; as platform) {
|
||||
<td class="platform">
|
||||
<tui-icon [icon]="info.icon" />
|
||||
{{ info.name }}
|
||||
<tui-icon [icon]="platform.icon" />
|
||||
{{ platform.name }}
|
||||
</td>
|
||||
}
|
||||
<td class="date">{{ session.lastActive | date: 'medium' }}</td>
|
||||
|
||||
@@ -103,7 +103,7 @@ import { wifiSpec } from './wifi.const'
|
||||
`,
|
||||
styles: `
|
||||
:host {
|
||||
max-width: 40rem;
|
||||
max-width: 36rem;
|
||||
}
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
||||
@@ -935,26 +935,17 @@ export namespace Mock {
|
||||
loggedIn: '2021-07-14T20:49:17.774Z',
|
||||
lastActive: '2021-07-14T20:49:17.774Z',
|
||||
userAgent: 'AppleWebKit/{WebKit Rev} (KHTML, like Gecko)',
|
||||
metadata: {
|
||||
platforms: ['iphone', 'mobileweb', 'mobile', 'ios'],
|
||||
},
|
||||
},
|
||||
klndsfjhbwsajkdnaksj: {
|
||||
loggedIn: '2021-07-14T20:49:17.774Z',
|
||||
lastActive: '2019-07-14T20:49:17.774Z',
|
||||
userAgent: 'AppleWebKit/{WebKit Rev} (KHTML, like Gecko)',
|
||||
metadata: {
|
||||
platforms: ['cli'],
|
||||
},
|
||||
},
|
||||
b7b1a9cef4284f00af9e9dda6e676177: {
|
||||
loggedIn: '2021-07-14T20:49:17.774Z',
|
||||
lastActive: '2021-06-14T20:49:17.774Z',
|
||||
userAgent:
|
||||
'Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:47.0) Gecko/20100101 Firefox/47.0',
|
||||
metadata: {
|
||||
platforms: ['desktop'],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -1500,7 +1491,6 @@ export namespace Mock {
|
||||
},
|
||||
{
|
||||
spec: ISB.InputSpec.of({
|
||||
/* TODO: Convert range for this value ([0, 2])*/
|
||||
union: ISB.Value.union(
|
||||
{
|
||||
name: 'Preference',
|
||||
@@ -1560,19 +1550,18 @@ export namespace Mock {
|
||||
},
|
||||
disabled: ['option2'],
|
||||
})),
|
||||
'favorite-number':
|
||||
/* TODO: Convert range for this value ((-100,100])*/ ISB.Value.number(
|
||||
{
|
||||
name: 'Favorite Number',
|
||||
description: 'Your favorite number of all time',
|
||||
warning:
|
||||
'Once you set this number, it can never be changed without severe consequences.',
|
||||
required: false,
|
||||
default: 7,
|
||||
integer: false,
|
||||
units: 'BTC',
|
||||
},
|
||||
),
|
||||
'favorite-number': ISB.Value.number({
|
||||
name: 'Favorite Number',
|
||||
description: 'Your favorite number of all time',
|
||||
warning:
|
||||
'Once you set this number, it can never be changed without severe consequences.',
|
||||
required: false,
|
||||
default: 7,
|
||||
integer: false,
|
||||
units: 'BTC',
|
||||
min: -100,
|
||||
max: 100,
|
||||
}),
|
||||
rpcsettings: ISB.Value.object(
|
||||
{
|
||||
name: 'RPC Settings',
|
||||
@@ -1906,7 +1895,7 @@ export namespace Mock {
|
||||
name: 'View Properties',
|
||||
description: 'view important information about Bitcoin',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
visibility: 'hidden',
|
||||
allowedStatuses: 'any',
|
||||
hasInput: false,
|
||||
group: null,
|
||||
|
||||
@@ -31,7 +31,6 @@ export namespace RR {
|
||||
|
||||
export type LoginReq = {
|
||||
password: string
|
||||
metadata: SessionMetadata
|
||||
ephemeral?: boolean
|
||||
} // auth.login - unauthed
|
||||
export type loginRes = null
|
||||
@@ -421,30 +420,8 @@ export type Session = {
|
||||
loggedIn: string
|
||||
lastActive: string
|
||||
userAgent: string
|
||||
metadata: SessionMetadata
|
||||
}
|
||||
|
||||
export type SessionMetadata = {
|
||||
platforms: PlatformType[]
|
||||
}
|
||||
|
||||
export type PlatformType =
|
||||
| 'cli'
|
||||
| 'ios'
|
||||
| 'ipad'
|
||||
| 'iphone'
|
||||
| 'android'
|
||||
| 'phablet'
|
||||
| 'tablet'
|
||||
| 'cordova'
|
||||
| 'capacitor'
|
||||
| 'electron'
|
||||
| 'pwa'
|
||||
| 'mobile'
|
||||
| 'mobileweb'
|
||||
| 'desktop'
|
||||
| 'hybrid'
|
||||
|
||||
export type BackupTarget = DiskBackupTarget | CifsBackupTarget
|
||||
|
||||
export interface DiskBackupTarget {
|
||||
@@ -604,7 +581,7 @@ export type DependencyErrorTransitive = {
|
||||
type: 'transitive'
|
||||
}
|
||||
|
||||
// **** @TODO 041 ****
|
||||
// @TODO 041
|
||||
|
||||
// export namespace RR041 {
|
||||
// // ** domains **
|
||||
|
||||
@@ -252,7 +252,7 @@ export const mockPatchData: DataModel = {
|
||||
name: 'View Properties',
|
||||
description: 'view important information about Bitcoin',
|
||||
warning: null,
|
||||
visibility: 'enabled',
|
||||
visibility: 'hidden',
|
||||
allowedStatuses: 'any',
|
||||
hasInput: false,
|
||||
group: null,
|
||||
|
||||
@@ -357,11 +357,11 @@ export function listUnique(spec: IST.ValueSpecList): ValidatorFn {
|
||||
const objSpec = spec.spec
|
||||
let display1: string
|
||||
let display2: string
|
||||
let uniqueMessage = isObject(objSpec)
|
||||
let uniqueMessage = isListObject(objSpec)
|
||||
? uniqueByMessageWrapper(objSpec.uniqueBy, objSpec)
|
||||
: ''
|
||||
|
||||
if (isObject(objSpec) && objSpec.displayAs) {
|
||||
if (isListObject(objSpec) && objSpec.displayAs) {
|
||||
display1 = `"${(Mustache as any).render(
|
||||
objSpec.displayAs,
|
||||
list[idx],
|
||||
@@ -390,7 +390,6 @@ function listItemEquals(
|
||||
val1: any,
|
||||
val2: any,
|
||||
): boolean {
|
||||
// TODO: fix types
|
||||
switch (spec.spec.type) {
|
||||
case 'text':
|
||||
return val1 == val2
|
||||
@@ -402,45 +401,6 @@ function listItemEquals(
|
||||
}
|
||||
}
|
||||
|
||||
function itemEquals(spec: IST.ValueSpec, val1: any, val2: any): boolean {
|
||||
switch (spec.type) {
|
||||
case 'text':
|
||||
case 'textarea':
|
||||
case 'number':
|
||||
case 'toggle':
|
||||
case 'select':
|
||||
return val1 == val2
|
||||
case 'object':
|
||||
// TODO: 'unique-by' does not exist on ValueSpecObject, fix types
|
||||
return objEquals(
|
||||
(spec as any)['unique-by'],
|
||||
spec as IST.ValueSpecObject,
|
||||
val1,
|
||||
val2,
|
||||
)
|
||||
case 'union':
|
||||
// TODO: 'unique-by' does not exist onIST.ValueSpecUnion, fix types
|
||||
return unionEquals(
|
||||
(spec as any)['unique-by'],
|
||||
spec as IST.ValueSpecUnion,
|
||||
val1,
|
||||
val2,
|
||||
)
|
||||
case 'list':
|
||||
if (val1.length !== val2.length) {
|
||||
return false
|
||||
}
|
||||
for (let idx = 0; idx < val1.length; idx++) {
|
||||
if (listItemEquals(spec, val1[idx], val2[idx])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function listObjEquals(
|
||||
uniqueBy: IST.UniqueBy,
|
||||
spec: IST.ListValueSpecObject,
|
||||
@@ -450,17 +410,17 @@ function listObjEquals(
|
||||
if (!uniqueBy) {
|
||||
return false
|
||||
} else if (typeof uniqueBy === 'string') {
|
||||
return itemEquals(spec.spec[uniqueBy], val1[uniqueBy], val2[uniqueBy])
|
||||
return uniqueByEquals(spec.spec[uniqueBy], val1[uniqueBy], val2[uniqueBy])
|
||||
} else if ('any' in uniqueBy) {
|
||||
for (let subSpec of uniqueBy.any) {
|
||||
if (listObjEquals(subSpec, spec, val1, val2)) {
|
||||
for (let unique of uniqueBy.any) {
|
||||
if (listObjEquals(unique, spec, val1, val2)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else if ('all' in uniqueBy) {
|
||||
for (let subSpec of uniqueBy.all) {
|
||||
if (!listObjEquals(subSpec, spec, val1, val2)) {
|
||||
for (let unique of uniqueBy.all) {
|
||||
if (!listObjEquals(unique, spec, val1, val2)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -469,66 +429,29 @@ function listObjEquals(
|
||||
return false
|
||||
}
|
||||
|
||||
function objEquals(
|
||||
uniqueBy: IST.UniqueBy,
|
||||
spec: IST.ValueSpecObject,
|
||||
val1: any,
|
||||
val2: any,
|
||||
): boolean {
|
||||
if (!uniqueBy) {
|
||||
return false
|
||||
} else if (typeof uniqueBy === 'string') {
|
||||
// TODO: fix types
|
||||
return itemEquals((spec as any)[uniqueBy], val1[uniqueBy], val2[uniqueBy])
|
||||
} else if ('any' in uniqueBy) {
|
||||
for (let subSpec of uniqueBy.any) {
|
||||
if (objEquals(subSpec, spec, val1, val2)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else if ('all' in uniqueBy) {
|
||||
for (let subSpec of uniqueBy.all) {
|
||||
if (!objEquals(subSpec, spec, val1, val2)) {
|
||||
function uniqueByEquals(spec: IST.ValueSpec, val1: any, val2: any): boolean {
|
||||
switch (spec.type) {
|
||||
case 'text':
|
||||
case 'textarea':
|
||||
case 'number':
|
||||
case 'toggle':
|
||||
case 'select':
|
||||
case 'color':
|
||||
case 'datetime':
|
||||
return val1 == val2
|
||||
case 'list':
|
||||
if (val1.length !== val2.length) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function unionEquals(
|
||||
uniqueBy: IST.UniqueBy,
|
||||
spec: IST.ValueSpecUnion,
|
||||
val1: any,
|
||||
val2: any,
|
||||
): boolean {
|
||||
const variantSpec = spec.variants[val1.selection].spec
|
||||
if (!uniqueBy) {
|
||||
return false
|
||||
} else if (typeof uniqueBy === 'string') {
|
||||
if (uniqueBy === 'selection') {
|
||||
return val1.selection === val2.selection
|
||||
} else {
|
||||
return itemEquals(variantSpec[uniqueBy], val1[uniqueBy], val2[uniqueBy])
|
||||
}
|
||||
} else if ('any' in uniqueBy) {
|
||||
for (let subSpec of uniqueBy.any) {
|
||||
if (unionEquals(subSpec, spec, val1, val2)) {
|
||||
return true
|
||||
for (let idx = 0; idx < val1.length; idx++) {
|
||||
if (listItemEquals(spec, val1[idx], val2[idx])) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
} else if ('all' in uniqueBy) {
|
||||
for (let subSpec of uniqueBy.all) {
|
||||
if (!unionEquals(subSpec, spec, val1, val2)) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
function uniqueByMessageWrapper(
|
||||
@@ -573,7 +496,7 @@ function uniqueByMessage(
|
||||
: '(' + ret + ')'
|
||||
}
|
||||
|
||||
function isObject(
|
||||
function isListObject(
|
||||
spec: IST.ListValueSpecOf<any>,
|
||||
): spec is IST.ListValueSpecObject {
|
||||
// only lists of objects have uniqueBy
|
||||
|
||||
@@ -48,7 +48,6 @@ export class MarketplaceService {
|
||||
this.registryUrl$.pipe(
|
||||
switchMap(url => this.fetchRegistry$(url)),
|
||||
filter(Boolean),
|
||||
// @TODO is updateStoreName needed?
|
||||
map(registry => {
|
||||
registry.info.categories = {
|
||||
all: {
|
||||
@@ -217,7 +216,7 @@ export class MarketplaceService {
|
||||
map(packages => {
|
||||
return Object.entries(packages).flatMap(([id, pkgInfo]) =>
|
||||
Object.keys(pkgInfo.best).map(version =>
|
||||
this.convertToMarketplacePkg(
|
||||
this.convertRegistryPkgToMarketplacePkg(
|
||||
id,
|
||||
version,
|
||||
this.exver.getFlavor(version),
|
||||
@@ -239,12 +238,12 @@ export class MarketplaceService {
|
||||
this.api.getRegistryPackage(url, id, version ? `=${version}` : null),
|
||||
).pipe(
|
||||
map(pkgInfo =>
|
||||
this.convertToMarketplacePkg(id, version, flavor, pkgInfo),
|
||||
this.convertRegistryPkgToMarketplacePkg(id, version, flavor, pkgInfo),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private convertToMarketplacePkg(
|
||||
private convertRegistryPkgToMarketplacePkg(
|
||||
id: string,
|
||||
version: string | null | undefined,
|
||||
flavor: string | null,
|
||||
|
||||
@@ -40,7 +40,7 @@ hr {
|
||||
top left,
|
||||
top right;
|
||||
|
||||
// TODO: Theme
|
||||
// @TODO Theme
|
||||
background-color: color-mix(
|
||||
in hsl,
|
||||
var(--tui-background-base) 90%,
|
||||
|
||||
@@ -22,7 +22,6 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"paths": {
|
||||
/* These paths are relative to each app base folder */
|
||||
/* @TODO Alex the marketplace path is different on 0351. verify */
|
||||
"@start9labs/marketplace": ["../marketplace/index"],
|
||||
"@start9labs/shared": ["../shared/src/public-api"],
|
||||
"path": ["../../node_modules/path-browserify"]
|
||||
|
||||
Reference in New Issue
Block a user