frontend fixes for alpha.2 (#2919)

* rmeove icon from toggles

* fix: comments

* fix: more comments

* always show public domains even if interface private, only show delete on domains

* fix: even more comments

* fix: last comments

* feat: empty state for dashboard

* rework welcome, dlete update-toast, minor

* translation improvements

---------

Co-authored-by: waterplea <alexander@inkin.ru>
This commit is contained in:
Matt Hill
2025-05-09 10:29:17 -06:00
committed by GitHub
parent a3252f9671
commit 8c977c51ca
45 changed files with 585 additions and 497 deletions

214
web/package-lock.json generated
View File

@@ -25,18 +25,18 @@
"@noble/hashes": "^1.4.0", "@noble/hashes": "^1.4.0",
"@start9labs/argon2": "^0.2.2", "@start9labs/argon2": "^0.2.2",
"@start9labs/start-sdk": "file:../sdk/baseDist", "@start9labs/start-sdk": "file:../sdk/baseDist",
"@taiga-ui/addon-charts": "4.32.0", "@taiga-ui/addon-charts": "4.36.0",
"@taiga-ui/addon-commerce": "4.32.0", "@taiga-ui/addon-commerce": "4.36.0",
"@taiga-ui/addon-mobile": "4.32.0", "@taiga-ui/addon-mobile": "4.36.0",
"@taiga-ui/addon-table": "4.32.0", "@taiga-ui/addon-table": "4.36.0",
"@taiga-ui/cdk": "4.32.0", "@taiga-ui/cdk": "4.36.0",
"@taiga-ui/core": "4.32.0", "@taiga-ui/core": "4.36.0",
"@taiga-ui/event-plugins": "4.5.1", "@taiga-ui/event-plugins": "4.5.1",
"@taiga-ui/experimental": "4.32.0", "@taiga-ui/experimental": "4.36.0",
"@taiga-ui/icons": "4.32.0", "@taiga-ui/icons": "4.36.0",
"@taiga-ui/kit": "4.32.0", "@taiga-ui/kit": "4.36.0",
"@taiga-ui/layout": "4.32.0", "@taiga-ui/layout": "4.36.0",
"@taiga-ui/legacy": "4.32.0", "@taiga-ui/legacy": "4.36.0",
"@taiga-ui/polymorpheus": "4.9.0", "@taiga-ui/polymorpheus": "4.9.0",
"@tinkoff/ng-dompurify": "4.0.0", "@tinkoff/ng-dompurify": "4.0.0",
"ansi-to-html": "^0.7.2", "ansi-to-html": "^0.7.2",
@@ -3422,9 +3422,9 @@
} }
}, },
"node_modules/@maskito/angular": { "node_modules/@maskito/angular": {
"version": "3.5.0", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/@maskito/angular/-/angular-3.5.0.tgz", "resolved": "https://registry.npmjs.org/@maskito/angular/-/angular-3.7.2.tgz",
"integrity": "sha512-5uwar32qsGdZNHUgZpFnICg9tJKCXbZEGk2ZnchHzDIfN5ojNT7wKzoq8NhpRlGb3p4qQCE+PXb5GERkcWM/Sw==", "integrity": "sha512-0auXz5dsS0pNuSZ3WFsQK3Fd6nucC9Um1WVlxoVbi0VEZZG68WirVQybwo7J5/J5gCDffqJn2bAw0qrbbrt0mQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -3433,35 +3433,35 @@
"peerDependencies": { "peerDependencies": {
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@angular/forms": ">=16.0.0", "@angular/forms": ">=16.0.0",
"@maskito/core": "^3.5.0" "@maskito/core": "^3.7.2"
} }
}, },
"node_modules/@maskito/core": { "node_modules/@maskito/core": {
"version": "3.5.0", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/@maskito/core/-/core-3.5.0.tgz", "resolved": "https://registry.npmjs.org/@maskito/core/-/core-3.7.2.tgz",
"integrity": "sha512-zgmBjXeXc7BSBaw8jQw25dnwkFmKDvdj5rHzhEIxYhgGtnpli236F0YWPIOYzIwADjbefwDq1o7qpJfMsdDO4Q==", "integrity": "sha512-jnX1u2HAZFy0K8Ll6AoeMpe502aVzrqgPKG7rk/qtbivW+71U3vutP3kqmjZtgsPoNzQ2wzKVWMgi7JEXDK11g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true "peer": true
}, },
"node_modules/@maskito/kit": { "node_modules/@maskito/kit": {
"version": "3.5.0", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-3.5.0.tgz", "resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-3.7.2.tgz",
"integrity": "sha512-QnpZsPTINgK4ScA4pMMJagoj+ufIXc/VGOP61AsQa/H/lmXII4pEZTLzpmMNUYmCEIEyjHR2DIbfEed04sktvQ==", "integrity": "sha512-2gx/7k0iRcKWq7+2yeUEUuv13+4KibxkQrTdGSwZZVNbPsmj4b7r5KTbjfHl2ZdwuASSxJiVV8DWF91ON+cmBA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"peerDependencies": { "peerDependencies": {
"@maskito/core": "^3.5.0" "@maskito/core": "^3.7.2"
} }
}, },
"node_modules/@maskito/phone": { "node_modules/@maskito/phone": {
"version": "3.5.0", "version": "3.7.2",
"resolved": "https://registry.npmjs.org/@maskito/phone/-/phone-3.5.0.tgz", "resolved": "https://registry.npmjs.org/@maskito/phone/-/phone-3.7.2.tgz",
"integrity": "sha512-qh/GGRFn8cZBY/JUTLa5yeSSKSVlekggKeiCbf0eX0I53/HM2pNZ/5667S8SXwn5WjIEeB79Eltl8MNvK74yvA==", "integrity": "sha512-27KfVE9fR+NiwB35z03gPZ/UM+wJR5AfRYiMh37JZCXBz+/WRhtH3mntpzxmp0CCGoFTD/c3hBkCR9p5MAaxiQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"peerDependencies": { "peerDependencies": {
"@maskito/core": "^3.5.0", "@maskito/core": "^3.7.2",
"@maskito/kit": "^3.5.0", "@maskito/kit": "^3.7.2",
"libphonenumber-js": ">=1.0.0" "libphonenumber-js": ">=1.0.0"
} }
}, },
@@ -4417,9 +4417,9 @@
"link": true "link": true
}, },
"node_modules/@taiga-ui/addon-charts": { "node_modules/@taiga-ui/addon-charts": {
"version": "4.32.0", "version": "4.36.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.36.0.tgz",
"integrity": "sha512-VhGkBxwfra5eijSvZdXhMKOWEnFMESo5TX3OfsahIXWJXivwguvIc63rIhHYq2uC+t5sj1kINveO4yLqOeAm/Q==", "integrity": "sha512-0lGbEPFVQfc8ntWUs+kmi1MyhFFZefIxdJmgbD1cB6Irb8T/JXxNizsJR4JXUJt3ozcE9or7Avbux2lkZaPjWw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4428,15 +4428,15 @@
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@ng-web-apis/common": "^4.12.0", "@ng-web-apis/common": "^4.12.0",
"@taiga-ui/cdk": "^4.32.0", "@taiga-ui/cdk": "^4.36.0",
"@taiga-ui/core": "^4.32.0", "@taiga-ui/core": "^4.36.0",
"@taiga-ui/polymorpheus": "^4.9.0" "@taiga-ui/polymorpheus": "^4.9.0"
} }
}, },
"node_modules/@taiga-ui/addon-commerce": { "node_modules/@taiga-ui/addon-commerce": {
"version": "4.32.0", "version": "4.36.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.36.0.tgz",
"integrity": "sha512-AC3VU/RVTNapS8ltSAemZPeDb2CopJEj298rI3Vl4qER1oVl0zunmWVy5ncwK1F1zWKU2/QNDjjo8yKYWeU/Nw==", "integrity": "sha512-DiUBCdvsk+3aNAHXPpGZI9KNuS0hc+T/XSz0RUPKlFWqQ1TUV4aqTE9o575EU7q+IyAn1yNXq40izmuvx/BLwg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4445,22 +4445,22 @@
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@angular/forms": ">=16.0.0", "@angular/forms": ">=16.0.0",
"@maskito/angular": "^3.5.0", "@maskito/angular": "^3.7.2",
"@maskito/core": "^3.5.0", "@maskito/core": "^3.7.2",
"@maskito/kit": "^3.5.0", "@maskito/kit": "^3.7.2",
"@ng-web-apis/common": "^4.12.0", "@ng-web-apis/common": "^4.12.0",
"@taiga-ui/cdk": "^4.32.0", "@taiga-ui/cdk": "^4.36.0",
"@taiga-ui/core": "^4.32.0", "@taiga-ui/core": "^4.36.0",
"@taiga-ui/i18n": "^4.32.0", "@taiga-ui/i18n": "^4.36.0",
"@taiga-ui/kit": "^4.32.0", "@taiga-ui/kit": "^4.36.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/addon-mobile": { "node_modules/@taiga-ui/addon-mobile": {
"version": "4.32.0", "version": "4.36.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.36.0.tgz",
"integrity": "sha512-pUoHWyILPj6KIAhna1JDzz48c2nCjqYb1tb7AL3LQ3qfNwAbg9fvjBIfrgWMhW0LaDeh5+FfrS7oiO/ERcHTLg==", "integrity": "sha512-7JvUkBkTlcBVqtwIu93WPJGs50w6UFeuG354XatzGzyAMGL7ipDkrWoxqxHl5HbKt1n/70V8wfAaBT3mgKFvBg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4470,18 +4470,18 @@
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@ng-web-apis/common": "^4.12.0", "@ng-web-apis/common": "^4.12.0",
"@taiga-ui/cdk": "^4.32.0", "@taiga-ui/cdk": "^4.36.0",
"@taiga-ui/core": "^4.32.0", "@taiga-ui/core": "^4.36.0",
"@taiga-ui/kit": "^4.32.0", "@taiga-ui/kit": "^4.36.0",
"@taiga-ui/layout": "^4.32.0", "@taiga-ui/layout": "^4.36.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/addon-table": { "node_modules/@taiga-ui/addon-table": {
"version": "4.32.0", "version": "4.36.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.36.0.tgz",
"integrity": "sha512-8oXeqLO1wGH8RYHTYWhjCvrKWptPN1we04NRahmFY4AxSJ3u7MqaR4420RRNO4zZG9kGyktLXPjqGocMoymL8Q==", "integrity": "sha512-VKztLMvDo3YeEvEjLHO1DZ1y7CK9Vj+jwLyDKoDpJWBGOJfzqZfYb/lqu228NgKL+3JmGAqvq0VQnN5gx1MtSA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4490,18 +4490,18 @@
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@ng-web-apis/intersection-observer": "^4.12.0", "@ng-web-apis/intersection-observer": "^4.12.0",
"@taiga-ui/cdk": "^4.32.0", "@taiga-ui/cdk": "^4.36.0",
"@taiga-ui/core": "^4.32.0", "@taiga-ui/core": "^4.36.0",
"@taiga-ui/i18n": "^4.32.0", "@taiga-ui/i18n": "^4.36.0",
"@taiga-ui/kit": "^4.32.0", "@taiga-ui/kit": "^4.36.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/cdk": { "node_modules/@taiga-ui/cdk": {
"version": "4.32.0", "version": "4.36.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.36.0.tgz",
"integrity": "sha512-qvYe79uh6Tw2LJSEGLJYUlAidbZi6JgcuMRqWAB1JhyIGpgnaqar5v+oJJg28zJZZ81PCj59VkFNLL0dNVXRUg==", "integrity": "sha512-JgnP7DDZsv87tVK6w9VE+mNTR3nomMgbVq1XoFh5JXbxUwC5FlEIMRKULygBW2FwjbfDehA+2Tm+OeB1HmCSZQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": "2.8.1" "tslib": "2.8.1"
@@ -4511,7 +4511,7 @@
"@angular-devkit/schematics": ">=16.0.0", "@angular-devkit/schematics": ">=16.0.0",
"@schematics/angular": ">=16.0.0", "@schematics/angular": ">=16.0.0",
"ng-morph": "^4.8.4", "ng-morph": "^4.8.4",
"parse5": ">=7.2.1" "parse5": ">=7.3.0"
}, },
"peerDependencies": { "peerDependencies": {
"@angular/animations": ">=16.0.0", "@angular/animations": ">=16.0.0",
@@ -4530,9 +4530,9 @@
} }
}, },
"node_modules/@taiga-ui/core": { "node_modules/@taiga-ui/core": {
"version": "4.32.0", "version": "4.36.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.36.0.tgz",
"integrity": "sha512-e1z7YhhjePMRLTk+s83OclN45wMixCwZWMxM9WuXIyd2KXMPhJvrrgBDjoK66GuFtjZ4qaSF/H2FTIJmJ/6MiQ==", "integrity": "sha512-BwSF/ZA0pvqqbW2UYbtwjvaSfb7uIj1gNbaMTF7uhjQkvwL22x/Opbxcs2DAoja4I/EPpT3AJMr++S1bzUriWw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4546,9 +4546,9 @@
"@angular/router": ">=16.0.0", "@angular/router": ">=16.0.0",
"@ng-web-apis/common": "^4.12.0", "@ng-web-apis/common": "^4.12.0",
"@ng-web-apis/mutation-observer": "^4.12.0", "@ng-web-apis/mutation-observer": "^4.12.0",
"@taiga-ui/cdk": "^4.32.0", "@taiga-ui/cdk": "^4.36.0",
"@taiga-ui/event-plugins": "^4.5.1", "@taiga-ui/event-plugins": "^4.5.1",
"@taiga-ui/i18n": "^4.32.0", "@taiga-ui/i18n": "^4.36.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
@@ -4568,9 +4568,9 @@
} }
}, },
"node_modules/@taiga-ui/experimental": { "node_modules/@taiga-ui/experimental": {
"version": "4.32.0", "version": "4.36.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-4.36.0.tgz",
"integrity": "sha512-sCOasTF9UlgPOW4vXSeM5M1tgGrjgofa+Qq7hejYW3BXs/4mnmdm5yiYzWfMVZd4jgTSeV5kobkIJ9Fkp2zt6g==", "integrity": "sha512-g1TN+hcr7zF/BLw9Sj9X/uKDx8pSEWU9282og5itxsPHgans+ENuh/0dS6kShrx+zwoWLOnjI0MyIR6IktqmQg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4578,18 +4578,18 @@
"peerDependencies": { "peerDependencies": {
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@taiga-ui/addon-commerce": "^4.32.0", "@taiga-ui/addon-commerce": "^4.36.0",
"@taiga-ui/cdk": "^4.32.0", "@taiga-ui/cdk": "^4.36.0",
"@taiga-ui/core": "^4.32.0", "@taiga-ui/core": "^4.36.0",
"@taiga-ui/kit": "^4.32.0", "@taiga-ui/kit": "^4.36.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/i18n": { "node_modules/@taiga-ui/i18n": {
"version": "4.32.0", "version": "4.36.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.36.0.tgz",
"integrity": "sha512-PAQv9RxSgvf3RBUps9bXX2erCk9oiSt9ApM3SAIa/OuzET0TJsW6yZ4EQrtLw03bMX3wyA8PnEYva9wzoYAqxA==", "integrity": "sha512-jGc+5ytZKA4zEDyt1uBxUq/4tp4hJmppKdl6GZ+AWlLl6Tx2YiYqcA3RLDaPdiXsS/UeEoJ7Vjyn6PVd/fv35A==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -4602,18 +4602,18 @@
} }
}, },
"node_modules/@taiga-ui/icons": { "node_modules/@taiga-ui/icons": {
"version": "4.32.0", "version": "4.36.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.36.0.tgz",
"integrity": "sha512-X2ZSiqeMKigULgX91fBZkFJRUbwzeW934yLEGhq7C1JMcC2+ppLmL/NbkD2kpKZ4OeHnGsItxKauoXu44rXeLA==", "integrity": "sha512-mcGvUnwF7g1wcnXFJenrcfmlzE1+k2Haz2Cudws0htZQ5VnR/kC1f1pEJlhZYycqGgoJJrNEBPbHCBGrIkqfXg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
} }
}, },
"node_modules/@taiga-ui/kit": { "node_modules/@taiga-ui/kit": {
"version": "4.32.0", "version": "4.36.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.36.0.tgz",
"integrity": "sha512-J8XoqeQHBNbAAuTz0kVACujDOb3zuh4Vps83lYl+msFIaUmkjC37muXF3eRlImH3m4DpT8yI8+ffh/T3+jky7w==", "integrity": "sha512-jUVMa3kUrv49HvOCzVymMo/+FRTv1Yrflbjjm+V7A4z9CnZR+tg2VZ+wJzjU0qNppp/nstu+ge+XTJ5B5Hsatw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4623,25 +4623,25 @@
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@angular/forms": ">=16.0.0", "@angular/forms": ">=16.0.0",
"@angular/router": ">=16.0.0", "@angular/router": ">=16.0.0",
"@maskito/angular": "^3.5.0", "@maskito/angular": "^3.7.2",
"@maskito/core": "^3.5.0", "@maskito/core": "^3.7.2",
"@maskito/kit": "^3.5.0", "@maskito/kit": "^3.7.2",
"@maskito/phone": "^3.5.0", "@maskito/phone": "^3.7.2",
"@ng-web-apis/common": "^4.12.0", "@ng-web-apis/common": "^4.12.0",
"@ng-web-apis/intersection-observer": "^4.12.0", "@ng-web-apis/intersection-observer": "^4.12.0",
"@ng-web-apis/mutation-observer": "^4.12.0", "@ng-web-apis/mutation-observer": "^4.12.0",
"@ng-web-apis/resize-observer": "^4.12.0", "@ng-web-apis/resize-observer": "^4.12.0",
"@taiga-ui/cdk": "^4.32.0", "@taiga-ui/cdk": "^4.36.0",
"@taiga-ui/core": "^4.32.0", "@taiga-ui/core": "^4.36.0",
"@taiga-ui/i18n": "^4.32.0", "@taiga-ui/i18n": "^4.36.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/layout": { "node_modules/@taiga-ui/layout": {
"version": "4.32.0", "version": "4.36.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.36.0.tgz",
"integrity": "sha512-ECaoJ3CbL+eoqL1MleaHvD9/FQ5OCaUMkjOdXId2Jg2MNbuDhtS9hqVZvSWLXRWz3XgC3aADYnPwrNvIsy5Mng==", "integrity": "sha512-Y4s2mdYJopuVjTd38HmqaACK9RwoJAirC7PCYg8NVCWb2Q4mgqHzHtIwTcERRfxRANoSTt+Ne9rLIlxY5JOuVQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4649,17 +4649,17 @@
"peerDependencies": { "peerDependencies": {
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@taiga-ui/cdk": "^4.32.0", "@taiga-ui/cdk": "^4.36.0",
"@taiga-ui/core": "^4.32.0", "@taiga-ui/core": "^4.36.0",
"@taiga-ui/kit": "^4.32.0", "@taiga-ui/kit": "^4.36.0",
"@taiga-ui/polymorpheus": "^4.9.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/legacy": { "node_modules/@taiga-ui/legacy": {
"version": "4.32.0", "version": "4.36.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/legacy/-/legacy-4.32.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/legacy/-/legacy-4.36.0.tgz",
"integrity": "sha512-wEsywt6hK2NNpHddqVrL0MTd1QFzmhMdPPgtraNOieQmzrSW2jpA37KJO11cVleuRdDsk98rFtzQ3stlNNFy5Q==", "integrity": "sha512-x4F5ZrAhJPhlCda8daLns7tADkjLDzSNmZa8xVMHeL8nuMjx7FbpKxqzVKcKJ/SLWqF5WHs30D6S/mXjl5YvYw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -9526,9 +9526,9 @@
} }
}, },
"node_modules/libphonenumber-js": { "node_modules/libphonenumber-js": {
"version": "1.12.6", "version": "1.12.7",
"resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.6.tgz", "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.7.tgz",
"integrity": "sha512-PJiS4ETaUfCOFLpmtKzAbqZQjCCKVu2OhTV4SVNNE7c2nu/dACvtCqj4L0i/KWNnIgRv7yrILvBj5Lonv5Ncxw==", "integrity": "sha512-0nYZSNj/QEikyhcM5RZFXGlCB/mr4PVamnT1C2sKBnDDTYndrvbybYjvg+PMqAndQHlLbwQ3socolnL3WWTUFA==",
"license": "MIT", "license": "MIT",
"peer": true "peer": true
}, },
@@ -11665,12 +11665,12 @@
} }
}, },
"node_modules/parse5": { "node_modules/parse5": {
"version": "7.2.1", "version": "7.3.0",
"resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
"integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"entities": "^4.5.0" "entities": "^6.0.0"
}, },
"funding": { "funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1" "url": "https://github.com/inikulin/parse5?sponsor=1"
@@ -11715,9 +11715,9 @@
} }
}, },
"node_modules/parse5/node_modules/entities": { "node_modules/parse5/node_modules/entities": {
"version": "4.5.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz",
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==",
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"engines": { "engines": {
"node": ">=0.12" "node": ">=0.12"

View File

@@ -47,18 +47,18 @@
"@noble/hashes": "^1.4.0", "@noble/hashes": "^1.4.0",
"@start9labs/argon2": "^0.2.2", "@start9labs/argon2": "^0.2.2",
"@start9labs/start-sdk": "file:../sdk/baseDist", "@start9labs/start-sdk": "file:../sdk/baseDist",
"@taiga-ui/addon-charts": "4.32.0", "@taiga-ui/addon-charts": "4.36.0",
"@taiga-ui/addon-commerce": "4.32.0", "@taiga-ui/addon-commerce": "4.36.0",
"@taiga-ui/addon-mobile": "4.32.0", "@taiga-ui/addon-mobile": "4.36.0",
"@taiga-ui/addon-table": "4.32.0", "@taiga-ui/addon-table": "4.36.0",
"@taiga-ui/cdk": "4.32.0", "@taiga-ui/cdk": "4.36.0",
"@taiga-ui/core": "4.32.0", "@taiga-ui/core": "4.36.0",
"@taiga-ui/event-plugins": "4.5.1", "@taiga-ui/event-plugins": "4.5.1",
"@taiga-ui/experimental": "4.32.0", "@taiga-ui/experimental": "4.36.0",
"@taiga-ui/icons": "4.32.0", "@taiga-ui/icons": "4.36.0",
"@taiga-ui/kit": "4.32.0", "@taiga-ui/kit": "4.36.0",
"@taiga-ui/layout": "4.32.0", "@taiga-ui/layout": "4.36.0",
"@taiga-ui/legacy": "4.32.0", "@taiga-ui/legacy": "4.36.0",
"@taiga-ui/polymorpheus": "4.9.0", "@taiga-ui/polymorpheus": "4.9.0",
"@tinkoff/ng-dompurify": "4.0.0", "@tinkoff/ng-dompurify": "4.0.0",
"ansi-to-html": "^0.7.2", "ansi-to-html": "^0.7.2",

View File

@@ -441,7 +441,7 @@ export default {
438: 'Kein Internet', 438: 'Kein Internet',
439: 'Verbindung wird hergestellt', 439: 'Verbindung wird hergestellt',
440: 'Fährt herunter', 440: 'Fährt herunter',
441: 'Versionen', 441: 'Alle versionen',
442: 'Neue Benachrichtigungen', 442: 'Neue Benachrichtigungen',
443: 'Anzeigen', 443: 'Anzeigen',
444: 'PWA wird neu geladen', 444: 'PWA wird neu geladen',
@@ -498,4 +498,9 @@ export default {
495: 'Validierung', 495: 'Validierung',
496: 'in Bearbeitung', 496: 'in Bearbeitung',
497: 'abgeschlossen', 497: 'abgeschlossen',
498: 'Klicken Sie hier, um alle Versionen anzuzeigen',
499: 'Um loszulegen, besuche den Marktplatz und lade deinen ersten Dienst herunter',
500: 'Marktplatz anzeigen',
501: 'Willkommen bei',
502: 'souveränes computing',
} satisfies i18n } satisfies i18n

View File

@@ -282,7 +282,7 @@ export const ENGLISH = {
'Donation Link': 280, 'Donation Link': 280,
'Standard Actions': 281, 'Standard Actions': 281,
'Rebuild Service': 282, // as in, rebuild a software container 'Rebuild Service': 282, // as in, rebuild a software container
'Rebuilds the service container. Only necessary in there is a bug in StartOS': 283, 'Rebuilds the service container. Only necessary if there is a bug in StartOS': 283,
'Uninstall': 284, 'Uninstall': 284,
'Uninstalls this service from StartOS and delete all data permanently.': 285, 'Uninstalls this service from StartOS and delete all data permanently.': 285,
'Dashboard': 286, 'Dashboard': 286,
@@ -440,7 +440,7 @@ export const ENGLISH = {
'No Internet': 438, 'No Internet': 438,
'Connecting': 439, 'Connecting': 439,
'Shutting down': 440, 'Shutting down': 440,
'Versions': 441, 'All versions': 441,
'New notifications': 442, 'New notifications': 442,
'View': 443, 'View': 443,
'Reloading PWA': 444, 'Reloading PWA': 444,
@@ -497,4 +497,9 @@ export const ENGLISH = {
'Validating': 495, 'Validating': 495,
'in progress': 496, 'in progress': 496,
'complete': 497, 'complete': 497,
'Click to view all versions': 498,
'To get started, visit the Marketplace and download your first service': 499,
'View Marketplace': 500,
'Welcome to': 501,
'sovereign computing': 502,
} as const } as const

View File

@@ -441,7 +441,7 @@ export default {
438: 'Sin Internet', 438: 'Sin Internet',
439: 'Conectando', 439: 'Conectando',
440: 'Apagando', 440: 'Apagando',
441: 'Versiones', 441: 'Todas las versiones',
442: 'Nuevas notificaciones', 442: 'Nuevas notificaciones',
443: 'Ver', 443: 'Ver',
444: 'Recargando PWA', 444: 'Recargando PWA',
@@ -498,4 +498,9 @@ export default {
495: 'Validando', 495: 'Validando',
496: 'en progreso', 496: 'en progreso',
497: 'completo', 497: 'completo',
498: 'Haga clic para ver todas las versiones',
499: 'Para comenzar, visita el Mercado y descarga tu primer servicio',
500: 'Ver Marketplace',
501: 'Bienvenido a',
502: 'computación soberana',
} satisfies i18n } satisfies i18n

View File

@@ -441,7 +441,7 @@ export default {
438: 'Brak Internetu', 438: 'Brak Internetu',
439: 'Łączenie', 439: 'Łączenie',
440: 'Wyłączanie', 440: 'Wyłączanie',
441: 'Wersje', 441: 'Wszystkie wersje',
442: 'Nowe powiadomienia', 442: 'Nowe powiadomienia',
443: 'Zobacz', 443: 'Zobacz',
444: 'Przeładowywanie PWA', 444: 'Przeładowywanie PWA',
@@ -498,4 +498,9 @@ export default {
495: 'Weryfikowanie', 495: 'Weryfikowanie',
496: 'w toku', 496: 'w toku',
497: 'zakończono', 497: 'zakończono',
498: 'Kliknij, aby zobaczyć wszystkie wersje',
499: 'Aby rozpocząć, odwiedź Marketplace i pobierz swoją pierwszą usługę',
500: 'Zobacz Rynek',
501: 'Witamy w',
502: 'suwerenne przetwarzanie',
} satisfies i18n } satisfies i18n

View File

@@ -11,9 +11,11 @@ import { I18N, i18nKey } from './i18n.providers'
export class i18nPipe implements PipeTransform { export class i18nPipe implements PipeTransform {
private readonly i18n = inject(I18N) private readonly i18n = inject(I18N)
transform(englishKey: i18nKey | null | undefined): string | undefined { // @TODO uncomment to make sure translations are present
transform(englishKey: string | null | undefined): string | undefined {
// transform(englishKey: i18nKey | null | undefined): string | undefined {
return englishKey return englishKey
? this.i18n()?.[ENGLISH[englishKey]] || englishKey ? this.i18n()?.[ENGLISH[englishKey as i18nKey]] || englishKey
: undefined : undefined
} }
} }

View File

@@ -3,7 +3,6 @@ import {
TuiResponsiveDialogOptions, TuiResponsiveDialogOptions,
TuiResponsiveDialogService, TuiResponsiveDialogService,
} from '@taiga-ui/addon-mobile' } from '@taiga-ui/addon-mobile'
import { TuiAlertOptions } from '@taiga-ui/core'
import { TUI_CONFIRM, TuiConfirmData } from '@taiga-ui/kit' import { TUI_CONFIRM, TuiConfirmData } from '@taiga-ui/kit'
import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus'
import { PROMPT, PromptOptions } from '../components/prompt.component' import { PROMPT, PromptOptions } from '../components/prompt.component'
@@ -63,7 +62,7 @@ export class DialogService {
openAlert<T = void>( openAlert<T = void>(
message: i18nKey | undefined, message: i18nKey | undefined,
options: Partial<TuiAlertOptions<any>> & { options: Partial<TuiResponsiveDialogOptions<any>> & {
label?: i18nKey label?: i18nKey
} = {}, } = {},
) { ) {

View File

@@ -1,6 +1,5 @@
import { Component, inject } from '@angular/core' import { Component, inject } from '@angular/core'
import { takeUntilDestroyed } from '@angular/core/rxjs-interop' import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
import { Title } from '@angular/platform-browser'
import { i18nService } from '@start9labs/shared' import { i18nService } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { merge } from 'rxjs' import { merge } from 'rxjs'
@@ -29,7 +28,6 @@ import { PatchMonitorService } from './services/patch-monitor.service'
`, `,
}) })
export class AppComponent { export class AppComponent {
private readonly title = inject(Title)
private readonly i18n = inject(i18nService) private readonly i18n = inject(i18nService)
readonly subscription = merge( readonly subscription = merge(
@@ -40,10 +38,9 @@ export class AppComponent {
.subscribe() .subscribe()
readonly ui = inject<PatchDB<DataModel>>(PatchDB) readonly ui = inject<PatchDB<DataModel>>(PatchDB)
.watch$('ui') .watch$('ui', 'language')
.pipe(takeUntilDestroyed()) .pipe(takeUntilDestroyed())
.subscribe(({ name, language }) => { .subscribe(language => {
this.title.setTitle(name || 'StartOS')
this.i18n.setLanguage(language || 'english') this.i18n.setLanguage(language || 'english')
}) })
} }

View File

@@ -16,6 +16,7 @@ import {
import { import {
TUI_DATE_FORMAT, TUI_DATE_FORMAT,
TUI_DIALOGS_CLOSE, TUI_DIALOGS_CLOSE,
tuiAlertOptionsProvider,
tuiButtonOptionsProvider, tuiButtonOptionsProvider,
tuiDropdownOptionsProvider, tuiDropdownOptionsProvider,
tuiNumberFormatProvider, tuiNumberFormatProvider,
@@ -58,6 +59,9 @@ export const APP_PROVIDERS: Provider[] = [
tuiButtonOptionsProvider({ size: 'm' }), tuiButtonOptionsProvider({ size: 'm' }),
tuiTextfieldOptionsProvider({ hintOnDisabled: true }), tuiTextfieldOptionsProvider({ hintOnDisabled: true }),
tuiDropdownOptionsProvider({ appearance: 'start-os' }), tuiDropdownOptionsProvider({ appearance: 'start-os' }),
tuiAlertOptionsProvider({
autoClose: appearance => (appearance === 'negative' ? 0 : 3000),
}),
{ {
provide: TUI_DATE_FORMAT, provide: TUI_DATE_FORMAT,
useValue: of({ useValue: of({

View File

@@ -1,7 +1,6 @@
import { ChangeDetectionStrategy, Component } from '@angular/core' import { ChangeDetectionStrategy, Component } from '@angular/core'
import { NotificationsToastComponent } from './notifications-toast.component' import { NotificationsToastComponent } from './notifications-toast.component'
import { RefreshAlertComponent } from './refresh-alert.component' import { RefreshAlertComponent } from './refresh-alert.component'
import { UpdateToastComponent } from './update-toast.component'
@Component({ @Component({
standalone: true, standalone: true,
@@ -9,13 +8,8 @@ import { UpdateToastComponent } from './update-toast.component'
template: ` template: `
<notifications-toast /> <notifications-toast />
<refresh-alert /> <refresh-alert />
<update-toast />
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [NotificationsToastComponent, RefreshAlertComponent],
NotificationsToastComponent,
UpdateToastComponent,
RefreshAlertComponent,
],
}) })
export class ToastContainerComponent {} export class ToastContainerComponent {}

View File

@@ -1,80 +0,0 @@
import { AsyncPipe } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ErrorService, i18nPipe, LoadingService } from '@start9labs/shared'
import { TuiAlert, TuiButton } from '@taiga-ui/core'
import { PatchDB } from 'patch-db-client'
import {
distinctUntilChanged,
endWith,
filter,
merge,
Observable,
Subject,
} from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
@Component({
standalone: true,
selector: 'update-toast',
template: `
<ng-template
[tuiAlert]="!!(visible$ | async)"
[tuiAlertOptions]="{
label: 'StartOS download complete' | i18n,
appearance: 'positive',
autoClose: 0,
}"
(tuiAlertChange)="onDismiss()"
>
{{
'Restart your server for these updates to take effect. It can take several minutes to come back online.'
| i18n
}}
<div>
<button
tuiButton
appearance="secondary"
size="s"
style="margin-top: 8px"
(click)="restart()"
>
{{ 'Restart' | i18n }}
</button>
</div>
</ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiButton, TuiAlert, AsyncPipe, i18nPipe],
})
export class UpdateToastComponent {
private readonly api = inject(ApiService)
private readonly errorService = inject(ErrorService)
private readonly loader = inject(LoadingService)
private readonly dismiss$ = new Subject<boolean>()
readonly visible$: Observable<boolean> = merge(
this.dismiss$,
inject<PatchDB<DataModel>>(PatchDB)
.watch$('serverInfo', 'statusInfo', 'updated')
.pipe(distinctUntilChanged(), filter(Boolean), endWith(false)),
)
onDismiss() {
this.dismiss$.next(false)
}
async restart(): Promise<void> {
this.onDismiss()
const loader = this.loader.open('Restarting').subscribe()
try {
await this.api.restartServer({})
} catch (e: any) {
await this.errorService.handleError(e)
} finally {
await loader.unsubscribe()
}
}
}

View File

@@ -9,11 +9,11 @@ import {
import { takeUntilDestroyed } from '@angular/core/rxjs-interop' import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
import { AbstractTuiNullableControl } from '@taiga-ui/legacy' import { AbstractTuiNullableControl } from '@taiga-ui/legacy'
import { filter } from 'rxjs' import { filter } from 'rxjs'
import { TuiDialogContext } from '@taiga-ui/core' import { TuiAlertService, TuiDialogContext } from '@taiga-ui/core'
import { IST } from '@start9labs/start-sdk' import { IST } from '@start9labs/start-sdk'
import { ERRORS } from '../form-group/form-group.component' import { ERRORS } from '../form-group/form-group.component'
import { FORM_CONTROL_PROVIDERS } from './form-control.providers' import { FORM_CONTROL_PROVIDERS } from './form-control.providers'
import { DialogService, i18nKey } from '@start9labs/shared' import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared'
@Component({ @Component({
selector: 'form-control', selector: 'form-control',
@@ -26,6 +26,9 @@ export class FormControlComponent<
T extends Exclude<IST.ValueSpec, IST.ValueSpecHidden>, T extends Exclude<IST.ValueSpec, IST.ValueSpecHidden>,
V, V,
> extends AbstractTuiNullableControl<V> { > extends AbstractTuiNullableControl<V> {
private readonly alerts = inject(TuiAlertService)
private readonly i18n = inject(i18nPipe)
@Input({ required: true }) @Input({ required: true })
spec!: T spec!: T
@@ -35,7 +38,6 @@ export class FormControlComponent<
warned = false warned = false
focused = false focused = false
readonly order = ERRORS readonly order = ERRORS
private readonly dialog = inject(DialogService)
get immutable(): boolean { get immutable(): boolean {
return 'immutable' in this.spec && this.spec.immutable return 'immutable' in this.spec && this.spec.immutable
@@ -50,9 +52,9 @@ export class FormControlComponent<
const previous = this.value const previous = this.value
if (!this.warned && this.warning) { if (!this.warned && this.warning) {
this.dialog this.alerts
.openAlert<boolean>(this.warning as unknown as i18nKey, { .open<boolean>(this.warning, {
label: 'Warning', label: this.i18n.transform('Warning'),
appearance: 'warning', appearance: 'warning',
closeable: false, closeable: false,
autoClose: 0, autoClose: 0,

View File

@@ -7,6 +7,7 @@
type="checkbox" type="checkbox"
size="m" size="m"
[disabled]="!!spec.disabled || readOnly" [disabled]="!!spec.disabled || readOnly"
[showIcons]="false"
[(ngModel)]="value" [(ngModel)]="value"
(blur)="onFocus(false)" (blur)="onFocus(false)"
/> />

View File

@@ -26,7 +26,7 @@ import { getMenu } from 'src/app/utils/system-utilities'
[tuiHintShowDelay]="1000" [tuiHintShowDelay]="1000"
[routerLink]="item.routerLink" [routerLink]="item.routerLink"
[class.link_system]="item.routerLink === '/portal/system'" [class.link_system]="item.routerLink === '/portal/system'"
[tuiHint]="!rla.isActive ? item.name : ''" [tuiHint]="rla.isActive ? '' : (item.name | i18n)"
> >
<tui-badged-content <tui-badged-content
[style.--tui-radius.%]="50" [style.--tui-radius.%]="50"

View File

@@ -4,12 +4,8 @@ import {
inject, inject,
input, input,
} from '@angular/core' } from '@angular/core'
import { import { CopyService, DialogService, i18nPipe } from '@start9labs/shared'
CopyService, import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
DialogService,
i18nKey,
i18nPipe,
} from '@start9labs/shared'
import { import {
TuiButton, TuiButton,
tuiButtonOptionsProvider, tuiButtonOptionsProvider,
@@ -114,6 +110,7 @@ import { InterfaceComponent } from './interface.component'
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class InterfaceActionsComponent { export class InterfaceActionsComponent {
readonly isMobile = inject(TUI_IS_MOBILE)
readonly dialog = inject(DialogService) readonly dialog = inject(DialogService)
readonly copyService = inject(CopyService) readonly copyService = inject(CopyService)
readonly interface = inject(InterfaceComponent) readonly interface = inject(InterfaceComponent)
@@ -124,7 +121,7 @@ export class InterfaceActionsComponent {
this.dialog this.dialog
.openComponent(new PolymorpheusComponent(QRModal), { .openComponent(new PolymorpheusComponent(QRModal), {
size: 'auto', size: 'auto',
label: this.actions() as i18nKey, closeable: this.isMobile,
data: this.actions(), data: this.actions(),
}) })
.subscribe() .subscribe()

View File

@@ -37,7 +37,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
import { toAcmeName } from 'src/app/utils/acme' import { toAcmeName } from 'src/app/utils/acme'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { InterfaceActionsComponent } from './actions.component' import { InterfaceActionsComponent } from './actions.component'
import { AddressDetails } from './interface.utils' import { ClearnetAddress } from './interface.utils'
import { MaskPipe } from './mask.pipe' import { MaskPipe } from './mask.pipe'
type ClearnetForm = { type ClearnetForm = {
@@ -85,25 +85,35 @@ type ClearnetForm = {
<table [appTable]="['ACME', 'URL', null]"> <table [appTable]="['ACME', 'URL', null]">
@for (address of clearnet(); track $index) { @for (address of clearnet(); track $index) {
<tr> <tr>
<td [style.width.rem]="12">{{ address.acme | acme }}</td> <td [style.width.rem]="12">
{{
interface.serviceInterface().addSsl
? (address.acme | acme)
: '-'
}}
</td>
<td>{{ address.url | mask }}</td> <td>{{ address.url | mask }}</td>
<td [actions]="address.url"> <td [actions]="address.url">
<button @if (address.isDomain) {
tuiButton <button
appearance="primary-destructive" tuiButton
[style.margin-inline-end.rem]="0.5" appearance="primary-destructive"
(click)="remove(address)" [style.margin-inline-end.rem]="0.5"
> (click)="remove(address)"
{{ 'Delete' | i18n }} >
</button> {{ 'Delete' | i18n }}
<button </button>
tuiOption }
tuiAppearance="action-destructive" @if (address.isDomain) {
iconStart="@tui.trash" <button
(click)="remove(address)" tuiOption
> tuiAppearance="action-destructive"
{{ 'Delete' | i18n }} iconStart="@tui.trash"
</button> (click)="remove(address)"
>
{{ 'Delete' | i18n }}
</button>
}
</td> </td>
</tr> </tr>
} }
@@ -144,7 +154,7 @@ export class InterfaceClearnetComponent {
readonly interface = inject(InterfaceComponent) readonly interface = inject(InterfaceComponent)
readonly isPublic = computed(() => this.interface.serviceInterface().public) readonly isPublic = computed(() => this.interface.serviceInterface().public)
readonly clearnet = input.required<readonly AddressDetails[]>() readonly clearnet = input.required<readonly ClearnetAddress[]>()
readonly acme = toSignal( readonly acme = toSignal(
inject<PatchDB<DataModel>>(PatchDB) inject<PatchDB<DataModel>>(PatchDB)
.watch$('serverInfo', 'network', 'acme') .watch$('serverInfo', 'network', 'acme')
@@ -152,7 +162,7 @@ export class InterfaceClearnetComponent {
{ initialValue: [] }, { initialValue: [] },
) )
async remove({ url }: AddressDetails) { async remove({ url }: ClearnetAddress) {
const confirm = await firstValueFrom( const confirm = await firstValueFrom(
this.dialog this.dialog
.openConfirm({ label: 'Are you sure?', size: 's' }) .openConfirm({ label: 'Are you sure?', size: 's' })
@@ -213,33 +223,37 @@ export class InterfaceClearnetComponent {
} }
async add() { async add() {
const domain = ISB.Value.text({
name: 'Domain',
description: 'The domain or subdomain you want to use',
placeholder: `e.g. 'mydomain.com' or 'sub.mydomain.com'`,
required: true,
default: null,
patterns: [utils.Patterns.domain],
})
const acme = ISB.Value.select({
name: 'ACME Provider',
description:
'Select which ACME provider to use for obtaining your SSL certificate. Add new ACME providers in the System tab. Optionally use your system Root CA. Note: only devices that have trusted your Root CA will be able to access the domain without security warnings.',
values: this.acme().reduce(
(obj, url) => ({
...obj,
[url]: toAcmeName(url),
}),
{ none: 'None (use system Root CA)' } as Record<string, string>,
),
default: '',
})
this.formDialog.open<FormContext<ClearnetForm>>(FormComponent, { this.formDialog.open<FormContext<ClearnetForm>>(FormComponent, {
label: 'Select Domain', label: 'Select Domain',
data: { data: {
spec: await configBuilderToSpec( spec: await configBuilderToSpec(
ISB.InputSpec.of({ ISB.InputSpec.of(
domain: ISB.Value.text({ this.interface.serviceInterface().addSsl
name: 'Domain', ? { domain, acme }
description: 'The domain or subdomain you want to use', : { domain },
placeholder: `e.g. 'mydomain.com' or 'sub.mydomain.com'`, ),
required: true,
default: null,
patterns: [utils.Patterns.domain],
}),
acme: ISB.Value.select({
name: 'ACME Provider',
description:
'Select which ACME provider to use for obtaining your SSL certificate. Add new ACME providers in the System tab. Optionally use your system Root CA. Note: only devices that have trusted your Root CA will be able to access the domain without security warnings.',
values: this.acme().reduce(
(obj, url) => ({
...obj,
[url]: toAcmeName(url),
}),
{ none: 'None (use system Root CA)' } as Record<string, string>,
),
default: '',
}),
}),
), ),
buttons: [ buttons: [
{ {

View File

@@ -12,9 +12,9 @@ export function getAddresses(
host: T.Host, host: T.Host,
config: ConfigService, config: ConfigService,
): { ): {
clearnet: (AddressDetails & { acme: string | null })[] clearnet: ClearnetAddress[]
local: AddressDetails[] local: LocalAddress[]
tor: AddressDetails[] tor: TorAddress[]
} { } {
const addressInfo = serviceInterface.addressInfo const addressInfo = serviceInterface.addressInfo
const hostnames = const hostnames =
@@ -46,9 +46,9 @@ export function getAddresses(
} }
} }
const clearnet: (AddressDetails & { acme: string | null })[] = [] const clearnet: ClearnetAddress[] = []
const local: AddressDetails[] = [] const local: LocalAddress[] = []
const tor: AddressDetails[] = [] const tor: TorAddress[] = []
hostnames.forEach(h => { hostnames.forEach(h => {
const addresses = utils.addressHostToUrl(addressInfo, h) const addresses = utils.addressHostToUrl(addressInfo, h)
@@ -56,26 +56,28 @@ export function getAddresses(
addresses.forEach(url => { addresses.forEach(url => {
if (h.kind === 'onion') { if (h.kind === 'onion') {
tor.push({ tor.push({
label: protocol: new URL(url).protocol.replace(':', '').toUpperCase(),
addresses.length > 1
? new URL(url).protocol.replace(':', '').toUpperCase()
: '',
url, url,
}) })
} else { } else {
const hostnameKind = h.hostname.kind const hostnameKind = h.hostname.kind
if (h.public) { if (
h.public ||
(hostnameKind === 'domain' && host.domains[h.hostname.domain]?.public)
) {
clearnet.push({ clearnet.push({
url, url,
disabled: !h.public,
isDomain: hostnameKind == 'domain',
acme: acme:
hostnameKind == 'domain' hostnameKind == 'domain'
? host.domains[h.hostname.domain]?.acme || null ? host.domains[h.hostname.domain]?.acme || null
: null, // @TODO Matt make sure this is handled correctly - looks like ACME settings aren't built yet anyway, but ACME settings aren't *available* for public IPs : null,
}) })
} else { } else {
local.push({ local.push({
label: nid:
hostnameKind === 'local' hostnameKind === 'local'
? 'Local' ? 'Local'
: `${h.networkInterfaceId} (${hostnameKind})`, : `${h.networkInterfaceId} (${hostnameKind})`,
@@ -103,16 +105,28 @@ export function getAddresses(
} }
export type MappedServiceInterface = T.ServiceInterface & { export type MappedServiceInterface = T.ServiceInterface & {
addSsl?: T.AddSslOptions | null
public: boolean public: boolean
addresses: { addresses: {
clearnet: AddressDetails[] clearnet: ClearnetAddress[]
local: AddressDetails[] local: LocalAddress[]
tor: AddressDetails[] tor: TorAddress[]
} }
} }
export type AddressDetails = { export type ClearnetAddress = {
label?: string
url: string url: string
acme?: string | null acme: string | null
isDomain: boolean
disabled: boolean
}
export type LocalAddress = {
url: string
nid: string
}
export type TorAddress = {
url: string
protocol: string
} }

View File

@@ -3,7 +3,7 @@ import { TuiIcon, TuiLink } from '@taiga-ui/core'
import { TuiTooltip } from '@taiga-ui/kit' import { TuiTooltip } from '@taiga-ui/kit'
import { TableComponent } from 'src/app/routes/portal/components/table.component' import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { InterfaceActionsComponent } from './actions.component' import { InterfaceActionsComponent } from './actions.component'
import { AddressDetails } from './interface.utils' import { LocalAddress } from './interface.utils'
import { MaskPipe } from './mask.pipe' import { MaskPipe } from './mask.pipe'
import { DocsLinkDirective, i18nPipe } from '@start9labs/shared' import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
@@ -27,7 +27,7 @@ import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
<table [appTable]="['Network Interface', 'URL', null]"> <table [appTable]="['Network Interface', 'URL', null]">
@for (address of local(); track $index) { @for (address of local(); track $index) {
<tr> <tr>
<td [style.width.rem]="12">{{ address.label }}</td> <td [style.width.rem]="12">{{ address.nid }}</td>
<td>{{ address.url | mask }}</td> <td>{{ address.url | mask }}</td>
<td [actions]="address.url"></td> <td [actions]="address.url"></td>
</tr> </tr>
@@ -48,5 +48,5 @@ import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class InterfaceLocalComponent { export class InterfaceLocalComponent {
readonly local = input.required<readonly AddressDetails[]>() readonly local = input.required<readonly LocalAddress[]>()
} }

View File

@@ -32,7 +32,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service' import { FormDialogService } from 'src/app/services/form-dialog.service'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { InterfaceActionsComponent } from './actions.component' import { InterfaceActionsComponent } from './actions.component'
import { AddressDetails } from './interface.utils' import { TorAddress } from './interface.utils'
import { MaskPipe } from './mask.pipe' import { MaskPipe } from './mask.pipe'
type OnionForm = { type OnionForm = {
@@ -70,7 +70,7 @@ type OnionForm = {
<table [appTable]="['Protocol', 'URL', null]"> <table [appTable]="['Protocol', 'URL', null]">
@for (address of tor(); track $index) { @for (address of tor(); track $index) {
<tr> <tr>
<td [style.width.rem]="12">{{ address.label }}</td> <td [style.width.rem]="12">{{ address.protocol || '-' }}</td>
<td> <td>
<div [tuiFluidTypography]="[0.625, 0.8125]" tuiFade> <div [tuiFluidTypography]="[0.625, 0.8125]" tuiFade>
{{ address.url | mask }} {{ address.url | mask }}
@@ -140,9 +140,9 @@ export class InterfaceTorComponent {
private readonly interface = inject(InterfaceComponent) private readonly interface = inject(InterfaceComponent)
private readonly i18n = inject(i18nPipe) private readonly i18n = inject(i18nPipe)
readonly tor = input.required<readonly AddressDetails[]>() readonly tor = input.required<readonly TorAddress[]>()
async remove({ url }: AddressDetails) { async remove({ url }: TorAddress) {
const confirm = await firstValueFrom( const confirm = await firstValueFrom(
this.dialog this.dialog
.openConfirm({ label: 'Are you sure?', size: 's' }) .openConfirm({ label: 'Are you sure?', size: 's' })

View File

@@ -1,22 +1,56 @@
import { CommonModule } from '@angular/common' import {
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' ChangeDetectionStrategy,
Component,
inject,
signal,
} from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { RouterOutlet } from '@angular/router' import { RouterOutlet } from '@angular/router'
import { TuiScrollbar } from '@taiga-ui/core' import { ErrorService, LoadingService } from '@start9labs/shared'
import { TuiButton, TuiIcon, TuiLoader, TuiScrollbar } from '@taiga-ui/core'
import { TuiActionBar, TuiProgress } from '@taiga-ui/kit'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { TabsComponent } from 'src/app/routes/portal/components/tabs.component' import { TabsComponent } from 'src/app/routes/portal/components/tabs.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { OSService } from 'src/app/services/os.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { HeaderComponent } from './components/header/header.component' import { HeaderComponent } from './components/header/header.component'
@Component({ @Component({
standalone: true, standalone: true,
template: ` template: `
<header appHeader>{{ name$ | async }}</header> <header appHeader>{{ name() }}</header>
<main> <main>
<tui-scrollbar [style.max-height.%]="100"> <tui-scrollbar [style.max-height.%]="100">
<router-outlet /> <router-outlet />
</tui-scrollbar> </tui-scrollbar>
</main> </main>
<app-tabs /> <app-tabs />
@if (update(); as update) {
<tui-action-bar *tuiActionBar="true">
@if (update === true) {
<tui-icon icon="@tui.check" class="g-positive" />
Download complete, restart to apply changes
} @else if (
update.overall && update.overall !== true && update.overall.total
) {
<tui-progress-circle
size="xxs"
[style.display]="'flex'"
[max]="100"
[value]="getProgress(update.overall.total, update.overall.done)"
/>
Downloading:
{{ getProgress(update.overall.total, update.overall.done) }}%
} @else {
<tui-loader />
Calculating download size
}
@if (update === true) {
<button tuiButton size="s" (click)="restart()">Restart</button>
}
</tui-action-bar>
}
`, `,
styles: [ styles: [
` `
@@ -47,13 +81,39 @@ import { HeaderComponent } from './components/header/header.component'
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
CommonModule,
RouterOutlet, RouterOutlet,
HeaderComponent, HeaderComponent,
TabsComponent, TabsComponent,
TuiScrollbar, TuiScrollbar,
TuiActionBar,
TuiProgress,
TuiLoader,
TuiIcon,
TuiButton,
], ],
}) })
export class PortalComponent { export class PortalComponent {
readonly name$ = inject<PatchDB<DataModel>>(PatchDB).watch$('ui', 'name') private readonly loader = inject(LoadingService)
private readonly errorService = inject(ErrorService)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
private readonly api = inject(ApiService)
readonly name = toSignal(this.patch.watch$('ui', 'name'))
readonly update = toSignal(inject(OSService).updating$)
getProgress(size: number, downloaded: number): number {
return Math.round((100 * downloaded) / (size || 1))
}
async restart() {
const loader = this.loader.open('Beginning restart').subscribe()
try {
await this.api.restartServer({})
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
}
}
} }

View File

@@ -1,8 +1,7 @@
import { ActivatedRouteSnapshot, Routes } from '@angular/router' import { Routes } from '@angular/router'
import { PackageDataEntry } from '../../services/patch-db/data-model' import { SYSTEM_UTILITIES } from 'src/app/utils/system-utilities'
import { getManifest } from '../../utils/get-package-data' import { titleResolver } from 'src/app/utils/title-resolver'
import { SYSTEM_UTILITIES } from '../../utils/system-utilities' import { toRouterLink } from 'src/app/utils/to-router-link'
import { toRouterLink } from '../../utils/to-router-link'
import { PortalComponent } from './portal.component' import { PortalComponent } from './portal.component'
const ROUTES: Routes = [ const ROUTES: Routes = [
@@ -17,54 +16,56 @@ const ROUTES: Routes = [
}, },
{ {
path: 'services', path: 'services',
data: { title: 'Services' },
title: titleResolver,
loadChildren: () => import('./routes/services/services.routes'), loadChildren: () => import('./routes/services/services.routes'),
}, },
// @TODO 041 // @TODO 041
// { // {
// title: systemTabResolver, // title: titleResolver,
// path: 'backups', // path: 'backups',
// loadComponent: () => import('./routes/backups/backups.component'), // loadComponent: () => import('./routes/backups/backups.component'),
// data: toNavigationItem('/portal/backups'), // data: toNavigationItem('/portal/backups'),
// }, // },
{ {
title: systemTabResolver, title: titleResolver,
path: 'logs', path: 'logs',
loadComponent: () => import('./routes/logs/logs.component'), loadComponent: () => import('./routes/logs/logs.component'),
data: toNavigationItem('/portal/logs'), data: toNavigationItem('/portal/logs'),
}, },
{ {
title: systemTabResolver, title: titleResolver,
path: 'marketplace', path: 'marketplace',
loadChildren: () => import('./routes/marketplace/marketplace.routes'), loadChildren: () => import('./routes/marketplace/marketplace.routes'),
data: toNavigationItem('/portal/marketplace'), data: toNavigationItem('/portal/marketplace'),
}, },
{ {
title: systemTabResolver, title: titleResolver,
path: 'system', path: 'system',
loadChildren: () => import('./routes/system/system.routes'), loadChildren: () => import('./routes/system/system.routes'),
data: toNavigationItem('/portal/system'), data: toNavigationItem('/portal/system'),
}, },
{ {
title: systemTabResolver, title: titleResolver,
path: 'notifications', path: 'notifications',
loadComponent: () => loadComponent: () =>
import('./routes/notifications/notifications.component'), import('./routes/notifications/notifications.component'),
data: toNavigationItem('/portal/notifications'), data: toNavigationItem('/portal/notifications'),
}, },
{ {
title: systemTabResolver, title: titleResolver,
path: 'sideload', path: 'sideload',
loadComponent: () => import('./routes/sideload/sideload.component'), loadComponent: () => import('./routes/sideload/sideload.component'),
data: toNavigationItem('/portal/sideload'), data: toNavigationItem('/portal/sideload'),
}, },
{ {
title: systemTabResolver, title: titleResolver,
path: 'updates', path: 'updates',
loadComponent: () => import('./routes/updates/updates.component'), loadComponent: () => import('./routes/updates/updates.component'),
data: toNavigationItem('/portal/updates'), data: toNavigationItem('/portal/updates'),
}, },
{ {
title: systemTabResolver, title: titleResolver,
path: 'metrics', path: 'metrics',
loadComponent: () => import('./routes/metrics/metrics.component'), loadComponent: () => import('./routes/metrics/metrics.component'),
data: toNavigationItem('/portal/metrics'), data: toNavigationItem('/portal/metrics'),
@@ -75,26 +76,12 @@ const ROUTES: Routes = [
export default ROUTES export default ROUTES
function systemTabResolver({ data }: ActivatedRouteSnapshot): string { function toNavigationItem(id: string) {
return data['title'] const { icon, title } = SYSTEM_UTILITIES[id] || {}
}
function toNavigationItem( return {
id: string, icon,
packages: Record<string, PackageDataEntry> = {}, title,
) { routerLink: toRouterLink(id),
const item = SYSTEM_UTILITIES[id] }
const routerLink = toRouterLink(id)
return item
? {
icon: item.icon,
title: item.title,
routerLink,
}
: {
icon: packages[id]?.icon,
title: getManifest(packages[id]!).title,
routerLink,
}
} }

View File

@@ -63,7 +63,13 @@ import { TARGET, TARGET_CREATE } from './target.component'
</div> </div>
<div *ngIf="!job.job.id" class="g-toggle"> <div *ngIf="!job.job.id" class="g-toggle">
Also Execute Now Also Execute Now
<input tuiSwitch type="checkbox" name="now" [(ngModel)]="job.now" /> <input
tuiSwitch
type="checkbox"
name="now"
[showIcons]="false"
[(ngModel)]="job.now"
/>
</div> </div>
<button <button
tuiButton tuiButton

View File

@@ -20,6 +20,7 @@ import {
import { import {
DialogService, DialogService,
Exver, Exver,
i18nKey,
i18nPipe, i18nPipe,
MARKDOWN, MARKDOWN,
SharedPipesModule, SharedPipesModule,
@@ -56,22 +57,19 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
<marketplace-additional [pkg]="pkg" (static)="onStatic($event)"> <marketplace-additional [pkg]="pkg" (static)="onStatic($event)">
@if (versions$ | async; as versions) { @if (versions$ | async; as versions) {
<marketplace-additional-item <marketplace-additional-item
(click)="versions.length ? selectVersion(pkg, version) : 0" (click)="selectVersion(pkg, version)"
[data]=" [data]="('Click to view all versions' | i18n) || ''"
versions.length [icon]="versions.length > 1 ? '@tui.chevron-right' : ''"
? 'Click to view all versions'
: 'No other versions'
"
label="All versions" label="All versions"
icon="@tui.chevron-right"
class="versions" class="versions"
[class.versions_empty]="versions.length < 2"
/> />
<ng-template <ng-template
#version #version
let-data="data" let-data="data"
let-completeWith="completeWith" let-completeWith="completeWith"
> >
<tui-radio-list [items]="versions" [(ngModel)]="data.value" /> <tui-radio-list [items]="versions" [(ngModel)]="data.version" />
<footer class="buttons"> <footer class="buttons">
<button <button
tuiButton tuiButton
@@ -137,9 +135,14 @@ import { MarketplaceService } from 'src/app/services/marketplace.service'
border-color: rgb(113 113 122); border-color: rgb(113 113 122);
border-style: solid; border-style: solid;
cursor: pointer; cursor: pointer;
::ng-deep label { ::ng-deep label {
cursor: pointer; cursor: pointer;
} }
&_empty {
pointer-events: none;
}
} }
.loading { .loading {
@@ -209,10 +212,13 @@ export class MarketplacePreviewComponent {
this.pkg$.pipe(filter(Boolean)), this.pkg$.pipe(filter(Boolean)),
this.flavor$, this.flavor$,
]).pipe( ]).pipe(
map(([{ otherVersions }, flavor]) => map(([{ otherVersions, version }, flavor]) =>
Object.keys(otherVersions) [
.filter(v => this.exver.getFlavor(v) === flavor) version,
.sort((a, b) => -1 * (this.exver.compareExver(a, b) || 0)), ...Object.keys(otherVersions).filter(
v => this.exver.getFlavor(v) === flavor,
),
].sort((a, b) => -1 * (this.exver.compareExver(a, b) || 0)),
), ),
) )
@@ -243,11 +249,9 @@ export class MarketplacePreviewComponent {
) { ) {
this.dialog this.dialog
.openComponent<string>(template, { .openComponent<string>(template, {
label: 'Versions', label: 'All versions',
size: 's', size: 's',
data: { data: { version },
value: version,
},
}) })
.pipe(filter(Boolean)) .pipe(filter(Boolean))
.subscribe(version => this.version$.next(version)) .subscribe(version => this.version$.next(version))

View File

@@ -4,7 +4,8 @@ import {
inject, inject,
signal, signal,
} from '@angular/core' } from '@angular/core'
import { ErrorService, i18nPipe } from '@start9labs/shared' import { ActivatedRoute, Router } from '@angular/router'
import { ErrorService, i18nPipe, isEmptyObject } from '@start9labs/shared'
import { TuiButton, TuiDataList, TuiDropdown } from '@taiga-ui/core' import { TuiButton, TuiDataList, TuiDropdown } from '@taiga-ui/core'
import { RR, ServerNotifications } from 'src/app/services/api/api.types' import { RR, ServerNotifications } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
@@ -66,18 +67,23 @@ import { NotificationsTableComponent } from './table.component'
], ],
}) })
export default class NotificationsComponent { export default class NotificationsComponent {
private readonly router = inject(Router)
private readonly route = inject(ActivatedRoute)
readonly service = inject(NotificationService) readonly service = inject(NotificationService)
readonly api = inject(ApiService) readonly api = inject(ApiService)
readonly errorService = inject(ErrorService) readonly errorService = inject(ErrorService)
readonly notifications = signal<ServerNotifications | undefined>(undefined) readonly notifications = signal<ServerNotifications | undefined>(undefined)
readonly toast = this.route.queryParams.subscribe(params => {
this.router.navigate([], { relativeTo: this.route, queryParams: {} })
if (isEmptyObject(params)) {
this.getMore({})
}
})
open = false open = false
ngOnInit() {
this.getMore({})
}
async getMore(params: RR.GetNotificationsReq) { async getMore(params: RR.GetNotificationsReq) {
try { try {
this.notifications.set(undefined) this.notifications.set(undefined)

View File

@@ -18,6 +18,7 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
tuiCell tuiCell
[routerLink]="services[d.key] ? ['..', d.key] : ['/portal/marketplace']" [routerLink]="services[d.key] ? ['..', d.key] : ['/portal/marketplace']"
[queryParams]="services[d.key] ? {} : { id: d.key }" [queryParams]="services[d.key] ? {} : { id: d.key }"
[class.error]="getError(d.key)"
> >
<tui-avatar><img alt="" [src]="d.value.icon" /></tui-avatar> <tui-avatar><img alt="" [src]="d.value.icon" /></tui-avatar>
<span tuiTitle> <span tuiTitle>
@@ -42,6 +43,10 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
min-height: 12rem; min-height: 12rem;
grid-column: span 3; grid-column: span 3;
} }
.error {
box-shadow: inset 1.25rem 0 0 -1rem var(--tui-status-warning);
}
`, `,
host: { class: 'g-card' }, host: { class: 'g-card' },
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -82,7 +87,7 @@ export class ServiceDependenciesComponent {
case 'notRunning': case 'notRunning':
return 'Not running' return 'Not running'
case 'actionRequired': case 'actionRequired':
return 'Action required' return 'Task Required'
case 'healthChecksFailed': case 'healthChecksFailed':
return 'Required health check not passing' return 'Required health check not passing'
case 'transitive': case 'transitive':

View File

@@ -7,7 +7,7 @@ import {
import { RouterLink } from '@angular/router' import { RouterLink } from '@angular/router'
import { i18nPipe } from '@start9labs/shared' import { i18nPipe } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
import { TuiButton, TuiLink } from '@taiga-ui/core' import { TuiButton, TuiIcon, TuiLink } from '@taiga-ui/core'
import { TuiBadge } from '@taiga-ui/kit' import { TuiBadge } from '@taiga-ui/kit'
import { ConfigService } from 'src/app/services/config.service' import { ConfigService } from 'src/app/services/config.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
@@ -26,23 +26,11 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
<td class="g-secondary" [style.grid-area]="'2 / span 4'"> <td class="g-secondary" [style.grid-area]="'2 / span 4'">
{{ info.description }} {{ info.description }}
</td> </td>
<td> <td [style.text-align]="'center'">
@if (info.public) { @if (info.public) {
<a <tui-icon class="g-positive" icon="@tui.globe" />
class="hosting"
tuiLink
iconStart="@tui.globe"
appearance="positive"
[textContent]="'Public'"
></a>
} @else { } @else {
<a <tui-icon class="g-negative" icon="@tui.lock" />
class="hosting"
tuiLink
iconStart="@tui.lock"
appearance="negative"
[textContent]="'Private'"
></a>
} }
</td> </td>
<td [style.grid-area]="'span 2'"> <td [style.grid-area]="'span 2'">
@@ -90,6 +78,10 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
text-transform: uppercase; text-transform: uppercase;
} }
tui-icon {
font-size: 1rem;
}
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
display: grid; display: grid;
grid-template-columns: repeat(3, min-content) 1fr 2rem; grid-template-columns: repeat(3, min-content) 1fr 2rem;
@@ -100,15 +92,11 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
td { td {
padding: 0; padding: 0;
} }
.hosting {
font-size: 0;
}
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [TuiButton, TuiBadge, TuiLink, RouterLink, i18nPipe], imports: [TuiButton, TuiBadge, TuiLink, TuiIcon, RouterLink, i18nPipe],
}) })
export class ServiceInterfaceComponent { export class ServiceInterfaceComponent {
private readonly config = inject(ConfigService) private readonly config = inject(ConfigService)
@@ -138,7 +126,7 @@ export class ServiceInterfaceComponent {
get href(): string | null { get href(): string | null {
return this.disabled return this.disabled
? 'null' ? null
: this.config.launchableAddress(this.info, this.pkg.hosts) : this.config.launchableAddress(this.info, this.pkg.hosts)
} }
} }

View File

@@ -62,10 +62,12 @@ export class ServiceInterfacesComponent {
.sort((a, b) => tuiDefaultSort(a[1], b[1])) .sort((a, b) => tuiDefaultSort(a[1], b[1]))
.map(([id, value]) => { .map(([id, value]) => {
const host = hosts[value.addressInfo.hostId] const host = hosts[value.addressInfo.hostId]
const port = value.addressInfo.internalPort
return { return {
...value, ...value,
public: !!host?.bindings[value.addressInfo.internalPort]?.net.public, addSsl: host?.bindings[port]?.options.addSsl,
public: !!host?.bindings[port]?.net.public,
addresses: host ? getAddresses(value, host, this.config) : {}, addresses: host ? getAddresses(value, host, this.config) : {},
routerLink: `./interface/${id}`, routerLink: `./interface/${id}`,
} }

View File

@@ -74,7 +74,7 @@ export class ControlsComponent {
this.errors.getPkgDepErrors$(this.manifest().id).pipe( this.errors.getPkgDepErrors$(this.manifest().id).pipe(
map(errors => map(errors =>
Object.keys(this.pkg().currentDependencies) Object.keys(this.pkg().currentDependencies)
.map(id => errors[id]) .map(id => errors?.[id])
.some(Boolean), .some(Boolean),
), ),
), ),

View File

@@ -1,6 +1,8 @@
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop' import { toSignal } from '@angular/core/rxjs-interop'
import { RouterLink } from '@angular/router'
import { TuiComparator, TuiTable } from '@taiga-ui/addon-table' import { TuiComparator, TuiTable } from '@taiga-ui/addon-table'
import { TuiButton, TuiLoader } from '@taiga-ui/core'
import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest' import { ToManifestPipe } from 'src/app/routes/portal/pipes/to-manifest'
import { DepErrorService } from 'src/app/services/dep-error.service' import { DepErrorService } from 'src/app/services/dep-error.service'
import { PackageDataEntry } from 'src/app/services/patch-db/data-model' import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
@@ -15,47 +17,73 @@ import { i18nPipe } from '@start9labs/shared'
standalone: true, standalone: true,
template: ` template: `
<ng-container *title>{{ 'Services' | i18n }}</ng-container> <ng-container *title>{{ 'Services' | i18n }}</ng-container>
<table tuiTable class="g-table" [(sorter)]="sorter"> @if (!services()) {
<thead> <tui-loader [style.height.%]="100" [textContent]="'Loading' | i18n" />
<tr> } @else {
<th [style.width.rem]="3"></th> @if (!services()?.length) {
<th tuiTh [requiredSort]="true" [sorter]="name"> <table tuiTable class="g-table" [(sorter)]="sorter">
{{ 'Name' | i18n }} <thead>
</th> <tr>
<th tuiTh>{{ 'Version' | i18n }}</th> <th [style.width.rem]="3"></th>
<th tuiTh [requiredSort]="true" [sorter]="uptime"> <th tuiTh [requiredSort]="true" [sorter]="name">
{{ 'Uptime' | i18n }} {{ 'Name' | i18n }}
</th> </th>
<th tuiTh [requiredSort]="true" [sorter]="status"> <th tuiTh>{{ 'Version' | i18n }}</th>
{{ 'Status' | i18n }} <th tuiTh [requiredSort]="true" [sorter]="uptime">
</th> {{ 'Uptime' | i18n }}
<th [style.width.rem]="8" [style.text-indent.rem]="1.5"> </th>
{{ 'Controls' | i18n }} <th tuiTh [requiredSort]="true" [sorter]="status">
</th> {{ 'Status' | i18n }}
</tr> </th>
</thead> <th [style.width.rem]="8" [style.text-indent.rem]="1.5">
<tbody> {{ 'Controls' | i18n }}
@for (pkg of services() | tuiTableSort; track $index) { </th>
<tr </tr>
appService </thead>
[pkg]="pkg" <tbody>
[depErrors]="errors()?.[(pkg | toManifest).id]" @for (pkg of services() | tuiTableSort; track $index) {
></tr> <tr
} @empty { appService
<tr> [pkg]="pkg"
<td colspan="6"> [depErrors]="errors()?.[(pkg | toManifest).id]"
{{ ></tr>
services() }
? ('No services installed' | i18n) </tbody>
: ('Loading' | i18n) </table>
}} } @else {
</td> <section>
</tr> <div>
} {{ 'Welcome to' | i18n }}
</tbody> <span>StartOS</span>
</table> </div>
<p>
{{
'To get started, visit the Marketplace and download your first service'
| i18n
}}
</p>
<a tuiButton routerLink="../marketplace">
{{ 'View Marketplace' | i18n }}
</a>
</section>
}
}
`, `,
styles: ` styles: `
@keyframes slide {
50% {
margin-block-start: 0;
}
55% {
margin-block-start: -1em;
}
100% {
margin-block-start: -1em;
}
}
:host { :host {
position: relative; position: relative;
font-size: 1rem; font-size: 1rem;
@@ -65,6 +93,31 @@ import { i18nPipe } from '@start9labs/shared'
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
padding: 0; padding: 0;
} }
section {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
div {
font-size: min(12vw, 4rem);
line-height: normal;
}
p {
font-size: 1.5rem;
}
span {
color: #ff4961;
}
a {
margin-block-start: 1rem;
}
}
`, `,
host: { class: 'g-page' }, host: { class: 'g-page' },
imports: [ imports: [
@@ -73,6 +126,9 @@ import { i18nPipe } from '@start9labs/shared'
TuiTable, TuiTable,
TitleDirective, TitleDirective,
i18nPipe, i18nPipe,
TuiLoader,
TuiButton,
RouterLink,
], ],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })

View File

@@ -71,7 +71,7 @@ export class StatusComponent {
get status(): i18nKey { get status(): i18nKey {
if (this.pkg.stateInfo.installingInfo) { if (this.pkg.stateInfo.installingInfo) {
return `${this.i18n.transform('Installing')}...${this.i18n.transform(getProgressText(this.pkg.stateInfo.installingInfo.progress.overall))}` as i18nKey return `${this.i18n.transform('Installing')}... ${this.i18n.transform(getProgressText(this.pkg.stateInfo.installingInfo.progress.overall))}` as i18nKey
} }
switch (this.getStatus(this.pkg).primary) { switch (this.getStatus(this.pkg).primary) {
@@ -108,7 +108,6 @@ export class StatusComponent {
case 'backingUp': case 'backingUp':
case 'restarting': case 'restarting':
case 'removing': case 'removing':
case 'restoring':
return '...' return '...'
default: default:
return '' return ''

View File

@@ -96,7 +96,7 @@ export default class ServiceActionsRoute {
readonly rebuild = { readonly rebuild = {
name: this.i18n.transform('Rebuild Service')!, name: this.i18n.transform('Rebuild Service')!,
description: this.i18n.transform( description: this.i18n.transform(
'Rebuilds the service container. Only necessary in there is a bug in StartOS', 'Rebuilds the service container. Only necessary if there is a bug in StartOS',
)!, )!,
} }

View File

@@ -86,14 +86,16 @@ export default class ServiceInterfaceRoute {
const item = serviceInterfaces[this.interfaceId()] const item = serviceInterfaces[this.interfaceId()]
const key = item?.addressInfo.hostId || '' const key = item?.addressInfo.hostId || ''
const host = hosts[key] const host = hosts[key]
const port = item?.addressInfo.internalPort
if (!host || !item) { if (!host || !item || !port) {
return return
} }
return { return {
...item, ...item,
public: !!host?.bindings[item.addressInfo.internalPort]?.net.public, addSsl: host?.bindings[port]?.options.addSsl,
public: !!host?.bindings[port]?.net.public,
addresses: getAddresses(item, host, this.config), addresses: getAddresses(item, host, this.config),
} }
}) })

View File

@@ -4,6 +4,7 @@ import { MarkdownComponent } from '@start9labs/shared'
import { defer, map, Observable, of } from 'rxjs' import { defer, map, Observable, of } from 'rxjs'
import { share } from 'rxjs/operators' import { share } from 'rxjs/operators'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { titleResolver } from 'src/app/utils/title-resolver'
import { ServiceOutletComponent } from './routes/outlet.component' import { ServiceOutletComponent } from './routes/outlet.component'
import { ServiceRoute } from './routes/service.component' import { ServiceRoute } from './routes/service.component'
@@ -11,6 +12,7 @@ import { ServiceRoute } from './routes/service.component'
export const ROUTES: Routes = [ export const ROUTES: Routes = [
{ {
path: ':pkgId', path: ':pkgId',
title: titleResolver,
component: ServiceOutletComponent, component: ServiceOutletComponent,
children: [ children: [
{ {

View File

@@ -189,18 +189,10 @@ export class BackupNetworkComponent {
select(target: MappedBackupTarget<CifsBackupTarget>) { select(target: MappedBackupTarget<CifsBackupTarget>) {
if (!target.entry.mountable) { if (!target.entry.mountable) {
this.dialog this.dialog.openAlert(ERROR, { label: 'Unable to connect' }).subscribe()
.openAlert(ERROR, {
appearance: 'negative',
label: 'Unable to connect',
autoClose: 0,
})
.subscribe()
} else if (this.type === 'restore' && !target.hasAnyBackup) { } else if (this.type === 'restore' && !target.hasAnyBackup) {
this.dialog this.dialog
.openAlert('Network Folder does not contain a valid backup', { .openAlert('Network Folder does not contain a valid backup')
appearance: 'negative',
})
.subscribe() .subscribe()
} else { } else {
this.networkFolders.emit(target) this.networkFolders.emit(target)

View File

@@ -132,9 +132,7 @@ export class BackupPhysicalComponent {
select(target: MappedBackupTarget<DiskBackupTarget>) { select(target: MappedBackupTarget<DiskBackupTarget>) {
if (this.type === 'restore' && !target.hasAnyBackup) { if (this.type === 'restore' && !target.hasAnyBackup) {
this.dialog this.dialog
.openAlert('Drive partition does not contain a valid backup', { .openAlert('Drive partition does not contain a valid backup')
appearance: 'negative',
})
.subscribe() .subscribe()
} else { } else {
this.physicalFolders.emit(target) this.physicalFolders.emit(target)

View File

@@ -187,25 +187,16 @@ export default class SystemEmailComponent {
async sendTestEmail(value: typeof inputSpec.constants.customSmtp._TYPE) { async sendTestEmail(value: typeof inputSpec.constants.customSmtp._TYPE) {
const loader = this.loader.open('Sending email').subscribe() const loader = this.loader.open('Sending email').subscribe()
const success =
`${this.i18n.transform('A test email has been sent to')} ${this.testAddress}.<br /><br /><b>${this.i18n.transform('Check your spam folder and mark as not spam.')}</b>` as i18nKey
try { try {
await this.api.testSmtp({ await this.api.testSmtp({ to: this.testAddress, ...value })
to: this.testAddress, this.dialog.openAlert(success, { label: 'Success' }).subscribe()
...value,
})
} catch (e: any) { } catch (e: any) {
this.errorService.handleError(e) this.errorService.handleError(e)
} finally { } finally {
loader.unsubscribe() loader.unsubscribe()
} }
this.dialog
.openAlert(
`${this.i18n.transform('A test email has been sent to')} ${this.testAddress}.<br /><br /><b>${this.i18n.transform('Check your spam folder and mark as not spam.')}</b>` as i18nKey,
{
label: 'Success',
},
)
.subscribe()
} }
} }

View File

@@ -337,7 +337,6 @@ export default class SystemGeneralComponent {
try { try {
await this.api.resetTor({ wipeState, reason: 'User triggered' }) await this.api.resetTor({ wipeState, reason: 'User triggered' })
this.dialog.openAlert('Tor reset in progress').subscribe() this.dialog.openAlert('Tor reset in progress').subscribe()
} catch (e: any) { } catch (e: any) {
this.errorService.handleError(e) this.errorService.handleError(e)

View File

@@ -80,12 +80,16 @@ export default class StartOsUiComponent {
inject<PatchDB<DataModel>>(PatchDB) inject<PatchDB<DataModel>>(PatchDB)
.watch$('serverInfo', 'network', 'host') .watch$('serverInfo', 'network', 'host')
.pipe( .pipe(
map(host => ({ map(host => {
...this.iface, const port = this.iface.addressInfo.internalPort
public:
!!host.bindings[this.iface.addressInfo.internalPort]?.net.public, return {
addresses: getAddresses(this.iface, host, this.config), ...this.iface,
})), addSsl: host.bindings[port]?.options.addSsl,
public: !!host.bindings[port]?.net.public,
addresses: getAddresses(this.iface, host, this.config),
}
}),
), ),
) )

View File

@@ -66,6 +66,7 @@ import { wifiSpec } from './wifi.const'
type="checkbox" type="checkbox"
tuiSwitch tuiSwitch
[style.margin-inline-start]="'auto'" [style.margin-inline-start]="'auto'"
[showIcons]="false"
[ngModel]="status()?.enabled" [ngModel]="status()?.enabled"
(ngModelChange)="onToggle($event)" (ngModelChange)="onToggle($event)"
/> />

View File

@@ -89,9 +89,7 @@ export class ActionService {
this.dialog this.dialog
.openAlert( .openAlert(
`${this.i18n.transform('Action can only be executed when service is')} ${statusesStr}` as i18nKey, `${this.i18n.transform('Action can only be executed when service is')} ${statusesStr}` as i18nKey,
{ { label: 'Forbidden' },
label: 'Forbidden',
},
) )
.pipe(filter(Boolean)) .pipe(filter(Boolean))
.subscribe() .subscribe()

View File

@@ -1,4 +1,4 @@
import { Injectable } from '@angular/core' import { inject, Injectable } from '@angular/core'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { import {
BehaviorSubject, BehaviorSubject,
@@ -17,11 +17,14 @@ import { RR } from './api/api.types'
providedIn: 'root', providedIn: 'root',
}) })
export class OSService { export class OSService {
private readonly api = inject(ApiService)
private readonly patch = inject<PatchDB<DataModel>>(PatchDB)
osUpdate?: RR.CheckOsUpdateRes osUpdate?: RR.CheckOsUpdateRes
updateAvailable$ = new BehaviorSubject<boolean>(false) readonly updateAvailable$ = new BehaviorSubject<boolean>(false)
readonly updating$ = this.patch.watch$('serverInfo', 'statusInfo').pipe( readonly updating$ = this.patch.watch$('serverInfo', 'statusInfo').pipe(
map(status => !!status.updateProgress || status.updated), map(status => status.updateProgress ?? status.updated),
distinctUntilChanged(), distinctUntilChanged(),
) )
@@ -35,21 +38,12 @@ export class OSService {
readonly updatingOrBackingUp$ = combineLatest([ readonly updatingOrBackingUp$ = combineLatest([
this.updating$, this.updating$,
this.backingUp$, this.backingUp$,
]).pipe(map(([updating, backingUp]) => updating || backingUp)) ]).pipe(map(([updating, backingUp]) => !!updating || backingUp))
readonly showUpdate$ = combineLatest([ readonly showUpdate$ = combineLatest([
this.updateAvailable$, this.updateAvailable$,
this.updating$, this.updating$,
]).pipe( ]).pipe(map(([available, updating]) => available && !updating))
map(([available, updating]) => {
return available && !updating
}),
)
constructor(
private readonly api: ApiService,
private readonly patch: PatchDB<DataModel>,
) {}
async loadOS(): Promise<void> { async loadOS(): Promise<void> {
const { version, id } = await getServerInfo(this.patch) const { version, id } = await getServerInfo(this.patch)
@@ -59,9 +53,11 @@ export class OSService {
registry: startosRegistry, registry: startosRegistry,
serverId: id, serverId: id,
}) })
const [latestVersion, _] = Object.entries(this.osUpdate).at(-1)!
const updateAvailable = const [latest, _] = Object.entries(this.osUpdate).at(-1)!
Version.parse(latestVersion).compare(Version.parse(version)) === 'greater'
this.updateAvailable$.next(updateAvailable) this.updateAvailable$.next(
Version.parse(latest).compare(Version.parse(version)) === 'greater',
)
} }
} }

View File

@@ -1,7 +1,8 @@
import { inject, Injectable } from '@angular/core' import { inject, Injectable } from '@angular/core'
import { CanActivateFn, IsActiveMatchOptions, Router } from '@angular/router' import { CanActivateFn, IsActiveMatchOptions, Router } from '@angular/router'
import { DialogService } from '@start9labs/shared' import { DialogService, i18nPipe } from '@start9labs/shared'
import { TUI_TRUE_HANDLER } from '@taiga-ui/cdk' import { TUI_TRUE_HANDLER } from '@taiga-ui/cdk'
import { TuiAlertService } from '@taiga-ui/core'
import { import {
BehaviorSubject, BehaviorSubject,
combineLatest, combineLatest,
@@ -41,7 +42,8 @@ const OPTIONS: IsActiveMatchOptions = {
providedIn: 'root', providedIn: 'root',
}) })
export class StateService extends Observable<RR.ServerState | null> { export class StateService extends Observable<RR.ServerState | null> {
private readonly dialog = inject(DialogService) private readonly alerts = inject(TuiAlertService)
private readonly i18n = inject(i18nPipe)
private readonly api = inject(ApiService) private readonly api = inject(ApiService)
private readonly router = inject(Router) private readonly router = inject(Router)
private readonly network$ = inject(NetworkService) private readonly network$ = inject(NetworkService)
@@ -91,10 +93,10 @@ export class StateService extends Observable<RR.ServerState | null> {
.pipe( .pipe(
exhaustMap(() => exhaustMap(() =>
concat( concat(
this.dialog this.alerts
.openAlert('Trying to reach server', { .open(this.i18n.transform('Trying to reach server'), {
label: 'State unknown', label: this.i18n.transform('State unknown'),
autoClose: 0, closeable: false,
appearance: 'negative', appearance: 'negative',
}) })
.pipe( .pipe(
@@ -104,8 +106,8 @@ export class StateService extends Observable<RR.ServerState | null> {
), ),
), ),
), ),
this.dialog.openAlert('Connection restored', { this.alerts.open(this.i18n.transform('Connection restored'), {
label: 'Server connected', label: this.i18n.transform('Server connected'),
appearance: 'positive', appearance: 'positive',
}), }),
), ),

View File

@@ -23,7 +23,7 @@ export async function getAllPackages(
} }
export function getManifest(pkg: PackageDataEntry): T.Manifest { export function getManifest(pkg: PackageDataEntry): T.Manifest {
return isInstalling(pkg) return isInstalling(pkg) || isRestoring(pkg)
? pkg.stateInfo.installingInfo.newManifest ? pkg.stateInfo.installingInfo.newManifest
: pkg.stateInfo.manifest! : pkg.stateInfo.manifest!
} }

View File

@@ -0,0 +1,26 @@
import { inject } from '@angular/core'
import { ActivatedRouteSnapshot } from '@angular/router'
import { i18nPipe } from '@start9labs/shared'
import { PatchDB } from 'patch-db-client'
import { firstValueFrom } from 'rxjs'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { getManifest } from 'src/app/utils/get-package-data'
export async function titleResolver({
data,
params,
}: ActivatedRouteSnapshot): Promise<string> {
let route = inject(i18nPipe).transform(data['title'])
const patch = inject<PatchDB<DataModel>>(PatchDB)
const title = await firstValueFrom(patch.watch$('ui', 'name'))
const id = params['pkgId']
if (id) {
const service = await firstValueFrom(patch.watch$('packageData', id))
route = service && getManifest(service).title
}
return `${title || 'StartOS'}${route}`
}