feat: finalize desktop and mobile design of system routes (#2855)

* feat: finalize desktop and mobile design of system routes

* clean up messaging and mobile tabbar utilities

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
This commit is contained in:
Alex Inkin
2025-03-27 16:41:47 +04:00
committed by GitHub
parent 5318cccc5f
commit e6af7e9885
30 changed files with 701 additions and 546 deletions

200
web/package-lock.json generated
View File

@@ -25,17 +25,17 @@
"@noble/hashes": "^1.4.0", "@noble/hashes": "^1.4.0",
"@start9labs/argon2": "^0.2.2", "@start9labs/argon2": "^0.2.2",
"@start9labs/start-sdk": "file:../sdk/baseDist", "@start9labs/start-sdk": "file:../sdk/baseDist",
"@taiga-ui/addon-charts": "4.28.0", "@taiga-ui/addon-charts": "4.30.0",
"@taiga-ui/addon-commerce": "4.28.0", "@taiga-ui/addon-commerce": "4.30.0",
"@taiga-ui/addon-mobile": "4.28.0", "@taiga-ui/addon-mobile": "4.30.0",
"@taiga-ui/addon-table": "4.28.0", "@taiga-ui/addon-table": "4.30.0",
"@taiga-ui/cdk": "4.28.0", "@taiga-ui/cdk": "4.30.0",
"@taiga-ui/core": "4.28.0", "@taiga-ui/core": "4.30.0",
"@taiga-ui/event-plugins": "4.4.1", "@taiga-ui/event-plugins": "4.5.0",
"@taiga-ui/icons": "4.28.0", "@taiga-ui/icons": "4.30.0",
"@taiga-ui/kit": "4.28.0", "@taiga-ui/kit": "4.30.0",
"@taiga-ui/layout": "4.28.0", "@taiga-ui/layout": "4.30.0",
"@taiga-ui/legacy": "4.28.0", "@taiga-ui/legacy": "4.30.0",
"@taiga-ui/polymorpheus": "4.9.0", "@taiga-ui/polymorpheus": "4.9.0",
"@tinkoff/ng-dompurify": "4.0.0", "@tinkoff/ng-dompurify": "4.0.0",
"ansi-to-html": "^0.7.2", "ansi-to-html": "^0.7.2",
@@ -3422,9 +3422,9 @@
} }
}, },
"node_modules/@maskito/angular": { "node_modules/@maskito/angular": {
"version": "3.4.0", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/@maskito/angular/-/angular-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@maskito/angular/-/angular-3.5.0.tgz",
"integrity": "sha512-iMFP/siEgU9Ki+g1PReZlA5+LlBMp6inqXGG5KCezhmDleZnG5lL9gxk3+ktJvKu+2kayLcwyBeUKXPwMBVt9w==", "integrity": "sha512-5uwar32qsGdZNHUgZpFnICg9tJKCXbZEGk2ZnchHzDIfN5ojNT7wKzoq8NhpRlGb3p4qQCE+PXb5GERkcWM/Sw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -3433,35 +3433,35 @@
"peerDependencies": { "peerDependencies": {
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@angular/forms": ">=16.0.0", "@angular/forms": ">=16.0.0",
"@maskito/core": "^3.4.0" "@maskito/core": "^3.5.0"
} }
}, },
"node_modules/@maskito/core": { "node_modules/@maskito/core": {
"version": "3.4.0", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/@maskito/core/-/core-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@maskito/core/-/core-3.5.0.tgz",
"integrity": "sha512-gFM6qk675YwOEGhxu9Xm6/sl1TZBRab6+B3Gstqml7xJopHHZ0rUOrWXwmX0z2JI+1PsgUL/ftV/CSZ8CpIONg==", "integrity": "sha512-zgmBjXeXc7BSBaw8jQw25dnwkFmKDvdj5rHzhEIxYhgGtnpli236F0YWPIOYzIwADjbefwDq1o7qpJfMsdDO4Q==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true "peer": true
}, },
"node_modules/@maskito/kit": { "node_modules/@maskito/kit": {
"version": "3.4.0", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-3.5.0.tgz",
"integrity": "sha512-jkexr7wjAqFeMpyc7s0IlinL+3F9xC4BYUHDQcEqlAJisDgVFtGCZZK/RvV1C+HGDn2gtzzVrJ3G/OY66k6EXg==", "integrity": "sha512-QnpZsPTINgK4ScA4pMMJagoj+ufIXc/VGOP61AsQa/H/lmXII4pEZTLzpmMNUYmCEIEyjHR2DIbfEed04sktvQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"peerDependencies": { "peerDependencies": {
"@maskito/core": "^3.4.0" "@maskito/core": "^3.5.0"
} }
}, },
"node_modules/@maskito/phone": { "node_modules/@maskito/phone": {
"version": "3.4.0", "version": "3.5.0",
"resolved": "https://registry.npmjs.org/@maskito/phone/-/phone-3.4.0.tgz", "resolved": "https://registry.npmjs.org/@maskito/phone/-/phone-3.5.0.tgz",
"integrity": "sha512-KR6JuuWhTumIOCUV3CzPhh1niCXcuqsogNsLW3YfdmeVo8GygS9isnHNbSaAA/b9OnmIEkh25mur6x3yEJuYjA==", "integrity": "sha512-qh/GGRFn8cZBY/JUTLa5yeSSKSVlekggKeiCbf0eX0I53/HM2pNZ/5667S8SXwn5WjIEeB79Eltl8MNvK74yvA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"peerDependencies": { "peerDependencies": {
"@maskito/core": "^3.4.0", "@maskito/core": "^3.5.0",
"@maskito/kit": "^3.4.0", "@maskito/kit": "^3.5.0",
"libphonenumber-js": ">=1.0.0" "libphonenumber-js": ">=1.0.0"
} }
}, },
@@ -4417,9 +4417,9 @@
"link": true "link": true
}, },
"node_modules/@taiga-ui/addon-charts": { "node_modules/@taiga-ui/addon-charts": {
"version": "4.28.0", "version": "4.30.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.28.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-4.30.0.tgz",
"integrity": "sha512-Lvi2R8Y50kBbfbru31YHon+CEpnOzAx0G4GnqjN2goTLNQ6iX7pgUeyRyiXI4ay1yLrzVIOZJhSmBwWSDocZEg==", "integrity": "sha512-QrM2Oh4hUcg/I0K3KWFkc/dbTCYZn2n5GU2FSpZaK6I7pwjfRoMjBU7vswPLVVdmgeWTJxxoQlbfYnbUbkMAJw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4428,15 +4428,15 @@
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@ng-web-apis/common": "^4.11.1", "@ng-web-apis/common": "^4.11.1",
"@taiga-ui/cdk": "^4.28.0", "@taiga-ui/cdk": "^4.30.0",
"@taiga-ui/core": "^4.28.0", "@taiga-ui/core": "^4.30.0",
"@taiga-ui/polymorpheus": "^4.8.0" "@taiga-ui/polymorpheus": "^4.9.0"
} }
}, },
"node_modules/@taiga-ui/addon-commerce": { "node_modules/@taiga-ui/addon-commerce": {
"version": "4.28.0", "version": "4.30.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.28.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/addon-commerce/-/addon-commerce-4.30.0.tgz",
"integrity": "sha512-VYygBL7oySCZYLBimGJPx/VGGtUGhpes3XwBHAPBmmyiVxct0kxXzhCQdAvNMQcSvDzXDBjg3wmJiUbZA/uHGQ==", "integrity": "sha512-6diktxvxMpWjbEHXThS0pTrURdUiF/47jf2jdBFkMwX3BbbekisM1qkwxY24V7q8fN0IIxfO8CVEjTeLRrCw5g==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4445,22 +4445,22 @@
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@angular/forms": ">=16.0.0", "@angular/forms": ">=16.0.0",
"@maskito/angular": "^3.4.0", "@maskito/angular": "^3.5.0",
"@maskito/core": "^3.4.0", "@maskito/core": "^3.5.0",
"@maskito/kit": "^3.4.0", "@maskito/kit": "^3.5.0",
"@ng-web-apis/common": "^4.11.1", "@ng-web-apis/common": "^4.11.1",
"@taiga-ui/cdk": "^4.28.0", "@taiga-ui/cdk": "^4.30.0",
"@taiga-ui/core": "^4.28.0", "@taiga-ui/core": "^4.30.0",
"@taiga-ui/i18n": "^4.28.0", "@taiga-ui/i18n": "^4.30.0",
"@taiga-ui/kit": "^4.28.0", "@taiga-ui/kit": "^4.30.0",
"@taiga-ui/polymorpheus": "^4.8.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/addon-mobile": { "node_modules/@taiga-ui/addon-mobile": {
"version": "4.28.0", "version": "4.30.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.28.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/addon-mobile/-/addon-mobile-4.30.0.tgz",
"integrity": "sha512-1RRaX37Ddl24q4nHrMEz6iDqHWi/mkTyXQ+kADX7+ydx9JkbU2H4R+qXrOx4+GUi93Y05HvAWCNCToBu3Ytt2A==", "integrity": "sha512-8cYyU0UDLUd74v+Zjs4m9S4AsSWchUojAexDLvaAHzfi0x+tdtA+ZN0h49v8AmOWHK0v69z4FMjyyc52p/jiDw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4470,18 +4470,18 @@
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@ng-web-apis/common": "^4.11.1", "@ng-web-apis/common": "^4.11.1",
"@taiga-ui/cdk": "^4.28.0", "@taiga-ui/cdk": "^4.30.0",
"@taiga-ui/core": "^4.28.0", "@taiga-ui/core": "^4.30.0",
"@taiga-ui/kit": "^4.28.0", "@taiga-ui/kit": "^4.30.0",
"@taiga-ui/layout": "^4.28.0", "@taiga-ui/layout": "^4.30.0",
"@taiga-ui/polymorpheus": "^4.8.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/addon-table": { "node_modules/@taiga-ui/addon-table": {
"version": "4.28.0", "version": "4.30.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.28.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/addon-table/-/addon-table-4.30.0.tgz",
"integrity": "sha512-C8MW6kJ3T9zy51rSxqYApll+S84oizK6C85gZyDM3gEV2RAlK2DP+r657ZlEwEgobrFCtBZe++TT7ZoKpQBBHg==", "integrity": "sha512-OdCEwlrMs42Z2pINK1wvNk7OZmAlkj+mbgHTyMGdrUdA49dFZfYXNpVUCwVOqHAm2PDOeVN4ybZ8FSbzYefJyw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4490,18 +4490,18 @@
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@ng-web-apis/intersection-observer": "^4.11.1", "@ng-web-apis/intersection-observer": "^4.11.1",
"@taiga-ui/cdk": "^4.28.0", "@taiga-ui/cdk": "^4.30.0",
"@taiga-ui/core": "^4.28.0", "@taiga-ui/core": "^4.30.0",
"@taiga-ui/i18n": "^4.28.0", "@taiga-ui/i18n": "^4.30.0",
"@taiga-ui/kit": "^4.28.0", "@taiga-ui/kit": "^4.30.0",
"@taiga-ui/polymorpheus": "^4.8.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/cdk": { "node_modules/@taiga-ui/cdk": {
"version": "4.28.0", "version": "4.30.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.28.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-4.30.0.tgz",
"integrity": "sha512-P2vK+4WDnSt/nnilqxvDS4lyMAEH/M73z9YSzyH5mEwVTNxD3m82jJgpHqV5Re7geooAyaKqS6MJwDxaN0+9eQ==", "integrity": "sha512-ndfnLOnL6vriItm5lq8/0slzj03CatkGVYG8zAT3fx00Vuam5Wf8Sh6h2ObqCFAljT7WJxHqMF9A1cBfLPI/iQ==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": "2.8.1" "tslib": "2.8.1"
@@ -4525,14 +4525,14 @@
"@ng-web-apis/resize-observer": "^4.11.1", "@ng-web-apis/resize-observer": "^4.11.1",
"@ng-web-apis/screen-orientation": "^4.11.1", "@ng-web-apis/screen-orientation": "^4.11.1",
"@taiga-ui/event-plugins": "^4.4.1", "@taiga-ui/event-plugins": "^4.4.1",
"@taiga-ui/polymorpheus": "^4.8.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/core": { "node_modules/@taiga-ui/core": {
"version": "4.28.0", "version": "4.30.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.28.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-4.30.0.tgz",
"integrity": "sha512-4eP6PJvmHZCrV/9apxfu6Bgj7L72yjVg1R5c4j1MsVmMESLCCRGlk0hPPvuxVQ+ZYrOZwNeWyKHPZDPL5uQawA==", "integrity": "sha512-IeZ6QBpSuv7k4bQx2BSDr8N3dDiMDwgnnwkkKqtJ0yJayZ/ZlCMq3nUQA0kg3VjH2spJeUbdqkDqpEuzrWJGkA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4546,17 +4546,17 @@
"@angular/router": ">=16.0.0", "@angular/router": ">=16.0.0",
"@ng-web-apis/common": "^4.11.1", "@ng-web-apis/common": "^4.11.1",
"@ng-web-apis/mutation-observer": "^4.11.1", "@ng-web-apis/mutation-observer": "^4.11.1",
"@taiga-ui/cdk": "^4.28.0", "@taiga-ui/cdk": "^4.30.0",
"@taiga-ui/event-plugins": "^4.4.1", "@taiga-ui/event-plugins": "^4.4.1",
"@taiga-ui/i18n": "^4.28.0", "@taiga-ui/i18n": "^4.30.0",
"@taiga-ui/polymorpheus": "^4.8.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/event-plugins": { "node_modules/@taiga-ui/event-plugins": {
"version": "4.4.1", "version": "4.5.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/event-plugins/-/event-plugins-4.4.1.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/event-plugins/-/event-plugins-4.5.0.tgz",
"integrity": "sha512-gwEkgyZsbAdRfmb98KlKWivYVF88eP0bOtbHwfj8Ec8DgJ5809qFqeWvJEIxZZ829iox1m8z2UuVrqN2/tI1tQ==", "integrity": "sha512-bMW36eqr4Q+EnUM8ZNjx1Sw8POIAcyALY74xVPq9UHoQ3NqnRkeEDnZdfPhq9IYxtC3sO2BttNjWYcvBAkU2+A==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
@@ -4568,9 +4568,9 @@
} }
}, },
"node_modules/@taiga-ui/i18n": { "node_modules/@taiga-ui/i18n": {
"version": "4.28.0", "version": "4.30.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.28.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-4.30.0.tgz",
"integrity": "sha512-kM7bbqllzir4nEk3X+YMKATm23UoKJeWSGmwnjLEmhWkpNAGqfErDRbE2puf+jXy7eufGhaB7ht/mK4+HkLXbw==", "integrity": "sha512-OvtUqSRQE988XfiH1MS7Wd3Eg6dE1mkP2sqYRLw0HyE5Oc9hgHMwdPstSaoMN9aeJRVZnKXGsYmX4iaQ3x7drw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true, "peer": true,
"dependencies": { "dependencies": {
@@ -4583,18 +4583,18 @@
} }
}, },
"node_modules/@taiga-ui/icons": { "node_modules/@taiga-ui/icons": {
"version": "4.28.0", "version": "4.30.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.28.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-4.30.0.tgz",
"integrity": "sha512-1QS7gvYHuTRUUodE58OXm+4Ree5FhFe0co0Lj+3sqeqkYb495z5q3CXBNiXD3y8IcDTjNuYkxKxEthbPnQrsVQ==", "integrity": "sha512-EAbvw1ii4UVDgt9+5t7NQkV0WBqkVm5SGixH0ux8Vb4qhhLJJwp5xvXOCGt5QPzviT7nFGqXD6EqB23aYcuusg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": "^2.3.0" "tslib": "^2.3.0"
} }
}, },
"node_modules/@taiga-ui/kit": { "node_modules/@taiga-ui/kit": {
"version": "4.28.0", "version": "4.30.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.28.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-4.30.0.tgz",
"integrity": "sha512-JEHUZhWU0vgPorvO3l9POzWKPbFQA57jFh9Iv5/RlWxMI8EUI+OKH5J8z1ptX+RJE2dWB9+Yi84zasgr8TWcSA==", "integrity": "sha512-tCHZbsiq1u19ariarFuP9iwnNSxJGicQnYvJYy2+QojL65KsC9p8VgZv36rpggpuPEUXRXwmhyz2Qi6fwFcbLg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4604,25 +4604,25 @@
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@angular/forms": ">=16.0.0", "@angular/forms": ">=16.0.0",
"@angular/router": ">=16.0.0", "@angular/router": ">=16.0.0",
"@maskito/angular": "^3.4.0", "@maskito/angular": "^3.5.0",
"@maskito/core": "^3.4.0", "@maskito/core": "^3.5.0",
"@maskito/kit": "^3.4.0", "@maskito/kit": "^3.5.0",
"@maskito/phone": "^3.4.0", "@maskito/phone": "^3.5.0",
"@ng-web-apis/common": "^4.11.1", "@ng-web-apis/common": "^4.11.1",
"@ng-web-apis/intersection-observer": "^4.11.1", "@ng-web-apis/intersection-observer": "^4.11.1",
"@ng-web-apis/mutation-observer": "^4.11.1", "@ng-web-apis/mutation-observer": "^4.11.1",
"@ng-web-apis/resize-observer": "^4.11.1", "@ng-web-apis/resize-observer": "^4.11.1",
"@taiga-ui/cdk": "^4.28.0", "@taiga-ui/cdk": "^4.30.0",
"@taiga-ui/core": "^4.28.0", "@taiga-ui/core": "^4.30.0",
"@taiga-ui/i18n": "^4.28.0", "@taiga-ui/i18n": "^4.30.0",
"@taiga-ui/polymorpheus": "^4.8.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/layout": { "node_modules/@taiga-ui/layout": {
"version": "4.28.0", "version": "4.30.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.28.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/layout/-/layout-4.30.0.tgz",
"integrity": "sha512-NlXdEmXGhYvTWeSSpGlT9XS0SU1aQDuFAMFBSDVsZqLPWh2DTnNsxSf1/b6UYMmX5JKXhH/bRVvX97N5L5XZqQ==", "integrity": "sha512-DyIqpmXcv/OP4byt7L1f1iBKPysf3L+sj/dBpkeYvAUUnJnXnJsXav0j57d43VkXPn9lpGqz0gEBtzVDt7xxTw==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"
@@ -4630,17 +4630,17 @@
"peerDependencies": { "peerDependencies": {
"@angular/common": ">=16.0.0", "@angular/common": ">=16.0.0",
"@angular/core": ">=16.0.0", "@angular/core": ">=16.0.0",
"@taiga-ui/cdk": "^4.28.0", "@taiga-ui/cdk": "^4.30.0",
"@taiga-ui/core": "^4.28.0", "@taiga-ui/core": "^4.30.0",
"@taiga-ui/kit": "^4.28.0", "@taiga-ui/kit": "^4.30.0",
"@taiga-ui/polymorpheus": "^4.8.0", "@taiga-ui/polymorpheus": "^4.9.0",
"rxjs": ">=7.0.0" "rxjs": ">=7.0.0"
} }
}, },
"node_modules/@taiga-ui/legacy": { "node_modules/@taiga-ui/legacy": {
"version": "4.28.0", "version": "4.30.0",
"resolved": "https://registry.npmjs.org/@taiga-ui/legacy/-/legacy-4.28.0.tgz", "resolved": "https://registry.npmjs.org/@taiga-ui/legacy/-/legacy-4.30.0.tgz",
"integrity": "sha512-mWE5w7alYsT8GMBNTfcvrf/sJjh1li2/mTykH/aoWklgYHHmSt6moY4Myi8wKdlRFBzi82eXsvJcUSCwD8Y5ew==", "integrity": "sha512-ebFJMddzlsq3TUAWxopn5Qju4REkC4bHzoYYx5OEzPq1VW1zmCvNC+X6usMnluhc9aS50UI8ZB7Xd3N4Zdgtfg==",
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": ">=2.8.1" "tslib": ">=2.8.1"

View File

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

View File

@@ -162,20 +162,3 @@ tui-badge-notification {
align-self: center !important; align-self: center !important;
} }
} }
// TODO Remove after Taiga UI update
[tuiTitle] {
h1,
h2,
h3,
h4,
h5,
h6 {
font: inherit;
margin: 0;
}
[tuiSubtitle] {
margin: 0;
}
}

View File

@@ -1,4 +1,4 @@
import { signal } from '@angular/core' import { forwardRef, signal } from '@angular/core'
import { tuiCreateToken, tuiProvide } from '@taiga-ui/cdk' import { tuiCreateToken, tuiProvide } from '@taiga-ui/cdk'
import { import {
TuiLanguageName, TuiLanguageName,
@@ -34,5 +34,8 @@ export const I18N_PROVIDERS = [
} }
}, },
}, },
tuiProvide(TuiLanguageSwitcherService, i18nService), tuiProvide(
TuiLanguageSwitcherService,
forwardRef(() => i18nService),
),
] ]

View File

@@ -87,9 +87,7 @@ import { InterfaceComponent } from './interface.component'
:host { :host {
text-align: right; text-align: right;
grid-area: 1 / 2 / 3 / 3; grid-area: 1 / 2 / 3 / 3;
} place-content: center;
.desktop {
} }
.mobile { .mobile {

View File

@@ -19,14 +19,6 @@ import { MappedServiceInterface } from './interface.utils'
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
} }
section {
padding-block-end: 1rem;
}
:host-context(tui-root:not(._mobile)) section ::ng-deep > header {
background: none;
}
`, `,
providers: [tuiButtonOptionsProvider({ size: 'xs' })], providers: [tuiButtonOptionsProvider({ size: 'xs' })],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,

View File

@@ -13,6 +13,11 @@ import { ChangeDetectionStrategy, Component, input } from '@angular/core'
</thead> </thead>
<tbody><ng-content /></tbody> <tbody><ng-content /></tbody>
`, `,
styles: `
:host:has(app-placeholder) thead {
display: none;
}
`,
host: { class: 'g-table' }, host: { class: 'g-table' },
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })

View File

@@ -10,9 +10,7 @@ import { RouterLink, RouterLinkActive } from '@angular/router'
import { TuiResponsiveDialogService, TuiTabBar } from '@taiga-ui/addon-mobile' import { TuiResponsiveDialogService, TuiTabBar } from '@taiga-ui/addon-mobile'
import { TuiIcon } from '@taiga-ui/core' import { TuiIcon } from '@taiga-ui/core'
import { TuiBadgeNotification } from '@taiga-ui/kit' import { TuiBadgeNotification } from '@taiga-ui/kit'
import { ABOUT } from 'src/app/routes/portal/components/header/about.component'
import { BadgeService } from 'src/app/services/badge.service' import { BadgeService } from 'src/app/services/badge.service'
import { RESOURCES } from 'src/app/utils/resources'
import { getMenu } from 'src/app/utils/system-utilities' import { getMenu } from 'src/app/utils/system-utilities'
const FILTER = ['/portal/services', '/portal/system', '/portal/marketplace'] const FILTER = ['/portal/services', '/portal/system', '/portal/marketplace']
@@ -72,20 +70,6 @@ const FILTER = ['/portal/services', '/portal/system', '/portal/marketplace']
} }
</a> </a>
} }
<button class="item" (click)="about()">
<tui-icon icon="@tui.info" />
About this server
</button>
@for (link of resources; track $index) {
<a class="item" target="_blank" rel="noreferrer" [href]="link.href">
<tui-icon [icon]="link.icon" />
{{ link.name }}
<tui-icon
icon="@tui.external-link"
[style.margin-inline-start]="'auto'"
/>
</a>
}
</ng-template> </ng-template>
</button> </button>
</nav> </nav>
@@ -138,7 +122,6 @@ export class TabsComponent {
index = 3 index = 3
readonly resources = RESOURCES
readonly menu = getMenu().filter(item => !FILTER.includes(item.routerLink)) readonly menu = getMenu().filter(item => !FILTER.includes(item.routerLink))
readonly badge = toSignal(inject(BadgeService).getCount('/portal/system'), { readonly badge = toSignal(inject(BadgeService).getCount('/portal/system'), {
initialValue: 0, initialValue: 0,
@@ -148,10 +131,6 @@ export class TabsComponent {
this.menu.reduce((acc, item) => acc + item.badge(), 0), this.menu.reduce((acc, item) => acc + item.badge(), 0),
) )
about() {
this.dialogs.open(ABOUT, { label: 'About this server' }).subscribe()
}
more(content: TemplateRef<any>) { more(content: TemplateRef<any>) {
this.dialogs.open(content, { label: 'Start OS' }).subscribe({ this.dialogs.open(content, { label: 'Start OS' }).subscribe({
complete: () => this.update(), complete: () => this.update(),

View File

@@ -55,6 +55,10 @@ import { ServicesService } from './services.service'
font-size: 1rem; font-size: 1rem;
overflow: hidden; overflow: hidden;
} }
:host-context(tui-root._mobile) {
padding: 0;
}
`, `,
host: { class: 'g-page' }, host: { class: 'g-page' },
imports: [ServiceComponent, ToManifestPipe, TuiTable, TitleDirective], imports: [ServiceComponent, ToManifestPipe, TuiTable, TitleDirective],

View File

@@ -2,8 +2,8 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop' import { toSignal } from '@angular/core/rxjs-interop'
import { ErrorService, LoadingService } from '@start9labs/shared' import { ErrorService, LoadingService } from '@start9labs/shared'
import { ISB, utils } from '@start9labs/start-sdk' import { ISB, utils } from '@start9labs/start-sdk'
import { TuiButton, TuiLoader, TuiTitle } from '@taiga-ui/core' import { TuiButton, TuiLink, TuiLoader, TuiTitle } from '@taiga-ui/core'
import { TuiCell } from '@taiga-ui/layout' import { TuiCell, TuiHeader } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { map } from 'rxjs' import { map } from 'rxjs'
import { FormComponent } from 'src/app/routes/portal/components/form.component' import { FormComponent } from 'src/app/routes/portal/components/form.component'
@@ -12,11 +12,28 @@ import { FormDialogService } from 'src/app/services/form-dialog.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { knownACME, toAcmeName } from 'src/app/utils/acme' import { knownACME, toAcmeName } from 'src/app/utils/acme'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { AcmeInfoComponent } from './info.component'
@Component({ @Component({
template: ` template: `
<acme-info /> <header tuiHeader>
<hgroup tuiTitle>
<h3>ACME</h3>
<p tuiSubtitle>
Add ACME providers in order to generate SSL (https) certificates for
clearnet access.
<a
tuiLink
href="https://docs.start9.com/latest/user-manual/acme"
target="_blank"
rel="noreferrer"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[pseudo]="true"
[textContent]="'View instructions'"
></a>
</p>
</hgroup>
</header>
<section class="g-card"> <section class="g-card">
<header> <header>
Saved Providers Saved Providers
@@ -62,9 +79,14 @@ import { AcmeInfoComponent } from './info.component'
} }
</section> </section>
`, `,
styles: `
:host {
max-width: 40rem;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [TuiButton, TuiLoader, TuiCell, TuiTitle, AcmeInfoComponent], imports: [TuiButton, TuiLoader, TuiCell, TuiTitle, TuiHeader, TuiLink],
}) })
export default class SystemAcmeComponent { export default class SystemAcmeComponent {
private readonly formDialog = inject(FormDialogService) private readonly formDialog = inject(FormDialogService)

View File

@@ -1,24 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { TuiLink, TuiNotification } from '@taiga-ui/core'
@Component({
selector: 'acme-info',
template: `
<tui-notification>
Register with one or more ACME providers such as Let's Encrypt in order to
generate SSL (https) certificates on-demand for clearnet hosting.
<a
tuiLink
href="https://docs.start9.com/latest/user-manual/acme"
target="_blank"
rel="noreferrer"
iconEnd="@tui.external-link"
[textContent]="'View instructions'"
></a>
</tui-notification>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TuiNotification, TuiLink],
})
export class AcmeInfoComponent {}

View File

@@ -1,21 +1,32 @@
import { AsyncPipe } from '@angular/common' import { AsyncPipe, DatePipe } from '@angular/common'
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
inject, inject,
OnInit, OnInit,
} from '@angular/core' } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { ActivatedRoute, RouterLink } from '@angular/router' import { ActivatedRoute, RouterLink } from '@angular/router'
import { UnitConversionPipesModule } from '@start9labs/shared' import { UnitConversionPipesModule } from '@start9labs/shared'
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile' import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
import { TuiButton, TuiLink, TuiLoader } from '@taiga-ui/core' import { TuiMapperPipe } from '@taiga-ui/cdk'
import { BACKUP } from 'src/app/routes/portal/routes/system/routes/backups/backup.component' import {
TuiButton,
TuiLink,
TuiLoader,
TuiNotification,
TuiTitle,
} from '@taiga-ui/core'
import { TuiHeader } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client'
import { import {
CifsBackupTarget, CifsBackupTarget,
DiskBackupTarget, DiskBackupTarget,
} from 'src/app/services/api/api.types' } from 'src/app/services/api/api.types'
import { EOSService } from 'src/app/services/eos.service' import { EOSService } from 'src/app/services/eos.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service' import { TitleDirective } from 'src/app/services/title.service'
import { BACKUP } from './backup.component'
import { BackupService, MappedBackupTarget } from './backup.service' import { BackupService, MappedBackupTarget } from './backup.service'
import { BackupNetworkComponent } from './network.component' import { BackupNetworkComponent } from './network.component'
import { BackupPhysicalComponent } from './physical.component' import { BackupPhysicalComponent } from './physical.component'
@@ -29,6 +40,53 @@ import { BACKUP_RESTORE } from './restore.component'
{{ type === 'create' ? 'Create Backup' : 'Restore Backup' }} {{ type === 'create' ? 'Create Backup' : 'Restore Backup' }}
</ng-container> </ng-container>
<header tuiHeader>
<hgroup tuiTitle>
<h3>{{ type === 'create' ? 'Create Backup' : 'Restore Backup' }}</h3>
<p tuiSubtitle>
@if (type === 'create') {
Back up StartOS and service data by connecting to a device on your
local network or a physical drive connected to your server.
<a
tuiLink
href="https://docs.start9.com/0.3.5.x/user-manual/backups/backup-create"
target="_blank"
rel="noreferrer"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[pseudo]="true"
[textContent]="'View instructions'"
></a>
} @else {
Restore StartOS and service data from a device on your local network
or a physical drive connected to your server that contains an
existing backup.
<a
tuiLink
href="https://docs.start9.com/0.3.5.x/user-manual/backups/backup-restore"
target="_blank"
rel="noreferrer"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[pseudo]="true"
[textContent]="'View instructions'"
></a>
}
</p>
</hgroup>
</header>
@if (type === 'create' && server(); as s) {
<tui-notification [appearance]="s.lastBackup | tuiMapper: toAppearance">
<div tuiTitle>
Last Backup
<div tuiSubtitle>
{{ s.lastBackup ? (s.lastBackup | date: 'medium') : 'never' }}
</div>
</div>
</tui-notification>
}
@if (type === 'create' && (eos.backingUp$ | async)) { @if (type === 'create' && (eos.backingUp$ | async)) {
<section backupProgress></section> <section backupProgress></section>
} @else { } @else {
@@ -40,8 +98,7 @@ import { BACKUP_RESTORE } from './restore.component'
/> />
} @else { } @else {
<section (networkFolders)="onTarget($event)"> <section (networkFolders)="onTarget($event)">
{{ text }} A folder on another computer that is connected to the same network as
a folder on another computer that is connected to the same network as
your Start9 server. View the your Start9 server. View the
<a <a
tuiLink tuiLink
@@ -53,17 +110,7 @@ import { BACKUP_RESTORE } from './restore.component'
></a> ></a>
</section> </section>
<section (physicalFolders)="onTarget($event)"> <section (physicalFolders)="onTarget($event)">
{{ text }} A physical drive that is plugged directly into your Start9 Server.
a physical drive that is plugged directly into your Start9 Server.
View the
<a
tuiLink
href="https://docs.start9.com/0.3.5.x/user-manual/backups/backup-create#physical-drive"
target="_blank"
rel="noreferrer"
iconEnd="@tui.external-link"
[textContent]="'Instructions'"
></a>
</section> </section>
} }
} }
@@ -71,15 +118,20 @@ import { BACKUP_RESTORE } from './restore.component'
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [ imports: [
AsyncPipe,
DatePipe,
RouterLink, RouterLink,
TuiButton, TuiButton,
TuiLoader, TuiLoader,
TuiLink, TuiLink,
TuiHeader,
TuiTitle,
TuiNotification,
TuiMapperPipe,
TitleDirective, TitleDirective,
UnitConversionPipesModule, UnitConversionPipesModule,
BackupNetworkComponent, BackupNetworkComponent,
BackupPhysicalComponent, BackupPhysicalComponent,
AsyncPipe,
BackupProgressComponent, BackupProgressComponent,
], ],
}) })
@@ -88,11 +140,25 @@ export default class SystemBackupComponent implements OnInit {
readonly type = inject(ActivatedRoute).snapshot.data['type'] readonly type = inject(ActivatedRoute).snapshot.data['type']
readonly service = inject(BackupService) readonly service = inject(BackupService)
readonly eos = inject(EOSService) readonly eos = inject(EOSService)
readonly server = toSignal(
inject<PatchDB<DataModel>>(PatchDB).watch$('serverInfo'),
)
get text() { readonly toAppearance = (lastBackup: string | null) => {
return this.type === 'create' if (!lastBackup) return 'negative'
? 'Backup server to'
: 'Restore your services from' const currentDate = new Date().valueOf()
const backupDate = new Date(lastBackup).valueOf()
const diff = currentDate - backupDate
const week = 604800000
if (diff <= week) {
return 'positive'
} else if (diff > week && diff <= week * 2) {
return 'warning'
} else {
return 'negative'
}
} }
ngOnInit() { ngOnInit() {

View File

@@ -14,6 +14,7 @@ import { TuiCell } from '@taiga-ui/layout'
import { filter } from 'rxjs' import { filter } from 'rxjs'
import { FormComponent } from 'src/app/routes/portal/components/form.component' import { FormComponent } from 'src/app/routes/portal/components/form.component'
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component' import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { CifsBackupTarget, RR } from 'src/app/services/api/api.types' import { CifsBackupTarget, RR } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service' import { FormDialogService } from 'src/app/services/form-dialog.service'
@@ -37,69 +38,132 @@ const ERROR =
</button> </button>
</header> </header>
@for (target of service.cifs(); track $index) { <table [appTable]="['Status', 'Name', 'Hostname', 'Path', '']">
<button tuiCell (click)="select(target)"> @for (target of service.cifs(); track $index) {
<tui-icon icon="@tui.folder-open" /> <tr
<span tuiTitle> tabindex="0"
<strong>{{ target.entry.path.split('/').pop() }}</strong> (click)="select(target)"
@if (target.entry.mountable) { (keydown.enter)="select(target)"
<span tuiSubtitle [backupStatus]="target.hasAnyBackup"></span>
} @else {
<span tuiSubtitle>
<tui-icon
icon="@tui.signal-high"
class="g-negative"
[style.font-size.rem]="1"
/>
Unable to connect
</span>
}
<span tuiSubtitle>
<b>Hostname:</b>
{{ target.entry.hostname }}
</span>
<span tuiSubtitle>
<b>Path:</b>
{{ target.entry.path }}
</span>
</span>
<button
tuiIconButton
appearance="action-destructive"
iconStart="@tui.trash"
(click.stop)="forget(target, $index)"
> >
Forget <td>
</button> @if (target.entry.mountable) {
<button <span [backupStatus]="target.hasAnyBackup"></span>
tuiIconButton } @else {
appearance="icon" <span>
size="xs" <tui-icon
iconStart="@tui.pencil" icon="@tui.signal-high"
(click.stop)="edit(target)" class="g-negative"
> [style.font-size.rem]="1"
Edit />
</button> Unable to connect
</button> </span>
} @empty { }
<app-placeholder icon="@tui.folder-x">No network folders</app-placeholder> </td>
} <td class="name">{{ target.entry.path.split('/').pop() }}</td>
<td>{{ target.entry.hostname }}</td>
<td>{{ target.entry.path }}</td>
<td>
<button
tuiIconButton
size="s"
appearance="action-destructive"
iconStart="@tui.trash"
(click.stop)="forget(target, $index)"
>
Forget
</button>
<button
tuiIconButton
appearance="icon"
size="xs"
iconStart="@tui.pencil"
(click.stop)="edit(target)"
>
Edit
</button>
</td>
</tr>
} @empty {
<tr>
<td colspan="5">
<app-placeholder icon="@tui.folder-x">
No network folders
</app-placeholder>
</td>
</tr>
}
</table>
`, `,
styles: ` styles: `
@import '@taiga-ui/core/styles/taiga-ui-local';
tr {
cursor: pointer;
@include transition(background);
@media ($tui-mouse) {
&:hover {
background: var(--tui-background-neutral-1-hover);
}
}
}
td:first-child {
width: 13rem;
}
[tuiButton] { [tuiButton] {
margin-inline-start: auto; margin-inline-start: auto;
} }
span {
display: flex;
align-items: center;
gap: 0.25rem;
}
:host-context(tui-root._mobile) {
tr {
grid-template-columns: min-content 1fr 4rem;
white-space: nowrap;
}
td {
grid-column: span 2;
&:first-child {
font-size: 0;
width: auto;
grid-area: 1 / 2;
place-content: center;
margin: 0 0.5rem;
}
&:last-child {
grid-area: 1 / 3 / 4 / 3;
align-self: center;
justify-self: end;
}
}
.name {
color: var(--tui-text-primary);
font: var(--tui-font-text-m);
font-weight: bold;
grid-column: 1;
max-width: 12rem;
}
}
`, `,
host: { class: 'g-card' }, host: { class: 'g-card' },
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
TuiButton, TuiButton,
TuiCell,
TuiIcon, TuiIcon,
TuiTitle,
TuiTooltip, TuiTooltip,
PlaceholderComponent, PlaceholderComponent,
BackupStatusComponent, BackupStatusComponent,
TableComponent,
], ],
}) })
export class BackupNetworkComponent { export class BackupNetworkComponent {

View File

@@ -6,16 +6,10 @@ import {
} from '@angular/core' } from '@angular/core'
import { ActivatedRoute } from '@angular/router' import { ActivatedRoute } from '@angular/router'
import { UnitConversionPipesModule } from '@start9labs/shared' import { UnitConversionPipesModule } from '@start9labs/shared'
import { import { TuiAlertService, TuiButton, TuiIcon } from '@taiga-ui/core'
TuiAlertService,
TuiButton,
TuiIcon,
TuiNotification,
TuiTitle,
} from '@taiga-ui/core'
import { TuiTooltip } from '@taiga-ui/kit' import { TuiTooltip } from '@taiga-ui/kit'
import { TuiCell } from '@taiga-ui/layout'
import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component' import { PlaceholderComponent } from 'src/app/routes/portal/components/placeholder.component'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { DiskBackupTarget } from 'src/app/services/api/api.types' import { DiskBackupTarget } from 'src/app/services/api/api.types'
import { BackupService, MappedBackupTarget } from './backup.service' import { BackupService, MappedBackupTarget } from './backup.service'
import { BackupStatusComponent } from './status.component' import { BackupStatusComponent } from './status.component'
@@ -30,58 +24,97 @@ import { BackupStatusComponent } from './status.component'
<ng-template #drives><ng-content /></ng-template> <ng-template #drives><ng-content /></ng-template>
</header> </header>
<tui-notification appearance="warning"> <table [appTable]="['Status', 'Name', 'Model', 'Capacity']">
Warning. Do not use this option if you are using a Raspberry Pi with an @for (target of service.drives(); track $index) {
external SSD. The Raspberry Pi does not support more than one external <tr
drive without additional power and can cause data corruption. tabindex="0"
</tui-notification> (click)="select(target)"
(keydown.enter)="select(target)"
@for (target of service.drives(); track $index) { >
<button tuiCell (click)="select(target)"> <td><span [backupStatus]="target.hasAnyBackup"></span></td>
<tui-icon icon="@tui.save" /> <td class="name">
<span tuiTitle> {{ target.entry.label || target.entry.logicalname }}
<strong>{{ target.entry.label || target.entry.logicalname }}</strong> </td>
<span tuiSubtitle [backupStatus]="target.hasAnyBackup"></span> <td>
<span tuiSubtitle>
{{ target.entry.vendor || 'Unknown Vendor' }} - {{ target.entry.vendor || 'Unknown Vendor' }} -
{{ target.entry.model || 'Unknown Model' }} {{ target.entry.model || 'Unknown Model' }}
</span> </td>
<span tuiSubtitle> <td>{{ target.entry.capacity | convertBytes }}</td>
<b>Capacity:</b> </tr>
{{ target.entry.capacity | convertBytes }} } @empty {
</span> <tr>
</span> <td colspan="4">
</button> <app-placeholder icon="@tui.save-off">
} @empty { No drives detected
<app-placeholder icon="@tui.save-off"> <button
No drives detected tuiButton
<button iconStart="@tui.refresh-cw"
tuiButton (click)="service.getBackupTargets()"
iconStart="@tui.refresh-cw" >
(click)="service.getBackupTargets()" Refresh
> </button>
Refresh </app-placeholder>
</button> </td>
</app-placeholder> </tr>
} }
</table>
`, `,
styles: ` styles: `
tui-notification { @import '@taiga-ui/core/styles/taiga-ui-local';
margin: 0.5rem 0 0.75rem;
tr {
cursor: pointer;
@include transition(background);
@media ($tui-mouse) {
&:hover {
background: var(--tui-background-neutral-1-hover);
}
}
}
td:first-child {
width: 13rem;
}
:host-context(tui-root._mobile) {
tr {
max-width: 18rem;
grid-template-columns: min-content 2rem;
white-space: nowrap;
}
td {
grid-column: span 2;
&:first-child {
font-size: 0;
width: auto;
grid-area: 1 / 2;
place-content: center;
margin: 0 0.5rem;
}
}
.name {
color: var(--tui-text-primary);
font: var(--tui-font-text-m);
font-weight: bold;
grid-column: 1;
max-width: 12rem;
}
} }
`, `,
host: { class: 'g-card' }, host: { class: 'g-card' },
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ imports: [
TuiButton, TuiButton,
TuiCell,
TuiIcon, TuiIcon,
TuiTitle,
TuiTooltip, TuiTooltip,
TuiNotification,
UnitConversionPipesModule, UnitConversionPipesModule,
PlaceholderComponent, PlaceholderComponent,
BackupStatusComponent, BackupStatusComponent,
TableComponent,
], ],
}) })
export class BackupPhysicalComponent { export class BackupPhysicalComponent {

View File

@@ -26,12 +26,19 @@ import { TuiIcon } from '@taiga-ui/core'
`, `,
styles: ` styles: `
:host { :host {
color: var(--tui-text-primary); height: 2rem;
display: flex;
align-items: center;
gap: 0.25rem;
} }
tui-icon { tui-icon {
font-size: 1rem; font-size: 1rem;
} }
:host-context(tui-root._mobile) {
height: auto;
}
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [TuiIcon], imports: [TuiIcon],

View File

@@ -3,8 +3,9 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { FormsModule, ReactiveFormsModule } from '@angular/forms' import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { RouterLink } from '@angular/router' import { RouterLink } from '@angular/router'
import { ErrorService, LoadingService } from '@start9labs/shared' import { ErrorService, LoadingService } from '@start9labs/shared'
import { IST, inputSpec } from '@start9labs/start-sdk' import { inputSpec, IST } from '@start9labs/start-sdk'
import { TuiButton, TuiDialogService } from '@taiga-ui/core' import { TuiButton, TuiDialogService, TuiLink, TuiTitle } from '@taiga-ui/core'
import { TuiHeader } from '@taiga-ui/layout'
import { TuiInputModule } from '@taiga-ui/legacy' import { TuiInputModule } from '@taiga-ui/legacy'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { switchMap, tap } from 'rxjs' import { switchMap, tap } from 'rxjs'
@@ -14,7 +15,6 @@ import { FormService } from 'src/app/services/form.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service' import { TitleDirective } from 'src/app/services/title.service'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec' import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { EmailInfoComponent } from './info.component'
@Component({ @Component({
template: ` template: `
@@ -22,18 +22,38 @@ import { EmailInfoComponent } from './info.component'
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a> <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
Email Email
</ng-container> </ng-container>
<email-info /> <header tuiHeader>
<ng-container *ngIf="form$ | async as form"> <hgroup tuiTitle>
<form class="g-card" [formGroup]="form"> <h3>Email</h3>
<header>SMTP Credentials</header> <p tuiSubtitle>
<form-group Connect to an external SMTP server for sending emails. Adding SMTP
*ngIf="spec | async as resolved" credentials enables StartOS and some services to send you emails.
[spec]="resolved" <a
></form-group> tuiLink
href="https://docs.start9.com/latest/user-manual/smtp"
target="_blank"
rel="noreferrer"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[pseudo]="true"
[textContent]="'View instructions'"
></a>
</p>
</hgroup>
</header>
@if (form$ | async; as form) {
<form [formGroup]="form">
<header tuiHeader="body-l">
<h3 tuiTitle><b>SMTP Credentials</b></h3>
</header>
@if (spec | async; as resolved) {
<form-group [spec]="resolved" />
}
<footer> <footer>
@if (isSaved) { @if (isSaved) {
<button <button
tuiButton tuiButton
size="l"
appearance="secondary-destructive" appearance="secondary-destructive"
(click)="save(null)" (click)="save(null)"
> >
@@ -42,6 +62,7 @@ import { EmailInfoComponent } from './info.component'
} }
<button <button
tuiButton tuiButton
size="l"
[disabled]="form.invalid" [disabled]="form.invalid"
(click)="save(form.value)" (click)="save(form.value)"
> >
@@ -49,19 +70,22 @@ import { EmailInfoComponent } from './info.component'
</button> </button>
</footer> </footer>
</form> </form>
<form class="g-card"> <form>
<header>Send Test Email</header> <header tuiHeader="body-l">
<h3 tuiTitle><b>Send Test Email</b></h3>
</header>
<tui-input <tui-input
[(ngModel)]="testAddress" [(ngModel)]="testAddress"
[ngModelOptions]="{ standalone: true }" [ngModelOptions]="{ standalone: true }"
> >
Firstname Lastname &lt;email&#64;example.com&gt; Name Lastname &lt;email&#64;example.com&gt;
<input tuiTextfieldLegacy inputmode="email" /> <input tuiTextfieldLegacy inputmode="email" />
</tui-input> </tui-input>
<footer> <footer>
<button <button
tuiButton tuiButton
appearance="secondary" appearance="secondary"
size="l"
[disabled]="!testAddress || form.invalid" [disabled]="!testAddress || form.invalid"
(click)="sendTestEmail(form.value)" (click)="sendTestEmail(form.value)"
> >
@@ -69,17 +93,22 @@ import { EmailInfoComponent } from './info.component'
</button> </button>
</footer> </footer>
</form> </form>
</ng-container> }
`, `,
styles: ` styles: `
:host { :host {
display: grid !important; max-width: 40rem;
grid-template-columns: 1fr 1fr;
align-items: start;
} }
:host-context(tui-root._mobile) { form header,
grid-template-columns: 1fr; form footer {
margin: 1rem 0;
display: flex;
gap: 1rem;
}
footer {
justify-content: flex-end;
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -91,7 +120,9 @@ import { EmailInfoComponent } from './info.component'
FormModule, FormModule,
TuiButton, TuiButton,
TuiInputModule, TuiInputModule,
EmailInfoComponent, TuiHeader,
TuiTitle,
TuiLink,
RouterLink, RouterLink,
TitleDirective, TitleDirective,
], ],

View File

@@ -1,33 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { TuiLink, TuiNotification } from '@taiga-ui/core'
@Component({
selector: 'email-info',
template: `
<tui-notification>
Adding SMTP credentials to StartOS enables StartOS and some services to
send you emails.
<a
tuiLink
href="https://docs.start9.com/latest/user-manual/smtp"
target="_blank"
rel="noreferrer"
iconEnd="@tui.external-link"
[textContent]="'View instructions'"
></a>
</tui-notification>
`,
styles: `
:host {
grid-column: span 2;
}
:host-context(tui-root._mobile) {
grid-column: 1;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TuiNotification, TuiLink],
})
export class EmailInfoComponent {}

View File

@@ -2,7 +2,8 @@ import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop' import { toSignal } from '@angular/core/rxjs-interop'
import { RouterLink } from '@angular/router' import { RouterLink } from '@angular/router'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
import { TuiButton } from '@taiga-ui/core' import { TuiButton, TuiTitle } from '@taiga-ui/core'
import { TuiHeader } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { map } from 'rxjs' import { map } from 'rxjs'
import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component' import { InterfaceComponent } from 'src/app/routes/portal/components/interfaces/interface.component'
@@ -34,13 +35,29 @@ const iface: T.ServiceInterface = {
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a> <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
Web Addresses Web Addresses
</ng-container> </ng-container>
<header tuiHeader>
<hgroup tuiTitle>
<h3>User Interface Addresses</h3>
<p tuiSubtitle>
View and manage private and public addresses for accessing your
StartOS UI
</p>
</hgroup>
</header>
@if (ui(); as ui) { @if (ui(); as ui) {
<app-interface [serviceInterface]="ui" /> <app-interface [serviceInterface]="ui" />
} }
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [InterfaceComponent, RouterLink, TuiButton, TitleDirective], imports: [
InterfaceComponent,
RouterLink,
TuiButton,
TitleDirective,
TuiHeader,
TuiTitle,
],
}) })
export default class StartOsUiComponent { export default class StartOsUiComponent {
private readonly config = inject(ConfigService) private readonly config = inject(ConfigService)

View File

@@ -10,6 +10,7 @@ import {
TuiNotification, TuiNotification,
TuiTitle, TuiTitle,
} from '@taiga-ui/core' } from '@taiga-ui/core'
import { TuiHeader } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { from } from 'rxjs' import { from } from 'rxjs'
import { FormComponent } from 'src/app/routes/portal/components/form.component' import { FormComponent } from 'src/app/routes/portal/components/form.component'
@@ -23,22 +24,23 @@ import { getServerInfo } from 'src/app/utils/get-server-info'
template: ` template: `
<ng-container *title> <ng-container *title>
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a> <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
Password Reset Change Password
</ng-container> </ng-container>
<tui-notification appearance="warning"> <header tuiHeader>
<div tuiTitle> <hgroup tuiTitle>
<strong>Warning</strong> <h3>Change Password</h3>
<div tuiSubtitle> <p tuiSubtitle>
You will still need your current password to decrypt existing backups! Change your StartOS master password.
</div> <strong>
</div> You will still need your current password to decrypt existing
</tui-notification> backups!
<section class="g-card"> </strong>
<header>Change Master Password</header> </p>
@if (spec(); as spec) { </hgroup>
<app-form [spec]="spec" [buttons]="buttons" /> </header>
} @if (spec(); as spec) {
</section> <app-form [spec]="spec" [buttons]="buttons" />
}
`, `,
styles: ` styles: `
:host { :host {
@@ -46,6 +48,7 @@ import { getServerInfo } from 'src/app/utils/get-server-info'
::ng-deep footer { ::ng-deep footer {
background: transparent !important; background: transparent !important;
margin: 0;
} }
} }
@@ -56,11 +59,11 @@ import { getServerInfo } from 'src/app/utils/get-server-info'
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [ imports: [
TuiNotification,
TuiTitle,
FormComponent,
RouterLink, RouterLink,
TuiHeader,
TuiTitle,
TuiButton, TuiButton,
FormComponent,
TitleDirective, TitleDirective,
], ],
}) })

View File

@@ -1,10 +1,11 @@
import { RouterLink } from '@angular/router' import { RouterLink } from '@angular/router'
import { TuiTable } from '@taiga-ui/addon-table' import { TuiTable } from '@taiga-ui/addon-table'
import { TuiLet } from '@taiga-ui/cdk' import { TuiLet } from '@taiga-ui/cdk'
import { TuiButton } from '@taiga-ui/core' import { TuiButton, TuiTitle } from '@taiga-ui/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ErrorService, LoadingService } from '@start9labs/shared' import { ErrorService, LoadingService } from '@start9labs/shared'
import { TuiHeader } from '@taiga-ui/layout'
import { from, map, merge, Observable, Subject } from 'rxjs' import { from, map, merge, Observable, Subject } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { Session } from 'src/app/services/api/api.types' import { Session } from 'src/app/services/api/api.types'
@@ -17,14 +18,18 @@ import { SSHTableComponent } from './table.component'
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a> <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
Active Sessions Active Sessions
</ng-container> </ng-container>
<header tuiHeader>
<hgroup tuiTitle>
<h3>Active Sessions</h3>
<p tuiSubtitle>
A session is a device that is currently logged into StartOS. For best
security, terminate sessions you do not recognize or no longer use.
</p>
</hgroup>
</header>
<section class="g-card"> <section class="g-card">
<header>Current session</header> <header>Current session</header>
<table <div [single]="true" [sessions]="current$ | async"></div>
tuiTable
class="g-table"
[single]="true"
[sessions]="current$ | async"
></table>
</section> </section>
<section *tuiLet="other$ | async as others" class="g-card"> <section *tuiLet="other$ | async as others" class="g-card">
@@ -43,7 +48,7 @@ import { SSHTableComponent } from './table.component'
</button> </button>
} }
</header> </header>
<table #table tuiTable class="g-table" [sessions]="others"></table> <div #table [sessions]="others"></div>
</section> </section>
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -56,6 +61,8 @@ import { SSHTableComponent } from './table.component'
RouterLink, RouterLink,
TitleDirective, TitleDirective,
TuiTable, TuiTable,
TuiHeader,
TuiTitle,
], ],
}) })
export default class SystemSessionsComponent { export default class SystemSessionsComponent {

View File

@@ -6,53 +6,32 @@ import {
OnChanges, OnChanges,
} from '@angular/core' } from '@angular/core'
import { FormsModule } from '@angular/forms' import { FormsModule } from '@angular/forms'
import { TuiTable } from '@taiga-ui/addon-table'
import { TuiIcon } from '@taiga-ui/core' import { TuiIcon } from '@taiga-ui/core'
import { TuiCheckbox, TuiFade, TuiSkeleton } from '@taiga-ui/kit' import { TuiCheckbox, TuiFade, TuiSkeleton } from '@taiga-ui/kit'
import { BehaviorSubject } from 'rxjs' import { BehaviorSubject } from 'rxjs'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { Session } from 'src/app/services/api/api.types' import { Session } from 'src/app/services/api/api.types'
import { PlatformInfoPipe } from './platform-info.pipe' import { PlatformInfoPipe } from './platform-info.pipe'
@Component({ @Component({
selector: 'table[sessions]', selector: '[sessions]',
template: ` template: `
<thead> <table [appTable]="['User Agent', 'Platform', 'Last Active']">
<tr>
<th
tuiTh
[style.width.%]="50"
[style.padding-left.rem]="single ? null : 2"
>
@if (!single) {
<input
tuiCheckbox
size="s"
type="checkbox"
[disabled]="!sessions?.length"
[ngModel]="all"
(ngModelChange)="onAll($event)"
/>
}
User Agent
</th>
<th tuiTh [style.width.%]="25">Platform</th>
<th tuiTh [style.width.%]="25">Last Active</th>
</tr>
</thead>
<tbody>
@for (session of sessions; track $index) { @for (session of sessions; track $index) {
<tr> <tr>
<td [style.padding-left.rem]="single ? null : 2.25"> <td [style.padding-left.rem]="single ? null : 2.5">
@if (!single) { <label>
<input @if (!single) {
tuiCheckbox <input
size="s" tuiCheckbox
type="checkbox" size="s"
[ngModel]="selected$.value.includes(session)" type="checkbox"
(ngModelChange)="onToggle(session)" [ngModel]="selected$.value.includes(session)"
/> (ngModelChange)="onToggle(session)"
} />
<span tuiFade class="agent">{{ session.userAgent }}</span> }
<span tuiFade class="agent">{{ session.userAgent }}</span>
</label>
</td> </td>
@if (session.metadata.platforms | platformInfo; as info) { @if (session.metadata.platforms | platformInfo; as info) {
<td class="platform"> <td class="platform">
@@ -64,16 +43,16 @@ import { PlatformInfoPipe } from './platform-info.pipe'
</tr> </tr>
} @empty { } @empty {
@if (sessions) { @if (sessions) {
<tr><td colspan="5">No sessions</td></tr> <tr><td colspan="3">No sessions</td></tr>
} @else { } @else {
@for (item of single ? [''] : ['', '']; track $index) { @for (item of single ? [''] : ['', '']; track $index) {
<tr> <tr>
<td colspan="5"><div [tuiSkeleton]="true">Loading</div></td> <td colspan="3"><div [tuiSkeleton]="true">Loading</div></td>
</tr> </tr>
} }
} }
} }
</tbody> </table>
`, `,
styles: [ styles: [
` `
@@ -81,16 +60,37 @@ import { PlatformInfoPipe } from './platform-info.pipe'
td { td {
position: relative; position: relative;
width: 25%;
&[colspan] {
grid-column: span 2;
}
&:first-child {
width: 50%;
}
} }
input { input {
position: absolute; position: absolute;
top: 50%; top: 50%;
left: 0.5rem; left: 0.75rem;
transform: translateY(-50%); transform: translateY(-50%);
} }
.platform {
white-space: nowrap;
}
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
tr {
grid-template-columns: 2.5rem 1fr;
&:has(:checked) .platform {
color: var(--tui-text-action);
}
}
input { input {
@include fullsize(); @include fullsize();
z-index: 1; z-index: 1;
@@ -98,8 +98,12 @@ import { PlatformInfoPipe } from './platform-info.pipe'
transform: none; transform: none;
} }
td:first-child { td {
padding: 0 0.25rem !important; width: 100%;
&:first-child {
padding: 0 !important;
}
} }
.agent { .agent {
@@ -108,11 +112,9 @@ import { PlatformInfoPipe } from './platform-info.pipe'
} }
.platform { .platform {
font-weight: bold; font-size: 0;
display: flex; grid-area: 1 / 1 / 3 / 1;
align-items: center; place-content: center;
gap: 0.5rem;
padding: 0;
} }
.date { .date {
@@ -131,7 +133,7 @@ import { PlatformInfoPipe } from './platform-info.pipe'
TuiCheckbox, TuiCheckbox,
TuiFade, TuiFade,
TuiSkeleton, TuiSkeleton,
TuiTable, TableComponent,
], ],
}) })
export class SSHTableComponent<T extends Session> implements OnChanges { export class SSHTableComponent<T extends Session> implements OnChanges {
@@ -143,26 +145,10 @@ export class SSHTableComponent<T extends Session> implements OnChanges {
@Input() @Input()
single = false single = false
get all(): boolean | null {
if (!this.sessions?.length || !this.selected$.value.length) {
return false
}
if (this.sessions?.length === this.selected$.value.length) {
return true
}
return null
}
ngOnChanges() { ngOnChanges() {
this.selected$.next([]) this.selected$.next([])
} }
onAll(selected: boolean) {
this.selected$.next((selected && this.sessions) || [])
}
onToggle(session: T) { onToggle(session: T) {
const selected = this.selected$.value const selected = this.selected$.value

View File

@@ -1,24 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { TuiLink, TuiNotification } from '@taiga-ui/core'
@Component({
selector: 'ssh-info',
template: `
<tui-notification>
Adding domains to StartOS enables you to access your server and service
interfaces over clearnet.
<a
tuiLink
href="https://docs.start9.com/0.3.5.x/user-manual/ssh"
target="_blank"
rel="noreferrer"
>
View instructions
</a>
</tui-notification>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TuiNotification, TuiLink],
})
export class SSHInfoComponent {}

View File

@@ -1,13 +1,13 @@
import { RouterLink } from '@angular/router' import { RouterLink } from '@angular/router'
import { TuiTable } from '@taiga-ui/addon-table' import { TuiTable } from '@taiga-ui/addon-table'
import { TuiButton } from '@taiga-ui/core' import { TuiButton, TuiLink, TuiTitle } from '@taiga-ui/core'
import { CommonModule } from '@angular/common' import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, inject } from '@angular/core' import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ErrorService } from '@start9labs/shared' import { ErrorService } from '@start9labs/shared'
import { TuiHeader } from '@taiga-ui/layout'
import { catchError, defer, of } from 'rxjs' import { catchError, defer, of } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
import { TitleDirective } from 'src/app/services/title.service' import { TitleDirective } from 'src/app/services/title.service'
import { SSHInfoComponent } from './info.component'
import { SSHTableComponent } from './table.component' import { SSHTableComponent } from './table.component'
@Component({ @Component({
@@ -16,7 +16,24 @@ import { SSHTableComponent } from './table.component'
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a> <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
SSH SSH
</ng-container> </ng-container>
<ssh-info /> <header tuiHeader>
<hgroup tuiTitle>
<h3>SSH</h3>
<p tuiSubtitle>
Manage your SSH keys to access your server from the command line
<a
tuiLink
href="https://docs.start9.com/latest/user-manual/ssh"
target="_blank"
rel="noreferrer"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[pseudo]="true"
[textContent]="'View instructions'"
></a>
</p>
</hgroup>
</header>
<section class="g-card"> <section class="g-card">
<header> <header>
Saved Keys Saved Keys
@@ -30,7 +47,7 @@ import { SSHTableComponent } from './table.component'
Add Key Add Key
</button> </button>
</header> </header>
<table #table tuiTable class="g-table" [keys]="keys$ | async"></table> <div #table [keys]="keys$ | async"></div>
</section> </section>
`, `,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@@ -39,10 +56,12 @@ import { SSHTableComponent } from './table.component'
CommonModule, CommonModule,
TuiButton, TuiButton,
SSHTableComponent, SSHTableComponent,
SSHInfoComponent,
RouterLink, RouterLink,
TitleDirective, TitleDirective,
TuiTable, TuiTable,
TuiHeader,
TuiTitle,
TuiLink,
], ],
}) })
export default class SystemSSHComponent { export default class SystemSSHComponent {

View File

@@ -7,32 +7,25 @@ import {
Input, Input,
} from '@angular/core' } from '@angular/core'
import { ErrorService, LoadingService } from '@start9labs/shared' import { ErrorService, LoadingService } from '@start9labs/shared'
import { TuiTable } from '@taiga-ui/addon-table' import { TuiButton, TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
import { TuiDialogOptions, TuiDialogService, TuiButton } from '@taiga-ui/core'
import { import {
TUI_CONFIRM,
TuiConfirmData, TuiConfirmData,
TuiFade, TuiFade,
TUI_CONFIRM,
TuiSkeleton, TuiSkeleton,
} from '@taiga-ui/kit' } from '@taiga-ui/kit'
import { filter, take } from 'rxjs' import { filter, take } from 'rxjs'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { PROMPT } from 'src/app/routes/portal/modals/prompt.component' import { PROMPT } from 'src/app/routes/portal/modals/prompt.component'
import { SSHKey } from 'src/app/services/api/api.types' import { SSHKey } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service' import { ApiService } from 'src/app/services/api/embassy-api.service'
@Component({ @Component({
selector: 'table[keys]', selector: '[keys]',
template: ` template: `
<thead> <table
<tr> [appTable]="['Hostname', 'Created At', 'Algorithm', 'Fingerprint', '']"
<th tuiTh>Hostname</th> >
<th tuiTh>Created At</th>
<th tuiTh>Algorithm</th>
<th tuiTh>Fingerprint</th>
<th tuiTh></th>
</tr>
</thead>
<tbody>
@for (key of keys; track $index) { @for (key of keys; track $index) {
<tr> <tr>
<td class="title">{{ key.hostname }}</td> <td class="title">{{ key.hostname }}</td>
@@ -62,7 +55,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
} }
} }
} }
</tbody> </table>
`, `,
styles: ` styles: `
:host-context(tui-root._mobile) { :host-context(tui-root._mobile) {
@@ -109,7 +102,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
`, `,
standalone: true, standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, TuiButton, TuiFade, TuiSkeleton, TuiTable], imports: [CommonModule, TuiButton, TuiFade, TuiSkeleton, TableComponent],
}) })
export class SSHTableComponent { export class SSHTableComponent {
private readonly loader = inject(LoadingService) private readonly loader = inject(LoadingService)

View File

@@ -1,25 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'
import { TuiLink, TuiNotification } from '@taiga-ui/core'
@Component({
selector: 'wifi-info',
template: `
<tui-notification>
Adding WiFi credentials to StartOS allows you to remove the Ethernet cable
and move the device anywhere you want. StartOS will automatically connect
to available networks.
<a
tuiLink
href="https://docs.start9.com/latest/user-manual/wifi"
target="_blank"
rel="noreferrer"
iconEnd="@tui.external-link"
[textContent]="'View instructions'"
></a>
</tui-notification>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TuiNotification, TuiLink],
})
export class WifiInfoComponent {}

View File

@@ -24,27 +24,26 @@ import SystemWifiComponent from './wifi.component'
template: ` template: `
@for (network of wifi; track $index) { @for (network of wifi; track $index) {
@if (network.ssid) { @if (network.ssid) {
<div tuiCell [style.padding]="0"> <button
tuiCell
[disabled]="network.connected"
(click)="prompt(network)"
>
<div tuiTitle> <div tuiTitle>
<strong tuiFade> <strong tuiFade>
{{ network.ssid }} {{ network.ssid }}
@if (network.connected) { @if (network.connected) {
<tui-badge appearance="success">Connected</tui-badge> <tui-badge appearance="positive">Connected</tui-badge>
} }
</strong> </strong>
</div> </div>
@if (!network.connected) {
<button tuiButton size="xs" (click)="prompt(network)">
Connect
</button>
}
@if (network.connected !== undefined) { @if (network.connected !== undefined) {
<button <button
tuiIconButton tuiIconButton
size="s" size="s"
appearance="icon" appearance="icon"
iconStart="@tui.trash-2" iconStart="@tui.trash-2"
(click)="forget(network)" (click.stop)="forget(network)"
> >
Forget Forget
</button> </button>
@@ -63,14 +62,22 @@ import SystemWifiComponent from './wifi.component'
} @else { } @else {
<tui-icon icon="@tui.wifi-off" /> <tui-icon icon="@tui.wifi-off" />
} }
</div> </button>
} }
} }
`, `,
styles: ` styles: `
:host { :host {
align-items: stretch; align-items: stretch;
white-space: nowrap; padding: 0.5rem !important;
}
[tuiCell] {
padding-inline: 1rem !important;
&:disabled > * {
opacity: 1;
}
} }
tui-icon { tui-icon {

View File

@@ -13,10 +13,12 @@ import {
TuiAppearance, TuiAppearance,
TuiButton, TuiButton,
TuiDialogOptions, TuiDialogOptions,
TuiLink,
TuiLoader, TuiLoader,
TuiTitle,
} from '@taiga-ui/core' } from '@taiga-ui/core'
import { TuiSwitch } from '@taiga-ui/kit' import { TuiSwitch } from '@taiga-ui/kit'
import { TuiCardLarge } from '@taiga-ui/layout' import { TuiCardLarge, TuiHeader } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client' import { PatchDB } from 'patch-db-client'
import { catchError, defer, map, merge, Observable, of, Subject } from 'rxjs' import { catchError, defer, map, merge, Observable, of, Subject } from 'rxjs'
import { import {
@@ -28,7 +30,6 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service' import { FormDialogService } from 'src/app/services/form-dialog.service'
import { DataModel } from 'src/app/services/patch-db/data-model' import { DataModel } from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service' import { TitleDirective } from 'src/app/services/title.service'
import { WifiInfoComponent } from './info.component'
import { WifiTableComponent } from './table.component' import { WifiTableComponent } from './table.component'
import { parseWifi, WifiData, WiFiForm } from './utils' import { parseWifi, WifiData, WiFiForm } from './utils'
import { wifiSpec } from './wifi.const' import { wifiSpec } from './wifi.const'
@@ -39,7 +40,26 @@ import { wifiSpec } from './wifi.const'
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a> <a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
WiFi WiFi
</ng-container> </ng-container>
<wifi-info /> <header tuiHeader>
<hgroup tuiTitle>
<h3>WiFi</h3>
<p tuiSubtitle>
Adding WiFi credentials to StartOS allows you to remove the Ethernet
cable and move the device anywhere you want. StartOS will
automatically connect to available networks.
<a
tuiLink
href="https://docs.start9.com/latest/user-manual/wifi"
target="_blank"
rel="noreferrer"
appearance="action-grayscale"
iconEnd="@tui.external-link"
[pseudo]="true"
[textContent]="'View instructions'"
></a>
</p>
</hgroup>
</header>
@if (status()?.interface) { @if (status()?.interface) {
<section class="g-card"> <section class="g-card">
<header> <header>
@@ -60,7 +80,6 @@ import { wifiSpec } from './wifi.const'
tuiCardLarge="compact" tuiCardLarge="compact"
tuiAppearance="neutral" tuiAppearance="neutral"
[wifi]="data.known" [wifi]="data.known"
[style.padding-block.rem]="0.5"
></div> ></div>
} }
@if (data.available.length) { @if (data.available.length) {
@@ -69,11 +88,10 @@ import { wifiSpec } from './wifi.const'
tuiCardLarge="compact" tuiCardLarge="compact"
tuiAppearance="neutral" tuiAppearance="neutral"
[wifi]="data.available" [wifi]="data.available"
[style.padding-block.rem]="0.5"
></div> ></div>
} }
<p> <p>
<button tuiButton size="s" (click)="other(data)">Add</button> <button tuiButton (click)="other(data)">Add</button>
</p> </p>
} @else { } @else {
<tui-loader [style.height.rem]="5" /> <tui-loader [style.height.rem]="5" />
@@ -88,6 +106,11 @@ import { wifiSpec } from './wifi.const'
</app-placeholder> </app-placeholder>
} }
`, `,
styles: `
:host {
max-width: 40rem;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true, standalone: true,
imports: [ imports: [
@@ -97,11 +120,13 @@ import { wifiSpec } from './wifi.const'
TuiCardLarge, TuiCardLarge,
TuiLoader, TuiLoader,
TuiAppearance, TuiAppearance,
WifiInfoComponent,
WifiTableComponent, WifiTableComponent,
TitleDirective, TitleDirective,
RouterLink, RouterLink,
PlaceholderComponent, PlaceholderComponent,
TuiHeader,
TuiTitle,
TuiLink,
], ],
}) })
export default class SystemWifiComponent { export default class SystemWifiComponent {

View File

@@ -11,7 +11,7 @@ import { SYSTEM_MENU } from './system.const'
@Component({ @Component({
template: ` template: `
<span *title>{{ 'system.outlet.general' | i18n }}</span> <span *title>{{ 'system.outlet.system' | i18n }}</span>
<aside class="g-aside"> <aside class="g-aside">
@for (cat of menu; track $index) { @for (cat of menu; track $index) {
@if ($index) { @if ($index) {
@@ -45,6 +45,10 @@ import { SYSTEM_MENU } from './system.const'
padding: 0; padding: 0;
} }
tui-badge-notification {
vertical-align: baseline;
}
[tuiCell] { [tuiCell] {
color: var(--tui-text-secondary); color: var(--tui-text-secondary);
@@ -94,6 +98,10 @@ import { SYSTEM_MENU } from './system.const'
hr { hr {
background: var(--tui-border-normal); background: var(--tui-border-normal);
} }
::ng-deep hgroup h3 {
display: none;
}
} }
`, `,
], ],

View File

@@ -16,6 +16,7 @@ export const SYSTEM_UTILITIES: Record<string, { icon: string; title: string }> =
icon: '@tui.upload', icon: '@tui.upload',
title: 'Sideload', title: 'Sideload',
}, },
// @TODO 040
// '/portal/updates': { // '/portal/updates': {
// icon: '@tui.globe', // icon: '@tui.globe',
// title: 'Updates', // title: 'Updates',

View File

@@ -121,6 +121,14 @@ hr {
padding-top: 4rem; padding-top: 4rem;
} }
tui-root:not(._mobile) &:has(.g-table:not([tuiTable])) {
padding-block-end: 1rem;
> header {
background: none;
}
}
[tuiCell] { [tuiCell] {
margin: 0 -0.625rem; margin: 0 -0.625rem;
border-radius: var(--tui-radius-s); border-radius: var(--tui-radius-s);
@@ -179,7 +187,6 @@ hr {
th { th {
position: relative; position: relative;
font: var(--tui-font-text-s); font: var(--tui-font-text-s);
height: 2rem;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border: 1px solid var(--tui-background-neutral-1); border: 1px solid var(--tui-background-neutral-1);
border-left: 0; border-left: 0;
@@ -193,6 +200,11 @@ hr {
text-align: left; text-align: left;
} }
tr:focus-visible {
outline: none;
box-shadow: inset 0 0 0 0.125rem var(--tui-border-focus);
}
tui-root._mobile & { tui-root._mobile & {
min-width: 0; min-width: 0;
border: none; border: none;
@@ -215,10 +227,6 @@ hr {
} }
} }
tr:has(:checked) {
box-shadow: inset 0 0 0 0.125rem var(--tui-background-accent-1);
}
td, td,
th { th {
position: static; position: static;