From 8c977c51cac098a3e5317c7eeaf12583b8f77307 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Fri, 9 May 2025 10:29:17 -0600 Subject: [PATCH] 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 --- web/package-lock.json | 214 +++++++++--------- web/package.json | 22 +- .../shared/src/i18n/dictionaries/de.ts | 7 +- .../shared/src/i18n/dictionaries/en.ts | 9 +- .../shared/src/i18n/dictionaries/es.ts | 7 +- .../shared/src/i18n/dictionaries/pl.ts | 7 +- web/projects/shared/src/i18n/i18n.pipe.ts | 6 +- .../shared/src/services/dialog.service.ts | 3 +- web/projects/ui/src/app/app.component.ts | 7 +- web/projects/ui/src/app/app.providers.ts | 4 + .../components/toast-container.component.ts | 8 +- .../app/components/update-toast.component.ts | 80 ------- .../form-control/form-control.component.ts | 14 +- .../form-toggle/form-toggle.component.html | 1 + .../components/header/navigation.component.ts | 2 +- .../interfaces/actions.component.ts | 11 +- .../interfaces/clearnet.component.ts | 100 ++++---- .../components/interfaces/interface.utils.ts | 52 +++-- .../components/interfaces/local.component.ts | 6 +- .../components/interfaces/tor.component.ts | 8 +- .../src/app/routes/portal/portal.component.ts | 72 +++++- .../ui/src/app/routes/portal/portal.routes.ts | 55 ++--- .../routes/backups/modals/edit.component.ts | 8 +- .../marketplace/modals/preview.component.ts | 36 +-- .../notifications/notifications.component.ts | 18 +- .../components/dependencies.component.ts | 7 +- .../components/interface.component.ts | 32 +-- .../components/interfaces.component.ts | 4 +- .../services/dashboard/controls.component.ts | 2 +- .../services/dashboard/dashboard.component.ts | 134 +++++++---- .../services/dashboard/status.component.ts | 3 +- .../services/routes/actions.component.ts | 2 +- .../services/routes/interface.component.ts | 6 +- .../portal/routes/services/services.routes.ts | 2 + .../routes/backups/network.component.ts | 12 +- .../routes/backups/physical.component.ts | 4 +- .../system/routes/email/email.component.ts | 17 +- .../routes/general/general.component.ts | 1 - .../routes/interfaces/interfaces.component.ts | 16 +- .../system/routes/wifi/wifi.component.ts | 1 + .../ui/src/app/services/action.service.ts | 4 +- .../ui/src/app/services/os.service.ts | 32 ++- .../ui/src/app/services/state.service.ts | 18 +- .../ui/src/app/utils/get-package-data.ts | 2 +- .../ui/src/app/utils/title-resolver.ts | 26 +++ 45 files changed, 585 insertions(+), 497 deletions(-) delete mode 100644 web/projects/ui/src/app/components/update-toast.component.ts create mode 100644 web/projects/ui/src/app/utils/title-resolver.ts diff --git a/web/package-lock.json b/web/package-lock.json index f8e01984c..0d0a43490 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -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.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/addon-charts": "4.36.0", + "@taiga-ui/addon-commerce": "4.36.0", + "@taiga-ui/addon-mobile": "4.36.0", + "@taiga-ui/addon-table": "4.36.0", + "@taiga-ui/cdk": "4.36.0", + "@taiga-ui/core": "4.36.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/experimental": "4.36.0", + "@taiga-ui/icons": "4.36.0", + "@taiga-ui/kit": "4.36.0", + "@taiga-ui/layout": "4.36.0", + "@taiga-ui/legacy": "4.36.0", "@taiga-ui/polymorpheus": "4.9.0", "@tinkoff/ng-dompurify": "4.0.0", "ansi-to-html": "^0.7.2", @@ -3422,9 +3422,9 @@ } }, "node_modules/@maskito/angular": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@maskito/angular/-/angular-3.5.0.tgz", - "integrity": "sha512-5uwar32qsGdZNHUgZpFnICg9tJKCXbZEGk2ZnchHzDIfN5ojNT7wKzoq8NhpRlGb3p4qQCE+PXb5GERkcWM/Sw==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@maskito/angular/-/angular-3.7.2.tgz", + "integrity": "sha512-0auXz5dsS0pNuSZ3WFsQK3Fd6nucC9Um1WVlxoVbi0VEZZG68WirVQybwo7J5/J5gCDffqJn2bAw0qrbbrt0mQ==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -3433,35 +3433,35 @@ "peerDependencies": { "@angular/core": ">=16.0.0", "@angular/forms": ">=16.0.0", - "@maskito/core": "^3.5.0" + "@maskito/core": "^3.7.2" } }, "node_modules/@maskito/core": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@maskito/core/-/core-3.5.0.tgz", - "integrity": "sha512-zgmBjXeXc7BSBaw8jQw25dnwkFmKDvdj5rHzhEIxYhgGtnpli236F0YWPIOYzIwADjbefwDq1o7qpJfMsdDO4Q==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@maskito/core/-/core-3.7.2.tgz", + "integrity": "sha512-jnX1u2HAZFy0K8Ll6AoeMpe502aVzrqgPKG7rk/qtbivW+71U3vutP3kqmjZtgsPoNzQ2wzKVWMgi7JEXDK11g==", "license": "Apache-2.0", "peer": true }, "node_modules/@maskito/kit": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-3.5.0.tgz", - "integrity": "sha512-QnpZsPTINgK4ScA4pMMJagoj+ufIXc/VGOP61AsQa/H/lmXII4pEZTLzpmMNUYmCEIEyjHR2DIbfEed04sktvQ==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-3.7.2.tgz", + "integrity": "sha512-2gx/7k0iRcKWq7+2yeUEUuv13+4KibxkQrTdGSwZZVNbPsmj4b7r5KTbjfHl2ZdwuASSxJiVV8DWF91ON+cmBA==", "license": "Apache-2.0", "peer": true, "peerDependencies": { - "@maskito/core": "^3.5.0" + "@maskito/core": "^3.7.2" } }, "node_modules/@maskito/phone": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/@maskito/phone/-/phone-3.5.0.tgz", - "integrity": "sha512-qh/GGRFn8cZBY/JUTLa5yeSSKSVlekggKeiCbf0eX0I53/HM2pNZ/5667S8SXwn5WjIEeB79Eltl8MNvK74yvA==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/@maskito/phone/-/phone-3.7.2.tgz", + "integrity": "sha512-27KfVE9fR+NiwB35z03gPZ/UM+wJR5AfRYiMh37JZCXBz+/WRhtH3mntpzxmp0CCGoFTD/c3hBkCR9p5MAaxiQ==", "license": "Apache-2.0", "peer": true, "peerDependencies": { - "@maskito/core": "^3.5.0", - "@maskito/kit": "^3.5.0", + "@maskito/core": "^3.7.2", + "@maskito/kit": "^3.7.2", "libphonenumber-js": ">=1.0.0" } }, @@ -4417,9 +4417,9 @@ "link": true }, "node_modules/@taiga-ui/addon-charts": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.32.0.tgz", - "integrity": "sha512-VhGkBxwfra5eijSvZdXhMKOWEnFMESo5TX3OfsahIXWJXivwguvIc63rIhHYq2uC+t5sj1kINveO4yLqOeAm/Q==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.36.0.tgz", + "integrity": "sha512-0lGbEPFVQfc8ntWUs+kmi1MyhFFZefIxdJmgbD1cB6Irb8T/JXxNizsJR4JXUJt3ozcE9or7Avbux2lkZaPjWw==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4428,15 +4428,15 @@ "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.0", "@ng-web-apis/common": "^4.12.0", - "@taiga-ui/cdk": "^4.32.0", - "@taiga-ui/core": "^4.32.0", + "@taiga-ui/cdk": "^4.36.0", + "@taiga-ui/core": "^4.36.0", "@taiga-ui/polymorpheus": "^4.9.0" } }, "node_modules/@taiga-ui/addon-commerce": { - "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==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.36.0.tgz", + "integrity": "sha512-DiUBCdvsk+3aNAHXPpGZI9KNuS0hc+T/XSz0RUPKlFWqQ1TUV4aqTE9o575EU7q+IyAn1yNXq40izmuvx/BLwg==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4445,22 +4445,22 @@ "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.0", "@angular/forms": ">=16.0.0", - "@maskito/angular": "^3.5.0", - "@maskito/core": "^3.5.0", - "@maskito/kit": "^3.5.0", + "@maskito/angular": "^3.7.2", + "@maskito/core": "^3.7.2", + "@maskito/kit": "^3.7.2", "@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/cdk": "^4.36.0", + "@taiga-ui/core": "^4.36.0", + "@taiga-ui/i18n": "^4.36.0", + "@taiga-ui/kit": "^4.36.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/addon-mobile": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.32.0.tgz", - "integrity": "sha512-pUoHWyILPj6KIAhna1JDzz48c2nCjqYb1tb7AL3LQ3qfNwAbg9fvjBIfrgWMhW0LaDeh5+FfrS7oiO/ERcHTLg==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.36.0.tgz", + "integrity": "sha512-7JvUkBkTlcBVqtwIu93WPJGs50w6UFeuG354XatzGzyAMGL7ipDkrWoxqxHl5HbKt1n/70V8wfAaBT3mgKFvBg==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4470,18 +4470,18 @@ "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.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/cdk": "^4.36.0", + "@taiga-ui/core": "^4.36.0", + "@taiga-ui/kit": "^4.36.0", + "@taiga-ui/layout": "^4.36.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/addon-table": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.32.0.tgz", - "integrity": "sha512-8oXeqLO1wGH8RYHTYWhjCvrKWptPN1we04NRahmFY4AxSJ3u7MqaR4420RRNO4zZG9kGyktLXPjqGocMoymL8Q==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.36.0.tgz", + "integrity": "sha512-VKztLMvDo3YeEvEjLHO1DZ1y7CK9Vj+jwLyDKoDpJWBGOJfzqZfYb/lqu228NgKL+3JmGAqvq0VQnN5gx1MtSA==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4490,18 +4490,18 @@ "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.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/cdk": "^4.36.0", + "@taiga-ui/core": "^4.36.0", + "@taiga-ui/i18n": "^4.36.0", + "@taiga-ui/kit": "^4.36.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/cdk": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.32.0.tgz", - "integrity": "sha512-qvYe79uh6Tw2LJSEGLJYUlAidbZi6JgcuMRqWAB1JhyIGpgnaqar5v+oJJg28zJZZ81PCj59VkFNLL0dNVXRUg==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.36.0.tgz", + "integrity": "sha512-JgnP7DDZsv87tVK6w9VE+mNTR3nomMgbVq1XoFh5JXbxUwC5FlEIMRKULygBW2FwjbfDehA+2Tm+OeB1HmCSZQ==", "license": "Apache-2.0", "dependencies": { "tslib": "2.8.1" @@ -4511,7 +4511,7 @@ "@angular-devkit/schematics": ">=16.0.0", "@schematics/angular": ">=16.0.0", "ng-morph": "^4.8.4", - "parse5": ">=7.2.1" + "parse5": ">=7.3.0" }, "peerDependencies": { "@angular/animations": ">=16.0.0", @@ -4530,9 +4530,9 @@ } }, "node_modules/@taiga-ui/core": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.32.0.tgz", - "integrity": "sha512-e1z7YhhjePMRLTk+s83OclN45wMixCwZWMxM9WuXIyd2KXMPhJvrrgBDjoK66GuFtjZ4qaSF/H2FTIJmJ/6MiQ==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.36.0.tgz", + "integrity": "sha512-BwSF/ZA0pvqqbW2UYbtwjvaSfb7uIj1gNbaMTF7uhjQkvwL22x/Opbxcs2DAoja4I/EPpT3AJMr++S1bzUriWw==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4546,9 +4546,9 @@ "@angular/router": ">=16.0.0", "@ng-web-apis/common": "^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/i18n": "^4.32.0", + "@taiga-ui/i18n": "^4.36.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } @@ -4568,9 +4568,9 @@ } }, "node_modules/@taiga-ui/experimental": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-4.32.0.tgz", - "integrity": "sha512-sCOasTF9UlgPOW4vXSeM5M1tgGrjgofa+Qq7hejYW3BXs/4mnmdm5yiYzWfMVZd4jgTSeV5kobkIJ9Fkp2zt6g==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/experimental/-/experimental-4.36.0.tgz", + "integrity": "sha512-g1TN+hcr7zF/BLw9Sj9X/uKDx8pSEWU9282og5itxsPHgans+ENuh/0dS6kShrx+zwoWLOnjI0MyIR6IktqmQg==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4578,18 +4578,18 @@ "peerDependencies": { "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.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/addon-commerce": "^4.36.0", + "@taiga-ui/cdk": "^4.36.0", + "@taiga-ui/core": "^4.36.0", + "@taiga-ui/kit": "^4.36.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/i18n": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.32.0.tgz", - "integrity": "sha512-PAQv9RxSgvf3RBUps9bXX2erCk9oiSt9ApM3SAIa/OuzET0TJsW6yZ4EQrtLw03bMX3wyA8PnEYva9wzoYAqxA==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.36.0.tgz", + "integrity": "sha512-jGc+5ytZKA4zEDyt1uBxUq/4tp4hJmppKdl6GZ+AWlLl6Tx2YiYqcA3RLDaPdiXsS/UeEoJ7Vjyn6PVd/fv35A==", "license": "Apache-2.0", "peer": true, "dependencies": { @@ -4602,18 +4602,18 @@ } }, "node_modules/@taiga-ui/icons": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.32.0.tgz", - "integrity": "sha512-X2ZSiqeMKigULgX91fBZkFJRUbwzeW934yLEGhq7C1JMcC2+ppLmL/NbkD2kpKZ4OeHnGsItxKauoXu44rXeLA==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.36.0.tgz", + "integrity": "sha512-mcGvUnwF7g1wcnXFJenrcfmlzE1+k2Haz2Cudws0htZQ5VnR/kC1f1pEJlhZYycqGgoJJrNEBPbHCBGrIkqfXg==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.3.0" } }, "node_modules/@taiga-ui/kit": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.32.0.tgz", - "integrity": "sha512-J8XoqeQHBNbAAuTz0kVACujDOb3zuh4Vps83lYl+msFIaUmkjC37muXF3eRlImH3m4DpT8yI8+ffh/T3+jky7w==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.36.0.tgz", + "integrity": "sha512-jUVMa3kUrv49HvOCzVymMo/+FRTv1Yrflbjjm+V7A4z9CnZR+tg2VZ+wJzjU0qNppp/nstu+ge+XTJ5B5Hsatw==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4623,25 +4623,25 @@ "@angular/core": ">=16.0.0", "@angular/forms": ">=16.0.0", "@angular/router": ">=16.0.0", - "@maskito/angular": "^3.5.0", - "@maskito/core": "^3.5.0", - "@maskito/kit": "^3.5.0", - "@maskito/phone": "^3.5.0", + "@maskito/angular": "^3.7.2", + "@maskito/core": "^3.7.2", + "@maskito/kit": "^3.7.2", + "@maskito/phone": "^3.7.2", "@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/cdk": "^4.36.0", + "@taiga-ui/core": "^4.36.0", + "@taiga-ui/i18n": "^4.36.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/layout": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.32.0.tgz", - "integrity": "sha512-ECaoJ3CbL+eoqL1MleaHvD9/FQ5OCaUMkjOdXId2Jg2MNbuDhtS9hqVZvSWLXRWz3XgC3aADYnPwrNvIsy5Mng==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.36.0.tgz", + "integrity": "sha512-Y4s2mdYJopuVjTd38HmqaACK9RwoJAirC7PCYg8NVCWb2Q4mgqHzHtIwTcERRfxRANoSTt+Ne9rLIlxY5JOuVQ==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -4649,17 +4649,17 @@ "peerDependencies": { "@angular/common": ">=16.0.0", "@angular/core": ">=16.0.0", - "@taiga-ui/cdk": "^4.32.0", - "@taiga-ui/core": "^4.32.0", - "@taiga-ui/kit": "^4.32.0", + "@taiga-ui/cdk": "^4.36.0", + "@taiga-ui/core": "^4.36.0", + "@taiga-ui/kit": "^4.36.0", "@taiga-ui/polymorpheus": "^4.9.0", "rxjs": ">=7.0.0" } }, "node_modules/@taiga-ui/legacy": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/legacy/-/legacy-4.32.0.tgz", - "integrity": "sha512-wEsywt6hK2NNpHddqVrL0MTd1QFzmhMdPPgtraNOieQmzrSW2jpA37KJO11cVleuRdDsk98rFtzQ3stlNNFy5Q==", + "version": "4.36.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/legacy/-/legacy-4.36.0.tgz", + "integrity": "sha512-x4F5ZrAhJPhlCda8daLns7tADkjLDzSNmZa8xVMHeL8nuMjx7FbpKxqzVKcKJ/SLWqF5WHs30D6S/mXjl5YvYw==", "license": "Apache-2.0", "dependencies": { "tslib": ">=2.8.1" @@ -9526,9 +9526,9 @@ } }, "node_modules/libphonenumber-js": { - "version": "1.12.6", - "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.6.tgz", - "integrity": "sha512-PJiS4ETaUfCOFLpmtKzAbqZQjCCKVu2OhTV4SVNNE7c2nu/dACvtCqj4L0i/KWNnIgRv7yrILvBj5Lonv5Ncxw==", + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/libphonenumber-js/-/libphonenumber-js-1.12.7.tgz", + "integrity": "sha512-0nYZSNj/QEikyhcM5RZFXGlCB/mr4PVamnT1C2sKBnDDTYndrvbybYjvg+PMqAndQHlLbwQ3socolnL3WWTUFA==", "license": "MIT", "peer": true }, @@ -11665,12 +11665,12 @@ } }, "node_modules/parse5": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", - "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", "license": "MIT", "dependencies": { - "entities": "^4.5.0" + "entities": "^6.0.0" }, "funding": { "url": "https://github.com/inikulin/parse5?sponsor=1" @@ -11715,9 +11715,9 @@ } }, "node_modules/parse5/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.0.tgz", + "integrity": "sha512-aKstq2TDOndCn4diEyp9Uq/Flu2i1GlLkc6XIDQSDMuaFE3OPW5OphLCyQ5SpSJZTb4reN+kTcYru5yIfXoRPw==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" diff --git a/web/package.json b/web/package.json index d28baa1d9..1d3398a88 100644 --- a/web/package.json +++ b/web/package.json @@ -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.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/addon-charts": "4.36.0", + "@taiga-ui/addon-commerce": "4.36.0", + "@taiga-ui/addon-mobile": "4.36.0", + "@taiga-ui/addon-table": "4.36.0", + "@taiga-ui/cdk": "4.36.0", + "@taiga-ui/core": "4.36.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/experimental": "4.36.0", + "@taiga-ui/icons": "4.36.0", + "@taiga-ui/kit": "4.36.0", + "@taiga-ui/layout": "4.36.0", + "@taiga-ui/legacy": "4.36.0", "@taiga-ui/polymorpheus": "4.9.0", "@tinkoff/ng-dompurify": "4.0.0", "ansi-to-html": "^0.7.2", diff --git a/web/projects/shared/src/i18n/dictionaries/de.ts b/web/projects/shared/src/i18n/dictionaries/de.ts index 0bda129cf..9a61bab97 100644 --- a/web/projects/shared/src/i18n/dictionaries/de.ts +++ b/web/projects/shared/src/i18n/dictionaries/de.ts @@ -441,7 +441,7 @@ export default { 438: 'Kein Internet', 439: 'Verbindung wird hergestellt', 440: 'Fährt herunter', - 441: 'Versionen', + 441: 'Alle versionen', 442: 'Neue Benachrichtigungen', 443: 'Anzeigen', 444: 'PWA wird neu geladen', @@ -498,4 +498,9 @@ export default { 495: 'Validierung', 496: 'in Bearbeitung', 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 diff --git a/web/projects/shared/src/i18n/dictionaries/en.ts b/web/projects/shared/src/i18n/dictionaries/en.ts index abce9c25c..6464837e5 100644 --- a/web/projects/shared/src/i18n/dictionaries/en.ts +++ b/web/projects/shared/src/i18n/dictionaries/en.ts @@ -282,7 +282,7 @@ export const ENGLISH = { 'Donation Link': 280, 'Standard Actions': 281, '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, 'Uninstalls this service from StartOS and delete all data permanently.': 285, 'Dashboard': 286, @@ -440,7 +440,7 @@ export const ENGLISH = { 'No Internet': 438, 'Connecting': 439, 'Shutting down': 440, - 'Versions': 441, + 'All versions': 441, 'New notifications': 442, 'View': 443, 'Reloading PWA': 444, @@ -497,4 +497,9 @@ export const ENGLISH = { 'Validating': 495, 'in progress': 496, '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 diff --git a/web/projects/shared/src/i18n/dictionaries/es.ts b/web/projects/shared/src/i18n/dictionaries/es.ts index aa5610024..c8e5cb8be 100644 --- a/web/projects/shared/src/i18n/dictionaries/es.ts +++ b/web/projects/shared/src/i18n/dictionaries/es.ts @@ -441,7 +441,7 @@ export default { 438: 'Sin Internet', 439: 'Conectando', 440: 'Apagando', - 441: 'Versiones', + 441: 'Todas las versiones', 442: 'Nuevas notificaciones', 443: 'Ver', 444: 'Recargando PWA', @@ -498,4 +498,9 @@ export default { 495: 'Validando', 496: 'en progreso', 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 diff --git a/web/projects/shared/src/i18n/dictionaries/pl.ts b/web/projects/shared/src/i18n/dictionaries/pl.ts index 360f73f91..bc2b6c064 100644 --- a/web/projects/shared/src/i18n/dictionaries/pl.ts +++ b/web/projects/shared/src/i18n/dictionaries/pl.ts @@ -441,7 +441,7 @@ export default { 438: 'Brak Internetu', 439: 'Łączenie', 440: 'Wyłączanie', - 441: 'Wersje', + 441: 'Wszystkie wersje', 442: 'Nowe powiadomienia', 443: 'Zobacz', 444: 'Przeładowywanie PWA', @@ -498,4 +498,9 @@ export default { 495: 'Weryfikowanie', 496: 'w toku', 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 diff --git a/web/projects/shared/src/i18n/i18n.pipe.ts b/web/projects/shared/src/i18n/i18n.pipe.ts index 0c4bb3240..100c43f12 100644 --- a/web/projects/shared/src/i18n/i18n.pipe.ts +++ b/web/projects/shared/src/i18n/i18n.pipe.ts @@ -11,9 +11,11 @@ import { I18N, i18nKey } from './i18n.providers' export class i18nPipe implements PipeTransform { 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 - ? this.i18n()?.[ENGLISH[englishKey]] || englishKey + ? this.i18n()?.[ENGLISH[englishKey as i18nKey]] || englishKey : undefined } } diff --git a/web/projects/shared/src/services/dialog.service.ts b/web/projects/shared/src/services/dialog.service.ts index 9a542f87f..45abb13b2 100644 --- a/web/projects/shared/src/services/dialog.service.ts +++ b/web/projects/shared/src/services/dialog.service.ts @@ -3,7 +3,6 @@ import { TuiResponsiveDialogOptions, TuiResponsiveDialogService, } from '@taiga-ui/addon-mobile' -import { TuiAlertOptions } from '@taiga-ui/core' import { TUI_CONFIRM, TuiConfirmData } from '@taiga-ui/kit' import { PolymorpheusComponent } from '@taiga-ui/polymorpheus' import { PROMPT, PromptOptions } from '../components/prompt.component' @@ -63,7 +62,7 @@ export class DialogService { openAlert( message: i18nKey | undefined, - options: Partial> & { + options: Partial> & { label?: i18nKey } = {}, ) { diff --git a/web/projects/ui/src/app/app.component.ts b/web/projects/ui/src/app/app.component.ts index 6f4feeb5c..a652e97c6 100644 --- a/web/projects/ui/src/app/app.component.ts +++ b/web/projects/ui/src/app/app.component.ts @@ -1,6 +1,5 @@ import { Component, inject } from '@angular/core' import { takeUntilDestroyed } from '@angular/core/rxjs-interop' -import { Title } from '@angular/platform-browser' import { i18nService } from '@start9labs/shared' import { PatchDB } from 'patch-db-client' import { merge } from 'rxjs' @@ -29,7 +28,6 @@ import { PatchMonitorService } from './services/patch-monitor.service' `, }) export class AppComponent { - private readonly title = inject(Title) private readonly i18n = inject(i18nService) readonly subscription = merge( @@ -40,10 +38,9 @@ export class AppComponent { .subscribe() readonly ui = inject>(PatchDB) - .watch$('ui') + .watch$('ui', 'language') .pipe(takeUntilDestroyed()) - .subscribe(({ name, language }) => { - this.title.setTitle(name || 'StartOS') + .subscribe(language => { this.i18n.setLanguage(language || 'english') }) } diff --git a/web/projects/ui/src/app/app.providers.ts b/web/projects/ui/src/app/app.providers.ts index 5ac21f426..bc05d4b01 100644 --- a/web/projects/ui/src/app/app.providers.ts +++ b/web/projects/ui/src/app/app.providers.ts @@ -16,6 +16,7 @@ import { import { TUI_DATE_FORMAT, TUI_DIALOGS_CLOSE, + tuiAlertOptionsProvider, tuiButtonOptionsProvider, tuiDropdownOptionsProvider, tuiNumberFormatProvider, @@ -58,6 +59,9 @@ export const APP_PROVIDERS: Provider[] = [ tuiButtonOptionsProvider({ size: 'm' }), tuiTextfieldOptionsProvider({ hintOnDisabled: true }), tuiDropdownOptionsProvider({ appearance: 'start-os' }), + tuiAlertOptionsProvider({ + autoClose: appearance => (appearance === 'negative' ? 0 : 3000), + }), { provide: TUI_DATE_FORMAT, useValue: of({ diff --git a/web/projects/ui/src/app/components/toast-container.component.ts b/web/projects/ui/src/app/components/toast-container.component.ts index edf74664f..d0c6bdcbc 100644 --- a/web/projects/ui/src/app/components/toast-container.component.ts +++ b/web/projects/ui/src/app/components/toast-container.component.ts @@ -1,7 +1,6 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' import { NotificationsToastComponent } from './notifications-toast.component' import { RefreshAlertComponent } from './refresh-alert.component' -import { UpdateToastComponent } from './update-toast.component' @Component({ standalone: true, @@ -9,13 +8,8 @@ import { UpdateToastComponent } from './update-toast.component' template: ` - `, changeDetection: ChangeDetectionStrategy.OnPush, - imports: [ - NotificationsToastComponent, - UpdateToastComponent, - RefreshAlertComponent, - ], + imports: [NotificationsToastComponent, RefreshAlertComponent], }) export class ToastContainerComponent {} diff --git a/web/projects/ui/src/app/components/update-toast.component.ts b/web/projects/ui/src/app/components/update-toast.component.ts deleted file mode 100644 index a58a47cfb..000000000 --- a/web/projects/ui/src/app/components/update-toast.component.ts +++ /dev/null @@ -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: ` - - {{ - 'Restart your server for these updates to take effect. It can take several minutes to come back online.' - | i18n - }} -
- -
-
- `, - 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() - - readonly visible$: Observable = merge( - this.dismiss$, - inject>(PatchDB) - .watch$('serverInfo', 'statusInfo', 'updated') - .pipe(distinctUntilChanged(), filter(Boolean), endWith(false)), - ) - - onDismiss() { - this.dismiss$.next(false) - } - - async restart(): Promise { - 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() - } - } -} diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.ts b/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.ts index 0c9a77dcb..d7bce8a52 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/form/form-control/form-control.component.ts @@ -9,11 +9,11 @@ import { import { takeUntilDestroyed } from '@angular/core/rxjs-interop' import { AbstractTuiNullableControl } from '@taiga-ui/legacy' import { filter } from 'rxjs' -import { TuiDialogContext } from '@taiga-ui/core' +import { TuiAlertService, TuiDialogContext } from '@taiga-ui/core' import { IST } from '@start9labs/start-sdk' import { ERRORS } from '../form-group/form-group.component' import { FORM_CONTROL_PROVIDERS } from './form-control.providers' -import { DialogService, i18nKey } from '@start9labs/shared' +import { DialogService, i18nKey, i18nPipe } from '@start9labs/shared' @Component({ selector: 'form-control', @@ -26,6 +26,9 @@ export class FormControlComponent< T extends Exclude, V, > extends AbstractTuiNullableControl { + private readonly alerts = inject(TuiAlertService) + private readonly i18n = inject(i18nPipe) + @Input({ required: true }) spec!: T @@ -35,7 +38,6 @@ export class FormControlComponent< warned = false focused = false readonly order = ERRORS - private readonly dialog = inject(DialogService) get immutable(): boolean { return 'immutable' in this.spec && this.spec.immutable @@ -50,9 +52,9 @@ export class FormControlComponent< const previous = this.value if (!this.warned && this.warning) { - this.dialog - .openAlert(this.warning as unknown as i18nKey, { - label: 'Warning', + this.alerts + .open(this.warning, { + label: this.i18n.transform('Warning'), appearance: 'warning', closeable: false, autoClose: 0, diff --git a/web/projects/ui/src/app/routes/portal/components/form/form-toggle/form-toggle.component.html b/web/projects/ui/src/app/routes/portal/components/form/form-toggle/form-toggle.component.html index 9cd61288d..8b49e15c9 100644 --- a/web/projects/ui/src/app/routes/portal/components/form/form-toggle/form-toggle.component.html +++ b/web/projects/ui/src/app/routes/portal/components/form/form-toggle/form-toggle.component.html @@ -7,6 +7,7 @@ type="checkbox" size="m" [disabled]="!!spec.disabled || readOnly" + [showIcons]="false" [(ngModel)]="value" (blur)="onFocus(false)" /> diff --git a/web/projects/ui/src/app/routes/portal/components/header/navigation.component.ts b/web/projects/ui/src/app/routes/portal/components/header/navigation.component.ts index 1753a1aad..cfdbe370a 100644 --- a/web/projects/ui/src/app/routes/portal/components/header/navigation.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/header/navigation.component.ts @@ -26,7 +26,7 @@ import { getMenu } from 'src/app/utils/system-utilities' [tuiHintShowDelay]="1000" [routerLink]="item.routerLink" [class.link_system]="item.routerLink === '/portal/system'" - [tuiHint]="!rla.isActive ? item.name : ''" + [tuiHint]="rla.isActive ? '' : (item.name | i18n)" > @for (address of clearnet(); track $index) { - {{ address.acme | acme }} + + {{ + interface.serviceInterface().addSsl + ? (address.acme | acme) + : '-' + }} + {{ address.url | mask }} - - + @if (address.isDomain) { + + } + @if (address.isDomain) { + + } } @@ -144,7 +154,7 @@ export class InterfaceClearnetComponent { readonly interface = inject(InterfaceComponent) readonly isPublic = computed(() => this.interface.serviceInterface().public) - readonly clearnet = input.required() + readonly clearnet = input.required() readonly acme = toSignal( inject>(PatchDB) .watch$('serverInfo', 'network', 'acme') @@ -152,7 +162,7 @@ export class InterfaceClearnetComponent { { initialValue: [] }, ) - async remove({ url }: AddressDetails) { + async remove({ url }: ClearnetAddress) { const confirm = await firstValueFrom( this.dialog .openConfirm({ label: 'Are you sure?', size: 's' }) @@ -213,33 +223,37 @@ export class InterfaceClearnetComponent { } 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, + ), + default: '', + }) + this.formDialog.open>(FormComponent, { label: 'Select Domain', data: { spec: await configBuilderToSpec( - ISB.InputSpec.of({ - 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], - }), - 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, - ), - default: '', - }), - }), + ISB.InputSpec.of( + this.interface.serviceInterface().addSsl + ? { domain, acme } + : { domain }, + ), ), buttons: [ { diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts index 935f5ce80..4e2ebc1e8 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/interface.utils.ts @@ -12,9 +12,9 @@ export function getAddresses( host: T.Host, config: ConfigService, ): { - clearnet: (AddressDetails & { acme: string | null })[] - local: AddressDetails[] - tor: AddressDetails[] + clearnet: ClearnetAddress[] + local: LocalAddress[] + tor: TorAddress[] } { const addressInfo = serviceInterface.addressInfo const hostnames = @@ -46,9 +46,9 @@ export function getAddresses( } } - const clearnet: (AddressDetails & { acme: string | null })[] = [] - const local: AddressDetails[] = [] - const tor: AddressDetails[] = [] + const clearnet: ClearnetAddress[] = [] + const local: LocalAddress[] = [] + const tor: TorAddress[] = [] hostnames.forEach(h => { const addresses = utils.addressHostToUrl(addressInfo, h) @@ -56,26 +56,28 @@ export function getAddresses( addresses.forEach(url => { if (h.kind === 'onion') { tor.push({ - label: - addresses.length > 1 - ? new URL(url).protocol.replace(':', '').toUpperCase() - : '', + protocol: new URL(url).protocol.replace(':', '').toUpperCase(), url, }) } else { const hostnameKind = h.hostname.kind - if (h.public) { + if ( + h.public || + (hostnameKind === 'domain' && host.domains[h.hostname.domain]?.public) + ) { clearnet.push({ url, + disabled: !h.public, + isDomain: hostnameKind == 'domain', acme: hostnameKind == 'domain' ? 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 { local.push({ - label: + nid: hostnameKind === 'local' ? 'Local' : `${h.networkInterfaceId} (${hostnameKind})`, @@ -103,16 +105,28 @@ export function getAddresses( } export type MappedServiceInterface = T.ServiceInterface & { + addSsl?: T.AddSslOptions | null public: boolean addresses: { - clearnet: AddressDetails[] - local: AddressDetails[] - tor: AddressDetails[] + clearnet: ClearnetAddress[] + local: LocalAddress[] + tor: TorAddress[] } } -export type AddressDetails = { - label?: string +export type ClearnetAddress = { 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 } diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/local.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/local.component.ts index 696db6974..068634762 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/local.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/local.component.ts @@ -3,7 +3,7 @@ import { TuiIcon, TuiLink } from '@taiga-ui/core' import { TuiTooltip } from '@taiga-ui/kit' import { TableComponent } from 'src/app/routes/portal/components/table.component' import { InterfaceActionsComponent } from './actions.component' -import { AddressDetails } from './interface.utils' +import { LocalAddress } from './interface.utils' import { MaskPipe } from './mask.pipe' import { DocsLinkDirective, i18nPipe } from '@start9labs/shared' @@ -27,7 +27,7 @@ import { DocsLinkDirective, i18nPipe } from '@start9labs/shared' @for (address of local(); track $index) { - + @@ -48,5 +48,5 @@ import { DocsLinkDirective, i18nPipe } from '@start9labs/shared' changeDetection: ChangeDetectionStrategy.OnPush, }) export class InterfaceLocalComponent { - readonly local = input.required() + readonly local = input.required() } diff --git a/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts b/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts index e01f10617..7ecfb0f44 100644 --- a/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts +++ b/web/projects/ui/src/app/routes/portal/components/interfaces/tor.component.ts @@ -32,7 +32,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' import { FormDialogService } from 'src/app/services/form-dialog.service' import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' import { InterfaceActionsComponent } from './actions.component' -import { AddressDetails } from './interface.utils' +import { TorAddress } from './interface.utils' import { MaskPipe } from './mask.pipe' type OnionForm = { @@ -70,7 +70,7 @@ type OnionForm = {
{{ address.label }}{{ address.nid }} {{ address.url | mask }}
@for (address of tor(); track $index) { - + -
{{ address.label }}{{ address.protocol || '-' }}
{{ address.url | mask }} @@ -140,9 +140,9 @@ export class InterfaceTorComponent { private readonly interface = inject(InterfaceComponent) private readonly i18n = inject(i18nPipe) - readonly tor = input.required() + readonly tor = input.required() - async remove({ url }: AddressDetails) { + async remove({ url }: TorAddress) { const confirm = await firstValueFrom( this.dialog .openConfirm({ label: 'Are you sure?', size: 's' }) diff --git a/web/projects/ui/src/app/routes/portal/portal.component.ts b/web/projects/ui/src/app/routes/portal/portal.component.ts index b5c300b3c..7189585af 100644 --- a/web/projects/ui/src/app/routes/portal/portal.component.ts +++ b/web/projects/ui/src/app/routes/portal/portal.component.ts @@ -1,22 +1,56 @@ -import { CommonModule } from '@angular/common' -import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { + ChangeDetectionStrategy, + Component, + inject, + signal, +} from '@angular/core' +import { toSignal } from '@angular/core/rxjs-interop' 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 { 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 { HeaderComponent } from './components/header/header.component' @Component({ standalone: true, template: ` -
{{ name$ | async }}
+
{{ name() }}
+ @if (update(); as update) { + + @if (update === true) { + + Download complete, restart to apply changes + } @else if ( + update.overall && update.overall !== true && update.overall.total + ) { + + Downloading: + {{ getProgress(update.overall.total, update.overall.done) }}% + } @else { + + Calculating download size + } + @if (update === true) { + + } + + } `, styles: [ ` @@ -47,13 +81,39 @@ import { HeaderComponent } from './components/header/header.component' ], changeDetection: ChangeDetectionStrategy.OnPush, imports: [ - CommonModule, RouterOutlet, HeaderComponent, TabsComponent, TuiScrollbar, + TuiActionBar, + TuiProgress, + TuiLoader, + TuiIcon, + TuiButton, ], }) export class PortalComponent { - readonly name$ = inject>(PatchDB).watch$('ui', 'name') + private readonly loader = inject(LoadingService) + private readonly errorService = inject(ErrorService) + private readonly patch = inject>(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() + } + } } diff --git a/web/projects/ui/src/app/routes/portal/portal.routes.ts b/web/projects/ui/src/app/routes/portal/portal.routes.ts index e38e6c8c4..cb1538380 100644 --- a/web/projects/ui/src/app/routes/portal/portal.routes.ts +++ b/web/projects/ui/src/app/routes/portal/portal.routes.ts @@ -1,8 +1,7 @@ -import { ActivatedRouteSnapshot, Routes } from '@angular/router' -import { PackageDataEntry } from '../../services/patch-db/data-model' -import { getManifest } from '../../utils/get-package-data' -import { SYSTEM_UTILITIES } from '../../utils/system-utilities' -import { toRouterLink } from '../../utils/to-router-link' +import { Routes } from '@angular/router' +import { SYSTEM_UTILITIES } from 'src/app/utils/system-utilities' +import { titleResolver } from 'src/app/utils/title-resolver' +import { toRouterLink } from 'src/app/utils/to-router-link' import { PortalComponent } from './portal.component' const ROUTES: Routes = [ @@ -17,54 +16,56 @@ const ROUTES: Routes = [ }, { path: 'services', + data: { title: 'Services' }, + title: titleResolver, loadChildren: () => import('./routes/services/services.routes'), }, // @TODO 041 // { - // title: systemTabResolver, + // title: titleResolver, // path: 'backups', // loadComponent: () => import('./routes/backups/backups.component'), // data: toNavigationItem('/portal/backups'), // }, { - title: systemTabResolver, + title: titleResolver, path: 'logs', loadComponent: () => import('./routes/logs/logs.component'), data: toNavigationItem('/portal/logs'), }, { - title: systemTabResolver, + title: titleResolver, path: 'marketplace', loadChildren: () => import('./routes/marketplace/marketplace.routes'), data: toNavigationItem('/portal/marketplace'), }, { - title: systemTabResolver, + title: titleResolver, path: 'system', loadChildren: () => import('./routes/system/system.routes'), data: toNavigationItem('/portal/system'), }, { - title: systemTabResolver, + title: titleResolver, path: 'notifications', loadComponent: () => import('./routes/notifications/notifications.component'), data: toNavigationItem('/portal/notifications'), }, { - title: systemTabResolver, + title: titleResolver, path: 'sideload', loadComponent: () => import('./routes/sideload/sideload.component'), data: toNavigationItem('/portal/sideload'), }, { - title: systemTabResolver, + title: titleResolver, path: 'updates', loadComponent: () => import('./routes/updates/updates.component'), data: toNavigationItem('/portal/updates'), }, { - title: systemTabResolver, + title: titleResolver, path: 'metrics', loadComponent: () => import('./routes/metrics/metrics.component'), data: toNavigationItem('/portal/metrics'), @@ -75,26 +76,12 @@ const ROUTES: Routes = [ export default ROUTES -function systemTabResolver({ data }: ActivatedRouteSnapshot): string { - return data['title'] -} +function toNavigationItem(id: string) { + const { icon, title } = SYSTEM_UTILITIES[id] || {} -function toNavigationItem( - id: string, - packages: Record = {}, -) { - 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, - } + return { + icon, + title, + routerLink: toRouterLink(id), + } } diff --git a/web/projects/ui/src/app/routes/portal/routes/backups/modals/edit.component.ts b/web/projects/ui/src/app/routes/portal/routes/backups/modals/edit.component.ts index 3a990a048..53e358f3f 100644 --- a/web/projects/ui/src/app/routes/portal/routes/backups/modals/edit.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/backups/modals/edit.component.ts @@ -63,7 +63,13 @@ import { TARGET, TARGET_CREATE } from './target.component'
Also Execute Now - +
{{ info.description }} + @if (info.public) { - + } @else { - + } @@ -90,6 +78,10 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model' text-transform: uppercase; } + tui-icon { + font-size: 1rem; + } + :host-context(tui-root._mobile) { display: grid; grid-template-columns: repeat(3, min-content) 1fr 2rem; @@ -100,15 +92,11 @@ import { PackageDataEntry } from 'src/app/services/patch-db/data-model' td { padding: 0; } - - .hosting { - font-size: 0; - } } `, changeDetection: ChangeDetectionStrategy.OnPush, standalone: true, - imports: [TuiButton, TuiBadge, TuiLink, RouterLink, i18nPipe], + imports: [TuiButton, TuiBadge, TuiLink, TuiIcon, RouterLink, i18nPipe], }) export class ServiceInterfaceComponent { private readonly config = inject(ConfigService) @@ -138,7 +126,7 @@ export class ServiceInterfaceComponent { get href(): string | null { return this.disabled - ? 'null' + ? null : this.config.launchableAddress(this.info, this.pkg.hosts) } } diff --git a/web/projects/ui/src/app/routes/portal/routes/services/components/interfaces.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/components/interfaces.component.ts index 3518c5bd2..a4c649852 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/components/interfaces.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/components/interfaces.component.ts @@ -62,10 +62,12 @@ export class ServiceInterfacesComponent { .sort((a, b) => tuiDefaultSort(a[1], b[1])) .map(([id, value]) => { const host = hosts[value.addressInfo.hostId] + const port = value.addressInfo.internalPort return { ...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) : {}, routerLink: `./interface/${id}`, } diff --git a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/controls.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/controls.component.ts index 2aaaa9d64..fc4afcc79 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/controls.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/controls.component.ts @@ -74,7 +74,7 @@ export class ControlsComponent { this.errors.getPkgDepErrors$(this.manifest().id).pipe( map(errors => Object.keys(this.pkg().currentDependencies) - .map(id => errors[id]) + .map(id => errors?.[id]) .some(Boolean), ), ), diff --git a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/dashboard.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/dashboard.component.ts index 01c92dbb0..a2057c775 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/dashboard.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/dashboard.component.ts @@ -1,6 +1,8 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { toSignal } from '@angular/core/rxjs-interop' +import { RouterLink } from '@angular/router' 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 { DepErrorService } from 'src/app/services/dep-error.service' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' @@ -15,47 +17,73 @@ import { i18nPipe } from '@start9labs/shared' standalone: true, template: ` {{ 'Services' | i18n }} - - - - - - - - - - - - - @for (pkg of services() | tuiTableSort; track $index) { - - } @empty { - - - - } - -
- {{ 'Name' | i18n }} - {{ 'Version' | i18n }} - {{ 'Uptime' | i18n }} - - {{ 'Status' | i18n }} - - {{ 'Controls' | i18n }} -
- {{ - services() - ? ('No services installed' | i18n) - : ('Loading' | i18n) - }} -
+ @if (!services()) { + + } @else { + @if (!services()?.length) { + + + + + + + + + + + + + @for (pkg of services() | tuiTableSort; track $index) { + + } + +
+ {{ 'Name' | i18n }} + {{ 'Version' | i18n }} + {{ 'Uptime' | i18n }} + + {{ 'Status' | i18n }} + + {{ 'Controls' | i18n }} +
+ } @else { +
+
+ {{ 'Welcome to' | i18n }} + StartOS +
+

+ {{ + 'To get started, visit the Marketplace and download your first service' + | i18n + }} +

+ + {{ 'View Marketplace' | i18n }} + +
+ } + } `, styles: ` + @keyframes slide { + 50% { + margin-block-start: 0; + } + + 55% { + margin-block-start: -1em; + } + + 100% { + margin-block-start: -1em; + } + } + :host { position: relative; font-size: 1rem; @@ -65,6 +93,31 @@ import { i18nPipe } from '@start9labs/shared' :host-context(tui-root._mobile) { 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' }, imports: [ @@ -73,6 +126,9 @@ import { i18nPipe } from '@start9labs/shared' TuiTable, TitleDirective, i18nPipe, + TuiLoader, + TuiButton, + RouterLink, ], changeDetection: ChangeDetectionStrategy.OnPush, }) diff --git a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/status.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/status.component.ts index 859fc46a9..bad6671c1 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/dashboard/status.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/dashboard/status.component.ts @@ -71,7 +71,7 @@ export class StatusComponent { get status(): i18nKey { 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) { @@ -108,7 +108,6 @@ export class StatusComponent { case 'backingUp': case 'restarting': case 'removing': - case 'restoring': return '...' default: return '' diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts index 6a9967bda..76bde4805 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/actions.component.ts @@ -96,7 +96,7 @@ export default class ServiceActionsRoute { readonly rebuild = { name: this.i18n.transform('Rebuild Service')!, 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', )!, } diff --git a/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts b/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts index d34dec8f7..8d4ddd1f6 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/routes/interface.component.ts @@ -86,14 +86,16 @@ export default class ServiceInterfaceRoute { const item = serviceInterfaces[this.interfaceId()] const key = item?.addressInfo.hostId || '' const host = hosts[key] + const port = item?.addressInfo.internalPort - if (!host || !item) { + if (!host || !item || !port) { return } return { ...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), } }) diff --git a/web/projects/ui/src/app/routes/portal/routes/services/services.routes.ts b/web/projects/ui/src/app/routes/portal/routes/services/services.routes.ts index 16aec3a31..228452d44 100644 --- a/web/projects/ui/src/app/routes/portal/routes/services/services.routes.ts +++ b/web/projects/ui/src/app/routes/portal/routes/services/services.routes.ts @@ -4,6 +4,7 @@ import { MarkdownComponent } from '@start9labs/shared' import { defer, map, Observable, of } from 'rxjs' import { share } from 'rxjs/operators' 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 { ServiceRoute } from './routes/service.component' @@ -11,6 +12,7 @@ import { ServiceRoute } from './routes/service.component' export const ROUTES: Routes = [ { path: ':pkgId', + title: titleResolver, component: ServiceOutletComponent, children: [ { diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/network.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/network.component.ts index 0ddc88479..15b606eb7 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/network.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/network.component.ts @@ -189,18 +189,10 @@ export class BackupNetworkComponent { select(target: MappedBackupTarget) { if (!target.entry.mountable) { - this.dialog - .openAlert(ERROR, { - appearance: 'negative', - label: 'Unable to connect', - autoClose: 0, - }) - .subscribe() + this.dialog.openAlert(ERROR, { label: 'Unable to connect' }).subscribe() } else if (this.type === 'restore' && !target.hasAnyBackup) { this.dialog - .openAlert('Network Folder does not contain a valid backup', { - appearance: 'negative', - }) + .openAlert('Network Folder does not contain a valid backup') .subscribe() } else { this.networkFolders.emit(target) diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/physical.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/physical.component.ts index 36fac8734..e7234effb 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/physical.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/backups/physical.component.ts @@ -132,9 +132,7 @@ export class BackupPhysicalComponent { select(target: MappedBackupTarget) { if (this.type === 'restore' && !target.hasAnyBackup) { this.dialog - .openAlert('Drive partition does not contain a valid backup', { - appearance: 'negative', - }) + .openAlert('Drive partition does not contain a valid backup') .subscribe() } else { this.physicalFolders.emit(target) diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts index 4fefa5654..2dfe9ff71 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/email/email.component.ts @@ -187,25 +187,16 @@ export default class SystemEmailComponent { async sendTestEmail(value: typeof inputSpec.constants.customSmtp._TYPE) { const loader = this.loader.open('Sending email').subscribe() + const success = + `${this.i18n.transform('A test email has been sent to')} ${this.testAddress}.

${this.i18n.transform('Check your spam folder and mark as not spam.')}` as i18nKey try { - await this.api.testSmtp({ - to: this.testAddress, - ...value, - }) + await this.api.testSmtp({ to: this.testAddress, ...value }) + this.dialog.openAlert(success, { label: 'Success' }).subscribe() } catch (e: any) { this.errorService.handleError(e) } finally { loader.unsubscribe() } - - this.dialog - .openAlert( - `${this.i18n.transform('A test email has been sent to')} ${this.testAddress}.

${this.i18n.transform('Check your spam folder and mark as not spam.')}` as i18nKey, - { - label: 'Success', - }, - ) - .subscribe() } } diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts index d78a0e8da..4b94c0f77 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/general/general.component.ts @@ -337,7 +337,6 @@ export default class SystemGeneralComponent { try { await this.api.resetTor({ wipeState, reason: 'User triggered' }) - this.dialog.openAlert('Tor reset in progress').subscribe() } catch (e: any) { this.errorService.handleError(e) diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/interfaces/interfaces.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/interfaces/interfaces.component.ts index c663842bb..a185ce1f7 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/interfaces/interfaces.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/interfaces/interfaces.component.ts @@ -80,12 +80,16 @@ export default class StartOsUiComponent { inject>(PatchDB) .watch$('serverInfo', 'network', 'host') .pipe( - map(host => ({ - ...this.iface, - public: - !!host.bindings[this.iface.addressInfo.internalPort]?.net.public, - addresses: getAddresses(this.iface, host, this.config), - })), + map(host => { + const port = this.iface.addressInfo.internalPort + + return { + ...this.iface, + addSsl: host.bindings[port]?.options.addSsl, + public: !!host.bindings[port]?.net.public, + addresses: getAddresses(this.iface, host, this.config), + } + }), ), ) diff --git a/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.component.ts b/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.component.ts index 62c2683bf..4e4335520 100644 --- a/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.component.ts +++ b/web/projects/ui/src/app/routes/portal/routes/system/routes/wifi/wifi.component.ts @@ -66,6 +66,7 @@ import { wifiSpec } from './wifi.const' type="checkbox" tuiSwitch [style.margin-inline-start]="'auto'" + [showIcons]="false" [ngModel]="status()?.enabled" (ngModelChange)="onToggle($event)" /> diff --git a/web/projects/ui/src/app/services/action.service.ts b/web/projects/ui/src/app/services/action.service.ts index b71812acd..51cf57906 100644 --- a/web/projects/ui/src/app/services/action.service.ts +++ b/web/projects/ui/src/app/services/action.service.ts @@ -89,9 +89,7 @@ export class ActionService { this.dialog .openAlert( `${this.i18n.transform('Action can only be executed when service is')} ${statusesStr}` as i18nKey, - { - label: 'Forbidden', - }, + { label: 'Forbidden' }, ) .pipe(filter(Boolean)) .subscribe() diff --git a/web/projects/ui/src/app/services/os.service.ts b/web/projects/ui/src/app/services/os.service.ts index aea7ac21c..5e29d53c2 100644 --- a/web/projects/ui/src/app/services/os.service.ts +++ b/web/projects/ui/src/app/services/os.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core' +import { inject, Injectable } from '@angular/core' import { PatchDB } from 'patch-db-client' import { BehaviorSubject, @@ -17,11 +17,14 @@ import { RR } from './api/api.types' providedIn: 'root', }) export class OSService { + private readonly api = inject(ApiService) + private readonly patch = inject>(PatchDB) + osUpdate?: RR.CheckOsUpdateRes - updateAvailable$ = new BehaviorSubject(false) + readonly updateAvailable$ = new BehaviorSubject(false) readonly updating$ = this.patch.watch$('serverInfo', 'statusInfo').pipe( - map(status => !!status.updateProgress || status.updated), + map(status => status.updateProgress ?? status.updated), distinctUntilChanged(), ) @@ -35,21 +38,12 @@ export class OSService { readonly updatingOrBackingUp$ = combineLatest([ this.updating$, this.backingUp$, - ]).pipe(map(([updating, backingUp]) => updating || backingUp)) + ]).pipe(map(([updating, backingUp]) => !!updating || backingUp)) readonly showUpdate$ = combineLatest([ this.updateAvailable$, this.updating$, - ]).pipe( - map(([available, updating]) => { - return available && !updating - }), - ) - - constructor( - private readonly api: ApiService, - private readonly patch: PatchDB, - ) {} + ]).pipe(map(([available, updating]) => available && !updating)) async loadOS(): Promise { const { version, id } = await getServerInfo(this.patch) @@ -59,9 +53,11 @@ export class OSService { registry: startosRegistry, serverId: id, }) - const [latestVersion, _] = Object.entries(this.osUpdate).at(-1)! - const updateAvailable = - Version.parse(latestVersion).compare(Version.parse(version)) === 'greater' - this.updateAvailable$.next(updateAvailable) + + const [latest, _] = Object.entries(this.osUpdate).at(-1)! + + this.updateAvailable$.next( + Version.parse(latest).compare(Version.parse(version)) === 'greater', + ) } } diff --git a/web/projects/ui/src/app/services/state.service.ts b/web/projects/ui/src/app/services/state.service.ts index c20df8a14..6be7e8b82 100644 --- a/web/projects/ui/src/app/services/state.service.ts +++ b/web/projects/ui/src/app/services/state.service.ts @@ -1,7 +1,8 @@ import { inject, Injectable } from '@angular/core' 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 { TuiAlertService } from '@taiga-ui/core' import { BehaviorSubject, combineLatest, @@ -41,7 +42,8 @@ const OPTIONS: IsActiveMatchOptions = { providedIn: 'root', }) export class StateService extends Observable { - private readonly dialog = inject(DialogService) + private readonly alerts = inject(TuiAlertService) + private readonly i18n = inject(i18nPipe) private readonly api = inject(ApiService) private readonly router = inject(Router) private readonly network$ = inject(NetworkService) @@ -91,10 +93,10 @@ export class StateService extends Observable { .pipe( exhaustMap(() => concat( - this.dialog - .openAlert('Trying to reach server', { - label: 'State unknown', - autoClose: 0, + this.alerts + .open(this.i18n.transform('Trying to reach server'), { + label: this.i18n.transform('State unknown'), + closeable: false, appearance: 'negative', }) .pipe( @@ -104,8 +106,8 @@ export class StateService extends Observable { ), ), ), - this.dialog.openAlert('Connection restored', { - label: 'Server connected', + this.alerts.open(this.i18n.transform('Connection restored'), { + label: this.i18n.transform('Server connected'), appearance: 'positive', }), ), diff --git a/web/projects/ui/src/app/utils/get-package-data.ts b/web/projects/ui/src/app/utils/get-package-data.ts index b929069d1..60bdc0bba 100644 --- a/web/projects/ui/src/app/utils/get-package-data.ts +++ b/web/projects/ui/src/app/utils/get-package-data.ts @@ -23,7 +23,7 @@ export async function getAllPackages( } export function getManifest(pkg: PackageDataEntry): T.Manifest { - return isInstalling(pkg) + return isInstalling(pkg) || isRestoring(pkg) ? pkg.stateInfo.installingInfo.newManifest : pkg.stateInfo.manifest! } diff --git a/web/projects/ui/src/app/utils/title-resolver.ts b/web/projects/ui/src/app/utils/title-resolver.ts new file mode 100644 index 000000000..a4bed3349 --- /dev/null +++ b/web/projects/ui/src/app/utils/title-resolver.ts @@ -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 { + let route = inject(i18nPipe).transform(data['title']) + + const patch = inject>(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}` +}