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",
"@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"

View File

@@ -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",

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
}
}

View File

@@ -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<T = void>(
message: i18nKey | undefined,
options: Partial<TuiAlertOptions<any>> & {
options: Partial<TuiResponsiveDialogOptions<any>> & {
label?: i18nKey
} = {},
) {

View File

@@ -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<DataModel>>(PatchDB)
.watch$('ui')
.watch$('ui', 'language')
.pipe(takeUntilDestroyed())
.subscribe(({ name, language }) => {
this.title.setTitle(name || 'StartOS')
.subscribe(language => {
this.i18n.setLanguage(language || 'english')
})
}

View File

@@ -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({

View File

@@ -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: `
<notifications-toast />
<refresh-alert />
<update-toast />
`,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
NotificationsToastComponent,
UpdateToastComponent,
RefreshAlertComponent,
],
imports: [NotificationsToastComponent, RefreshAlertComponent],
})
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 { 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<IST.ValueSpec, IST.ValueSpecHidden>,
V,
> extends AbstractTuiNullableControl<V> {
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<boolean>(this.warning as unknown as i18nKey, {
label: 'Warning',
this.alerts
.open<boolean>(this.warning, {
label: this.i18n.transform('Warning'),
appearance: 'warning',
closeable: false,
autoClose: 0,

View File

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

View File

@@ -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)"
>
<tui-badged-content
[style.--tui-radius.%]="50"

View File

@@ -4,12 +4,8 @@ import {
inject,
input,
} from '@angular/core'
import {
CopyService,
DialogService,
i18nKey,
i18nPipe,
} from '@start9labs/shared'
import { CopyService, DialogService, i18nPipe } from '@start9labs/shared'
import { TUI_IS_MOBILE } from '@taiga-ui/cdk'
import {
TuiButton,
tuiButtonOptionsProvider,
@@ -114,6 +110,7 @@ import { InterfaceComponent } from './interface.component'
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class InterfaceActionsComponent {
readonly isMobile = inject(TUI_IS_MOBILE)
readonly dialog = inject(DialogService)
readonly copyService = inject(CopyService)
readonly interface = inject(InterfaceComponent)
@@ -124,7 +121,7 @@ export class InterfaceActionsComponent {
this.dialog
.openComponent(new PolymorpheusComponent(QRModal), {
size: 'auto',
label: this.actions() as i18nKey,
closeable: this.isMobile,
data: this.actions(),
})
.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 { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { InterfaceActionsComponent } from './actions.component'
import { AddressDetails } from './interface.utils'
import { ClearnetAddress } from './interface.utils'
import { MaskPipe } from './mask.pipe'
type ClearnetForm = {
@@ -85,25 +85,35 @@ type ClearnetForm = {
<table [appTable]="['ACME', 'URL', null]">
@for (address of clearnet(); track $index) {
<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 [actions]="address.url">
<button
tuiButton
appearance="primary-destructive"
[style.margin-inline-end.rem]="0.5"
(click)="remove(address)"
>
{{ 'Delete' | i18n }}
</button>
<button
tuiOption
tuiAppearance="action-destructive"
iconStart="@tui.trash"
(click)="remove(address)"
>
{{ 'Delete' | i18n }}
</button>
@if (address.isDomain) {
<button
tuiButton
appearance="primary-destructive"
[style.margin-inline-end.rem]="0.5"
(click)="remove(address)"
>
{{ 'Delete' | i18n }}
</button>
}
@if (address.isDomain) {
<button
tuiOption
tuiAppearance="action-destructive"
iconStart="@tui.trash"
(click)="remove(address)"
>
{{ 'Delete' | i18n }}
</button>
}
</td>
</tr>
}
@@ -144,7 +154,7 @@ export class InterfaceClearnetComponent {
readonly interface = inject(InterfaceComponent)
readonly isPublic = computed(() => this.interface.serviceInterface().public)
readonly clearnet = input.required<readonly AddressDetails[]>()
readonly clearnet = input.required<readonly ClearnetAddress[]>()
readonly acme = toSignal(
inject<PatchDB<DataModel>>(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<string, string>,
),
default: '',
})
this.formDialog.open<FormContext<ClearnetForm>>(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<string, string>,
),
default: '',
}),
}),
ISB.InputSpec.of(
this.interface.serviceInterface().addSsl
? { domain, acme }
: { domain },
),
),
buttons: [
{

View File

@@ -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
}

View File

@@ -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'
<table [appTable]="['Network Interface', 'URL', null]">
@for (address of local(); track $index) {
<tr>
<td [style.width.rem]="12">{{ address.label }}</td>
<td [style.width.rem]="12">{{ address.nid }}</td>
<td>{{ address.url | mask }}</td>
<td [actions]="address.url"></td>
</tr>
@@ -48,5 +48,5 @@ import { DocsLinkDirective, i18nPipe } from '@start9labs/shared'
changeDetection: ChangeDetectionStrategy.OnPush,
})
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 { 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 = {
<table [appTable]="['Protocol', 'URL', null]">
@for (address of tor(); track $index) {
<tr>
<td [style.width.rem]="12">{{ address.label }}</td>
<td [style.width.rem]="12">{{ address.protocol || '-' }}</td>
<td>
<div [tuiFluidTypography]="[0.625, 0.8125]" tuiFade>
{{ 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 AddressDetails[]>()
readonly tor = input.required<readonly TorAddress[]>()
async remove({ url }: AddressDetails) {
async remove({ url }: TorAddress) {
const confirm = await firstValueFrom(
this.dialog
.openConfirm({ label: 'Are you sure?', size: 's' })

View File

@@ -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: `
<header appHeader>{{ name$ | async }}</header>
<header appHeader>{{ name() }}</header>
<main>
<tui-scrollbar [style.max-height.%]="100">
<router-outlet />
</tui-scrollbar>
</main>
<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: [
`
@@ -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<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 { 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<string, PackageDataEntry> = {},
) {
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),
}
}

View File

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

View File

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

View File

@@ -4,7 +4,8 @@ import {
inject,
signal,
} 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 { RR, ServerNotifications } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
@@ -66,18 +67,23 @@ import { NotificationsTableComponent } from './table.component'
],
})
export default class NotificationsComponent {
private readonly router = inject(Router)
private readonly route = inject(ActivatedRoute)
readonly service = inject(NotificationService)
readonly api = inject(ApiService)
readonly errorService = inject(ErrorService)
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
ngOnInit() {
this.getMore({})
}
async getMore(params: RR.GetNotificationsReq) {
try {
this.notifications.set(undefined)

View File

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

View File

@@ -7,7 +7,7 @@ import {
import { RouterLink } from '@angular/router'
import { i18nPipe } from '@start9labs/shared'
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 { ConfigService } from 'src/app/services/config.service'
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'">
{{ info.description }}
</td>
<td>
<td [style.text-align]="'center'">
@if (info.public) {
<a
class="hosting"
tuiLink
iconStart="@tui.globe"
appearance="positive"
[textContent]="'Public'"
></a>
<tui-icon class="g-positive" icon="@tui.globe" />
} @else {
<a
class="hosting"
tuiLink
iconStart="@tui.lock"
appearance="negative"
[textContent]="'Private'"
></a>
<tui-icon class="g-negative" icon="@tui.lock" />
}
</td>
<td [style.grid-area]="'span 2'">
@@ -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)
}
}

View File

@@ -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}`,
}

View File

@@ -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),
),
),

View File

@@ -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: `
<ng-container *title>{{ 'Services' | i18n }}</ng-container>
<table tuiTable class="g-table" [(sorter)]="sorter">
<thead>
<tr>
<th [style.width.rem]="3"></th>
<th tuiTh [requiredSort]="true" [sorter]="name">
{{ 'Name' | i18n }}
</th>
<th tuiTh>{{ 'Version' | i18n }}</th>
<th tuiTh [requiredSort]="true" [sorter]="uptime">
{{ 'Uptime' | i18n }}
</th>
<th tuiTh [requiredSort]="true" [sorter]="status">
{{ 'Status' | i18n }}
</th>
<th [style.width.rem]="8" [style.text-indent.rem]="1.5">
{{ 'Controls' | i18n }}
</th>
</tr>
</thead>
<tbody>
@for (pkg of services() | tuiTableSort; track $index) {
<tr
appService
[pkg]="pkg"
[depErrors]="errors()?.[(pkg | toManifest).id]"
></tr>
} @empty {
<tr>
<td colspan="6">
{{
services()
? ('No services installed' | i18n)
: ('Loading' | i18n)
}}
</td>
</tr>
}
</tbody>
</table>
@if (!services()) {
<tui-loader [style.height.%]="100" [textContent]="'Loading' | i18n" />
} @else {
@if (!services()?.length) {
<table tuiTable class="g-table" [(sorter)]="sorter">
<thead>
<tr>
<th [style.width.rem]="3"></th>
<th tuiTh [requiredSort]="true" [sorter]="name">
{{ 'Name' | i18n }}
</th>
<th tuiTh>{{ 'Version' | i18n }}</th>
<th tuiTh [requiredSort]="true" [sorter]="uptime">
{{ 'Uptime' | i18n }}
</th>
<th tuiTh [requiredSort]="true" [sorter]="status">
{{ 'Status' | i18n }}
</th>
<th [style.width.rem]="8" [style.text-indent.rem]="1.5">
{{ 'Controls' | i18n }}
</th>
</tr>
</thead>
<tbody>
@for (pkg of services() | tuiTableSort; track $index) {
<tr
appService
[pkg]="pkg"
[depErrors]="errors()?.[(pkg | toManifest).id]"
></tr>
}
</tbody>
</table>
} @else {
<section>
<div>
{{ 'Welcome to' | i18n }}
<span>StartOS</span>
</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: `
@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,
})

View File

@@ -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 ''

View File

@@ -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',
)!,
}

View File

@@ -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),
}
})

View File

@@ -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: [
{

View File

@@ -189,18 +189,10 @@ export class BackupNetworkComponent {
select(target: MappedBackupTarget<CifsBackupTarget>) {
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)

View File

@@ -132,9 +132,7 @@ export class BackupPhysicalComponent {
select(target: MappedBackupTarget<DiskBackupTarget>) {
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)

View File

@@ -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}.<br /><br /><b>${this.i18n.transform('Check your spam folder and mark as not spam.')}</b>` 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}.<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 {
await this.api.resetTor({ wipeState, reason: 'User triggered' })
this.dialog.openAlert('Tor reset in progress').subscribe()
} catch (e: any) {
this.errorService.handleError(e)

View File

@@ -80,12 +80,16 @@ export default class StartOsUiComponent {
inject<PatchDB<DataModel>>(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),
}
}),
),
)

View File

@@ -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)"
/>

View File

@@ -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()

View File

@@ -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<DataModel>>(PatchDB)
osUpdate?: RR.CheckOsUpdateRes
updateAvailable$ = new BehaviorSubject<boolean>(false)
readonly updateAvailable$ = new BehaviorSubject<boolean>(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<DataModel>,
) {}
]).pipe(map(([available, updating]) => available && !updating))
async loadOS(): Promise<void> {
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',
)
}
}

View File

@@ -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<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 router = inject(Router)
private readonly network$ = inject(NetworkService)
@@ -91,10 +93,10 @@ export class StateService extends Observable<RR.ServerState | null> {
.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<RR.ServerState | null> {
),
),
),
this.dialog.openAlert('Connection restored', {
label: 'Server connected',
this.alerts.open(this.i18n.transform('Connection restored'), {
label: this.i18n.transform('Server connected'),
appearance: 'positive',
}),
),

View File

@@ -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!
}

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}`
}