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

View File

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

View File

@@ -162,20 +162,3 @@ tui-badge-notification {
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 {
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 {
text-align: right;
grid-area: 1 / 2 / 3 / 3;
}
.desktop {
place-content: center;
}
.mobile {

View File

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

View File

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

View File

@@ -10,9 +10,7 @@ import { RouterLink, RouterLinkActive } from '@angular/router'
import { TuiResponsiveDialogService, TuiTabBar } from '@taiga-ui/addon-mobile'
import { TuiIcon } from '@taiga-ui/core'
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 { RESOURCES } from 'src/app/utils/resources'
import { getMenu } from 'src/app/utils/system-utilities'
const FILTER = ['/portal/services', '/portal/system', '/portal/marketplace']
@@ -72,20 +70,6 @@ const FILTER = ['/portal/services', '/portal/system', '/portal/marketplace']
}
</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>
</button>
</nav>
@@ -138,7 +122,6 @@ export class TabsComponent {
index = 3
readonly resources = RESOURCES
readonly menu = getMenu().filter(item => !FILTER.includes(item.routerLink))
readonly badge = toSignal(inject(BadgeService).getCount('/portal/system'), {
initialValue: 0,
@@ -148,10 +131,6 @@ export class TabsComponent {
this.menu.reduce((acc, item) => acc + item.badge(), 0),
)
about() {
this.dialogs.open(ABOUT, { label: 'About this server' }).subscribe()
}
more(content: TemplateRef<any>) {
this.dialogs.open(content, { label: 'Start OS' }).subscribe({
complete: () => this.update(),

View File

@@ -55,6 +55,10 @@ import { ServicesService } from './services.service'
font-size: 1rem;
overflow: hidden;
}
:host-context(tui-root._mobile) {
padding: 0;
}
`,
host: { class: 'g-page' },
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 { ErrorService, LoadingService } from '@start9labs/shared'
import { ISB, utils } from '@start9labs/start-sdk'
import { TuiButton, TuiLoader, TuiTitle } from '@taiga-ui/core'
import { TuiCell } from '@taiga-ui/layout'
import { TuiButton, TuiLink, TuiLoader, TuiTitle } from '@taiga-ui/core'
import { TuiCell, TuiHeader } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client'
import { map } from 'rxjs'
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 { knownACME, toAcmeName } from 'src/app/utils/acme'
import { configBuilderToSpec } from 'src/app/utils/configBuilderToSpec'
import { AcmeInfoComponent } from './info.component'
@Component({
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">
<header>
Saved Providers
@@ -62,9 +79,14 @@ import { AcmeInfoComponent } from './info.component'
}
</section>
`,
styles: `
:host {
max-width: 40rem;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [TuiButton, TuiLoader, TuiCell, TuiTitle, AcmeInfoComponent],
imports: [TuiButton, TuiLoader, TuiCell, TuiTitle, TuiHeader, TuiLink],
})
export default class SystemAcmeComponent {
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 {
ChangeDetectionStrategy,
Component,
inject,
OnInit,
} from '@angular/core'
import { toSignal } from '@angular/core/rxjs-interop'
import { ActivatedRoute, RouterLink } from '@angular/router'
import { UnitConversionPipesModule } from '@start9labs/shared'
import { TuiResponsiveDialogService } from '@taiga-ui/addon-mobile'
import { TuiButton, TuiLink, TuiLoader } from '@taiga-ui/core'
import { BACKUP } from 'src/app/routes/portal/routes/system/routes/backups/backup.component'
import { TuiMapperPipe } from '@taiga-ui/cdk'
import {
TuiButton,
TuiLink,
TuiLoader,
TuiNotification,
TuiTitle,
} from '@taiga-ui/core'
import { TuiHeader } from '@taiga-ui/layout'
import { PatchDB } from 'patch-db-client'
import {
CifsBackupTarget,
DiskBackupTarget,
} from 'src/app/services/api/api.types'
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 { BACKUP } from './backup.component'
import { BackupService, MappedBackupTarget } from './backup.service'
import { BackupNetworkComponent } from './network.component'
import { BackupPhysicalComponent } from './physical.component'
@@ -29,6 +40,53 @@ import { BACKUP_RESTORE } from './restore.component'
{{ type === 'create' ? 'Create Backup' : 'Restore Backup' }}
</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)) {
<section backupProgress></section>
} @else {
@@ -40,8 +98,7 @@ import { BACKUP_RESTORE } from './restore.component'
/>
} @else {
<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
<a
tuiLink
@@ -53,17 +110,7 @@ import { BACKUP_RESTORE } from './restore.component'
></a>
</section>
<section (physicalFolders)="onTarget($event)">
{{ text }}
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>
A physical drive that is plugged directly into your Start9 Server.
</section>
}
}
@@ -71,15 +118,20 @@ import { BACKUP_RESTORE } from './restore.component'
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
AsyncPipe,
DatePipe,
RouterLink,
TuiButton,
TuiLoader,
TuiLink,
TuiHeader,
TuiTitle,
TuiNotification,
TuiMapperPipe,
TitleDirective,
UnitConversionPipesModule,
BackupNetworkComponent,
BackupPhysicalComponent,
AsyncPipe,
BackupProgressComponent,
],
})
@@ -88,11 +140,25 @@ export default class SystemBackupComponent implements OnInit {
readonly type = inject(ActivatedRoute).snapshot.data['type']
readonly service = inject(BackupService)
readonly eos = inject(EOSService)
readonly server = toSignal(
inject<PatchDB<DataModel>>(PatchDB).watch$('serverInfo'),
)
get text() {
return this.type === 'create'
? 'Backup server to'
: 'Restore your services from'
readonly toAppearance = (lastBackup: string | null) => {
if (!lastBackup) return 'negative'
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() {

View File

@@ -14,6 +14,7 @@ import { TuiCell } from '@taiga-ui/layout'
import { filter } from 'rxjs'
import { FormComponent } from 'src/app/routes/portal/components/form.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 { ApiService } from 'src/app/services/api/embassy-api.service'
import { FormDialogService } from 'src/app/services/form-dialog.service'
@@ -37,69 +38,132 @@ const ERROR =
</button>
</header>
@for (target of service.cifs(); track $index) {
<button tuiCell (click)="select(target)">
<tui-icon icon="@tui.folder-open" />
<span tuiTitle>
<strong>{{ target.entry.path.split('/').pop() }}</strong>
@if (target.entry.mountable) {
<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)"
<table [appTable]="['Status', 'Name', 'Hostname', 'Path', '']">
@for (target of service.cifs(); track $index) {
<tr
tabindex="0"
(click)="select(target)"
(keydown.enter)="select(target)"
>
Forget
</button>
<button
tuiIconButton
appearance="icon"
size="xs"
iconStart="@tui.pencil"
(click.stop)="edit(target)"
>
Edit
</button>
</button>
} @empty {
<app-placeholder icon="@tui.folder-x">No network folders</app-placeholder>
}
<td>
@if (target.entry.mountable) {
<span [backupStatus]="target.hasAnyBackup"></span>
} @else {
<span>
<tui-icon
icon="@tui.signal-high"
class="g-negative"
[style.font-size.rem]="1"
/>
Unable to connect
</span>
}
</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: `
@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] {
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' },
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
TuiButton,
TuiCell,
TuiIcon,
TuiTitle,
TuiTooltip,
PlaceholderComponent,
BackupStatusComponent,
TableComponent,
],
})
export class BackupNetworkComponent {

View File

@@ -6,16 +6,10 @@ import {
} from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { UnitConversionPipesModule } from '@start9labs/shared'
import {
TuiAlertService,
TuiButton,
TuiIcon,
TuiNotification,
TuiTitle,
} from '@taiga-ui/core'
import { TuiAlertService, TuiButton, TuiIcon } from '@taiga-ui/core'
import { TuiTooltip } from '@taiga-ui/kit'
import { TuiCell } from '@taiga-ui/layout'
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 { BackupService, MappedBackupTarget } from './backup.service'
import { BackupStatusComponent } from './status.component'
@@ -30,58 +24,97 @@ import { BackupStatusComponent } from './status.component'
<ng-template #drives><ng-content /></ng-template>
</header>
<tui-notification appearance="warning">
Warning. Do not use this option if you are using a Raspberry Pi with an
external SSD. The Raspberry Pi does not support more than one external
drive without additional power and can cause data corruption.
</tui-notification>
@for (target of service.drives(); track $index) {
<button tuiCell (click)="select(target)">
<tui-icon icon="@tui.save" />
<span tuiTitle>
<strong>{{ target.entry.label || target.entry.logicalname }}</strong>
<span tuiSubtitle [backupStatus]="target.hasAnyBackup"></span>
<span tuiSubtitle>
<table [appTable]="['Status', 'Name', 'Model', 'Capacity']">
@for (target of service.drives(); track $index) {
<tr
tabindex="0"
(click)="select(target)"
(keydown.enter)="select(target)"
>
<td><span [backupStatus]="target.hasAnyBackup"></span></td>
<td class="name">
{{ target.entry.label || target.entry.logicalname }}
</td>
<td>
{{ target.entry.vendor || 'Unknown Vendor' }} -
{{ target.entry.model || 'Unknown Model' }}
</span>
<span tuiSubtitle>
<b>Capacity:</b>
{{ target.entry.capacity | convertBytes }}
</span>
</span>
</button>
} @empty {
<app-placeholder icon="@tui.save-off">
No drives detected
<button
tuiButton
iconStart="@tui.refresh-cw"
(click)="service.getBackupTargets()"
>
Refresh
</button>
</app-placeholder>
}
</td>
<td>{{ target.entry.capacity | convertBytes }}</td>
</tr>
} @empty {
<tr>
<td colspan="4">
<app-placeholder icon="@tui.save-off">
No drives detected
<button
tuiButton
iconStart="@tui.refresh-cw"
(click)="service.getBackupTargets()"
>
Refresh
</button>
</app-placeholder>
</td>
</tr>
}
</table>
`,
styles: `
tui-notification {
margin: 0.5rem 0 0.75rem;
@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;
}
: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' },
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [
TuiButton,
TuiCell,
TuiIcon,
TuiTitle,
TuiTooltip,
TuiNotification,
UnitConversionPipesModule,
PlaceholderComponent,
BackupStatusComponent,
TableComponent,
],
})
export class BackupPhysicalComponent {

View File

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

View File

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

View File

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

View File

@@ -1,10 +1,11 @@
import { RouterLink } from '@angular/router'
import { TuiTable } from '@taiga-ui/addon-table'
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 { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ErrorService, LoadingService } from '@start9labs/shared'
import { TuiHeader } from '@taiga-ui/layout'
import { from, map, merge, Observable, Subject } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service'
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>
Active Sessions
</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">
<header>Current session</header>
<table
tuiTable
class="g-table"
[single]="true"
[sessions]="current$ | async"
></table>
<div [single]="true" [sessions]="current$ | async"></div>
</section>
<section *tuiLet="other$ | async as others" class="g-card">
@@ -43,7 +48,7 @@ import { SSHTableComponent } from './table.component'
</button>
}
</header>
<table #table tuiTable class="g-table" [sessions]="others"></table>
<div #table [sessions]="others"></div>
</section>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -56,6 +61,8 @@ import { SSHTableComponent } from './table.component'
RouterLink,
TitleDirective,
TuiTable,
TuiHeader,
TuiTitle,
],
})
export default class SystemSessionsComponent {

View File

@@ -6,53 +6,32 @@ import {
OnChanges,
} from '@angular/core'
import { FormsModule } from '@angular/forms'
import { TuiTable } from '@taiga-ui/addon-table'
import { TuiIcon } from '@taiga-ui/core'
import { TuiCheckbox, TuiFade, TuiSkeleton } from '@taiga-ui/kit'
import { BehaviorSubject } from 'rxjs'
import { TableComponent } from 'src/app/routes/portal/components/table.component'
import { Session } from 'src/app/services/api/api.types'
import { PlatformInfoPipe } from './platform-info.pipe'
@Component({
selector: 'table[sessions]',
selector: '[sessions]',
template: `
<thead>
<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>
<table [appTable]="['User Agent', 'Platform', 'Last Active']">
@for (session of sessions; track $index) {
<tr>
<td [style.padding-left.rem]="single ? null : 2.25">
@if (!single) {
<input
tuiCheckbox
size="s"
type="checkbox"
[ngModel]="selected$.value.includes(session)"
(ngModelChange)="onToggle(session)"
/>
}
<span tuiFade class="agent">{{ session.userAgent }}</span>
<td [style.padding-left.rem]="single ? null : 2.5">
<label>
@if (!single) {
<input
tuiCheckbox
size="s"
type="checkbox"
[ngModel]="selected$.value.includes(session)"
(ngModelChange)="onToggle(session)"
/>
}
<span tuiFade class="agent">{{ session.userAgent }}</span>
</label>
</td>
@if (session.metadata.platforms | platformInfo; as info) {
<td class="platform">
@@ -64,16 +43,16 @@ import { PlatformInfoPipe } from './platform-info.pipe'
</tr>
} @empty {
@if (sessions) {
<tr><td colspan="5">No sessions</td></tr>
<tr><td colspan="3">No sessions</td></tr>
} @else {
@for (item of single ? [''] : ['', '']; track $index) {
<tr>
<td colspan="5"><div [tuiSkeleton]="true">Loading</div></td>
<td colspan="3"><div [tuiSkeleton]="true">Loading</div></td>
</tr>
}
}
}
</tbody>
</table>
`,
styles: [
`
@@ -81,16 +60,37 @@ import { PlatformInfoPipe } from './platform-info.pipe'
td {
position: relative;
width: 25%;
&[colspan] {
grid-column: span 2;
}
&:first-child {
width: 50%;
}
}
input {
position: absolute;
top: 50%;
left: 0.5rem;
left: 0.75rem;
transform: translateY(-50%);
}
.platform {
white-space: nowrap;
}
:host-context(tui-root._mobile) {
tr {
grid-template-columns: 2.5rem 1fr;
&:has(:checked) .platform {
color: var(--tui-text-action);
}
}
input {
@include fullsize();
z-index: 1;
@@ -98,8 +98,12 @@ import { PlatformInfoPipe } from './platform-info.pipe'
transform: none;
}
td:first-child {
padding: 0 0.25rem !important;
td {
width: 100%;
&:first-child {
padding: 0 !important;
}
}
.agent {
@@ -108,11 +112,9 @@ import { PlatformInfoPipe } from './platform-info.pipe'
}
.platform {
font-weight: bold;
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0;
font-size: 0;
grid-area: 1 / 1 / 3 / 1;
place-content: center;
}
.date {
@@ -131,7 +133,7 @@ import { PlatformInfoPipe } from './platform-info.pipe'
TuiCheckbox,
TuiFade,
TuiSkeleton,
TuiTable,
TableComponent,
],
})
export class SSHTableComponent<T extends Session> implements OnChanges {
@@ -143,26 +145,10 @@ export class SSHTableComponent<T extends Session> implements OnChanges {
@Input()
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() {
this.selected$.next([])
}
onAll(selected: boolean) {
this.selected$.next((selected && this.sessions) || [])
}
onToggle(session: T) {
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 { 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 { ChangeDetectionStrategy, Component, inject } from '@angular/core'
import { ErrorService } from '@start9labs/shared'
import { TuiHeader } from '@taiga-ui/layout'
import { catchError, defer, of } from 'rxjs'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { TitleDirective } from 'src/app/services/title.service'
import { SSHInfoComponent } from './info.component'
import { SSHTableComponent } from './table.component'
@Component({
@@ -16,7 +16,24 @@ import { SSHTableComponent } from './table.component'
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
SSH
</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">
<header>
Saved Keys
@@ -30,7 +47,7 @@ import { SSHTableComponent } from './table.component'
Add Key
</button>
</header>
<table #table tuiTable class="g-table" [keys]="keys$ | async"></table>
<div #table [keys]="keys$ | async"></div>
</section>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -39,10 +56,12 @@ import { SSHTableComponent } from './table.component'
CommonModule,
TuiButton,
SSHTableComponent,
SSHInfoComponent,
RouterLink,
TitleDirective,
TuiTable,
TuiHeader,
TuiTitle,
TuiLink,
],
})
export default class SystemSSHComponent {

View File

@@ -7,32 +7,25 @@ import {
Input,
} from '@angular/core'
import { ErrorService, LoadingService } from '@start9labs/shared'
import { TuiTable } from '@taiga-ui/addon-table'
import { TuiDialogOptions, TuiDialogService, TuiButton } from '@taiga-ui/core'
import { TuiButton, TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
import {
TUI_CONFIRM,
TuiConfirmData,
TuiFade,
TUI_CONFIRM,
TuiSkeleton,
} from '@taiga-ui/kit'
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 { SSHKey } from 'src/app/services/api/api.types'
import { ApiService } from 'src/app/services/api/embassy-api.service'
@Component({
selector: 'table[keys]',
selector: '[keys]',
template: `
<thead>
<tr>
<th tuiTh>Hostname</th>
<th tuiTh>Created At</th>
<th tuiTh>Algorithm</th>
<th tuiTh>Fingerprint</th>
<th tuiTh></th>
</tr>
</thead>
<tbody>
<table
[appTable]="['Hostname', 'Created At', 'Algorithm', 'Fingerprint', '']"
>
@for (key of keys; track $index) {
<tr>
<td class="title">{{ key.hostname }}</td>
@@ -62,7 +55,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
}
}
}
</tbody>
</table>
`,
styles: `
:host-context(tui-root._mobile) {
@@ -109,7 +102,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service'
`,
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [CommonModule, TuiButton, TuiFade, TuiSkeleton, TuiTable],
imports: [CommonModule, TuiButton, TuiFade, TuiSkeleton, TableComponent],
})
export class SSHTableComponent {
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: `
@for (network of wifi; track $index) {
@if (network.ssid) {
<div tuiCell [style.padding]="0">
<button
tuiCell
[disabled]="network.connected"
(click)="prompt(network)"
>
<div tuiTitle>
<strong tuiFade>
{{ network.ssid }}
@if (network.connected) {
<tui-badge appearance="success">Connected</tui-badge>
<tui-badge appearance="positive">Connected</tui-badge>
}
</strong>
</div>
@if (!network.connected) {
<button tuiButton size="xs" (click)="prompt(network)">
Connect
</button>
}
@if (network.connected !== undefined) {
<button
tuiIconButton
size="s"
appearance="icon"
iconStart="@tui.trash-2"
(click)="forget(network)"
(click.stop)="forget(network)"
>
Forget
</button>
@@ -63,14 +62,22 @@ import SystemWifiComponent from './wifi.component'
} @else {
<tui-icon icon="@tui.wifi-off" />
}
</div>
</button>
}
}
`,
styles: `
:host {
align-items: stretch;
white-space: nowrap;
padding: 0.5rem !important;
}
[tuiCell] {
padding-inline: 1rem !important;
&:disabled > * {
opacity: 1;
}
}
tui-icon {

View File

@@ -13,10 +13,12 @@ import {
TuiAppearance,
TuiButton,
TuiDialogOptions,
TuiLink,
TuiLoader,
TuiTitle,
} from '@taiga-ui/core'
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 { catchError, defer, map, merge, Observable, of, Subject } from 'rxjs'
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 { DataModel } from 'src/app/services/patch-db/data-model'
import { TitleDirective } from 'src/app/services/title.service'
import { WifiInfoComponent } from './info.component'
import { WifiTableComponent } from './table.component'
import { parseWifi, WifiData, WiFiForm } from './utils'
import { wifiSpec } from './wifi.const'
@@ -39,7 +40,26 @@ import { wifiSpec } from './wifi.const'
<a routerLink=".." tuiIconButton iconStart="@tui.arrow-left">Back</a>
WiFi
</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) {
<section class="g-card">
<header>
@@ -60,7 +80,6 @@ import { wifiSpec } from './wifi.const'
tuiCardLarge="compact"
tuiAppearance="neutral"
[wifi]="data.known"
[style.padding-block.rem]="0.5"
></div>
}
@if (data.available.length) {
@@ -69,11 +88,10 @@ import { wifiSpec } from './wifi.const'
tuiCardLarge="compact"
tuiAppearance="neutral"
[wifi]="data.available"
[style.padding-block.rem]="0.5"
></div>
}
<p>
<button tuiButton size="s" (click)="other(data)">Add</button>
<button tuiButton (click)="other(data)">Add</button>
</p>
} @else {
<tui-loader [style.height.rem]="5" />
@@ -88,6 +106,11 @@ import { wifiSpec } from './wifi.const'
</app-placeholder>
}
`,
styles: `
:host {
max-width: 40rem;
}
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
@@ -97,11 +120,13 @@ import { wifiSpec } from './wifi.const'
TuiCardLarge,
TuiLoader,
TuiAppearance,
WifiInfoComponent,
WifiTableComponent,
TitleDirective,
RouterLink,
PlaceholderComponent,
TuiHeader,
TuiTitle,
TuiLink,
],
})
export default class SystemWifiComponent {

View File

@@ -11,7 +11,7 @@ import { SYSTEM_MENU } from './system.const'
@Component({
template: `
<span *title>{{ 'system.outlet.general' | i18n }}</span>
<span *title>{{ 'system.outlet.system' | i18n }}</span>
<aside class="g-aside">
@for (cat of menu; track $index) {
@if ($index) {
@@ -45,6 +45,10 @@ import { SYSTEM_MENU } from './system.const'
padding: 0;
}
tui-badge-notification {
vertical-align: baseline;
}
[tuiCell] {
color: var(--tui-text-secondary);
@@ -94,6 +98,10 @@ import { SYSTEM_MENU } from './system.const'
hr {
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',
title: 'Sideload',
},
// @TODO 040
// '/portal/updates': {
// icon: '@tui.globe',
// title: 'Updates',

View File

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