diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76dfec490..ea2486ad1 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -220,9 +220,6 @@ instructions. is running normally. - `projects/setup-wizard`(frontend/README.md) - Code for the user interface that is displayed during the setup and recovery process for StartOS. - - `projects/diagnostic-ui` - Code for the user interface that is displayed - when something has gone wrong with starting up StartOS, which provides - helpful debugging tools. - `libs` (Rust) is a set of standalone crates that were separated out of `backend` for the purpose of portability - `patch-db` - A diff based data store that is used to synchronize data between diff --git a/Makefile b/Makefile index 1586ce8d5..71ffac6a8 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ ENVIRONMENT_FILE = $(shell ./check-environment.sh) GIT_HASH_FILE = $(shell ./check-git-hash.sh) VERSION_FILE = $(shell ./check-version.sh) EMBASSY_BINS := backend/target/$(ARCH)-unknown-linux-gnu/release/embassyd backend/target/$(ARCH)-unknown-linux-gnu/release/embassy-init backend/target/$(ARCH)-unknown-linux-gnu/release/embassy-cli backend/target/$(ARCH)-unknown-linux-gnu/release/embassy-sdk backend/target/$(ARCH)-unknown-linux-gnu/release/avahi-alias libs/target/aarch64-unknown-linux-musl/release/embassy_container_init libs/target/x86_64-unknown-linux-musl/release/embassy_container_init -EMBASSY_UIS := frontend/dist/ui frontend/dist/setup-wizard frontend/dist/diagnostic-ui frontend/dist/install-wizard +EMBASSY_UIS := frontend/dist/ui frontend/dist/setup-wizard frontend/dist/install-wizard BUILD_SRC := $(shell find build) EMBASSY_SRC := backend/embassyd.service backend/embassy-init.service $(EMBASSY_UIS) $(BUILD_SRC) COMPAT_SRC := $(shell find system-images/compat/ -not -path 'system-images/compat/target/*' -and -not -name *.tar -and -not -name target) @@ -14,7 +14,6 @@ BACKEND_SRC := $(shell find backend/src) $(shell find backend/migrations) $(shel FRONTEND_SHARED_SRC := $(shell find frontend/projects/shared) $(shell ls -p frontend/ | grep -v / | sed 's/^/frontend\//g') frontend/package.json frontend/node_modules frontend/config.json patch-db/client/dist frontend/patchdb-ui-seed.json FRONTEND_UI_SRC := $(shell find frontend/projects/ui) FRONTEND_SETUP_WIZARD_SRC := $(shell find frontend/projects/setup-wizard) -FRONTEND_DIAGNOSTIC_UI_SRC := $(shell find frontend/projects/diagnostic-ui) FRONTEND_INSTALL_WIZARD_SRC := $(shell find frontend/projects/install-wizard) PATCH_DB_CLIENT_SRC := $(shell find patch-db/client -not -path patch-db/client/dist) GZIP_BIN := $(shell which pigz || which gzip) @@ -95,7 +94,6 @@ install: $(ALL_TARGETS) $(call cp,system-images/binfmt/docker-images/$(ARCH).tar,$(DESTDIR)/usr/lib/embassy/system-images/binfmt.tar) $(call mkdir,$(DESTDIR)/var/www/html) - $(call cp,frontend/dist/diagnostic-ui,$(DESTDIR)/var/www/html/diagnostic) $(call cp,frontend/dist/setup-wizard,$(DESTDIR)/var/www/html/setup) $(call cp,frontend/dist/install-wizard,$(DESTDIR)/var/www/html/install) $(call cp,frontend/dist/ui,$(DESTDIR)/var/www/html/main) @@ -154,9 +152,6 @@ frontend/dist/ui: $(FRONTEND_UI_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE) frontend/dist/setup-wizard: $(FRONTEND_SETUP_WIZARD_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE) npm --prefix frontend run build:setup -frontend/dist/diagnostic-ui: $(FRONTEND_DIAGNOSTIC_UI_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE) - npm --prefix frontend run build:dui - frontend/dist/install-wizard: $(FRONTEND_INSTALL_WIZARD_SRC) $(FRONTEND_SHARED_SRC) $(ENVIRONMENT_FILE) npm --prefix frontend run build:install-wiz diff --git a/frontend/README.md b/frontend/README.md index 369b9cfe6..950aff264 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -5,7 +5,6 @@ StartOS has three user interfaces and a shared library, all written in Ionic/Ang 1. **ui**: the main user interface 1. **install-wizard**: used to install StartOS 1. **setup-wizard**: used to facilitate initial setup -1. **diagnostic-ui**: used to display certain diagnostic information in the event StartOS fails to initialize 1. **marketplace**: abstracted ui elements to search for, list and display details for packages and their dependencies 1. **shared**: contains components, types, and functions shared amongst all of the UIs. diff --git a/frontend/angular.json b/frontend/angular.json index dca324ac9..d7b637042 100644 --- a/frontend/angular.json +++ b/frontend/angular.json @@ -50,6 +50,7 @@ ], "styles": [ "node_modules/@taiga-ui/core/styles/taiga-ui-theme.less", + "projects/shared/styles/taiga.scss", "projects/shared/styles/variables.scss", "projects/shared/styles/global.scss", "projects/shared/styles/shared.scss", @@ -169,9 +170,16 @@ "glob": "**/*.svg", "input": "node_modules/ionicons/dist/ionicons/svg", "output": "./svg" + }, + { + "glob": "**/*", + "input": "node_modules/@taiga-ui/icons/src", + "output": "assets/taiga-ui/icons" } ], "styles": [ + "node_modules/@taiga-ui/core/styles/taiga-ui-theme.less", + "projects/shared/styles/taiga.scss", "projects/shared/styles/variables.scss", "projects/shared/styles/global.scss", "projects/shared/styles/shared.scss", @@ -299,9 +307,16 @@ "glob": "**/*.svg", "input": "node_modules/ionicons/dist/ionicons/svg", "output": "./svg" + }, + { + "glob": "**/*", + "input": "node_modules/@taiga-ui/icons/src", + "output": "assets/taiga-ui/icons" } ], "styles": [ + "node_modules/@taiga-ui/core/styles/taiga-ui-theme.less", + "projects/shared/styles/taiga.scss", "projects/shared/styles/variables.scss", "projects/shared/styles/global.scss", "projects/shared/styles/shared.scss", @@ -393,136 +408,6 @@ } } }, - "diagnostic-ui": { - "projectType": "application", - "schematics": {}, - "root": "projects/diagnostic-ui", - "sourceRoot": "projects/diagnostic-ui/src", - "prefix": "app", - "architect": { - "build": { - "builder": "@angular-devkit/build-angular:browser", - "options": { - "outputPath": "dist/diagnostic-ui", - "index": "projects/diagnostic-ui/src/index.html", - "main": "projects/diagnostic-ui/src/main.ts", - "polyfills": "projects/diagnostic-ui/src/polyfills.ts", - "tsConfig": "projects/diagnostic-ui/tsconfig.json", - "inlineStyleLanguage": "scss", - "assets": [ - { - "glob": "**/*", - "input": "projects/shared/assets", - "output": "assets" - }, - { - "glob": "**/*.svg", - "input": "node_modules/ionicons/dist/ionicons/svg", - "output": "./svg" - } - ], - "styles": [ - "projects/shared/styles/variables.scss", - "projects/shared/styles/global.scss", - "projects/shared/styles/shared.scss", - "projects/diagnostic-ui/src/styles.scss" - ], - "scripts": [] - }, - "configurations": { - "production": { - "fileReplacements": [ - { - "replace": "projects/diagnostic-ui/src/environments/environment.ts", - "with": "projects/diagnostic-ui/src/environments/environment.prod.ts" - } - ], - "optimization": true, - "outputHashing": "all", - "sourceMap": false, - "namedChunks": false, - "aot": true, - "extractLicenses": true, - "vendorChunk": false, - "buildOptimizer": true, - "budgets": [ - { - "type": "initial", - "maximumWarning": "2mb", - "maximumError": "5mb" - } - ] - }, - "ci": { - "progress": false - }, - "development": { - "buildOptimizer": false, - "optimization": false, - "vendorChunk": true, - "extractLicenses": false, - "sourceMap": true, - "namedChunks": true - } - }, - "defaultConfiguration": "production" - }, - "serve": { - "builder": "@angular-devkit/build-angular:dev-server", - "options": { - "browserTarget": "diagnostic-ui:build" - }, - "configurations": { - "production": { - "browserTarget": "diagnostic-ui:build:production" - }, - "development": { - "browserTarget": "diagnostic-ui:build:development" - } - }, - "defaultConfiguration": "development" - }, - "extract-i18n": { - "builder": "@angular-devkit/build-angular:extract-i18n", - "options": { - "browserTarget": "diagnostic-ui:build" - } - }, - "lint": { - "builder": "@angular-eslint/builder:lint", - "options": { - "lintFilePatterns": [ - "projects/diagnostic-ui/src/**/*.ts", - "projects/diagnostic-ui/src/**/*.html" - ] - } - }, - "ionic-cordova-build": { - "builder": "@ionic/angular-toolkit:cordova-build", - "options": { - "browserTarget": "diagnostic-ui:build" - }, - "configurations": { - "production": { - "browserTarget": "diagnostic-ui:build:production" - } - } - }, - "ionic-cordova-serve": { - "builder": "@ionic/angular-toolkit:cordova-serve", - "options": { - "cordovaBuildTarget": "diagnostic-ui:ionic-cordova-build", - "devServerTarget": "diagnostic-ui:serve" - }, - "configurations": { - "production": { - "cordovaBuildTarget": "diagnostic-ui:ionic-cordova-build:production", - "devServerTarget": "diagnostic-ui:serve:production" - } - } - } - } - }, "marketplace": { "projectType": "library", "root": "projects/marketplace", diff --git a/frontend/ionic.config.json b/frontend/ionic.config.json index ee434f78a..c5810bc10 100644 --- a/frontend/ionic.config.json +++ b/frontend/ionic.config.json @@ -17,12 +17,6 @@ "integrations": {}, "type": "angular", "root": "projects/setup-wizard" - }, - "diagnostic-ui": { - "name": "diagnostic-ui", - "integrations": {}, - "type": "angular", - "root": "projects/diagnostic-ui" } }, "defaultProject": "ui" diff --git a/frontend/lint-staged.config.js b/frontend/lint-staged.config.js index 80ea7cf8b..731cc9d5e 100644 --- a/frontend/lint-staged.config.js +++ b/frontend/lint-staged.config.js @@ -4,7 +4,6 @@ module.exports = { 'projects/ui/**/*.ts': () => 'npm run check:ui', 'projects/shared/**/*.ts': () => 'npm run check:shared', 'projects/marketplace/**/*.ts': () => 'npm run check:marketplace', - 'projects/diagnostic-ui/**/*.ts': () => 'npm run check:dui', 'projects/install-wizard/**/*.ts': () => 'npm run check:install-wiz', 'projects/setup-wizard/**/*.ts': () => 'npm run check:setup', } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8cbbe67d5..8d70ccebf 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -25,11 +25,12 @@ "@start9labs/argon2": "^0.1.0", "@start9labs/emver": "^0.1.5", "@start9labs/start-sdk": "0.4.0-rev0.lib0.rc5", - "@taiga-ui/addon-charts": "3.28.0", - "@taiga-ui/cdk": "3.28.0", - "@taiga-ui/core": "3.28.0", - "@taiga-ui/icons": "3.28.0", - "@taiga-ui/kit": "3.28.0", + "@taiga-ui/addon-charts": "3.33.1", + "@taiga-ui/cdk": "3.33.1", + "@taiga-ui/core": "3.33.1", + "@taiga-ui/icons": "3.33.1", + "@taiga-ui/kit": "3.33.1", + "@tinkoff/ng-dompurify": "4.0.0", "angular-svg-round-progressbar": "^9.0.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", @@ -3716,9 +3717,9 @@ } }, "node_modules/@ng-web-apis/intersection-observer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@ng-web-apis/intersection-observer/-/intersection-observer-3.0.0.tgz", - "integrity": "sha512-Y3ts9WgXG/A6atyMlFOoP8ZNczUNxUGHSV4ii4xCepwcKW2gN/kkimsP4oPtb7UsTWzN1tF1n0bgD2civraZiA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@ng-web-apis/intersection-observer/-/intersection-observer-3.0.1.tgz", + "integrity": "sha512-oTQ+oA6eFt46xs5EQcpAZTlwxabEAeAcNm0/bzo/60WPX+003HUgkHO2ipwrVia2gF+w7oJa/zCFsQ4+agql9w==", "dependencies": { "tslib": "^2.2.0" }, @@ -4020,9 +4021,9 @@ } }, "node_modules/@taiga-ui/addon-charts": { - "version": "3.28.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-3.28.0.tgz", - "integrity": "sha512-ZLsOKrEfni8T+ppteJLULooRqtmvP8aZ0cf7WUEEjEeNR05out6eh8a3uHsnx241HI/or8b4OVKHbTmiFm9Mzg==", + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-3.33.1.tgz", + "integrity": "sha512-AL2rIt53hBq3fuV4wZjZvU/DXIz0bRMIS6IwN0/1J6J0dsawuvjAn71wVvTkI0ooI8OvDRsLyBSMPxl/QauN+w==", "dependencies": { "tslib": ">=2.0.0" }, @@ -4030,22 +4031,22 @@ "@angular/common": ">=12.0.0", "@angular/core": ">=12.0.0", "@ng-web-apis/common": ">=2.0.0", - "@taiga-ui/cdk": ">=3.28.0", - "@taiga-ui/core": ">=3.28.0", + "@taiga-ui/cdk": ">=3.33.1", + "@taiga-ui/core": ">=3.33.1", "@tinkoff/ng-polymorpheus": ">=4.0.0" } }, "node_modules/@taiga-ui/cdk": { - "version": "3.28.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-3.28.0.tgz", - "integrity": "sha512-U9LTaiaHABanwxssPyutqiK1I8aUKX8ZpJ3CpMvhxszHC3zMYp4/N3RvxYfI8Mb2sqeLR8D+x85EElbWQIxRkA==", + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-3.33.1.tgz", + "integrity": "sha512-Zo+3orOG9BrAgIxQhfWRU+arPYyZmOy7LNTOJzwc+jAw31mCcHZLBQ9Iys+DFBMUKhARO9PRytjKysKw0YR6QQ==", "dependencies": { "@ng-web-apis/common": "2.1.0", "@ng-web-apis/mutation-observer": "2.0.0", "@ng-web-apis/resize-observer": "2.0.0", "@tinkoff/ng-event-plugins": "3.1.0", "@tinkoff/ng-polymorpheus": "4.1.0", - "tslib": "2.5.2" + "tslib": "2.5.3" }, "optionalDependencies": { "ng-morph": "2.2.4", @@ -4059,17 +4060,12 @@ "rxjs": ">=6.0.0" } }, - "node_modules/@taiga-ui/cdk/node_modules/tslib": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.2.tgz", - "integrity": "sha512-5svOrSA2w3iGFDs1HibEVBGbDrAY82bFQ3HZ3ixB+88nsbsWQoKqDRb5UBYAUPEzbBn6dAp5gRNXglySbx1MlA==" - }, "node_modules/@taiga-ui/core": { - "version": "3.28.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-3.28.0.tgz", - "integrity": "sha512-7P62xmja4kpEwVe43zgMfSg1UmYzkdMjNr4DF1S1zU8u0gKQGYHcUFQL1hqTJk6W50xSXVyi4tlWKKCXMvEd5Q==", + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-3.33.1.tgz", + "integrity": "sha512-NIcjC9Sy0FDW8CUpq5KN803yM5XjTfoSqcbkrtHIYf90gDD/gWyJOQ6zBea+rlqjvfOcRlILu+/q+YJ48ldgWA==", "dependencies": { - "@taiga-ui/i18n": "^3.28.0", + "@taiga-ui/i18n": "^3.33.1", "tslib": ">=2.0.0" }, "peerDependencies": { @@ -4081,17 +4077,17 @@ "@angular/router": ">=12.0.0", "@ng-web-apis/common": ">=2.0.0", "@ng-web-apis/mutation-observer": ">=2.0.0", - "@taiga-ui/cdk": ">=3.28.0", - "@taiga-ui/i18n": ">=3.28.0", + "@taiga-ui/cdk": ">=3.33.1", + "@taiga-ui/i18n": ">=3.33.1", "@tinkoff/ng-event-plugins": ">=3.1.0", "@tinkoff/ng-polymorpheus": ">=4.0.0", "rxjs": ">=6.0.0" } }, "node_modules/@taiga-ui/i18n": { - "version": "3.30.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-3.30.0.tgz", - "integrity": "sha512-238T1LaNmXbo/fUTQF3D4CQDV3TXw/NTc1ObpnWNDXKY7HJXEoI/tf09RZEPlQYlDT9yJJeo1m0iYp6JGipYIQ==", + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-3.33.1.tgz", + "integrity": "sha512-XWV+fWyIBi2sr5WK2W2MBVDE+0mxiiRMORBHkpSSl3bdUUakwKzBPktNn0Q7X8RSAhABemkmUKyGAsO5YbvU2w==", "dependencies": { "tslib": ">=2.0.0" }, @@ -4101,22 +4097,22 @@ } }, "node_modules/@taiga-ui/icons": { - "version": "3.28.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-3.28.0.tgz", - "integrity": "sha512-TzQEKgRLP5f+wGsDLMqnBUYPhCN/jgRzQbOWZPIrl+CzaYQTbsFRo1YlKEfMO3Wk55R8QBKv0qpj35+i2Q8Mmg==", + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-3.33.1.tgz", + "integrity": "sha512-JPO/7vBXBtp1ryp0n8Al3wl6ah3mEb0GMKIPVycpX0fc8jccE7i/BtHc43ep1fMSWw/3Gg+6U4YWozMQkPbvNQ==", "dependencies": { "tslib": "^2.2.0" } }, "node_modules/@taiga-ui/kit": { - "version": "3.28.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-3.28.0.tgz", - "integrity": "sha512-jLi/mmIS7kqG1FEY7LT+1uH76pEAiWZsZEQH+3rOwvEGaQBjLE73OPf83f/swaYtFm/DgJemMNnfEMYu661DYA==", + "version": "3.33.1", + "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-3.33.1.tgz", + "integrity": "sha512-l2OwxFitcDJe3D2YuCm/4FYI8EwWcb2Iifp1f7k9ZRn/bFcIbT90WdZMbNW2QSdIi+/8HRpyQEFwECjSejTk1Q==", "dependencies": { - "@maskito/angular": "0.11.1", - "@maskito/core": "0.11.1", - "@maskito/kit": "0.11.1", - "@ng-web-apis/intersection-observer": "3.0.0", + "@maskito/angular": "1.0.0", + "@maskito/core": "1.0.0", + "@maskito/kit": "1.0.0", + "@ng-web-apis/intersection-observer": "3.0.1", "text-mask-core": "5.1.2", "tslib": ">=2.0.0" }, @@ -4128,17 +4124,17 @@ "@ng-web-apis/common": ">=2.0.0", "@ng-web-apis/mutation-observer": ">=2.0.0", "@ng-web-apis/resize-observer": ">=2.0.0", - "@taiga-ui/cdk": ">=3.28.0", - "@taiga-ui/core": ">=3.28.0", - "@taiga-ui/i18n": ">=3.28.0", + "@taiga-ui/cdk": ">=3.33.1", + "@taiga-ui/core": ">=3.33.1", + "@taiga-ui/i18n": ">=3.33.1", "@tinkoff/ng-polymorpheus": ">=4.0.0", "rxjs": ">=6.0.0" } }, "node_modules/@taiga-ui/kit/node_modules/@maskito/angular": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/@maskito/angular/-/angular-0.11.1.tgz", - "integrity": "sha512-80V4FT2jHv+VrJA2gRJpvWvbYVJvPHHoS0ZDqt8DZO/ejWe2SJP3+i/tFHar3i423tXk59dBLp0ahfwkaaNN1A==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@maskito/angular/-/angular-1.0.0.tgz", + "integrity": "sha512-y3uMog1Ez5l/dvWmCpiC4LnZvDvQK/JDdsVgg0YFZPQU+onnxIgdNp3S/3axN3LzuRG2bUa7xo5fBZXUt3R0JQ==", "dependencies": { "tslib": "^2.3.0" }, @@ -4146,21 +4142,35 @@ "@angular/common": ">=12.0.0", "@angular/core": ">=12.0.0", "@angular/forms": ">=12.0.0", - "@maskito/core": "^0.11.1", + "@maskito/core": "^1.0.0", "rxjs": ">=6.0.0" } }, "node_modules/@taiga-ui/kit/node_modules/@maskito/core": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/@maskito/core/-/core-0.11.1.tgz", - "integrity": "sha512-8wPNVvlf+q1g4KF1By++eppIZxYs0XWCd/dzvtbfLQRwPXIPTnp9Cm8yWFPGbUVkfA5znkpk5OiiCLzkuYYg7A==" + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@maskito/core/-/core-1.0.0.tgz", + "integrity": "sha512-zFfGkc3Ir+zNudJQF727RNkcPkwvIWI/F7UcOq4Ur2Zn/n09bYoQoW4jijJ8ZZpbf2ReCzvxFKtplGnR9s/K2Q==" }, "node_modules/@taiga-ui/kit/node_modules/@maskito/kit": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-0.11.1.tgz", - "integrity": "sha512-5P+WC/oP9Cwk2aEyxGLpy934jpOwagvm2wLGGfNLZ7D0WaXSuDtXJGizG0Yt6EOnx3/EdChwI3WcmdLhDKK+bQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@maskito/kit/-/kit-1.0.0.tgz", + "integrity": "sha512-LTgIPmJZk9VPv6/tC+goPbbM3tI/XCZBCuZGsPYiYuJdKZhyF0nIhPL/Aa7ymwcwSdkZZbFHZp2vub9xDsSoUA==", "peerDependencies": { - "@maskito/core": "^0.11.1" + "@maskito/core": "^1.0.0" + } + }, + "node_modules/@tinkoff/ng-dompurify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@tinkoff/ng-dompurify/-/ng-dompurify-4.0.0.tgz", + "integrity": "sha512-BjKUweWLrOx8UOZw+Tl+Dae5keYuSbeMkppcXQdsvwASMrPfmP7d3Q206Q6HDqOV2WnpnFqGUB95IMbLAeRRuw==", + "dependencies": { + "tslib": "^2.0.0" + }, + "peerDependencies": { + "@angular/core": ">=12.0.0", + "@angular/platform-browser": ">=12.0.0", + "@types/dompurify": ">=2.3.0", + "dompurify": ">= 2.3.0" } }, "node_modules/@tinkoff/ng-event-plugins": { @@ -4307,7 +4317,6 @@ "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/dompurify/-/dompurify-2.4.0.tgz", "integrity": "sha512-IDBwO5IZhrKvHFUl+clZxgf3hn2b/lU6H1KaBShPkQyGJUQ0xwebezIPSuiyGwfz1UzJWQl4M7BDxtHtCCPlTg==", - "dev": true, "dependencies": { "@types/trusted-types": "*" } @@ -4526,8 +4535,7 @@ "node_modules/@types/trusted-types": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.3.tgz", - "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==", - "dev": true + "integrity": "sha512-NfQ4gyz38SL8sDNrSixxU2Os1a5xcdFxipAFxYEuLUlvU2uDwS4NUpsImcf1//SlWItCVMMLiylsxbmNMToV/g==" }, "node_modules/@types/uuid": { "version": "8.3.4", diff --git a/frontend/package.json b/frontend/package.json index b92c8c172..c96e73d45 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,12 +8,10 @@ "check": "npm run check:shared && npm run check:marketplace && npm run check:ui && npm run check:install-wiz && npm run check:setup && npm run check:dui", "check:shared": "tsc --project projects/shared/tsconfig.json --noEmit --skipLibCheck", "check:marketplace": "tsc --project projects/marketplace/tsconfig.json --noEmit --skipLibCheck", - "check:dui": "tsc --project projects/diagnostic-ui/tsconfig.json --noEmit --skipLibCheck", "check:install-wiz": "tsc --project projects/install-wizard/tsconfig.json --noEmit --skipLibCheck", "check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck", "check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck", "build:deps": "rm -rf .angular/cache && cd ../patch-db/client && npm ci && npm run build", - "build:dui": "ng run diagnostic-ui:build", "build:install-wiz": "ng run install-wizard:build", "build:setup": "ng run setup-wizard:build", "build:ui": "ng run ui:build", @@ -25,7 +23,6 @@ "analyze:ui": "webpack-bundle-analyzer dist/ui/stats.json", "publish:shared": "npm run build:shared && npm publish ./dist/shared --access public", "publish:marketplace": "npm run build:marketplace && npm publish ./dist/marketplace --access public", - "start:dui": "npm run-script build-config && ionic serve --project diagnostic-ui --host 0.0.0.0", "start:install-wiz": "npm run-script build-config && ionic serve --project install-wizard --host 0.0.0.0", "start:setup": "npm run-script build-config && ionic serve --project setup-wizard --host 0.0.0.0", "start:ui": "npm run-script build-config && ionic serve --project ui --ip --host 0.0.0.0", @@ -49,11 +46,12 @@ "@materia-ui/ngx-monaco-editor": "^6.0.0", "@start9labs/argon2": "^0.1.0", "@start9labs/emver": "^0.1.5", - "@taiga-ui/addon-charts": "3.28.0", - "@taiga-ui/cdk": "3.28.0", - "@taiga-ui/core": "3.28.0", - "@taiga-ui/icons": "3.28.0", - "@taiga-ui/kit": "3.28.0", + "@taiga-ui/addon-charts": "3.33.1", + "@taiga-ui/cdk": "3.33.1", + "@taiga-ui/core": "3.33.1", + "@taiga-ui/icons": "3.33.1", + "@taiga-ui/kit": "3.33.1", + "@tinkoff/ng-dompurify": "4.0.0", "angular-svg-round-progressbar": "^9.0.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", diff --git a/frontend/projects/diagnostic-ui/src/app/app-routing.module.ts b/frontend/projects/diagnostic-ui/src/app/app-routing.module.ts deleted file mode 100644 index fffdfeece..000000000 --- a/frontend/projects/diagnostic-ui/src/app/app-routing.module.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { NgModule } from '@angular/core' -import { PreloadAllModules, RouterModule, Routes } from '@angular/router' - -const routes: Routes = [ - { - path: '', - loadChildren: () => import('./pages/home/home.module').then( m => m.HomePageModule) - }, - { - path: 'logs', - loadChildren: () => import('./pages/logs/logs.module').then( m => m.LogsPageModule) - }, -] - -@NgModule({ - imports: [ - RouterModule.forRoot(routes, { - scrollPositionRestoration: 'enabled', - preloadingStrategy: PreloadAllModules, - useHash: true, - }) - ], - exports: [RouterModule] -}) -export class AppRoutingModule { } diff --git a/frontend/projects/diagnostic-ui/src/app/app.component.html b/frontend/projects/diagnostic-ui/src/app/app.component.html deleted file mode 100644 index cd28a7e80..000000000 --- a/frontend/projects/diagnostic-ui/src/app/app.component.html +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontend/projects/diagnostic-ui/src/app/app.component.scss b/frontend/projects/diagnostic-ui/src/app/app.component.scss deleted file mode 100644 index b528fd9bd..000000000 --- a/frontend/projects/diagnostic-ui/src/app/app.component.scss +++ /dev/null @@ -1,8 +0,0 @@ -:host { - display: block; - height: 100%; -} - -tui-root { - height: 100%; -} diff --git a/frontend/projects/diagnostic-ui/src/app/app.component.ts b/frontend/projects/diagnostic-ui/src/app/app.component.ts deleted file mode 100644 index 5ac82a652..000000000 --- a/frontend/projects/diagnostic-ui/src/app/app.component.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Component } from '@angular/core' - -@Component({ - selector: 'app-root', - templateUrl: 'app.component.html', - styleUrls: ['app.component.scss'], -}) -export class AppComponent { - constructor() {} -} diff --git a/frontend/projects/diagnostic-ui/src/app/app.module.ts b/frontend/projects/diagnostic-ui/src/app/app.module.ts deleted file mode 100644 index 1abde53a3..000000000 --- a/frontend/projects/diagnostic-ui/src/app/app.module.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { NgModule } from '@angular/core' -import { BrowserAnimationsModule } from '@angular/platform-browser/animations' -import { RouteReuseStrategy } from '@angular/router' -import { IonicModule, IonicRouteStrategy } from '@ionic/angular' -import { TuiRootModule } from '@taiga-ui/core' -import { AppComponent } from './app.component' -import { AppRoutingModule } from './app-routing.module' -import { HttpClientModule } from '@angular/common/http' -import { ApiService } from './services/api/api.service' -import { MockApiService } from './services/api/mock-api.service' -import { LiveApiService } from './services/api/live-api.service' -import { RELATIVE_URL, WorkspaceConfig } from '@start9labs/shared' - -const { - useMocks, - ui: { api }, -} = require('../../../../config.json') as WorkspaceConfig - -@NgModule({ - declarations: [AppComponent], - imports: [ - HttpClientModule, - BrowserAnimationsModule, - IonicModule.forRoot({ - mode: 'md', - }), - AppRoutingModule, - TuiRootModule, - ], - providers: [ - { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, - { - provide: ApiService, - useClass: useMocks ? MockApiService : LiveApiService, - }, - { - provide: RELATIVE_URL, - useValue: `/${api.url}/${api.version}`, - }, - ], - bootstrap: [AppComponent], -}) -export class AppModule {} diff --git a/frontend/projects/diagnostic-ui/src/app/pages/home/home-routing.module.ts b/frontend/projects/diagnostic-ui/src/app/pages/home/home-routing.module.ts deleted file mode 100644 index 6ac28af67..000000000 --- a/frontend/projects/diagnostic-ui/src/app/pages/home/home-routing.module.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { NgModule } from '@angular/core' -import { RouterModule, Routes } from '@angular/router' -import { HomePage } from './home.page' - -const routes: Routes = [ - { - path: '', - component: HomePage, - } -]; - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule] -}) -export class HomePageRoutingModule {} diff --git a/frontend/projects/diagnostic-ui/src/app/pages/home/home.module.ts b/frontend/projects/diagnostic-ui/src/app/pages/home/home.module.ts deleted file mode 100644 index 63184b7a2..000000000 --- a/frontend/projects/diagnostic-ui/src/app/pages/home/home.module.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { FormsModule } from '@angular/forms' -import { HomePage } from './home.page' -import { HomePageRoutingModule } from './home-routing.module' - - -@NgModule({ - imports: [ - CommonModule, - FormsModule, - IonicModule, - HomePageRoutingModule - ], - declarations: [HomePage] -}) -export class HomePageModule {} diff --git a/frontend/projects/diagnostic-ui/src/app/pages/home/home.page.html b/frontend/projects/diagnostic-ui/src/app/pages/home/home.page.html deleted file mode 100644 index 9cba08258..000000000 --- a/frontend/projects/diagnostic-ui/src/app/pages/home/home.page.html +++ /dev/null @@ -1,81 +0,0 @@ - -
- -

- StartOS - Diagnostic Mode -

- - -

- StartOS launch error: -

-
- - {{ error.problem }} - -
-
- {{ error.details }} -
-
-
- View Logs -

- Possible solutions: -

-
- {{ error.solution }} -
- Restart Server - - {{ error.code === 15 ? 'Setup Current Drive' : 'Enter Recovery Mode' - }} - - -
- - System Rebuild - -
- -
- - Repair Drive - -
-
-
- - -

- Server is restarting -

-

- Wait for the server to restart, then refresh this page. -

- Refresh -
-
-
diff --git a/frontend/projects/diagnostic-ui/src/app/pages/home/home.page.scss b/frontend/projects/diagnostic-ui/src/app/pages/home/home.page.scss deleted file mode 100644 index 214e26874..000000000 --- a/frontend/projects/diagnostic-ui/src/app/pages/home/home.page.scss +++ /dev/null @@ -1,5 +0,0 @@ -.code-block { - background-color: rgb(69, 69, 69); - padding: 12px; - margin-bottom: 32px; -} \ No newline at end of file diff --git a/frontend/projects/diagnostic-ui/src/environments/environment.prod.ts b/frontend/projects/diagnostic-ui/src/environments/environment.prod.ts deleted file mode 100644 index bc0327dbe..000000000 --- a/frontend/projects/diagnostic-ui/src/environments/environment.prod.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const environment = { - production: true -} diff --git a/frontend/projects/diagnostic-ui/src/environments/environment.ts b/frontend/projects/diagnostic-ui/src/environments/environment.ts deleted file mode 100644 index 745ee023b..000000000 --- a/frontend/projects/diagnostic-ui/src/environments/environment.ts +++ /dev/null @@ -1,16 +0,0 @@ -// This file can be replaced during build by using the `fileReplacements` array. -// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. -// The list of file replacements can be found in `angular.json`. - -export const environment = { - production: false -} - -/* - * For easier debugging in development mode, you can import the following file - * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. - * - * This import should be commented out in production mode because it will have a negative impact - * on performance if an error is thrown. - */ -// import 'zone.js/dist/zone-error'; // Included with Angular CLI. diff --git a/frontend/projects/diagnostic-ui/src/index.html b/frontend/projects/diagnostic-ui/src/index.html deleted file mode 100644 index 1822018f3..000000000 --- a/frontend/projects/diagnostic-ui/src/index.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - StartOS Diagnostic UI - - - - - - - - - - - - - - - diff --git a/frontend/projects/diagnostic-ui/src/main.ts b/frontend/projects/diagnostic-ui/src/main.ts deleted file mode 100644 index 21499c3cd..000000000 --- a/frontend/projects/diagnostic-ui/src/main.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { enableProdMode } from '@angular/core' -import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' -import { AppModule } from './app/app.module' -import { environment } from './environments/environment' - -if (environment.production) { - enableProdMode() -} - -platformBrowserDynamic() - .bootstrapModule(AppModule) - .catch(err => console.error(err)) diff --git a/frontend/projects/diagnostic-ui/src/polyfills.ts b/frontend/projects/diagnostic-ui/src/polyfills.ts deleted file mode 100644 index f9f1dd06f..000000000 --- a/frontend/projects/diagnostic-ui/src/polyfills.ts +++ /dev/null @@ -1,65 +0,0 @@ -/** - * This file includes polyfills needed by Angular and is loaded before the app. - * You can add your own extra polyfills to this file. - * - * This file is divided into 2 sections: - * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. - * 2. Application imports. Files imported after ZoneJS that should be loaded before your main - * file. - * - * The current setup is for so-called "evergreen" browsers; the last versions of browsers that - * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), - * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. - * - * Learn more in https://angular.io/guide/browser-support - */ - -/*************************************************************************************************** - * BROWSER POLYFILLS - */ - -/** IE11 requires the following for NgClass support on SVG elements */ -// import 'classlist.js'; // Run `npm install --save classlist.js`. - -/** - * Web Animations `@angular/platform-browser/animations` - * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. - * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). - */ -// import 'web-animations-js'; // Run `npm install --save web-animations-js`. - -/** - * By default, zone.js will patch all possible macroTask and DomEvents - * user can disable parts of macroTask/DomEvents patch by setting following flags - * because those flags need to be set before `zone.js` being loaded, and webpack - * will put import in the top of bundle, so user need to create a separate file - * in this directory (for example: zone-flags.ts), and put the following flags - * into that file, and then add the following code before importing zone.js. - * import './zone-flags'; - * - * The flags allowed in zone-flags.ts are listed here. - * - * The following flags will work for all browsers. - * - * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame - * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick - * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames - * - * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js - * with the following flag, it will bypass `zone.js` patch for IE/Edge - * - * (window as any).__Zone_enable_cross_context_check = true; - * - */ - -import './zone-flags' - -/*************************************************************************************************** - * Zone JS is required by default for Angular itself. - */ -import 'zone.js/dist/zone' // Included with Angular CLI. - - -/*************************************************************************************************** - * APPLICATION IMPORTS - */ diff --git a/frontend/projects/diagnostic-ui/src/styles.scss b/frontend/projects/diagnostic-ui/src/styles.scss deleted file mode 100644 index 07a1d8ea0..000000000 --- a/frontend/projects/diagnostic-ui/src/styles.scss +++ /dev/null @@ -1,41 +0,0 @@ -@font-face { - font-family: 'Montserrat'; - font-style: normal; - font-weight: normal; - src: url('/assets/fonts/Montserrat/Montserrat-Regular.ttf'); -} - -/** Ionic CSS Variables overrides **/ -:root { - --ion-font-family: 'Montserrat', sans-serif; - - --ion-color-primary: #0075e1; - - --ion-color-medium: #989aa2; - --ion-color-medium-rgb: 152,154,162; - --ion-color-medium-contrast: #000000; - --ion-color-medium-contrast-rgb: 0,0,0; - --ion-color-medium-shade: #86888f; - --ion-color-medium-tint: #a2a4ab; - - --ion-color-light: #222428; - --ion-color-light-rgb: 34,36,40; - --ion-color-light-contrast: #ffffff; - --ion-color-light-contrast-rgb: 255,255,255; - --ion-color-light-shade: #1e2023; - --ion-color-light-tint: #383a3e; - - --ion-item-background: #2b2b2b; - --ion-toolbar-background: #2b2b2b; - --ion-card-background: #2b2b2b; - - --ion-background-color: #282828; - --ion-background-color-rgb: 30,30,30; - --ion-text-color: var(--ion-color-dark); - --ion-text-color-rgb: var(--ion-color-dark-rgb); -} - -.loader { - --spinner-color: var(--ion-color-warning) !important; - z-index: 40000 !important; -} diff --git a/frontend/projects/diagnostic-ui/src/zone-flags.ts b/frontend/projects/diagnostic-ui/src/zone-flags.ts deleted file mode 100644 index 24ca60fe2..000000000 --- a/frontend/projects/diagnostic-ui/src/zone-flags.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * Prevents Angular change detection from - * running with certain Web Component callbacks - */ -// eslint-disable-next-line no-underscore-dangle -(window as any).__Zone_disable_customElements = true diff --git a/frontend/projects/diagnostic-ui/tsconfig.json b/frontend/projects/diagnostic-ui/tsconfig.json deleted file mode 100644 index f642f09b3..000000000 --- a/frontend/projects/diagnostic-ui/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -/* To learn more about this file see: https://angular.io/config/tsconfig. */ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "baseUrl": "./" - }, - "files": ["src/main.ts", "src/polyfills.ts"], - "include": ["src/**/*.d.ts"] -} diff --git a/frontend/projects/install-wizard/src/app/app.component.html b/frontend/projects/install-wizard/src/app/app.component.html index cd28a7e80..2d86be205 100644 --- a/frontend/projects/install-wizard/src/app/app.component.html +++ b/frontend/projects/install-wizard/src/app/app.component.html @@ -1,4 +1,5 @@ - + + diff --git a/frontend/projects/install-wizard/src/app/app.module.ts b/frontend/projects/install-wizard/src/app/app.module.ts index 1abde53a3..9cc91ba3f 100644 --- a/frontend/projects/install-wizard/src/app/app.module.ts +++ b/frontend/projects/install-wizard/src/app/app.module.ts @@ -2,14 +2,23 @@ import { NgModule } from '@angular/core' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { RouteReuseStrategy } from '@angular/router' import { IonicModule, IonicRouteStrategy } from '@ionic/angular' -import { TuiRootModule } from '@taiga-ui/core' +import { + TuiDialogModule, + TuiModeModule, + TuiRootModule, + TuiThemeNightModule, +} from '@taiga-ui/core' import { AppComponent } from './app.component' import { AppRoutingModule } from './app-routing.module' import { HttpClientModule } from '@angular/common/http' import { ApiService } from './services/api/api.service' import { MockApiService } from './services/api/mock-api.service' import { LiveApiService } from './services/api/live-api.service' -import { RELATIVE_URL, WorkspaceConfig } from '@start9labs/shared' +import { + LoadingModule, + RELATIVE_URL, + WorkspaceConfig, +} from '@start9labs/shared' const { useMocks, @@ -26,6 +35,10 @@ const { }), AppRoutingModule, TuiRootModule, + TuiDialogModule, + LoadingModule, + TuiModeModule, + TuiThemeNightModule, ], providers: [ { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, diff --git a/frontend/projects/install-wizard/src/app/pages/home/home.page.ts b/frontend/projects/install-wizard/src/app/pages/home/home.page.ts index c3764a976..b2bf633ee 100644 --- a/frontend/projects/install-wizard/src/app/pages/home/home.page.ts +++ b/frontend/projects/install-wizard/src/app/pages/home/home.page.ts @@ -1,8 +1,11 @@ import { Component } from '@angular/core' -import { AlertController, IonicSlides, LoadingController } from '@ionic/angular' +import { IonicSlides } from '@ionic/angular' import { ApiService } from 'src/app/services/api/api.service' import SwiperCore, { Swiper } from 'swiper' -import { DiskInfo } from '@start9labs/shared' +import { DiskInfo, LoadingService } from '@start9labs/shared' +import { TuiDialogService } from '@taiga-ui/core' +import { TUI_PROMPT } from '@taiga-ui/kit' +import { filter } from 'rxjs' SwiperCore.use([IonicSlides]) @@ -18,9 +21,9 @@ export class HomePage { error = '' constructor( - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly api: ApiService, - private readonly alertCtrl: AlertController, + private readonly dialogs: TuiDialogService, ) {} async ngOnInit() { @@ -55,10 +58,7 @@ export class HomePage { } private async install(overwrite: boolean) { - const loader = await this.loadingCtrl.create({ - message: 'Installing StartOS...', - }) - await loader.present() + const loader = this.loader.open('Installing StartOS...').subscribe() try { await this.api.install({ @@ -69,56 +69,52 @@ export class HomePage { } catch (e: any) { this.error = e.message } finally { - loader.dismiss() + loader.unsubscribe() } } - private async presentAlertDanger() { + private presentAlertDanger() { const { vendor, model } = this.selectedDisk! - const alert = await this.alertCtrl.create({ - header: 'Warning', - message: `This action will COMPLETELY erase the disk ${ - vendor || 'Unknown Vendor' - } - ${model || 'Unknown Model'} and install StartOS in its place`, - buttons: [ - { - text: 'Cancel', - role: 'cancel', + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content: `This action will COMPLETELY erase the disk ${ + vendor || 'Unknown Vendor' + } - ${model || 'Unknown Model'} and install StartOS in its place`, + yes: 'Continue', + no: 'Cancel', }, - { - text: 'Continue', - handler: () => { - this.install(true) - }, - }, - ], - cssClass: 'alert-danger-message', - }) - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => { + this.install(true) + }) } private async presentAlertReboot() { - const alert = await this.alertCtrl.create({ - header: 'Install Success', - message: + this.dialogs + .open( 'Remove the USB stick and reboot your device to begin using your new Start9 server', - buttons: [ { - text: 'Reboot', - handler: () => { - this.reboot() - }, + label: 'Install Success', + closeable: false, + dismissible: false, + size: 's', + data: { button: 'Reboot' }, }, - ], - cssClass: 'alert-success-message', - }) - await alert.present() + ) + .subscribe({ + complete: () => { + this.reboot() + }, + }) } private async reboot() { - const loader = await this.loadingCtrl.create() - await loader.present() + const loader = this.loader.open('').subscribe() try { await this.api.reboot() @@ -126,16 +122,16 @@ export class HomePage { } catch (e: any) { this.error = e.message } finally { - loader.dismiss() + loader.unsubscribe() } } - private async presentAlertComplete() { - const alert = await this.alertCtrl.create({ - header: 'Rebooting', - message: 'Please wait for StartOS to restart, then refresh this page', - buttons: ['OK'], - }) - await alert.present() + private presentAlertComplete() { + this.dialogs + .open('Please wait for StartOS to restart, then refresh this page', { + label: 'Rebooting', + size: 's', + }) + .subscribe() } } diff --git a/frontend/projects/marketplace/package.json b/frontend/projects/marketplace/package.json index 827b6e456..85ef8b4db 100644 --- a/frontend/projects/marketplace/package.json +++ b/frontend/projects/marketplace/package.json @@ -1,12 +1,13 @@ { "name": "@start9labs/marketplace", - "version": "0.3.11", + "version": "0.3.12", "peerDependencies": { "@angular/common": ">=13.2.0", "@angular/core": ">=13.2.0", "@ionic/angular": ">=6.0.0", "@start9labs/shared": ">=0.3.0", "@taiga-ui/cdk": ">=3.0.0", + "@tinkoff/ng-dompurify": ">=4.0.0", "fuse.js": "^6.4.6" }, "dependencies": { diff --git a/frontend/projects/ui/src/app/common/store-icon/store-icon.component.html b/frontend/projects/marketplace/src/components/store-icon/store-icon.component.html similarity index 76% rename from frontend/projects/ui/src/app/common/store-icon/store-icon.component.html rename to frontend/projects/marketplace/src/components/store-icon/store-icon.component.html index 43ecb41a1..76638337e 100644 --- a/frontend/projects/ui/src/app/common/store-icon/store-icon.component.html +++ b/frontend/projects/marketplace/src/components/store-icon/store-icon.component.html @@ -1,8 +1,8 @@ diff --git a/frontend/projects/ui/src/app/common/store-icon/store-icon.component.module.ts b/frontend/projects/marketplace/src/components/store-icon/store-icon.component.module.ts similarity index 67% rename from frontend/projects/ui/src/app/common/store-icon/store-icon.component.module.ts rename to frontend/projects/marketplace/src/components/store-icon/store-icon.component.module.ts index 34b6dd2dd..5006663eb 100644 --- a/frontend/projects/ui/src/app/common/store-icon/store-icon.component.module.ts +++ b/frontend/projects/marketplace/src/components/store-icon/store-icon.component.module.ts @@ -1,10 +1,10 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' -import { GetIconPipe, StoreIconComponent } from './store-icon.component' +import { StoreIconComponent } from './store-icon.component' @NgModule({ - declarations: [StoreIconComponent, GetIconPipe], + declarations: [StoreIconComponent], imports: [CommonModule, IonicModule], exports: [StoreIconComponent], }) diff --git a/frontend/projects/ui/src/app/common/store-icon/store-icon.component.scss b/frontend/projects/marketplace/src/components/store-icon/store-icon.component.scss similarity index 100% rename from frontend/projects/ui/src/app/common/store-icon/store-icon.component.scss rename to frontend/projects/marketplace/src/components/store-icon/store-icon.component.scss diff --git a/frontend/projects/marketplace/src/components/store-icon/store-icon.component.ts b/frontend/projects/marketplace/src/components/store-icon/store-icon.component.ts new file mode 100644 index 000000000..ff4a1aeae --- /dev/null +++ b/frontend/projects/marketplace/src/components/store-icon/store-icon.component.ts @@ -0,0 +1,28 @@ +import { ChangeDetectionStrategy, Component, Input } from '@angular/core' +import { MarketplaceConfig, sameUrl } from '@start9labs/shared' + +@Component({ + selector: 'store-icon', + templateUrl: './store-icon.component.html', + styleUrls: ['./store-icon.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class StoreIconComponent { + @Input() + url = '' + @Input() + size?: string + @Input() + marketplace!: MarketplaceConfig + + get icon() { + const { start9, community } = this.marketplace + + if (sameUrl(this.url, start9)) { + return 'assets/img/icon_transparent.png' + } else if (sameUrl(this.url, community)) { + return 'assets/img/community-store.png' + } + return null + } +} diff --git a/frontend/projects/marketplace/src/pages/release-notes/release-notes.component.html b/frontend/projects/marketplace/src/pages/release-notes/release-notes.component.html index 74e34c88f..7cb79764b 100644 --- a/frontend/projects/marketplace/src/pages/release-notes/release-notes.component.html +++ b/frontend/projects/marketplace/src/pages/release-notes/release-notes.component.html @@ -1,6 +1,6 @@ -
+
diff --git a/frontend/projects/marketplace/src/pages/release-notes/release-notes.module.ts b/frontend/projects/marketplace/src/pages/release-notes/release-notes.module.ts index 583631dc4..59ec09c96 100644 --- a/frontend/projects/marketplace/src/pages/release-notes/release-notes.module.ts +++ b/frontend/projects/marketplace/src/pages/release-notes/release-notes.module.ts @@ -4,9 +4,11 @@ import { IonicModule } from '@ionic/angular' import { EmverPipesModule, MarkdownPipeModule, + SafeLinksModule, TextSpinnerComponentModule, } from '@start9labs/shared' import { TuiElementModule } from '@taiga-ui/cdk' +import { NgDompurifyModule } from '@tinkoff/ng-dompurify' import { ReleaseNotesComponent } from './release-notes.component' @@ -18,6 +20,8 @@ import { ReleaseNotesComponent } from './release-notes.component' EmverPipesModule, MarkdownPipeModule, TuiElementModule, + NgDompurifyModule, + SafeLinksModule, ], declarations: [ReleaseNotesComponent], exports: [ReleaseNotesComponent], diff --git a/frontend/projects/marketplace/src/pages/show/about/about.component.html b/frontend/projects/marketplace/src/pages/show/about/about.component.html index c1d76dd2c..bf8495095 100644 --- a/frontend/projects/marketplace/src/pages/show/about/about.component.html +++ b/frontend/projects/marketplace/src/pages/show/about/about.component.html @@ -4,7 +4,10 @@ -
+
diff --git a/frontend/projects/marketplace/src/pages/show/about/about.module.ts b/frontend/projects/marketplace/src/pages/show/about/about.module.ts index b48bbcbaa..cc7d4f234 100644 --- a/frontend/projects/marketplace/src/pages/show/about/about.module.ts +++ b/frontend/projects/marketplace/src/pages/show/about/about.module.ts @@ -2,7 +2,12 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' -import { EmverPipesModule, MarkdownPipeModule } from '@start9labs/shared' +import { + EmverPipesModule, + MarkdownPipeModule, + SafeLinksModule, +} from '@start9labs/shared' +import { NgDompurifyModule } from '@tinkoff/ng-dompurify' import { AboutComponent } from './about.component' @@ -13,6 +18,8 @@ import { AboutComponent } from './about.component' IonicModule, MarkdownPipeModule, EmverPipesModule, + NgDompurifyModule, + SafeLinksModule, ], declarations: [AboutComponent], exports: [AboutComponent], diff --git a/frontend/projects/marketplace/src/pages/show/additional/additional.component.html b/frontend/projects/marketplace/src/pages/show/additional/additional.component.html index 8937e8c74..a639cb994 100644 --- a/frontend/projects/marketplace/src/pages/show/additional/additional.component.html +++ b/frontend/projects/marketplace/src/pages/show/additional/additional.component.html @@ -18,7 +18,7 @@ *ngIf="manifest['git-hash'] as gitHash; else noHash" button detail="false" - (click)="copy(gitHash)" + (click)="copyService.copy(gitHash)" >

Git Hash

@@ -34,14 +34,39 @@
- +

Other Versions

Click to view other versions

+ + +
+ + +
+
- +

License

{{ manifest.license }}

@@ -51,7 +76,7 @@

Instructions

diff --git a/frontend/projects/marketplace/src/pages/show/additional/additional.component.scss b/frontend/projects/marketplace/src/pages/show/additional/additional.component.scss new file mode 100644 index 000000000..8508da686 --- /dev/null +++ b/frontend/projects/marketplace/src/pages/show/additional/additional.component.scss @@ -0,0 +1,10 @@ +.radio { + display: block; + margin: 1rem 0; +} + +.buttons { + display: flex; + justify-content: flex-end; + gap: 1rem; +} diff --git a/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts b/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts index 778ea6c54..8ac6164ae 100644 --- a/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts +++ b/frontend/projects/marketplace/src/pages/show/additional/additional.component.ts @@ -4,25 +4,30 @@ import { EventEmitter, Input, Output, + TemplateRef, } from '@angular/core' +import { ActivatedRoute } from '@angular/router' import { - AlertController, - ModalController, - ToastController, -} from '@ionic/angular' + TuiAlertService, + TuiDialogContext, + TuiDialogService, +} from '@taiga-ui/core' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' import { + CopyService, copyToClipboard, displayEmver, Emver, MarkdownComponent, } from '@start9labs/shared' +import { filter } from 'rxjs' import { MarketplacePkg } from '../../../types' import { AbstractMarketplaceService } from '../../../services/marketplace.service' -import { ActivatedRoute } from '@angular/router' @Component({ selector: 'marketplace-additional', templateUrl: 'additional.component.html', + styleUrls: ['additional.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, }) export class AdditionalComponent { @@ -34,68 +39,46 @@ export class AdditionalComponent { readonly url = this.route.snapshot.queryParamMap.get('url') || undefined + readonly displayEmver = displayEmver + constructor( - private readonly alertCtrl: AlertController, - private readonly modalCtrl: ModalController, + readonly copyService: CopyService, + private readonly alerts: TuiAlertService, + private readonly dialogs: TuiDialogService, private readonly emver: Emver, private readonly marketplaceService: AbstractMarketplaceService, - private readonly toastCtrl: ToastController, private readonly route: ActivatedRoute, ) {} - async copy(address: string): Promise { - const success = await copyToClipboard(address) - const message = success - ? 'Copied to clipboard!' - : 'Failed to copy to clipboard.' - - const toast = await this.toastCtrl.create({ - header: message, - position: 'bottom', - duration: 1000, - }) - await toast.present() + presentAlertVersions(version: TemplateRef) { + this.dialogs + .open(version, { + label: 'Versions', + size: 's', + data: { + value: this.pkg.manifest.version, + items: this.pkg.versions.sort( + (a, b) => -1 * (this.emver.compare(a, b) || 0), + ), + }, + }) + .pipe(filter(Boolean)) + .subscribe(version => this.version.emit(version)) } - async presentAlertVersions() { - const alert = await this.alertCtrl.create({ - header: 'Versions', - inputs: this.pkg.versions - .sort((a, b) => -1 * (this.emver.compare(a, b) || 0)) - .map(v => ({ - name: v, // for CSS - type: 'radio', - label: displayEmver(v), // appearance on screen - value: v, // literal SEM version value - checked: this.pkg.manifest.version === v, - })), - buttons: [ - { - text: 'Cancel', - role: 'cancel', + presentModalMd(label: string) { + this.dialogs + .open(new PolymorpheusComponent(MarkdownComponent), { + label, + size: 'l', + data: { + content: this.marketplaceService.fetchStatic$( + this.pkg.manifest.id, + label.toLowerCase(), + this.url, + ), }, - { - text: 'Ok', - handler: (version: string) => this.version.emit(version), - }, - ], - }) - - await alert.present() - } - - async presentModalMd(title: string) { - const content = this.marketplaceService.fetchStatic$( - this.pkg.manifest.id, - title, - this.url, - ) - - const modal = await this.modalCtrl.create({ - componentProps: { title, content }, - component: MarkdownComponent, - }) - - await modal.present() + }) + .subscribe() } } diff --git a/frontend/projects/marketplace/src/pages/show/additional/additional.module.ts b/frontend/projects/marketplace/src/pages/show/additional/additional.module.ts index 8d85c7b70..640d9e7d4 100644 --- a/frontend/projects/marketplace/src/pages/show/additional/additional.module.ts +++ b/frontend/projects/marketplace/src/pages/show/additional/additional.module.ts @@ -4,9 +4,24 @@ import { IonicModule } from '@ionic/angular' import { MarkdownModule, ResponsiveColModule } from '@start9labs/shared' import { AdditionalComponent } from './additional.component' +import { + TuiRadioListModule, + TuiStringifyContentPipeModule, +} from '@taiga-ui/kit' +import { FormsModule } from '@angular/forms' +import { TuiButtonModule } from '@taiga-ui/core' @NgModule({ - imports: [CommonModule, IonicModule, MarkdownModule, ResponsiveColModule], + imports: [ + CommonModule, + IonicModule, + MarkdownModule, + ResponsiveColModule, + TuiRadioListModule, + FormsModule, + TuiStringifyContentPipeModule, + TuiButtonModule, + ], declarations: [AdditionalComponent], exports: [AdditionalComponent], }) diff --git a/frontend/projects/marketplace/src/public-api.ts b/frontend/projects/marketplace/src/public-api.ts index fef451a3e..605b76ebe 100644 --- a/frontend/projects/marketplace/src/public-api.ts +++ b/frontend/projects/marketplace/src/public-api.ts @@ -24,6 +24,8 @@ export * from './pages/show/package/package.module' export * from './pipes/filter-packages.pipe' export * from './pipes/mime-type.pipe' +export * from './components/store-icon/store-icon.component.module' + export * from './services/marketplace.service' export * from './types' diff --git a/frontend/projects/marketplace/src/types.ts b/frontend/projects/marketplace/src/types.ts index d079985e5..7ea64c967 100644 --- a/frontend/projects/marketplace/src/types.ts +++ b/frontend/projects/marketplace/src/types.ts @@ -48,7 +48,7 @@ export interface Manifest { long: string } assets: { - icon: string // ie. icon.png + icon: Url // filename } replaces?: string[] 'release-notes': string diff --git a/frontend/projects/marketplace/tsconfig.json b/frontend/projects/marketplace/tsconfig.json index e3a6b521c..e1f4625bf 100644 --- a/frontend/projects/marketplace/tsconfig.json +++ b/frontend/projects/marketplace/tsconfig.json @@ -6,8 +6,7 @@ "outDir": "../../out-tsc/lib", "declaration": true, "declarationMap": true, - "inlineSources": true, - "types": [] + "inlineSources": true }, "exclude": ["src/test.ts", "**/*.spec.ts"] } diff --git a/frontend/projects/setup-wizard/src/app/app.component.html b/frontend/projects/setup-wizard/src/app/app.component.html index cd28a7e80..2d86be205 100644 --- a/frontend/projects/setup-wizard/src/app/app.component.html +++ b/frontend/projects/setup-wizard/src/app/app.component.html @@ -1,4 +1,5 @@ - + + diff --git a/frontend/projects/setup-wizard/src/app/app.component.ts b/frontend/projects/setup-wizard/src/app/app.component.ts index b821e089d..aee925f41 100644 --- a/frontend/projects/setup-wizard/src/app/app.component.ts +++ b/frontend/projects/setup-wizard/src/app/app.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core' import { NavController } from '@ionic/angular' import { ApiService } from './services/api/api.service' -import { ErrorToastService } from '@start9labs/shared' +import { ErrorService } from '@start9labs/shared' @Component({ selector: 'app-root', @@ -11,7 +11,7 @@ import { ErrorToastService } from '@start9labs/shared' export class AppComponent { constructor( private readonly apiService: ApiService, - private readonly errorToastService: ErrorToastService, + private readonly errorService: ErrorService, private readonly navCtrl: NavController, ) {} @@ -26,7 +26,7 @@ export class AppComponent { await this.navCtrl.navigateForward(route) } catch (e: any) { - this.errorToastService.present(e) + this.errorService.handleError(e) } } } diff --git a/frontend/projects/setup-wizard/src/app/app.module.ts b/frontend/projects/setup-wizard/src/app/app.module.ts index 0f48d072d..b346a135c 100644 --- a/frontend/projects/setup-wizard/src/app/app.module.ts +++ b/frontend/projects/setup-wizard/src/app/app.module.ts @@ -2,7 +2,14 @@ import { NgModule } from '@angular/core' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import { RouteReuseStrategy } from '@angular/router' import { HttpClientModule } from '@angular/common/http' -import { TuiRootModule } from '@taiga-ui/core' +import { + TuiAlertModule, + tuiButtonOptionsProvider, + TuiDialogModule, + TuiModeModule, + TuiRootModule, + TuiThemeNightModule, +} from '@taiga-ui/core' import { ApiService } from './services/api/api.service' import { MockApiService } from './services/api/mock-api.service' import { LiveApiService } from './services/api/live-api.service' @@ -19,6 +26,7 @@ import { LoadingPageModule } from './pages/loading/loading.module' import { RecoverPageModule } from './pages/recover/recover.module' import { TransferPageModule } from './pages/transfer/transfer.module' import { + LoadingModule, provideSetupLogsService, provideSetupService, RELATIVE_URL, @@ -46,10 +54,16 @@ const { RecoverPageModule, TransferPageModule, TuiRootModule, + TuiDialogModule, + TuiAlertModule, + LoadingModule, + TuiModeModule, + TuiThemeNightModule, ], providers: [ provideSetupService(ApiService), provideSetupLogsService(ApiService), + tuiButtonOptionsProvider({ size: 'm' }), { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, { provide: ApiService, diff --git a/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts b/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts index f10455e0c..3e26600bc 100644 --- a/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts +++ b/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.module.ts @@ -1,20 +1,26 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { FormsModule } from '@angular/forms' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { TuiButtonModule, TuiErrorModule } from '@taiga-ui/core' +import { + TuiFieldErrorPipeModule, + TuiInputModule, + TuiInputPasswordModule, +} from '@taiga-ui/kit' import { CifsModal } from './cifs-modal.page' @NgModule({ - declarations: [ - CifsModal, - ], + declarations: [CifsModal], imports: [ CommonModule, FormsModule, - IonicModule, - ], - exports: [ - CifsModal, + TuiButtonModule, + TuiInputModule, + TuiErrorModule, + ReactiveFormsModule, + TuiFieldErrorPipeModule, + TuiInputPasswordModule, ], + exports: [CifsModal], }) -export class CifsModalModule { } +export class CifsModalModule {} diff --git a/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.html b/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.html index ebec1b21f..6250ad636 100644 --- a/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.html +++ b/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.html @@ -1,94 +1,39 @@ - - - Connect Network Folder - - +
+ + Hostname + + + - - -

Hostname *

- - - -

- Hostname is required. e.g. 'My Computer' OR - 'my-computer.local' -

+ + Path + + + -

Path *

- - - -

- Path is required -

+ + Username + + + -

Username *

- - - -

- Username is required -

+ + Password + -

Password

- - - - - - -
- - - - +
+ + +
+ diff --git a/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.scss b/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.scss index db8acb8f7..5638f9537 100644 --- a/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.scss +++ b/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.scss @@ -1,16 +1,3 @@ -.item-interactive { - --highlight-background: var(--ion-color-dark) !important; +.input { + margin-top: 16px; } - -ion-item { - - &:hover { - transition-property: transform; - transform: none; - } - -} - -.item-has-focus { - --background: var(--ion-color-dark-tint) !important; -} \ No newline at end of file diff --git a/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts b/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts index 7f293f5e0..4335f8a0e 100644 --- a/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts +++ b/frontend/projects/setup-wizard/src/app/modals/cifs-modal/cifs-modal.page.ts @@ -1,94 +1,117 @@ -import { Component } from '@angular/core' +import { Component, Inject } from '@angular/core' +import { FormControl, FormGroup, Validators } from '@angular/forms' +import { TUI_VALIDATION_ERRORS } from '@taiga-ui/kit' +import { LoadingService, StartOSDiskInfo } from '@start9labs/shared' +import { TuiDialogContext, TuiDialogService } from '@taiga-ui/core' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' import { - AlertController, - LoadingController, - ModalController, -} from '@ionic/angular' -import { ApiService, CifsBackupTarget } from 'src/app/services/api/api.service' -import { StartOSDiskInfo } from '@start9labs/shared' -import { PasswordPage } from '../password/password.page' + ApiService, + CifsBackupTarget, + CifsRecoverySource, +} from 'src/app/services/api/api.service' +import { PASSWORD } from '../password/password.page' @Component({ selector: 'cifs-modal', templateUrl: 'cifs-modal.page.html', styleUrls: ['cifs-modal.page.scss'], + providers: [ + { + provide: TUI_VALIDATION_ERRORS, + useValue: { + required: 'This field is required', + }, + }, + ], }) export class CifsModal { - cifs = { - type: 'cifs' as 'cifs', - hostname: '', - path: '', - username: '', - password: '', - } + readonly form = new FormGroup({ + hostname: new FormControl('', { + validators: [ + Validators.required, + Validators.pattern(/^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$/), + ], + nonNullable: true, + }), + path: new FormControl('', { + validators: [Validators.required], + nonNullable: true, + }), + username: new FormControl('', { + validators: [Validators.required], + nonNullable: true, + }), + password: new FormControl(), + }) constructor( - private readonly modalController: ModalController, + @Inject(POLYMORPHEUS_CONTEXT) + private readonly context: TuiDialogContext<{ + cifs: CifsRecoverySource + recoveryPassword: string + }>, + private readonly dialogs: TuiDialogService, private readonly api: ApiService, - private readonly loadingCtrl: LoadingController, - private readonly alertCtrl: AlertController, + private readonly loader: LoadingService, ) {} cancel() { - this.modalController.dismiss() + this.context.$implicit.complete() } async submit(): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Connecting to shared folder...', - cssClass: 'loader', - }) - await loader.present() + const loader = this.loader + .open('Connecting to shared folder...') + .subscribe() try { const diskInfo = await this.api.verifyCifs({ - ...this.cifs, - password: this.cifs.password - ? await this.api.encrypt(this.cifs.password) + ...this.form.getRawValue(), + type: 'cifs', + password: this.form.value.password + ? await this.api.encrypt(String(this.form.value.password)) : null, }) - await loader.dismiss() + loader.unsubscribe() this.presentModalPassword(diskInfo) } catch (e) { - await loader.dismiss() + loader.unsubscribe() this.presentAlertFailed() } } - private async presentModalPassword(diskInfo: StartOSDiskInfo): Promise { + private presentModalPassword(diskInfo: StartOSDiskInfo) { const target: CifsBackupTarget = { - ...this.cifs, + ...this.form.getRawValue(), mountable: true, 'embassy-os': diskInfo, } - const modal = await this.modalController.create({ - component: PasswordPage, - componentProps: { target }, - }) - modal.onDidDismiss().then(res => { - if (res.role === 'success') { - this.modalController.dismiss( - { - cifs: this.cifs, - recoveryPassword: res.data.password, - }, - 'success', - ) - } - }) - await modal.present() + this.dialogs + .open(PASSWORD, { + label: 'Unlock Drive', + size: 's', + data: { target }, + }) + .subscribe(recoveryPassword => { + this.context.completeWith({ + cifs: { ...this.form.getRawValue(), type: 'cifs' }, + recoveryPassword, + }) + }) } - private async presentAlertFailed(): Promise { - const alert = await this.alertCtrl.create({ - header: 'Connection Failed', - message: + private presentAlertFailed() { + this.dialogs + .open( 'Unable to connect to shared folder. Ensure (1) target computer is connected to LAN, (2) target folder is being shared, and (3) hostname, path, and credentials are accurate.', - buttons: ['OK'], - }) - alert.present() + { + label: 'Connection Failed', + size: 's', + }, + ) + .subscribe() } } diff --git a/frontend/projects/setup-wizard/src/app/modals/password/password.module.ts b/frontend/projects/setup-wizard/src/app/modals/password/password.module.ts index 416c558f5..ce89d0709 100644 --- a/frontend/projects/setup-wizard/src/app/modals/password/password.module.ts +++ b/frontend/projects/setup-wizard/src/app/modals/password/password.module.ts @@ -1,20 +1,20 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { FormsModule } from '@angular/forms' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { TuiButtonModule, TuiErrorModule } from '@taiga-ui/core' +import { TuiInputPasswordModule } from '@taiga-ui/kit' import { PasswordPage } from './password.page' @NgModule({ - declarations: [ - PasswordPage, - ], + declarations: [PasswordPage], imports: [ CommonModule, FormsModule, - IonicModule, - ], - exports: [ - PasswordPage, + TuiButtonModule, + TuiInputPasswordModule, + TuiErrorModule, + ReactiveFormsModule, ], + exports: [PasswordPage], }) -export class PasswordPageModule { } +export class PasswordPageModule {} diff --git a/frontend/projects/setup-wizard/src/app/modals/password/password.page.html b/frontend/projects/setup-wizard/src/app/modals/password/password.page.html index d779077c6..10dae39cd 100644 --- a/frontend/projects/setup-wizard/src/app/modals/password/password.page.html +++ b/frontend/projects/setup-wizard/src/app/modals/password/password.page.html @@ -1,91 +1,35 @@ - - - {{ storageDrive ? 'Set Password' : 'Unlock Drive' }} - - +

+ Enter the password that was used to encrypt this drive. +

+ +

+ Choose a password for your server. + Make it good. Write it down. +

+
- -
-

- Enter the password that was used to encrypt this drive. -

- -

- Choose a password for your server. - Make it good. Write it down. -

-
- -
- - - - - - -

{{ pwError }}

- - - - - - - -

{{ verError }}

-
- -
-
-
- - - - +
+ + Enter Password + + + + + + Retype Password + + + + +
+ +
+
diff --git a/frontend/projects/setup-wizard/src/app/modals/password/password.page.scss b/frontend/projects/setup-wizard/src/app/modals/password/password.page.scss deleted file mode 100644 index d3af3bcbd..000000000 --- a/frontend/projects/setup-wizard/src/app/modals/password/password.page.scss +++ /dev/null @@ -1,21 +0,0 @@ -.item-interactive { - --highlight-background: var(--ion-color-dark) !important; -} - -ion-item { - &:hover { - transition-property: transform; - transform: none; - } -} - -.item-has-focus { - --background: var(--ion-color-dark-tint) !important; -} - -.error-message { - color: var(--ion-color-danger) !important; - font-size: .9rem !important; - margin-left: 36px; - margin-top: -16px; -} \ No newline at end of file diff --git a/frontend/projects/setup-wizard/src/app/modals/password/password.page.ts b/frontend/projects/setup-wizard/src/app/modals/password/password.page.ts index 98de93e1a..ec500c4ad 100644 --- a/frontend/projects/setup-wizard/src/app/modals/password/password.page.ts +++ b/frontend/projects/setup-wizard/src/app/modals/password/password.page.ts @@ -1,81 +1,77 @@ -import { Component, Input, ViewChild } from '@angular/core' -import { IonInput, ModalController } from '@ionic/angular' +import { Component, Inject } from '@angular/core' +import { FormControl } from '@angular/forms' +import * as argon2 from '@start9labs/argon2' +import { ErrorService } from '@start9labs/shared' +import { TuiDialogContext } from '@taiga-ui/core' +import { + PolymorpheusComponent, + POLYMORPHEUS_CONTEXT, +} from '@tinkoff/ng-polymorpheus' import { CifsBackupTarget, DiskBackupTarget, } from 'src/app/services/api/api.service' -import * as argon2 from '@start9labs/argon2' + +interface DialogData { + target?: CifsBackupTarget | DiskBackupTarget + storageDrive?: boolean +} @Component({ selector: 'app-password', templateUrl: 'password.page.html', - styleUrls: ['password.page.scss'], }) export class PasswordPage { - @ViewChild('focusInput') elem?: IonInput - @Input() target?: CifsBackupTarget | DiskBackupTarget - @Input() storageDrive = false + readonly target = this.context.data.target + readonly storageDrive = this.context.data.storageDrive + readonly password = new FormControl('', { nonNullable: true }) + readonly confirm = new FormControl('', { nonNullable: true }) - pwError = '' - password = '' - unmasked1 = false + constructor( + @Inject(POLYMORPHEUS_CONTEXT) + private readonly context: TuiDialogContext, + private readonly errorService: ErrorService, + ) {} - verError = '' - passwordVer = '' - unmasked2 = false + get passwordError(): string | null { + if (!this.password.touched || this.target) return null - constructor(private modalController: ModalController) {} + if (!this.storageDrive && !this.target?.['embassy-os']) + return 'No recovery target' // unreachable - ngAfterViewInit() { - setTimeout(() => this.elem?.setFocus(), 400) + if (this.password.value.length < 12) + return 'Must be 12 characters or greater' + + if (this.password.value.length > 64) + return 'Must be less than 65 characters' + + return null } - async verifyPw() { - if (!this.target || !this.target['embassy-os']) - this.pwError = 'No recovery target' // unreachable + get confirmError(): string | null { + return this.confirm.touched && this.password.value !== this.confirm.value + ? 'Passwords do not match' + : null + } + verifyPw() { try { const passwordHash = this.target!['embassy-os']?.['password-hash'] || '' - argon2.verify(passwordHash, this.password) - this.modalController.dismiss({ password: this.password }, 'success') + argon2.verify(passwordHash, this.password.value) + this.context.completeWith(this.password.value) } catch (e) { - this.pwError = 'Incorrect password provided' + this.errorService.handleError('Incorrect password provided') } } - async submitPw() { - this.validate() - if (this.password !== this.passwordVer) { - this.verError = '*passwords do not match' - } - - if (this.pwError || this.verError) return - this.modalController.dismiss({ password: this.password }, 'success') - } - - validate() { - if (!!this.target) return (this.pwError = '') - - if (this.passwordVer) { - this.checkVer() - } - - if (this.password.length < 12) { - this.pwError = 'Must be 12 characters or greater' - } else if (this.password.length > 64) { - this.pwError = 'Must be less than 65 characters' - } else { - this.pwError = '' - } - } - - checkVer() { - this.verError = - this.password !== this.passwordVer ? 'Passwords do not match' : '' + submitPw() { + this.context.completeWith(this.password.value) } cancel() { - this.modalController.dismiss() + this.context.$implicit.complete() } } + +export const PASSWORD = new PolymorpheusComponent(PasswordPage) diff --git a/frontend/projects/setup-wizard/src/app/pages/attach/attach.page.ts b/frontend/projects/setup-wizard/src/app/pages/attach/attach.page.ts index b4d6eb9f9..2662abc9d 100644 --- a/frontend/projects/setup-wizard/src/app/pages/attach/attach.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/attach/attach.page.ts @@ -1,13 +1,10 @@ import { Component } from '@angular/core' -import { - LoadingController, - ModalController, - NavController, -} from '@ionic/angular' +import { NavController } from '@ionic/angular' +import { DiskInfo, ErrorService, LoadingService } from '@start9labs/shared' +import { TuiDialogService } from '@taiga-ui/core' import { ApiService } from 'src/app/services/api/api.service' -import { DiskInfo, ErrorToastService } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' -import { PasswordPage } from 'src/app/modals/password/password.page' +import { PASSWORD, PasswordPage } from 'src/app/modals/password/password.page' @Component({ selector: 'app-attach', @@ -21,10 +18,10 @@ export class AttachPage { constructor( private readonly apiService: ApiService, private readonly navCtrl: NavController, - private readonly errToastService: ErrorToastService, + private readonly errorService: ErrorService, private readonly stateService: StateService, - private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, + private readonly dialogs: TuiDialogService, + private readonly loader: LoadingService, ) {} async ngOnInit() { @@ -41,38 +38,34 @@ export class AttachPage { try { this.drives = await this.apiService.getDrives() } catch (e: any) { - this.errToastService.present(e) + this.errorService.handleError(e) } finally { this.loading = false } } - async select(guid: string) { - const modal = await this.modalCtrl.create({ - component: PasswordPage, - componentProps: { storageDrive: true }, - }) - modal.onDidDismiss().then(res => { - if (res.data && res.data.password) { - this.attachDrive(guid, res.data.password) - } - }) - await modal.present() + select(guid: string) { + this.dialogs + .open(PASSWORD, { + label: 'Set Password', + size: 's', + data: { storageDrive: true }, + }) + .subscribe(password => { + this.attachDrive(guid, password) + }) } private async attachDrive(guid: string, password: string) { - const loader = await this.loadingCtrl.create({ - message: 'Connecting to drive...', - cssClass: 'loader', - }) - await loader.present() + const loader = this.loader.open('Connecting to drive...').subscribe() + try { await this.stateService.importDrive(guid, password) await this.navCtrl.navigateForward(`/loading`) } catch (e: any) { - this.errToastService.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts b/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts index 855051879..81a9faaba 100644 --- a/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/embassy/embassy.page.ts @@ -1,19 +1,22 @@ import { Component } from '@angular/core' +import { NavController } from '@ionic/angular' import { - AlertController, - LoadingController, - ModalController, - NavController, -} from '@ionic/angular' + DiskInfo, + ErrorService, + GuidPipe, + LoadingService, +} from '@start9labs/shared' +import { TuiDialogService } from '@taiga-ui/core' import { ApiService, BackupRecoverySource, DiskRecoverySource, DiskMigrateSource, } from 'src/app/services/api/api.service' -import { DiskInfo, ErrorToastService, GuidPipe } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' -import { PasswordPage } from '../../modals/password/password.page' +import { PASSWORD, PasswordPage } from '../../modals/password/password.page' +import { TUI_PROMPT } from '@taiga-ui/kit' +import { filter, of, switchMap } from 'rxjs' @Component({ selector: 'app-embassy', @@ -28,11 +31,10 @@ export class EmbassyPage { constructor( private readonly apiService: ApiService, private readonly navCtrl: NavController, - private readonly modalController: ModalController, - private readonly alertCtrl: AlertController, + private readonly dialogs: TuiDialogService, private readonly stateService: StateService, - private readonly loadingCtrl: LoadingController, - private readonly errorToastService: ErrorToastService, + private readonly loader: LoadingService, + private readonly errorService: ErrorService, private readonly guidPipe: GuidPipe, ) {} @@ -77,87 +79,71 @@ export class EmbassyPage { }) } } catch (e: any) { - this.errorToastService.present(e) + this.errorService.handleError(e) } finally { this.loading = false } } - async chooseDrive(drive: DiskInfo) { - if ( - this.guidPipe.transform(drive) || - !!drive.partitions.find(p => p.used) - ) { - const alert = await this.alertCtrl.create({ - header: 'Warning', - subHeader: 'Drive contains data!', - message: 'All data stored on this drive will be permanently deleted.', - buttons: [ - { - role: 'cancel', - text: 'Cancel', - }, - { - text: 'Continue', - handler: () => { - // for backup recoveries - if (this.stateService.recoveryPassword) { - this.setupEmbassy( - drive.logicalname, - this.stateService.recoveryPassword, - ) - } else { - // for migrations and fresh setups - this.presentModalPassword(drive.logicalname) - } - }, - }, - ], + chooseDrive(drive: DiskInfo) { + of(!this.guidPipe.transform(drive) && !drive.partitions.some(p => p.used)) + .pipe( + switchMap(unused => + unused + ? of(true) + : this.dialogs.open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content: + 'Drive contains data!

All data stored on this drive will be permanently deleted.

', + yes: 'Continue', + no: 'Cancel', + }, + }), + ), + ) + .pipe(filter(Boolean)) + .subscribe(() => { + // for backup recoveries + if (this.stateService.recoveryPassword) { + this.setupEmbassy( + drive.logicalname, + this.stateService.recoveryPassword, + ) + } else { + // for migrations and fresh setups + this.presentModalPassword(drive.logicalname) + } }) - await alert.present() - } else { - // for backup recoveries - if (this.stateService.recoveryPassword) { - this.setupEmbassy(drive.logicalname, this.stateService.recoveryPassword) - } else { - // for migrations and fresh setups - this.presentModalPassword(drive.logicalname) - } - } } - private async presentModalPassword(logicalname: string): Promise { - const modal = await this.modalController.create({ - component: PasswordPage, - componentProps: { - storageDrive: true, - }, - }) - modal.onDidDismiss().then(async ret => { - if (!ret.data || !ret.data.password) return - this.setupEmbassy(logicalname, ret.data.password) - }) - await modal.present() + private presentModalPassword(logicalname: string) { + this.dialogs + .open(PASSWORD, { + label: 'Set Password', + size: 's', + data: { storageDrive: true }, + }) + .subscribe(password => { + this.setupEmbassy(logicalname, password) + }) } private async setupEmbassy( logicalname: string, password: string, ): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Connecting to drive...', - cssClass: 'loader', - }) - await loader.present() + const loader = this.loader.open('Connecting to drive...').subscribe() try { await this.stateService.setupEmbassy(logicalname, password) await this.navCtrl.navigateForward(`/loading`) } catch (e: any) { - this.errorToastService.present(e) + this.errorService.handleError(e) console.error(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/frontend/projects/setup-wizard/src/app/pages/home/home.page.ts b/frontend/projects/setup-wizard/src/app/pages/home/home.page.ts index c0e93d18a..88ab04160 100644 --- a/frontend/projects/setup-wizard/src/app/pages/home/home.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/home/home.page.ts @@ -2,7 +2,7 @@ import { Component } from '@angular/core' import { IonicSlides } from '@ionic/angular' import { ApiService } from 'src/app/services/api/api.service' import SwiperCore, { Swiper } from 'swiper' -import { ErrorToastService } from '@start9labs/shared' +import { ErrorService } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' SwiperCore.use([IonicSlides]) @@ -19,7 +19,7 @@ export class HomePage { constructor( private readonly api: ApiService, - private readonly errToastService: ErrorToastService, + private readonly errorService: ErrorService, private readonly stateService: StateService, ) {} @@ -33,7 +33,7 @@ export class HomePage { await this.api.getPubKey() } catch (e: any) { this.error = true - this.errToastService.present(e) + this.errorService.handleError(e) } finally { this.loading = false } diff --git a/frontend/projects/setup-wizard/src/app/pages/loading/loading.module.ts b/frontend/projects/setup-wizard/src/app/pages/loading/loading.module.ts index 9c7ae1bc9..3de110846 100644 --- a/frontend/projects/setup-wizard/src/app/pages/loading/loading.module.ts +++ b/frontend/projects/setup-wizard/src/app/pages/loading/loading.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' -import { LoadingModule } from '@start9labs/shared' +import { InitializingModule } from '@start9labs/shared' import { LoadingPage } from './loading.page' const routes: Routes = [ @@ -11,7 +11,7 @@ const routes: Routes = [ ] @NgModule({ - imports: [LoadingModule, RouterModule.forChild(routes)], + imports: [InitializingModule, RouterModule.forChild(routes)], declarations: [LoadingPage], }) export class LoadingPageModule {} diff --git a/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.html b/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.html index 559705a7f..54609eb9a 100644 --- a/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.html +++ b/frontend/projects/setup-wizard/src/app/pages/loading/loading.page.html @@ -1,5 +1,5 @@ - +> diff --git a/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts b/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts index a8cd194ba..66cab3eff 100644 --- a/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/recover/recover.page.ts @@ -1,10 +1,17 @@ import { Component, Input } from '@angular/core' -import { ModalController, NavController } from '@ionic/angular' +import { NavController } from '@ionic/angular' import { CifsModal } from 'src/app/modals/cifs-modal/cifs-modal.page' -import { ApiService, DiskBackupTarget } from 'src/app/services/api/api.service' -import { ErrorToastService } from '@start9labs/shared' +import { + ApiService, + CifsRecoverySource, + DiskBackupTarget, +} from 'src/app/services/api/api.service' +import { ErrorService } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' -import { PasswordPage } from '../../modals/password/password.page' +import { PASSWORD } from '../../modals/password/password.page' +import { TuiDialogService } from '@taiga-ui/core' +import { filter } from 'rxjs' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' @Component({ selector: 'app-recover', @@ -18,9 +25,8 @@ export class RecoverPage { constructor( private readonly apiService: ApiService, private readonly navCtrl: NavController, - private readonly modalCtrl: ModalController, - private readonly modalController: ModalController, - private readonly errToastService: ErrorToastService, + private readonly dialogs: TuiDialogService, + private readonly errorService: ErrorService, private readonly stateService: StateService, ) {} @@ -62,34 +68,28 @@ export class RecoverPage { }) }) } catch (e: any) { - this.errToastService.present(e) + this.errorService.handleError(e) } finally { this.loading = false } } - async presentModalCifs(): Promise { - const modal = await this.modalCtrl.create({ - component: CifsModal, - }) - modal.onDidDismiss().then(res => { - if (res.role === 'success') { - const { hostname, path, username, password } = res.data.cifs + presentModalCifs() { + this.dialogs + .open<{ cifs: CifsRecoverySource; recoveryPassword: string }>( + new PolymorpheusComponent(CifsModal), + { + label: 'Connect Network Folder', + }, + ) + .subscribe(({ cifs, recoveryPassword }) => { this.stateService.recoverySource = { type: 'backup', - target: { - type: 'cifs', - hostname, - path, - username, - password, - }, + target: cifs, } - this.stateService.recoveryPassword = res.data.recoveryPassword + this.stateService.recoveryPassword = recoveryPassword this.navCtrl.navigateForward('/storage') - } - }) - await modal.present() + }) } async select(target: DiskBackupTarget) { @@ -97,17 +97,16 @@ export class RecoverPage { if (!logicalname) return - const modal = await this.modalController.create({ - component: PasswordPage, - componentProps: { target }, - cssClass: 'alertlike-modal', - }) - modal.onDidDismiss().then(res => { - if (res.data?.password) { - this.selectRecoverySource(logicalname, res.data.password) - } - }) - await modal.present() + this.dialogs + .open(PASSWORD, { + label: 'Unlock Drive', + size: 's', + data: { target }, + }) + .pipe(filter(Boolean)) + .subscribe(password => { + this.selectRecoverySource(logicalname, password) + }) } private async selectRecoverySource(logicalname: string, password?: string) { diff --git a/frontend/projects/setup-wizard/src/app/pages/success/success.page.ts b/frontend/projects/setup-wizard/src/app/pages/success/success.page.ts index 4ea73e619..17da17223 100644 --- a/frontend/projects/setup-wizard/src/app/pages/success/success.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/success/success.page.ts @@ -1,6 +1,6 @@ import { DOCUMENT } from '@angular/common' import { Component, ElementRef, Inject, NgZone, ViewChild } from '@angular/core' -import { DownloadHTMLService, ErrorToastService } from '@start9labs/shared' +import { DownloadHTMLService, ErrorService } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/api.service' import { StateService } from 'src/app/services/state.service' @@ -12,7 +12,8 @@ import { StateService } from 'src/app/services/state.service' }) export class SuccessPage { @ViewChild('canvas', { static: true }) - private canvas: ElementRef = {} as ElementRef + private canvas: ElementRef = + {} as ElementRef private ctx: CanvasRenderingContext2D = {} as CanvasRenderingContext2D torAddress?: string @@ -28,7 +29,7 @@ export class SuccessPage { constructor( @Inject(DOCUMENT) private readonly document: Document, - private readonly errCtrl: ErrorToastService, + private readonly errorService: ErrorService, private readonly stateService: StateService, private readonly api: ApiService, private readonly downloadHtml: DownloadHTMLService, @@ -55,7 +56,7 @@ export class SuccessPage { await this.api.exit() } } catch (e: any) { - await this.errCtrl.present(e) + await this.errorService.handleError(e) } } diff --git a/frontend/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts b/frontend/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts index 5de21a289..8cf58d7fa 100644 --- a/frontend/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts +++ b/frontend/projects/setup-wizard/src/app/pages/transfer/transfer.page.ts @@ -1,8 +1,11 @@ import { Component } from '@angular/core' -import { AlertController, NavController } from '@ionic/angular' +import { NavController } from '@ionic/angular' import { ApiService } from 'src/app/services/api/api.service' -import { DiskInfo, ErrorToastService } from '@start9labs/shared' +import { DiskInfo, ErrorService } from '@start9labs/shared' import { StateService } from 'src/app/services/state.service' +import { TuiDialogService } from '@taiga-ui/core' +import { TUI_PROMPT } from '@taiga-ui/kit' +import { filter } from 'rxjs' @Component({ selector: 'app-transfer', @@ -16,8 +19,8 @@ export class TransferPage { constructor( private readonly apiService: ApiService, private readonly navCtrl: NavController, - private readonly alertCtrl: AlertController, - private readonly errToastService: ErrorToastService, + private readonly dialogs: TuiDialogService, + private readonly errorService: ErrorService, private readonly stateService: StateService, ) {} @@ -35,34 +38,31 @@ export class TransferPage { try { this.drives = await this.apiService.getDrives() } catch (e: any) { - this.errToastService.present(e) + this.errorService.handleError(e) } finally { this.loading = false } } - async select(guid: string) { - const alert = await this.alertCtrl.create({ - header: 'Warning', - message: - 'After transferring data from this drive, do not attempt to boot into it again as a Start9 Server. This may result in services malfunctioning, data corruption, or loss of funds.', - buttons: [ - { - role: 'cancel', - text: 'Cancel', + select(guid: string) { + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content: + 'After transferring data from this drive, do not attempt to boot into it again as a Start9 Server. This may result in services malfunctioning, data corruption, or loss of funds.', + yes: 'Continue', + no: 'Cancel', }, - { - text: 'Continue', - handler: () => { - this.stateService.recoverySource = { - type: 'migrate', - guid, - } - this.navCtrl.navigateForward(`/storage`) - }, - }, - ], - }) - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => { + this.stateService.recoverySource = { + type: 'migrate', + guid, + } + this.navCtrl.navigateForward(`/storage`) + }) } } diff --git a/frontend/projects/shared/assets/fonts/Redacted/redacted.regular.ttf b/frontend/projects/shared/assets/fonts/Redacted/redacted.regular.ttf deleted file mode 100644 index 3bc1fe32c..000000000 Binary files a/frontend/projects/shared/assets/fonts/Redacted/redacted.regular.ttf and /dev/null differ diff --git a/frontend/projects/shared/package.json b/frontend/projects/shared/package.json index a2bbec95f..64e244562 100644 --- a/frontend/projects/shared/package.json +++ b/frontend/projects/shared/package.json @@ -10,6 +10,8 @@ "@ng-web-apis/resize-observer": ">=2.0.0", "@start9labs/emver": "^0.1.5", "@taiga-ui/cdk": ">=3.0.0", + "@taiga-ui/core": ">=3.0.0", + "@tinkoff/ng-dompurify": ">=4.0.0", "ansi-to-html": "^0.7.2" }, "exports": { diff --git a/frontend/projects/shared/src/components/alert/alert-button.directive.ts b/frontend/projects/shared/src/components/alert/alert-button.directive.ts deleted file mode 100644 index fc5320edb..000000000 --- a/frontend/projects/shared/src/components/alert/alert-button.directive.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Directive, ElementRef, Input } from '@angular/core' -import { AlertButton } from '@ionic/angular' - -@Directive({ - selector: `button[alertButton], a[alertButton]`, -}) -export class AlertButtonDirective implements AlertButton { - @Input() - icon?: string - - @Input() - role?: 'cancel' | 'destructive' | string - - handler = () => { - this.elementRef.nativeElement.click() - - return false - } - - constructor(private readonly elementRef: ElementRef) {} - - get text(): string { - return this.elementRef.nativeElement.textContent?.trim() || '' - } - - get cssClass(): string[] { - return Array.from(this.elementRef.nativeElement.classList) - } -} diff --git a/frontend/projects/shared/src/components/alert/alert-input.directive.ts b/frontend/projects/shared/src/components/alert/alert-input.directive.ts deleted file mode 100644 index af7879e37..000000000 --- a/frontend/projects/shared/src/components/alert/alert-input.directive.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Directive, ElementRef, Input } from '@angular/core' -import { AlertInput } from '@ionic/angular' - -@Directive({ - selector: `input[alertInput], textarea[alertInput]`, -}) -export class AlertInputDirective implements AlertInput { - @Input() - value?: T - - @Input() - label?: string - - constructor(private readonly elementRef: ElementRef) {} - - get checked(): boolean { - return this.elementRef.nativeElement.checked - } - - get name(): string { - return this.elementRef.nativeElement.name - } - - get type(): AlertInput['type'] { - return this.elementRef.nativeElement.type as AlertInput['type'] - } -} diff --git a/frontend/projects/shared/src/components/alert/alert.component.ts b/frontend/projects/shared/src/components/alert/alert.component.ts deleted file mode 100644 index 522ba93d3..000000000 --- a/frontend/projects/shared/src/components/alert/alert.component.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { - AfterViewInit, - ChangeDetectionStrategy, - Component, - ContentChildren, - ElementRef, - EventEmitter, - Input, - OnDestroy, - Output, - QueryList, - ViewChild, -} from '@angular/core' -import { AlertController, AlertOptions, IonicSafeString } from '@ionic/angular' -import { OverlayEventDetail } from '@ionic/core' -import { AlertButtonDirective } from './alert-button.directive' -import { AlertInputDirective } from './alert-input.directive' - -@Component({ - selector: 'alert', - template: ` -
- - - `, - styles: [':host { display: none !important; }'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class AlertComponent implements AfterViewInit, OnDestroy { - @Output() - readonly dismiss = new EventEmitter>() - - @Input() - header = '' - - @Input() - subHeader = '' - - @Input() - backdropDismiss = true - - @ViewChild('message', { static: true }) - private readonly content?: ElementRef - - @ContentChildren(AlertButtonDirective) - private readonly buttons: QueryList = new QueryList() - - @ContentChildren(AlertInputDirective) - private readonly inputs: QueryList> = new QueryList() - - private alert?: HTMLIonAlertElement - - constructor( - private readonly elementRef: ElementRef, - private readonly controller: AlertController, - ) {} - - get cssClass(): string[] { - return Array.from(this.elementRef.nativeElement.classList) - } - - get message(): IonicSafeString { - return new IonicSafeString(this.content?.nativeElement.innerHTML || '') - } - - async ngAfterViewInit() { - this.alert = await this.controller.create(this.getOptions()) - this.alert.onDidDismiss().then(event => { - this.dismiss.emit(event) - }) - - await this.alert.present() - } - - async ngOnDestroy() { - await this.alert?.dismiss() - } - - private getOptions(): AlertOptions { - const { - header, - subHeader, - message, - cssClass, - buttons, - inputs, - backdropDismiss, - } = this - return { - header, - subHeader, - message, - cssClass, - backdropDismiss, - buttons: buttons.toArray(), - inputs: inputs.toArray(), - } - } -} diff --git a/frontend/projects/shared/src/components/alert/alert.module.ts b/frontend/projects/shared/src/components/alert/alert.module.ts deleted file mode 100644 index 45fa01f55..000000000 --- a/frontend/projects/shared/src/components/alert/alert.module.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { NgModule } from '@angular/core' -import { AlertComponent } from './alert.component' -import { AlertButtonDirective } from './alert-button.directive' -import { AlertInputDirective } from './alert-input.directive' - -@NgModule({ - declarations: [AlertComponent, AlertButtonDirective, AlertInputDirective], - exports: [AlertComponent, AlertButtonDirective, AlertInputDirective], -}) -export class AlertModule {} diff --git a/frontend/projects/shared/src/components/loading/loading.component.html b/frontend/projects/shared/src/components/initializing/initializing.component.html similarity index 100% rename from frontend/projects/shared/src/components/loading/loading.component.html rename to frontend/projects/shared/src/components/initializing/initializing.component.html diff --git a/frontend/projects/shared/src/components/initializing/initializing.component.scss b/frontend/projects/shared/src/components/initializing/initializing.component.scss new file mode 100644 index 000000000..f21705ce5 --- /dev/null +++ b/frontend/projects/shared/src/components/initializing/initializing.component.scss @@ -0,0 +1,18 @@ +ion-card-title { + font-size: 42px; +} + +.progress { + max-width: 700px; + padding-bottom: 20px; + margin: auto auto 40px; +} + +.logs-container { + margin-top: 24px; + height: 280px; + text-align: left; + overflow: hidden; + border-radius: 31px; + margin-inline: 10px; +} diff --git a/frontend/projects/shared/src/components/initializing/initializing.component.ts b/frontend/projects/shared/src/components/initializing/initializing.component.ts new file mode 100644 index 000000000..e72cecb9e --- /dev/null +++ b/frontend/projects/shared/src/components/initializing/initializing.component.ts @@ -0,0 +1,35 @@ +import { Component, inject, Input, Output } from '@angular/core' +import { delay, filter } from 'rxjs' +import { SetupService } from '../../services/setup.service' + +@Component({ + selector: 'app-initializing', + templateUrl: 'initializing.component.html', + styleUrls: ['initializing.component.scss'], +}) +export class InitializingComponent { + readonly progress$ = inject(SetupService) + + @Input() + setupType?: 'fresh' | 'restore' | 'attach' | 'transfer' + + @Output() + readonly finished = this.progress$.pipe( + filter(progress => progress === 1), + delay(500), + ) + + getMessage(progress: number | null): string { + if (['fresh', 'attach'].includes(this.setupType || '')) { + return 'Setting up your server' + } + + if (!progress) { + return 'Preparing data. This can take a while' + } else if (progress < 1) { + return 'Copying data' + } else { + return 'Finalizing' + } + } +} diff --git a/frontend/projects/shared/src/components/initializing/initializing.module.ts b/frontend/projects/shared/src/components/initializing/initializing.module.ts new file mode 100644 index 000000000..daa025aa3 --- /dev/null +++ b/frontend/projects/shared/src/components/initializing/initializing.module.ts @@ -0,0 +1,14 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { TuiLetModule } from '@taiga-ui/cdk' + +import { LogsWindowComponent } from './logs-window/logs-window.component' +import { InitializingComponent } from './initializing.component' + +@NgModule({ + imports: [CommonModule, IonicModule, TuiLetModule], + declarations: [InitializingComponent, LogsWindowComponent], + exports: [InitializingComponent], +}) +export class InitializingModule {} diff --git a/frontend/projects/shared/src/components/loading/logs-window/logs-window.component.html b/frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.html similarity index 100% rename from frontend/projects/shared/src/components/loading/logs-window/logs-window.component.html rename to frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.html diff --git a/frontend/projects/shared/src/components/loading/logs-window/logs-window.component.scss b/frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.scss similarity index 100% rename from frontend/projects/shared/src/components/loading/logs-window/logs-window.component.scss rename to frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.scss diff --git a/frontend/projects/shared/src/components/loading/logs-window/logs-window.component.ts b/frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.ts similarity index 96% rename from frontend/projects/shared/src/components/loading/logs-window/logs-window.component.ts rename to frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.ts index 4378be4af..6439ba062 100644 --- a/frontend/projects/shared/src/components/loading/logs-window/logs-window.component.ts +++ b/frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.ts @@ -6,8 +6,8 @@ import { SetupLogsService } from '../../../services/setup-logs.service' import { Log } from '../../../types/api' import { toLocalIsoString } from '../../../util/to-local-iso-string' -var Convert = require('ansi-to-html') -var convert = new Convert({ +const Convert = require('ansi-to-html') +const convert = new Convert({ bg: 'transparent', }) diff --git a/frontend/projects/shared/src/components/loading/loading.component.scss b/frontend/projects/shared/src/components/loading/loading.component.scss index f21705ce5..9a7d10100 100644 --- a/frontend/projects/shared/src/components/loading/loading.component.scss +++ b/frontend/projects/shared/src/components/loading/loading.component.scss @@ -1,18 +1,20 @@ -ion-card-title { - font-size: 42px; +@import '@taiga-ui/core/styles/taiga-ui-local'; + +:host { + @include shadow(3); + + display: flex; + align-items: center; + max-width: 80%; + margin: auto; + padding: 1.5rem; + background: var(--tui-elevation-01); + border-radius: var(--tui-radius-m); + + --tui-primary: var(--tui-warning-fill); } -.progress { - max-width: 700px; - padding-bottom: 20px; - margin: auto auto 40px; -} - -.logs-container { - margin-top: 24px; - height: 280px; - text-align: left; - overflow: hidden; - border-radius: 31px; - margin-inline: 10px; +tui-loader { + flex-shrink: 0; + min-width: 2rem; } diff --git a/frontend/projects/shared/src/components/loading/loading.component.ts b/frontend/projects/shared/src/components/loading/loading.component.ts index 3207aebb7..373f013a1 100644 --- a/frontend/projects/shared/src/components/loading/loading.component.ts +++ b/frontend/projects/shared/src/components/loading/loading.component.ts @@ -1,35 +1,17 @@ -import { Component, inject, Input, Output } from '@angular/core' -import { delay, filter } from 'rxjs' -import { SetupService } from '../../services/setup.service' +import { ChangeDetectionStrategy, Component, inject } from '@angular/core' +import { + POLYMORPHEUS_CONTEXT, + PolymorpheusContent, +} from '@tinkoff/ng-polymorpheus' @Component({ - selector: 'app-loading', - templateUrl: 'loading.component.html', - styleUrls: ['loading.component.scss'], + template: ` + + `, + styleUrls: ['./loading.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, }) export class LoadingComponent { - readonly progress$ = inject(SetupService) - - @Input() - setupType?: 'fresh' | 'restore' | 'attach' | 'transfer' - - @Output() - readonly finished = this.progress$.pipe( - filter(progress => progress === 1), - delay(500), - ) - - getMessage(progress: number | null): string { - if (['fresh', 'attach'].includes(this.setupType || '')) { - return 'Setting up your server' - } - - if (!progress) { - return 'Preparing data. This can take a while' - } else if (progress < 1) { - return 'Copying data' - } else { - return 'Finalizing' - } - } + readonly content: PolymorpheusContent = + inject(POLYMORPHEUS_CONTEXT)['content'] } diff --git a/frontend/projects/shared/src/components/loading/loading.module.ts b/frontend/projects/shared/src/components/loading/loading.module.ts index 1ffcd7e36..4a3798041 100644 --- a/frontend/projects/shared/src/components/loading/loading.module.ts +++ b/frontend/projects/shared/src/components/loading/loading.module.ts @@ -1,14 +1,13 @@ import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { TuiLetModule } from '@taiga-ui/cdk' - -import { LogsWindowComponent } from './logs-window/logs-window.component' +import { TuiLoaderModule } from '@taiga-ui/core' +import { tuiAsDialog } from '@taiga-ui/cdk' import { LoadingComponent } from './loading.component' +import { LoadingService } from './loading.service' @NgModule({ - imports: [CommonModule, IonicModule, TuiLetModule], - declarations: [LoadingComponent, LogsWindowComponent], + imports: [TuiLoaderModule], + declarations: [LoadingComponent], exports: [LoadingComponent], + providers: [tuiAsDialog(LoadingService)], }) export class LoadingModule {} diff --git a/frontend/projects/ui/src/app/common/loading/loading.service.ts b/frontend/projects/shared/src/components/loading/loading.service.ts similarity index 100% rename from frontend/projects/ui/src/app/common/loading/loading.service.ts rename to frontend/projects/shared/src/components/loading/loading.service.ts diff --git a/frontend/projects/shared/src/components/markdown/markdown.component.html b/frontend/projects/shared/src/components/markdown/markdown.component.html index 090070c4e..45271946d 100644 --- a/frontend/projects/shared/src/components/markdown/markdown.component.html +++ b/frontend/projects/shared/src/components/markdown/markdown.component.html @@ -1,29 +1,16 @@ - - - {{ title | titlecase }} - - - - - - - + + + {{ error }} + + - - - - {{ error }} - - +
-
- - - - -
+ + + diff --git a/frontend/projects/shared/src/components/markdown/markdown.component.module.ts b/frontend/projects/shared/src/components/markdown/markdown.component.module.ts index 6da4673d1..8b8e3a6fe 100644 --- a/frontend/projects/shared/src/components/markdown/markdown.component.module.ts +++ b/frontend/projects/shared/src/components/markdown/markdown.component.module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' +import { NgDompurifyModule } from '@tinkoff/ng-dompurify' import { MarkdownPipeModule } from '../../pipes/markdown/markdown.module' import { SafeLinksModule } from '../../directives/safe-links/safe-links.module' @@ -15,6 +16,7 @@ import { MarkdownComponent } from './markdown.component' MarkdownPipeModule, TextSpinnerComponentModule, SafeLinksModule, + NgDompurifyModule, ], exports: [MarkdownComponent], }) diff --git a/frontend/projects/shared/src/components/markdown/markdown.component.ts b/frontend/projects/shared/src/components/markdown/markdown.component.ts index 7e47acc39..922ad645c 100644 --- a/frontend/projects/shared/src/components/markdown/markdown.component.ts +++ b/frontend/projects/shared/src/components/markdown/markdown.component.ts @@ -1,5 +1,6 @@ -import { Component, Input } from '@angular/core' -import { ModalController } from '@ionic/angular' +import { Component, Inject } from '@angular/core' +import { TuiDialogContext } from '@taiga-ui/core' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' import { catchError, ignoreElements, @@ -10,7 +11,7 @@ import { of, } from 'rxjs' -import { getErrorMessage } from '../../services/error-toast.service' +import { getErrorMessage } from '../../services/error.service' @Component({ selector: 'markdown', @@ -18,11 +19,10 @@ import { getErrorMessage } from '../../services/error-toast.service' styleUrls: ['./markdown.component.scss'], }) export class MarkdownComponent { - @Input() content!: string | Observable - @Input() title!: string - readonly content$ = defer(() => - isObservable(this.content) ? this.content : of(this.content), + isObservable(this.context.data.content) + ? this.context.data.content + : of(this.context.data.content), ).pipe(share()) readonly error$ = this.content$.pipe( @@ -30,9 +30,15 @@ export class MarkdownComponent { catchError(e => of(getErrorMessage(e))), ) - constructor(private readonly modalCtrl: ModalController) {} + constructor( + @Inject(POLYMORPHEUS_CONTEXT) + private readonly context: TuiDialogContext< + void, + { content: string | Observable } + >, + ) {} - async dismiss() { - return this.modalCtrl.dismiss(true) + get title(): string { + return this.context.label || '' } } diff --git a/frontend/projects/shared/src/components/toast/toast-button.directive.ts b/frontend/projects/shared/src/components/toast/toast-button.directive.ts deleted file mode 100644 index 7c564961e..000000000 --- a/frontend/projects/shared/src/components/toast/toast-button.directive.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Directive, ElementRef, Input } from '@angular/core' -import { ToastButton } from '@ionic/angular' - -@Directive({ - selector: `button[toastButton], a[toastButton]`, -}) -export class ToastButtonDirective implements ToastButton { - @Input() - icon?: string - - @Input() - side?: 'start' | 'end' - - @Input() - role?: 'cancel' | string - - handler = () => { - this.elementRef.nativeElement.click() - - return false - } - - constructor(private readonly elementRef: ElementRef) {} - - get text(): string | undefined { - return this.elementRef.nativeElement.textContent?.trim() || undefined - } - - get cssClass(): string[] { - return Array.from(this.elementRef.nativeElement.classList) - } -} diff --git a/frontend/projects/shared/src/components/toast/toast.component.ts b/frontend/projects/shared/src/components/toast/toast.component.ts deleted file mode 100644 index b6431c532..000000000 --- a/frontend/projects/shared/src/components/toast/toast.component.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { - AfterViewInit, - ChangeDetectionStrategy, - Component, - ContentChildren, - ElementRef, - EventEmitter, - Input, - OnDestroy, - Output, - QueryList, - ViewChild, -} from '@angular/core' -import { IonicSafeString, ToastController, ToastOptions } from '@ionic/angular' -import { OverlayEventDetail } from '@ionic/core' -import { ToastButtonDirective } from './toast-button.directive' - -@Component({ - selector: 'toast', - template: ` -
- - `, - styles: [':host { display: none !important; }'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class ToastComponent implements AfterViewInit, OnDestroy { - @Output() - readonly dismiss = new EventEmitter>() - - @Input() - header = '' - - @Input() - duration = 0 - - @Input() - position: 'top' | 'bottom' | 'middle' = 'bottom' - - @ViewChild('message', { static: true }) - private readonly content?: ElementRef - - @ContentChildren(ToastButtonDirective) - private readonly buttons: QueryList = new QueryList() - - private toast?: HTMLIonToastElement - - constructor( - private readonly elementRef: ElementRef, - private readonly controller: ToastController, - ) {} - - get cssClass(): string[] { - return Array.from(this.elementRef.nativeElement.classList) - } - - get message(): IonicSafeString { - return new IonicSafeString(this.content?.nativeElement.innerHTML || '') - } - - async ngAfterViewInit() { - this.toast = await this.controller.create(this.getOptions()) - this.toast.onDidDismiss().then(event => { - this.dismiss.emit(event) - }) - - await this.toast.present() - } - - async ngOnDestroy() { - await this.toast?.dismiss() - } - - private getOptions(): ToastOptions { - const { header, message, duration, position, cssClass, buttons } = this - return { - header, - message, - duration, - position, - cssClass, - buttons: buttons.toArray(), - } - } -} diff --git a/frontend/projects/shared/src/components/toast/toast.module.ts b/frontend/projects/shared/src/components/toast/toast.module.ts deleted file mode 100644 index 9f5304f5d..000000000 --- a/frontend/projects/shared/src/components/toast/toast.module.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { NgModule } from '@angular/core' -import { ToastComponent } from './toast.component' -import { ToastButtonDirective } from './toast-button.directive' - -@NgModule({ - declarations: [ToastComponent, ToastButtonDirective], - exports: [ToastComponent, ToastButtonDirective], -}) -export class ToastModule {} diff --git a/frontend/projects/shared/src/directives/alert/alert.directive.ts b/frontend/projects/shared/src/directives/alert/alert.directive.ts new file mode 100644 index 000000000..c9c7e3b4a --- /dev/null +++ b/frontend/projects/shared/src/directives/alert/alert.directive.ts @@ -0,0 +1,22 @@ +import { Directive } from '@angular/core' +import { + AbstractTuiDialogDirective, + AbstractTuiDialogService, +} from '@taiga-ui/cdk' +import { TuiAlertOptions, TuiAlertService } from '@taiga-ui/core' + +// TODO: Move to Taiga UI +@Directive({ + selector: 'ng-template[tuiAlert]', + providers: [ + { + provide: AbstractTuiDialogService, + useExisting: TuiAlertService, + }, + ], + inputs: ['options: tuiAlertOptions', 'open: tuiAlert'], + outputs: ['openChange: tuiAlertChange'], +}) +export class TuiAlertDirective extends AbstractTuiDialogDirective< + TuiAlertOptions +> {} diff --git a/frontend/projects/shared/src/directives/alert/alert.module.ts b/frontend/projects/shared/src/directives/alert/alert.module.ts new file mode 100644 index 000000000..75791bd29 --- /dev/null +++ b/frontend/projects/shared/src/directives/alert/alert.module.ts @@ -0,0 +1,8 @@ +import { NgModule } from '@angular/core' +import { TuiAlertDirective } from './alert.directive' + +@NgModule({ + declarations: [TuiAlertDirective], + exports: [TuiAlertDirective], +}) +export class TuiAlertModule {} diff --git a/frontend/projects/shared/src/pipes/markdown/markdown.pipe.ts b/frontend/projects/shared/src/pipes/markdown/markdown.pipe.ts index f2c024670..bd6bb8630 100644 --- a/frontend/projects/shared/src/pipes/markdown/markdown.pipe.ts +++ b/frontend/projects/shared/src/pipes/markdown/markdown.pipe.ts @@ -1,28 +1,11 @@ import { Pipe, PipeTransform } from '@angular/core' import { marked } from 'marked' -import * as DOMPurify from 'dompurify' @Pipe({ name: 'markdown', }) export class MarkdownPipe implements PipeTransform { transform(value: string): string { - if (value && value.length > 0) { - // convert markdown to html - const html = marked(value) - // sanitize html - const sanitized = DOMPurify.sanitize(html) - // parse html to find all links - let parser = new DOMParser() - const doc = parser.parseFromString(sanitized, 'text/html') - const links = Array.from(doc.getElementsByTagName('a')) - // add target="_blank" to every link - links.forEach(link => { - link.setAttribute('target', '_blank') - }) - // return new html string - return doc.documentElement.innerHTML - } - return value + return value?.length ? marked(value) : '' } } diff --git a/frontend/projects/shared/src/public-api.ts b/frontend/projects/shared/src/public-api.ts index da0632cc1..2f0dea166 100644 --- a/frontend/projects/shared/src/public-api.ts +++ b/frontend/projects/shared/src/public-api.ts @@ -5,23 +5,21 @@ export * from './classes/http-error' export * from './classes/rpc-error' -export * from './components/alert/alert.component' -export * from './components/alert/alert.module' -export * from './components/alert/alert-button.directive' -export * from './components/alert/alert-input.directive' -export * from './components/loading/logs-window/logs-window.component' -export * from './components/loading/loading.module' +export * from './components/initializing/logs-window/logs-window.component' +export * from './components/initializing/initializing.module' +export * from './components/initializing/initializing.component' export * from './components/loading/loading.component' +export * from './components/loading/loading.module' +export * from './components/loading/loading.service' export * from './components/markdown/markdown.component' export * from './components/markdown/markdown.component.module' export * from './components/text-spinner/text-spinner.component' export * from './components/text-spinner/text-spinner.component.module' export * from './components/ticker/ticker.component' export * from './components/ticker/ticker.module' -export * from './components/toast/toast.component' -export * from './components/toast/toast.module' -export * from './components/toast/toast-button.directive' +export * from './directives/alert/alert.directive' +export * from './directives/alert/alert.module' export * from './directives/responsive-col/responsive-col.directive' export * from './directives/responsive-col/responsive-col.module' export * from './directives/responsive-col/responsive-col-viewport.directive' @@ -43,10 +41,10 @@ export * from './pipes/shared/trust.pipe' export * from './pipes/unit-conversion/unit-conversion.module' export * from './pipes/unit-conversion/unit-conversion.pipe' +export * from './services/copy.service' export * from './services/download-html.service' export * from './services/emver.service' export * from './services/error.service' -export * from './services/error-toast.service' export * from './services/http.service' export * from './services/setup.service' export * from './services/setup-logs.service' diff --git a/frontend/projects/shared/src/services/copy.service.ts b/frontend/projects/shared/src/services/copy.service.ts new file mode 100644 index 000000000..39bf3d733 --- /dev/null +++ b/frontend/projects/shared/src/services/copy.service.ts @@ -0,0 +1,16 @@ +import { inject, Injectable } from '@angular/core' +import { TuiAlertService } from '@taiga-ui/core' +import { copyToClipboard } from '../util/copy-to-clipboard' + +@Injectable({ providedIn: 'root' }) +export class CopyService { + private readonly alerts = inject(TuiAlertService) + + async copy(text: string) { + const success = await copyToClipboard(text) + + this.alerts + .open(success ? 'Copied to clipboard!' : 'Failed to copy to clipboard.') + .subscribe() + } +} diff --git a/frontend/projects/shared/src/services/error-toast.service.ts b/frontend/projects/shared/src/services/error-toast.service.ts deleted file mode 100644 index 2ac1314f7..000000000 --- a/frontend/projects/shared/src/services/error-toast.service.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { Injectable } from '@angular/core' -import { IonicSafeString, ToastController } from '@ionic/angular' -import { HttpError } from '../classes/http-error' - -@Injectable({ - providedIn: 'root', -}) -export class ErrorToastService { - private toast?: HTMLIonToastElement - - constructor(private readonly toastCtrl: ToastController) {} - - async present(e: HttpError | string, link?: string): Promise { - console.error(e) - - if (this.toast) return - - this.toast = await this.toastCtrl.create({ - header: 'Error', - message: getErrorMessage(e, link), - duration: 0, - position: 'top', - cssClass: 'error-toast', - buttons: [ - { - side: 'end', - icon: 'close', - handler: () => { - this.dismiss() - }, - }, - ], - }) - await this.toast.present() - } - - async dismiss(): Promise { - if (this.toast) { - await this.toast.dismiss() - this.toast = undefined - } - } -} - -export function getErrorMessage( - e: HttpError | string, - link?: string, -): string | IonicSafeString { - let message = '' - - if (typeof e === 'string') { - message = e - } else if (e.code === 0) { - message = - 'Request Error. Your browser blocked the request. This is usually caused by a corrupt browser cache or an overly aggressive ad blocker. Please clear your browser cache and/or adjust your ad blocker and try again' - } else if (!e.message) { - message = 'Unknown Error' - link = 'https://docs.start9.com/latest/support/faq' - } else { - message = e.message - } - - if (link) { - return new IonicSafeString( - `${message}

Get Help`, - ) - } - - return message -} diff --git a/frontend/projects/shared/src/services/error.service.ts b/frontend/projects/shared/src/services/error.service.ts index bb0221ce2..45891e0f4 100644 --- a/frontend/projects/shared/src/services/error.service.ts +++ b/frontend/projects/shared/src/services/error.service.ts @@ -22,7 +22,7 @@ export class ErrorService extends ErrorHandler { } } -function getErrorMessage(e: HttpError | string, link?: string): string { +export function getErrorMessage(e: HttpError | string, link?: string): string { let message = '' if (typeof e === 'string') { diff --git a/frontend/projects/shared/src/services/setup.service.ts b/frontend/projects/shared/src/services/setup.service.ts index f05007869..118cf35fe 100644 --- a/frontend/projects/shared/src/services/setup.service.ts +++ b/frontend/projects/shared/src/services/setup.service.ts @@ -1,4 +1,4 @@ -import { inject, StaticClassProvider, Type } from '@angular/core' +import { inject, StaticClassProvider } from '@angular/core' import { catchError, EMPTY, @@ -12,8 +12,8 @@ import { takeWhile, } from 'rxjs' import { SetupStatus } from '../types/api' -import { ErrorToastService } from './error-toast.service' import { Constructor } from '../types/constructor' +import { ErrorService } from './error.service' export function provideSetupService( api: Constructor[0]>, @@ -26,12 +26,12 @@ export function provideSetupService( } export class SetupService extends Observable { - private readonly errorToastService = inject(ErrorToastService) + private readonly errorService = inject(ErrorService) private readonly progress$ = interval(500).pipe( exhaustMap(() => from(this.api.getSetupStatus()).pipe( catchError(e => { - this.errorToastService.present(e) + this.errorService.handleError(e) return EMPTY }), diff --git a/frontend/projects/shared/src/types/workspace-config.ts b/frontend/projects/shared/src/types/workspace-config.ts index 997ded733..2da7d3f8d 100644 --- a/frontend/projects/shared/src/types/workspace-config.ts +++ b/frontend/projects/shared/src/types/workspace-config.ts @@ -4,19 +4,21 @@ export type WorkspaceConfig = { gitHash: string useMocks: boolean enableWidgets: boolean - // each key corresponds to a project and values adjust settings for that project, eg: ui, install-wizard, setup-wizard, diagnostic-ui + // each key corresponds to a project and values adjust settings for that project, eg: ui, install-wizard, setup-wizard ui: { api: { url: string version: string } - marketplace: { - start9: 'https://registry.start9.com/' - community: 'https://community-registry.start9.com/' - } + marketplace: MarketplaceConfig mocks: { maskAs: 'tor' | 'lan' skipStartupAlerts: boolean } } } + +export interface MarketplaceConfig { + start9: 'https://registry.start9.com/' + community: 'https://community-registry.start9.com/' +} diff --git a/frontend/projects/shared/styles/shared.scss b/frontend/projects/shared/styles/shared.scss index 725d7d985..4bcedd0f4 100644 --- a/frontend/projects/shared/styles/shared.scss +++ b/frontend/projects/shared/styles/shared.scss @@ -160,3 +160,10 @@ a { color: aqua; text-decoration: none; } + +.modal-buttons { + display: flex; + justify-content: flex-end; + gap: 16px; + margin-top: 24px; +} diff --git a/frontend/projects/shared/styles/taiga.scss b/frontend/projects/shared/styles/taiga.scss new file mode 100644 index 000000000..8bd35d622 --- /dev/null +++ b/frontend/projects/shared/styles/taiga.scss @@ -0,0 +1,49 @@ +@import '@taiga-ui/core/styles/taiga-ui-local'; + +/* stylelint-disable order/order */ +[tuiWrapper][data-appearance='secondary-warning'] { + background: var(--tui-warning-bg); + color: var(--tui-warning-fill); + + &[data-mode='onDark'] { + background: var(--tui-warning-bg-night); + color: var(--tui-warning-fill-night); + + @include wrapper-hover { + background: var(--tui-warning-bg-night-hover); + } + + @include wrapper-active { + background: var(--tui-warning-bg-night-hover); + } + } + + @include wrapper-hover { + background: var(--tui-warning-bg-hover); + } + + @include wrapper-active { + background: var(--tui-warning-bg-hover); + } +} + +tui-dialog { + transform: translate3d(0, 0, 0); +} + +tui-opt-group[data-label^='⚠️']:before { + color: var(--tui-warning-fill); +} + +tui-hint[data-appearance='onDark'] { + background: white !important; + color: #222 !important; +} + +[tuiLink] { + color: var(--tui-link) !important; + + &:hover { + color: var(--tui-link-hover) !important; + } +} diff --git a/frontend/projects/ui/src/app/app.component.html b/frontend/projects/ui/src/app/app.component.html index 29c7e11a3..dcc65b8c6 100644 --- a/frontend/projects/ui/src/app/app.component.html +++ b/frontend/projects/ui/src/app/app.component.html @@ -7,7 +7,7 @@
@@ -84,3 +84,7 @@ + + + + diff --git a/frontend/projects/ui/src/app/app.component.ts b/frontend/projects/ui/src/app/app.component.ts index af049e130..68e77ffee 100644 --- a/frontend/projects/ui/src/app/app.component.ts +++ b/frontend/projects/ui/src/app/app.component.ts @@ -1,5 +1,6 @@ import { Component, inject, OnDestroy } from '@angular/core' -import { merge } from 'rxjs' +import { Router } from '@angular/router' +import { combineLatest, map, merge } from 'rxjs' import { AuthService } from './services/auth.service' import { SplitPaneTracker } from './services/split-pane.service' import { PatchDataService } from './services/patch-data.service' @@ -15,6 +16,10 @@ import { THEME } from '@start9labs/shared' import { PatchDB } from 'patch-db-client' import { DataModel } from './services/patch-db/data-model' +function hasNavigation(url: string): boolean { + return !url.startsWith('/loading') && !url.startsWith('/diagnostic') +} + @Component({ selector: 'app-root', templateUrl: 'app.component.html', @@ -25,8 +30,13 @@ export class AppComponent implements OnDestroy { readonly sidebarOpen$ = this.splitPane.sidebarOpen$ readonly widgetDrawer$ = this.clientStorageService.widgetDrawer$ readonly theme$ = inject(THEME) + readonly navigation$ = combineLatest([ + this.authService.isVerified$, + this.router.events.pipe(map(() => hasNavigation(this.router.url))), + ]).pipe(map(([isVerified, hasNavigation]) => isVerified && hasNavigation)) constructor( + private readonly router: Router, private readonly titleService: Title, private readonly patchData: PatchDataService, private readonly patchMonitor: PatchMonitorService, diff --git a/frontend/projects/ui/src/app/app.module.ts b/frontend/projects/ui/src/app/app.module.ts index 215c97e83..c3f9c6fb1 100644 --- a/frontend/projects/ui/src/app/app.module.ts +++ b/frontend/projects/ui/src/app/app.module.ts @@ -16,6 +16,7 @@ import { ResponsiveColModule, SharedPipesModule, LightThemeModule, + LoadingModule, } from '@start9labs/shared' import { AppComponent } from './app.component' @@ -32,7 +33,6 @@ import { ConnectionBarComponentModule } from './app/connection-bar/connection-ba import { WidgetsPageModule } from 'src/app/apps/ui/pages/widgets/widgets.module' import { ServiceWorkerModule } from '@angular/service-worker' import { environment } from '../environments/environment' -import { LoadingModule } from './common/loading/loading.module' @NgModule({ declarations: [AppComponent], diff --git a/frontend/projects/ui/src/app/app/menu/menu.component.html b/frontend/projects/ui/src/app/app/menu/menu.component.html index bd9a2ce2c..a6305090d 100644 --- a/frontend/projects/ui/src/app/app/menu/menu.component.html +++ b/frontend/projects/ui/src/app/app/menu/menu.component.html @@ -54,9 +54,9 @@ Play Snek diff --git a/frontend/projects/ui/src/app/app/preloader/preloader.component.html b/frontend/projects/ui/src/app/app/preloader/preloader.component.html index f20fe4a96..5c1c4f388 100644 --- a/frontend/projects/ui/src/app/app/preloader/preloader.component.html +++ b/frontend/projects/ui/src/app/app/preloader/preloader.component.html @@ -63,7 +63,6 @@ - @@ -83,5 +82,4 @@

a

a

a

-

a

diff --git a/frontend/projects/ui/src/app/app/preloader/preloader.component.ts b/frontend/projects/ui/src/app/app/preloader/preloader.component.ts index df0ae29b7..bb08430b1 100644 --- a/frontend/projects/ui/src/app/app/preloader/preloader.component.ts +++ b/frontend/projects/ui/src/app/app/preloader/preloader.component.ts @@ -82,9 +82,11 @@ const ICONS = [ 'settings-outline', 'shield-checkmark-outline', 'stop-outline', + 'stopwatch-outline', 'storefront-outline', 'swap-vertical', 'terminal-outline', + 'trail-sign-outline', 'trash', 'trash-outline', 'warning-outline', diff --git a/frontend/projects/ui/src/app/app/snek/snake.page.html b/frontend/projects/ui/src/app/app/snek/snake.page.html index 9e037ce8d..1fc48c47c 100644 --- a/frontend/projects/ui/src/app/app/snek/snake.page.html +++ b/frontend/projects/ui/src/app/app/snek/snake.page.html @@ -1,28 +1,8 @@ - - - Play Snek! - Score: {{ score }} - - - - -
- -
-
- - - - High Score: {{ highScore }} - - - Save and Quit - - - - +
+ +
+
+ Score: {{ score }} + High Score: {{ highScore }} + +
diff --git a/frontend/projects/ui/src/app/app/snek/snake.page.scss b/frontend/projects/ui/src/app/app/snek/snake.page.scss index c07d3a2b7..50605f1dc 100644 --- a/frontend/projects/ui/src/app/app/snek/snake.page.scss +++ b/frontend/projects/ui/src/app/app/snek/snake.page.scss @@ -1,6 +1,14 @@ .canvas-center { + min-height: 50vh; padding-top: 20px; display: flex; align-items: center; justify-content: center; -} \ No newline at end of file +} + +.footer { + display: flex; + align-items: center; + justify-content: space-between; + padding-top: 32px; +} diff --git a/frontend/projects/ui/src/app/app/snek/snake.page.ts b/frontend/projects/ui/src/app/app/snek/snake.page.ts index 6c671201d..eeadb9df0 100644 --- a/frontend/projects/ui/src/app/app/snek/snake.page.ts +++ b/frontend/projects/ui/src/app/app/snek/snake.page.ts @@ -1,15 +1,22 @@ -import { Component, HostListener, Input } from '@angular/core' -import { ModalController } from '@ionic/angular' -import { pauseFor } from '../../../../../shared/src/public-api' +import { + AfterViewInit, + Component, + HostListener, + Inject, + OnDestroy, +} from '@angular/core' +import { pauseFor } from '@start9labs/shared' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' +import { TuiDialogContext } from '@taiga-ui/core' +import { DOCUMENT } from '@angular/common' @Component({ selector: 'snake', templateUrl: './snake.page.html', styleUrls: ['./snake.page.scss'], }) -export class SnakePage { - @Input() - highScore = 0 +export class SnakePage implements AfterViewInit, OnDestroy { + highScore = this.dialog.data.highScore score = 0 @@ -30,11 +37,16 @@ export class SnakePage { private bitcoin: { x: number; y: number } = { x: NaN, y: NaN } private moveQueue: String[] = [] + private destroyed = false - constructor(private readonly modalCtrl: ModalController) {} + constructor( + @Inject(DOCUMENT) private readonly document: Document, + @Inject(POLYMORPHEUS_CONTEXT) + private readonly dialog: TuiDialogContext, + ) {} - async dismiss() { - return this.modalCtrl.dismiss({ highScore: this.highScore }) + dismiss() { + this.dialog.completeWith(this.highScore) } @HostListener('document:keydown', ['$event']) @@ -57,7 +69,11 @@ export class SnakePage { this.init() } - ionViewDidEnter() { + ngOnDestroy() { + this.destroyed = true + } + + ngAfterViewInit() { this.init() this.image = new Image() @@ -68,10 +84,10 @@ export class SnakePage { } init() { - this.canvas = document.querySelector('canvas#game')! + this.canvas = this.document.querySelector('canvas#game')! this.canvas.style.border = '1px solid #e0e0e0' this.context = this.canvas.getContext('2d')! - const container = document.getElementsByClassName('canvas-center')[0] + const container = this.document.querySelector('.canvas-center')! this.grid = Math.min( Math.floor(container.clientWidth / this.width), Math.floor(container.clientHeight / this.height), @@ -139,13 +155,15 @@ export class SnakePage { // game loop async loop() { + if (this.destroyed) return + await pauseFor(this.speed) requestAnimationFrame(async () => await this.loop()) this.context.clearRect(0, 0, this.canvas.width, this.canvas.height) - // move snake by it's velocity + // move snake by its velocity this.snake.x += this.snake.dx this.snake.y += this.snake.dy diff --git a/frontend/projects/ui/src/app/app/snek/snek.directive.ts b/frontend/projects/ui/src/app/app/snek/snek.directive.ts index 5c8cc76b4..246db7c09 100644 --- a/frontend/projects/ui/src/app/app/snek/snek.directive.ts +++ b/frontend/projects/ui/src/app/app/snek/snek.directive.ts @@ -1,7 +1,9 @@ import { Directive, HostListener, Input } from '@angular/core' -import { LoadingController, ModalController } from '@ionic/angular' -import { ErrorToastService } from '@start9labs/shared' +import { ErrorService, LoadingService } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/embassy-api.service' +import { TuiDialogService } from '@taiga-ui/core' +import { filter } from 'rxjs' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' import { SnakePage } from './snake.page' @Directive({ @@ -9,45 +11,40 @@ import { SnakePage } from './snake.page' }) export class SnekDirective { @Input() - appSnekHighScore: number | null = null + appSnekHighScore = 0 constructor( - private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, - private readonly errToast: ErrorToastService, + private readonly dialogs: TuiDialogService, + private readonly loader: LoadingService, + private readonly errorService: ErrorService, private readonly embassyApi: ApiService, ) {} @HostListener('click') async onClick() { - const modal = await this.modalCtrl.create({ - component: SnakePage, - cssClass: 'snake-modal', - backdropDismiss: false, - componentProps: { highScore: this.appSnekHighScore || 0 }, - }) - - modal.onDidDismiss().then(async ({ data }) => { - if (data?.highScore <= (this.appSnekHighScore || 0)) return - - const loader = await this.loadingCtrl.create({ - message: 'Saving high score...', + this.dialogs + .open(new PolymorpheusComponent(SnakePage), { + label: 'Snake!', + closeable: false, + dismissible: false, + data: { + highScore: this.appSnekHighScore, + }, }) + .pipe(filter(score => score > this.appSnekHighScore)) + .subscribe(async score => { + const loader = this.loader.open('Saving high score...').subscribe() - await loader.present() - - try { - await this.embassyApi.setDbValue( - ['gaming', 'snake', 'high-score'], - data.highScore, - ) - } catch (e: any) { - this.errToast.present(e) - } finally { - this.loadingCtrl.dismiss() - } - }) - - modal.present() + try { + await this.embassyApi.setDbValue( + ['gaming', 'snake', 'high-score'], + score, + ) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + }) } } diff --git a/frontend/projects/ui/src/app/app/snek/snek.module.ts b/frontend/projects/ui/src/app/app/snek/snek.module.ts index 73f4d0e8f..8bb81a01e 100644 --- a/frontend/projects/ui/src/app/app/snek/snek.module.ts +++ b/frontend/projects/ui/src/app/app/snek/snek.module.ts @@ -4,9 +4,10 @@ import { IonicModule } from '@ionic/angular' import { SnekDirective } from './snek.directive' import { SnakePage } from './snake.page' +import { TuiButtonModule } from '@taiga-ui/core' @NgModule({ - imports: [CommonModule, IonicModule], + imports: [CommonModule, IonicModule, TuiButtonModule], declarations: [SnekDirective, SnakePage], exports: [SnekDirective, SnakePage], }) diff --git a/frontend/projects/ui/src/app/apps/diagnostic/diagnostic.module.ts b/frontend/projects/ui/src/app/apps/diagnostic/diagnostic.module.ts new file mode 100644 index 000000000..ddb8d4def --- /dev/null +++ b/frontend/projects/ui/src/app/apps/diagnostic/diagnostic.module.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core' +import { RouterModule, Routes } from '@angular/router' +import { WorkspaceConfig } from '@start9labs/shared' +import { DiagnosticService } from './services/diagnostic.service' +import { MockDiagnosticService } from './services/mock-diagnostic.service' +import { LiveDiagnosticService } from './services/live-diagnostic.service' + +const { useMocks } = require('../../../../../../config.json') as WorkspaceConfig + +const ROUTES: Routes = [ + { + path: '', + loadChildren: () => + import('./home/home.module').then(m => m.HomePageModule), + }, + { + path: 'logs', + loadChildren: () => + import('./logs/logs.module').then(m => m.LogsPageModule), + }, +] + +@NgModule({ + imports: [RouterModule.forChild(ROUTES)], + providers: [ + { + provide: DiagnosticService, + useClass: useMocks ? MockDiagnosticService : LiveDiagnosticService, + }, + ], +}) +export class DiagnosticModule {} diff --git a/frontend/projects/ui/src/app/apps/diagnostic/home/home.module.ts b/frontend/projects/ui/src/app/apps/diagnostic/home/home.module.ts new file mode 100644 index 000000000..62f6394e5 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/diagnostic/home/home.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { RouterModule, Routes } from '@angular/router' +import { TuiButtonModule } from '@taiga-ui/core' +import { HomePage } from './home.page' + +const ROUTES: Routes = [ + { + path: '', + component: HomePage, + }, +] + +@NgModule({ + imports: [CommonModule, TuiButtonModule, RouterModule.forChild(ROUTES)], + declarations: [HomePage], +}) +export class HomePageModule {} diff --git a/frontend/projects/ui/src/app/apps/diagnostic/home/home.page.html b/frontend/projects/ui/src/app/apps/diagnostic/home/home.page.html new file mode 100644 index 000000000..9accfe6ce --- /dev/null +++ b/frontend/projects/ui/src/app/apps/diagnostic/home/home.page.html @@ -0,0 +1,53 @@ + +

StartOS - Diagnostic Mode

+ + +

StartOS launch error:

+ +

{{ error.problem }}

+

{{ error.details }}

+
+ + View Logs + +

Possible solutions:

+

{{ error.solution }}

+ +
+ + + + + + + +
+
+
+ + +

Server is restarting

+

+ Wait for the server to restart, then refresh this page. +

+ +
diff --git a/frontend/projects/ui/src/app/apps/diagnostic/home/home.page.scss b/frontend/projects/ui/src/app/apps/diagnostic/home/home.page.scss new file mode 100644 index 000000000..15ec44f64 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/diagnostic/home/home.page.scss @@ -0,0 +1,35 @@ +:host { + display: block; + padding: 32px; + overflow: auto; +} + +.title { + text-align: center; + padding-bottom: 24px; + font-size: calc(2vw + 14px); +} + +.subtitle { + padding-top: 16px; + padding-bottom: 16px; + font-size: calc(1vw + 12px); + font-weight: bold; +} + +.code { + display: block; + color: var(--tui-success-fill); + background: rgb(69, 69, 69); + padding: 1px 16px; + margin-bottom: 32px; +} + +.warning { + color: var(--tui-warning-fill); +} + +.buttons { + display: flex; + gap: 16px; +} diff --git a/frontend/projects/diagnostic-ui/src/app/pages/home/home.page.ts b/frontend/projects/ui/src/app/apps/diagnostic/home/home.page.ts similarity index 59% rename from frontend/projects/diagnostic-ui/src/app/pages/home/home.page.ts rename to frontend/projects/ui/src/app/apps/diagnostic/home/home.page.ts index bbda6939f..8af9be855 100644 --- a/frontend/projects/diagnostic-ui/src/app/pages/home/home.page.ts +++ b/frontend/projects/ui/src/app/apps/diagnostic/home/home.page.ts @@ -1,6 +1,10 @@ -import { Component } from '@angular/core' -import { AlertController, LoadingController } from '@ionic/angular' -import { ApiService } from 'src/app/services/api/api.service' +import { Component, Inject } from '@angular/core' +import { WINDOW } from '@ng-web-apis/common' +import { LoadingService } from '@start9labs/shared' +import { TuiDialogService } from '@taiga-ui/core' +import { TUI_PROMPT } from '@taiga-ui/kit' +import { filter } from 'rxjs' +import { DiagnosticService } from '../services/diagnostic.service' @Component({ selector: 'app-home', @@ -8,19 +12,19 @@ import { ApiService } from 'src/app/services/api/api.service' styleUrls: ['home.page.scss'], }) export class HomePage { + restarted = false error?: { code: number problem: string solution: string details?: string } - solutions: string[] = [] - restarted = false constructor( - private readonly loadingCtrl: LoadingController, - private readonly api: ApiService, - private readonly alertCtrl: AlertController, + private readonly loader: LoadingService, + private readonly api: DiagnosticService, + private readonly dialogs: TuiDialogService, + @Inject(WINDOW) private readonly window: Window, ) {} async ngOnInit() { @@ -86,10 +90,7 @@ export class HomePage { } async restart(): Promise { - const loader = await this.loadingCtrl.create({ - cssClass: 'loader', - }) - await loader.present() + const loader = this.loader.open('').subscribe() try { await this.api.restart() @@ -97,15 +98,12 @@ export class HomePage { } catch (e) { console.error(e) } finally { - loader.dismiss() + loader.unsubscribe() } } async forgetDrive(): Promise { - const loader = await this.loadingCtrl.create({ - cssClass: 'loader', - }) - await loader.present() + const loader = this.loader.open('').subscribe() try { await this.api.forgetDrive() @@ -114,71 +112,60 @@ export class HomePage { } catch (e) { console.error(e) } finally { - loader.dismiss() + loader.unsubscribe() } } async presentAlertSystemRebuild() { - const alert = await this.alertCtrl.create({ - header: 'Warning', - message: - '

This action will tear down all service containers and rebuild them from scratch. No data will be deleted.

A system rebuild can be useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues.

It may take up to an hour to complete. During this time, you will lose all connectivity to your Start9 server.

', - buttons: [ - { - text: 'Cancel', - role: 'cancel', + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + no: 'Cancel', + yes: 'Rebuild', + content: + '

This action will tear down all service containers and rebuild them from scratch. No data will be deleted.

A system rebuild can be useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues.

It may take up to an hour to complete. During this time, you will lose all connectivity to your Start9 server.

', }, - { - text: 'Rebuild', - handler: () => { - try { - this.systemRebuild() - } catch (e) { - console.error(e) - } - }, - }, - ], - cssClass: 'alert-warning-message', - }) - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => { + try { + this.systemRebuild() + } catch (e) { + console.error(e) + } + }) } async presentAlertRepairDisk() { - const alert = await this.alertCtrl.create({ - header: 'Warning', - message: - '

This action should only be executed if directed by a Start9 support specialist.

If anything happens to the device during the reboot, such as losing power or unplugging the drive, the filesystem will be in an unrecoverable state. Please proceed with caution.

', - buttons: [ - { - text: 'Cancel', - role: 'cancel', + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + no: 'Cancel', + yes: 'Repair', + content: + '

This action should only be executed if directed by a Start9 support specialist.

If anything happens to the device during the reboot, such as losing power or unplugging the drive, the filesystem will be in an unrecoverable state. Please proceed with caution.

', }, - { - text: 'Repair', - handler: () => { - try { - this.repairDisk() - } catch (e) { - console.error(e) - } - }, - }, - ], - cssClass: 'alert-error-message', - }) - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => { + try { + this.repairDisk() + } catch (e) { + console.error(e) + } + }) } refreshPage(): void { - window.location.reload() + this.window.location.reload() } private async systemRebuild(): Promise { - const loader = await this.loadingCtrl.create({ - cssClass: 'loader', - }) - await loader.present() + const loader = this.loader.open('').subscribe() try { await this.api.systemRebuild() @@ -187,15 +174,12 @@ export class HomePage { } catch (e) { console.error(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async repairDisk(): Promise { - const loader = await this.loadingCtrl.create({ - cssClass: 'loader', - }) - await loader.present() + const loader = this.loader.open('').subscribe() try { await this.api.repairDisk() @@ -204,7 +188,7 @@ export class HomePage { } catch (e) { console.error(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.module.ts b/frontend/projects/ui/src/app/apps/diagnostic/logs/logs.module.ts similarity index 68% rename from frontend/projects/diagnostic-ui/src/app/pages/logs/logs.module.ts rename to frontend/projects/ui/src/app/apps/diagnostic/logs/logs.module.ts index da4d046b4..7cb2cc2e1 100644 --- a/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.module.ts +++ b/frontend/projects/ui/src/app/apps/diagnostic/logs/logs.module.ts @@ -4,7 +4,7 @@ import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' import { LogsPage } from './logs.page' -const routes: Routes = [ +const ROUTES: Routes = [ { path: '', component: LogsPage, @@ -12,11 +12,7 @@ const routes: Routes = [ ] @NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - ], + imports: [CommonModule, IonicModule, RouterModule.forChild(ROUTES)], declarations: [LogsPage], }) -export class LogsPageModule { } +export class LogsPageModule {} diff --git a/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.html b/frontend/projects/ui/src/app/apps/diagnostic/logs/logs.page.html similarity index 100% rename from frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.html rename to frontend/projects/ui/src/app/apps/diagnostic/logs/logs.page.html diff --git a/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts b/frontend/projects/ui/src/app/apps/diagnostic/logs/logs.page.ts similarity index 84% rename from frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts rename to frontend/projects/ui/src/app/apps/diagnostic/logs/logs.page.ts index 317cd1ea3..7aaf0f519 100644 --- a/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.ts +++ b/frontend/projects/ui/src/app/apps/diagnostic/logs/logs.page.ts @@ -1,17 +1,16 @@ import { Component, ViewChild } from '@angular/core' import { IonContent } from '@ionic/angular' -import { ApiService } from 'src/app/services/api/api.service' -import { ErrorToastService, toLocalIsoString } from '@start9labs/shared' +import { ErrorService, toLocalIsoString } from '@start9labs/shared' +import { DiagnosticService } from '../services/diagnostic.service' -var Convert = require('ansi-to-html') -var convert = new Convert({ +const Convert = require('ansi-to-html') +const convert = new Convert({ bg: 'transparent', }) @Component({ selector: 'logs', templateUrl: './logs.page.html', - styleUrls: ['./logs.page.scss'], }) export class LogsPage { @ViewChild(IonContent) private content?: IonContent @@ -22,8 +21,8 @@ export class LogsPage { isOnBottom = true constructor( - private readonly api: ApiService, - private readonly errToast: ErrorToastService, + private readonly api: DiagnosticService, + private readonly errorService: ErrorService, ) {} async ngOnInit() { @@ -89,7 +88,7 @@ export class LogsPage { this.needInfinite = false } } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } } } diff --git a/frontend/projects/diagnostic-ui/src/app/services/api/api.service.ts b/frontend/projects/ui/src/app/apps/diagnostic/services/diagnostic.service.ts similarity index 90% rename from frontend/projects/diagnostic-ui/src/app/services/api/api.service.ts rename to frontend/projects/ui/src/app/apps/diagnostic/services/diagnostic.service.ts index 562d486c3..e8bd20a28 100644 --- a/frontend/projects/diagnostic-ui/src/app/services/api/api.service.ts +++ b/frontend/projects/ui/src/app/apps/diagnostic/services/diagnostic.service.ts @@ -1,6 +1,6 @@ import { LogsRes, ServerLogsReq } from '@start9labs/shared' -export abstract class ApiService { +export abstract class DiagnosticService { abstract getError(): Promise abstract restart(): Promise abstract forgetDrive(): Promise diff --git a/frontend/projects/diagnostic-ui/src/app/services/api/live-api.service.ts b/frontend/projects/ui/src/app/apps/diagnostic/services/live-diagnostic.service.ts similarity index 91% rename from frontend/projects/diagnostic-ui/src/app/services/api/live-api.service.ts rename to frontend/projects/ui/src/app/apps/diagnostic/services/live-diagnostic.service.ts index bbde6e5ba..dc4d3e9c4 100644 --- a/frontend/projects/diagnostic-ui/src/app/services/api/live-api.service.ts +++ b/frontend/projects/ui/src/app/apps/diagnostic/services/live-diagnostic.service.ts @@ -5,11 +5,11 @@ import { RpcError, RPCOptions, } from '@start9labs/shared' -import { ApiService, GetErrorRes } from './api.service' import { LogsRes, ServerLogsReq } from '@start9labs/shared' +import { DiagnosticService, GetErrorRes } from './diagnostic.service' @Injectable() -export class LiveApiService implements ApiService { +export class LiveDiagnosticService implements DiagnosticService { constructor(private readonly http: HttpService) {} async getError(): Promise { diff --git a/frontend/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts b/frontend/projects/ui/src/app/apps/diagnostic/services/mock-diagnostic.service.ts similarity index 91% rename from frontend/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts rename to frontend/projects/ui/src/app/apps/diagnostic/services/mock-diagnostic.service.ts index 5d8c13a4f..4a16f3e58 100644 --- a/frontend/projects/diagnostic-ui/src/app/services/api/mock-api.service.ts +++ b/frontend/projects/ui/src/app/apps/diagnostic/services/mock-diagnostic.service.ts @@ -1,10 +1,10 @@ import { Injectable } from '@angular/core' import { pauseFor } from '@start9labs/shared' -import { ApiService, GetErrorRes } from './api.service' import { LogsRes, ServerLogsReq, Log } from '@start9labs/shared' +import { DiagnosticService, GetErrorRes } from './diagnostic.service' @Injectable() -export class MockApiService implements ApiService { +export class MockDiagnosticService implements DiagnosticService { async getError(): Promise { await pauseFor(1000) return { diff --git a/frontend/projects/ui/src/app/apps/loading/loading.module.ts b/frontend/projects/ui/src/app/apps/loading/loading.module.ts index 9c7ae1bc9..3de110846 100644 --- a/frontend/projects/ui/src/app/apps/loading/loading.module.ts +++ b/frontend/projects/ui/src/app/apps/loading/loading.module.ts @@ -1,6 +1,6 @@ import { NgModule } from '@angular/core' import { RouterModule, Routes } from '@angular/router' -import { LoadingModule } from '@start9labs/shared' +import { InitializingModule } from '@start9labs/shared' import { LoadingPage } from './loading.page' const routes: Routes = [ @@ -11,7 +11,7 @@ const routes: Routes = [ ] @NgModule({ - imports: [LoadingModule, RouterModule.forChild(routes)], + imports: [InitializingModule, RouterModule.forChild(routes)], declarations: [LoadingPage], }) export class LoadingPageModule {} diff --git a/frontend/projects/ui/src/app/apps/loading/loading.page.html b/frontend/projects/ui/src/app/apps/loading/loading.page.html index 5b9740f3d..c4ac56866 100644 --- a/frontend/projects/ui/src/app/apps/loading/loading.page.html +++ b/frontend/projects/ui/src/app/apps/loading/loading.page.html @@ -1,4 +1,4 @@ - +> diff --git a/frontend/projects/ui/src/app/apps/login/login.page.ts b/frontend/projects/ui/src/app/apps/login/login.page.ts index c86f6057e..9e0b2f42b 100644 --- a/frontend/projects/ui/src/app/apps/login/login.page.ts +++ b/frontend/projects/ui/src/app/apps/login/login.page.ts @@ -1,26 +1,32 @@ -import { Component } from '@angular/core' -import { LoadingController, getPlatforms } from '@ionic/angular' +import { Component, Inject } from '@angular/core' +import { getPlatforms } from '@ionic/angular' import { ApiService } from 'src/app/services/api/embassy-api.service' import { AuthService } from 'src/app/services/auth.service' import { Router } from '@angular/router' import { ConfigService } from 'src/app/services/config.service' +import { LoadingService } from '@start9labs/shared' +import { TuiDestroyService } from '@taiga-ui/cdk' +import { takeUntil } from 'rxjs' +import { DOCUMENT } from '@angular/common' @Component({ selector: 'login', templateUrl: './login.page.html', styleUrls: ['./login.page.scss'], + providers: [TuiDestroyService], }) export class LoginPage { password = '' unmasked = false error = '' - loader?: HTMLIonLoadingElement secure = this.config.isSecure() constructor( + @Inject(DOCUMENT) private readonly document: Document, + private readonly destroy$: TuiDestroyService, private readonly router: Router, private readonly authService: AuthService, - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly api: ApiService, private readonly config: ConfigService, ) {} @@ -35,10 +41,6 @@ export class LoginPage { } } - ngOnDestroy() { - this.loader?.dismiss() - } - toggleMask() { this.unmasked = !this.unmasked } @@ -46,13 +48,13 @@ export class LoginPage { async submit() { this.error = '' - this.loader = await this.loadingCtrl.create({ - message: 'Logging in...', - }) - await this.loader.present() + const loader = this.loader + .open('Logging in...') + .pipe(takeUntil(this.destroy$)) + .subscribe() try { - document.cookie = '' + this.document.cookie = '' if (this.password.length > 64) { this.error = 'Password must be less than 65 characters' return @@ -71,7 +73,7 @@ export class LoginPage { // code 7 is for incorrect password this.error = e.code === 7 ? 'Invalid Password' : e.message } finally { - this.loader.dismiss() + loader.unsubscribe() } } } diff --git a/frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.component.html b/frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.component.html new file mode 100644 index 000000000..e0dac9074 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.component.html @@ -0,0 +1,32 @@ + + + Completed: {{ timestamp | date : 'medium' }} + + + +

System data

+

+ {{ system.result }} +

+
+ +
+ + +

{{ pkg.key }}

+

+ + {{ pkg.value.error ? 'Failed: ' + pkg.value.error : 'Succeeded' }} + +

+
+ +
+
diff --git a/frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.page.ts b/frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.component.ts similarity index 55% rename from frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.page.ts rename to frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.component.ts index 7434f5152..bbf0ceff4 100644 --- a/frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.component.ts @@ -1,24 +1,26 @@ -import { Component, Input } from '@angular/core' -import { ModalController } from '@ionic/angular' +import { Component, Inject } from '@angular/core' import { BackupReport } from 'src/app/services/api/api.types' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' +import { TuiDialogContext } from '@taiga-ui/core' @Component({ selector: 'backup-report', - templateUrl: './backup-report.page.html', + templateUrl: './backup-report.component.html', }) -export class BackupReportPage { - @Input() report!: BackupReport - @Input() timestamp!: string - - system!: { +export class BackupReportComponent { + readonly system: { result: string icon: 'remove' | 'remove-circle-outline' | 'checkmark' color: 'dark' | 'danger' | 'success' } - constructor(private readonly modalCtrl: ModalController) {} - - ngOnInit() { + constructor( + @Inject(POLYMORPHEUS_CONTEXT) + private readonly context: TuiDialogContext< + void, + { report: BackupReport; timestamp: string } + >, + ) { if (!this.report.server.attempted) { this.system = { result: 'Not Attempted', @@ -40,7 +42,11 @@ export class BackupReportPage { } } - async dismiss() { - return this.modalCtrl.dismiss(true) + get report(): BackupReport { + return this.context.data.report + } + + get timestamp(): string { + return this.context.data.timestamp } } diff --git a/frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.module.ts b/frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.module.ts index f21ff0918..a41a63e53 100644 --- a/frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.module.ts @@ -1,11 +1,11 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' -import { BackupReportPage } from './backup-report.page' +import { BackupReportComponent } from './backup-report.component' @NgModule({ - declarations: [BackupReportPage], + declarations: [BackupReportComponent], imports: [CommonModule, IonicModule], - exports: [BackupReportPage], + exports: [BackupReportComponent], }) export class BackupReportPageModule {} diff --git a/frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.page.html b/frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.page.html deleted file mode 100644 index 4ecf064d9..000000000 --- a/frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.page.html +++ /dev/null @@ -1,44 +0,0 @@ - - - Backup Report - - - - - - - - - - - - Completed: {{ timestamp | date : 'medium' }} - - - -

System data

-

{{ system.result }}

-
- -
- - -

{{ pkg.key }}

-

- - {{ pkg.value.error ? 'Failed: ' + pkg.value.error : 'Succeeded' }} - -

-
- -
-
-
diff --git a/frontend/projects/ui/src/app/apps/ui/modals/form/form.module.ts b/frontend/projects/ui/src/app/apps/ui/modals/form/form.module.ts index 814655fa0..464f60770 100644 --- a/frontend/projects/ui/src/app/apps/ui/modals/form/form.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/modals/form/form.module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { ReactiveFormsModule } from '@angular/forms' +import { RouterModule } from '@angular/router' import { TuiValueChangesModule } from '@taiga-ui/cdk' import { TuiButtonModule, TuiModeModule } from '@taiga-ui/core' import { FormModule } from 'src/app/common/form/form.module' @@ -10,6 +11,7 @@ import { FormPage } from './form.page' imports: [ CommonModule, ReactiveFormsModule, + RouterModule, TuiValueChangesModule, TuiButtonModule, TuiModeModule, diff --git a/frontend/projects/ui/src/app/apps/ui/modals/form/form.page.html b/frontend/projects/ui/src/app/apps/ui/modals/form/form.page.html index 58854a691..3bd7567a9 100644 --- a/frontend/projects/ui/src/app/apps/ui/modals/form/form.page.html +++ b/frontend/projects/ui/src/app/apps/ui/modals/form/form.page.html @@ -7,14 +7,26 @@ diff --git a/frontend/projects/ui/src/app/apps/ui/modals/form/form.page.ts b/frontend/projects/ui/src/app/apps/ui/modals/form/form.page.ts index c36cf0dd0..f7fcb1def 100644 --- a/frontend/projects/ui/src/app/apps/ui/modals/form/form.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/modals/form/form.page.ts @@ -17,7 +17,8 @@ import { FormService } from 'src/app/services/form.service' export interface ActionButton { text: string - handler: (value: T) => Promise | void + handler?: (value: T) => Promise | void + link?: string } export interface FormContext { @@ -65,12 +66,12 @@ export class FormPage> implements OnInit { this.markAsDirty() } - async onClick(handler: ActionButton['handler']) { + async onClick(handler: Required>['handler']) { tuiMarkControlAsTouchedAndValidate(this.form) this.invalidService.scrollIntoView() if (this.form.valid && (await handler(this.form.value as T))) { - this.context?.$implicit.complete() + this.close() } } @@ -78,6 +79,10 @@ export class FormPage> implements OnInit { this.dialogFormService.markAsDirty() } + close() { + this.context?.$implicit.complete() + } + private process(patch: Operation[]) { patch.forEach(({ op, path }) => { const control = this.form.get(path.substring(1).split('/')) diff --git a/frontend/projects/ui/src/app/apps/ui/modals/generic-input/generic-input.component.html b/frontend/projects/ui/src/app/apps/ui/modals/generic-input/generic-input.component.html deleted file mode 100644 index 308afd7dc..000000000 --- a/frontend/projects/ui/src/app/apps/ui/modals/generic-input/generic-input.component.html +++ /dev/null @@ -1,67 +0,0 @@ - -
- - -

{{ options.title }}

-
-

{{ options.message }}

- -
-

- {{ options.warning }} -

-
-
-
- -
-
-

{{ options.label }}

- - - - - - - -

- {{ error }} -

-
- -
- Cancel - - {{ options.buttonText }} - -
-
-
-
diff --git a/frontend/projects/ui/src/app/apps/ui/modals/generic-input/generic-input.component.module.ts b/frontend/projects/ui/src/app/apps/ui/modals/generic-input/generic-input.component.module.ts deleted file mode 100644 index d2b1faab4..000000000 --- a/frontend/projects/ui/src/app/apps/ui/modals/generic-input/generic-input.component.module.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { GenericInputComponent } from './generic-input.component' -import { IonicModule } from '@ionic/angular' -import { RouterModule } from '@angular/router' -import { SharedPipesModule } from '@start9labs/shared' -import { FormsModule } from '@angular/forms' - -@NgModule({ - declarations: [GenericInputComponent], - imports: [ - CommonModule, - IonicModule, - FormsModule, - RouterModule.forChild([]), - SharedPipesModule, - ], - exports: [GenericInputComponent], -}) -export class GenericInputComponentModule {} diff --git a/frontend/projects/ui/src/app/apps/ui/modals/generic-input/generic-input.component.ts b/frontend/projects/ui/src/app/apps/ui/modals/generic-input/generic-input.component.ts deleted file mode 100644 index 2ebf80539..000000000 --- a/frontend/projects/ui/src/app/apps/ui/modals/generic-input/generic-input.component.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { Component, inject, Input, ViewChild } from '@angular/core' -import { ModalController, IonicSafeString, IonInput } from '@ionic/angular' -import { getErrorMessage, THEME } from '@start9labs/shared' -import { mask } from 'src/app/util/mask' - -@Component({ - selector: 'generic-input', - templateUrl: './generic-input.component.html', -}) -export class GenericInputComponent { - @ViewChild('mainInput') elem?: IonInput - - @Input() options!: GenericInputOptions - - value!: string - masked!: boolean - - maskedValue?: string - - error: string | IonicSafeString = '' - - readonly theme$ = inject(THEME) - - constructor(private readonly modalCtrl: ModalController) {} - - ngOnInit() { - const defaultOptions: Partial = { - buttonText: 'Submit', - required: true, - useMask: false, - initialValue: '', - } - this.options = { - ...defaultOptions, - ...this.options, - } - - this.masked = !!this.options.useMask - this.value = this.options.initialValue || '' - } - - ngAfterViewInit() { - setTimeout(() => this.elem?.setFocus(), 400) - } - - toggleMask() { - this.masked = !this.masked - } - - cancel() { - this.modalCtrl.dismiss() - } - - transformInput(newValue: string) { - let i = 0 - this.value = newValue - .split('') - .map(x => (x === '●' ? this.value[i++] : x)) - .join('') - this.maskedValue = mask(this.value) - } - - async submit() { - const value = this.value.trim() - - if (!value && this.options.required) return - - try { - const response = await this.options.submitFn(value) - this.modalCtrl.dismiss({ response, value }, 'success') - } catch (e: any) { - this.error = getErrorMessage(e) - } - } -} - -export interface GenericInputOptions { - // required - title: string - message: string - submitFn: (value: string) => Promise - // optional - label?: string - warning?: string - buttonText?: string - placeholder?: string - required?: boolean - useMask?: boolean - initialValue?: string | null -} diff --git a/frontend/projects/ui/src/app/apps/ui/modals/prompt/prompt.component.html b/frontend/projects/ui/src/app/apps/ui/modals/prompt/prompt.component.html new file mode 100644 index 000000000..788210973 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/modals/prompt/prompt.component.html @@ -0,0 +1,40 @@ +

{{ options.message }}

+

{{ options.warning }}

+
+ + {{ options.label }} + * + + +
+ + +
+
+ + + + diff --git a/frontend/projects/ui/src/app/apps/ui/modals/prompt/prompt.component.scss b/frontend/projects/ui/src/app/apps/ui/modals/prompt/prompt.component.scss new file mode 100644 index 000000000..d95d85925 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/modals/prompt/prompt.component.scss @@ -0,0 +1,13 @@ +.warning { + color: var(--tui-warning-fill); +} + +.button { + pointer-events: auto; + margin-left: 0.25rem; +} + +.masked { + font-family: text-security-disc; + -webkit-text-security: disc; +} diff --git a/frontend/projects/ui/src/app/apps/ui/modals/prompt/prompt.component.ts b/frontend/projects/ui/src/app/apps/ui/modals/prompt/prompt.component.ts new file mode 100644 index 000000000..9842afe02 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/modals/prompt/prompt.component.ts @@ -0,0 +1,49 @@ +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' +import { + POLYMORPHEUS_CONTEXT, + PolymorpheusComponent, +} from '@tinkoff/ng-polymorpheus' +import { TuiDialogContext } from '@taiga-ui/core' + +@Component({ + selector: 'prompt', + templateUrl: 'prompt.component.html', + styleUrls: ['prompt.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PromptComponent { + masked = this.options.useMask + value = this.options.initialValue || '' + + constructor( + @Inject(POLYMORPHEUS_CONTEXT) + private readonly context: TuiDialogContext, + ) {} + + get options(): PromptOptions { + return this.context.data + } + + cancel() { + this.context.$implicit.complete() + } + + submit(value: string) { + if (value || !this.options.required) { + this.context.$implicit.next(value) + } + } +} + +export const PROMPT = new PolymorpheusComponent(PromptComponent) + +export interface PromptOptions { + message: string + label?: string + warning?: string + buttonText?: string + placeholder?: string + required?: boolean + useMask?: boolean + initialValue?: string | null +} diff --git a/frontend/projects/ui/src/app/apps/ui/modals/prompt/prompt.module.ts b/frontend/projects/ui/src/app/apps/ui/modals/prompt/prompt.module.ts new file mode 100644 index 000000000..12cd96f6b --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/modals/prompt/prompt.module.ts @@ -0,0 +1,21 @@ +import { CommonModule } from '@angular/common' +import { NgModule } from '@angular/core' +import { FormsModule } from '@angular/forms' +import { TuiButtonModule, TuiTextfieldControllerModule } from '@taiga-ui/core' +import { TuiInputModule } from '@taiga-ui/kit' +import { TuiAutoFocusModule } from '@taiga-ui/cdk' +import { PromptComponent } from './prompt.component' + +@NgModule({ + imports: [ + CommonModule, + FormsModule, + TuiInputModule, + TuiButtonModule, + TuiTextfieldControllerModule, + TuiAutoFocusModule, + ], + declarations: [PromptComponent], + exports: [PromptComponent], +}) +export class PromptModule {} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/directives/backup-create.directive.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/directives/backup-create.directive.ts index 50ee7f8a8..91769e709 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/directives/backup-create.directive.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/directives/backup-create.directive.ts @@ -1,77 +1,60 @@ import { Directive, HostListener } from '@angular/core' -import { LoadingController, ModalController } from '@ionic/angular' +import { LoadingService } from '@start9labs/shared' +import { TuiDialogService } from '@taiga-ui/core' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' +import { BackupTarget } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' import { TargetSelectPage } from '../modals/target-select/target-select.page' -import { - CifsBackupTarget, - DiskBackupTarget, -} from 'src/app/services/api/api.types' import { BackupSelectPage } from '../modals/backup-select/backup-select.page' @Directive({ selector: '[backupCreate]', }) export class BackupCreateDirective { - serviceIds: string[] = [] - constructor( - private readonly loadingCtrl: LoadingController, - private readonly modalCtrl: ModalController, + private readonly loader: LoadingService, + private readonly dialogs: TuiDialogService, private readonly embassyApi: ApiService, ) {} - @HostListener('click') onClick() { + @HostListener('click') + onClick() { this.presentModalTarget() } - async presentModalTarget() { - const modal = await this.modalCtrl.create({ - presentingElement: await this.modalCtrl.getTop(), - component: TargetSelectPage, - componentProps: { type: 'create' }, - }) - - modal.onDidDismiss().then(res => { - if (res.data) { - this.presentModalSelect(res.data.id) - } - }) - - await modal.present() + presentModalTarget() { + this.dialogs + .open(new PolymorpheusComponent(TargetSelectPage), { + label: 'Select Backup Target', + data: { type: 'create' }, + }) + .subscribe(({ id }) => { + this.presentModalSelect(id) + }) } - private async presentModalSelect(targetId: string) { - const modal = await this.modalCtrl.create({ - presentingElement: await this.modalCtrl.getTop(), - component: BackupSelectPage, - componentProps: { - btnText: 'Create Backup', - }, - }) - - modal.onWillDismiss().then(res => { - if (res.data) { - this.createBackup(targetId, res.data) - } - }) - - await modal.present() + private presentModalSelect(targetId: string) { + this.dialogs + .open(new PolymorpheusComponent(BackupSelectPage), { + label: 'Select Services to Back Up', + data: { btnText: 'Create Backup' }, + }) + .subscribe(pkgIds => { + this.createBackup(targetId, pkgIds) + }) } private async createBackup( targetId: string, pkgIds: string[], ): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Beginning backup...', - }) - await loader.present() + const loader = this.loader.open('Beginning backup...').subscribe() await this.embassyApi .createBackup({ 'target-id': targetId, 'package-ids': pkgIds, }) - .finally(() => loader.dismiss()) + .finally(() => loader.unsubscribe()) } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/directives/backup-restore.directive.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/directives/backup-restore.directive.ts index 3b1710030..b762c6fe0 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/directives/backup-restore.directive.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/directives/backup-restore.directive.ts @@ -1,28 +1,42 @@ import { Directive, HostListener } from '@angular/core' -import { - LoadingController, - ModalController, - NavController, -} from '@ionic/angular' +import { NavController } from '@ionic/angular' +import { TuiDialogService } from '@taiga-ui/core' +import { ErrorService, LoadingService } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { - GenericInputComponent, - GenericInputOptions, -} from 'src/app/apps/ui/modals/generic-input/generic-input.component' import { BackupInfo, BackupTarget } from 'src/app/services/api/api.types' import * as argon2 from '@start9labs/argon2' import { TargetSelectPage } from '../modals/target-select/target-select.page' -import { RecoverSelectPage } from '../modals/recover-select/recover-select.page' +import { + RecoverData, + RecoverSelectPage, +} from '../modals/recover-select/recover-select.page' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' +import { + PROMPT, + PromptOptions, +} from 'src/app/apps/ui/modals/prompt/prompt.component' +import { + catchError, + EMPTY, + exhaustMap, + map, + Observable, + of, + switchMap, + take, + tap, +} from 'rxjs' @Directive({ selector: '[backupRestore]', }) export class BackupRestoreDirective { constructor( - private readonly modalCtrl: ModalController, + private readonly errorService: ErrorService, + private readonly dialogs: TuiDialogService, private readonly navCtrl: NavController, private readonly embassyApi: ApiService, - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, ) {} @HostListener('click') onClick() { @@ -30,92 +44,81 @@ export class BackupRestoreDirective { } async presentModalTarget() { - const modal = await this.modalCtrl.create({ - presentingElement: await this.modalCtrl.getTop(), - component: TargetSelectPage, - componentProps: { type: 'restore' }, - }) - - modal.onDidDismiss().then(res => { - if (res.data) { - this.presentModalPassword(res.data) - } - }) - - await modal.present() + this.dialogs + .open(new PolymorpheusComponent(TargetSelectPage), { + label: 'Select Backup Source', + data: { type: 'restore' }, + }) + .subscribe(data => { + this.presentModalPassword(data) + }) } - async presentModalPassword(target: BackupTarget): Promise { - const options: GenericInputOptions = { - title: 'Password Required', + presentModalPassword(target: BackupTarget) { + const data: PromptOptions = { message: 'Enter the master password that was used to encrypt this backup. On the next screen, you will select the individual services you want to restore.', label: 'Master Password', placeholder: 'Enter master password', useMask: true, - buttonText: 'Next', - submitFn: async (password: string) => { - const passwordHash = target['embassy-os']?.['password-hash'] || '' - argon2.verify(passwordHash, password) - return this.getBackupInfo(target.id, password) - }, } - const modal = await this.modalCtrl.create({ - componentProps: { options }, - cssClass: 'alertlike-modal', - presentingElement: await this.modalCtrl.getTop(), - component: GenericInputComponent, - }) + this.dialogs + .open(PROMPT, { + label: 'Password Required', + data, + }) + .pipe( + exhaustMap(password => + this.getRecoverData( + target.id, + password, + target['embassy-os']?.['password-hash'] || '', + ), + ), + take(1), + switchMap(data => this.presentModalSelect(data)), + ) + .subscribe(() => { + this.navCtrl.navigateRoot('/services') + }) + } - modal.onDidDismiss().then(res => { - if (res.data) { - const { value, response } = res.data - this.presentModalSelect(target.id, response, value) - } - }) + private getRecoverData( + targetId: string, + password: string, + hash: string, + ): Observable { + return of(password).pipe( + tap(() => argon2.verify(hash, password)), + switchMap(() => this.getBackupInfo(targetId, password)), + catchError(e => { + this.errorService.handleError(e) - await modal.present() + return EMPTY + }), + map(backupInfo => ({ targetId, password, backupInfo })), + ) } private async getBackupInfo( targetId: string, password: string, ): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Decrypting drive...', - }) - await loader.present() + const loader = this.loader.open('Decrypting drive...').subscribe() return this.embassyApi .getBackupInfo({ 'target-id': targetId, password, }) - .finally(() => loader.dismiss()) + .finally(() => loader.unsubscribe()) } - private async presentModalSelect( - targetId: string, - backupInfo: BackupInfo, - password: string, - ): Promise { - const modal = await this.modalCtrl.create({ - componentProps: { - targetId, - backupInfo, - password, - }, - presentingElement: await this.modalCtrl.getTop(), - component: RecoverSelectPage, + private presentModalSelect(data: RecoverData): Observable { + return this.dialogs.open(new PolymorpheusComponent(RecoverSelectPage), { + label: 'Select Services to Restore', + data, }) - - modal.onWillDismiss().then(res => { - if (res.role === 'success') { - this.navCtrl.navigateRoot('/services') - } - }) - - await modal.present() } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/backup-select/backup-select.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/backup-select/backup-select.module.ts index be840eff2..bcb9ed156 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/backup-select/backup-select.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/backup-select/backup-select.module.ts @@ -1,12 +1,19 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { BackupSelectPage } from './backup-select.page' import { FormsModule } from '@angular/forms' +import { TuiButtonModule, TuiGroupModule } from '@taiga-ui/core' +import { TuiCheckboxBlockModule } from '@taiga-ui/kit' +import { BackupSelectPage } from './backup-select.page' @NgModule({ declarations: [BackupSelectPage], - imports: [CommonModule, IonicModule, FormsModule], + imports: [ + CommonModule, + FormsModule, + TuiButtonModule, + TuiGroupModule, + TuiCheckboxBlockModule, + ], exports: [BackupSelectPage], }) export class BackupSelectPageModule {} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/backup-select/backup-select.page.html b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/backup-select/backup-select.page.html index 457152a45..f79a9d0f8 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/backup-select/backup-select.page.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/backup-select/backup-select.page.html @@ -1,57 +1,31 @@ - - - Select Services to Back Up - - - - - - - +
+ +
+ + {{ pkg.title }} +
+
+
- - - - - - - {{ selectAll ? 'Select All' : 'Deselect All' }} - - - - - - - - -

{{ pkg.title }}

-
- -
-
-
- -

No services installed!

-
-
+ +

No services installed!

+
- - - - - {{ btnText }} - - - - +
+ + +
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/backup-select/backup-select.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/backup-select/backup-select.page.scss index 854c0ba4e..89ba0a7aa 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/backup-select/backup-select.page.scss +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/backup-select/backup-select.page.scss @@ -1,5 +1,25 @@ .center { - display: flex; - align-items: center; - justify-content: center; -} \ No newline at end of file + display: flex; + align-items: center; + justify-content: center; +} + +.pkgs { + width: 100%; + margin-top: 24px; +} + +.label { + display: flex; + align-items: center; + gap: 16px; +} + +.icon { + width: 40px; + height: 40px; +} + +ion-item { + --background: transparent; +} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/backup-select/backup-select.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/backup-select/backup-select.page.ts index 6c1f84614..f21a5ca7f 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/backup-select/backup-select.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/backup-select/backup-select.page.ts @@ -1,8 +1,9 @@ -import { Component, Input } from '@angular/core' -import { ModalController } from '@ionic/angular' +import { Component, Inject, Input } from '@angular/core' import { PatchDB } from 'patch-db-client' import { firstValueFrom, map } from 'rxjs' import { DataModel, PackageState } from 'src/app/services/patch-db/data-model' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' +import { TuiDialogContext } from '@taiga-ui/core' @Component({ selector: 'backup-select', @@ -10,11 +11,9 @@ import { DataModel, PackageState } from 'src/app/services/patch-db/data-model' styleUrls: ['./backup-select.page.scss'], }) export class BackupSelectPage { - @Input() btnText!: string @Input() selectedIds: string[] = [] hasSelection = false - selectAll = false pkgs: { id: string title: string @@ -24,10 +23,15 @@ export class BackupSelectPage { }[] = [] constructor( - private readonly modalCtrl: ModalController, + @Inject(POLYMORPHEUS_CONTEXT) + private readonly context: TuiDialogContext, private readonly patch: PatchDB, ) {} + get btnText(): string { + return this.context.data.btnText + } + async ngOnInit() { this.pkgs = await firstValueFrom( this.patch.watch$('package-data').pipe( @@ -51,13 +55,8 @@ export class BackupSelectPage { ) } - dismiss() { - this.modalCtrl.dismiss() - } - - async done() { - const pkgIds = this.pkgs.filter(p => p.checked).map(p => p.id) - this.modalCtrl.dismiss(pkgIds) + done() { + this.context.completeWith(this.pkgs.filter(p => p.checked).map(p => p.id)) } handleChange() { @@ -65,7 +64,7 @@ export class BackupSelectPage { } toggleSelectAll() { - this.pkgs.forEach(pkg => (pkg.checked = this.selectAll)) - this.selectAll = !this.selectAll + this.pkgs.forEach(pkg => (pkg.checked = !this.hasSelection)) + this.hasSelection = !this.hasSelection } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/recover-select/recover-select.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/recover-select/recover-select.module.ts index 3cf866171..0f7e63288 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/recover-select/recover-select.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/recover-select/recover-select.module.ts @@ -1,13 +1,20 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' import { FormsModule } from '@angular/forms' +import { TuiButtonModule, TuiGroupModule } from '@taiga-ui/core' +import { TuiCheckboxBlockModule } from '@taiga-ui/kit' import { RecoverSelectPage } from './recover-select.page' import { ToOptionsPipe } from './to-options.pipe' @NgModule({ declarations: [RecoverSelectPage, ToOptionsPipe], - imports: [CommonModule, IonicModule, FormsModule], + imports: [ + CommonModule, + FormsModule, + TuiButtonModule, + TuiGroupModule, + TuiCheckboxBlockModule, + ], exports: [RecoverSelectPage], }) export class RecoverSelectPageModule {} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/recover-select/recover-select.page.html b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/recover-select/recover-select.page.html index 09a055650..8fd2e77ce 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/recover-select/recover-select.page.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/recover-select/recover-select.page.html @@ -1,61 +1,36 @@ - - - Select Services to Restore - - - - - - - +
+ +
+ {{ option.title }} +
Version {{ option.version }}
+
Backup made: {{ option.timestamp | date : 'medium' }}
+
+ Ready to restore +
+
+ Unavailable. {{ option.title }} is already installed. +
+
+ Unavailable. Backup was made on a newer version of StartOS. +
+
+
+
- - - - -

{{ option.title }}

-

Version {{ option.version }}

-

Backup made: {{ option.timestamp | date : 'medium' }}

-

- Ready to restore -

-

- - Unavailable. {{ option.title }} is already installed. - -

-

- - Unavailable. Backup was made on a newer version of StartOS. - -

-
- -
-
-
- - - - - - Restore Selected - - - - +
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/recover-select/recover-select.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/recover-select/recover-select.page.scss index e69de29bb..4897866d3 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/recover-select/recover-select.page.scss +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/recover-select/recover-select.page.scss @@ -0,0 +1,31 @@ +.items { + width: 100%; + margin: 12px 0 24px; +} + +.label { + padding: 8px 0; + font-size: 14px; +} + +.title { + font-size: 16px; + margin-bottom: 4px; + display: block; +} + +.success { + color: var(--tui-success-fill); +} + +.warning { + color: var(--tui-warning-fill); +} + +.danger { + color: var(--tui-error-fill); +} + +.button { + float: right; +} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/recover-select/recover-select.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/recover-select/recover-select.page.ts index 85989cc45..5052d9eb9 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/recover-select/recover-select.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/recover-select/recover-select.page.ts @@ -1,10 +1,7 @@ -import { Component, Input } from '@angular/core' -import { - LoadingController, - ModalController, - IonicSafeString, -} from '@ionic/angular' -import { getErrorMessage } from '@start9labs/shared' +import { Component, Inject } from '@angular/core' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' +import { TuiDialogContext } from '@taiga-ui/core' import { BackupInfo } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' import { PatchDB } from 'patch-db-client' @@ -12,31 +9,33 @@ import { AppRecoverOption } from './to-options.pipe' import { DataModel } from 'src/app/services/patch-db/data-model' import { take } from 'rxjs' +export interface RecoverData { + targetId: string + backupInfo: BackupInfo + password: string +} + @Component({ selector: 'recover-select', templateUrl: './recover-select.page.html', styleUrls: ['./recover-select.page.scss'], }) export class RecoverSelectPage { - @Input() targetId!: string - @Input() backupInfo!: BackupInfo - @Input() password!: string - @Input() oldPassword?: string - readonly packageData$ = this.patch.watch$('package-data').pipe(take(1)) hasSelection = false - error: string | IonicSafeString = '' constructor( - private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, + @Inject(POLYMORPHEUS_CONTEXT) + private readonly context: TuiDialogContext, + private readonly loader: LoadingService, + private readonly errorService: ErrorService, private readonly embassyApi: ApiService, private readonly patch: PatchDB, ) {} - dismiss() { - this.modalCtrl.dismiss() + get backupInfo(): BackupInfo { + return this.context.data.backupInfo } handleChange(options: AppRecoverOption[]) { @@ -45,22 +44,20 @@ export class RecoverSelectPage { async restore(options: AppRecoverOption[]): Promise { const ids = options.filter(({ checked }) => !!checked).map(({ id }) => id) - const loader = await this.loadingCtrl.create({ - message: 'Initializing...', - }) - await loader.present() + const loader = this.loader.open('Initializing...').subscribe() try { await this.embassyApi.restorePackages({ ids, - 'target-id': this.targetId, - password: this.password, + 'target-id': this.context.data.targetId, + password: this.context.data.password, }) - this.modalCtrl.dismiss(undefined, 'success') + + this.context.completeWith(undefined) } catch (e: any) { - this.error = getErrorMessage(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.module.ts index 7cfa69407..3b88319de 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.module.ts @@ -1,6 +1,7 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' +import { TuiButtonModule } from '@taiga-ui/core' import { TargetSelectPage, TargetStatusComponent } from './target-select.page' import { TargetPipesModule } from '../../pipes/target-pipes.module' import { TextSpinnerComponentModule } from '@start9labs/shared' @@ -12,6 +13,7 @@ import { TextSpinnerComponentModule } from '@start9labs/shared' IonicModule, TargetPipesModule, TextSpinnerComponentModule, + TuiButtonModule, ], exports: [TargetSelectPage], }) diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.page.html b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.page.html index ea2a34953..8aa08a50a 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.page.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.page.html @@ -1,55 +1,40 @@ - - - - Select Backup {{ type === 'create' ? 'Target' : 'Source' }} - - - - - - - - + + - - - - - - - - Saved Targets - - - - -

{{ displayInfo.name }}

- -

{{ displayInfo.description }}

-

{{ displayInfo.path }}

-
-
-
+ > + + + +

{{ displayInfo.name }}

+ +

{{ displayInfo.description }}

+

{{ displayInfo.path }}

+
+
+ -
-

No saved targets

- Go to Targets -
-
-
-
+
+

No saved targets

+ +
+ +
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.page.scss index e69de29bb..bfffad405 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.page.scss +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.page.scss @@ -0,0 +1,3 @@ +ion-item { + --background: transparent; +} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.page.ts index 176c40d4e..353fcdc14 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/modals/target-select/target-select.page.ts @@ -1,10 +1,17 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { ModalController, NavController } from '@ionic/angular' +import { + ChangeDetectionStrategy, + Component, + Inject, + Input, +} from '@angular/core' +import { NavController } from '@ionic/angular' import { BehaviorSubject } from 'rxjs' import { BackupTarget } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService } from '@start9labs/shared' +import { ErrorService } from '@start9labs/shared' import { BackupType } from '../../pages/backup-targets/backup-targets.page' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' +import { TuiDialogContext } from '@taiga-ui/core' @Component({ selector: 'target-select', @@ -13,36 +20,36 @@ import { BackupType } from '../../pages/backup-targets/backup-targets.page' changeDetection: ChangeDetectionStrategy.OnPush, }) export class TargetSelectPage { - @Input() type!: BackupType - @Input() isOneOff = true - targets: BackupTarget[] = [] loading$ = new BehaviorSubject(true) constructor( - private readonly modalCtrl: ModalController, + @Inject(POLYMORPHEUS_CONTEXT) + private readonly context: TuiDialogContext< + BackupTarget, + { type: BackupType } + >, private readonly navCtrl: NavController, private readonly api: ApiService, - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, ) {} + get type(): BackupType { + return this.context.data.type + } + async ngOnInit() { await this.getTargets() } - dismiss() { - this.modalCtrl.dismiss() - } - select(target: BackupTarget): void { - this.modalCtrl.dismiss(target) + this.context.completeWith(target) } goToTargets() { - this.modalCtrl - .dismiss() - .then(() => this.navCtrl.navigateForward(`/backups/targets`)) + this.context.$implicit.complete() + this.navCtrl.navigateForward(`/backups/targets`) } async refresh() { @@ -54,7 +61,7 @@ export class TargetSelectPage { try { this.targets = (await this.api.getBackupTargets({})).saved } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { this.loading$.next(false) } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-history/backup-history.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-history/backup-history.page.ts index 48f5dafdf..0cec0874d 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-history/backup-history.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-history/backup-history.page.ts @@ -1,11 +1,12 @@ import { Component } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { TuiDialogService } from '@taiga-ui/core' import { BackupReport, BackupRun } from 'src/app/services/api/api.types' -import { LoadingController, ModalController } from '@ionic/angular' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService } from '@start9labs/shared' import { BehaviorSubject } from 'rxjs' -import { BackupReportPage } from 'src/app/apps/ui/modals/backup-report/backup-report.page' +import { BackupReportComponent } from '../../../../modals/backup-report/backup-report.component' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' @Component({ selector: 'backup-history', @@ -18,9 +19,9 @@ export class BackupHistoryPage { loading$ = new BehaviorSubject(true) constructor( - private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, - private readonly errToast: ErrorToastService, + private readonly dialogs: TuiDialogService, + private readonly loader: LoadingService, + private readonly errorService: ErrorService, private readonly api: ApiService, ) {} @@ -28,7 +29,7 @@ export class BackupHistoryPage { try { this.runs = await this.api.getBackupRuns({}) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { this.loading$.next(false) } @@ -42,15 +43,16 @@ export class BackupHistoryPage { return Object.keys(this.selected).length } - async presentModalReport(run: BackupRun) { - const modal = await this.modalCtrl.create({ - component: BackupReportPage, - componentProps: { - report: run.report, - timestamp: run['completed-at'], - }, - }) - await modal.present() + presentModalReport(run: BackupRun) { + this.dialogs + .open(new PolymorpheusComponent(BackupReportComponent), { + label: 'Backup Report', + data: { + report: run.report, + timestamp: run['completed-at'], + }, + }) + .subscribe() } async toggleChecked(id: string) { @@ -71,20 +73,16 @@ export class BackupHistoryPage { async deleteSelected(): Promise { const ids = Object.keys(this.selected) - - const loader = await this.loadingCtrl.create({ - message: 'Deleting...', - }) - await loader.present() + const loader = this.loader.open('Deleting...').subscribe() try { await this.api.deleteBackupRuns({ ids }) this.selected = {} this.runs = this.runs.filter(r => !ids.includes(r.id)) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/backup-jobs.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/backup-jobs.module.ts index 3ef154196..b26d01ed4 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/backup-jobs.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/backup-jobs.module.ts @@ -1,13 +1,17 @@ -import { NgModule } from '@angular/core' -import { RouterModule, Routes } from '@angular/router' import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { BackupJobsPage } from './backup-jobs.page' -import { NewJobPage } from './new-job/new-job.page' -import { EditJobPage } from './edit-job/edit-job.page' -import { JobOptionsComponent } from './job-options/job-options.component' -import { ToHumanCronPipe } from './pipes' +import { NgModule } from '@angular/core' import { FormsModule } from '@angular/forms' +import { RouterModule, Routes } from '@angular/router' +import { IonicModule } from '@ionic/angular' +import { + TuiButtonModule, + TuiNotificationModule, + TuiWrapperModule, +} from '@taiga-ui/core' +import { TuiInputModule, TuiToggleModule } from '@taiga-ui/kit' +import { BackupJobsPage } from './backup-jobs.page' +import { EditJobComponent } from './edit-job/edit-job.component' +import { ToHumanCronPipe } from './pipes' import { TargetSelectPageModule } from '../../modals/target-select/target-select.module' import { TargetPipesModule } from '../../pipes/target-pipes.module' @@ -26,13 +30,12 @@ const routes: Routes = [ FormsModule, TargetSelectPageModule, TargetPipesModule, + TuiNotificationModule, + TuiButtonModule, + TuiInputModule, + TuiToggleModule, + TuiWrapperModule, ], - declarations: [ - BackupJobsPage, - ToHumanCronPipe, - NewJobPage, - EditJobPage, - JobOptionsComponent, - ], + declarations: [BackupJobsPage, ToHumanCronPipe, EditJobComponent], }) export class BackupJobsPageModule {} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/backup-jobs.page.html b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/backup-jobs.page.html index 516b0ad7d..871fc55eb 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/backup-jobs.page.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/backup-jobs.page.html @@ -8,20 +8,16 @@ - - - -

- Scheduling automatic backups is an excellent way to ensure your - Embassy data is safely backed up. Your Embassy will issue a - notification whenever one of your scheduled backups succeeds or fails. - - View instructions - -

-
-
+
+ + Scheduling automatic backups is an excellent way to ensure your Embassy + data is safely backed up. Your Embassy will issue a notification whenever + one of your scheduled backups succeeds or fails. + View instructions + +
+ Saved Jobs - New Job + Create New Job diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/backup-jobs.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/backup-jobs.page.ts index a84a3c1b7..2399239c0 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/backup-jobs.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/backup-jobs.page.ts @@ -1,15 +1,13 @@ import { Component } from '@angular/core' -import { - AlertController, - LoadingController, - ModalController, -} from '@ionic/angular' -import { BehaviorSubject } from 'rxjs' +import { TuiDialogService } from '@taiga-ui/core' +import { TUI_PROMPT } from '@taiga-ui/kit' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' +import { BehaviorSubject, filter } from 'rxjs' import { BackupJob } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService } from '@start9labs/shared' -import { EditJobPage } from './edit-job/edit-job.page' -import { NewJobPage } from './new-job/new-job.page' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { EditJobComponent } from './edit-job/edit-job.component' +import { BackupJobBuilder } from './edit-job/job-builder' @Component({ selector: 'backup-jobs', @@ -25,10 +23,9 @@ export class BackupJobsPage { loading$ = new BehaviorSubject(true) constructor( - private readonly modalCtrl: ModalController, - private readonly alertCtrl: AlertController, - private readonly loadingCtrl: LoadingController, - private readonly errToast: ErrorToastService, + private readonly dialogs: TuiDialogService, + private readonly loader: LoadingService, + private readonly errorService: ErrorService, private readonly api: ApiService, ) {} @@ -36,86 +33,64 @@ export class BackupJobsPage { try { this.jobs = await this.api.getBackupJobs({}) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { this.loading$.next(false) } } - async presentModalCreate() { - const modal = await this.modalCtrl.create({ - presentingElement: await this.modalCtrl.getTop(), - component: NewJobPage, - componentProps: { - count: this.jobs.length + 1, - }, - }) - - modal.onWillDismiss().then(res => { - if (res.data) { - this.jobs.push(res.data) - } - }) - - await modal.present() + presentModalCreate() { + this.dialogs + .open(new PolymorpheusComponent(EditJobComponent), { + label: 'Create New Job', + data: new BackupJobBuilder({ + name: `Backup Job ${this.jobs.length + 1}`, + }), + }) + .subscribe(job => this.jobs.push(job)) } - async presentModalUpdate(job: BackupJob) { - const modal = await this.modalCtrl.create({ - presentingElement: await this.modalCtrl.getTop(), - component: EditJobPage, - componentProps: { - existingJob: job, - }, - }) - - modal.onWillDismiss().then((res: { data?: BackupJob }) => { - if (res.data) { - const { name, target, cron } = res.data - job.name = name - job.target = target - job.cron = cron - job['package-ids'] = res.data['package-ids'] - } - }) - - await modal.present() + presentModalUpdate(data: BackupJob) { + this.dialogs + .open(new PolymorpheusComponent(EditJobComponent), { + label: 'Edit Job', + data: new BackupJobBuilder(data), + }) + .subscribe(job => { + data.name = job.name + data.target = job.target + data.cron = job.cron + data['package-ids'] = job['package-ids'] + }) } - async presentAlertDelete(id: string, index: number) { - const alert = await this.alertCtrl.create({ - header: 'Confirm', - message: 'Delete backup job? This action cannot be undone.', - buttons: [ - { - text: 'Cancel', - role: 'cancel', + presentAlertDelete(id: string, index: number) { + this.dialogs + .open(TUI_PROMPT, { + label: 'Confirm', + size: 's', + data: { + content: 'Delete backup job? This action cannot be undone.', + yes: 'Delete', + no: 'Cancel', }, - { - text: 'Delete', - handler: () => { - this.delete(id, index) - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => { + this.delete(id, index) + }) } private async delete(id: string, i: number): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Deleting...', - }) - await loader.present() + const loader = this.loader.open('Deleting...').subscribe() try { await this.api.removeBackupTarget({ id }) this.jobs.splice(i, 1) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.component.html b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.component.html new file mode 100644 index 000000000..1e599bfb3 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.component.html @@ -0,0 +1,47 @@ +
+ + Job Name + + + + + + + + + Schedule + + + +

+ {{ human.message }} +

+ +
+ Also Execute Now + +
+ +
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.component.scss b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.component.scss new file mode 100644 index 000000000..18b650d0c --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.component.scss @@ -0,0 +1,33 @@ +.button { + height: var(--tui-height-l); + display: flex; + align-items: center; + justify-content: space-between; + margin: 1rem 0; + padding: 0 1rem; + border-radius: var(--tui-radius-m); + font: var(--tui-font-text-l); + font-weight: bold; +} + +.value { + font: var(--tui-font-text-m); + color: var(--tui-positive); +} + +.toggle { + height: var(--tui-height-l); + display: flex; + align-items: center; + justify-content: space-between; + padding: 0 1rem; + box-shadow: inset 0 0 0 1px var(--tui-base-03); + font: var(--tui-font-text-l); + font-weight: bold; + border-radius: var(--tui-radius-m); +} + +.submit { + float: right; + margin-top: 1rem; +} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.component.ts new file mode 100644 index 000000000..1553dd541 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.component.ts @@ -0,0 +1,75 @@ +import { Component, Inject } from '@angular/core' +import { TuiDialogContext, TuiDialogService } from '@taiga-ui/core' +import { + POLYMORPHEUS_CONTEXT, + PolymorpheusComponent, +} from '@tinkoff/ng-polymorpheus' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { BackupJob, BackupTarget } from 'src/app/services/api/api.types' +import { TargetSelectPage } from '../../../modals/target-select/target-select.page' +import { BackupSelectPage } from '../../../modals/backup-select/backup-select.page' +import { BackupJobBuilder } from './job-builder' + +@Component({ + selector: 'edit-job', + templateUrl: './edit-job.component.html', + styleUrls: ['./edit-job.component.scss'], +}) +export class EditJobComponent { + constructor( + @Inject(POLYMORPHEUS_CONTEXT) + private readonly context: TuiDialogContext, + private readonly dialogs: TuiDialogService, + private readonly loader: LoadingService, + private readonly api: ApiService, + private readonly errorService: ErrorService, + ) {} + + get job() { + return this.context.data + } + + async save() { + const loader = this.loader.open('Saving Job').subscribe() + + try { + const { id } = this.job.job + let job: BackupJob + + if (id) { + job = await this.api.updateBackupJob(this.job.buildUpdate(id)) + } else { + job = await this.api.createBackupJob(this.job.buildCreate()) + } + + this.context.completeWith(job) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } + + presentModalTarget() { + this.dialogs + .open(new PolymorpheusComponent(TargetSelectPage), { + label: 'Select Backup Target', + data: { type: 'create' }, + }) + .subscribe(target => { + this.job.target = target + }) + } + + presentModalPackages() { + this.dialogs + .open(new PolymorpheusComponent(BackupSelectPage), { + label: 'Select Services to Back Up', + data: { btnText: 'Done' }, + }) + .subscribe(id => { + this.job['package-ids'] = id + }) + } +} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.page.html b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.page.html deleted file mode 100644 index f3cdbb119..000000000 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.page.html +++ /dev/null @@ -1,33 +0,0 @@ - - - Edit Job - - - - - - - - - - - - - - - - - - - Save - - - - diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.page.scss deleted file mode 100644 index 5255d7814..000000000 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.page.scss +++ /dev/null @@ -1,3 +0,0 @@ -h2 { - font-weight: bold; -} \ No newline at end of file diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.page.ts deleted file mode 100644 index f9a484412..000000000 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.page.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Component, Input } from '@angular/core' -import { BackupJob } from 'src/app/services/api/api.types' -import { LoadingController, ModalController } from '@ionic/angular' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService } from '@start9labs/shared' -import { BackupJobBuilder } from '../job-options/job-options.component' - -@Component({ - selector: 'edit-job', - templateUrl: './edit-job.page.html', - styleUrls: ['./edit-job.page.scss'], -}) -export class EditJobPage { - @Input() existingJob!: BackupJob - - job = {} as BackupJobBuilder - - saving = false - - constructor( - private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, - private readonly api: ApiService, - private readonly errToast: ErrorToastService, - ) {} - - ngOnInit() { - this.job = new BackupJobBuilder(this.existingJob) - } - - async dismiss() { - this.modalCtrl.dismiss() - } - - async save() { - this.saving = true - const loader = await this.loadingCtrl.create({ - message: 'Saving Job', - }) - await loader.present() - - try { - const job = await this.api.updateBackupJob( - this.job.buildUpdate(this.existingJob.id), - ) - this.modalCtrl.dismiss(job) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - this.saving = false - } - } -} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/job-builder.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/job-builder.ts new file mode 100644 index 000000000..b84e4d369 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/job-builder.ts @@ -0,0 +1,41 @@ +import { BackupJob, BackupTarget, RR } from 'src/app/services/api/api.types' + +export class BackupJobBuilder { + name: string + target: BackupTarget + cron: string + 'package-ids': string[] + now = false + + constructor(readonly job: Partial) { + const { name, target, cron } = job + this.name = name || '' + this.target = target || ({} as BackupTarget) + this.cron = cron || '0 2 * * *' + this['package-ids'] = job['package-ids'] || [] + } + + buildCreate(): RR.CreateBackupJobReq { + const { name, target, cron, now } = this + + return { + name, + 'target-id': target.id, + cron, + 'package-ids': this['package-ids'], + now, + } + } + + buildUpdate(id: string): RR.UpdateBackupJobReq { + const { name, target, cron } = this + + return { + id, + name, + 'target-id': target.id, + cron, + 'package-ids': this['package-ids'], + } + } +} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/job-options/job-options.component.html b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/job-options/job-options.component.html deleted file mode 100644 index b220b7ead..000000000 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/job-options/job-options.component.html +++ /dev/null @@ -1,34 +0,0 @@ -
-

Job Name

- - - -
- - - -

Target

-
- - {{ job.target.type || 'Select target' }} - -
- - - -

Packages

-
- - {{ job['package-ids'].length + ' selected' }} - -
- -
-

Schedule

- - - -

- {{ human.message }} -

-
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/job-options/job-options.component.scss b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/job-options/job-options.component.scss deleted file mode 100644 index dbb2f1b60..000000000 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/job-options/job-options.component.scss +++ /dev/null @@ -1,9 +0,0 @@ -h2 { - font-weight: bold; -} - -.input-label { - margin-bottom: 6px; - font-size: medium; - font-weight: bold; -} \ No newline at end of file diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/job-options/job-options.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/job-options/job-options.component.ts deleted file mode 100644 index fef1920fb..000000000 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/job-options/job-options.component.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Component, Input } from '@angular/core' -import { ModalController } from '@ionic/angular' -import { BackupJob, BackupTarget, RR } from 'src/app/services/api/api.types' -import { BackupSelectPage } from '../../../modals/backup-select/backup-select.page' -import { TargetSelectPage } from '../../../modals/target-select/target-select.page' - -@Component({ - selector: 'job-options', - templateUrl: './job-options.component.html', - styleUrls: ['./job-options.component.scss'], -}) -export class JobOptionsComponent { - @Input() job!: BackupJobBuilder - - constructor(private readonly modalCtrl: ModalController) {} - - async presentModalTarget() { - const modal = await this.modalCtrl.create({ - presentingElement: await this.modalCtrl.getTop(), - component: TargetSelectPage, - componentProps: { type: 'create' }, - }) - - modal.onWillDismiss().then(res => { - if (res.data) { - this.job.target = res.data - } - }) - - await modal.present() - } - - async presentModalPackages() { - const modal = await this.modalCtrl.create({ - presentingElement: await this.modalCtrl.getTop(), - component: BackupSelectPage, - componentProps: { - btnText: 'Done', - selectedIds: this.job['package-ids'], - }, - }) - - modal.onWillDismiss().then(res => { - if (res.data) { - this.job['package-ids'] = res.data - } - }) - - await modal.present() - } -} - -export class BackupJobBuilder { - name: string - target: BackupTarget - cron: string - 'package-ids': string[] - now = false - - constructor(readonly job: Partial) { - const { name, target, cron } = job - this.name = name || '' - this.target = target || ({} as BackupTarget) - this.cron = cron || '0 2 * * *' - this['package-ids'] = job['package-ids'] || [] - } - - buildCreate(): RR.CreateBackupJobReq { - const { name, target, cron, now } = this - - return { - name, - 'target-id': target.id, - cron, - 'package-ids': this['package-ids'], - now, - } - } - - buildUpdate(id: string): RR.UpdateBackupJobReq { - const { name, target, cron } = this - - return { - id, - name, - 'target-id': target.id, - cron, - 'package-ids': this['package-ids'], - } - } -} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/new-job/new-job.page.html b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/new-job/new-job.page.html deleted file mode 100644 index f740e44d8..000000000 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/new-job/new-job.page.html +++ /dev/null @@ -1,40 +0,0 @@ - - - Create New Job - - - - - - - - - - - - - - -

Also Execute Now

-
- -
-
-
- - - - - - Save Job - - - - diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/new-job/new-job.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/new-job/new-job.page.scss deleted file mode 100644 index 5255d7814..000000000 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/new-job/new-job.page.scss +++ /dev/null @@ -1,3 +0,0 @@ -h2 { - font-weight: bold; -} \ No newline at end of file diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/new-job/new-job.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/new-job/new-job.page.ts deleted file mode 100644 index e87a3af85..000000000 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/new-job/new-job.page.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { Component, Input } from '@angular/core' -import { LoadingController, ModalController } from '@ionic/angular' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService } from '@start9labs/shared' -import { BackupJobBuilder } from '../job-options/job-options.component' - -@Component({ - selector: 'new-job', - templateUrl: './new-job.page.html', - styleUrls: ['./new-job.page.scss'], -}) -export class NewJobPage { - @Input() count!: number - - readonly docsUrl = - 'https://docs.start9.com/latest/user-manual/backups/backup-jobs' - - job = {} as BackupJobBuilder - - saving = false - - constructor( - private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, - private readonly api: ApiService, - private readonly errToast: ErrorToastService, - ) {} - - ngOnInit() { - this.job = new BackupJobBuilder({ name: `Backup Job ${this.count}` }) - } - - async dismiss() { - this.modalCtrl.dismiss() - } - - async save() { - const loader = await this.loadingCtrl.create({ - message: 'Saving Job', - }) - await loader.present() - this.saving = true - - try { - const job = await this.api.createBackupJob(this.job.buildCreate()) - this.modalCtrl.dismiss(job) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - this.saving = false - } - } -} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/pipes.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/pipes.ts index dc7afc3fb..0e756aa9a 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/pipes.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/pipes.ts @@ -8,7 +8,7 @@ export class ToHumanCronPipe implements PipeTransform { transform(cron: string): { message: string; color: string } { const toReturn = { message: '', - color: 'success', + color: 'var(--tui-positive)', } try { @@ -26,7 +26,7 @@ export class ToHumanCronPipe implements PipeTransform { toReturn.message = human } catch (e) { toReturn.message = e as string - toReturn.color = 'danger' + toReturn.color = 'var(--tui-negative)' } return toReturn diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-targets/backup-targets.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-targets/backup-targets.module.ts index 9c3f65886..fb507215d 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-targets/backup-targets.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-targets/backup-targets.module.ts @@ -6,6 +6,7 @@ import { UnitConversionPipesModule } from '@start9labs/shared' import { SkeletonListComponentModule } from 'src/app/common/skeleton-list/skeleton-list.component.module' import { FormPageModule } from 'src/app/apps/ui/modals/form/form.module' import { BackupTargetsPage } from './backup-targets.page' +import { TuiNotificationModule } from '@taiga-ui/core' const routes: Routes = [ { @@ -23,6 +24,7 @@ const routes: Routes = [ UnitConversionPipesModule, FormPageModule, RouterModule.forChild(routes), + TuiNotificationModule, ], }) export class BackupTargetsPageModule {} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-targets/backup-targets.page.html b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-targets/backup-targets.page.html index c8819ed62..655bf28e6 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-targets/backup-targets.page.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-targets/backup-targets.page.html @@ -8,21 +8,17 @@ - - - -

- Backup targets are physical or virtual locations for storing encrypted - backups. They can be physical drives plugged into your server, shared - folders on your Local Area Network (LAN), or third party clouds such - as Dropbox or Google Drive. - - View instructions - -

-
-
+
+ + Backup targets are physical or virtual locations for storing encrypted + backups. They can be physical drives plugged into your server, shared + folders on your Local Area Network (LAN), or third party clouds such as + Dropbox or Google Drive. + View instructions + +
+ Unknown Physical Drives diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-targets/backup-targets.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-targets/backup-targets.page.ts index 7571b9974..fb5e193a3 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-targets/backup-targets.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-targets/backup-targets.page.ts @@ -16,7 +16,7 @@ import { import { BehaviorSubject, filter } from 'rxjs' import { TuiDialogService } from '@taiga-ui/core' import { TUI_PROMPT } from '@taiga-ui/kit' -import { ErrorService } from '@start9labs/shared' +import { ErrorService, LoadingService } from '@start9labs/shared' import { InputSpec, unionSelectKey, @@ -24,7 +24,6 @@ import { } from '@start9labs/start-sdk/lib/config/configTypes' import { FormDialogService } from 'src/app/services/form-dialog.service' import { FormPage } from 'src/app/apps/ui/modals/form/form.page' -import { LoadingService } from 'src/app/common/loading/loading.service' import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec' type BackupConfig = diff --git a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backups/backups.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backups/backups.module.ts index 5c56787df..c4c53f140 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backups/backups.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backups/backups.module.ts @@ -4,7 +4,6 @@ import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { BadgeMenuComponentModule } from 'src/app/common/badge-menu-button/badge-menu.component.module' import { InsecureWarningComponentModule } from 'src/app/common/insecure-warning/insecure-warning.module' -import { GenericInputComponentModule } from 'src/app/apps/ui/modals/generic-input/generic-input.component.module' import { BackupCreateDirective } from '../../directives/backup-create.directive' import { BackupRestoreDirective } from '../../directives/backup-restore.directive' import { @@ -15,6 +14,7 @@ import { BackupSelectPageModule } from '../../modals/backup-select/backup-select import { RecoverSelectPageModule } from '../../modals/recover-select/recover-select.module' import { TargetPipesModule } from '../../pipes/target-pipes.module' import { BackupsPage } from './backups.page' +import { PromptModule } from 'src/app/apps/ui/modals/prompt/prompt.module' const routes: Routes = [ { @@ -33,7 +33,7 @@ const routes: Routes = [ BadgeMenuComponentModule, InsecureWarningComponentModule, TargetPipesModule, - GenericInputComponentModule, + PromptModule, ], declarations: [ BackupsPage, diff --git a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-list.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-list.module.ts index 918fe65a5..7a7e19e81 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-list.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-list.module.ts @@ -14,9 +14,9 @@ import { ItemModule, SearchModule, SkeletonModule, + StoreIconComponentModule, } from '@start9labs/marketplace' import { BadgeMenuComponentModule } from 'src/app/common/badge-menu-button/badge-menu.component.module' -import { StoreIconComponentModule } from 'src/app/common/store-icon/store-icon.component.module' import { MarketplaceStatusModule } from '../marketplace-status/marketplace-status.module' import { MarketplaceListPage } from './marketplace-list.page' import { MarketplaceSettingsPageModule } from './marketplace-settings/marketplace-settings.module' diff --git a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-list.page.html b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-list.page.html index 734cb8910..235d1b368 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-list.page.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-list.page.html @@ -27,6 +27,7 @@ class="icon" size="80px" [url]="details.url" + [marketplace]="config.marketplace" >

{{ details.name }}

diff --git a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-list.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-list.page.ts index fe9281485..813e9109b 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-list.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-list.page.ts @@ -41,7 +41,7 @@ export class MarketplaceListPage { if (url === start9) { color = 'success' description = - 'Services from this registry are packaged and maintained by the Start9 team. If you experience an issue or have a question related to a service from this registry, one of our dedicated support staff will be happy to assist you.' + 'Services from this registry are packaged and maintained by the Start9 team. If you experience an issue or have questions related to a service from this registry, one of our dedicated support staff will be happy to assist you.' } else if (url === community) { color = 'tertiary' description = @@ -75,7 +75,7 @@ export class MarketplaceListPage { @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, private readonly dialogs: TuiDialogService, - private readonly config: ConfigService, + readonly config: ConfigService, private readonly route: ActivatedRoute, ) {} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-settings/marketplace-settings.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-settings/marketplace-settings.module.ts index 9eaebbd34..0304046c2 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-settings/marketplace-settings.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-settings/marketplace-settings.module.ts @@ -7,9 +7,9 @@ import { TuiHostedDropdownModule, TuiSvgModule, } from '@taiga-ui/core' -import { StoreIconComponentModule } from 'src/app/common/store-icon/store-icon.component.module' import { FormPageModule } from 'src/app/apps/ui/modals/form/form.module' import { MarketplaceSettingsPage } from './marketplace-settings.page' +import { StoreIconComponentModule } from '@start9labs/marketplace' @NgModule({ imports: [ diff --git a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-settings/marketplace-settings.page.html b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-settings/marketplace-settings.page.html index 8d0da271a..77dde15e7 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-settings/marketplace-settings.page.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-settings/marketplace-settings.page.html @@ -8,7 +8,10 @@ (click)="s.selected ? '' : connect(s.url)" > - +

{{ s.name }}

@@ -42,7 +45,11 @@ > - +

{{ a.name }}

diff --git a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-settings/marketplace-settings.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-settings/marketplace-settings.page.ts index 09d30780e..a5fee0e90 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-settings/marketplace-settings.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-list/marketplace-settings/marketplace-settings.page.ts @@ -1,5 +1,10 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' -import { ErrorService, sameUrl, toUrl } from '@start9labs/shared' +import { + ErrorService, + LoadingService, + sameUrl, + toUrl, +} from '@start9labs/shared' import { AbstractMarketplaceService } from '@start9labs/marketplace' import { ValueSpecObject } from '@start9labs/start-sdk/lib/config/configTypes' import { TuiDialogService } from '@taiga-ui/core' @@ -11,7 +16,7 @@ import { DataModel, UIStore } from 'src/app/services/patch-db/data-model' import { MarketplaceService } from 'src/app/services/marketplace.service' import { FormDialogService } from 'src/app/services/form-dialog.service' import { FormPage } from 'src/app/apps/ui/modals/form/form.page' -import { LoadingService } from 'src/app/common/loading/loading.service' +import { ConfigService } from 'src/app/services/config.service' @Component({ selector: 'marketplace-settings', @@ -47,6 +52,7 @@ export class MarketplaceSettingsPage { private readonly marketplaceService: MarketplaceService, private readonly patch: PatchDB, private readonly dialogs: TuiDialogService, + readonly config: ConfigService, ) {} async presentModalAdd() { diff --git a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-show/components/marketplace-show-controls/marketplace-show-controls.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-show/components/marketplace-show-controls/marketplace-show-controls.component.ts index 732427cf2..9dc921ea8 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-show/components/marketplace-show-controls/marketplace-show-controls.component.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/marketplace/marketplace-show/components/marketplace-show-controls/marketplace-show-controls.component.ts @@ -4,17 +4,19 @@ import { Inject, Input, } from '@angular/core' -import { AlertController, LoadingController } from '@ionic/angular' import { AbstractMarketplaceService, MarketplacePkg, } from '@start9labs/marketplace' import { Emver, - ErrorToastService, + ErrorService, isEmptyObject, + LoadingService, sameUrl, } from '@start9labs/shared' +import { TuiDialogService } from '@taiga-ui/core' +import { filter, firstValueFrom, of, Subscription, switchMap } from 'rxjs' import { DataModel, PackageDataEntry, @@ -27,7 +29,7 @@ import { ApiService } from 'src/app/services/api/embassy-api.service' import { Breakages } from 'src/app/services/api/api.types' import { PatchDB } from 'patch-db-client' import { getAllPackages } from 'src/app/util/get-package-data' -import { firstValueFrom } from 'rxjs' +import { TUI_PROMPT } from '@taiga-ui/kit' @Component({ selector: 'marketplace-show-controls', @@ -50,13 +52,13 @@ export class MarketplaceShowControlsComponent { readonly PackageState = PackageState constructor( - private readonly alertCtrl: AlertController, + private readonly dialogs: TuiDialogService, private readonly ClientStorageService: ClientStorageService, @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly emver: Emver, - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, private readonly embassyApi: ApiService, private readonly patch: PatchDB, ) {} @@ -112,39 +114,26 @@ export class MarketplaceShowControlsComponent { } return new Promise(async resolve => { - const alert = await this.alertCtrl.create({ - header: 'Warning', - message: `This service was originally ${ - originalName ? 'installed from ' + originalName : 'side loaded' - }, but you are currently connected to ${name}. To install from ${name} anyway, click "Continue".`, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - handler: () => { - resolve(false) - }, + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content: `This service was originally ${ + originalName ? 'installed from ' + originalName : 'side loaded' + }, but you are currently connected to ${name}. To install from ${name} anyway, click "Continue".`, + yes: 'Continue', + no: 'Cancel', }, - { - text: 'Continue', - handler: () => { - resolve(true) - }, - cssClass: 'enter-click', - }, - ], - cssClass: 'alert-warning-message', - }) - - await alert.present() + }) + .subscribe(response => resolve(response)) }) } private async dryInstall(url: string) { - const loader = await this.loadingCtrl.create({ - message: 'Checking dependent services...', - }) - await loader.present() + const loader = this.loader + .open('Checking dependent services...') + .subscribe() const { id, version } = this.pkg.manifest @@ -157,49 +146,47 @@ export class MarketplaceShowControlsComponent { if (isEmptyObject(breakages)) { this.install(url, loader) } else { - await loader.dismiss() + loader.unsubscribe() const proceed = await this.presentAlertBreakages(breakages) if (proceed) { this.install(url) } } } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } } - private async alertInstall(url: string) { - const installAlert = this.pkg.manifest.alerts.install - - if (!installAlert) return this.install(url) - - const alert = await this.alertCtrl.create({ - header: 'Alert', - message: installAlert, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Install', - handler: () => { - this.install(url) - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() + private alertInstall(url: string) { + of(this.pkg.manifest.alerts.install) + .pipe( + switchMap(content => + content + ? of(true) + : this.dialogs.open(TUI_PROMPT, { + label: 'Alert', + size: 's', + data: { + content, + yes: 'Install', + no: 'Cancel', + }, + }), + ), + filter(Boolean), + ) + .subscribe(() => this.install(url)) } - private async install(url: string, loader?: HTMLIonLoadingElement) { + private async install(url: string, loader?: Subscription) { const message = 'Beginning Install...' + if (loader) { - loader.message = message + loader.unsubscribe() + loader.closed = false + loader.add(this.loader.open(message).subscribe()) } else { - loader = await this.loadingCtrl.create({ message }) - await loader.present() + loader = this.loader.open(message).subscribe() } const { id, version } = this.pkg.manifest @@ -207,46 +194,34 @@ export class MarketplaceShowControlsComponent { try { await this.marketplaceService.installPackage(id, version, url) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async presentAlertBreakages(breakages: Breakages): Promise { - let message: string = + let content: string = 'As a result of this update, the following services will no longer work properly and may crash:
    ' const localPkgs = await getAllPackages(this.patch) const bullets = Object.keys(breakages).map(id => { const title = localPkgs[id].manifest.title return `
  • ${title}
  • ` }) - message = `${message}${bullets.join('')}
` + content = `${content}${bullets.join('')}` return new Promise(async resolve => { - const alert = await this.alertCtrl.create({ - header: 'Warning', - message, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - handler: () => { - resolve(false) - }, + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content, + yes: 'Continue', + no: 'Cancel', }, - { - text: 'Continue', - handler: () => { - resolve(true) - }, - cssClass: 'enter-click', - }, - ], - cssClass: 'alert-warning-message', - }) - - await alert.present() + }) + .subscribe(response => resolve(response)) }) } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/notifications/notifications.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/notifications/notifications.module.ts index dfbb6036d..2f50677a3 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/notifications/notifications.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/notifications/notifications.module.ts @@ -2,9 +2,10 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { RouterModule, Routes } from '@angular/router' +import { SharedPipesModule } from '@start9labs/shared' +import { TuiPromptModule } from '@taiga-ui/kit' import { NotificationsPage } from './notifications.page' import { BadgeMenuComponentModule } from 'src/app/common/badge-menu-button/badge-menu.component.module' -import { SharedPipesModule } from '@start9labs/shared' import { BackupReportPageModule } from '../../modals/backup-report/backup-report.module' const routes: Routes = [ @@ -22,6 +23,7 @@ const routes: Routes = [ BadgeMenuComponentModule, SharedPipesModule, BackupReportPageModule, + TuiPromptModule, ], declarations: [NotificationsPage], }) diff --git a/frontend/projects/ui/src/app/apps/ui/pages/notifications/notifications.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/notifications/notifications.page.ts index e47b95276..909ffcc75 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/notifications/notifications.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/notifications/notifications.page.ts @@ -1,21 +1,19 @@ import { Component } from '@angular/core' +import { ActivatedRoute } from '@angular/router' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { TuiDialogService } from '@taiga-ui/core' +import { TUI_PROMPT } from '@taiga-ui/kit' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' +import { PatchDB } from 'patch-db-client' +import { filter, first } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ServerNotifications, NotificationLevel, ServerNotification, } from 'src/app/services/api/api.types' -import { - AlertController, - LoadingController, - ModalController, -} from '@ionic/angular' -import { ActivatedRoute } from '@angular/router' -import { ErrorToastService } from '@start9labs/shared' -import { BackupReportPage } from 'src/app/apps/ui/modals/backup-report/backup-report.page' -import { PatchDB } from 'patch-db-client' +import { BackupReportComponent } from '../../modals/backup-report/backup-report.component' import { DataModel } from 'src/app/services/patch-db/data-model' -import { first } from 'rxjs' @Component({ selector: 'notifications', @@ -33,10 +31,9 @@ export class NotificationsPage { constructor( private readonly embassyApi: ApiService, - private readonly alertCtrl: AlertController, - private readonly loadingCtrl: LoadingController, - private readonly modalCtrl: ModalController, - private readonly errToast: ErrorToastService, + private readonly loader: LoadingService, + private readonly dialogs: TuiDialogService, + private readonly errorService: ErrorService, private readonly route: ActivatedRoute, private readonly patch: PatchDB, ) {} @@ -66,77 +63,55 @@ export class NotificationsPage { return notifications } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } return [] } async delete(id: number, index: number): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Deleting...', - }) - await loader.present() + const loader = this.loader.open('Deleting...').subscribe() try { await this.embassyApi.deleteNotification({ id }) this.notifications.splice(index, 1) this.beforeCursor = this.notifications[this.notifications.length - 1]?.id } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } - async presentAlertDeleteAll() { - const alert = await this.alertCtrl.create({ - header: 'Delete All?', - message: 'Are you sure you want to delete all notifications?', - buttons: [ - { - text: 'Cancel', - role: 'cancel', + presentAlertDeleteAll() { + this.dialogs + .open(TUI_PROMPT, { + label: 'Delete All?', + size: 's', + data: { + content: 'Are you sure you want to delete all notifications?', + yes: 'Delete', + no: 'Cancel', }, - { - text: 'Delete', - handler: () => { - this.deleteAll() - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => this.deleteAll()) } async viewBackupReport(notification: ServerNotification<1>) { - const modal = await this.modalCtrl.create({ - component: BackupReportPage, - componentProps: { - report: notification.data, - timestamp: notification['created-at'], - }, - }) - await modal.present() + this.dialogs + .open(new PolymorpheusComponent(BackupReportComponent), { + label: 'Backup Report', + data: { + report: notification.data, + timestamp: notification['created-at'], + }, + }) + .subscribe() } - async viewFullMessage(header: string, message: string) { - const alert = await this.alertCtrl.create({ - header, - message, - cssClass: 'notification-detail-alert', - buttons: [ - { - text: `OK`, - handler: () => { - alert.dismiss() - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() + viewFullMessage(label: string, message: string) { + this.dialogs.open(message, { label }).subscribe() } truncate(message: string): string { @@ -159,10 +134,7 @@ export class NotificationsPage { } private async deleteAll(): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Deleting...', - }) - await loader.present() + const loader = this.loader.open('Deleting...').subscribe() try { await this.embassyApi.deleteAllNotifications({ @@ -171,9 +143,9 @@ export class NotificationsPage { this.notifications = [] this.beforeCursor = undefined } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-actions/action-success/action-success.page.html b/frontend/projects/ui/src/app/apps/ui/pages/services/app-actions/action-success/action-success.page.html index da8cc7be5..ed8babcc0 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-actions/action-success/action-success.page.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-actions/action-success/action-success.page.html @@ -1,35 +1,22 @@ - - - Execution Complete - - - - - - - +

{{ actionRes.message }}

- -

{{ actionRes.message }}

- -
-
- -
- -

{{ actionRes.value }}

- - {{ actionRes.value }} - - - - +
+
+
- + +

{{ actionRes.value }}

+ + {{ actionRes.value }} + + + + +
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-actions/action-success/action-success.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-actions/action-success/action-success.page.ts index 48adb138a..f4b390269 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-actions/action-success/action-success.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-actions/action-success/action-success.page.ts @@ -1,38 +1,21 @@ -import { Component, Input } from '@angular/core' -import { ModalController, ToastController } from '@ionic/angular' +import { Component, Inject } from '@angular/core' +import { CopyService } from '@start9labs/shared' +import { TuiDialogContext } from '@taiga-ui/core' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' import { ActionResponse } from 'src/app/services/api/api.types' -import { copyToClipboard } from '@start9labs/shared' @Component({ selector: 'action-success', templateUrl: './action-success.page.html', }) export class ActionSuccessPage { - @Input() - actionRes!: ActionResponse - constructor( - private readonly modalCtrl: ModalController, - private readonly toastCtrl: ToastController, + @Inject(POLYMORPHEUS_CONTEXT) + private readonly context: TuiDialogContext, + readonly copyService: CopyService, ) {} - async copy(address: string) { - let message = '' - await copyToClipboard(address || '').then(success => { - message = success - ? 'Copied to clipboard!' - : 'Failed to copy to clipboard.' - }) - - const toast = await this.toastCtrl.create({ - header: message, - position: 'bottom', - duration: 1000, - }) - await toast.present() - } - - async dismiss() { - return this.modalCtrl.dismiss() + get actionRes(): ActionResponse { + return this.context.data } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-actions/app-actions.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-actions/app-actions.page.ts index e8eaa1010..42e354d06 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-actions/app-actions.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-actions/app-actions.page.ts @@ -6,27 +6,30 @@ import { PipeTransform, } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { AlertController, ModalController, NavController } from '@ionic/angular' +import { NavController } from '@ionic/angular' +import { + isEmptyObject, + getPkgId, + WithId, + ErrorService, + LoadingService, +} from '@start9labs/shared' +import { TuiDialogService } from '@taiga-ui/core' import { PatchDB } from 'patch-db-client' +import { filter, switchMap, timer } from 'rxjs' +import { ApiService } from 'src/app/services/api/embassy-api.service' import { Action, DataModel, PackageDataEntry, PackageState, } from 'src/app/services/patch-db/data-model' -import { - isEmptyObject, - getPkgId, - WithId, - ErrorService, -} from '@start9labs/shared' import { ActionSuccessPage } from './action-success/action-success.page' import { hasCurrentDeps } from 'src/app/util/has-deps' -import { filter } from 'rxjs' import { FormDialogService } from 'src/app/services/form-dialog.service' import { FormPage } from 'src/app/apps/ui/modals/form/form.page' -import { LoadingService } from 'src/app/common/loading/loading.service' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' +import { TUI_PROMPT } from '@taiga-ui/kit' @Component({ selector: 'app-actions', @@ -43,8 +46,7 @@ export class AppActionsPage { constructor( private readonly route: ActivatedRoute, private readonly embassyApi: ApiService, - private readonly modalCtrl: ModalController, - private readonly alertCtrl: AlertController, + private readonly dialogs: TuiDialogService, private readonly errorService: ErrorService, private readonly loader: LoadingService, private readonly navCtrl: NavController, @@ -54,13 +56,12 @@ export class AppActionsPage { async handleAction(action: WithId) { if (action.disabled) { - const alert = await this.alertCtrl.create({ - header: 'Forbidden', - message: action.disabled, - buttons: ['OK'], - cssClass: 'alert-error-message enter-click', - }) - await alert.present() + this.dialogs + .open(action.disabled, { + label: 'Forbidden', + size: 's', + }) + .subscribe() } else { if (action['input-spec'] && !isEmptyObject(action['input-spec'])) { this.formDialog.open(FormPage, { @@ -77,24 +78,20 @@ export class AppActionsPage { }, }) } else { - const alert = await this.alertCtrl.create({ - header: 'Confirm', - message: `Are you sure you want to execute action "${action.name}"? ${ - action.warning || '' - }`, - buttons: [ - { - text: 'Cancel', - role: 'cancel', + this.dialogs + .open(TUI_PROMPT, { + label: 'Confirm', + size: 's', + data: { + content: `Are you sure you want to execute action "${ + action.name + }"? ${action.warning || ''}`, + yes: 'Execute', + no: 'Cancel', }, - { - text: 'Execute', - handler: async () => this.executeAction(action.id), - cssClass: 'enter-click', - }, - ], - }) - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => this.executeAction(action.id)) } } } @@ -102,34 +99,26 @@ export class AppActionsPage { async tryUninstall(pkg: PackageDataEntry): Promise { const { title, alerts, id } = pkg.manifest - let message = + let content = alerts.uninstall || `Uninstalling ${title} will permanently delete its data` if (await hasCurrentDeps(this.patch, id)) { - message = `${message}. Services that depend on ${title} will no longer work properly and may crash` + content = `${content}. Services that depend on ${title} will no longer work properly and may crash` } - const alert = await this.alertCtrl.create({ - header: 'Warning', - message, - buttons: [ - { - text: 'Cancel', - role: 'cancel', + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content, + yes: 'Uninstall', + no: 'Cancel', }, - { - text: 'Uninstall', - handler: () => { - this.uninstall() - }, - cssClass: 'enter-click', - }, - ], - cssClass: 'alert-warning-message', - }) - - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => this.uninstall()) } private async uninstall() { @@ -155,20 +144,23 @@ export class AppActionsPage { const loader = this.loader.open('Executing action...').subscribe() try { - const res = await this.embassyApi.executePackageAction({ + const data = await this.embassyApi.executePackageAction({ id: this.pkgId, 'action-id': actionId, input, }) - const successModal = await this.modalCtrl.create({ - component: ActionSuccessPage, - componentProps: { - actionRes: res, - }, - }) + timer(500) + .pipe( + switchMap(() => + this.dialogs.open(new PolymorpheusComponent(ActionSuccessPage), { + label: 'Execution Complete', + data, + }), + ), + ) + .subscribe() - setTimeout(() => successModal.present(), 500) return true } catch (e: any) { this.errorService.handleError(e) diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-credentials/app-credentials.page.html b/frontend/projects/ui/src/app/apps/ui/pages/services/app-credentials/app-credentials.page.html index 4c9a8b41f..f2f9ecb18 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-credentials/app-credentials.page.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-credentials/app-credentials.page.html @@ -43,7 +43,7 @@ size="small" > - + { - const success = await copyToClipboard(text) - const message = success - ? 'Copied. Clearing clipboard in 20 seconds' - : 'Failed to copy.' - - const toast = await this.toastCtrl.create({ - header: message, - position: 'bottom', - duration: 2000, - }) - await toast.present() - } - mask(value: string) { return mask(value, 64) } @@ -64,7 +45,7 @@ export class AppCredentialsPage { id: this.pkgId, }) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { this.loading = false } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces-item.component.html b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces-item.component.html index 0e5942b6a..c04293e09 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces-item.component.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces-item.component.html @@ -26,7 +26,7 @@ name="qr-code-outline" > - + diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces.page.ts index 86650d044..52537ac0e 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/app-interfaces.page.ts @@ -1,11 +1,12 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { ModalController, ToastController } from '@ionic/angular' -import { getPkgId, copyToClipboard } from '@start9labs/shared' +import { getPkgId, CopyService } from '@start9labs/shared' import { AddressInfo, DataModel } from 'src/app/services/patch-db/data-model' import { PatchDB } from 'patch-db-client' import { map } from 'rxjs' import { QRComponent } from './qr.component' +import { TuiDialogService } from '@taiga-ui/core' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' @Component({ selector: 'app-interfaces', @@ -39,38 +40,20 @@ export class AppInterfacesItemComponent { addressInfo!: AddressInfo constructor( - private readonly toastCtrl: ToastController, - private readonly modalCtrl: ModalController, + private readonly dialogs: TuiDialogService, + readonly copyService: CopyService, ) {} launch(url: string): void { window.open(url, '_blank', 'noreferrer') } - async showQR(text: string): Promise { - const modal = await this.modalCtrl.create({ - component: QRComponent, - componentProps: { - text, - }, - cssClass: 'qr-modal', - }) - await modal.present() - } - - async copy(address: string): Promise { - let message = '' - await copyToClipboard(address || '').then(success => { - message = success - ? 'Copied to clipboard!' - : 'Failed to copy to clipboard.' - }) - - const toast = await this.toastCtrl.create({ - header: message, - position: 'bottom', - duration: 1000, - }) - await toast.present() + showQR(data: string) { + this.dialogs + .open(new PolymorpheusComponent(QRComponent), { + size: 'auto', + data, + }) + .subscribe() } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/qr.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/qr.component.ts index 8f34aa01d..a87e43863 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/qr.component.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-interfaces/qr.component.ts @@ -1,9 +1,14 @@ -import { Component, Input } from '@angular/core' +import { Component, Inject } from '@angular/core' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' +import { TuiDialogContext } from '@taiga-ui/core' @Component({ selector: 'qr', - template: '', + template: '', }) export class QRComponent { - @Input() text!: string + constructor( + @Inject(POLYMORPHEUS_CONTEXT) + readonly context: TuiDialogContext, + ) {} } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-additional/app-show-additional.component.html b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-additional/app-show-additional.component.html index 4fc0eea32..e337dd202 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-additional/app-show-additional.component.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-additional/app-show-additional.component.html @@ -13,7 +13,7 @@ *ngIf="manifest['git-hash'] as gitHash; else noHash" button detail="false" - (click)="copy(gitHash)" + (click)="copyService.copy(gitHash)" >

Git Hash

diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-additional/app-show-additional.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-additional/app-show-additional.component.ts index 3b4e84787..a61101cd0 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-additional/app-show-additional.component.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-additional/app-show-additional.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { ModalController, ToastController } from '@ionic/angular' -import { copyToClipboard, MarkdownComponent } from '@start9labs/shared' +import { CopyService, MarkdownComponent } from '@start9labs/shared' +import { TuiDialogService } from '@taiga-ui/core' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' import { from } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' import { PackageDataEntry } from 'src/app/services/patch-db/data-model' @@ -15,40 +16,26 @@ export class AppShowAdditionalComponent { pkg!: PackageDataEntry constructor( - private readonly modalCtrl: ModalController, - private readonly toastCtrl: ToastController, + readonly copyService: CopyService, + private readonly dialogs: TuiDialogService, private readonly api: ApiService, ) {} - async copy(address: string): Promise { - const success = await copyToClipboard(address) - const message = success - ? 'Copied to clipboard!' - : 'Failed to copy to clipboard.' - - const toast = await this.toastCtrl.create({ - header: message, - position: 'bottom', - duration: 1000, - }) - await toast.present() - } - - async presentModalLicense() { + presentModalLicense() { const { id, version } = this.pkg.manifest - const modal = await this.modalCtrl.create({ - componentProps: { - title: 'License', - content: from( - this.api.getStatic( - `/public/package-data/${id}/${version}/LICENSE.md`, + this.dialogs + .open(new PolymorpheusComponent(MarkdownComponent), { + label: 'License', + size: 'l', + data: { + content: from( + this.api.getStatic( + `/public/package-data/${id}/${version}/LICENSE.md`, + ), ), - ), - }, - component: MarkdownComponent, - }) - - await modal.present() + }, + }) + .subscribe() } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-status/app-show-status.component.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-status/app-show-status.component.ts index c3ad71beb..91eb1f7f0 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-status/app-show-status.component.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/components/app-show-status/app-show-status.component.ts @@ -4,6 +4,11 @@ import { Input, ViewChild, } from '@angular/core' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { TuiDialogService } from '@taiga-ui/core' +import { TUI_PROMPT } from '@taiga-ui/kit' +import { PatchDB } from 'patch-db-client' +import { filter } from 'rxjs' import { PackageStatus, PrimaryRendering, @@ -16,8 +21,6 @@ import { PackageDataEntry, PackageState, } from 'src/app/services/patch-db/data-model' -import { ErrorToastService } from '@start9labs/shared' -import { AlertController, LoadingController } from '@ionic/angular' import { ApiService } from 'src/app/services/api/embassy-api.service' import { FormDialogService } from 'src/app/services/form-dialog.service' import { @@ -27,7 +30,6 @@ import { import { DependencyInfo } from '../../pipes/to-dependencies.pipe' import { hasCurrentDeps } from 'src/app/util/has-deps' import { ConnectionService } from 'src/app/services/connection.service' -import { PatchDB } from 'patch-db-client' import { LaunchMenuComponent } from '../../../launch-menu/launch-menu.component' @Component({ @@ -51,9 +53,9 @@ export class AppShowStatusComponent { readonly connected$ = this.connectionService.connected$ constructor( - private readonly alertCtrl: AlertController, - private readonly errToast: ErrorToastService, - private readonly loadingCtrl: LoadingController, + private readonly dialogs: TuiDialogService, + private readonly errorService: ErrorService, + private readonly loader: LoadingService, private readonly embassyApi: ApiService, private readonly formDialog: FormDialogService, private readonly connectionService: ConnectionService, @@ -122,33 +124,25 @@ export class AppShowStatusComponent { async tryStop(): Promise { const { title, alerts, id } = this.pkg.manifest - let message = alerts.stop || '' + let content = alerts.stop || '' if (await hasCurrentDeps(this.patch, id)) { const depMessage = `Services that depend on ${title} will no longer work properly and may crash` - message = message ? `${message}.\n\n${depMessage}` : depMessage + content = content ? `${content}.\n\n${depMessage}` : depMessage } - if (message) { - const alert = await this.alertCtrl.create({ - header: 'Warning', - message, - buttons: [ - { - text: 'Cancel', - role: 'cancel', + if (content) { + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content, + yes: 'Stop', + no: 'Cancel', }, - { - text: 'Stop', - handler: () => { - this.stop() - }, - cssClass: 'enter-click', - }, - ], - cssClass: 'alert-warning-message', - }) - - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => this.stop()) } else { this.stop() } @@ -158,99 +152,71 @@ export class AppShowStatusComponent { const { id, title } = this.pkg.manifest if (await hasCurrentDeps(this.patch, id)) { - const alert = await this.alertCtrl.create({ - header: 'Warning', - message: `Services that depend on ${title} may temporarily experiences issues`, - buttons: [ - { - text: 'Cancel', - role: 'cancel', + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content: `Services that depend on ${title} may temporarily experiences issues`, + yes: 'Restart', + no: 'Cancel', }, - { - text: 'Restart', - handler: () => { - this.restart() - }, - cssClass: 'enter-click', - }, - ], - cssClass: 'alert-warning-message', - }) - - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => this.restart()) } else { this.restart() } } private async start(): Promise { - const loader = await this.loadingCtrl.create({ - message: `Starting...`, - }) - await loader.present() + const loader = this.loader.open(`Starting...`).subscribe() try { await this.embassyApi.startPackage({ id: this.id }) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async stop(): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Stopping...', - }) - await loader.present() + const loader = this.loader.open(`Stopping...`).subscribe() try { await this.embassyApi.stopPackage({ id: this.id }) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async restart(): Promise { - const loader = await this.loadingCtrl.create({ - message: `Restarting...`, - }) - await loader.present() + const loader = this.loader.open(`Restarting...`).subscribe() try { await this.embassyApi.restartPackage({ id: this.id }) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } - private async presentAlertStart(message: string): Promise { + private async presentAlertStart(content: string): Promise { return new Promise(async resolve => { - const alert = await this.alertCtrl.create({ - header: 'Alert', - message, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - handler: () => { - resolve(false) - }, + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content, + yes: 'Continue', + no: 'Cancel', }, - { - text: 'Continue', - handler: () => { - resolve(true) - }, - cssClass: 'enter-click', - }, - ], - }) - - await alert.present() + }) + .subscribe(response => resolve(response)) }) } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/modals/app-config/app-config.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/modals/app-config/app-config.page.ts index f606d067e..14596b7b8 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/modals/app-config/app-config.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/modals/app-config/app-config.page.ts @@ -1,16 +1,15 @@ import { Component, Inject } from '@angular/core' import { endWith, firstValueFrom, Subscription } from 'rxjs' -import { tuiIsString } from '@taiga-ui/cdk' -import { - TuiAlertService, - TuiDialogContext, - TuiDialogService, - TuiNotification, -} from '@taiga-ui/core' +import { TuiDialogContext, TuiDialogService } from '@taiga-ui/core' import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit' import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { getErrorMessage, isEmptyObject } from '@start9labs/shared' +import { + ErrorService, + getErrorMessage, + isEmptyObject, + LoadingService, +} from '@start9labs/shared' import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes' import { DataModel, @@ -22,7 +21,6 @@ import { hasCurrentDeps } from 'src/app/util/has-deps' import { getAllPackages, getPackage } from 'src/app/util/get-package-data' import { Breakages } from 'src/app/services/api/api.types' import { InvalidService } from 'src/app/common/form/invalid.service' -import { LoadingService } from 'src/app/common/loading/loading.service' import { DependentInfo } from 'src/app/types/dependent-info' import { ActionButton } from 'src/app/apps/ui/modals/form/form.page' @@ -63,7 +61,7 @@ export class AppConfigPage { @Inject(POLYMORPHEUS_CONTEXT) private readonly context: TuiDialogContext, private readonly dialogs: TuiDialogService, - private readonly alerts: TuiAlertService, + private readonly errorService: ErrorService, private readonly loader: LoadingService, private readonly embassyApi: ApiService, private readonly patchDb: PatchDB, @@ -99,9 +97,7 @@ export class AppConfigPage { this.spec = spec } } catch (e: any) { - const message = getErrorMessage(e) - - this.loadingError = tuiIsString(message) ? message : message.value + this.loadingError = getErrorMessage(e) } finally { this.loadingText = '' } @@ -119,7 +115,7 @@ export class AppConfigPage { await this.configure(config, loader) } } catch (e: any) { - this.showError(e) + this.errorService.handleError(e) } finally { loader.unsubscribe() } @@ -186,16 +182,4 @@ export class AppConfigPage { this.dialogs.open(TUI_PROMPT, { data }).pipe(endWith(false)), ) } - - private showError(e: any) { - const message = getErrorMessage(e) - - this.alerts - .open(tuiIsString(message) ? message : message.value, { - status: TuiNotification.Error, - autoClose: false, - label: 'Error', - }) - .subscribe() - } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/pipes/to-buttons.pipe.ts b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/pipes/to-buttons.pipe.ts index 02113250d..cf8252b75 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/pipes/to-buttons.pipe.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/services/app-show/pipes/to-buttons.pipe.ts @@ -1,7 +1,8 @@ import { Pipe, PipeTransform } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { ModalController, NavController } from '@ionic/angular' +import { NavController } from '@ionic/angular' import { MarkdownComponent } from '@start9labs/shared' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' import { DataModel, PackageDataEntry, @@ -14,6 +15,7 @@ import { import { ApiService } from 'src/app/services/api/embassy-api.service' import { from, map, Observable } from 'rxjs' import { PatchDB } from 'patch-db-client' +import { TuiDialogService } from '@taiga-ui/core' export interface Button { title: string @@ -31,7 +33,7 @@ export class ToButtonsPipe implements PipeTransform { constructor( private readonly route: ActivatedRoute, private readonly navCtrl: NavController, - private readonly modalCtrl: ModalController, + private readonly dialogs: TuiDialogService, private readonly formDialog: FormDialogService, private readonly apiService: ApiService, private readonly patch: PatchDB, @@ -110,19 +112,19 @@ export class ToButtonsPipe implements PipeTransform { .setDbValue(['ack-instructions', id], true) .catch(e => console.error('Failed to mark instructions as seen', e)) - const modal = await this.modalCtrl.create({ - componentProps: { - title: 'Instructions', - content: from( - this.apiService.getStatic( - `/public/package-data/${id}/${version}/INSTRUCTIONS.md`, + this.dialogs + .open(new PolymorpheusComponent(MarkdownComponent), { + label: 'Instructions', + size: 'l', + data: { + content: from( + this.apiService.getStatic( + `/public/package-data/${id}/${version}/INSTRUCTIONS.md`, + ), ), - ), - }, - component: MarkdownComponent, - }) - - await modal.present() + }, + }) + .subscribe() } private viewInMarketplaceButton(pkg: PackageDataEntry): Button { diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domain.const.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domain.const.ts new file mode 100644 index 000000000..1670fde80 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domain.const.ts @@ -0,0 +1,34 @@ +import { Config } from '@start9labs/start-sdk/lib/config/builder/config' +import { Value } from '@start9labs/start-sdk/lib/config/builder/value' + +export const domainSpec = Config.of({ + provider: Value.select({ + name: 'Provider', + required: { default: null }, + values: { + namecheap: 'Namecheap', + googledomains: 'Google Domains', + duckdns: 'Duck DNS', + changeip: 'ChangeIP', + easydns: 'easyDNS', + zoneedit: 'Zoneedit', + dyn: 'DynDNS', + }, + }), + domain: Value.text({ + name: 'Domain Name', + required: { default: null }, + placeholder: 'yourdomain.com', + }), + username: Value.text({ + name: 'Username', + required: { default: null }, + }), + password: Value.text({ + name: 'Password', + required: { default: null }, + masked: true, + }), +}) + +export type DomainSpec = typeof domainSpec.validator._TYPE diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/lan/lan.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.module.ts similarity index 57% rename from frontend/projects/ui/src/app/apps/ui/pages/system/lan/lan.module.ts rename to frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.module.ts index dc0216ce2..39af70e92 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/lan/lan.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.module.ts @@ -1,14 +1,15 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' -import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' -import { LANPage } from './lan.page' +import { RouterModule, Routes } from '@angular/router' +import { DomainsPage } from './domains.page' +import { TuiNotificationModule } from '@taiga-ui/core' import { SharedPipesModule } from '@start9labs/shared' const routes: Routes = [ { path: '', - component: LANPage, + component: DomainsPage, }, ] @@ -16,9 +17,10 @@ const routes: Routes = [ imports: [ CommonModule, IonicModule, + TuiNotificationModule, RouterModule.forChild(routes), SharedPipesModule, ], - declarations: [LANPage], + declarations: [DomainsPage], }) -export class LANPageModule {} +export class DomainsPageModule {} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.page.html b/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.page.html new file mode 100644 index 000000000..9ce477592 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.page.html @@ -0,0 +1,126 @@ + + + + + + Domains + + + + +
+ + Adding domains to StartOS enables you to access your server and service + interfaces over clearnet. + View instructions + +
+ + + + Start9.me + + + Claim + + + +
+ + + Domain + Added + Provider + In Use + + + + {{ start9Me.value }} + {{ start9Me.createdAt| date: 'medium' }} + Start9 + + + {{ qty }} Interfaces + + + N/A + + + + + + + + + + + +
+ + + Custom Domains + + + Add Domain + + + +
+ + + Domain + Added + Provider + In Use + + + + {{ domain.value }} + {{ domain.createdAt| date: 'medium' }} + {{ domain.provider }} + + + {{ qty }} Interfaces + + + N/A + + + + + + + + + + + +
+
+
diff --git a/frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.page.scss similarity index 100% rename from frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.scss rename to frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.page.scss diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.page.ts new file mode 100644 index 000000000..4ccaed895 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.page.ts @@ -0,0 +1,215 @@ +import { Component } from '@angular/core' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core' +import { TUI_PROMPT } from '@taiga-ui/kit' +import { combineLatest, filter, first, map, switchMap } from 'rxjs' +import { PatchDB } from 'patch-db-client' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { DataModel } from 'src/app/services/patch-db/data-model' +import { FormDialogService } from 'src/app/services/form-dialog.service' +import { DomainSpec, domainSpec } from './domain.const' +import { ConnectionService } from 'src/app/services/connection.service' +import { FormContext, FormPage } from '../../../modals/form/form.page' +import { getClearnetAddress } from 'src/app/util/clearnetAddress' + +@Component({ + selector: 'domains', + templateUrl: 'domains.page.html', + styleUrls: ['domains.page.scss'], +}) +export class DomainsPage { + readonly docsUrl = 'https://docs.start9.com/latest/user-manual/domains' + + readonly server$ = this.patch.watch$('server-info') + readonly pkgs$ = this.patch.watch$('package-data').pipe(first()) + + readonly domains$ = this.connectionService.connected$.pipe( + filter(Boolean), + switchMap(() => + combineLatest([this.server$, this.pkgs$]).pipe( + map(([{ ui, network }, packageData]) => { + const start9MeSubdomain = network.start9MeSubdomain + const start9Me = !start9MeSubdomain + ? null + : { + value: `${start9MeSubdomain.value}.start9.me`, + createdAt: start9MeSubdomain.createdAt, + provider: 'Start9', + usedBy: usedBy( + start9MeSubdomain.value, + getClearnetAddress('https', ui.domainInfo), + packageData, + ), + } + const custom = network.domains.map(domain => ({ + value: domain.value, + createdAt: domain.createdAt, + provider: domain.provider, + usedBy: usedBy( + domain.value, + getClearnetAddress('https', ui.domainInfo), + packageData, + ), + })) + + return { start9Me, custom } + }), + ), + ), + ) + + constructor( + private readonly errorService: ErrorService, + private readonly dialogs: TuiDialogService, + private readonly api: ApiService, + private readonly loader: LoadingService, + private readonly formDialog: FormDialogService, + private readonly connectionService: ConnectionService, + private readonly patch: PatchDB, + ) {} + + async presentModalAdd() { + const options: Partial>> = { + label: 'Custom Domain', + data: { + spec: await domainSpec.build({} as any), + buttons: [ + { + text: 'Save', + handler: async value => this.save(value), + }, + ], + }, + } + this.formDialog.open(FormPage, options) + } + + presentAlertClaimStart9MeDomain() { + this.dialogs + .open(TUI_PROMPT, { + label: 'Confirm', + size: 's', + data: { + content: 'Claim your start9.me domain?', + yes: 'Claim', + no: 'Cancel', + }, + }) + .pipe(filter(Boolean)) + .subscribe(() => this.claimStart9MeDomain()) + } + + presentAlertDelete(hostname: string) { + this.dialogs + .open(TUI_PROMPT, { + label: 'Confirm', + size: 's', + data: { + content: 'Delete domain?', + yes: 'Delete', + no: 'Cancel', + }, + }) + .pipe(filter(Boolean)) + .subscribe(() => this.delete(hostname)) + } + + presentAlertDeleteStart9Me() { + this.dialogs + .open(TUI_PROMPT, { + label: 'Confirm', + size: 's', + data: { + content: 'Delete start9.me domain?', + yes: 'Delete', + no: 'Cancel', + }, + }) + .pipe(filter(Boolean)) + .subscribe(() => this.deleteStart9MeDomain()) + } + + presentAlertUsedBy(domain: string, usedBy: string[]) { + this.dialogs + .open( + `${domain} is currently being used by:
    ${usedBy.map( + u => `
  • ${u}
  • `, + )}
`, + { + label: 'Used by', + size: 's', + }, + ) + .subscribe() + } + + private async claimStart9MeDomain(): Promise { + const loader = this.loader.open('Saving...').subscribe() + + try { + await this.api.claimStart9MeDomain({}) + return true + } catch (e: any) { + this.errorService.handleError(e) + return false + } finally { + loader.unsubscribe() + } + } + + private async save(value: DomainSpec): Promise { + const loader = this.loader.open('Saving...').subscribe() + + try { + await this.api.addDomain(value) + return true + } catch (e: any) { + this.errorService.handleError(e) + return false + } finally { + loader.unsubscribe() + } + } + + private async delete(hostname: string): Promise { + const loader = this.loader.open('Deleting...').subscribe() + + try { + await this.api.deleteDomain({ hostname }) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } + + private async deleteStart9MeDomain(): Promise { + const loader = this.loader.open('Deleting...').subscribe() + + try { + await this.api.deleteStart9MeDomain({}) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } +} + +function usedBy( + domain: string, + serverUi: string | null, + pkgs: DataModel['package-data'], +): string[] { + const list = [] + if (serverUi && serverUi.includes(domain)) list.push('StartOS Web Interface') + return list.concat( + Object.values(pkgs) + .filter(pkg => + Object.values(pkg.installed?.['address-info'] || {}).some(ai => + ai.addresses.some(a => a.includes(domain)), + ), + ) + .map(pkg => pkg.manifest.title), + ) +} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/email/email.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/email/email.page.ts index a7ea7a233..fb8ae298e 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/email/email.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/email/email.page.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core' import { UntypedFormGroup } from '@angular/forms' -import { ErrorService } from '@start9labs/shared' +import { ErrorService, LoadingService } from '@start9labs/shared' import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes' import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants' import { TuiDialogService } from '@taiga-ui/core' @@ -9,7 +9,6 @@ import { switchMap } from 'rxjs' import { ApiService } from 'src/app/services/api/embassy-api.service' import { DataModel } from 'src/app/services/patch-db/data-model' import { FormService } from 'src/app/services/form.service' -import { LoadingService } from 'src/app/common/loading/loading.service' import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec' @Component({ diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/experimental-features/experimental-features.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/experimental-features/experimental-features.module.ts index 86e374b17..436a9ed06 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/experimental-features/experimental-features.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/experimental-features/experimental-features.module.ts @@ -2,8 +2,10 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { Routes, RouterModule } from '@angular/router' import { IonicModule } from '@ionic/angular' -import { ExperimentalFeaturesPage } from './experimental-features.page' import { EmverPipesModule } from '@start9labs/shared' +import { TuiCheckboxLabeledModule, TuiPromptModule } from '@taiga-ui/kit' +import { ExperimentalFeaturesPage } from './experimental-features.page' +import { FormsModule } from '@angular/forms' const routes: Routes = [ { @@ -16,8 +18,11 @@ const routes: Routes = [ imports: [ CommonModule, IonicModule, + TuiPromptModule, RouterModule.forChild(routes), EmverPipesModule, + TuiCheckboxLabeledModule, + FormsModule, ], declarations: [ExperimentalFeaturesPage], }) diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/experimental-features/experimental-features.page.html b/frontend/projects/ui/src/app/apps/ui/pages/system/experimental-features/experimental-features.page.html index 69f82ff43..fefb783c7 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/experimental-features/experimental-features.page.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/experimental-features/experimental-features.page.html @@ -34,3 +34,18 @@ + + +

+ You are currently connected over Tor. If you reset the Tor daemon, you will + lose connectivity until it comes back online. +

+

Reset Tor?

+

+ Optionally wipe state to forcibly acquire new guard nodes. It is recommended + to try without wiping state first. +

+ + Wipe state + +
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/experimental-features/experimental-features.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/experimental-features/experimental-features.page.ts index ae0025204..c45646993 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/experimental-features/experimental-features.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/experimental-features/experimental-features.page.ts @@ -1,14 +1,17 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core' import { - AlertController, - LoadingController, - ToastController, -} from '@ionic/angular' + ChangeDetectionStrategy, + Component, + TemplateRef, + ViewChild, +} from '@angular/core' import { PatchDB } from 'patch-db-client' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ConfigService } from 'src/app/services/config.service' import { DataModel } from 'src/app/services/patch-db/data-model' -import { ErrorToastService } from '@start9labs/shared' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { TuiAlertService, TuiDialogService } from '@taiga-ui/core' +import { TUI_PROMPT } from '@taiga-ui/kit' +import { filter } from 'rxjs' @Component({ selector: 'experimental-features', @@ -19,138 +22,84 @@ import { ErrorToastService } from '@start9labs/shared' export class ExperimentalFeaturesPage { readonly server$ = this.patch.watch$('server-info') + @ViewChild('tor') + template?: TemplateRef + + wipe = false + constructor( - private readonly toastCtrl: ToastController, + private readonly alerts: TuiAlertService, private readonly patch: PatchDB, private readonly config: ConfigService, - private readonly alertCtrl: AlertController, - private readonly loadingCtrl: LoadingController, + private readonly dialogs: TuiDialogService, + private readonly loader: LoadingService, private readonly api: ApiService, - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, ) {} - async presentAlertResetTor() { - const isTor = this.config.isTor() - const shared = - 'Optionally wipe state to forcibly acquire new guard nodes. It is recommended to try without wiping state first.' - const alert = await this.alertCtrl.create({ - header: isTor ? 'Warning' : 'Confirm', - message: isTor - ? `You are currently connected over Tor. If you reset the Tor daemon, you will loose connectivity until it comes back online.

${shared}` - : `Reset Tor?

${shared}`, - inputs: [ - { - label: 'Wipe state', - type: 'checkbox', - value: 'wipe', - handler: val => { - console.error(val) - }, - }, - ], - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Reset', - handler: (value: string[]) => { - console.error(value) - this.resetTor(value.some(v => 'wipe')) - }, - cssClass: 'enter-click', - }, - ], - cssClass: isTor ? 'alert-warning-message' : '', - }) - await alert.present() + get isTor(): boolean { + return this.config.isTor() } - async presentAlertZram(enabled: boolean) { - const alert = await this.alertCtrl.create({ - header: enabled ? 'Confirm' : 'Warning', - message: enabled - ? 'Are you sure you want to disable zram?' - : 'zram on StartOS is experimental. It may increase performance of you server, especially if it is a low RAM device.', - buttons: [ - { - text: 'Cancel', - role: 'cancel', + async presentAlertResetTor() { + this.wipe = false + this.dialogs + .open(TUI_PROMPT, { + label: this.isTor ? 'Warning' : 'Confirm', + data: { + content: this.template, + yes: 'Reset', + no: 'Cancel', }, - { - text: enabled ? 'Disable' : 'Enable', - handler: () => { - this.toggleZram(enabled) - }, - cssClass: 'enter-click', + }) + .pipe(filter(Boolean)) + .subscribe(() => this.resetTor(this.wipe)) + } + + presentAlertZram(enabled: boolean) { + this.dialogs + .open(TUI_PROMPT, { + label: enabled ? 'Confirm' : 'Warning', + data: { + content: enabled + ? 'Are you sure you want to disable zram?' + : 'zram on StartOS is experimental. It may increase performance of you server, especially if it is a low RAM device.', + yes: enabled ? 'Disable' : 'Enable', + no: 'Cancel', }, - ], - cssClass: enabled ? '' : 'alert-warning-message', - }) - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => this.toggleZram(enabled)) } private async resetTor(wipeState: boolean) { - const loader = await this.loadingCtrl.create({ - message: 'Resetting Tor...', - }) - await loader.present() + const loader = this.loader.open('Resetting Tor...').subscribe() try { await this.api.resetTor({ 'wipe-state': wipeState, reason: 'User triggered', }) - const toast = await this.toastCtrl.create({ - header: 'Tor reset in progress', - position: 'bottom', - duration: 4000, - buttons: [ - { - side: 'start', - icon: 'close', - handler: () => { - return true - }, - }, - ], - }) - await toast.present() + this.alerts.open('Tor reset in progress').subscribe() } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async toggleZram(enabled: boolean) { - const loader = await this.loadingCtrl.create({ - message: enabled ? 'Disabling zram...' : 'Enabling zram', - }) - await loader.present() + const loader = this.loader + .open(enabled ? 'Disabling zram...' : 'Enabling zram') + .subscribe() try { await this.api.toggleZram({ enable: !enabled }) - const toast = await this.toastCtrl.create({ - header: `Zram ${enabled ? 'disabled' : 'enabled'}`, - position: 'bottom', - duration: 4000, - buttons: [ - { - side: 'start', - icon: 'close', - handler: () => { - return true - }, - }, - ], - }) - await toast.present() + this.alerts.open(`Zram ${enabled ? 'disabled' : 'enabled'}`).subscribe() } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/lan/lan.page.html b/frontend/projects/ui/src/app/apps/ui/pages/system/lan/lan.page.html deleted file mode 100644 index 5c11fea02..000000000 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/lan/lan.page.html +++ /dev/null @@ -1,43 +0,0 @@ - - - Secure LAN - - - - - - - - - - - -

- For a secure local connection, - - follow instructions - - to download and trust your server's Root Certificate Authority -

-
-
- - - - -

Download Certificate

-
-
-
- - - -
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/lan/lan.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/system/lan/lan.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/lan/lan.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/lan/lan.page.ts deleted file mode 100644 index b07b3f2f1..000000000 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/lan/lan.page.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core' -import { PatchDB } from 'patch-db-client' -import { map } from 'rxjs' -import { DataModel } from 'src/app/services/patch-db/data-model' - -@Component({ - selector: 'lan', - templateUrl: './lan.page.html', - styleUrls: ['./lan.page.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class LANPage { - readonly crtName$ = this.patch - .watch$('server-info', 'lan-address') - .pipe(map(addr => `${new URL(addr).hostname}.crt`)) - - constructor(private readonly patch: PatchDB) {} - - installCert(): void { - document.getElementById('install-cert')?.click() - } -} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.module.ts new file mode 100644 index 000000000..5eb444af1 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.module.ts @@ -0,0 +1,18 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { Routes, RouterModule } from '@angular/router' +import { IonicModule } from '@ionic/angular' +import { OSAddressesPage, OsClearnetPipe } from './os-addresses.page' + +const routes: Routes = [ + { + path: '', + component: OSAddressesPage, + }, +] + +@NgModule({ + imports: [CommonModule, IonicModule, RouterModule.forChild(routes)], + declarations: [OSAddressesPage, OsClearnetPipe], +}) +export class OSAddressesPageModule {} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.html b/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.html new file mode 100644 index 000000000..1f24cbd57 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.html @@ -0,0 +1,228 @@ + + + + + + StartOS Web Interface + + + + +
+ + Clearnet + + + +

+ Clearnet provides a fast and convenient experience. It not not + provide anonymity, and the addresses can be discovered and accessed + by anyone. + + View instructions + +

+
+
+ + + +

Clearnet

+

{{ clearnetAddress }}

+
+ + Update + + + Remove + +
+
+
+ + + + + + +
+
+
+ +
+ + + Add Clearnet Address + +
+
+
+ + + Tor + + + +

+ Tor offers privacy and anonymity at the expense of speed and + reliability. A Tor-enabled browser is required to use a Tor address. + + View instructions + +

+
+
+ + +

Tor

+

{{ torHostname }}

+
+
+ + + + + + +
+
+
+ + + LAN + + + +

+ LAN offers a fast and private experience. These addresses can only + be accessed from a device connected to the same LAN as your server, + either directly or using a VPN. + + View instructions + +

+
+ + + Download Root CA + +
+
+
+ + +

Local

+

{{ lanHostname }}

+
+
+ + + + + + +
+
+ + + +

{{ iface.key }} (IPv4)

+

{{ ipv4 }}

+
+
+ + + + + + +
+
+ + +

{{ iface.key }} (IPv6)

+

{{ ipv6 }}

+
+
+ + + + + + +
+
+
+
+
+ + + +
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.scss new file mode 100644 index 000000000..fab7b4db2 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.scss @@ -0,0 +1,15 @@ +ion-item-divider { + text-transform: unset; + padding-bottom: 12px; + padding-left: 0; +} + +ion-item-group { + background-color: #1e2024; + border: 1px solid #717171; + border-radius: 6px; +} + +ion-item { + --background: #1e2024; +} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.ts new file mode 100644 index 000000000..36c5c16b3 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.ts @@ -0,0 +1,168 @@ +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' +import { LoadingService, CopyService, ErrorService } from '@start9labs/shared' +import { Config } from '@start9labs/start-sdk/lib/config/builder/config' +import { Value } from '@start9labs/start-sdk/lib/config/builder/value' +import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes' +import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core' +import { PatchDB } from 'patch-db-client' +import { filter, map } from 'rxjs' +import { + DomainInfo, + DataModel, + NetworkInfo, + ServerInfo, +} from 'src/app/services/patch-db/data-model' +import { FormDialogService } from 'src/app/services/form-dialog.service' +import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { FormContext, FormPage } from '../../../modals/form/form.page' +import { TUI_PROMPT } from '@taiga-ui/kit' +import { DOCUMENT } from '@angular/common' +import { Pipe, PipeTransform } from '@angular/core' +import { getClearnetAddress } from 'src/app/util/clearnetAddress' + +export type ClearnetForm = { + domain: string + subdomain: string | null +} + +@Component({ + selector: 'os-addresses', + templateUrl: './os-addresses.page.html', + styleUrls: ['./os-addresses.page.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class OSAddressesPage { + readonly server$ = this.patch.watch$('server-info') + + readonly crtName$ = this.server$.pipe( + map(server => `${server.ui.lanHostname}.crt`), + ) + + constructor( + readonly copyService: CopyService, + private readonly loader: LoadingService, + private readonly formDialog: FormDialogService, + private readonly patch: PatchDB, + private readonly errorService: ErrorService, + private readonly api: ApiService, + private readonly dialogs: TuiDialogService, + @Inject(DOCUMENT) private readonly document: Document, + ) {} + + launch(url: string): void { + this.document.defaultView?.open(url, '_blank', 'noreferrer') + } + + installCert(): void { + this.document.getElementById('install-cert')?.click() + } + + async presentModalAddClearnet(server: ServerInfo) { + const domainInfo = server.ui.domainInfo + const options: Partial>> = { + label: 'Select Domain/Subdomain', + data: { + value: { + domain: domainInfo?.domain || '', + subdomain: domainInfo?.subdomain || '', + }, + spec: await this.getClearnetSpec(server.network), + buttons: [ + { + text: 'Manage domains', + link: '/system/domains', + }, + { + text: 'Save', + handler: async value => this.saveClearnet(value), + }, + ], + }, + } + this.formDialog.open(FormPage, options) + } + + presentAlertRemoveClearnet() { + this.dialogs + .open(TUI_PROMPT, { + label: 'Confirm', + size: 's', + data: { + content: 'Remove clearnet address?', + yes: 'Remove', + no: 'Cancel', + }, + }) + .pipe(filter(Boolean)) + .subscribe(() => this.removeClearnet()) + } + + private async saveClearnet(domainInfo: ClearnetForm): Promise { + const loader = this.loader.open('Saving...').subscribe() + + try { + await this.api.setServerClearnetAddress({ domainInfo }) + return true + } catch (e: any) { + this.errorService.handleError(e) + return false + } finally { + loader.unsubscribe() + } + } + + private async removeClearnet(): Promise { + const loader = this.loader.open('Removing...').subscribe() + + try { + await this.api.setServerClearnetAddress({ domainInfo: null }) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } + + private async getClearnetSpec({ + domains, + start9MeSubdomain, + }: NetworkInfo): Promise { + const start9MeDomain = `${start9MeSubdomain?.value}.start9.me` + const base = start9MeSubdomain ? { [start9MeDomain]: start9MeDomain } : {} + + return configBuilderToSpec( + Config.of({ + domain: Value.dynamicSelect(() => { + return { + name: 'Domain', + required: { default: null }, + values: domains.reduce((prev, curr) => { + return { + [curr.value]: curr.value, + ...prev, + } + }, base), + } + }), + subdomain: Value.text({ + name: 'Subdomain', + required: false, + }), + }), + ) + } + + asIsOrder(a: any, b: any) { + return 0 + } +} + +@Pipe({ + name: 'osClearnetPipe', +}) +export class OsClearnetPipe implements PipeTransform { + transform(clearnet: DomainInfo): string { + return getClearnetAddress('https', clearnet) + } +} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.module.ts new file mode 100644 index 000000000..b81c53fc3 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.module.ts @@ -0,0 +1,26 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { Routes, RouterModule } from '@angular/router' +import { IonicModule } from '@ionic/angular' +import { PortForwardsPage } from './port-forwards.page' +import { PrimaryIpPipeModule } from 'src/app/common/primary-ip/primary-ip.module' +import { FormsModule } from '@angular/forms' + +const routes: Routes = [ + { + path: '', + component: PortForwardsPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + RouterModule.forChild(routes), + PrimaryIpPipeModule, + FormsModule, + ], + declarations: [PortForwardsPage], +}) +export class PortForwardsPageModule {} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.html b/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.html new file mode 100644 index 000000000..a25127a9a --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.html @@ -0,0 +1,153 @@ + + + + + + Port Forwards + + + + +
+ + + + +

+ UPnP Disabled +

+

+ Below are a list of ports that must be + manually + forwarded in your router in order to enable clearnet access. +
+
+ Alternatively, you can enable UPnP in your router for automatic + configuration. + + View instructions + +

+
+ + +

+ UPnP Enabled! +

+

+ The ports below have been + automatically + forwarded in your router. +
+
+ If you are running multiple servers, you may want to override + specific ports to suite your needs. + + View instructions + +

+
+
+
+ + + + + Port + + + Target + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

{{ ip }}:{{ pf.target }}

+
+ + + +
+
+
+
+
+
+
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.scss new file mode 100644 index 000000000..50f21298d --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.scss @@ -0,0 +1,26 @@ +ion-item-divider { + padding-bottom: 8px; + padding-left: 0px; +} + +ion-item-group { + background-color: #1e2024; + border: 1px solid #717171; + border-radius: 6px; +} + +ion-item { + --inner-padding-end: 0; +} + +ion-buttons { + margin-left: 0; + margin-right: 8px; + ion-button::part(native) { + padding: 0 2px; + } +} + +.larger-icon { + font-size: 20px; +} \ No newline at end of file diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.ts new file mode 100644 index 000000000..fcb977bba --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.ts @@ -0,0 +1,46 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core' +import { PatchDB } from 'patch-db-client' +import { DataModel, PortForward } from 'src/app/services/patch-db/data-model' +import { LoadingService, CopyService, ErrorService } from '@start9labs/shared' +import { ApiService } from 'src/app/services/api/embassy-api.service' + +@Component({ + selector: 'port-forwards', + templateUrl: './port-forwards.page.html', + styleUrls: ['./port-forwards.page.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class PortForwardsPage { + readonly server$ = this.patch.watch$('server-info') + editing: Record = {} + overrides: Record = {} + + constructor( + readonly copyService: CopyService, + private readonly patch: PatchDB, + private readonly loader: LoadingService, + private readonly errorService: ErrorService, + private readonly api: ApiService, + ) {} + + async editPort(pf: PortForward) { + this.editing[pf.target] = !this.editing[pf.target] + this.overrides[pf.target] = pf.override || pf.assigned + } + + async saveOverride(pf: PortForward) { + const loader = this.loader.open('Saving...').subscribe() + + try { + await this.api.overridePortForward({ + target: pf.target, + port: this.overrides[pf.target], + }) + delete this.editing[pf.target] + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } +} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/os-update/os-update.page.html b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/os-update/os-update.page.html index 011a5cc51..e2eaebcfb 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/os-update/os-update.page.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/os-update/os-update.page.html @@ -1,43 +1,13 @@ - - -
- - StartOS {{ versions[0].version }} - -
- - Release Notes - -
- - - - - -
-
+

StartOS {{ versions[0].version }}

+

Release Notes

- -
- -

{{ v.version }}

-
-
-
-
-
+ + +

{{ v.version }}

+
+
+
- - - - - Begin Update - - - - + diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/os-update/os-update.page.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/os-update/os-update.page.module.ts index 2d3e0176a..426dc846d 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/os-update/os-update.page.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/os-update/os-update.page.module.ts @@ -1,12 +1,22 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' +import { MarkdownPipeModule, SafeLinksModule } from '@start9labs/shared' +import { TuiButtonModule, TuiScrollbarModule } from '@taiga-ui/core' +import { TuiAutoFocusModule } from '@taiga-ui/cdk' +import { NgDompurifyModule } from '@tinkoff/ng-dompurify' import { OSUpdatePage } from './os-update.page' -import { MarkdownPipeModule } from '@start9labs/shared' @NgModule({ declarations: [OSUpdatePage], - imports: [CommonModule, IonicModule, MarkdownPipeModule], + imports: [ + CommonModule, + MarkdownPipeModule, + TuiButtonModule, + TuiAutoFocusModule, + TuiScrollbarModule, + SafeLinksModule, + NgDompurifyModule, + ], exports: [OSUpdatePage], }) export class OSUpdatePageModule {} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/os-update/os-update.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/os-update/os-update.page.scss index 586a54126..d2f78caf7 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/os-update/os-update.page.scss +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/os-update/os-update.page.scss @@ -1,6 +1,24 @@ -.underline { - margin: 6px 0 8px 16px; - border-style: solid; - border-width: 0px 0px 1px 0px; - border-color: #404040; - } \ No newline at end of file +.title { + margin-top: 0; + font-weight: bold; +} + +.subtitle { + color: var(--tui-text-02); + font-weight: normal; +} + +.scrollbar { + margin: 24px 0; + max-height: 50vh; +} + +.version { + box-shadow: 0 1px var(--tui-base-02); + margin: 0 24px 0 0; + padding: 6px 0; +} + +.button { + float: right; +} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/os-update/os-update.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/os-update/os-update.page.ts index ffc544459..d87a3856e 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/os-update/os-update.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/os-update/os-update.page.ts @@ -1,6 +1,7 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core' -import { LoadingController, ModalController } from '@ionic/angular' -import { ErrorToastService } from '@start9labs/shared' +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' +import { TuiDialogContext } from '@taiga-ui/core' import { ApiService } from 'src/app/services/api/embassy-api.service' import { EOSService } from 'src/app/services/eos.service' @@ -14,9 +15,9 @@ export class OSUpdatePage { versions: { version: string; notes: string }[] = [] constructor( - private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, - private readonly errToast: ErrorToastService, + @Inject(POLYMORPHEUS_CONTEXT) private readonly context: TuiDialogContext, + private readonly loader: LoadingService, + private readonly errorService: ErrorService, private readonly embassyApi: ApiService, private readonly eosService: EOSService, ) {} @@ -27,35 +28,22 @@ export class OSUpdatePage { this.versions = Object.keys(releaseNotes) .sort() .reverse() - .map(version => { - return { - version, - notes: releaseNotes[version], - } - }) - } - - dismiss() { - this.modalCtrl.dismiss() + .map(version => ({ + version, + notes: releaseNotes[version], + })) } async updateEOS() { - const loader = await this.loadingCtrl.create({ - message: 'Beginning update...', - }) - await loader.present() + const loader = this.loader.open('Beginning update...').subscribe() try { await this.embassyApi.updateServer() - this.dismiss() + this.context.$implicit.complete() } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } - - asIsOrder() { - return 0 - } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.module.ts index f12152c8a..18799d6b5 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.module.ts @@ -8,7 +8,7 @@ import { TextSpinnerComponentModule } from '@start9labs/shared' import { BadgeMenuComponentModule } from 'src/app/common/badge-menu-button/badge-menu.component.module' import { InsecureWarningComponentModule } from 'src/app/common/insecure-warning/insecure-warning.module' import { OSUpdatePageModule } from './os-update/os-update.page.module' -import { GenericInputComponentModule } from 'src/app/apps/ui/modals/generic-input/generic-input.component.module' +import { PromptModule } from 'src/app/apps/ui/modals/prompt/prompt.module' import { ThemeSwitcherModule } from '../theme-switcher/theme-switcher.module' import { BackupColorPipe } from './backup-color.pipe' @@ -29,7 +29,7 @@ const routes: Routes = [ OSUpdatePageModule, ThemeSwitcherModule, InsecureWarningComponentModule, - GenericInputComponentModule, + PromptModule, RouterModule.forChild(routes), ], declarations: [ServerShowPage, BackupColorPipe], diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.html b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.html index cc0d96c74..9c9dc22fb 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.html @@ -7,14 +7,18 @@ - + - +
@@ -23,46 +27,48 @@ {{ cat.key }} - - - - -

{{ button.title }}

-

{{ button.description }}

+ + + + + +

{{ button.title }}

+

{{ button.description }}

- -

- - Update Complete. Restart to apply changes - - - - - - Update Available - - - - - - Check for updates - + +

+ + Update Complete. Restart to apply changes + + + + + + Update Available + + + + + + Check for updates + + - -

-
-
+

+
+
+
- +
diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.scss index e69de29bb..84f709c07 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.scss +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.scss @@ -0,0 +1,15 @@ +ion-item-divider { + text-transform: unset; + padding-bottom: 12px; + padding-left: 0; +} + +ion-item-group { + background-color: #1e2024; + border: 1px solid #717171; + border-radius: 6px; +} + +ion-item { + --background: #1e2024; +} \ No newline at end of file diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.ts index 7dd200e63..d7915d551 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/server-show/server-show.page.ts @@ -1,28 +1,22 @@ import { DOCUMENT } from '@angular/common' import { Component, Inject } from '@angular/core' -import { - AlertController, - LoadingController, - NavController, - ModalController, - ToastController, -} from '@ionic/angular' +import { NavController } from '@ionic/angular' import { ApiService } from 'src/app/services/api/embassy-api.service' import { ActivatedRoute } from '@angular/router' import { PatchDB } from 'patch-db-client' -import { firstValueFrom, Observable, of } from 'rxjs' -import { ErrorToastService } from '@start9labs/shared' +import { filter, Observable, of, switchMap, take } from 'rxjs' +import { ErrorService, LoadingService } from '@start9labs/shared' import { EOSService } from 'src/app/services/eos.service' import { ClientStorageService } from 'src/app/services/client-storage.service' import { OSUpdatePage } from './os-update/os-update.page' import { getAllPackages } from 'src/app/util/get-package-data' import { AuthService } from 'src/app/services/auth.service' import { DataModel } from 'src/app/services/patch-db/data-model' -import { - GenericInputComponent, - GenericInputOptions, -} from 'src/app/apps/ui/modals/generic-input/generic-input.component' import { ConfigService } from 'src/app/services/config.service' +import { TuiAlertService, TuiDialogService } from '@taiga-ui/core' +import { PROMPT } from 'src/app/apps/ui/modals/prompt/prompt.component' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' +import { TUI_PROMPT } from '@taiga-ui/kit' @Component({ selector: 'server-show', @@ -35,31 +29,30 @@ export class ServerShowPage { readonly server$ = this.patch.watch$('server-info') readonly showUpdate$ = this.eosService.showUpdate$ - readonly showDiskRepair$ = this.ClientStorageService.showDiskRepair$ + readonly showDiskRepair$ = this.clientStorageService.showDiskRepair$ readonly secure = this.config.isSecure() constructor( - private readonly alertCtrl: AlertController, - private readonly modalCtrl: ModalController, - private readonly loadingCtrl: LoadingController, - private readonly errToast: ErrorToastService, + private readonly dialogs: TuiDialogService, + private readonly loader: LoadingService, + private readonly errorService: ErrorService, private readonly embassyApi: ApiService, private readonly navCtrl: NavController, private readonly route: ActivatedRoute, private readonly patch: PatchDB, private readonly eosService: EOSService, - private readonly ClientStorageService: ClientStorageService, + private readonly clientStorageService: ClientStorageService, private readonly authService: AuthService, - private readonly toastCtrl: ToastController, + private readonly alerts: TuiAlertService, private readonly config: ConfigService, @Inject(DOCUMENT) private readonly document: Document, ) {} addClick(title: string) { switch (title) { - case 'Manage': - this.addManageClick() + case 'Security': + this.addSecurityClick() break case 'Power': this.addPowerClick() @@ -70,164 +63,118 @@ export class ServerShowPage { } private async setBrowserTab(): Promise { - const chosenName = await firstValueFrom(this.patch.watch$('ui', 'name')) - - const options: GenericInputOptions = { - title: 'Browser Tab Title', - message: `This value will be displayed as the title of your browser tab.`, - label: 'Device Name', - useMask: false, - placeholder: 'StartOS', - required: false, - initialValue: chosenName, - buttonText: 'Save', - submitFn: (name: string) => this.setName(name || null), - } - - const modal = await this.modalCtrl.create({ - componentProps: { options }, - cssClass: 'alertlike-modal', - presentingElement: await this.modalCtrl.getTop(), - component: GenericInputComponent, - }) - - await modal.present() + this.patch + .watch$('ui', 'name') + .pipe( + switchMap(initialValue => + this.dialogs.open(PROMPT, { + label: 'Browser Tab Title', + data: { + message: `This value will be displayed as the title of your browser tab.`, + label: 'Device Name', + placeholder: 'StartOS', + required: false, + buttonText: 'Save', + initialValue, + }, + }), + ), + take(1), + ) + .subscribe(name => this.setName(name || null)) } - private async updateEos(): Promise { - const modal = await this.modalCtrl.create({ - component: OSUpdatePage, - }) - modal.present() + private updateEos() { + this.dialogs.open(new PolymorpheusComponent(OSUpdatePage)).subscribe() } - private async presentAlertLogout() { - const alert = await this.alertCtrl.create({ - header: 'Confirm', - message: 'Are you sure you want to log out?', - buttons: [ - { - text: 'Cancel', - role: 'cancel', + private presentAlertLogout() { + this.dialogs + .open(TUI_PROMPT, { + label: 'Confirm', + size: 's', + data: { + content: 'Are you sure you want to log out?', + yes: 'Logout', + no: 'Cancel', }, - { - text: 'Logout', - handler: () => this.logout(), - cssClass: 'enter-click', - }, - ], - }) - - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => this.logout()) } - private async presentAlertRestart() { - const alert = await this.alertCtrl.create({ - header: 'Restart', - message: - 'Are you sure you want to restart your server? It can take several minutes to come back online.', - buttons: [ - { - text: 'Cancel', - role: 'cancel', + private presentAlertRestart() { + this.dialogs + .open(TUI_PROMPT, { + label: 'Restart', + size: 's', + data: { + content: + 'Are you sure you want to restart your server? It can take several minutes to come back online.', + yes: 'Restart', + no: 'Cancel', }, - { - text: 'Restart', - handler: () => { - this.restart() - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => this.restart()) } - private async presentAlertShutdown() { - const alert = await this.alertCtrl.create({ - header: 'Warning', - message: - 'Are you sure you want to power down your server? This can take several minutes, and your server will not come back online automatically. To power on again, you will need to physically unplug your server and plug it back in.', - buttons: [ - { - text: 'Cancel', - role: 'cancel', + private presentAlertShutdown() { + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content: + 'Are you sure you want to power down your server? This can take several minutes, and your server will not come back online automatically. To power on again, You will need to physically unplug your server and plug it back in', + yes: 'Shutdown', + no: 'Cancel', }, - { - text: 'Shutdown', - handler: () => { - this.shutdown() - }, - cssClass: 'enter-click', - }, - ], - cssClass: 'alert-warning-message', - }) - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => this.shutdown()) } private async presentAlertSystemRebuild() { const localPkgs = await getAllPackages(this.patch) const minutes = Object.keys(localPkgs).length * 2 - const alert = await this.alertCtrl.create({ - header: 'Warning', - message: `This action will tear down all service containers and rebuild them from scratch. No data will be deleted. This action is useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues. It may take up to ${minutes} minutes to complete. During this time, you will lose all connectivity to your server.`, - buttons: [ - { - text: 'Cancel', - role: 'cancel', + + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content: `This action will tear down all service containers and rebuild them from scratch. No data will be deleted. This action is useful if your system gets into a bad state, and it should only be performed if you are experiencing general performance or reliability issues. It may take up to ${minutes} minutes to complete. During this time, you will lose all connectivity to your server.`, + yes: 'Rebuild', + no: 'Cancel', }, - { - text: 'Rebuild', - handler: () => { - this.systemRebuild() - }, - cssClass: 'enter-click', - }, - ], - cssClass: 'alert-warning-message', - }) - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => this.systemRebuild()) } - private async presentAlertRepairDisk() { - const alert = await this.alertCtrl.create({ - header: 'Warning', - message: `

This action should only be executed if directed by a Start9 support specialist. We recommend backing up your device before preforming this action.

If anything happens to the device during the reboot, such as losing power or unplugging the drive, the filesystem will be in an unrecoverable state. Please proceed with caution.

`, - buttons: [ - { - text: 'Cancel', - role: 'cancel', + private presentAlertRepairDisk() { + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content: `This action should only be executed if directed by a Start9 support specialist. We recommend backing up your device before preforming this action.

If anything happens to the device during the reboot, such as losing power or unplugging the drive, the filesystem will be in an unrecoverable state. Please proceed with caution.

`, + yes: 'Rebuild', + no: 'Cancel', }, - { - text: 'Repair', - handler: () => { - try { - this.embassyApi.repairDisk({}).then(_ => { - this.restart() - }) - } catch (e: any) { - this.errToast.present(e) - } - }, - cssClass: 'enter-click', - }, - ], - cssClass: 'alert-warning-message', - }) - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => this.systemRebuild()) } private async setName(value: string | null): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Saving...', - }) - await loader.present() + const loader = this.loader.open('Saving...').subscribe() try { await this.embassyApi.setDbValue(['name'], value) } finally { - loader.dismiss() + loader.unsubscribe() } } @@ -239,29 +186,21 @@ export class ServerShowPage { private async restart() { const action = 'Restart' - - const loader = await this.loadingCtrl.create({ - message: `Beginning ${action}...`, - }) - await loader.present() + const loader = this.loader.open(`Beginning ${action}...`).subscribe() try { await this.embassyApi.restartServer({}) this.presentAlertInProgress(action, ` until ${action} completes.`) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async shutdown() { const action = 'Shutdown' - - const loader = await this.loadingCtrl.create({ - message: `Beginning ${action}...`, - }) - await loader.present() + const loader = this.loader.open(`Beginning ${action}...`).subscribe() try { await this.embassyApi.shutdownServer({}) @@ -270,40 +209,33 @@ export class ServerShowPage { '.

You will need to physically power cycle the device to regain connectivity.', ) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async systemRebuild() { const action = 'System Rebuild' - - const loader = await this.loadingCtrl.create({ - message: `Beginning ${action}...`, - }) - await loader.present() + const loader = this.loader.open(`Beginning ${action}...`).subscribe() try { await this.embassyApi.systemRebuild({}) this.presentAlertInProgress(action, ` until ${action} completes.`) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } private async checkForEosUpdate(): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Checking for updates', - }) - await loader.present() + const loader = this.loader.open('Checking for updates').subscribe() try { await this.eosService.loadEos() - await loader.dismiss() + loader.unsubscribe() if (this.eosService.updateAvailable$.value) { this.updateEos() @@ -311,44 +243,43 @@ export class ServerShowPage { this.presentAlertLatest() } } catch (e: any) { - await loader.dismiss() - this.errToast.present(e) + loader.unsubscribe() + this.errorService.handleError(e) } } - private async presentAlertLatest() { - const alert = await this.alertCtrl.create({ - header: 'Up to date!', - message: 'You are on the latest version of StartOS.', - buttons: [ - { - text: 'OK', - role: 'cancel', - cssClass: 'enter-click', - }, - ], - cssClass: 'alert-success-message', - }) - alert.present() + private presentAlertLatest() { + this.dialogs + .open('You are on the latest version of StartOS.', { + label: 'Up to date!', + size: 's', + }) + .subscribe() } - private async presentAlertInProgress(verb: string, message: string) { - const alert = await this.alertCtrl.create({ - header: `${verb} In Progress...`, - message: `Stopping all services gracefully. This can take a while.

If you have a speaker, your server will ♫ play a melody ♫ before shutting down. Your server will then become unreachable${message}`, - buttons: [ + private presentAlertInProgress(verb: string, message: string) { + this.dialogs + .open( + `Stopping all services gracefully. This can take a while.

If you have a speaker, your server will ♫ play a melody ♫ before shutting down. Your server will then become unreachable${message}`, { - text: 'OK', - role: 'cancel', - cssClass: 'enter-click', + label: `${verb} In Progress...`, + size: 's', }, - ], - }) - alert.present() + ) + .subscribe() } settings: ServerSettings = { - Manage: [ + General: [ + { + title: 'About', + description: 'Basic information about your server', + icon: 'information-circle-outline', + action: () => + this.navCtrl.navigateForward(['specs'], { relativeTo: this.route }), + detail: true, + disabled$: of(false), + }, { title: 'Software Update', description: 'Get the latest version of StartOS', @@ -368,43 +299,16 @@ export class ServerShowPage { detail: false, disabled$: of(false), }, - { - title: 'LAN', - description: `Download and trust your server's certificate for a secure local connection`, - icon: 'home-outline', - action: () => - this.navCtrl.navigateForward(['lan'], { relativeTo: this.route }), - detail: true, - disabled$: of(false), - }, - { - title: 'SSH', - description: - 'Manage your SSH keys to access your server from the command line', - icon: 'terminal-outline', - action: () => - this.navCtrl.navigateForward(['ssh'], { relativeTo: this.route }), - detail: true, - disabled$: of(false), - }, { title: 'Email', - description: 'Provide an external SMTP server for sending emails', + description: + 'Connect to an external SMTP server to send yourself emails', icon: 'mail-outline', action: () => this.navCtrl.navigateForward(['email'], { relativeTo: this.route }), detail: true, disabled$: of(false), }, - { - title: 'WiFi', - description: 'Add or remove WiFi networks', - icon: 'wifi', - action: () => - this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }), - detail: true, - disabled$: of(false), - }, { title: 'Sideload a Service', description: `Manually install a service`, @@ -428,29 +332,65 @@ export class ServerShowPage { disabled$: of(false), }, ], - Insights: [ + Network: [ { - title: 'About', - description: 'Basic information about your server', - icon: 'information-circle-outline', + title: 'StartOS Web Interface', + description: 'Addresses for accessing this StartOS web interface', + icon: 'desktop-outline', action: () => - this.navCtrl.navigateForward(['specs'], { relativeTo: this.route }), + this.navCtrl.navigateForward(['addresses'], { + relativeTo: this.route, + }), detail: true, disabled$: of(false), }, { - title: 'Monitor', - description: 'CPU, disk, memory, and other useful metrics', - icon: 'pulse', + title: 'Domains', + description: + 'Add domains to your server to enable clearnet connections', + icon: 'globe-outline', action: () => - this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }), + this.navCtrl.navigateForward(['domains'], { relativeTo: this.route }), + detail: true, + disabled$: of(false), + }, + { + title: 'Port Forwards', + description: + 'A list of ports that should be forwarded through your router', + icon: 'trail-sign-outline', + action: () => + this.navCtrl.navigateForward(['port-forwards'], { + relativeTo: this.route, + }), + detail: true, + disabled$: of(false), + }, + { + title: 'WiFi', + description: 'Add or remove WiFi networks', + icon: 'wifi', + action: () => + this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }), + detail: true, + disabled$: of(false), + }, + ], + Security: [ + { + title: 'SSH', + description: + 'Manage your SSH keys to access your server from the command line', + icon: 'terminal-outline', + action: () => + this.navCtrl.navigateForward(['ssh'], { relativeTo: this.route }), detail: true, disabled$: of(false), }, { title: 'Active Sessions', description: 'View and manage device access', - icon: 'desktop-outline', + icon: 'stopwatch-outline', action: () => this.navCtrl.navigateForward(['sessions'], { relativeTo: this.route, @@ -458,6 +398,17 @@ export class ServerShowPage { detail: true, disabled$: of(false), }, + ], + Logs: [ + { + title: 'System Resources', + description: 'CPU, disk, memory, and other useful metrics', + icon: 'pulse', + action: () => + this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }), + detail: true, + disabled$: of(false), + }, { title: 'OS Logs', description: 'Raw, unfiltered operating system logs', @@ -510,11 +461,7 @@ export class ServerShowPage { description: 'Get help from the Start9 team and community', icon: 'chatbubbles-outline', action: () => - window.open( - 'https://start9.com/contact', - '_blank', - 'noreferrer', - ), + window.open('https://start9.com/contact', '_blank', 'noreferrer'), detail: true, disabled$: of(false), }, @@ -576,18 +523,18 @@ export class ServerShowPage { ], } - private async addManageClick() { + private addSecurityClick() { this.manageClicks++ + if (this.manageClicks === 5) { this.manageClicks = 0 - const newVal = this.ClientStorageService.toggleShowDevTools() - const toast = await this.toastCtrl.create({ - header: newVal ? 'Dev tools unlocked' : 'Dev tools hidden', - position: 'bottom', - duration: 1000, - }) - - await toast.present() + this.alerts + .open( + this.clientStorageService.toggleShowDevTools() + ? 'Dev tools unlocked' + : 'Dev tools hidden', + ) + .subscribe() } } @@ -595,7 +542,7 @@ export class ServerShowPage { this.powerClicks++ if (this.powerClicks === 5) { this.powerClicks = 0 - this.ClientStorageService.toggleShowDiskRepair() + this.clientStorageService.toggleShowDiskRepair() } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.page.html b/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.page.html index 2cebf019e..0c3b24f92 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.page.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.page.html @@ -21,68 +21,18 @@

Git Hash

{{ gitHash }}

- + - Web Addresses - - -

Tor

-

{{ torAddress }}

-
-
- - - - - - -
-
- - -

LAN

-

{{ lanAddress }}

-
-
- - - - - - -
-
- - - -

{{ iface.key }} (IPv4)

-

{{ ipv4 || 'n/a' }}

-
- - - -
- - -

{{ iface.key }} (IPv6)

-

{{ ipv6 || 'n/a' }}

-
- - - -
-
- Device Credentials @@ -94,7 +44,7 @@ diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.page.ts index d4efca83d..7e09f38f8 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/server-specs/server-specs.page.ts @@ -1,8 +1,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' -import { ToastController } from '@ionic/angular' import { PatchDB } from 'patch-db-client' import { ConfigService } from 'src/app/services/config.service' -import { copyToClipboard } from '@start9labs/shared' +import { CopyService } from '@start9labs/shared' import { DataModel } from 'src/app/services/patch-db/data-model' @Component({ @@ -15,7 +14,7 @@ export class ServerSpecsPage { readonly server$ = this.patch.watch$('server-info') constructor( - private readonly toastCtrl: ToastController, + readonly copyService: CopyService, private readonly patch: PatchDB, private readonly config: ConfigService, ) {} @@ -28,22 +27,6 @@ export class ServerSpecsPage { window.open(url, '_blank', 'noreferrer') } - async copy(address: string) { - let message = '' - await copyToClipboard(address || '').then(success => { - message = success - ? 'Copied to clipboard!' - : 'Failed to copy to clipboard.' - }) - - const toast = await this.toastCtrl.create({ - header: message, - position: 'bottom', - duration: 1000, - }) - await toast.present() - } - asIsOrder(a: any, b: any) { return 0 } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/sessions/sessions.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/sessions/sessions.module.ts index be0905775..444321c36 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/sessions/sessions.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/sessions/sessions.module.ts @@ -4,6 +4,7 @@ import { IonicModule } from '@ionic/angular' import { RouterModule, Routes } from '@angular/router' import { PlatformInfoPipe, SessionsPage } from './sessions.page' import { SharedPipesModule } from '@start9labs/shared' +import { TuiLetModule } from '@taiga-ui/cdk' const routes: Routes = [ { @@ -18,6 +19,7 @@ const routes: Routes = [ IonicModule, RouterModule.forChild(routes), SharedPipesModule, + TuiLetModule, ], declarations: [SessionsPage, PlatformInfoPipe], }) diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/sessions/sessions.page.html b/frontend/projects/ui/src/app/apps/ui/pages/system/sessions/sessions.page.html index 631d9aaee..d8579c6fa 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/sessions/sessions.page.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/sessions/sessions.page.html @@ -18,16 +18,10 @@ Platform Last Active - - - - - - - - - + + + + + + + + +
- - Other Sessions - - Terminate Selected - - + + + Other Sessions + + Terminate Selected + + -
- - - -
- -
- User Agent -
- Platform - Last Active -
- - - - - - - - - - - +
+ + -
+
- {{ session['user-agent'] }} - - - - -   {{ info.name }} - - - - {{ session['last-active']| date: 'medium' }} + User Agent + Platform + Last Active -

- You are not logged in anywhere else -

- - -
+ + + + +
+ +
+ {{ session['user-agent'] }} +
+ + + +   {{ info.name }} + + + + {{ session['last-active']| date: 'medium' }} + +
+

+ You are not logged in anywhere else +

+
+ + + + + + + + +
+
+ diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/sessions/sessions.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/sessions/sessions.page.ts index 6a4a565a5..607d4ce29 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/sessions/sessions.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/sessions/sessions.page.ts @@ -1,10 +1,9 @@ import { Component } from '@angular/core' import { Pipe, PipeTransform } from '@angular/core' -import { LoadingController } from '@ionic/angular' -import { ErrorToastService } from '@start9labs/shared' +import { ErrorService, LoadingService } from '@start9labs/shared' import { ApiService } from 'src/app/services/api/embassy-api.service' import { PlatformType, Session } from 'src/app/services/api/api.types' -import { BehaviorSubject } from 'rxjs' +import { Observable, Subject, from, map, merge, shareReplay } from 'rxjs' @Component({ selector: 'sessions', @@ -12,14 +11,37 @@ import { BehaviorSubject } from 'rxjs' styleUrls: ['sessions.page.scss'], }) export class SessionsPage { - currentSession?: Session - otherSessions: SessionWithId[] = [] + private readonly sessions$ = from(this.api.getSessions({})) + private readonly localOther$ = new Subject() + private readonly remoteOther$: Observable = + this.sessions$.pipe( + map(s => + Object.entries(s.sessions) + .filter(([id, _]) => id !== s.current) + .map(([id, session]) => ({ + id, + ...session, + })) + .sort( + (a, b) => + new Date(b['last-active']).valueOf() - + new Date(a['last-active']).valueOf(), + ), + ), + ) + + readonly currentSession$ = this.sessions$.pipe( + map(s => s.sessions[s.current]), + shareReplay(), + ) + + readonly otherSessions$ = merge(this.localOther$, this.remoteOther$) + selected: Record = {} - loading$ = new BehaviorSubject(true) constructor( - private readonly loadingCtrl: LoadingController, - private readonly errToast: ErrorToastService, + private readonly loader: LoadingService, + private readonly errorService: ErrorService, private readonly api: ApiService, ) {} @@ -31,31 +53,6 @@ export class SessionsPage { return Object.keys(this.selected).length } - async ngOnInit() { - try { - const sessionInfo = await this.api.getSessions({}) - this.currentSession = sessionInfo.sessions[sessionInfo.current] - delete sessionInfo.sessions[sessionInfo.current] - this.otherSessions = Object.entries(sessionInfo.sessions) - .map(([id, session]) => { - return { - id, - ...session, - } - }) - .sort((a, b) => { - return ( - new Date(b['last-active']).valueOf() - - new Date(a['last-active']).valueOf() - ) - }) - } catch (e: any) { - this.errToast.present(e) - } finally { - this.loading$.next(false) - } - } - async toggleChecked(id: string) { if (this.selected[id]) { delete this.selected[id] @@ -64,30 +61,29 @@ export class SessionsPage { } } - async toggleAll() { + async toggleAll(otherSessions: SessionWithId[]) { if (this.empty) { - this.otherSessions.forEach(s => (this.selected[s.id] = true)) + otherSessions.forEach(s => (this.selected[s.id] = true)) } else { this.selected = {} } } - async kill(): Promise { + async kill(otherSessions: SessionWithId[]): Promise { const ids = Object.keys(this.selected) - const loader = await this.loadingCtrl.create({ - message: `Terminating session${ids.length > 1 ? 's' : ''}...`, - }) - await loader.present() + const loader = this.loader + .open(`Terminating session${ids.length > 1 ? 's' : ''}...`) + .subscribe() try { await this.api.killSessions({ ids }) this.selected = {} - this.otherSessions = this.otherSessions.filter(s => !ids.includes(s.id)) + this.localOther$.next(otherSessions.filter(s => !ids.includes(s.id))) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/sideload/sideload.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/sideload/sideload.page.ts index 4674b7bb2..f60b62ca9 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/sideload/sideload.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/sideload/sideload.page.ts @@ -1,9 +1,9 @@ import { Component } from '@angular/core' -import { isPlatform, LoadingController, NavController } from '@ionic/angular' +import { isPlatform, NavController } from '@ionic/angular' import { ApiService } from 'src/app/services/api/embassy-api.service' import { Manifest, MarketplacePkg } from '@start9labs/marketplace' import { ConfigService } from 'src/app/services/config.service' -import { ErrorToastService } from '@start9labs/shared' +import { ErrorService, LoadingService } from '@start9labs/shared' import cbor from 'cbor' interface Positions { @@ -28,10 +28,10 @@ export class SideloadPage { invalid = false constructor( - private readonly loadingCtrl: LoadingController, + private readonly loader: LoadingService, private readonly api: ApiService, private readonly navCtrl: NavController, - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, private readonly config: ConfigService, ) {} @@ -52,12 +52,7 @@ export class SideloadPage { async handleUpload() { if (!this.pkgData) return - const loader = await this.loadingCtrl.create({ - message: 'Uploading package', - cssClass: 'loader', - }) - await loader.present() - + const loader = this.loader.open('Uploading package').subscribe() const { pkg, file } = this.pkgData try { @@ -66,13 +61,13 @@ export class SideloadPage { icon: pkg.icon, size: file.size, }) - this.api.uploadPackage(guid, file).catch(e => console.error(e)) + this.api.uploadPackage(guid, file!).catch(e => console.error(e)) this.navCtrl.navigateRoot('/services') } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() this.clear() } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/ssh-keys/ssh-keys.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/ssh-keys/ssh-keys.module.ts index 6192f8f11..84114149a 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/ssh-keys/ssh-keys.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/ssh-keys/ssh-keys.module.ts @@ -3,8 +3,9 @@ import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { RouterModule, Routes } from '@angular/router' import { SharedPipesModule } from '@start9labs/shared' -import { GenericInputComponentModule } from 'src/app/apps/ui/modals/generic-input/generic-input.component.module' +import { PromptModule } from 'src/app/apps/ui/modals/prompt/prompt.module' import { SSHKeysPage } from './ssh-keys.page' +import { TuiNotificationModule } from '@taiga-ui/core' const routes: Routes = [ { @@ -18,7 +19,8 @@ const routes: Routes = [ CommonModule, IonicModule, SharedPipesModule, - GenericInputComponentModule, + PromptModule, + TuiNotificationModule, RouterModule.forChild(routes), ], declarations: [SSHKeysPage], diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/ssh-keys/ssh-keys.page.html b/frontend/projects/ui/src/app/apps/ui/pages/system/ssh-keys/ssh-keys.page.html index ce2c35b62..ff61301a0 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/ssh-keys/ssh-keys.page.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/ssh-keys/ssh-keys.page.html @@ -8,20 +8,15 @@ - - - - -

- Adding SSH keys to StartOS is useful for command line access, as well - as for debugging purposes. - - View instructions - -

-
-
+
+ + Adding domains to StartOS enables you to access your server and service + interfaces over clearnet. + View instructions + +
+ Saved Keys - Add New Key + Add Key diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/ssh-keys/ssh-keys.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/ssh-keys/ssh-keys.page.ts index 794e5dd47..68383b86b 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/ssh-keys/ssh-keys.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/ssh-keys/ssh-keys.page.ts @@ -1,17 +1,11 @@ import { Component } from '@angular/core' -import { - AlertController, - LoadingController, - ModalController, -} from '@ionic/angular' -import { ErrorToastService } from '@start9labs/shared' -import { BehaviorSubject } from 'rxjs' +import { ErrorService, LoadingService } from '@start9labs/shared' +import { TuiDialogService } from '@taiga-ui/core' +import { BehaviorSubject, filter, take } from 'rxjs' import { SSHKey } from 'src/app/services/api/api.types' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { - GenericInputComponent, - GenericInputOptions, -} from 'src/app/apps/ui/modals/generic-input/generic-input.component' +import { PROMPT } from 'src/app/apps/ui/modals/prompt/prompt.component' +import { TUI_PROMPT } from '@taiga-ui/kit' @Component({ selector: 'ssh-keys', @@ -24,10 +18,9 @@ export class SSHKeysPage { loading$ = new BehaviorSubject(true) constructor( - private readonly loadingCtrl: LoadingController, - private readonly modalCtrl: ModalController, - private readonly errToast: ErrorToastService, - private readonly alertCtrl: AlertController, + private readonly loader: LoadingService, + private readonly dialogs: TuiDialogService, + private readonly errorService: ErrorService, private readonly embassyApi: ApiService, ) {} @@ -39,77 +32,61 @@ export class SSHKeysPage { try { this.sshKeys = await this.embassyApi.getSshKeys({}) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { this.loading$.next(false) } } async presentModalAdd() { - const options: GenericInputOptions = { - title: 'SSH Key', - message: - 'Enter the SSH public key you would like to authorize for root access to your Embassy.', - label: '', - submitFn: (pk: string) => this.add(pk), - } - - const modal = await this.modalCtrl.create({ - component: GenericInputComponent, - componentProps: { options }, - cssClass: 'alertlike-modal', - }) - await modal.present() + this.dialogs + .open(PROMPT, { + label: 'SSH Key', + data: { + message: + 'Enter the SSH public key you would like to authorize for root access to your Embassy.', + }, + }) + .pipe(take(1)) + .subscribe(pk => this.add(pk)) } - async add(pubkey: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Saving...', - }) - await loader.present() + presentAlertDelete(key: SSHKey, i: number) { + this.dialogs + .open(TUI_PROMPT, { + label: 'Confirm', + size: 's', + data: { + content: 'Delete key? This action cannot be undone.', + yes: 'Delete', + no: 'Cancel', + }, + }) + .pipe(filter(Boolean)) + .subscribe(() => this.delete(key, i)) + } + + private async add(pubkey: string): Promise { + const loader = this.loader.open('Saving...').subscribe() try { const key = await this.embassyApi.addSshKey({ key: pubkey }) this.sshKeys.push(key) } finally { - loader.dismiss() + loader.unsubscribe() } } - async presentAlertDelete(key: SSHKey, i: number) { - const alert = await this.alertCtrl.create({ - header: 'Confirm', - message: 'Delete key? This action cannot be undone.', - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Delete', - handler: () => { - this.delete(key, i) - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() - } - - async delete(key: SSHKey, i: number): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Deleting...', - }) - await loader.present() + private async delete(key: SSHKey, i: number): Promise { + const loader = this.loader.open('Deleting...').subscribe() try { await this.embassyApi.deleteSshKey({ fingerprint: key.fingerprint }) this.sshKeys.splice(i, 1) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/system.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/system.module.ts index ff4093f02..77bb24b69 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/system.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/system.module.ts @@ -10,8 +10,18 @@ const routes: Routes = [ ), }, { - path: 'lan', - loadChildren: () => import('./lan/lan.module').then(m => m.LANPageModule), + path: 'addresses', + loadChildren: () => + import('./os-addresses/os-addresses.module').then( + m => m.OSAddressesPageModule, + ), + }, + { + path: 'port-forwards', + loadChildren: () => + import('./port-forwards/port-forwards.module').then( + m => m.PortForwardsPageModule, + ), }, { path: 'logs', @@ -56,6 +66,11 @@ const routes: Routes = [ m => m.ServerSpecsPageModule, ), }, + { + path: 'domains', + loadChildren: () => + import('./domains/domains.module').then(m => m.DomainsPageModule), + }, { path: 'ssh', loadChildren: () => diff --git a/frontend/projects/ui/src/app/apps/ui/pages/system/wifi/wifi.page.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/wifi/wifi.page.ts index 71f1e285d..842853177 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/wifi/wifi.page.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/wifi/wifi.page.ts @@ -1,17 +1,18 @@ -import { Component } from '@angular/core' -import { ToastController } from '@ionic/angular' -import { TuiDialogOptions } from '@taiga-ui/core' +import { Component, Pipe, PipeTransform } from '@angular/core' +import { + TuiAlertService, + TuiDialogOptions, + TuiNotification, +} from '@taiga-ui/core' import { ToggleCustomEvent } from '@ionic/core' import { ApiService } from 'src/app/services/api/embassy-api.service' import { AvailableWifi, RR } from 'src/app/services/api/api.types' -import { pauseFor, ErrorToastService } from '@start9labs/shared' +import { ErrorService, LoadingService, pauseFor } from '@start9labs/shared' import { FormDialogService } from 'src/app/services/form-dialog.service' import { FormContext, FormPage } from 'src/app/apps/ui/modals/form/form.page' -import { LoadingService } from 'src/app/common/loading/loading.service' import { PatchDB } from 'patch-db-client' import { DataModel } from 'src/app/services/patch-db/data-model' import { ConnectionService } from 'src/app/services/connection.service' -import { Pipe, PipeTransform } from '@angular/core' import { BehaviorSubject, catchError, @@ -38,12 +39,14 @@ interface WiFiForm { }) export class WifiPage { readonly connected$ = this.connectionService.connected$.pipe(filter(Boolean)) - readonly enabled$ = this.patch.watch$('server-info', 'wifi-enabled').pipe( - distinctUntilChanged(), - tap(enabled => { - if (enabled) this.trigger$.next('') - }), - ) + readonly enabled$ = this.patch + .watch$('server-info', 'network', 'wifi', 'enabled') + .pipe( + distinctUntilChanged(), + tap(enabled => { + if (enabled) this.trigger$.next('') + }), + ) readonly trigger$ = new BehaviorSubject('') readonly localChanges$ = new Subject() readonly wifi$ = merge( @@ -53,10 +56,10 @@ export class WifiPage { constructor( private readonly api: ApiService, - private readonly toastCtrl: ToastController, + private readonly alerts: TuiAlertService, private readonly loader: LoadingService, private readonly formDialog: FormDialogService, - private readonly errToast: ErrorToastService, + private readonly errorService: ErrorService, private readonly patch: PatchDB, private readonly connectionService: ConnectionService, ) {} @@ -70,7 +73,7 @@ export class WifiPage { try { await this.api.enableWifi({ enable }) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { loader.unsubscribe() } @@ -85,7 +88,7 @@ export class WifiPage { await this.api.connectWifi({ ssid }) await this.confirmWifi(ssid) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { loader.unsubscribe() } @@ -100,7 +103,7 @@ export class WifiPage { this.localChanges$.next(wifi) this.trigger$.next('') } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { loader.unsubscribe() } @@ -152,51 +155,27 @@ export class WifiPage { private getWifi$(): Observable { return from(this.api.getWifi({}, 10000)).pipe( catchError((e: any) => { - this.errToast.present(e) + this.errorService.handleError(e) return [] }), ) } - private async presentToastSuccess(): Promise { - const toast = await this.toastCtrl.create({ - header: 'Connection successful!', - position: 'bottom', - duration: 4000, - buttons: [ - { - side: 'start', - icon: 'close', - handler: () => { - return true - }, - }, - ], - cssClass: 'success-toast', - }) - - await toast.present() + private presentToastSuccess() { + this.alerts + .open('Connection successful!', { + status: TuiNotification.Success, + }) + .subscribe() } private async presentToastFail(): Promise { - const toast = await this.toastCtrl.create({ - header: 'Failed to connect:', - message: `Check credentials and try again`, - position: 'bottom', - duration: 4000, - buttons: [ - { - side: 'start', - icon: 'close', - handler: () => { - return true - }, - }, - ], - cssClass: 'warning-toast', - }) - - await toast.present() + this.alerts + .open('Check credentials and try again', { + label: 'Failed to connect', + status: TuiNotification.Warning, + }) + .subscribe() } private async save( @@ -218,7 +197,7 @@ export class WifiPage { this.trigger$.next('') return true } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) return false } finally { loader.unsubscribe() @@ -243,7 +222,7 @@ export class WifiPage { await this.confirmWifi(ssid) return true } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) return false } finally { loader.unsubscribe() diff --git a/frontend/projects/ui/src/app/apps/ui/pages/updates/updates.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/updates/updates.module.ts index 8fc07fd36..d557ae7bb 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/updates/updates.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/updates/updates.module.ts @@ -2,16 +2,20 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { RouterModule, Routes } from '@angular/router' -import { MimeTypePipeModule } from '@start9labs/marketplace' +import { + MimeTypePipeModule, + StoreIconComponentModule, +} from '@start9labs/marketplace' import { EmverPipesModule, MarkdownPipeModule, + SafeLinksModule, SharedPipesModule, } from '@start9labs/shared' +import { NgDompurifyModule } from '@tinkoff/ng-dompurify' import { RoundProgressModule } from 'angular-svg-round-progressbar' import { BadgeMenuComponentModule } from 'src/app/common/badge-menu-button/badge-menu.component.module' import { SkeletonListComponentModule } from 'src/app/common/skeleton-list/skeleton-list.component.module' -import { StoreIconComponentModule } from 'src/app/common/store-icon/store-icon.component.module' import { UpdatesPage } from './updates.page' import { InstallProgressPipe } from './install-progress.pipe' import { FilterUpdatesPipe } from './filter-updates.pipe' @@ -37,6 +41,8 @@ const routes: Routes = [ StoreIconComponentModule, EmverPipesModule, MimeTypePipeModule, + SafeLinksModule, + NgDompurifyModule, ], }) export class UpdatesPageModule {} diff --git a/frontend/projects/ui/src/app/apps/ui/pages/updates/updates.page.html b/frontend/projects/ui/src/app/apps/ui/pages/updates/updates.page.html index a4c4cc8f0..e0daac362 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/updates/updates.page.html +++ b/frontend/projects/ui/src/app/apps/ui/pages/updates/updates.page.html @@ -11,7 +11,11 @@
- +   {{ host.name }} @@ -87,7 +91,8 @@
What's new

, private readonly navCtrl: NavController, - private readonly loadingCtrl: LoadingController, - private readonly alertCtrl: AlertController, + private readonly loader: LoadingService, + private readonly dialogs: TuiDialogService, + readonly config: ConfigService, ) {} viewInMarketplace(event: Event, url: string, id: string) { @@ -82,11 +82,9 @@ export class UpdatesPage { } private async dryUpdate(manifest: Manifest, url: string) { - const loader = await this.loadingCtrl.create({ - message: 'Checking dependent services...', - }) - await loader.present() - + const loader = this.loader + .open('Checking dependent services...') + .subscribe() const { id, version } = manifest try { @@ -94,7 +92,7 @@ export class UpdatesPage { id, version: `${version}`, }) - await loader.dismiss() + loader.unsubscribe() if (isEmptyObject(breakages)) { this.update(id, version, url) @@ -112,6 +110,7 @@ export class UpdatesPage { } catch (e: any) { delete this.marketplaceService.updateQueue[id] this.marketplaceService.updateErrors[id] = e.message + loader.unsubscribe() } } @@ -119,38 +118,26 @@ export class UpdatesPage { title: string, breakages: Breakages, ): Promise { - let message: string = `As a result of updating ${title}, the following services will no longer work properly and may crash:
    ` + let content: string = `As a result of updating ${title}, the following services will no longer work properly and may crash:
      ` const localPkgs = await getAllPackages(this.patch) const bullets = Object.keys(breakages).map(id => { const title = localPkgs[id].manifest.title return `
    • ${title}
    • ` }) - message = `${message}${bullets.join('')}
    ` + content = `${content}${bullets.join('')}
` return new Promise(async resolve => { - const alert = await this.alertCtrl.create({ - header: 'Warning', - message, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - handler: () => { - resolve(false) - }, + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content, + yes: 'Continue', + no: 'Cancel', }, - { - text: 'Continue', - handler: () => { - resolve(true) - }, - cssClass: 'enter-click', - }, - ], - cssClass: 'alert-warning-message', - }) - - await alert.present() + }) + .subscribe(response => resolve(response)) }) } diff --git a/frontend/projects/ui/src/app/common/form/form-select/form-select.component.ts b/frontend/projects/ui/src/app/common/form/form-select/form-select.component.ts index b36b1c417..1c4525f86 100644 --- a/frontend/projects/ui/src/app/common/form/form-select/form-select.component.ts +++ b/frontend/projects/ui/src/app/common/form/form-select/form-select.component.ts @@ -21,7 +21,7 @@ export class FormSelectComponent extends Control { } get selected(): string | null { - return this.value && this.spec.values[this.value] + return (this.value && this.spec.values[this.value]) || null } set selected(value: string | null) { diff --git a/frontend/projects/ui/src/app/common/form/form-text/form-text.component.html b/frontend/projects/ui/src/app/common/form/form-text/form-text.component.html index 55acd437f..e466d16aa 100644 --- a/frontend/projects/ui/src/app/common/form/form-text/form-text.component.html +++ b/frontend/projects/ui/src/app/common/form/form-text/form-text.component.html @@ -1,5 +1,5 @@ diff --git a/frontend/projects/ui/src/app/common/os-welcome/os-welcome.page.scss b/frontend/projects/ui/src/app/common/os-welcome/os-welcome.page.scss index 0dc939f99..ae9b93b7b 100644 --- a/frontend/projects/ui/src/app/common/os-welcome/os-welcome.page.scss +++ b/frontend/projects/ui/src/app/common/os-welcome/os-welcome.page.scss @@ -26,4 +26,9 @@ h2 { h4 { font-style: italic; -} \ No newline at end of file +} + +.begin { + display: block; + margin: 0 auto; +} diff --git a/frontend/projects/ui/src/app/common/os-welcome/os-welcome.page.ts b/frontend/projects/ui/src/app/common/os-welcome/os-welcome.page.ts index f9a6ecd7b..678705446 100644 --- a/frontend/projects/ui/src/app/common/os-welcome/os-welcome.page.ts +++ b/frontend/projects/ui/src/app/common/os-welcome/os-welcome.page.ts @@ -1,5 +1,6 @@ -import { Component, Input } from '@angular/core' -import { ModalController } from '@ionic/angular' +import { Component, Inject } from '@angular/core' +import { TuiDialogContext } from '@taiga-ui/core' +import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' @Component({ selector: 'os-welcome', @@ -7,9 +8,7 @@ import { ModalController } from '@ionic/angular' styleUrls: ['./os-welcome.page.scss'], }) export class OSWelcomePage { - constructor(private readonly modalCtrl: ModalController) {} - - async dismiss() { - return this.modalCtrl.dismiss() - } + constructor( + @Inject(POLYMORPHEUS_CONTEXT) readonly context: TuiDialogContext, + ) {} } diff --git a/frontend/projects/ui/src/app/common/primary-ip/primary-ip.module.ts b/frontend/projects/ui/src/app/common/primary-ip/primary-ip.module.ts new file mode 100644 index 000000000..941518ab2 --- /dev/null +++ b/frontend/projects/ui/src/app/common/primary-ip/primary-ip.module.ts @@ -0,0 +1,8 @@ +import { NgModule } from '@angular/core' +import { PrimaryIpPipe } from './primary-ip.pipe' + +@NgModule({ + declarations: [PrimaryIpPipe], + exports: [PrimaryIpPipe], +}) +export class PrimaryIpPipeModule {} diff --git a/frontend/projects/ui/src/app/common/primary-ip/primary-ip.pipe.ts b/frontend/projects/ui/src/app/common/primary-ip/primary-ip.pipe.ts new file mode 100644 index 000000000..4cfa98552 --- /dev/null +++ b/frontend/projects/ui/src/app/common/primary-ip/primary-ip.pipe.ts @@ -0,0 +1,17 @@ +import { Pipe, PipeTransform } from '@angular/core' +import { IpInfo } from '../../services/patch-db/data-model' + +@Pipe({ + name: 'primaryIp', +}) +export class PrimaryIpPipe implements PipeTransform { + transform(ipInfo: IpInfo): string { + return getPrimaryIp(ipInfo) + } +} + +export function getPrimaryIp(ipInfo: IpInfo): string { + return Object.values(ipInfo) + .filter(iface => iface.ipv4) + .sort((a, b) => (a.wireless ? -1 : 1))[0].ipv4! +} diff --git a/frontend/projects/ui/src/app/common/store-icon/store-icon.component.ts b/frontend/projects/ui/src/app/common/store-icon/store-icon.component.ts deleted file mode 100644 index 076311697..000000000 --- a/frontend/projects/ui/src/app/common/store-icon/store-icon.component.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { - ChangeDetectionStrategy, - Component, - Input, - Pipe, - PipeTransform, -} from '@angular/core' -import { ConfigService } from 'src/app/services/config.service' -import { sameUrl } from '@start9labs/shared' - -@Component({ - selector: 'store-icon', - templateUrl: './store-icon.component.html', - styleUrls: ['./store-icon.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class StoreIconComponent { - @Input() - url: string = '' - @Input() - size?: string -} - -@Pipe({ - name: 'getIcon', -}) -export class GetIconPipe implements PipeTransform { - constructor(private readonly config: ConfigService) {} - - transform(url: string): string | null { - const { start9, community } = this.config.marketplace - - if (sameUrl(url, start9)) { - return 'assets/img/icon_transparent.png' - } else if (sameUrl(url, community)) { - return 'assets/img/community-store.png' - } - return null - } -} diff --git a/frontend/projects/ui/src/app/common/toast-container/notifications-toast/notifications-toast.component.html b/frontend/projects/ui/src/app/common/toast-container/notifications-toast/notifications-toast.component.html index 6e7f7c6fa..d75364715 100644 --- a/frontend/projects/ui/src/app/common/toast-container/notifications-toast/notifications-toast.component.html +++ b/frontend/projects/ui/src/app/common/toast-container/notifications-toast/notifications-toast.component.html @@ -1,17 +1,8 @@ - New notifications - - - View - - + View + diff --git a/frontend/projects/ui/src/app/common/toast-container/refresh-alert/refresh-alert.component.html b/frontend/projects/ui/src/app/common/toast-container/refresh-alert/refresh-alert.component.html index 20985c11e..db2480687 100644 --- a/frontend/projects/ui/src/app/common/toast-container/refresh-alert/refresh-alert.component.html +++ b/frontend/projects/ui/src/app/common/toast-container/refresh-alert/refresh-alert.component.html @@ -1,4 +1,8 @@ - + Your user interface is cached and out of date. Hard refresh the page to get the latest UI.
    @@ -11,5 +15,13 @@ : ctrl + shift + R
- Ok -
+ + diff --git a/frontend/projects/ui/src/app/common/toast-container/refresh-alert/refresh-alert.component.ts b/frontend/projects/ui/src/app/common/toast-container/refresh-alert/refresh-alert.component.ts index e9a2e75ae..3a2740af9 100644 --- a/frontend/projects/ui/src/app/common/toast-container/refresh-alert/refresh-alert.component.ts +++ b/frontend/projects/ui/src/app/common/toast-container/refresh-alert/refresh-alert.component.ts @@ -1,5 +1,5 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' -import { Observable, Subject, merge } from 'rxjs' +import { Observable, Subject, merge, debounceTime } from 'rxjs' import { RefreshAlertService } from './refresh-alert.service' @@ -11,7 +11,7 @@ import { RefreshAlertService } from './refresh-alert.service' export class RefreshAlertComponent { private readonly dismiss$ = new Subject() - readonly show$ = merge(this.dismiss$, this.refresh$) + readonly show$ = merge(this.dismiss$, this.refresh$).pipe(debounceTime(0)) constructor( @Inject(RefreshAlertService) private readonly refresh$: Observable, diff --git a/frontend/projects/ui/src/app/common/toast-container/toast-container.module.ts b/frontend/projects/ui/src/app/common/toast-container/toast-container.module.ts index e23fcc454..86294542a 100644 --- a/frontend/projects/ui/src/app/common/toast-container/toast-container.module.ts +++ b/frontend/projects/ui/src/app/common/toast-container/toast-container.module.ts @@ -1,15 +1,24 @@ import { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' import { RouterModule } from '@angular/router' -import { AlertModule, ToastModule } from '@start9labs/shared' +import { TuiAlertModule } from '@start9labs/shared' import { ToastContainerComponent } from './toast-container.component' import { NotificationsToastComponent } from './notifications-toast/notifications-toast.component' import { RefreshAlertComponent } from './refresh-alert/refresh-alert.component' import { UpdateToastComponent } from './update-toast/update-toast.component' +import { TuiButtonModule, TuiDialogModule } from '@taiga-ui/core' +import { TuiAutoFocusModule } from '@taiga-ui/cdk' @NgModule({ - imports: [CommonModule, ToastModule, AlertModule, RouterModule], + imports: [ + CommonModule, + RouterModule, + TuiDialogModule, + TuiButtonModule, + TuiAutoFocusModule, + TuiAlertModule, + ], declarations: [ ToastContainerComponent, NotificationsToastComponent, diff --git a/frontend/projects/ui/src/app/common/toast-container/update-toast/update-toast.component.html b/frontend/projects/ui/src/app/common/toast-container/update-toast/update-toast.component.html index b7a4af51b..350b0fff9 100644 --- a/frontend/projects/ui/src/app/common/toast-container/update-toast/update-toast.component.html +++ b/frontend/projects/ui/src/app/common/toast-container/update-toast/update-toast.component.html @@ -1,11 +1,21 @@ - Restart your server for these updates to take effect. It can take several minutes to come back online. - - - + + diff --git a/frontend/projects/ui/src/app/common/toast-container/update-toast/update-toast.component.ts b/frontend/projects/ui/src/app/common/toast-container/update-toast/update-toast.component.ts index 676aa0dc6..0b02faa4e 100644 --- a/frontend/projects/ui/src/app/common/toast-container/update-toast/update-toast.component.ts +++ b/frontend/projects/ui/src/app/common/toast-container/update-toast/update-toast.component.ts @@ -1,6 +1,5 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' -import { LoadingController } from '@ionic/angular' -import { ErrorToastService } from '@start9labs/shared' +import { ErrorService, LoadingService } from '@start9labs/shared' import { Observable, Subject, merge } from 'rxjs' import { UpdateToastService } from './update-toast.service' @@ -19,8 +18,8 @@ export class UpdateToastComponent { constructor( @Inject(UpdateToastService) private readonly update$: Observable, private readonly embassyApi: ApiService, - private readonly errToast: ErrorToastService, - private readonly loadingCtrl: LoadingController, + private readonly errorService: ErrorService, + private readonly loader: LoadingService, ) {} onDismiss() { @@ -30,18 +29,14 @@ export class UpdateToastComponent { async restart(): Promise { this.onDismiss() - const loader = await this.loadingCtrl.create({ - message: 'Restarting...', - }) - - await loader.present() + const loader = this.loader.open('Restarting...').subscribe() try { await this.embassyApi.restartServer({}) } catch (e: any) { - await this.errToast.present(e) + await this.errorService.handleError(e) } finally { - await loader.dismiss() + await loader.unsubscribe() } } } diff --git a/frontend/projects/ui/src/app/routing.module.ts b/frontend/projects/ui/src/app/routing.module.ts index d835713bc..f8b67c9f9 100644 --- a/frontend/projects/ui/src/app/routing.module.ts +++ b/frontend/projects/ui/src/app/routing.module.ts @@ -4,6 +4,13 @@ import { AuthGuard } from './guards/auth.guard' import { UnauthGuard } from './guards/unauth.guard' const routes: Routes = [ + { + path: 'diagnostic', + loadChildren: () => + import('./apps/diagnostic/diagnostic.module').then( + m => m.DiagnosticModule, + ), + }, { path: 'loading', loadChildren: () => diff --git a/frontend/projects/ui/src/app/services/api/api.fixures.ts b/frontend/projects/ui/src/app/services/api/api.fixures.ts index 800eb6914..063e91d5a 100644 --- a/frontend/projects/ui/src/app/services/api/api.fixures.ts +++ b/frontend/projects/ui/src/app/services/api/api.fixures.ts @@ -1354,7 +1354,7 @@ export module Mock { 'dependency-info': { bitcoind: { title: 'Bitcoin Core', - icon: 'assets/img/service-icons/bitcoind.svg', + icon: 'assets/img/service-icons/bitcoind.png', }, }, 'marketplace-url': 'https://registry.start9.com/', @@ -1416,7 +1416,7 @@ export module Mock { 'dependency-info': { bitcoind: { title: 'Bitcoin Core', - icon: 'assets/img/service-icons/bitcoind.svg', + icon: 'assets/img/service-icons/bitcoind.png', }, 'btc-rpc-proxy': { title: 'Bitcoin Proxy', diff --git a/frontend/projects/ui/src/app/services/api/api.types.ts b/frontend/projects/ui/src/app/services/api/api.types.ts index 8ec0ecf59..a0a0454ba 100644 --- a/frontend/projects/ui/src/app/services/api/api.types.ts +++ b/frontend/projects/ui/src/app/services/api/api.types.ts @@ -4,9 +4,11 @@ import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes' import { DataModel, DependencyError, + DomainInfo, } from 'src/app/services/patch-db/data-model' import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared' import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants' +import { DomainSpec } from 'src/app/apps/ui/pages/system/domains/domain.const' export module RR { // DB @@ -55,6 +57,9 @@ export module RR { export type UpdateServerReq = { 'marketplace-url': string } // server.update export type UpdateServerRes = 'updating' | 'no-updates' + export type SetServerClearnetAddressReq = { domainInfo: DomainInfo | null } // server.set-clearnet + export type SetServerClearnetAddressRes = null + export type RestartServerReq = {} // server.restart export type RestartServerRes = null @@ -105,6 +110,25 @@ export module RR { export type DeleteAllNotificationsReq = { before: number } // notification.delete-before export type DeleteAllNotificationsRes = null + // domains + + export type ClaimStart9MeReq = {} // net.domain.me.claim + export type ClaimStart9MeRes = null + + export type DeleteStart9MeReq = {} // net.domain.me.delete + export type DeleteStart9MeRes = null + + export type AddDomainReq = DomainSpec // net.domain.add + export type AddDomainRes = null + + export type DeleteDomainReq = { hostname: string } // net.domain.delete + export type DeleteDomainRes = null + + // port forwards + + export type OverridePortReq = { target: number; port: number } // net.port-forwards.override + export type OverridePortRes = null + // wifi export type GetWifiReq = {} diff --git a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts index 168a7692b..683c3bbe9 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-api.service.ts @@ -99,6 +99,10 @@ export abstract class ApiService { abstract updateServer(url?: string): Promise + abstract setServerClearnetAddress( + params: RR.SetServerClearnetAddressReq, + ): Promise + abstract restartServer( params: RR.RestartServerReq, ): Promise @@ -145,6 +149,26 @@ export abstract class ApiService { params: RR.DeleteAllNotificationsReq, ): Promise + // domains + + abstract claimStart9MeDomain( + params: RR.ClaimStart9MeReq, + ): Promise + + abstract deleteStart9MeDomain( + params: RR.DeleteStart9MeReq, + ): Promise + + abstract addDomain(params: RR.AddDomainReq): Promise + + abstract deleteDomain(params: RR.DeleteDomainReq): Promise + + // port forwards + + abstract overridePortForward( + params: RR.OverridePortReq, + ): Promise + // wifi abstract enableWifi(params: RR.EnableWifiReq): Promise @@ -158,7 +182,7 @@ export abstract class ApiService { abstract connectWifi(params: RR.ConnectWifiReq): Promise - abstract deleteWifi(params: RR.DeleteWifiReq): Promise + abstract deleteWifi(params: RR.DeleteWifiReq): Promise // email diff --git a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts index 3fb8f5a47..f2cb5b57e 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-live-api.service.ts @@ -191,6 +191,12 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'server.update', params }) } + async setServerClearnetAddress( + params: RR.SetServerClearnetAddressReq, + ): Promise { + return this.rpcRequest({ method: 'server.set-clearnet', params }) + } + async restartServer( params: RR.RestartServerReq, ): Promise { @@ -276,6 +282,36 @@ export class LiveApiService extends ApiService { }) } + // domains + + async claimStart9MeDomain( + params: RR.ClaimStart9MeReq, + ): Promise { + return this.rpcRequest({ method: 'net.domain.me.claim', params }) + } + + async deleteStart9MeDomain( + params: RR.DeleteStart9MeReq, + ): Promise { + return this.rpcRequest({ method: 'net.domain.me.delete', params }) + } + + async addDomain(params: RR.AddDomainReq): Promise { + return this.rpcRequest({ method: 'net.domain.add', params }) + } + + async deleteDomain(params: RR.DeleteDomainReq): Promise { + return this.rpcRequest({ method: 'net.domain.delete', params }) + } + + // port forwards + + async overridePortForward( + params: RR.OverridePortReq, + ): Promise { + return this.rpcRequest({ method: 'net.port-forwards.override', params }) + } + // wifi async enableWifi(params: RR.EnableWifiReq): Promise { diff --git a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts index a94564b74..d12f98389 100644 --- a/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts +++ b/frontend/projects/ui/src/app/services/api/embassy-mock-api.service.ts @@ -307,6 +307,20 @@ export class MockApiService extends ApiService { return this.withRevision(patch, 'updating') } + async setServerClearnetAddress( + params: RR.SetServerClearnetAddressReq, + ): Promise { + await pauseFor(2000) + const patch = [ + { + op: PatchOp.REPLACE, + path: '/server-info/ui/domainInfo', + value: params.domainInfo, + }, + ] + return this.withRevision(patch, null) + } + async restartServer( params: RR.RestartServerReq, ): Promise { @@ -424,6 +438,88 @@ export class MockApiService extends ApiService { return null } + // domains + + async claimStart9MeDomain( + params: RR.ClaimStart9MeReq, + ): Promise { + await pauseFor(2000) + + const patch = [ + { + op: PatchOp.REPLACE, + path: '/server-info/network/start9MeSubdomain', + value: { + value: 'xyz', + createdAt: new Date(), + }, + }, + ] + return this.withRevision(patch, null) + } + + async deleteStart9MeDomain( + params: RR.DeleteStart9MeReq, + ): Promise { + await pauseFor(2000) + const patch = [ + { + op: PatchOp.REPLACE, + path: '/server-info/network/start9MeSubdomain', + value: null, + }, + ] + return this.withRevision(patch, null) + } + + async addDomain(params: RR.AddDomainReq): Promise { + await pauseFor(2000) + + const patch = [ + { + op: PatchOp.REPLACE, + path: '/server-info/network/domains', + value: [ + { + value: params.domain, + provider: params.provider, + createdAt: new Date(), + }, + ], + }, + ] + return this.withRevision(patch, null) + } + + async deleteDomain(params: RR.DeleteDomainReq): Promise { + await pauseFor(2000) + const patch = [ + { + op: PatchOp.REPLACE, + path: '/server-info/network/domains', + value: [], + }, + ] + return this.withRevision(patch, null) + } + + // port forwards + + async overridePortForward( + params: RR.OverridePortReq, + ): Promise { + await pauseFor(2000) + + const patch = [ + { + op: PatchOp.REPLACE, + path: '/server-info/network/wanConfig/forwards/0/override', + value: params.port, + }, + ] + return this.withRevision(patch, null) + } + // wifi async enableWifi(params: RR.EnableWifiReq): Promise { @@ -431,7 +527,7 @@ export class MockApiService extends ApiService { const patch = [ { op: PatchOp.REPLACE, - path: '/server-info/wifi-enabled', + path: '/server-info/network/wifi/enabled', value: params.enable, }, ] @@ -472,7 +568,7 @@ export class MockApiService extends ApiService { const patch = [ { op: PatchOp.REPLACE, - path: '/server-info/email', + path: '/server-info/smtp', value: params, }, ] diff --git a/frontend/projects/ui/src/app/services/api/mock-patch.ts b/frontend/projects/ui/src/app/services/api/mock-patch.ts index 4f678ecb5..c6c387d9e 100644 --- a/frontend/projects/ui/src/app/services/api/mock-patch.ts +++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts @@ -37,21 +37,55 @@ export const mockPatchData: DataModel = { id: 'abcdefgh', version: '0.3.4', country: 'us', - 'last-backup': new Date(new Date().valueOf() - 604800001).toISOString(), - 'lan-address': 'https://adjective-noun.local', - 'tor-address': 'http://myveryownspecialtoraddress.onion', - 'ip-info': { - eth0: { - ipv4: '10.0.0.1', - ipv6: null, + ui: { + lanHostname: 'adjective-noun.local', + torHostname: 'myveryownspecialtoraddress.onion', + ipInfo: { + eth0: { + wireless: false, + ipv4: '10.0.0.1', + ipv6: null, + }, + wlan0: { + wireless: true, + ipv4: '10.0.90.12', + ipv6: 'FE80:CD00:0000:0CDE:1257:0000:211E:729CD', + }, }, - wlan0: { - ipv4: '10.0.90.12', - ipv6: 'FE80:CD00:0000:0CDE:1257:0000:211E:729CD', + domainInfo: null, + }, + network: { + domains: [], + start9MeSubdomain: null, + wifi: { + enabled: false, + lastRegion: null, + }, + wanConfig: { + upnp: false, + forwards: [ + { + assigned: 443, + override: null, + target: 443, + error: null, + }, + { + assigned: 80, + override: null, + target: 80, + error: null, + }, + { + assigned: 8332, + override: null, + target: 8332, + error: null, + }, + ], }, }, - 'last-wifi-region': null, - 'wifi-enabled': false, + 'last-backup': new Date(new Date().valueOf() - 604800001).toISOString(), 'unread-notification-count': 4, 'eos-version-compat': '>=0.3.0 <=0.3.0.1', 'status-info': { @@ -60,7 +94,6 @@ export const mockPatchData: DataModel = { 'update-progress': null, 'shutting-down': false, }, - hostname: 'random-words', pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m', 'ca-fingerprint': 'SHA-256: 63 2B 11 99 44 40 17 DF 37 FC C3 DF 0F 3D 15', 'system-start-time': new Date(new Date().valueOf() - 360042).toUTCString(), diff --git a/frontend/projects/ui/src/app/services/form-dialog.service.ts b/frontend/projects/ui/src/app/services/form-dialog.service.ts index b44218f75..69df946bb 100644 --- a/frontend/projects/ui/src/app/services/form-dialog.service.ts +++ b/frontend/projects/ui/src/app/services/form-dialog.service.ts @@ -3,7 +3,7 @@ import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core' import { TuiDialogFormService, TuiPromptData } from '@taiga-ui/kit' import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' -export const PROMPT: Partial> = { +const PROMPT: Partial> = { label: 'Unsaved Changes', data: { content: 'You have unsaved changes. Are you sure you want to leave?', diff --git a/frontend/projects/ui/src/app/services/patch-data.service.ts b/frontend/projects/ui/src/app/services/patch-data.service.ts index 9efe05699..4eb2e166d 100644 --- a/frontend/projects/ui/src/app/services/patch-data.service.ts +++ b/frontend/projects/ui/src/app/services/patch-data.service.ts @@ -1,5 +1,6 @@ import { Inject, Injectable } from '@angular/core' -import { ModalController } from '@ionic/angular' +import { AbstractMarketplaceService } from '@start9labs/marketplace' +import { TuiDialogService } from '@taiga-ui/core' import { filter, share, switchMap, take, tap, Observable } from 'rxjs' import { PatchDB } from 'patch-db-client' import { DataModel } from 'src/app/services/patch-db/data-model' @@ -8,8 +9,8 @@ import { OSWelcomePage } from '../common/os-welcome/os-welcome.page' import { ConfigService } from 'src/app/services/config.service' import { ApiService } from 'src/app/services/api/embassy-api.service' import { MarketplaceService } from 'src/app/services/marketplace.service' -import { AbstractMarketplaceService } from '@start9labs/marketplace' import { ConnectionService } from 'src/app/services/connection.service' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' // Get data from PatchDb after is starts and act upon it @Injectable({ @@ -33,7 +34,7 @@ export class PatchDataService extends Observable { private readonly patch: PatchDB, private readonly eosService: EOSService, private readonly config: ConfigService, - private readonly modalCtrl: ModalController, + private readonly dialogs: TuiDialogService, private readonly embassyApi: ApiService, @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, @@ -47,22 +48,21 @@ export class PatchDataService extends Observable { this.marketplaceService.getMarketplace$().pipe(take(1)).subscribe() } - private async showEosWelcome(ackVersion: string): Promise { + private showEosWelcome(ackVersion: string) { if (this.config.skipStartupAlerts || ackVersion === this.config.version) { return } - const modal = await this.modalCtrl.create({ - component: OSWelcomePage, - presentingElement: await this.modalCtrl.getTop(), - backdropDismiss: false, - }) - modal.onWillDismiss().then(() => { - this.embassyApi - .setDbValue(['ack-welcome'], this.config.version) - .catch() - }) - - await modal.present() + this.dialogs + .open(new PolymorpheusComponent(OSWelcomePage), { + label: 'Release Notes', + }) + .subscribe({ + complete: () => { + this.embassyApi + .setDbValue(['ack-welcome'], this.config.version) + .catch() + }, + }) } } diff --git a/frontend/projects/ui/src/app/services/patch-db/data-model.ts b/frontend/projects/ui/src/app/services/patch-db/data-model.ts index 01629667b..6221a773d 100644 --- a/frontend/projects/ui/src/app/services/patch-db/data-model.ts +++ b/frontend/projects/ui/src/app/services/patch-db/data-model.ts @@ -3,6 +3,7 @@ import { Url } from '@start9labs/shared' import { Manifest } from '@start9labs/marketplace' import { BackupJob } from '../api/api.types' import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants' +import { DomainSpec } from 'src/app/apps/ui/pages/system/domains/domain.const' export interface DataModel { 'server-info': ServerInfo @@ -54,16 +55,12 @@ export interface ServerInfo { id: string version: string country: string + ui: StartOsUiInfo + network: NetworkInfo 'last-backup': string | null - 'lan-address': Url - 'tor-address': Url - 'ip-info': IpInfo - 'last-wifi-region': string | null - 'wifi-enabled': boolean 'unread-notification-count': number 'status-info': ServerStatusInfo 'eos-version-compat': string - hostname: string pubkey: string 'ca-fingerprint': string 'system-start-time': string @@ -71,8 +68,49 @@ export interface ServerInfo { smtp: typeof customSmtp.validator._TYPE } +export type StartOsUiInfo = { + ipInfo: IpInfo + lanHostname: string + torHostname: string + domainInfo: DomainInfo | null +} + +export type NetworkInfo = { + wifi: WiFiInfo + start9MeSubdomain: Omit | null + domains: Domain[] + wanConfig: { + upnp: boolean + forwards: PortForward[] + } +} + +export type DomainInfo = { + domain: string + subdomain: string | null +} + +export type PortForward = { + assigned: number + override: number | null + target: number + error: string | null +} + +export type WiFiInfo = { + enabled: boolean + lastRegion: string | null +} + +export type Domain = { + value: string + provider: DomainSpec['provider'] + createdAt: string +} + export interface IpInfo { [iface: string]: { + wireless: boolean ipv4: string | null ipv6: string | null } diff --git a/frontend/projects/ui/src/app/util/clearnetAddress.ts b/frontend/projects/ui/src/app/util/clearnetAddress.ts new file mode 100644 index 000000000..94d483ece --- /dev/null +++ b/frontend/projects/ui/src/app/util/clearnetAddress.ts @@ -0,0 +1,11 @@ +import { DomainInfo } from '../services/patch-db/data-model' + +export function getClearnetAddress( + protocol: string, + domainInfo: DomainInfo | null, + path = '', +) { + if (!domainInfo) return '' + const subdomain = domainInfo.subdomain ? `${domainInfo.subdomain}.` : '' + return `${protocol}://${subdomain}${domainInfo.domain}${path}` +} diff --git a/frontend/projects/ui/src/styles.scss b/frontend/projects/ui/src/styles.scss index 7159d6806..c98d52162 100644 --- a/frontend/projects/ui/src/styles.scss +++ b/frontend/projects/ui/src/styles.scss @@ -52,13 +52,6 @@ src: url('/assets/fonts/Open_Sans/OpenSans-Light.ttf'); } -@font-face { - font-family: 'Redacted'; - font-style: normal; - font-weight: normal; - src: url('/assets/fonts/Redacted/redacted.regular.ttf'); -} - @font-face { font-family: 'Courier New'; font-style: normal; @@ -278,7 +271,6 @@ ion-loading { .rec-item { margin: 20px; - border-style: solid; border-width: 1px; border-style: groove; border-color: dimgrey; @@ -368,29 +360,6 @@ ul { list-style-type: disc; } -// Taiga UI overrides - -tui-dialog { - transform: translate3d(0, 0, 0); -} - -tui-opt-group[data-label^='⚠️']:before { - color: var(--tui-warning-fill); -} - -tui-hint[data-appearance='onDark'] { - background: white !important; - color: #222 !important; -} - -[tuiLink] { - color: var(--tui-link) !important; - - &:hover { - color: var(--tui-link-hover) !important; - } -} - .checkbox { cursor: pointer; margin: 0 12px 6px 0; diff --git a/frontend/tsconfig.lib.json b/frontend/tsconfig.lib.json index 0dd228bf9..2d527a362 100644 --- a/frontend/tsconfig.lib.json +++ b/frontend/tsconfig.lib.json @@ -7,7 +7,6 @@ "declaration": true, "declarationMap": false, "inlineSources": true, - "types": [], "paths": { "@start9labs/marketplace": ["dist/marketplace"], "@start9labs/shared": ["dist/shared"]