From 75d2cde05c660589acb60deaa529eccc6dd48a46 Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Tue, 7 Mar 2023 14:37:14 -0700 Subject: [PATCH] Feat/domains update FE types and unify sideload page with marketplace show begin popover for UI launch select update node version for github workflows fix type errors eager load more components fix mocks for types recalculate updates bad on pkg uninstall chore: break form-object file structure files for config finish file upload API and implement for config chore: break down form-object by type, part 1 remove NEW from config comment entire setTimeout for new generic form options chore: break down form-object by type, part 2 headers for enums and unions implement select and multiselect for config update union types and camel case for specs implement textarea config value inputspec and required instead of nullable remove subtype from list spec update start-sdk bump start-sdk feat: use Taiga UI for config modal (#2250) * feat: use Taiga UI for config modal * chore: finish remaining changes * chore: address comments * bump sdk version --------- Co-authored-by: Matt Hill update package lock update to sdk 20 and fix types chore: update Taiga UI and migrate some more forms (#2252) update form to latest sdk validate length for textarea too chore: accommodate new changes to the specs (#2254) * chore: accommodate new changes to the specs * chore: fix error * chore: fix error feat: add input color (#2257) * feat: add input color * patterns will always be there --------- Co-authored-by: Matt Hill chore: properly type pattern error update to latest sdk Add sans-serif font fallback (#2263) * Add sans-serif font fallback * Update frontend readme start scripts feat: add datetime spec support (#2264) Wifi optional (#2249) * begin work * allow enable and disable wifi * nice styling * done except for popover not dismissing * update wifi.ts * address comments Feat/automated backups (#2142) * initial restructuring * very cool * new structure in place * delete unnecessary T * down the rabbit hole * getting better * dont like it * nice * very nice * sessions select all * nice * backup runs * fix targets and more * small improvements * mostly working * address PR comments * fix error * delete issue with merge * fix checkboxes and add API for deleting backup runs * better styling for checkboxes * small button in ssh kpage too * complete multiple UI launcher * fix actions * present error toast too * fix target forms Add logs window to setup wizard loading screen (#2076) * add logs window to setup wizard loading screen * fix type error * Update frontend/projects/setup-wizard/src/app/services/api/live-api.service.ts Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> --------- Co-authored-by: Lucy C <12953208+elvece@users.noreply.github.com> statically type server metrics and use websocket (#2124) Co-authored-by: Matt Hill Feat/external-smtp (#1791) * UI for EOS smtp, missing API layer * implement api * fix errors * switch to external smtp creds * fix things up * fix types * update types for new forms * feat: add new form to emails and marketplace (#2268) * import tuilet module * feat: get rid of old form completely (#2270) * move to builder spec and delete developer menu * update sdk * tiny * getting better * working * done * feat: add step to number config * chore: small fixes * update SDK and step for numbers --------- Co-authored-by: Alex Inkin latest sdk, fix build update SDK for better disabled props feat: implement `disabled`, `immutable` and `generate` (#2280) * feat: implement `disabled`, `immutable` and `generate` * chore: remove unnecessary code * chore: add generate to textarea and implement immutable * no generate for textarea --------- Co-authored-by: Matt Hill update lockfile refactor: extract loading status to shared library (#2282) * refactor: extract loading status to shared library * chore: remove inline style refactor: break routing down to apps level (#2285) closes #2212 and closes #2214 Feat/credentials (#2290) add credentials and remove properties refactor: break ui up further down (#2292) * refactor: break ui up further down * permit loading even when authed --------- Co-authored-by: Matt Hill update patchdb for package compatability fixes fix file structure WIP finish rebase mvp complete port forwards mvp looking good cleaner system page move experimental features manual port overrides better info headers for jobs pages refactor: move diagnostic-ui app under ui route (#2306) * refactor: move diagnostic-ui app under ui route * chore: hide navigation * chore: remove ionic from diagnostic * fix navbar showing on login --------- Co-authored-by: Matt Hill chore: partially remove ionic modals and loaders (#2308) * chore: partially remove ionic modals and loaders * change to snake --------- Co-authored-by: Matt Hill better session data fetching abstract store icon component to shared marketplace project (#2311) * abstract store icon component to shared marketplace project * better than using a pipe * minor cleanup * chore: fix missing node types in libraries * typo --------- Co-authored-by: Matt Hill Co-authored-by: waterplea refactor: continue to get rid of ionic infrastructure (#2325) refactor: finish removing ionic entities: (#2333) * refactor: finish removing ionic entities: ToastController ErrorToastService ModalController AlertController LoadingController * chore: rollback testing code * chore: fix comments * minor form change * chore: fix comments * update clearnet address parts * move around patchDB * chore: fix comments --------- Co-authored-by: Matt Hill --- CONTRIBUTING.md | 3 - Makefile | 7 +- frontend/README.md | 1 - frontend/angular.json | 145 +----- frontend/ionic.config.json | 6 - frontend/lint-staged.config.js | 1 - frontend/package-lock.json | 121 +++-- frontend/package.json | 14 +- frontend/patchdb-ui-seed.json | 2 +- .../src/app/app-routing.module.ts | 25 - .../diagnostic-ui/src/app/app.component.html | 5 - .../diagnostic-ui/src/app/app.component.scss | 8 - .../diagnostic-ui/src/app/app.component.ts | 10 - .../diagnostic-ui/src/app/app.module.ts | 43 -- .../src/app/pages/home/home-routing.module.ts | 16 - .../src/app/pages/home/home.module.ts | 18 - .../src/app/pages/home/home.page.html | 81 --- .../src/app/pages/home/home.page.scss | 5 - .../src/environments/environment.prod.ts | 3 - .../src/environments/environment.ts | 16 - .../projects/diagnostic-ui/src/index.html | 23 - frontend/projects/diagnostic-ui/src/main.ts | 12 - .../projects/diagnostic-ui/src/polyfills.ts | 65 --- .../projects/diagnostic-ui/src/styles.scss | 41 -- .../projects/diagnostic-ui/src/zone-flags.ts | 6 - frontend/projects/diagnostic-ui/tsconfig.json | 9 - .../install-wizard/src/app/app.component.html | 3 +- .../install-wizard/src/app/app.module.ts | 17 +- .../src/app/pages/home/home.page.ts | 98 ++-- frontend/projects/marketplace/package.json | 3 +- .../store-icon/store-icon.component.html | 4 +- .../store-icon/store-icon.component.module.ts | 4 +- .../store-icon/store-icon.component.scss} | 0 .../store-icon/store-icon.component.ts | 28 ++ .../release-notes.component.html | 5 +- .../release-notes/release-notes.module.ts | 4 + .../src/pages/show/about/about.component.html | 5 +- .../src/pages/show/about/about.module.ts | 9 +- .../show/additional/additional.component.html | 33 +- .../show/additional/additional.component.scss | 10 + .../show/additional/additional.component.ts | 101 ++-- .../show/additional/additional.module.ts | 17 +- .../projects/marketplace/src/public-api.ts | 2 + frontend/projects/marketplace/src/types.ts | 2 +- frontend/projects/marketplace/tsconfig.json | 3 +- .../setup-wizard/src/app/app.component.html | 3 +- .../setup-wizard/src/app/app.component.ts | 6 +- .../setup-wizard/src/app/app.module.ts | 16 +- .../modals/cifs-modal/cifs-modal.module.ts | 26 +- .../modals/cifs-modal/cifs-modal.page.html | 123 ++--- .../modals/cifs-modal/cifs-modal.page.scss | 17 +- .../app/modals/cifs-modal/cifs-modal.page.ts | 131 +++-- .../app/modals/password/password.module.ts | 20 +- .../app/modals/password/password.page.html | 118 ++--- .../app/modals/password/password.page.scss | 21 - .../src/app/modals/password/password.page.ts | 100 ++-- .../src/app/pages/attach/attach.page.ts | 51 +- .../src/app/pages/embassy/embassy.page.ts | 128 +++-- .../src/app/pages/home/home.page.ts | 6 +- .../src/app/pages/loading/loading.module.ts | 4 +- .../src/app/pages/loading/loading.page.html | 4 +- .../src/app/pages/recover/recover.page.ts | 73 ++- .../src/app/pages/success/success.page.ts | 9 +- .../src/app/pages/transfer/transfer.page.ts | 54 +- .../fonts/Redacted/redacted.regular.ttf | Bin 17036 -> 0 bytes frontend/projects/shared/package.json | 2 + .../alert/alert-button.directive.ts | 29 -- .../components/alert/alert-input.directive.ts | 27 - .../src/components/alert/alert.component.ts | 99 ---- .../src/components/alert/alert.module.ts | 10 - .../initializing/initializing.component.html | 34 ++ .../initializing/initializing.component.scss | 18 + .../initializing/initializing.component.ts | 35 ++ .../initializing/initializing.module.ts | 14 + .../logs-window/logs-window.component.html | 11 + .../logs-window/logs-window.component.scss | 10 + .../logs-window/logs-window.component.ts | 68 +++ .../components/loading/loading.component.scss | 30 +- .../components/loading/loading.component.ts | 42 +- .../src/components/loading/loading.module.ts | 13 +- .../src/components/loading/loading.service.ts | 10 + .../markdown/markdown.component.html | 41 +- .../markdown/markdown.component.module.ts | 2 + .../components/markdown/markdown.component.ts | 26 +- .../toast/toast-button.directive.ts | 32 -- .../src/components/toast/toast.component.ts | 85 ---- .../src/components/toast/toast.module.ts | 9 - .../src/directives/alert/alert.directive.ts | 22 + .../src/directives/alert/alert.module.ts | 8 + .../src/pipes/markdown/markdown.pipe.ts | 19 +- frontend/projects/shared/src/public-api.ts | 18 +- .../shared/src/services/copy.service.ts | 16 + .../src/services/error-toast.service.ts | 70 --- .../shared/src/services/error.service.ts | 2 +- .../shared/src/services/setup.service.ts | 8 +- .../shared/src/types/workspace-config.ts | 12 +- frontend/projects/shared/styles/shared.scss | 7 + frontend/projects/shared/styles/taiga.scss | 49 ++ .../projects/ui/src/app/app.component.html | 8 +- frontend/projects/ui/src/app/app.component.ts | 12 +- frontend/projects/ui/src/app/app.module.ts | 1 + .../ui/src/app/app/menu/menu.component.html | 4 +- .../app/preloader/preloader.component.html | 2 - .../app/app/preloader/preloader.component.ts | 2 + .../ui/src/app/app/snek/snake.page.html | 36 +- .../ui/src/app/app/snek/snake.page.scss | 10 +- .../ui/src/app/app/snek/snake.page.ts | 44 +- .../ui/src/app/app/snek/snek.directive.ts | 65 ++- .../ui/src/app/app/snek/snek.module.ts | 3 +- .../app/apps/diagnostic/diagnostic.module.ts | 32 ++ .../app/apps/diagnostic/home/home.module.ts | 18 + .../app/apps/diagnostic/home/home.page.html | 53 ++ .../app/apps/diagnostic/home/home.page.scss | 35 ++ .../app/apps/diagnostic}/home/home.page.ts | 130 +++-- .../app/apps/diagnostic}/logs/logs.module.ts | 10 +- .../app/apps/diagnostic}/logs/logs.page.html | 0 .../app/apps/diagnostic}/logs/logs.page.ts | 15 +- .../services/diagnostic.service.ts} | 2 +- .../services/live-diagnostic.service.ts} | 4 +- .../services/mock-diagnostic.service.ts} | 4 +- .../ui/src/app/apps/loading/loading.module.ts | 4 +- .../ui/src/app/apps/loading/loading.page.html | 4 +- .../ui/src/app/apps/login/login.page.ts | 30 +- .../backup-report.component.html | 32 ++ ...ort.page.ts => backup-report.component.ts} | 32 +- .../backup-report/backup-report.module.ts | 6 +- .../backup-report/backup-report.page.html | 44 -- .../app/apps/ui/modals/form/form.module.ts | 2 + .../app/apps/ui/modals/form/form.page.html | 30 +- .../src/app/apps/ui/modals/form/form.page.ts | 11 +- .../generic-input.component.html | 67 --- .../generic-input.component.module.ts | 20 - .../generic-input/generic-input.component.ts | 90 ---- .../ui/modals/prompt/prompt.component.html | 40 ++ .../ui/modals/prompt/prompt.component.scss | 13 + .../apps/ui/modals/prompt/prompt.component.ts | 49 ++ .../apps/ui/modals/prompt/prompt.module.ts | 21 + .../directives/backup-create.directive.ts | 73 ++- .../directives/backup-restore.directive.ts | 149 +++--- .../backup-select/backup-select.module.ts | 13 +- .../backup-select/backup-select.page.html | 84 ++-- .../backup-select/backup-select.page.scss | 28 +- .../backup-select/backup-select.page.ts | 27 +- .../recover-select/recover-select.module.ts | 11 +- .../recover-select/recover-select.page.html | 87 ++-- .../recover-select/recover-select.page.scss | 31 ++ .../recover-select/recover-select.page.ts | 49 +- .../target-select/target-select.module.ts | 2 + .../target-select/target-select.page.html | 87 ++-- .../target-select/target-select.page.scss | 3 + .../target-select/target-select.page.ts | 41 +- .../backup-history/backup-history.page.ts | 44 +- .../pages/backup-jobs/backup-jobs.module.ts | 27 +- .../pages/backup-jobs/backup-jobs.page.html | 24 +- .../pages/backup-jobs/backup-jobs.page.ts | 123 ++--- .../edit-job/edit-job.component.html | 47 ++ .../edit-job/edit-job.component.scss | 33 ++ .../edit-job/edit-job.component.ts | 75 +++ .../pages/backup-jobs/edit-job/job-builder.ts | 41 ++ .../pages/backups/pages/backup-jobs/pipes.ts | 4 +- .../backup-targets/backup-targets.module.ts | 2 + .../backup-targets/backup-targets.page.html | 24 +- .../backup-targets/backup-targets.page.ts | 3 +- .../backups/pages/backups/backups.module.ts | 4 +- .../marketplace-list.module.ts | 2 +- .../marketplace-list.page.html | 1 + .../marketplace-list/marketplace-list.page.ts | 4 +- .../marketplace-settings.module.ts | 2 +- .../marketplace-settings.page.html | 11 +- .../marketplace-settings.page.ts | 10 +- .../marketplace-show-controls.component.ts | 153 +++--- .../notifications/notifications.module.ts | 4 +- .../pages/notifications/notifications.page.ts | 110 ++--- .../action-success/action-success.page.html | 53 +- .../action-success/action-success.page.ts | 35 +- .../services/app-actions/app-actions.page.ts | 122 +++-- .../app-credentials/app-credentials.page.html | 2 +- .../app-credentials/app-credentials.page.ts | 27 +- .../app-interfaces-item.component.html | 2 +- .../app-interfaces/app-interfaces.page.ts | 41 +- .../services/app-interfaces/qr.component.ts | 11 +- .../app-show-additional.component.html | 2 +- .../app-show-additional.component.ts | 49 +- .../app-show-status.component.ts | 140 ++---- .../modals/app-config/app-config.page.ts | 36 +- .../app-show/pipes/to-buttons.pipe.ts | 30 +- .../ui/pages/system/domains/domain.const.ts | 34 ++ .../ui/pages/system/domains/domains.module.ts | 32 ++ .../ui/pages/system/domains/domains.page.html | 126 +++++ .../ui/pages/system/domains/domains.page.scss | 0 .../ui/pages/system/domains/domains.page.ts | 215 ++++++++ .../apps/ui/pages/system/email/email.page.ts | 3 +- .../experimental-features.module.ts | 7 +- .../experimental-features.page.html | 15 + .../experimental-features.page.ts | 163 +++--- .../apps/ui/pages/system/lan/lan.page.html | 43 -- .../app/apps/ui/pages/system/lan/lan.page.ts | 22 - .../os-addresses/os-addresses.module.ts | 18 + .../os-addresses/os-addresses.page.html | 228 +++++++++ .../os-addresses/os-addresses.page.scss | 15 + .../system/os-addresses/os-addresses.page.ts | 168 +++++++ .../port-forwards.module.ts} | 14 +- .../port-forwards/port-forwards.page.html | 153 ++++++ .../port-forwards/port-forwards.page.scss | 26 + .../port-forwards/port-forwards.page.ts | 46 ++ .../server-show/os-update/os-update.page.html | 52 +- .../os-update/os-update.page.module.ts | 16 +- .../server-show/os-update/os-update.page.scss | 30 +- .../server-show/os-update/os-update.page.ts | 42 +- .../system/server-show/server-show.module.ts | 4 +- .../system/server-show/server-show.page.html | 88 ++-- .../system/server-show/server-show.page.scss | 15 + .../system/server-show/server-show.page.ts | 467 ++++++++---------- .../server-specs/server-specs.page.html | 56 +-- .../system/server-specs/server-specs.page.ts | 21 +- .../pages/system/sessions/sessions.module.ts | 2 + .../pages/system/sessions/sessions.page.html | 148 +++--- .../ui/pages/system/sessions/sessions.page.ts | 82 ++- .../ui/pages/system/sideload/sideload.page.ts | 21 +- .../pages/system/ssh-keys/ssh-keys.module.ts | 6 +- .../pages/system/ssh-keys/ssh-keys.page.html | 23 +- .../ui/pages/system/ssh-keys/ssh-keys.page.ts | 103 ++-- .../app/apps/ui/pages/system/system.module.ts | 19 +- .../apps/ui/pages/system/wifi/wifi.module.ts | 10 +- .../apps/ui/pages/system/wifi/wifi.page.ts | 91 ++-- .../apps/ui/pages/updates/updates.module.ts | 10 +- .../apps/ui/pages/updates/updates.page.html | 9 +- .../app/apps/ui/pages/updates/updates.page.ts | 67 +-- .../form/form-select/form-select.component.ts | 2 +- .../form/form-text/form-text.component.html | 3 +- .../ui/src/app/common/logs/logs.component.ts | 20 +- .../common/os-welcome/os-welcome.module.ts | 7 +- .../common/os-welcome/os-welcome.page.html | 188 ++++--- .../common/os-welcome/os-welcome.page.scss | 7 +- .../app/common/os-welcome/os-welcome.page.ts | 13 +- .../common/primary-ip/primary-ip.module.ts | 8 + .../app/common/primary-ip/primary-ip.pipe.ts | 17 + .../common/store-icon/store-icon.component.ts | 40 -- .../notifications-toast.component.html | 21 +- .../refresh-alert.component.html | 18 +- .../refresh-alert/refresh-alert.component.ts | 4 +- .../toast-container/toast-container.module.ts | 13 +- .../update-toast/update-toast.component.html | 26 +- .../update-toast/update-toast.component.ts | 17 +- .../projects/ui/src/app/routing.module.ts | 7 + .../ui/src/app/services/api/api.fixures.ts | 6 +- .../ui/src/app/services/api/api.types.ts | 24 + .../app/services/api/embassy-api.service.ts | 34 +- .../services/api/embassy-live-api.service.ts | 36 ++ .../services/api/embassy-mock-api.service.ts | 100 +++- .../ui/src/app/services/api/mock-patch.ts | 59 ++- .../src/app/services/form-dialog.service.ts | 2 +- .../ui/src/app/services/patch-data.service.ts | 32 +- .../src/app/services/patch-db/data-model.ts | 50 +- .../ui/src/app/util/clearnetAddress.ts | 11 + frontend/projects/ui/src/styles.scss | 31 -- frontend/tsconfig.lib.json | 1 - 257 files changed, 4896 insertions(+), 4401 deletions(-) delete mode 100644 frontend/projects/diagnostic-ui/src/app/app-routing.module.ts delete mode 100644 frontend/projects/diagnostic-ui/src/app/app.component.html delete mode 100644 frontend/projects/diagnostic-ui/src/app/app.component.scss delete mode 100644 frontend/projects/diagnostic-ui/src/app/app.component.ts delete mode 100644 frontend/projects/diagnostic-ui/src/app/app.module.ts delete mode 100644 frontend/projects/diagnostic-ui/src/app/pages/home/home-routing.module.ts delete mode 100644 frontend/projects/diagnostic-ui/src/app/pages/home/home.module.ts delete mode 100644 frontend/projects/diagnostic-ui/src/app/pages/home/home.page.html delete mode 100644 frontend/projects/diagnostic-ui/src/app/pages/home/home.page.scss delete mode 100644 frontend/projects/diagnostic-ui/src/environments/environment.prod.ts delete mode 100644 frontend/projects/diagnostic-ui/src/environments/environment.ts delete mode 100644 frontend/projects/diagnostic-ui/src/index.html delete mode 100644 frontend/projects/diagnostic-ui/src/main.ts delete mode 100644 frontend/projects/diagnostic-ui/src/polyfills.ts delete mode 100644 frontend/projects/diagnostic-ui/src/styles.scss delete mode 100644 frontend/projects/diagnostic-ui/src/zone-flags.ts delete mode 100644 frontend/projects/diagnostic-ui/tsconfig.json rename frontend/projects/{ui/src/app/common => marketplace/src/components}/store-icon/store-icon.component.html (76%) rename frontend/projects/{ui/src/app/common => marketplace/src/components}/store-icon/store-icon.component.module.ts (67%) rename frontend/projects/{diagnostic-ui/src/app/pages/logs/logs.page.scss => marketplace/src/components/store-icon/store-icon.component.scss} (100%) create mode 100644 frontend/projects/marketplace/src/components/store-icon/store-icon.component.ts create mode 100644 frontend/projects/marketplace/src/pages/show/additional/additional.component.scss delete mode 100644 frontend/projects/setup-wizard/src/app/modals/password/password.page.scss delete mode 100644 frontend/projects/shared/assets/fonts/Redacted/redacted.regular.ttf delete mode 100644 frontend/projects/shared/src/components/alert/alert-button.directive.ts delete mode 100644 frontend/projects/shared/src/components/alert/alert-input.directive.ts delete mode 100644 frontend/projects/shared/src/components/alert/alert.component.ts delete mode 100644 frontend/projects/shared/src/components/alert/alert.module.ts create mode 100644 frontend/projects/shared/src/components/initializing/initializing.component.html create mode 100644 frontend/projects/shared/src/components/initializing/initializing.component.scss create mode 100644 frontend/projects/shared/src/components/initializing/initializing.component.ts create mode 100644 frontend/projects/shared/src/components/initializing/initializing.module.ts create mode 100644 frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.html create mode 100644 frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.scss create mode 100644 frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.ts create mode 100644 frontend/projects/shared/src/components/loading/loading.service.ts delete mode 100644 frontend/projects/shared/src/components/toast/toast-button.directive.ts delete mode 100644 frontend/projects/shared/src/components/toast/toast.component.ts delete mode 100644 frontend/projects/shared/src/components/toast/toast.module.ts create mode 100644 frontend/projects/shared/src/directives/alert/alert.directive.ts create mode 100644 frontend/projects/shared/src/directives/alert/alert.module.ts create mode 100644 frontend/projects/shared/src/services/copy.service.ts delete mode 100644 frontend/projects/shared/src/services/error-toast.service.ts create mode 100644 frontend/projects/shared/styles/taiga.scss create mode 100644 frontend/projects/ui/src/app/apps/diagnostic/diagnostic.module.ts create mode 100644 frontend/projects/ui/src/app/apps/diagnostic/home/home.module.ts create mode 100644 frontend/projects/ui/src/app/apps/diagnostic/home/home.page.html create mode 100644 frontend/projects/ui/src/app/apps/diagnostic/home/home.page.scss rename frontend/projects/{diagnostic-ui/src/app/pages => ui/src/app/apps/diagnostic}/home/home.page.ts (59%) rename frontend/projects/{diagnostic-ui/src/app/pages => ui/src/app/apps/diagnostic}/logs/logs.module.ts (68%) rename frontend/projects/{diagnostic-ui/src/app/pages => ui/src/app/apps/diagnostic}/logs/logs.page.html (100%) rename frontend/projects/{diagnostic-ui/src/app/pages => ui/src/app/apps/diagnostic}/logs/logs.page.ts (84%) rename frontend/projects/{diagnostic-ui/src/app/services/api/api.service.ts => ui/src/app/apps/diagnostic/services/diagnostic.service.ts} (90%) rename frontend/projects/{diagnostic-ui/src/app/services/api/live-api.service.ts => ui/src/app/apps/diagnostic/services/live-diagnostic.service.ts} (91%) rename frontend/projects/{diagnostic-ui/src/app/services/api/mock-api.service.ts => ui/src/app/apps/diagnostic/services/mock-diagnostic.service.ts} (91%) create mode 100644 frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.component.html rename frontend/projects/ui/src/app/apps/ui/modals/backup-report/{backup-report.page.ts => backup-report.component.ts} (55%) delete mode 100644 frontend/projects/ui/src/app/apps/ui/modals/backup-report/backup-report.page.html delete mode 100644 frontend/projects/ui/src/app/apps/ui/modals/generic-input/generic-input.component.html delete mode 100644 frontend/projects/ui/src/app/apps/ui/modals/generic-input/generic-input.component.module.ts delete mode 100644 frontend/projects/ui/src/app/apps/ui/modals/generic-input/generic-input.component.ts create mode 100644 frontend/projects/ui/src/app/apps/ui/modals/prompt/prompt.component.html create mode 100644 frontend/projects/ui/src/app/apps/ui/modals/prompt/prompt.component.scss create mode 100644 frontend/projects/ui/src/app/apps/ui/modals/prompt/prompt.component.ts create mode 100644 frontend/projects/ui/src/app/apps/ui/modals/prompt/prompt.module.ts create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.component.html create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.component.scss create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/edit-job.component.ts create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/edit-job/job-builder.ts create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/system/domains/domain.const.ts create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.module.ts create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.page.html create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.page.scss create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.page.ts delete mode 100644 frontend/projects/ui/src/app/apps/ui/pages/system/lan/lan.page.html delete mode 100644 frontend/projects/ui/src/app/apps/ui/pages/system/lan/lan.page.ts create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.module.ts create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.html create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.scss create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/system/os-addresses/os-addresses.page.ts rename frontend/projects/ui/src/app/apps/ui/pages/system/{lan/lan.module.ts => port-forwards/port-forwards.module.ts} (50%) create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.html create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.scss create mode 100644 frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.page.ts create mode 100644 frontend/projects/ui/src/app/common/primary-ip/primary-ip.module.ts create mode 100644 frontend/projects/ui/src/app/common/primary-ip/primary-ip.pipe.ts delete mode 100644 frontend/projects/ui/src/app/common/store-icon/store-icon.component.ts create mode 100644 frontend/projects/ui/src/app/util/clearnetAddress.ts diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 76dfec4901..ea2486ad18 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 1586ce8d5f..71ffac6a8d 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 369b9cfe6b..950aff2649 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 dca324ac9b..d7b637042c 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 ee434f78a3..c5810bc104 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 80ea7cf8b3..731cc9d5e6 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 8cbbe67d51..4b6d616b3b 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", @@ -4065,11 +4066,11 @@ "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 +4082,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 +4102,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==", - "dependencies": { - "@maskito/angular": "0.11.1", - "@maskito/core": "0.11.1", - "@maskito/kit": "0.11.1", - "@ng-web-apis/intersection-observer": "3.0.0", + "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": "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 +4129,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 +4147,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": "^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": { - "@maskito/core": "^0.11.1" + "@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 +4322,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 +4540,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 b92c8c172f..c96e73d457 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/patchdb-ui-seed.json b/frontend/patchdb-ui-seed.json index 04cdea674b..bf6ca2c916 100644 --- a/frontend/patchdb-ui-seed.json +++ b/frontend/patchdb-ui-seed.json @@ -1,6 +1,6 @@ { "name": null, - "ack-welcome": "0.4.0", + "ack-welcome": "0.3.4.3", "marketplace": { "selected-url": "https://registry.start9.com/", "known-hosts": { 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 fffdfeecec..0000000000 --- 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 cd28a7e802..0000000000 --- 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 b528fd9bd5..0000000000 --- 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 5ac82a6523..0000000000 --- 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 1abde53a3f..0000000000 --- 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 6ac28af676..0000000000 --- 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 63184b7a2b..0000000000 --- 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 9cba08258b..0000000000 --- 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 214e26874b..0000000000 --- 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 bc0327dbeb..0000000000 --- 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 745ee023ba..0000000000 --- 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 1822018f3c..0000000000 --- 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 21499c3cdd..0000000000 --- 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 f9f1dd06ff..0000000000 --- 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 07a1d8ea03..0000000000 --- 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 24ca60fe2d..0000000000 --- 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 f642f09b31..0000000000 --- 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 cd28a7e802..2d86be2052 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 1abde53a3f..9cc91ba3fe 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 c3764a976e..b2bf633eee 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', - }, - { - text: 'Continue', - handler: () => { - this.install(true) - }, + 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', }, - ], - 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' }, + }, + ) + .subscribe({ + complete: () => { + this.reboot() }, - ], - cssClass: 'alert-success-message', - }) - await alert.present() + }) } 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 827b6e4563..85ef8b4dbc 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 43ecb41a15..76638337e3 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 34b6dd2dd5..5006663eb1 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/diagnostic-ui/src/app/pages/logs/logs.page.scss b/frontend/projects/marketplace/src/components/store-icon/store-icon.component.scss similarity index 100% rename from frontend/projects/diagnostic-ui/src/app/pages/logs/logs.page.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 0000000000..ff4a1aeaed --- /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 74e34c88f0..7cb79764b3 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 583631dc42..59ec09c967 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 c1d76dd2c9..bf84950957 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 b48bbcbaa2..cc7d4f234b 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 8937e8c74d..a639cb9945 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 @@

Git Hash

- +

Other Versions

Click to view other versions

+ + +
+ + +
+
- +

License

{{ manifest.license }}

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

License

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 0000000000..8508da6869 --- /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 778ea6c543..8ac6164aea 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() - } - - 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', + 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), + ), }, - { - text: 'Ok', - handler: (version: string) => this.version.emit(version), - }, - ], - }) - - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(version => this.version.emit(version)) } - 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() + 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, + ), + }, + }) + .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 8d85c7b70c..640d9e7d49 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 fef451a3eb..605b76ebe7 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 d079985e56..7ea64c9678 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 e3a6b521ce..e1f4625bf4 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 cd28a7e802..2d86be2052 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 b821e089d8..aee925f417 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 0f48d072d5..b346a135c6 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 f10455e0c5..3e26600bc6 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 ebec1b21f6..6250ad6365 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 db8acb8f71..5638f95376 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 7f293f5e0e..4335f8a0eb 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 416c558f5b..ce89d07098 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 d779077c60..10dae39cd8 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 d3af3bcbd0..0000000000 --- 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 98de93e1a9..ec500c4ad8 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' - async verifyPw() { - if (!this.target || !this.target['embassy-os']) - this.pwError = 'No recovery target' // unreachable + if (this.password.value.length > 64) + return 'Must be less than 65 characters' - try { - const passwordHash = this.target!['embassy-os']?.['password-hash'] || '' - - argon2.verify(passwordHash, this.password) - this.modalController.dismiss({ password: this.password }, 'success') - } catch (e) { - this.pwError = 'Incorrect password provided' - } + return null } - 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') + get confirmError(): string | null { + return this.confirm.touched && this.password.value !== this.confirm.value + ? 'Passwords do not match' + : null } - validate() { - if (!!this.target) return (this.pwError = '') - - if (this.passwordVer) { - this.checkVer() - } + verifyPw() { + try { + const passwordHash = this.target!['embassy-os']?.['password-hash'] || '' - 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 = '' + argon2.verify(passwordHash, this.password.value) + this.context.completeWith(this.password.value) + } catch (e) { + this.errorService.handleError('Incorrect password provided') } } - 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 b4d6eb9f95..2662abc9d8 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 855051879d..81a9faaba9 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 c0e93d18a6..88ab04160c 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 9c7ae1bc90..3de110846e 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 559705a7f5..54609eb9a0 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 a8cd194bae..66cab3eff4 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 4ea73e6190..17da17223f 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 5de21a289b..8cf58d7fad 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 3bc1fe32c5d2531891d9441ddeb1bd99b8dd8af0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17036 zcmeHPYjjja60W}U%w#5+nM^_;0ecugAd`215CVh{!Ye=w4`(z8L*5XQm*meffSWf5h0LC>uMVs4;H?hLNvM* z`n*$DUse}=w5c~yZYJm_E-sr_zW-3sFyJeI8&QOH=-|lBZwvU=?54pXxVS*+i2w%8 zaY?`#eTD*S0fTx{J6lTz(i_tChk!pEg3=+nL5dS3f>6D59TfpNF3KU>capLEY&p~@ zXL~u(gQCw^ZbS=l$bkg4#48xlakPl`c(C3B=mnjgtf<-OR%&O{*%E2Lbevnbi}&Jv zcwat%C-W>mhi`ZHZKdy|_IA(;YEGM2kxEV6&SQDJNH3Kzk|EB%p_PyrBGT?~M!BM6dc^kZ6&K&T&sE&r*VC{6fU5@%8l2!wOiE5kO-s)hGIUtx z@T?KpBS($S8IwB}-aUT8#7UD2rc9kycumpt88c^H>+@fCeevuY=FAPy%DeBm_rdiW zw`|_}*zX^I;;ARMKfU7_x%t`Wc0J$n!b>l{{0h~VmQ{VY9|^Z4C~vd&u-%jKLxpIgrHC(3%S80z-8Z}NQ@ zcgPy!;j+WFPo*S=WD}hs*i_lVmFN1zhe0(rJ;0f}eM-k_d zsmLeifD-c)V&$ANfnZF8B5CB&ShqWl9}U6`7&H;O%~W-RejtnVdiYUQxQF{DPmgoU z%4F#%C=UNiwH>dE&6=Fh!wCSqum7n2Cy zEzCBl)DtwxywH~&w+B5g@3WM{a&mLo zcbfFNFVmWhhU3{gbrP!7tP{-!(Y(`S)PJA7lZp0phuhKL?QrKw{A@qA{%oaT=C@Df zDf=KI_iVW?9s8J$dc&=gWtPljeX>kPn|17Sr5IQOrKI*`Zig=YY`K}vD0*q*G`nd2 zqQ!%@PiXt+mGOS)LBHh9C*H~D-+kJcuAdva%XVEo*L0VDSDGBn-l6mK<-D%lxkg=; zrH#Avzqc-Dzv?dCy4yD{_jz@fewVYHE7d11XPbAIZe3m5uISfQS=zWuzppgAXm-*5 zo_zjq23kC5@u0A2M_0!0N7}hgJJ)IFI_+Gio$Iu7op!F% z&UOE5=Q`^AiD#$pT)Nt)or$zFk#;7+2h&Qf2y#SH@h!Hf|L5UO@zqUn0-3qBTlS{R ziOl|nt}=@+%#lZf{F!s`EOb+{TFmFNJn+36a$iOvMF>>whtP|bPSyDIbb;%PT}$4nc*|S>%v!uZwucS z-WvYBHP)J8Ewol!S6R1O_gIfx&)8hH6kCC<(ze33$@Y@%nC*0gJ;EC?A)+*5dBlc@ zmWU$}Uq@Oa2S(;a&W&6axju4NoJ;z>bUt(Wpm+c4bZ4SMIJF*=BlwR4+upR?8ZeN=2zMpR)`b=0b;tx@RCD$?6>1cbj_b2YG=n2uYqU)pAL_Zn5Kl-ESvoXD5hQ<`d)Zh-rJcfI3%!!yU zdKi1ahm>8o4O?y~8&BjsN))3sC#lPBr~PmTrVj(Y86{L_$obr?)+LIdooZc2189$0 z*Au5t)w+SUBLgrr)<|~NL2shLY_HmGratVHS`VW>l0~hDBfoN>TDMY~v{J3x7?)bq zdIV+Yj;Zwn6r;DQ^*1R+-4f;E{CDAbGIYP0+0#FK?a%f)-sq0U5{1xGSKwSo3S9oqcZXE-ztS(8I^A$JU211O>^3$U|w8ZZ|TiFN30fJEW7BC2Z9T7i|B zhVcbm8gnh?Jr5QXek=BYP!vp2aABV^$P}$Z@l1o4X2G8`@XWw{ezyqimvpv`Zcz8h z&Y%r@sKj*Gh4jc6HzMECOkrf9aAa@WuwO=E|8$U(qR2(j*l~Lx>%1rRqB!Jw_ohB{ z6=b=oFL|gR^~auiHDYrR4W7I=faz%qt)RQ; zWqOF(XeF(od+8B+g0?aP1?dC2lOAM7W}^F;nO4zjbb^J^MtYLIq0{tlydR#XH|Y&} z2KFwYweZD1!@39PPxLm^g7*5PqA2fgcBYQ!V9H6;1VJrGbXZP*+-h2oA~Dk87&0705VMkNn&e zMGwF&?f)@ diff --git a/frontend/projects/shared/package.json b/frontend/projects/shared/package.json index a2bbec95f0..64e2445625 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 fc5320edb3..0000000000 --- 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 af7879e371..0000000000 --- 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 522ba93d3a..0000000000 --- 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 45fa01f55d..0000000000 --- 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/initializing/initializing.component.html b/frontend/projects/shared/src/components/initializing/initializing.component.html new file mode 100644 index 0000000000..b58ee2f74f --- /dev/null +++ b/frontend/projects/shared/src/components/initializing/initializing.component.html @@ -0,0 +1,34 @@ + + + + + + + Initializing StartOS +
+ + Progress: {{ (progress * 100).toFixed(0) }}% + +
+
+ + + +

{{ getMessage(progress) }}

+
+
+ +
+ +
+
+
+
+
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 0000000000..f21705ce5c --- /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 0000000000..e72cecb9eb --- /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 0000000000..daa025aa31 --- /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/initializing/logs-window/logs-window.component.html b/frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.html new file mode 100644 index 0000000000..4c6866ff1e --- /dev/null +++ b/frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.html @@ -0,0 +1,11 @@ + +
+
+ +
diff --git a/frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.scss b/frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.scss new file mode 100644 index 0000000000..032ba006fe --- /dev/null +++ b/frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.scss @@ -0,0 +1,10 @@ +// Hide scrollbar for Chrome, Safari and Opera +ion-content::part(scroll)::-webkit-scrollbar { + display: none; +} + +// Hide scrollbar for IE, Edge and Firefox +ion-content::part(scroll) { + -ms-overflow-style: none; // IE and Edge + scrollbar-width: none; // Firefox +} \ No newline at end of file diff --git a/frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.ts b/frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.ts new file mode 100644 index 0000000000..6439ba062c --- /dev/null +++ b/frontend/projects/shared/src/components/initializing/logs-window/logs-window.component.ts @@ -0,0 +1,68 @@ +import { Component, ViewChild } from '@angular/core' +import { IonContent } from '@ionic/angular' +import { map, takeUntil } from 'rxjs' +import { TuiDestroyService } from '@taiga-ui/cdk' +import { SetupLogsService } from '../../../services/setup-logs.service' +import { Log } from '../../../types/api' +import { toLocalIsoString } from '../../../util/to-local-iso-string' + +const Convert = require('ansi-to-html') +const convert = new Convert({ + bg: 'transparent', +}) + +@Component({ + selector: 'logs-window', + templateUrl: 'logs-window.component.html', + styleUrls: ['logs-window.component.scss'], + providers: [TuiDestroyService], +}) +export class LogsWindowComponent { + @ViewChild(IonContent) + private content?: IonContent + + autoScroll = true + + constructor( + private readonly logs: SetupLogsService, + private readonly destroy$: TuiDestroyService, + ) {} + + ngOnInit() { + this.logs + .pipe( + map(log => this.convertToAnsi(log)), + takeUntil(this.destroy$), + ) + .subscribe(innerHTML => { + const container = document.getElementById('container') + const newLogs = document.getElementById('template')?.cloneNode() + + if (!(newLogs instanceof HTMLElement)) return + + newLogs.innerHTML = innerHTML + container?.append(newLogs) + + if (this.autoScroll) { + setTimeout(() => this.content?.scrollToBottom(250)) + } + }) + } + + handleScroll(e: any) { + if (e.detail.deltaY < 0) this.autoScroll = false + } + + async handleScrollEnd() { + const elem = await this.content?.getScrollElement() + if (elem && elem.scrollHeight - elem.scrollTop - elem.clientHeight < 64) { + this.autoScroll = true + } + } + + private convertToAnsi(log: Log) { + return `${toLocalIsoString( + new Date(log.timestamp), + )}  ${convert.toHtml(log.message)}
` + } +} diff --git a/frontend/projects/shared/src/components/loading/loading.component.scss b/frontend/projects/shared/src/components/loading/loading.component.scss index f21705ce5c..9a7d101009 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); -.progress { - max-width: 700px; - padding-bottom: 20px; - margin: auto auto 40px; + --tui-primary: var(--tui-warning-fill); } -.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 3207aebb7e..373f013a11 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 1ffcd7e363..4a3798041e 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/shared/src/components/loading/loading.service.ts b/frontend/projects/shared/src/components/loading/loading.service.ts new file mode 100644 index 0000000000..96ab4301f3 --- /dev/null +++ b/frontend/projects/shared/src/components/loading/loading.service.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@angular/core' +import { AbstractTuiDialogService } from '@taiga-ui/cdk' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' +import { LoadingComponent } from './loading.component' + +@Injectable({ providedIn: `root` }) +export class LoadingService extends AbstractTuiDialogService { + protected readonly component = new PolymorpheusComponent(LoadingComponent) + protected readonly defaultOptions = {} +} diff --git a/frontend/projects/shared/src/components/markdown/markdown.component.html b/frontend/projects/shared/src/components/markdown/markdown.component.html index 090070c4e3..45271946d4 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 6da4673d1d..8b8e3a6fe0 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 7e47acc390..922ad645ca 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 7c564961e4..0000000000 --- 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 b6431c5322..0000000000 --- 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 9f5304f5d6..0000000000 --- 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 0000000000..c9c7e3b4aa --- /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 0000000000..75791bd298 --- /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 f2c0246709..bd6bb8630f 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 da0632cc1b..2f0dea1669 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 0000000000..39bf3d7339 --- /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 2ac1314f7a..0000000000 --- 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 bb0221ce2b..45891e0f47 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 f050078691..118cf35fe5 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 997ded733e..2da7d3f8d3 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 725d7d9855..4bcedd0f4d 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 0000000000..8bd35d6229 --- /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 29c7e11a30..dcc65b8c6b 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 af049e1302..68e77ffee3 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 215c97e838..2c23b35141 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' 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 bd9a2ce2c5..a6305090d5 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 f20fe4a961..5c1c4f3886 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 df0ae29b75..bb08430b1e 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 9e037ce8d7..1fc48c47cd 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 c07d3a2b7e..50605f1dc5 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 6c671201d0..eeadb9df00 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 5c8cc76b4a..246db7c095 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() + + try { + await this.embassyApi.setDbValue( + ['gaming', 'snake', 'high-score'], + score, + ) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } }) - - 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() } } 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 73f4d0e8fa..8bb81a01eb 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 0000000000..ddb8d4defb --- /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 0000000000..62f6394e5a --- /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 0000000000..9accfe6cec --- /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 0000000000..15ec44f64f --- /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 bbda6939fb..8af9be855d 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', - }, - { - text: 'Rebuild', - handler: () => { - try { - this.systemRebuild() - } catch (e) { - console.error(e) - } - }, + 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.

', }, - ], - 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', - }, - { - text: 'Repair', - handler: () => { - try { - this.repairDisk() - } catch (e) { - console.error(e) - } - }, + 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.

', }, - ], - 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 da4d046b40..7cb2cc2e10 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 317cd1ea34..7aaf0f519e 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 562d486c36..e8bd20a28e 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 bbde6e5bae..dc4d3e9c4f 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 5d8c13a4f6..4a16f3e586 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 9c7ae1bc90..3de110846e 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 5b9740f3d5..c4ac568666 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 c86f6057e3..9e0b2f42b5 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 0000000000..e0dac90746 --- /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 7434f51522..bbf0ceff42 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 f21ff0918b..a41a63e534 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 4ecf064d98..0000000000 --- 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 814655fa03..464f607700 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 58854a6916..3bd7567a92 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 c36cf0dd08..f7fcb1defd 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 308afd7dc3..0000000000 --- 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 d2b1faab49..0000000000 --- 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 2ebf805392..0000000000 --- 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 0000000000..7882109736 --- /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 0000000000..d95d85925f --- /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 0000000000..9842afe022 --- /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 0000000000..12cd96f6b9 --- /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 50ee7f8a81..91769e709b 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 3b1710030e..b762c6fe07 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, - }) - - modal.onWillDismiss().then(res => { - if (res.role === 'success') { - this.navCtrl.navigateRoot('/services') - } + private presentModalSelect(data: RecoverData): Observable { + return this.dialogs.open(new PolymorpheusComponent(RecoverSelectPage), { + label: 'Select Services to Restore', + data, }) - - 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 be840eff2a..bcb9ed1569 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 457152a451..f79a9d0f8c 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 854c0ba4e0..89ba0a7aa4 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 6c1f84614e..f21a5ca7f4 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 3cf8661713..0f7e632883 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 09a0556507..8fd2e77ced 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 e69de29bb2..4897866d3a 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 85989cc45b..5052d9eb9e 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 7cfa694078..3b88319de8 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 ea2a349530..8aa08a50a5 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 e69de29bb2..bfffad405c 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 176c40d4e6..353fcdc143 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, ) {} - async ngOnInit() { - await this.getTargets() + get type(): BackupType { + return this.context.data.type } - dismiss() { - this.modalCtrl.dismiss() + async ngOnInit() { + await this.getTargets() } 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 48f5dafdfb..0cec0874d7 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 3ef1541968..b26d01ed44 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 { CommonModule } from '@angular/common' import { NgModule } from '@angular/core' +import { FormsModule } from '@angular/forms' import { RouterModule, Routes } from '@angular/router' -import { CommonModule } from '@angular/common' 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 { NewJobPage } from './new-job/new-job.page' -import { EditJobPage } from './edit-job/edit-job.page' -import { JobOptionsComponent } from './job-options/job-options.component' +import { EditJobComponent } from './edit-job/edit-job.component' import { ToHumanCronPipe } from './pipes' -import { FormsModule } from '@angular/forms' 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 516b0ad7d6..871fc55ebb 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 (click)="presentModalCreate()" > - 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 a84a3c1b77..2399239c05 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', - }, - { - text: 'Delete', - handler: () => { - this.delete(id, index) - }, - cssClass: 'enter-click', + 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', }, - ], - }) - 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 0000000000..1e599bfb39 --- /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 0000000000..18b650d0cd --- /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 0000000000..1553dd5415 --- /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/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 0000000000..b84e4d369f --- /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/pipes.ts b/frontend/projects/ui/src/app/apps/ui/pages/backups/pages/backup-jobs/pipes.ts index dc7afc3fba..0e756aa9ac 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 9c3f658860..fb507215da 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 c8819ed626..655bf28e6f 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 7571b9974d..fb5e193a38 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 5c56787df3..c4c53f1401 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 918fe65a5d..7a7e19e817 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 734cb89104..235d1b3685 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 fe92814855..813e9109b2 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 9eaebbd340..0304046c2e 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 8d0da271a5..77dde15e76 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 @@

{{ s.name }}

> - +

{{ 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 09d30780ec..a5fee0e906 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 732427cf25..9dc921ea88 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) - }, - }, - { - text: 'Continue', - handler: () => { - resolve(true) - }, - cssClass: 'enter-click', + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content, + yes: 'Continue', + no: 'Cancel', }, - ], - 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 dfbb6036d8..2f50677a3c 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 e47b952768..909ffcc75e 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 da8cc7be5b..ed8babcc0b 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 48adb138a4..f4b3902695 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 e8eaa10101..42e354d069 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', - }, - { - text: 'Execute', - handler: async () => this.executeAction(action.id), - cssClass: 'enter-click', + 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', }, - ], - }) - 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 4c9a8b41fd..f2f9ecb184 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 @@

{{ cred.key }}

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 0e5942b6a3..c04293e09a 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 @@

{{ address | addressType }}

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 86650d0443..52537ac0ee 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 8f34aa01d9..a87e43863f 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 4fc0eea32a..e337dd202f 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 @@

Installed

*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 3b4e847874..a61101cd02 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 c3ad71bebb..91eb1f7f02 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', - }, - { - text: 'Stop', - handler: () => { - this.stop() - }, - cssClass: 'enter-click', + if (content) { + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content, + yes: 'Stop', + no: 'Cancel', }, - ], - 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', - }, - { - text: 'Restart', - handler: () => { - this.restart() - }, - cssClass: 'enter-click', + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content: `Services that depend on ${title} may temporarily experiences issues`, + yes: 'Restart', + no: 'Cancel', }, - ], - 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 f606d067ef..14596b7b8c 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 02113250d0..cf8252b750 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 0000000000..1670fde807 --- /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/domains/domains.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.module.ts new file mode 100644 index 0000000000..14fe146a87 --- /dev/null +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.module.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { RouterModule, Routes } from '@angular/router' +import { DomainsPage } from './domains.page' +import { TuiNotificationModule } from '@taiga-ui/core' +import { SharedPipesModule } from '@start9labs/shared' +import { TuiLetModule } from '@taiga-ui/cdk' +import { FormPageModule } from 'src/app/apps/ui/modals/form/form.module' +import { WifiPage, ToWifiIconPipe } from './wifi.page' + +const routes: Routes = [ + { + path: '', + component: DomainsPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + TuiNotificationModule, + RouterModule.forChild(routes), + SharedPipesModule, + TuiLetModule, + FormPageModule, + RouterModule.forChild(routes), + ], + declarations: [DomainsPage], +}) +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 0000000000..9ce4775921 --- /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/ui/src/app/apps/ui/pages/system/domains/domains.page.scss b/frontend/projects/ui/src/app/apps/ui/pages/system/domains/domains.page.scss new file mode 100644 index 0000000000..e69de29bb2 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 0000000000..4ccaed8950 --- /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 a7ea7a2331..fb8ae298ea 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 86e374b170..436a9ed066 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 69f82ff43f..fefb783c70 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 @@

{{ server.zram ? 'Disable' : 'Enable' }} zram

+ + +

+ 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 ae00252046..c456469930 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, ) {} + get isTor(): boolean { + return this.config.isTor() + } + 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', + this.wipe = false + this.dialogs + .open(TUI_PROMPT, { + label: this.isTor ? 'Warning' : 'Confirm', + data: { + content: this.template, + yes: 'Reset', + no: 'Cancel', }, - ], - cssClass: isTor ? 'alert-warning-message' : '', - }) - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => this.resetTor(this.wipe)) } - 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', - }, - { - text: enabled ? 'Disable' : 'Enable', - handler: () => { - this.toggleZram(enabled) - }, - cssClass: 'enter-click', + 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 5c11fea028..0000000000 --- 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.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/lan/lan.page.ts deleted file mode 100644 index b07b3f2f1c..0000000000 --- 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 0000000000..5eb444af1b --- /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 0000000000..1f24cbd57a --- /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 0000000000..fab7b4db2f --- /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 0000000000..36c5c16b3e --- /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/lan/lan.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/port-forwards/port-forwards.module.ts similarity index 50% 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/port-forwards/port-forwards.module.ts index dc0216ce27..b81c53fc31 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/port-forwards/port-forwards.module.ts @@ -2,13 +2,14 @@ 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 { SharedPipesModule } from '@start9labs/shared' +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: LANPage, + component: PortForwardsPage, }, ] @@ -17,8 +18,9 @@ const routes: Routes = [ CommonModule, IonicModule, RouterModule.forChild(routes), - SharedPipesModule, + PrimaryIpPipeModule, + FormsModule, ], - declarations: [LANPage], + declarations: [PortForwardsPage], }) -export class LANPageModule {} +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 0000000000..a25127a9a7 --- /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 0000000000..50f21298df --- /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 0000000000..fcb977bbad --- /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 011a5cc51d..e2eaebcfbc 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 2d3e0176ab..426dc846dc 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 586a541268..d2f78caf75 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 ffc5444590..d87a3856e6 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 f12152c8a0..18799d6b50 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 cc0d96c742..9c9dc22fb8 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 e69de29bb2..84f709c07a 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 7dd200e632..d7915d5514 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', - }, - { - text: 'Logout', - handler: () => this.logout(), - cssClass: 'enter-click', + 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', }, - ], - }) - - 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', - }, - { - text: 'Restart', - handler: () => { - this.restart() - }, - cssClass: 'enter-click', + 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', }, - ], - }) - 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', - }, - { - text: 'Rebuild', - handler: () => { - this.systemRebuild() - }, - cssClass: 'enter-click', + + 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', }, - ], - 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', @@ -369,88 +300,97 @@ export class ServerShowPage { disabled$: of(false), }, { - title: 'LAN', - description: `Download and trust your server's certificate for a secure local connection`, - icon: 'home-outline', + title: 'Email', + description: + 'Connect to an external SMTP server to send yourself emails', + icon: 'mail-outline', action: () => - this.navCtrl.navigateForward(['lan'], { relativeTo: this.route }), + this.navCtrl.navigateForward(['email'], { 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', + title: 'Sideload a Service', + description: `Manually install a service`, + icon: 'push-outline', action: () => - this.navCtrl.navigateForward(['ssh'], { relativeTo: this.route }), + this.navCtrl.navigateForward(['sideload'], { + relativeTo: this.route, + }), detail: true, disabled$: of(false), }, { - title: 'Email', - description: 'Provide an external SMTP server for sending emails', - icon: 'mail-outline', + title: 'Experimental Features', + description: 'Try out new and potentially unstable new features', + icon: 'flask-outline', action: () => - this.navCtrl.navigateForward(['email'], { relativeTo: this.route }), + this.navCtrl.navigateForward(['experimental-features'], { + relativeTo: this.route, + }), detail: true, disabled$: of(false), }, + ], + Network: [ { - title: 'WiFi', - description: 'Add or remove WiFi networks', - icon: 'wifi', + title: 'StartOS Web Interface', + description: 'Addresses for accessing this StartOS web interface', + icon: 'desktop-outline', action: () => - this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }), + this.navCtrl.navigateForward(['addresses'], { + relativeTo: this.route, + }), detail: true, disabled$: of(false), }, { - title: 'Sideload a Service', - description: `Manually install a service`, - icon: 'push-outline', + title: 'Domains', + description: + 'Add domains to your server to enable clearnet connections', + icon: 'globe-outline', action: () => - this.navCtrl.navigateForward(['sideload'], { - relativeTo: this.route, - }), + this.navCtrl.navigateForward(['domains'], { relativeTo: this.route }), detail: true, disabled$: of(false), }, { - title: 'Experimental Features', - description: 'Try out new and potentially unstable new features', - icon: 'flask-outline', + title: 'Port Forwards', + description: + 'A list of ports that should be forwarded through your router', + icon: 'trail-sign-outline', action: () => - this.navCtrl.navigateForward(['experimental-features'], { + this.navCtrl.navigateForward(['port-forwards'], { relativeTo: this.route, }), detail: true, disabled$: of(false), }, - ], - Insights: [ { - title: 'About', - description: 'Basic information about your server', - icon: 'information-circle-outline', + title: 'WiFi', + description: 'Add or remove WiFi networks', + icon: 'wifi', action: () => - this.navCtrl.navigateForward(['specs'], { relativeTo: this.route }), + this.navCtrl.navigateForward(['wifi'], { relativeTo: this.route }), detail: true, disabled$: of(false), }, + ], + Security: [ { - title: 'Monitor', - description: 'CPU, disk, memory, and other useful metrics', - icon: 'pulse', + title: 'SSH', + description: + 'Manage your SSH keys to access your server from the command line', + icon: 'terminal-outline', action: () => - this.navCtrl.navigateForward(['metrics'], { relativeTo: this.route }), + 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 2cebf019e9..0c3b24f92e 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 @@

Version

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

CA fingerprint

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 d4efca83dd..7e09f38f8b 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 be0905775e..444321c36c 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 631d9aaee4..d8579c6fad 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 6a4a565a5f..607d4ce29b 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 4674b7bb28..f60b62ca92 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 6192f8f11e..84114149a3 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 ce2c35b624..ff61301a03 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 (click)="presentModalAdd()" > - 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 794e5dd475..68383b86b1 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), - } + 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)) + } - const modal = await this.modalCtrl.create({ - component: GenericInputComponent, - componentProps: { options }, - cssClass: 'alertlike-modal', - }) - await modal.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)) } - async add(pubkey: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Saving...', - }) - await loader.present() + 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 ff4093f02c..77bb24b698 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.module.ts b/frontend/projects/ui/src/app/apps/ui/pages/system/wifi/wifi.module.ts index 92323a1295..14fe146a87 100644 --- a/frontend/projects/ui/src/app/apps/ui/pages/system/wifi/wifi.module.ts +++ b/frontend/projects/ui/src/app/apps/ui/pages/system/wifi/wifi.module.ts @@ -2,6 +2,8 @@ import { NgModule } from '@angular/core' import { CommonModule } from '@angular/common' import { IonicModule } from '@ionic/angular' import { RouterModule, Routes } from '@angular/router' +import { DomainsPage } from './domains.page' +import { TuiNotificationModule } from '@taiga-ui/core' import { SharedPipesModule } from '@start9labs/shared' import { TuiLetModule } from '@taiga-ui/cdk' import { FormPageModule } from 'src/app/apps/ui/modals/form/form.module' @@ -10,7 +12,7 @@ import { WifiPage, ToWifiIconPipe } from './wifi.page' const routes: Routes = [ { path: '', - component: WifiPage, + component: DomainsPage, }, ] @@ -18,11 +20,13 @@ const routes: Routes = [ imports: [ CommonModule, IonicModule, + TuiNotificationModule, + RouterModule.forChild(routes), SharedPipesModule, TuiLetModule, FormPageModule, RouterModule.forChild(routes), ], - declarations: [WifiPage, ToWifiIconPipe], + declarations: [DomainsPage], }) -export class WifiPageModule {} +export class DomainsPageModule {} 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 71f1e285d3..8428531775 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 8fc07fd36a..d557ae7bb4 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 a4c4cc8f0b..e0daac362e 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) - }, - }, - { - text: 'Continue', - handler: () => { - resolve(true) - }, - cssClass: 'enter-click', + this.dialogs + .open(TUI_PROMPT, { + label: 'Warning', + size: 's', + data: { + content, + yes: 'Continue', + no: 'Cancel', }, - ], - 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 b36b1c4172..1c4525f866 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 55acd437f7..e466d16aaa 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 0dc939f99e..ae9b93b7b5 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 f9a6ecd7b0..6787054464 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 0000000000..941518ab2b --- /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 0000000000..4cfa985524 --- /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 0763116971..0000000000 --- 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 6e7f7c6fa0..d75364715b 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 20985c11e1..db2480687c 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 e9a2e75ae2..3a2740af91 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 e23fcc454a..86294542a4 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 b7a4af51b6..350b0fff99 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 676aa0dc6b..0b02faa4ee 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 d835713bc8..f8b67c9f9e 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 800eb6914a..787ccab952 100644 --- a/frontend/projects/ui/src/app/services/api/api.fixures.ts +++ b/frontend/projects/ui/src/app/services/api/api.fixures.ts @@ -844,7 +844,7 @@ export module Mock { integer: false, }), }), - displayAs: 'I\'m {{last-name}}, {{first-name}} {{last-name}}', + displayAs: "I'm {{last-name}}, {{first-name}} {{last-name}}", uniqueBy: 'last-name', }, ), @@ -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 8ec0ecf597..a0a0454ba7 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 168a7692b4..82ef784b3a 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,15 @@ export abstract class ApiService { abstract connectWifi(params: RR.ConnectWifiReq): Promise - abstract deleteWifi(params: RR.DeleteWifiReq): Promise + abstract deleteWifi(params: RR.DeleteWifiReq): Promise + + // email + + abstract testEmail(params: RR.TestEmailReq): Promise + + abstract configureEmail( + params: RR.ConfigureEmailReq, + ): 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 3fb8f5a47f..f2cb5b57eb 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 a94564b747..d12f983899 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 4f678ecb59..c6c387d9eb 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 b44218f75a..69df946bb8 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 9efe056996..4eb2e166d4 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 01629667bc..6221a773d1 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 0000000000..94d483ece8 --- /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 7159d68065..c98d52162d 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 0dd228bf91..2d527a3624 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"]