From 010be05920becfdbac1f8e42777296d609a2e98d Mon Sep 17 00:00:00 2001 From: Matt Hill Date: Tue, 16 May 2023 08:03:29 -0600 Subject: [PATCH] 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 --- frontend/package-lock.json | 143 +- frontend/package.json | 12 +- .../projects/ui/src/app/app-routing.module.ts | 9 - frontend/projects/ui/src/app/app.module.ts | 2 - .../app/app/preloader/preloader.component.ts | 1 + .../form-label/form-label.component.html | 15 - .../form-label/form-label.component.scss | 11 - .../form-label/form-label.component.ts | 34 - .../form-object/form-object.module.ts | 49 - .../form-object/form-object.pipes.ts | 64 - .../form-file/form-file.component.html | 40 - .../form-file/form-file.component.scss | 26 - .../controls/form-file/form-file.component.ts | 21 - .../form-input/form-input.component.html | 65 - .../form-input/form-input.component.scss | 24 - .../form-input/form-input.component.ts | 21 - .../form-select/form-select.component.html | 49 - .../form-select/form-select.component.scss | 5 - .../form-select/form-select.component.ts | 16 - .../form-subform/form-subform.component.html | 23 - .../form-subform/form-subform.component.scss | 25 - .../form-subform/form-subform.component.ts | 16 - .../form-object/form-object.component.html | 198 --- .../form-object/form-object.component.scss | 41 - .../form-object/form-object.component.ts | 253 --- .../form-union/form-union.component.html | 47 - .../form-union/form-union.component.scss | 30 - .../form-union/form-union.component.ts | 66 - .../form-object/form-warning.directive.ts | 51 - .../ui/src/app/components/form/control.ts | 2 +- .../form/form-array/form-array.component.ts | 2 +- .../form/form-color/form-color.component.ts | 2 +- .../form-control/form-control.component.ts | 8 +- .../form-control/form-control.providers.ts | 2 +- .../form-datetime/form-datetime.component.ts | 2 +- .../form/form-file/form-file.component.ts | 2 +- .../form/form-group/form-group.component.ts | 2 +- .../form-multiselect.component.ts | 2 +- .../form-number/form-number.component.html | 2 +- .../form/form-number/form-number.component.ts | 2 +- .../form/form-object/form-object.component.ts | 2 +- .../form/form-select/form-select.component.ts | 2 +- .../form/form-text/form-text.component.ts | 2 +- .../form-textarea/form-textarea.component.ts | 2 +- .../form/form-toggle/form-toggle.component.ts | 2 +- .../form/form-union/form-union.component.ts | 6 +- .../app/modals/app-config/app-config.page.ts | 6 +- .../ui/src/app/modals/form/form.page.ts | 2 +- .../generic-form/generic-form.module.ts | 19 - .../generic-form/generic-form.page.html | 35 - .../generic-form/generic-form.page.scss | 9 - .../modals/generic-form/generic-form.page.ts | 72 - .../marketplace-settings.module.ts | 8 + .../marketplace-settings.page.html | 72 +- .../marketplace-settings.page.scss | 16 + .../marketplace-settings.page.ts | 196 +-- .../app-actions/app-actions.page.ts | 67 +- .../backup-targets/backup-targets.page.ts | 185 +-- .../backups-routes/types/target-types.ts | 228 +-- .../dev-config/dev-config.module.ts | 30 - .../dev-config/dev-config.page.html | 28 - .../dev-config/dev-config.page.scss | 0 .../dev-config/dev-config.page.ts | 79 - .../dev-instructions.module.ts | 30 - .../dev-instructions.page.html | 28 - .../dev-instructions.page.scss | 0 .../dev-instructions/dev-instructions.page.ts | 69 - .../dev-manifest/dev-manifest.module.ts | 30 - .../dev-manifest/dev-manifest.page.html | 17 - .../dev-manifest/dev-manifest.page.scss | 0 .../dev-manifest/dev-manifest.page.ts | 32 - .../developer-list/developer-list.module.ts | 26 - .../developer-list/developer-list.page.html | 37 - .../developer-list/developer-list.page.scss | 0 .../developer-list/developer-list.page.ts | 279 ---- .../developer-menu/developer-menu.module.ts | 32 - .../developer-menu/developer-menu.page.html | 51 - .../developer-menu/developer-menu.page.scss | 0 .../developer-menu/developer-menu.page.ts | 64 - .../developer-menu/form-info.ts | 201 --- .../developer-routing.module.ts | 49 - .../marketplace-list/marketplace-list.page.ts | 16 +- .../notifications/notifications.page.html | 1 - .../pages/server-routes/email/email.module.ts | 32 + .../pages/server-routes/email/email.page.html | 63 + .../pages/server-routes/email/email.page.scss | 11 + .../pages/server-routes/email/email.page.ts | 76 + .../server-routes/server-routing.module.ts | 5 + .../server-show/server-show.page.ts | 9 + .../wifi/{wifiSpec.ts => wifi.const.ts} | 8 +- .../pages/server-routes/wifi/wifi.module.ts | 2 + .../pages/server-routes/wifi/wifi.page.html | 6 +- .../pages/server-routes/wifi/wifi.page.scss | 5 - .../app/pages/server-routes/wifi/wifi.page.ts | 2 +- .../ui/src/app/services/api/api.fixures.ts | 1428 ++++++----------- .../ui/src/app/services/api/api.types.ts | 11 +- .../app/services/api/embassy-api.service.ts | 8 + .../services/api/embassy-live-api.service.ts | 14 +- .../services/api/embassy-mock-api.service.ts | 29 +- .../ui/src/app/services/api/mock-patch.ts | 226 +-- .../ui/src/app/services/form.service.ts | 10 +- .../src/app/services/patch-db/data-model.ts | 17 +- .../ui/src/app/util/config-utilities.ts | 2 +- .../ui/src/app/util/configBuilderToSpec.ts | 9 + frontend/projects/ui/src/styles.scss | 5 + 105 files changed, 1237 insertions(+), 4156 deletions(-) delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-label/form-label.component.html delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-label/form-label.component.scss delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-label/form-label.component.ts delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-object.module.ts delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-object.pipes.ts delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-object/controls/form-file/form-file.component.html delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-object/controls/form-file/form-file.component.scss delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-object/controls/form-file/form-file.component.ts delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-object/controls/form-input/form-input.component.html delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-object/controls/form-input/form-input.component.scss delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-object/controls/form-input/form-input.component.ts delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-object/controls/form-select/form-select.component.html delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-object/controls/form-select/form-select.component.scss delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-object/controls/form-select/form-select.component.ts delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-object/controls/form-subform/form-subform.component.html delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-object/controls/form-subform/form-subform.component.scss delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-object/controls/form-subform/form-subform.component.ts delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-object/form-object.component.html delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-object/form-object.component.scss delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-object/form-object.component.ts delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-union/form-union.component.html delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-union/form-union.component.scss delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-union/form-union.component.ts delete mode 100644 frontend/projects/ui/src/app/components/form-object/form-warning.directive.ts delete mode 100644 frontend/projects/ui/src/app/modals/generic-form/generic-form.module.ts delete mode 100644 frontend/projects/ui/src/app/modals/generic-form/generic-form.page.html delete mode 100644 frontend/projects/ui/src/app/modals/generic-form/generic-form.page.scss delete mode 100644 frontend/projects/ui/src/app/modals/generic-form/generic-form.page.ts delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.module.ts delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.html delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.scss delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.module.ts delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.html delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.scss delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.module.ts delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.html delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.scss delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.ts delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.module.ts delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.scss delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.module.ts delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.scss delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/developer-menu/form-info.ts delete mode 100644 frontend/projects/ui/src/app/pages/developer-routes/developer-routing.module.ts create mode 100644 frontend/projects/ui/src/app/pages/server-routes/email/email.module.ts create mode 100644 frontend/projects/ui/src/app/pages/server-routes/email/email.page.html create mode 100644 frontend/projects/ui/src/app/pages/server-routes/email/email.page.scss create mode 100644 frontend/projects/ui/src/app/pages/server-routes/email/email.page.ts rename frontend/projects/ui/src/app/pages/server-routes/wifi/{wifiSpec.ts => wifi.const.ts} (81%) create mode 100644 frontend/projects/ui/src/app/util/configBuilderToSpec.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3ae07974f..ea921b697 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -27,11 +27,12 @@ "@ng-web-apis/resize-observer": "^2.0.0", "@start9labs/argon2": "^0.1.0", "@start9labs/emver": "^0.1.5", - "@taiga-ui/addon-charts": "3.26.0", - "@taiga-ui/cdk": "3.26.0", - "@taiga-ui/core": "3.26.0", - "@taiga-ui/icons": "3.26.0", - "@taiga-ui/kit": "3.26.0", + "@start9labs/start-sdk": "git+https://github.com/Start9Labs/start-sdk#9a23967a7a9c529b27868ca3d7628d271bfb38af", + "@taiga-ui/addon-charts": "3.27.0", + "@taiga-ui/cdk": "3.27.0", + "@taiga-ui/core": "3.27.0", + "@taiga-ui/icons": "3.27.0", + "@taiga-ui/kit": "3.27.0", "angular-svg-round-progressbar": "^9.0.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", @@ -53,7 +54,6 @@ "patch-db-client": "file: ../../../patch-db/client", "pbkdf2": "^3.1.2", "rxjs": "^7.5.6", - "start-sdk": "^0.4.0-lib0.rc3", "swiper": "^8.2.4", "ts-matches": "^5.2.1", "tslib": "^2.3.0", @@ -3830,6 +3830,25 @@ "resolved": "https://registry.npmjs.org/@start9labs/emver/-/emver-0.1.5.tgz", "integrity": "sha512-1dhiG03VkfEwSLx/JPKVms6srAbYFQgwfSGhwpUKMDliMXuAHGVaueStmqzVxn3JpH/HEVz0QW8w/PXHqjdiIg==" }, + "node_modules/@start9labs/start-sdk": { + "version": "0.4.0-rev0.lib0.rc2", + "resolved": "git+ssh://git@github.com/Start9Labs/start-sdk.git#9a23967a7a9c529b27868ca3d7628d271bfb38af", + "integrity": "sha512-P2EkO20hRszt2f/PdhsdRnNe3g0RG96RIV7n38htsVBouHOy/j4QZ1naBvWTuPOKOjdwb3Sbk5haq/FT10JPqw==", + "license": "MIT", + "dependencies": { + "@iarna/toml": "^2.2.5", + "ts-matches": "^5.4.1", + "yaml": "^2.2.2" + } + }, + "node_modules/@start9labs/start-sdk/node_modules/yaml": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.2.tgz", + "integrity": "sha512-CBKFWExMn46Foo4cldiChEzn7S7SRV+wqiluAb6xmueD/fGyRHIhX8m14vVGgeFWjN540nKCNVj6P21eQjgTuA==", + "engines": { + "node": ">= 14" + } + }, "node_modules/@stencil/core": { "version": "2.22.3", "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.22.3.tgz", @@ -3843,9 +3862,9 @@ } }, "node_modules/@taiga-ui/addon-charts": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-3.26.0.tgz", - "integrity": "sha512-nkAzI+B4CcPogUrpEwANu3D8n3cJzuIakF//8MyOzxvg0S4olpL81t9/Mx4+zyXxqjVTaU8q2a/rJNaV+7SyRg==", + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/addon-charts/-/addon-charts-3.27.0.tgz", + "integrity": "sha512-PZMwRl8pcbF1UcRXzrnzF6rcdg6ZMHSdiF7Q2VUO8Q39GFguyYNYIFdkRHOLvh1wbsXQKoSxho72RN2yeEybCA==", "dependencies": { "tslib": ">=2.0.0" }, @@ -3853,15 +3872,15 @@ "@angular/common": ">=12.0.0", "@angular/core": ">=12.0.0", "@ng-web-apis/common": ">=2.0.0", - "@taiga-ui/cdk": ">=3.26.0", - "@taiga-ui/core": ">=3.26.0", + "@taiga-ui/cdk": ">=3.27.0", + "@taiga-ui/core": ">=3.27.0", "@tinkoff/ng-polymorpheus": ">=4.0.0" } }, "node_modules/@taiga-ui/cdk": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-3.26.0.tgz", - "integrity": "sha512-vd2CMQ/Z6bhzCQSBSHjSoCIJEE2g4RKmjl3RBK/OdA/L46s9/nQS8oTRBG8I0zk8lNx7YHqqC6u9IY6BZgOeAg==", + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/cdk/-/cdk-3.27.0.tgz", + "integrity": "sha512-53XLDaQzStpjTV7a4X8658YVlaG7bp1JG4cgIamexylXwkWdsHa9o9KnFFOgsGO5I7heiQ2+kotKPWg7sgUwuQ==", "dependencies": { "@ng-web-apis/common": "2.1.0", "@ng-web-apis/mutation-observer": "2.0.0", @@ -3871,7 +3890,7 @@ "tslib": "2.5.0" }, "optionalDependencies": { - "ng-morph": "2.2.0", + "ng-morph": "2.2.4", "parse5": "6.0.1" }, "peerDependencies": { @@ -3883,11 +3902,11 @@ } }, "node_modules/@taiga-ui/core": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-3.26.0.tgz", - "integrity": "sha512-+IYn0ssZ3dO8Cm1HYAtbL5t+dvhp0RVzljdS72HBcr7IsnEhr2UDWWvsLv4DqsG4tXigWq6sL9wjXqg6/ylH4g==", + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/core/-/core-3.27.0.tgz", + "integrity": "sha512-kXODpMjhxR+4YcdEFVpVaC++G7scMCSuSKPuXXoOCWtEZsQTp/pvSCCxcg951/lLRyh0MkzvEHyz7a8BKikgog==", "dependencies": { - "@taiga-ui/i18n": "^3.26.0", + "@taiga-ui/i18n": "^3.27.0", "tslib": ">=2.0.0" }, "peerDependencies": { @@ -3899,17 +3918,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.26.0", - "@taiga-ui/i18n": ">=3.26.0", + "@taiga-ui/cdk": ">=3.27.0", + "@taiga-ui/i18n": ">=3.27.0", "@tinkoff/ng-event-plugins": ">=3.1.0", "@tinkoff/ng-polymorpheus": ">=4.0.0", "rxjs": ">=6.0.0" } }, "node_modules/@taiga-ui/i18n": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-3.26.0.tgz", - "integrity": "sha512-pI8IIQPYe3I7f/HQ4prCNpttEzwR1VA6ooJoaygVcSQDS8KVr03yyl9RBUzKpl57vnemuduVdfqM9LxX4bPeWQ==", + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/i18n/-/i18n-3.27.0.tgz", + "integrity": "sha512-orOoo4CeecBc4GVMFcMhwvYo83wsudgtbnEbmFecE2NZO3wdntjOGE/TNpVM28JinO3uL5yabgDTd3UaxK6NSw==", "dependencies": { "tslib": ">=2.0.0" }, @@ -3919,18 +3938,21 @@ } }, "node_modules/@taiga-ui/icons": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-3.26.0.tgz", - "integrity": "sha512-q42C7LYqmOEf1P6GZPl6we5YZe9dboke4kNmbSYxWMT1EWCsgPWK8QmK02BsDeltUwSp7cnCP7jGZG1lkbuzKg==", + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/icons/-/icons-3.27.0.tgz", + "integrity": "sha512-uXMe4B3cMgJ1qLfezsrOxvsHD9Bw6y39921GFMvlpeIwSEnXMc/rn1wEQpyd6Qo1Ib9AfFWHRDhBa7NPGnXllA==", "dependencies": { "tslib": "^2.2.0" } }, "node_modules/@taiga-ui/kit": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-3.26.0.tgz", - "integrity": "sha512-Sdp9FKSi/+C2PgirSLr03YQNyboewhFOaFRtT6cBXzscHJLfTWLSv6nNq1kMDLueVTtuPJjksAXsHj+fpnWIiQ==", + "version": "3.27.0", + "resolved": "https://registry.npmjs.org/@taiga-ui/kit/-/kit-3.27.0.tgz", + "integrity": "sha512-2YYiku5wXCr1XeqZHnOgLTH4o3rW3EsCx5O8FRSy2LCtkGFLfLemV7E8x1WQqYzOlTW7cCa2goo+K1NMrUWfMQ==", "dependencies": { + "@maskito/angular": "0.11.1", + "@maskito/core": "0.11.1", + "@maskito/kit": "0.11.1", "@ng-web-apis/intersection-observer": "3.0.0", "text-mask-core": "5.1.2", "tslib": ">=2.0.0" @@ -3943,13 +3965,41 @@ "@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.26.0", - "@taiga-ui/core": ">=3.26.0", - "@taiga-ui/i18n": ">=3.26.0", + "@taiga-ui/cdk": ">=3.27.0", + "@taiga-ui/core": ">=3.27.0", + "@taiga-ui/i18n": ">=3.27.0", "@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==", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": ">=12.0.0", + "@angular/core": ">=12.0.0", + "@angular/forms": ">=12.0.0", + "@maskito/core": "^0.11.1", + "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==" + }, + "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==", + "peerDependencies": { + "@maskito/core": "^0.11.1" + } + }, "node_modules/@tinkoff/ng-event-plugins": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@tinkoff/ng-event-plugins/-/ng-event-plugins-3.1.0.tgz", @@ -6277,6 +6327,7 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -10313,9 +10364,9 @@ } }, "node_modules/ng-morph": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ng-morph/-/ng-morph-2.2.0.tgz", - "integrity": "sha512-0CEswQ+QrxPBWv1dBBu/N6idk0wIXkdFmqk+GW55/Ta7DJTKMCPZLVGXpp+Lia9XF55vVyxnOBw9J3QNN2Dv5A==", + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/ng-morph/-/ng-morph-2.2.4.tgz", + "integrity": "sha512-4AIsjcvUAT6htnX56DsUPZDQuNhWxmi09exUS6TreD6hKghGuqT3QfRf+K9aFw1FJyCsLsh/0py3S/sMtarsIA==", "optional": true, "dependencies": { "jsonc-parser": "3.0.0", @@ -13796,26 +13847,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/start-sdk": { - "version": "0.4.0-lib0.rc3", - "resolved": "https://registry.npmjs.org/start-sdk/-/start-sdk-0.4.0-lib0.rc3.tgz", - "integrity": "sha512-PAExAKEw0AUhk0UYu25o/UfAwclLt8tvQIDqzv4MaiFg4stPSzWYyFFBBX2kIKlBDlIMlzC6Fj0/8qoxzqq8iQ==", - "dependencies": { - "@iarna/toml": "^2.2.5", - "deepmerge": "^4.3.1", - "lodash": "^4.17.21", - "ts-matches": "^5.4.1", - "yaml": "^2.2.1" - } - }, - "node_modules/start-sdk/node_modules/yaml": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.2.1.tgz", - "integrity": "sha512-e0WHiYql7+9wr4cWMx3TVQrNwejKaEe7/rHNmQmqRjazfOP5W8PB6Jpebb5o6fIapbz9o9+2ipcaTM2ZwDI6lw==", - "engines": { - "node": ">= 14" - } - }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 79fbac137..58e92d6cf 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -52,11 +52,11 @@ "@ng-web-apis/resize-observer": "^2.0.0", "@start9labs/argon2": "^0.1.0", "@start9labs/emver": "^0.1.5", - "@taiga-ui/addon-charts": "3.26.0", - "@taiga-ui/cdk": "3.26.0", - "@taiga-ui/core": "3.26.0", - "@taiga-ui/icons": "3.26.0", - "@taiga-ui/kit": "3.26.0", + "@taiga-ui/addon-charts": "3.27.0", + "@taiga-ui/cdk": "3.27.0", + "@taiga-ui/core": "3.27.0", + "@taiga-ui/icons": "3.27.0", + "@taiga-ui/kit": "3.27.0", "angular-svg-round-progressbar": "^9.0.0", "ansi-to-html": "^0.7.2", "base64-js": "^1.5.1", @@ -78,7 +78,7 @@ "patch-db-client": "file: ../../../patch-db/client", "pbkdf2": "^3.1.2", "rxjs": "^7.5.6", - "start-sdk": "^0.4.0-lib0.rc3", + "@start9labs/start-sdk": "git+https://github.com/Start9Labs/start-sdk#9a23967a7a9c529b27868ca3d7628d271bfb38af", "swiper": "^8.2.4", "ts-matches": "^5.2.1", "tslib": "^2.3.0", diff --git a/frontend/projects/ui/src/app/app-routing.module.ts b/frontend/projects/ui/src/app/app-routing.module.ts index bf6d7d9ce..cf4883108 100644 --- a/frontend/projects/ui/src/app/app-routing.module.ts +++ b/frontend/projects/ui/src/app/app-routing.module.ts @@ -63,15 +63,6 @@ const routes: Routes = [ m => m.AppsRoutingModule, ), }, - { - path: 'developer', - canActivate: [AuthGuard], - canActivateChild: [AuthGuard], - loadChildren: () => - import('./pages/developer-routes/developer-routing.module').then( - m => m.DeveloperRoutingModule, - ), - }, { path: 'backups', canActivate: [AuthGuard], diff --git a/frontend/projects/ui/src/app/app.module.ts b/frontend/projects/ui/src/app/app.module.ts index 2df574618..2e4bd2d2f 100644 --- a/frontend/projects/ui/src/app/app.module.ts +++ b/frontend/projects/ui/src/app/app.module.ts @@ -22,7 +22,6 @@ import { AppComponent } from './app.component' import { AppRoutingModule } from './app-routing.module' import { OSWelcomePageModule } from './modals/os-welcome/os-welcome.module' import { GenericInputComponentModule } from './modals/generic-input/generic-input.component.module' -import { GenericFormPageModule } from './modals/generic-form/generic-form.module' import { MarketplaceModule } from './marketplace.module' import { PreloaderModule } from './app/preloader/preloader.module' import { FooterModule } from './app/footer/footer.module' @@ -54,7 +53,6 @@ import { FormPageModule } from './modals/form/form.module' OSWelcomePageModule, MarkdownModule, GenericInputComponentModule, - GenericFormPageModule, MonacoEditorModule, SharedPipesModule, MarketplaceModule, 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 5cb33fe0e..2dbb23e27 100644 --- a/frontend/projects/ui/src/app/app/preloader/preloader.component.ts +++ b/frontend/projects/ui/src/app/app/preloader/preloader.component.ts @@ -3,6 +3,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core' // TODO: Turn into DI token if this is needed someplace else too const ICONS = [ 'add', + 'alarm-outline', 'alert-outline', 'alert-circle-outline', 'aperture-outline', diff --git a/frontend/projects/ui/src/app/components/form-object/form-label/form-label.component.html b/frontend/projects/ui/src/app/components/form-object/form-label/form-label.component.html deleted file mode 100644 index 3bf9a7951..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-label/form-label.component.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - -{{ data.name }} - - (New Options) - (Edited) - - * diff --git a/frontend/projects/ui/src/app/components/form-object/form-label/form-label.component.scss b/frontend/projects/ui/src/app/components/form-object/form-label/form-label.component.scss deleted file mode 100644 index 9bd13bdd4..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-label/form-label.component.scss +++ /dev/null @@ -1,11 +0,0 @@ -:host { - display: flex; - align-items: center; - font-weight: bold; -} - -.icon { - --padding-start: 0; - --padding-end: 4px; - margin-right: 4px; -} diff --git a/frontend/projects/ui/src/app/components/form-object/form-label/form-label.component.ts b/frontend/projects/ui/src/app/components/form-object/form-label/form-label.component.ts deleted file mode 100644 index a798660ce..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-label/form-label.component.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { AlertController } from '@ionic/angular' - -@Component({ - selector: 'form-label', - templateUrl: './form-label.component.html', - styleUrls: ['./form-label.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class FormLabelComponent { - @Input() data!: { - name: string - description: string | null - edited?: boolean - required?: boolean - newOptions?: boolean - } - - constructor(private readonly alertCtrl: AlertController) {} - - async presentAlertDescription() { - const alert = await this.alertCtrl.create({ - header: this.data.name, - message: this.data.description || '', - buttons: [ - { - text: 'OK', - cssClass: 'enter-click', - }, - ], - }) - await alert.present() - } -} diff --git a/frontend/projects/ui/src/app/components/form-object/form-object.module.ts b/frontend/projects/ui/src/app/components/form-object/form-object.module.ts deleted file mode 100644 index 9741114ec..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-object.module.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { CommonModule } from '@angular/common' -import { NgModule } from '@angular/core' -import { FormsModule, ReactiveFormsModule } from '@angular/forms' -import { IonicModule } from '@ionic/angular' -import { SharedPipesModule } from '@start9labs/shared' -import { TuiElasticContainerModule } from '@taiga-ui/kit' -import { TuiExpandModule } from '@taiga-ui/core' -import { FormLabelComponent } from './form-label/form-label.component' -import { FormObjectComponent } from './form-object/form-object.component' -import { FormUnionComponent } from './form-union/form-union.component' -import { - GetErrorPipe, - ToWarningTextPipe, - ToElementIdPipe, - ToRangePipe, -} from './form-object.pipes' -import { FormFileComponent } from './form-object/controls/form-file/form-file.component' -import { FormInputComponent } from './form-object/controls/form-input/form-input.component' -import { FormWarningDirective } from './form-warning.directive' -import { FormSubformComponent } from './form-object/controls/form-subform/form-subform.component' -import { FormSelectComponent } from './form-object/controls/form-select/form-select.component' - -@NgModule({ - declarations: [ - FormObjectComponent, - FormUnionComponent, - FormLabelComponent, - ToWarningTextPipe, - GetErrorPipe, - ToElementIdPipe, - ToRangePipe, - FormWarningDirective, - FormFileComponent, - FormInputComponent, - FormSubformComponent, - FormSelectComponent, - ], - imports: [ - CommonModule, - IonicModule, - FormsModule, - ReactiveFormsModule, - SharedPipesModule, - TuiElasticContainerModule, - TuiExpandModule, - ], - exports: [FormObjectComponent, FormLabelComponent], -}) -export class FormObjectModule {} diff --git a/frontend/projects/ui/src/app/components/form-object/form-object.pipes.ts b/frontend/projects/ui/src/app/components/form-object/form-object.pipes.ts deleted file mode 100644 index 43a6fd98b..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-object.pipes.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Pipe, PipeTransform } from '@angular/core' -import { ValidationErrors } from '@angular/forms' -import { IonicSafeString } from '@ionic/angular' -import { Range } from 'src/app/util/config-utilities' -import { getElementId } from './form-object/form-object.component' - -@Pipe({ - name: 'getError', -}) -export class GetErrorPipe implements PipeTransform { - transform( - errors: ValidationErrors, - patternDesc: string = 'Invalid pattern', - ): string { - if (errors['required']) { - return 'Required' - } else if (errors['pattern']) { - return patternDesc - } else if (errors['notNumber']) { - return 'Must be a number' - } else if (errors['numberNotInteger']) { - return 'Must be an integer' - } else if (errors['numberNotInRange']) { - return errors['numberNotInRange'].value - } else if (errors['listNotUnique']) { - return errors['listNotUnique'].value - } else if (errors['listNotInRange']) { - return errors['listNotInRange'].value - } else if (errors['listItemIssue']) { - return errors['listItemIssue'].value - } else { - return 'Unknown error' - } - } -} - -@Pipe({ - name: 'toWarningText', -}) -export class ToWarningTextPipe implements PipeTransform { - transform(text?: string | null): IonicSafeString | string { - return text - ? new IonicSafeString(`${text}`) - : '' - } -} - -@Pipe({ - name: 'toRange', -}) -export class ToRangePipe implements PipeTransform { - transform(range?: string): Range { - return Range.from(range) - } -} - -@Pipe({ - name: 'toElementId', -}) -export class ToElementIdPipe implements PipeTransform { - transform(objectId: string, key: string, index = 0): string { - return getElementId(objectId, key, index) - } -} diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-file/form-file.component.html b/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-file/form-file.component.html deleted file mode 100644 index 9a06542f2..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-file/form-file.component.html +++ /dev/null @@ -1,40 +0,0 @@ - - -
- - Browse... - - - -
-

{{ control.value.name }}

-
- -
-
-
-
-
-

- - {{ errors | getError }} - -

diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-file/form-file.component.scss b/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-file/form-file.component.scss deleted file mode 100644 index 716a14c66..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-file/form-file.component.scss +++ /dev/null @@ -1,26 +0,0 @@ -:host { - display: block; -} - -ion-item-divider { - text-transform: unset; - border-bottom: 1px solid - var( - --ion-item-border-color, - var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13))) - ); - - --padding-top: 18px; - --padding-start: 0; - - &.error-border { - border-color: var(--ion-color-danger-shade); - --border-color: var(--ion-color-danger-shade); - } -} - -.error-message { - margin-top: 2px; - font-size: small; - color: var(--ion-color-danger); -} diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-file/form-file.component.ts b/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-file/form-file.component.ts deleted file mode 100644 index d83c46838..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-file/form-file.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Component, Input } from '@angular/core' -import { AbstractControl } from '@angular/forms' -import { ValueSpecOf } from 'start-sdk/lib/config/configTypes' - -@Component({ - selector: 'form-file', - templateUrl: './form-file.component.html', - styleUrls: ['./form-file.component.scss'], -}) -export class FormFileComponent { - @Input() spec!: ValueSpecOf<'file'> - @Input() control!: AbstractControl - - handleFileInput(e: any) { - this.control.patchValue(e.target.files[0]) - } - - clearFile() { - this.control.patchValue(null) - } -} diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-input/form-input.component.html b/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-input/form-input.component.html deleted file mode 100644 index 3112793b5..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-input/form-input.component.html +++ /dev/null @@ -1,65 +0,0 @@ - - - - - - - - - - - {{ spec.units }} - - -

- - {{ errors | getError : $any(spec).patternDescription }} - -

diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-input/form-input.component.scss b/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-input/form-input.component.scss deleted file mode 100644 index 4b6f0774d..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-input/form-input.component.scss +++ /dev/null @@ -1,24 +0,0 @@ -.label { - margin: 16px 0 6px; -} - -.input { - font-family: 'Courier New'; - font-weight: bold; - - --placeholder-font-weight: 400; - - &_redacted { - font-family: 'Redacted'; - } -} - -.units { - font-size: medium; -} - -.error-message { - margin-top: 2px; - font-size: small; - color: var(--ion-color-danger); -} diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-input/form-input.component.ts b/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-input/form-input.component.ts deleted file mode 100644 index 03bd41d00..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-input/form-input.component.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Component, Input, inject, Output, EventEmitter } from '@angular/core' -import { FormControl } from '@angular/forms' -import { ValueSpecOf } from 'start-sdk/lib/config/configTypes' -import { THEME } from '@start9labs/shared' - -@Component({ - selector: 'form-input', - templateUrl: './form-input.component.html', - styleUrls: ['./form-input.component.scss'], -}) -export class FormInputComponent { - @Input() name!: string - @Input() spec!: ValueSpecOf<'text' | 'textarea' | 'number'> - @Input() control!: FormControl - - @Output() onInputChange = new EventEmitter() - - unmasked = false - - readonly theme$ = inject(THEME) -} diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-select/form-select.component.html b/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-select/form-select.component.html deleted file mode 100644 index ba236e1f1..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-select/form-select.component.html +++ /dev/null @@ -1,49 +0,0 @@ - - - - - - - - - {{ option.value }} - - - -

- - {{ errors | getError }} - -

diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-select/form-select.component.scss b/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-select/form-select.component.scss deleted file mode 100644 index f03aa603a..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-select/form-select.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -.error-message { - margin-top: 2px; - font-size: small; - color: var(--ion-color-danger); -} diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-select/form-select.component.ts b/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-select/form-select.component.ts deleted file mode 100644 index 0f714e810..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-select/form-select.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component, Input } from '@angular/core' -import { FormControl } from '@angular/forms' -import { ValueSpecOf } from 'start-sdk/lib/config/configTypes' - -@Component({ - selector: 'form-select', - templateUrl: './form-select.component.html', - styleUrls: ['./form-select.component.scss'], -}) -export class FormSelectComponent { - @Input() spec!: ValueSpecOf<'toggle' | 'select' | 'multiselect'> - @Input() control!: FormControl - @Input() name!: string - - cancelBool = () => this.control.setValue(!this.control.value) -} diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-subform/form-subform.component.html b/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-subform/form-subform.component.html deleted file mode 100644 index 6afcea097..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-subform/form-subform.component.html +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-subform/form-subform.component.scss b/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-subform/form-subform.component.scss deleted file mode 100644 index de5ee20dc..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-subform/form-subform.component.scss +++ /dev/null @@ -1,25 +0,0 @@ -ion-item-divider { - cursor: pointer; - text-transform: unset; - border-bottom: 1px solid - var( - --ion-item-border-color, - var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13))) - ); - - --padding-top: 18px; - --padding-start: 0; - - &.error-border { - border-color: var(--ion-color-danger-shade); - --border-color: var(--ion-color-danger-shade); - } -} - -.icon { - transition: transform 0.42s ease-out; - - &_rotated { - transform: rotate(180deg); - } -} diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-subform/form-subform.component.ts b/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-subform/form-subform.component.ts deleted file mode 100644 index 51fb9ef1a..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-object/controls/form-subform/form-subform.component.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Component, Input, Output, EventEmitter } from '@angular/core' -import { AbstractControl } from '@angular/forms' -import { ValueSpecOf } from 'start-sdk/lib/config/configTypes' - -@Component({ - selector: 'form-subform', - templateUrl: './form-subform.component.html', - styleUrls: ['./form-subform.component.scss'], -}) -export class FormSubformComponent { - @Input() spec!: ValueSpecOf<'object'> - @Input() control!: AbstractControl - @Input() hasNewOptions = false - - expanded = false -} diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/form-object.component.html b/frontend/projects/ui/src/app/components/form-object/form-object/form-object.component.html deleted file mode 100644 index 40f534ddc..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-object/form-object.component.html +++ /dev/null @@ -1,198 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - Add - - -

- - {{ errors | getError }} - -

- -
-
- - - - - - - - - - -
- - - Delete - -
-
-
- -
- - - - - - -

- - {{ errors | getError : $any(spec).patternDescription }} - -

-
-
-
-
-
-
-
-
diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/form-object.component.scss b/frontend/projects/ui/src/app/components/form-object/form-object/form-object.component.scss deleted file mode 100644 index 46b838de4..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-object/form-object.component.scss +++ /dev/null @@ -1,41 +0,0 @@ -:host { - display: block; -} - -.input { - font-family: 'Courier New'; - font-weight: bold; - - --placeholder-font-weight: 400; - - &_redacted { - font-family: 'Redacted'; - } -} - -ion-item-divider { - text-transform: unset; - border-bottom: 1px solid - var( - --ion-item-border-color, - var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13))) - ); - - --padding-top: 18px; - --padding-start: 0; - - &.error-border { - border-color: var(--ion-color-danger-shade); - --border-color: var(--ion-color-danger-shade); - } -} - -.nested-wrapper { - padding: 0 0 16px 24px; -} - -.error-message { - margin-top: 2px; - font-size: small; - color: var(--ion-color-danger); -} diff --git a/frontend/projects/ui/src/app/components/form-object/form-object/form-object.component.ts b/frontend/projects/ui/src/app/components/form-object/form-object/form-object.component.ts deleted file mode 100644 index ecbc8a728..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-object/form-object.component.ts +++ /dev/null @@ -1,253 +0,0 @@ -import { - Component, - Input, - Output, - EventEmitter, - Inject, - inject, - SimpleChanges, -} from '@angular/core' -import { UntypedFormArray, UntypedFormGroup } from '@angular/forms' -import { AlertButton, AlertController } from '@ionic/angular' -import { - InputSpec, - ListValueSpecOf, - ValueSpec, - ValueSpecToggle, - ValueSpecList, - ValueSpecUnion, -} from 'start-sdk/lib/config/configTypes' -import { FormService } from 'src/app/services/form.service' -import { THEME, pauseFor } from '@start9labs/shared' -import { v4 } from 'uuid' -import { DOCUMENT } from '@angular/common' - -const Mustache = require('mustache') - -@Component({ - selector: 'form-object', - templateUrl: './form-object.component.html', - styleUrls: ['./form-object.component.scss'], -}) -export class FormObjectComponent { - @Input() objectSpec!: InputSpec - @Input() formGroup!: UntypedFormGroup - @Input() current?: Record - @Input() original?: Record - @Output() onInputChange = new EventEmitter() - @Output() hasNewOptions = new EventEmitter() - warningAck: { [key: string]: boolean } = {} - objectDisplay: { - [key: string]: { expanded: boolean; hasNewOptions: boolean } - } = {} - objectListDisplay: { - [key: string]: { expanded: boolean; displayAs: string }[] - } = {} - objectId = v4() - - readonly theme$ = inject(THEME) - - constructor( - private readonly alertCtrl: AlertController, - private readonly formService: FormService, - @Inject(DOCUMENT) private readonly document: Document, - ) {} - - ngOnInit() { - this.setDisplays() - - // setTimeout hack to avoid ExpressionChangedAfterItHasBeenCheckedError - // setTimeout(() => { - // if (this.original && Object.values(this.objectSpec).some(spec => spec['is-new'])) this.hasNewOptions.emit() - // }) - } - - ngOnChanges(changes: SimpleChanges) { - const specChanges = changes['objectSpec'] - - if (!specChanges) return - - if ( - !specChanges.firstChange && - Object.keys({ - ...specChanges.previousValue, - ...specChanges.currentValue, - }).length !== Object.keys(specChanges.previousValue).length - ) { - this.setDisplays() - } - } - - private setDisplays() { - Object.keys(this.objectSpec).forEach(key => { - const spec = this.objectSpec[key] - - if (spec.type === 'list' && spec.spec.type === 'object') { - this.objectListDisplay[key] = [] - this.formGroup.get(key)?.value.forEach((obj: any, index: number) => { - const displayAs = (spec.spec as ListValueSpecOf<'object'>).displayAs - this.objectListDisplay[key][index] = { - expanded: false, - displayAs: displayAs - ? (Mustache as any).render(displayAs, obj) - : '', - } - }) - } else if (spec.type === 'object') { - this.objectDisplay[key] = { - expanded: false, - hasNewOptions: false, - } - } - }) - } - - addListItemWrapper( - key: string, - spec: T extends ValueSpecUnion ? never : T, - ) { - this.presentAlertChangeWarning(key, spec, () => this.addListItem(key)) - } - - toggleExpandObject(key: string) { - this.objectDisplay[key].expanded = !this.objectDisplay[key].expanded - } - - toggleExpandListObject(key: string, i: number) { - this.objectListDisplay[key][i].expanded = - !this.objectListDisplay[key][i].expanded - } - - updateLabel(key: string, i: number, displayAs: string) { - this.objectListDisplay[key][i].displayAs = displayAs - ? Mustache.render(displayAs, this.formGroup.get(key)?.value[i]) - : '' - } - - handleInputChange() { - this.onInputChange.emit() - } - - setHasNew(key: string) { - this.hasNewOptions.emit() - setTimeout(() => { - this.objectDisplay[key].hasNewOptions = true - }) - } - - handleBooleanChange(key: string, spec: ValueSpecToggle) { - if (spec.warning) { - const current = this.formGroup.get(key)?.value - const cancelFn = () => this.formGroup.get(key)?.setValue(!current) - this.presentAlertChangeWarning(key, spec, undefined, cancelFn) - } - } - - async presentAlertChangeWarning( - key: string, - spec: T extends ValueSpecUnion ? never : T, - okFn?: Function, - cancelFn?: Function, - ) { - if (!spec.warning || this.warningAck[key]) return okFn ? okFn() : null - this.warningAck[key] = true - - const buttons: AlertButton[] = [ - { - text: 'Ok', - handler: () => { - if (okFn) okFn() - }, - cssClass: 'enter-click', - }, - ] - - if (okFn || cancelFn) { - buttons.unshift({ - text: 'Cancel', - handler: () => { - if (cancelFn) cancelFn() - }, - }) - } - - const alert = await this.alertCtrl.create({ - header: 'Warning', - subHeader: `Editing ${spec.name} has consequences:`, - message: spec.warning, - buttons, - }) - await alert.present() - } - - async presentAlertDelete(key: string, index: number) { - const alert = await this.alertCtrl.create({ - header: 'Confirm', - message: 'Are you sure you want to delete this entry?', - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Delete', - handler: () => { - this.deleteListItem(key, index) - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() - } - - private addListItem(key: string): void { - const arr = this.formGroup.get(key) as UntypedFormArray - const listSpec = this.objectSpec[key] as ValueSpecList - const newItem = this.formService.getListItem(listSpec, undefined)! - - const index = arr.length - arr.insert(index, newItem) - - if (listSpec.spec.type === 'object') { - const displayAs = listSpec.spec.displayAs - this.objectListDisplay[key].push({ - expanded: false, - displayAs: displayAs ? Mustache.render(displayAs, newItem.value) : '', - }) - } - - setTimeout(() => { - const element = this.document.getElementById( - getElementId(this.objectId, key, index), - ) - element?.parentElement?.scrollIntoView({ behavior: 'smooth' }) - - if (listSpec.spec.type === 'object') { - pauseFor(250).then(() => this.toggleExpandListObject(key, index)) - } - }, 100) - - arr.markAsDirty() - } - - private deleteListItem(key: string, index: number, markDirty = true): void { - // if (this.objectListDisplay[key]) - // this.objectListDisplay[key][index].height = '0px' - const arr = this.formGroup.get(key) as UntypedFormArray - if (markDirty) arr.markAsDirty() - pauseFor(250).then(() => { - if (this.objectListDisplay[key]) - this.objectListDisplay[key].splice(index, 1) - arr.removeAt(index) - }) - } - - asIsOrder() { - return 0 - } -} - -export function getElementId(objectId: string, key: string, index = 0): string { - return `${key}-${index}-${objectId}` -} diff --git a/frontend/projects/ui/src/app/components/form-object/form-union/form-union.component.html b/frontend/projects/ui/src/app/components/form-object/form-union/form-union.component.html deleted file mode 100644 index b75d8d364..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-union/form-union.component.html +++ /dev/null @@ -1,47 +0,0 @@ -
- - - - - - - {{ spec.variants[option.key].name }} - - - -

- - {{ errors | getError }} - -

- - - - -
diff --git a/frontend/projects/ui/src/app/components/form-object/form-union/form-union.component.scss b/frontend/projects/ui/src/app/components/form-object/form-union/form-union.component.scss deleted file mode 100644 index 71943c417..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-union/form-union.component.scss +++ /dev/null @@ -1,30 +0,0 @@ -:host { - display: block; -} - -ion-item-divider { - text-transform: unset; - border-bottom: 1px solid - var( - --ion-item-border-color, - var(--ion-border-color, var(--ion-color-step-150, rgba(0, 0, 0, 0.13))) - ); - - --padding-top: 18px; - --padding-start: 0; - - &.error-border { - border-color: var(--ion-color-danger-shade); - --border-color: var(--ion-color-danger-shade); - } -} - -.indent { - margin-left: 24px; -} - -.error-message { - margin-top: 2px; - font-size: small; - color: var(--ion-color-danger); -} diff --git a/frontend/projects/ui/src/app/components/form-object/form-union/form-union.component.ts b/frontend/projects/ui/src/app/components/form-object/form-union/form-union.component.ts deleted file mode 100644 index 86ac6ad74..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-union/form-union.component.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { ChangeDetectionStrategy, Component, Input } from '@angular/core' -import { AbstractControl, UntypedFormGroup } from '@angular/forms' -import { v4 } from 'uuid' -import { FormService } from 'src/app/services/form.service' -import { - ValueSpecUnion, - InputSpec, - unionSelectKey, -} from 'start-sdk/lib/config/configTypes' - -@Component({ - selector: 'form-union', - templateUrl: './form-union.component.html', - styleUrls: ['./form-union.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class FormUnionComponent { - readonly unionSelectKey = unionSelectKey - - @Input() formGroup!: UntypedFormGroup - @Input() spec!: ValueSpecUnion - @Input() current?: Record - @Input() original?: Record - - get unionControl(): AbstractControl | null { - return this.formGroup.get(unionSelectKey) - } - - get selectedVariant(): string { - return this.unionControl?.value || '' - } - - get variantName(): string { - return this.spec.variants[this.selectedVariant]?.name || '' - } - - get variantSpec(): InputSpec { - return this.spec.variants[this.selectedVariant]?.spec || {} - } - - get hasNewOptions(): boolean { - // return Object.values(this.variantSpec).some(spec => spec['is-new']) - return false - } - - objectId = v4() - - constructor(private readonly formService: FormService) {} - - updateUnion(e: any): void { - Object.keys(this.formGroup.controls).forEach(control => { - if (control === unionSelectKey) return - this.formGroup.removeControl(control) - }) - - const unionGroup = this.formService.getUnionObject( - this.spec as ValueSpecUnion, - e.detail.value, - ) - - Object.keys(unionGroup.controls).forEach(control => { - if (control === unionSelectKey) return - this.formGroup.addControl(control, unionGroup.controls[control]) - }) - } -} diff --git a/frontend/projects/ui/src/app/components/form-object/form-warning.directive.ts b/frontend/projects/ui/src/app/components/form-object/form-warning.directive.ts deleted file mode 100644 index b05a617c4..000000000 --- a/frontend/projects/ui/src/app/components/form-object/form-warning.directive.ts +++ /dev/null @@ -1,51 +0,0 @@ -import { Directive } from '@angular/core' -import { ValueSpec, ValueSpecUnion } from 'start-sdk/lib/config/configTypes' -import { AlertButton, AlertController } from '@ionic/angular' - -@Directive({ - selector: '[formWarning]', - exportAs: 'formWarning', -}) -export class FormWarningDirective { - private warned = false - - constructor(private readonly alertCtrl: AlertController) {} - - async onChange( - key: string, - spec: T extends ValueSpecUnion ? never : T, - okFn?: Function, - cancelFn?: Function, - ) { - if (!spec.warning || this.warned) return okFn ? okFn() : null - - this.warned = true - - const buttons: AlertButton[] = [ - { - text: 'Ok', - handler: () => { - if (okFn) okFn() - }, - cssClass: 'enter-click', - }, - ] - - if (okFn || cancelFn) { - buttons.unshift({ - text: 'Cancel', - handler: () => { - if (cancelFn) cancelFn() - }, - }) - } - - const alert = await this.alertCtrl.create({ - header: 'Warning', - subHeader: `Editing ${spec.name} has consequences:`, - message: spec.warning, - buttons, - }) - await alert.present() - } -} diff --git a/frontend/projects/ui/src/app/components/form/control.ts b/frontend/projects/ui/src/app/components/form/control.ts index a945f8e56..74756e3f5 100644 --- a/frontend/projects/ui/src/app/components/form/control.ts +++ b/frontend/projects/ui/src/app/components/form/control.ts @@ -1,6 +1,6 @@ import { inject } from '@angular/core' import { FormControlComponent } from './form-control/form-control.component' -import { ValueSpec } from 'start-sdk/lib/config/configTypes' +import { ValueSpec } from '@start9labs/start-sdk/lib/config/configTypes' export abstract class Control { private readonly control: FormControlComponent = diff --git a/frontend/projects/ui/src/app/components/form/form-array/form-array.component.ts b/frontend/projects/ui/src/app/components/form/form-array/form-array.component.ts index a07c8cf7b..b426cc40c 100644 --- a/frontend/projects/ui/src/app/components/form/form-array/form-array.component.ts +++ b/frontend/projects/ui/src/app/components/form/form-array/form-array.component.ts @@ -9,7 +9,7 @@ import { } from '@taiga-ui/core' import { TUI_PROMPT } from '@taiga-ui/kit' import { filter, takeUntil } from 'rxjs' -import { ValueSpecList } from 'start-sdk/lib/config/configTypes' +import { ValueSpecList } from '@start9labs/start-sdk/lib/config/configTypes' import { FormService } from '../../../services/form.service' import { ERRORS } from '../form-group/form-group.component' diff --git a/frontend/projects/ui/src/app/components/form/form-color/form-color.component.ts b/frontend/projects/ui/src/app/components/form/form-color/form-color.component.ts index 01aa1ee10..9f7a119e5 100644 --- a/frontend/projects/ui/src/app/components/form/form-color/form-color.component.ts +++ b/frontend/projects/ui/src/app/components/form/form-color/form-color.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core' -import { ValueSpecColor } from 'start-sdk/lib/config/configTypes' +import { ValueSpecColor } from '@start9labs/start-sdk/lib/config/configTypes' import { Control } from '../control' import { MaskitoOptions } from '@maskito/core' diff --git a/frontend/projects/ui/src/app/components/form/form-control/form-control.component.ts b/frontend/projects/ui/src/app/components/form/form-control/form-control.component.ts index 472fa947d..a4d78245c 100644 --- a/frontend/projects/ui/src/app/components/form/form-control/form-control.component.ts +++ b/frontend/projects/ui/src/app/components/form/form-control/form-control.component.ts @@ -6,18 +6,14 @@ import { TemplateRef, ViewChild, } from '@angular/core' -import { - AbstractTuiNullableControl, - TuiContextWithImplicit, -} from '@taiga-ui/cdk' +import { AbstractTuiNullableControl } from '@taiga-ui/cdk' import { TuiAlertService, TuiDialogContext, TuiNotification, } from '@taiga-ui/core' -import { TUI_VALIDATION_ERRORS } from '@taiga-ui/kit' import { filter, takeUntil } from 'rxjs' -import { ValueSpec, ValueSpecText } from 'start-sdk/lib/config/configTypes' +import { ValueSpec } from '@start9labs/start-sdk/lib/config/configTypes' import { ERRORS } from '../form-group/form-group.component' import { FORM_CONTROL_PROVIDERS } from './form-control.providers' diff --git a/frontend/projects/ui/src/app/components/form/form-control/form-control.providers.ts b/frontend/projects/ui/src/app/components/form/form-control/form-control.providers.ts index ca85ef7f3..439a9fc53 100644 --- a/frontend/projects/ui/src/app/components/form/form-control/form-control.providers.ts +++ b/frontend/projects/ui/src/app/components/form/form-control/form-control.providers.ts @@ -1,6 +1,6 @@ import { forwardRef, Provider } from '@angular/core' import { TUI_VALIDATION_ERRORS } from '@taiga-ui/kit' -import { ValueSpec } from 'start-sdk/lib/config/configTypes' +import { ValueSpec } from '@start9labs/start-sdk/lib/config/configTypes' import { FormControlComponent } from './form-control.component' interface ValidatorsPatternError { diff --git a/frontend/projects/ui/src/app/components/form/form-datetime/form-datetime.component.ts b/frontend/projects/ui/src/app/components/form/form-datetime/form-datetime.component.ts index ff3e5bdd9..8495919ce 100644 --- a/frontend/projects/ui/src/app/components/form/form-datetime/form-datetime.component.ts +++ b/frontend/projects/ui/src/app/components/form/form-datetime/form-datetime.component.ts @@ -6,7 +6,7 @@ import { tuiPure, TuiTime, } from '@taiga-ui/cdk' -import { ValueSpecDatetime } from 'start-sdk/lib/config/configTypes' +import { ValueSpecDatetime } from '@start9labs/start-sdk/lib/config/configTypes' import { Control } from '../control' @Component({ diff --git a/frontend/projects/ui/src/app/components/form/form-file/form-file.component.ts b/frontend/projects/ui/src/app/components/form/form-file/form-file.component.ts index fbc9239c1..34a3b3a28 100644 --- a/frontend/projects/ui/src/app/components/form/form-file/form-file.component.ts +++ b/frontend/projects/ui/src/app/components/form/form-file/form-file.component.ts @@ -1,6 +1,6 @@ import { Component } from '@angular/core' import { TuiFileLike } from '@taiga-ui/kit' -import { ValueSpecFile } from 'start-sdk/lib/config/configTypes' +import { ValueSpecFile } from '@start9labs/start-sdk/lib/config/configTypes' import { Control } from '../control' @Component({ diff --git a/frontend/projects/ui/src/app/components/form/form-group/form-group.component.ts b/frontend/projects/ui/src/app/components/form/form-group/form-group.component.ts index 11c194278..8c6ff8af1 100644 --- a/frontend/projects/ui/src/app/components/form/form-group/form-group.component.ts +++ b/frontend/projects/ui/src/app/components/form/form-group/form-group.component.ts @@ -4,7 +4,7 @@ import { Input, ViewEncapsulation, } from '@angular/core' -import { InputSpec } from 'start-sdk/lib/config/configTypes' +import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes' import { FORM_GROUP_PROVIDERS } from './form-group.providers' export const ERRORS = [ diff --git a/frontend/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.ts b/frontend/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.ts index 4d7ddc909..53792d772 100644 --- a/frontend/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.ts +++ b/frontend/projects/ui/src/app/components/form/form-multiselect/form-multiselect.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core' -import { ValueSpecMultiselect } from 'start-sdk/lib/config/configTypes' +import { ValueSpecMultiselect } from '@start9labs/start-sdk/lib/config/configTypes' import { Control } from '../control' import { tuiPure } from '@taiga-ui/cdk' diff --git a/frontend/projects/ui/src/app/components/form/form-number/form-number.component.html b/frontend/projects/ui/src/app/components/form/form-number/form-number.component.html index 60ad350de..274a07bbb 100644 --- a/frontend/projects/ui/src/app/components/form/form-number/form-number.component.html +++ b/frontend/projects/ui/src/app/components/form/form-number/form-number.component.html @@ -1,4 +1,3 @@ - diff --git a/frontend/projects/ui/src/app/components/form/form-number/form-number.component.ts b/frontend/projects/ui/src/app/components/form/form-number/form-number.component.ts index ee131bcd2..387b33719 100644 --- a/frontend/projects/ui/src/app/components/form/form-number/form-number.component.ts +++ b/frontend/projects/ui/src/app/components/form/form-number/form-number.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core' -import { ValueSpecNumber } from 'start-sdk/lib/config/configTypes' +import { ValueSpecNumber } from '@start9labs/start-sdk/lib/config/configTypes' import { Control } from '../control' @Component({ diff --git a/frontend/projects/ui/src/app/components/form/form-object/form-object.component.ts b/frontend/projects/ui/src/app/components/form/form-object/form-object.component.ts index 1b72f128d..cb8a5c9e9 100644 --- a/frontend/projects/ui/src/app/components/form/form-object/form-object.component.ts +++ b/frontend/projects/ui/src/app/components/form/form-object/form-object.component.ts @@ -7,7 +7,7 @@ import { Output, } from '@angular/core' import { ControlContainer } from '@angular/forms' -import { ValueSpecObject } from 'start-sdk/lib/config/configTypes' +import { ValueSpecObject } from '@start9labs/start-sdk/lib/config/configTypes' @Component({ selector: 'form-object', diff --git a/frontend/projects/ui/src/app/components/form/form-select/form-select.component.ts b/frontend/projects/ui/src/app/components/form/form-select/form-select.component.ts index 500f23deb..d358f821d 100644 --- a/frontend/projects/ui/src/app/components/form/form-select/form-select.component.ts +++ b/frontend/projects/ui/src/app/components/form/form-select/form-select.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core' -import { ValueSpecSelect } from 'start-sdk/lib/config/configTypes' +import { ValueSpecSelect } from '@start9labs/start-sdk/lib/config/configTypes' import { Control } from '../control' @Component({ diff --git a/frontend/projects/ui/src/app/components/form/form-text/form-text.component.ts b/frontend/projects/ui/src/app/components/form/form-text/form-text.component.ts index 0baa92411..37da806c8 100644 --- a/frontend/projects/ui/src/app/components/form/form-text/form-text.component.ts +++ b/frontend/projects/ui/src/app/components/form/form-text/form-text.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core' -import { ValueSpecText } from 'start-sdk/lib/config/configTypes' +import { ValueSpecText } from '@start9labs/start-sdk/lib/config/configTypes' import { Control } from '../control' @Component({ diff --git a/frontend/projects/ui/src/app/components/form/form-textarea/form-textarea.component.ts b/frontend/projects/ui/src/app/components/form/form-textarea/form-textarea.component.ts index 47a6ad315..bead77088 100644 --- a/frontend/projects/ui/src/app/components/form/form-textarea/form-textarea.component.ts +++ b/frontend/projects/ui/src/app/components/form/form-textarea/form-textarea.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core' -import { ValueSpecTextarea } from 'start-sdk/lib/config/configTypes' +import { ValueSpecTextarea } from '@start9labs/start-sdk/lib/config/configTypes' import { Control } from '../control' @Component({ diff --git a/frontend/projects/ui/src/app/components/form/form-toggle/form-toggle.component.ts b/frontend/projects/ui/src/app/components/form/form-toggle/form-toggle.component.ts index debbe6a67..b35d75a57 100644 --- a/frontend/projects/ui/src/app/components/form/form-toggle/form-toggle.component.ts +++ b/frontend/projects/ui/src/app/components/form/form-toggle/form-toggle.component.ts @@ -1,5 +1,5 @@ import { Component } from '@angular/core' -import { ValueSpecToggle } from 'start-sdk/lib/config/configTypes' +import { ValueSpecToggle } from '@start9labs/start-sdk/lib/config/configTypes' import { Control } from '../control' @Component({ diff --git a/frontend/projects/ui/src/app/components/form/form-union/form-union.component.ts b/frontend/projects/ui/src/app/components/form/form-union/form-union.component.ts index f440d5e06..1898d8212 100644 --- a/frontend/projects/ui/src/app/components/form/form-union/form-union.component.ts +++ b/frontend/projects/ui/src/app/components/form/form-union/form-union.component.ts @@ -11,7 +11,7 @@ import { ValueSpecSelect, ValueSpecUnion, unionValueKey, -} from 'start-sdk/lib/config/configTypes' +} from '@start9labs/start-sdk/lib/config/configTypes' import { FormService } from '../../../services/form.service' import { tuiPure } from '@taiga-ui/cdk' @@ -50,10 +50,14 @@ export class FormUnionComponent implements OnChanges { this.formService.getFormGroup( union ? this.spec.variants[union].spec : {}, ), + { + emitEvent: false, + }, ) } ngOnChanges() { this.selectSpec = this.formService.getUnionSelectSpec(this.spec, this.union) + if (this.union) this.onUnion(this.union) } } diff --git a/frontend/projects/ui/src/app/modals/app-config/app-config.page.ts b/frontend/projects/ui/src/app/modals/app-config/app-config.page.ts index efc0db2c2..dbef7701f 100644 --- a/frontend/projects/ui/src/app/modals/app-config/app-config.page.ts +++ b/frontend/projects/ui/src/app/modals/app-config/app-config.page.ts @@ -11,7 +11,7 @@ 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 { InputSpec } from 'start-sdk/lib/config/configTypes' +import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes' import { DataModel, PackageDataEntry, @@ -127,6 +127,7 @@ export class AppConfigPage { private async uploadFiles(config: Record, loader: Subscription) { loader.unsubscribe() + loader.closed = false // TODO: Could be nested files const keys = Object.keys(config).filter(key => config[key] instanceof File) @@ -147,6 +148,7 @@ export class AppConfigPage { loader: Subscription, ) { loader.unsubscribe() + loader.closed = false loader.add(this.loader.open('Checking dependent services...').subscribe()) const breakages = await this.embassyApi.drySetPackageConfig({ @@ -155,6 +157,7 @@ export class AppConfigPage { }) loader.unsubscribe() + loader.closed = false if (isEmptyObject(breakages) || (await this.approveBreakages(breakages))) { await this.configure(config, loader) @@ -163,6 +166,7 @@ export class AppConfigPage { private async configure(config: Record, loader: Subscription) { loader.unsubscribe() + loader.closed = false loader.add(this.loader.open('Saving...').subscribe()) await this.embassyApi.setPackageConfig({ id: this.pkgId, config }) diff --git a/frontend/projects/ui/src/app/modals/form/form.page.ts b/frontend/projects/ui/src/app/modals/form/form.page.ts index 17fdc9c15..fc4a69653 100644 --- a/frontend/projects/ui/src/app/modals/form/form.page.ts +++ b/frontend/projects/ui/src/app/modals/form/form.page.ts @@ -6,7 +6,7 @@ import { OnInit, } from '@angular/core' import { FormService } from 'src/app/services/form.service' -import { InputSpec } from 'start-sdk/lib/config/configTypes' +import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes' import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus' import { TuiDialogContext } from '@taiga-ui/core' import { tuiMarkControlAsTouchedAndValidate } from '@taiga-ui/cdk' diff --git a/frontend/projects/ui/src/app/modals/generic-form/generic-form.module.ts b/frontend/projects/ui/src/app/modals/generic-form/generic-form.module.ts deleted file mode 100644 index 3ca863c3d..000000000 --- a/frontend/projects/ui/src/app/modals/generic-form/generic-form.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { GenericFormPage } from './generic-form.page' -import { FormsModule, ReactiveFormsModule } from '@angular/forms' -import { FormObjectModule } from 'src/app/components/form-object/form-object.module' - -@NgModule({ - declarations: [GenericFormPage], - imports: [ - CommonModule, - IonicModule, - FormsModule, - ReactiveFormsModule, - FormObjectModule, - ], - exports: [GenericFormPage], -}) -export class GenericFormPageModule {} diff --git a/frontend/projects/ui/src/app/modals/generic-form/generic-form.page.html b/frontend/projects/ui/src/app/modals/generic-form/generic-form.page.html deleted file mode 100644 index 3ff3e4aae..000000000 --- a/frontend/projects/ui/src/app/modals/generic-form/generic-form.page.html +++ /dev/null @@ -1,35 +0,0 @@ - - - {{ title }} - - - - - - - - - -
- - -
-
- - - - - - {{ button.text }} - - - - diff --git a/frontend/projects/ui/src/app/modals/generic-form/generic-form.page.scss b/frontend/projects/ui/src/app/modals/generic-form/generic-form.page.scss deleted file mode 100644 index 0353411b3..000000000 --- a/frontend/projects/ui/src/app/modals/generic-form/generic-form.page.scss +++ /dev/null @@ -1,9 +0,0 @@ -button:disabled, -button[disabled]{ - border: 1px solid #999999; - background-color: #cccccc; - color: #666666; -} -button { - color: var(--ion-color-primary); -} \ No newline at end of file diff --git a/frontend/projects/ui/src/app/modals/generic-form/generic-form.page.ts b/frontend/projects/ui/src/app/modals/generic-form/generic-form.page.ts deleted file mode 100644 index 7a4c4042c..000000000 --- a/frontend/projects/ui/src/app/modals/generic-form/generic-form.page.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { Component, Input } from '@angular/core' -import { UntypedFormGroup } from '@angular/forms' -import { ModalController } from '@ionic/angular' -import { - convertValuesRecursive, - FormService, -} from 'src/app/services/form.service' -import { InputSpec } from 'start-sdk/lib/config/configTypes' -import { ErrorToastService } from '@start9labs/shared' - -export interface ActionButton { - text: string - handler: (value: any) => Promise - isSubmit?: boolean -} - -@Component({ - selector: 'generic-form', - templateUrl: './generic-form.page.html', - styleUrls: ['./generic-form.page.scss'], -}) -export class GenericFormPage { - @Input() title!: string - @Input() spec!: InputSpec - @Input() buttons!: ActionButton[] - @Input() initialValue: Record = {} - - submitBtn!: ActionButton - formGroup!: UntypedFormGroup - - constructor( - private readonly modalCtrl: ModalController, - private readonly formService: FormService, - private readonly errToast: ErrorToastService, - ) {} - - ngOnInit() { - this.formGroup = this.formService.createForm(this.spec, this.initialValue) - this.submitBtn = this.buttons.find(btn => btn.isSubmit)! // @TODO this really needs to be redesigned. No way to enforce this with types. - } - - async dismiss(): Promise { - this.modalCtrl.dismiss() - } - - async handleClick(handler: ActionButton['handler']): Promise { - convertValuesRecursive(this.spec, this.formGroup) - - if (this.formGroup.invalid) { - document - .getElementsByClassName('validation-error')[0] - ?.scrollIntoView({ behavior: 'smooth' }) - return - } - - try { - const response = await handler(this.formGroup.value) - this.modalCtrl.dismiss({ response }, 'success') - } catch (e: any) { - this.errToast.present(e) - } - } -} - -export interface GenericFormOptions { - // required - title: string - spec: InputSpec - buttons: ActionButton[] - // optional - initialValue?: Record -} diff --git a/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.module.ts b/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.module.ts index 096e06add..8c1398515 100644 --- a/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.module.ts +++ b/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.module.ts @@ -4,6 +4,11 @@ import { IonicModule } from '@ionic/angular' import { MarketplaceSettingsPage } from './marketplace-settings.page' import { SharedPipesModule } from '@start9labs/shared' import { StoreIconComponentModule } from 'src/app/components/store-icon/store-icon.component.module' +import { + TuiDataListModule, + TuiHostedDropdownModule, + TuiSvgModule, +} from '@taiga-ui/core' @NgModule({ imports: [ @@ -11,6 +16,9 @@ import { StoreIconComponentModule } from 'src/app/components/store-icon/store-ic IonicModule, SharedPipesModule, StoreIconComponentModule, + TuiHostedDropdownModule, + TuiDataListModule, + TuiSvgModule, ], declarations: [MarketplaceSettingsPage], }) diff --git a/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.html b/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.html index 663b022cd..8d0da271a 100644 --- a/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.html +++ b/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.html @@ -1,22 +1,11 @@ - - - Change Registry - - - - - - - - - +
Default Registries @@ -44,24 +33,45 @@ - - - -

{{ a.name }}

-

{{ a.url }}

-
- -
+ + + + + +

{{ a.name }}

+

{{ a.url }}

+
+ +
+ + + + + + +
- +
diff --git a/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.scss b/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.scss index e69de29bb..c0655db59 100644 --- a/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.scss +++ b/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.scss @@ -0,0 +1,16 @@ +ion-item { + --background: transparent; +} + +.host { + display: flex; +} + +.delete { + background: var(--tui-error-bg); + color: var(--tui-error-fill); + + &:focus { + background: var(--tui-error-bg-hover); + } +} diff --git a/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts b/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts index 997f50b84..7e0778d2e 100644 --- a/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts +++ b/frontend/projects/ui/src/app/modals/marketplace-settings/marketplace-settings.page.ts @@ -1,29 +1,18 @@ -import { - ChangeDetectionStrategy, - Component, - Inject, - ViewChild, -} from '@angular/core' -import { - ActionSheetController, - AlertController, - LoadingController, - ModalController, -} from '@ionic/angular' -import { ActionSheetButton } from '@ionic/core' -import { ErrorToastService, sameUrl, toUrl } from '@start9labs/shared' +import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' +import { ErrorService, sameUrl, toUrl } from '@start9labs/shared' import { AbstractMarketplaceService } from '@start9labs/marketplace' +import { TuiDialogService } from '@taiga-ui/core' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ValueSpecObject } from 'start-sdk/lib/config/configTypes' -import { - GenericFormPage, - GenericFormOptions, -} from 'src/app/modals/generic-form/generic-form.page' +import { ValueSpecObject } from '@start9labs/start-sdk/lib/config/configTypes' import { PatchDB } from 'patch-db-client' import { DataModel, UIStore } from 'src/app/services/patch-db/data-model' import { MarketplaceService } from 'src/app/services/marketplace.service' import { map } from 'rxjs/operators' -import { combineLatest, firstValueFrom } from 'rxjs' +import { combineLatest, filter, firstValueFrom, Subscription } from 'rxjs' +import { FormDialogService } from '../../services/form-dialog.service' +import { FormPage } from '../form/form.page' +import { LoadingService } from '../loading/loading.service' +import { TUI_PROMPT } from '@taiga-ui/kit' @Component({ selector: 'marketplace-settings', @@ -52,140 +41,87 @@ export class MarketplaceSettingsPage { constructor( private readonly api: ApiService, - private readonly loadingCtrl: LoadingController, - private readonly modalCtrl: ModalController, - private readonly errToast: ErrorToastService, - private readonly actionCtrl: ActionSheetController, + private readonly loader: LoadingService, + private readonly formDialog: FormDialogService, + private readonly errorService: ErrorService, @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, private readonly patch: PatchDB, - private readonly alertCtrl: AlertController, + private readonly dialogs: TuiDialogService, ) {} - async dismiss() { - this.modalCtrl.dismiss() - } - async presentModalAdd() { const { name, spec } = getMarketplaceValueSpec() - const options: GenericFormOptions = { - title: name, - spec, - buttons: [ - { - text: 'Save for Later', - handler: async (value: { url: string }) => this.saveOnly(value.url), - }, - { - text: 'Save and Connect', - handler: async (value: { url: string }) => - this.saveAndConnect(value.url), - isSubmit: true, - }, - ], - } - - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: options, - cssClass: 'alertlike-modal', + this.formDialog.open(FormPage, { + label: name, + data: { + spec, + buttons: [ + { + text: 'Save for Later', + handler: async (value: { url: string }) => this.saveOnly(value.url), + }, + { + text: 'Save and Connect', + handler: async (value: { url: string }) => + this.saveAndConnect(value.url), + isSubmit: true, + }, + ], + }, }) - - await modal.present() } - async presentAction( - { url, name }: { url: string; name?: string }, - canDelete = false, - ) { - const buttons: ActionSheetButton[] = [ - { - text: 'Connect', - handler: () => { - this.connect(url) - }, - }, - ] - - if (canDelete) { - buttons.unshift({ - text: 'Delete', - role: 'destructive', - handler: () => { - this.presentAlertDelete(url, name!) + async presentAlertDelete(url: string, name: string = '') { + this.dialogs + .open(TUI_PROMPT, { + label: 'Confirm', + size: 's', + data: { + content: `Are you sure you want to delete ${name}?`, + yes: 'Delete', + no: 'Cancel', }, }) - } - - const action = await this.actionCtrl.create({ - header: name, - mode: 'ios', - buttons, - }) - - await action.present() + .pipe(filter(Boolean)) + .subscribe(() => this.delete(url)) } - private async presentAlertDelete(url: string, name: string) { - const alert = await this.alertCtrl.create({ - header: 'Confirm', - message: `Are you sure you want to delete ${name}?`, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Delete', - handler: () => this.delete(url), - cssClass: 'enter-click', - }, - ], - }) - - await alert.present() - } - - private async connect( + async connect( url: string, - loader?: HTMLIonLoadingElement, + loader: Subscription = new Subscription(), ): Promise { - const message = 'Changing Registry...' - if (!loader) { - loader = await this.loadingCtrl.create({ message }) - await loader.present() - } else { - loader.message = message - } + loader.unsubscribe() + loader.closed = false + loader.add(this.loader.open('Changing Registry...').subscribe()) try { await this.api.setDbValue(['marketplace', 'selected-url'], url) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() - this.dismiss() + loader.unsubscribe() } } private async saveOnly(rawUrl: string): Promise { - const loader = await this.loadingCtrl.create() + const loader = this.loader.open('Loading').subscribe() try { const url = new URL(rawUrl).toString() await this.validateAndSave(url, loader) return true } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) return false } finally { - loader.dismiss() + loader.unsubscribe() } } private async saveAndConnect(rawUrl: string): Promise { - const loader = await this.loadingCtrl.create() + const loader = this.loader.open('Loading').subscribe() try { const url = new URL(rawUrl).toString() @@ -193,17 +129,16 @@ export class MarketplaceSettingsPage { await this.connect(url, loader) return true } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) return false } finally { - loader.dismiss() - this.dismiss() + loader.unsubscribe() } } private async validateAndSave( url: string, - loader: HTMLIonLoadingElement, + loader: Subscription, ): Promise { // Error on duplicates const hosts = await firstValueFrom( @@ -213,15 +148,18 @@ export class MarketplaceSettingsPage { if (currentUrls.includes(url)) throw new Error('marketplace already added') // Validate - loader.message = 'Validating marketplace...' - await loader.present() + loader.unsubscribe() + loader.closed = false + loader.add(this.loader.open('Validating marketplace...').subscribe()) const { name } = await firstValueFrom( this.marketplaceService.fetchInfo$(url), ) // Save - loader.message = 'Saving...' + loader.unsubscribe() + loader.closed = false + loader.add(this.loader.open('Saving...').subscribe()) await this.api.setDbValue<{ name: string }>( ['marketplace', 'known-hosts', url], @@ -230,10 +168,7 @@ export class MarketplaceSettingsPage { } private async delete(url: string): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Deleting...', - }) - await loader.present() + const loader = this.loader.open('Deleting...').subscribe() const hosts = await firstValueFrom( this.patch.watch$('ui', 'marketplace', 'known-hosts'), @@ -255,9 +190,9 @@ export class MarketplaceSettingsPage { filtered, ) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } } @@ -287,6 +222,9 @@ function getMarketplaceValueSpec(): ValueSpecObject { placeholder: 'e.g. https://example.org', default: null, warning: null, + disabled: false, + immutable: false, + generate: null, }, }, } diff --git a/frontend/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts b/frontend/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts index 0095217d5..ba550d868 100644 --- a/frontend/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts +++ b/frontend/projects/ui/src/app/pages/apps-routes/app-actions/app-actions.page.ts @@ -7,12 +7,7 @@ import { } from '@angular/core' import { ActivatedRoute } from '@angular/router' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { - AlertController, - LoadingController, - ModalController, - NavController, -} from '@ionic/angular' +import { AlertController, ModalController, NavController } from '@ionic/angular' import { PatchDB } from 'patch-db-client' import { Action, @@ -20,19 +15,18 @@ import { PackageDataEntry, PackageState, } from 'src/app/services/patch-db/data-model' -import { - GenericFormPage, - GenericFormOptions, -} from 'src/app/modals/generic-form/generic-form.page' import { isEmptyObject, - ErrorToastService, getPkgId, WithId, + ErrorService, } from '@start9labs/shared' import { ActionSuccessPage } from 'src/app/modals/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/modals/form/form.page' +import { LoadingService } from 'src/app/modals/loading/loading.service' @Component({ selector: 'app-actions', @@ -51,10 +45,11 @@ export class AppActionsPage { private readonly embassyApi: ApiService, private readonly modalCtrl: ModalController, private readonly alertCtrl: AlertController, - private readonly errToast: ErrorToastService, - private readonly loadingCtrl: LoadingController, + private readonly errorService: ErrorService, + private readonly loader: LoadingService, private readonly navCtrl: NavController, private readonly patch: PatchDB, + private readonly formDialog: FormDialogService, ) {} async handleAction(action: WithId) { @@ -68,23 +63,19 @@ export class AppActionsPage { await alert.present() } else { if (action['input-spec'] && !isEmptyObject(action['input-spec'])) { - const options: GenericFormOptions = { - title: action.name, - spec: action['input-spec'], - buttons: [ - { - text: 'Execute', - handler: async (value: any) => - this.executeAction(action.id, value), - isSubmit: true, - }, - ], - } - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: options, + this.formDialog.open(FormPage, { + label: action.name, + data: { + spec: action['input-spec'], + buttons: [ + { + text: 'Execute', + handler: async (value: any) => + this.executeAction(action.id, value), + }, + ], + }, }) - await modal.present() } else { const alert = await this.alertCtrl.create({ header: 'Confirm', @@ -142,10 +133,7 @@ export class AppActionsPage { } private async uninstall() { - const loader = await this.loadingCtrl.create({ - message: `Beginning uninstall...`, - }) - await loader.present() + const loader = this.loader.open(`Beginning uninstall...`).subscribe() try { await this.embassyApi.uninstallPackage({ id: this.pkgId }) @@ -154,9 +142,9 @@ export class AppActionsPage { .catch(e => console.error('Failed to mark instructions as unseen', e)) this.navCtrl.navigateRoot('/services') } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } @@ -164,10 +152,7 @@ export class AppActionsPage { actionId: string, input?: object, ): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Executing action...', - }) - await loader.present() + const loader = this.loader.open('Executing action...').subscribe() try { const res = await this.embassyApi.executePackageAction({ @@ -186,10 +171,10 @@ export class AppActionsPage { setTimeout(() => successModal.present(), 500) return true } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) return false } finally { - loader.dismiss() + loader.unsubscribe() } } diff --git a/frontend/projects/ui/src/app/pages/backups-routes/pages/backup-targets/backup-targets.page.ts b/frontend/projects/ui/src/app/pages/backups-routes/pages/backup-targets/backup-targets.page.ts index 3b0301ede..01df98a8b 100644 --- a/frontend/projects/ui/src/app/pages/backups-routes/pages/backup-targets/backup-targets.page.ts +++ b/frontend/projects/ui/src/app/pages/backups-routes/pages/backup-targets/backup-targets.page.ts @@ -2,26 +2,44 @@ import { Component } from '@angular/core' import { BackupTarget, BackupTargetType, - DiskBackupTarget, RR, UnknownDisk, } from 'src/app/services/api/api.types' -import { - AlertController, - LoadingController, - ModalController, -} from '@ionic/angular' -import { GenericFormPage } from 'src/app/modals/generic-form/generic-form.page' import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorToastService } from '@start9labs/shared' import { - CifsSpec, - DropboxSpec, - GoogleDriveSpec, - DiskBackupTargetSpec, - RemoteBackupTargetSpec, + cifsSpec, + diskBackupTargetSpec, + dropboxSpec, + googleDriveSpec, + remoteBackupTargetSpec, } from '../../types/target-types' -import { BehaviorSubject } from 'rxjs' +import { BehaviorSubject, filter } from 'rxjs' +import { TuiDialogService } from '@taiga-ui/core' +import { ErrorService } from '@start9labs/shared' +import { FormDialogService } from '../../../../services/form-dialog.service' +import { FormPage } from '../../../../modals/form/form.page' +import { LoadingService } from '../../../../modals/loading/loading.service' +import { TUI_PROMPT } from '@taiga-ui/kit' +import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec' +import { + InputSpec, + unionSelectKey, + unionValueKey, +} from '@start9labs/start-sdk/lib/config/configTypes' + +type BackupConfig = + | { + type: { + [unionSelectKey]: 'dropbox' | 'google-drive' + [unionValueKey]: RR.AddCloudBackupTargetReq + } + } + | { + type: { + [unionSelectKey]: 'cifs' + [unionValueKey]: RR.AddCifsBackupTargetReq + } + } export type BackupType = 'create' | 'restore' @@ -41,27 +59,23 @@ export class BackupTargetsPage { 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, + private readonly formDialog: FormDialogService, ) {} ngOnInit() { this.getTargets() } - async presentModalAddPhysical( - disk: UnknownDisk, - index: number, - ): Promise { - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: { - title: 'New Physical Target', - spec: DiskBackupTargetSpec, - initialValue: { + async presentModalAddPhysical(disk: UnknownDisk, index: number) { + this.formDialog.open(FormPage, { + label: 'New Physical Target', + data: { + spec: await configBuilderToSpec(diskBackupTargetSpec), + value: { name: disk.label || disk.logicalname, }, buttons: [ @@ -74,60 +88,56 @@ export class BackupTargetsPage { }).then(disk => { this.targets['unknown-disks'].splice(index, 1) this.targets.saved.push(disk) + + return true }), - isSubmit: true, }, ], }, }) - - await modal.present() } - async presentModalAddRemote(): Promise { - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: { - title: 'New Remote Target', - spec: RemoteBackupTargetSpec, + async presentModalAddRemote() { + this.formDialog.open(FormPage, { + label: 'New Remote Target', + data: { + spec: await configBuilderToSpec(remoteBackupTargetSpec), buttons: [ { text: 'Save', - handler: ( - value: - | (RR.AddCifsBackupTargetReq & { type: BackupTargetType }) - | (RR.AddCloudBackupTargetReq & { type: BackupTargetType }), - ) => this.add(value.type, value), - isSubmit: true, + handler: ({ type }: BackupConfig) => + this.add( + type[unionSelectKey] === 'cifs' ? 'cifs' : 'cloud', + type[unionValueKey], + ), }, ], }, }) - - await modal.present() } - async presentModalUpdate(target: BackupTarget): Promise { - let spec: typeof RemoteBackupTargetSpec = {} + async presentModalUpdate(target: BackupTarget) { + let spec: InputSpec switch (target.type) { case 'cifs': - spec = CifsSpec + spec = await configBuilderToSpec(cifsSpec) break case 'cloud': - spec = target.provider === 'dropbox' ? DropboxSpec : GoogleDriveSpec + spec = await configBuilderToSpec( + target.provider === 'dropbox' ? dropboxSpec : googleDriveSpec, + ) break case 'disk': - spec = DiskBackupTargetSpec + spec = await configBuilderToSpec(diskBackupTargetSpec) break } - const modal = await this.modalCtrl.create({ - component: GenericFormPage, - componentProps: { - title: 'Update Remote Target', + this.formDialog.open(FormPage, { + label: 'Update Target', + data: { spec, - initialValue: target, + value: target, buttons: [ { text: 'Save', @@ -136,49 +146,38 @@ export class BackupTargetsPage { | RR.UpdateCifsBackupTargetReq | RR.UpdateCloudBackupTargetReq | RR.UpdateDiskBackupTargetReq, - ) => this.update(target.type, value), - isSubmit: true, + ) => this.update(target.type, { ...value, id: target.id }), }, ], }, }) - await modal.present() } - async presentAlertDelete(id: string, index: number) { - const alert = await this.alertCtrl.create({ - header: 'Confirm', - message: 'Forget backup target? This actions cannot be undone.', - buttons: [ - { - text: 'Cancel', - role: 'cancel', + presentAlertDelete(id: string, index: number) { + this.dialogs + .open(TUI_PROMPT, { + label: 'Confirm', + size: 's', + data: { + content: 'Forget backup target? This actions cannot be undone.', + no: 'Cancel', + yes: 'Delete', }, - { - text: 'Delete', - handler: () => { - this.delete(id, index) - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() + }) + .pipe(filter(Boolean)) + .subscribe(() => this.delete(id, index)) } async delete(id: string, index: number): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Removing...', - }) - await loader.present() + const loader = this.loader.open('Removing...').subscribe() try { await this.api.removeBackupTarget({ id }) this.targets.saved.splice(index, 1) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { - loader.dismiss() + loader.unsubscribe() } } @@ -202,7 +201,7 @@ export class BackupTargetsPage { try { this.targets = await this.api.getBackupTargets({}) } catch (e: any) { - this.errToast.present(e) + this.errorService.handleError(e) } finally { this.loading$.next(false) } @@ -215,16 +214,12 @@ export class BackupTargetsPage { | RR.AddCloudBackupTargetReq | RR.AddDiskBackupTargetReq, ): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Saving target...', - }) - await loader.present() + const loader = this.loader.open('Saving target...').subscribe() try { - const res = await this.api.addBackupTarget(type, value) - return res + return await this.api.addBackupTarget(type, value) } finally { - loader.dismiss() + loader.unsubscribe() } } @@ -235,16 +230,12 @@ export class BackupTargetsPage { | RR.UpdateCloudBackupTargetReq | RR.UpdateDiskBackupTargetReq, ): Promise { - const loader = await this.loadingCtrl.create({ - message: 'Saving target...', - }) - await loader.present() + const loader = this.loader.open('Saving target...').subscribe() try { - const res = await this.api.updateBackupTarget(type, value) - return res + return await this.api.updateBackupTarget(type, value) } finally { - loader.dismiss() + loader.unsubscribe() } } } diff --git a/frontend/projects/ui/src/app/pages/backups-routes/types/target-types.ts b/frontend/projects/ui/src/app/pages/backups-routes/types/target-types.ts index 704d0a080..4e38f7570 100644 --- a/frontend/projects/ui/src/app/pages/backups-routes/types/target-types.ts +++ b/frontend/projects/ui/src/app/pages/backups-routes/types/target-types.ts @@ -1,221 +1,121 @@ -import { InputSpec } from 'start-sdk/lib/config/configTypes' +import { Config } from '@start9labs/start-sdk/lib/config/builder/config' +import { Value } from '@start9labs/start-sdk/lib/config/builder/value' +import { Variants } from '@start9labs/start-sdk/lib/config/builder/variants' -export const DropboxSpec: InputSpec = { - name: { - type: 'text', - inputmode: 'text', - minLength: null, - maxLength: null, - patterns: [], +export const dropboxSpec = Config.of({ + name: Value.text({ name: 'Name', description: 'A friendly name for this Dropbox target', placeholder: 'My Dropbox', - required: true, - masked: false, - warning: null, - default: null, - }, - token: { - type: 'text', - inputmode: 'text', - minLength: null, - maxLength: null, - patterns: [], + required: { default: null }, + }), + token: Value.text({ name: 'Access Token', description: 'The secret access token for your custom Dropbox app', - warning: null, - placeholder: null, - required: true, + required: { default: null }, masked: true, - default: null, - }, - path: { - type: 'text', - inputmode: 'text', - minLength: null, - maxLength: null, - patterns: [], + }), + path: Value.text({ name: 'Path', description: 'The fully qualified path to the backup directory', - warning: null, placeholder: 'e.g. /Desktop/my-folder', - required: true, - masked: false, - default: null, - }, -} + required: { default: null }, + }), +}) -export const GoogleDriveSpec: InputSpec = { - name: { - type: 'text', - inputmode: 'text', - minLength: null, - maxLength: null, - patterns: [], +export const googleDriveSpec = Config.of({ + name: Value.text({ name: 'Name', description: 'A friendly name for this Google Drive target', - warning: null, placeholder: 'My Google Drive', - required: true, - masked: false, - default: null, - }, - key: { - type: 'file', + required: { default: null }, + }), + path: Value.text({ + name: 'Path', + description: 'The fully qualified path to the backup directory', + placeholder: 'e.g. /Desktop/my-folder', + required: { default: null }, + }), + key: Value.file({ name: 'Private Key File', description: 'Your Google Drive service account private key file (.json file)', - warning: null, required: true, extensions: ['json'], - }, - path: { - type: 'text', - inputmode: 'text', - minLength: null, - maxLength: null, - patterns: [], - name: 'Path', - description: 'The fully qualified path to the backup directory', - placeholder: 'e.g. /Desktop/my-folder', - required: true, - masked: false, - warning: null, - default: null, - }, -} + }), +}) -export const CifsSpec: InputSpec = { - name: { - type: 'text', - inputmode: 'text', - minLength: null, - maxLength: null, - patterns: [], +export const cifsSpec = Config.of({ + name: Value.text({ name: 'Name', description: 'A friendly name for this Network Folder', - warning: null, placeholder: 'My Network Folder', - required: true, - masked: false, - default: null, - }, - hostname: { - type: 'text', - inputmode: 'text', - minLength: null, - maxLength: null, - patterns: [ - { - regex: '^[a-zA-Z0-9._-]+( [a-zA-Z0-9]+)*$', - description: `Must be a valid hostname. e.g. 'My Computer' OR 'my-computer.local'`, - }, - ], + required: { default: null }, + }), + hostname: Value.text({ name: 'Hostname', description: 'The hostname of your target device on the Local Area Network.', warning: null, - required: true, - masked: false, placeholder: `e.g. 'My Computer' OR 'my-computer.local'`, - default: null, - }, - path: { - type: 'text', - inputmode: 'text', - minLength: null, - maxLength: null, + required: { default: null }, patterns: [], + }), + path: Value.text({ name: 'Path', description: `On Windows, this is the fully qualified path to the shared folder, (e.g. /Desktop/my-folder).\n\n On Linux and Mac, this is the literal name of the shared folder (e.g. my-shared-folder).`, placeholder: 'e.g. my-shared-folder or /Desktop/my-folder', - required: true, - masked: false, - warning: null, - default: null, - }, - username: { - type: 'text', - inputmode: 'text', - minLength: null, - maxLength: null, - patterns: [], + required: { default: null }, + }), + username: Value.text({ name: 'Username', description: `On Linux, this is the samba username you created when sharing the folder.\n\n On Mac and Windows, this is the username of the user who is sharing the folder.`, - required: true, - masked: false, - warning: null, + required: { default: null }, placeholder: 'My Network Folder', - default: null, - }, - password: { - type: 'text', - inputmode: 'text', - minLength: null, - maxLength: null, - patterns: [], + }), + password: Value.text({ name: 'Password', description: `On Linux, this is the samba password you created when sharing the folder.\n\n On Mac and Windows, this is the password of the user who is sharing the folder.`, required: false, masked: true, - warning: null, placeholder: 'My Network Folder', - default: null, - }, -} + }), +}) -export const RemoteBackupTargetSpec: InputSpec = { - type: { - type: 'union', - name: 'Target Type', - description: null, - warning: null, - required: true, - variants: { +export const remoteBackupTargetSpec = Config.of({ + type: Value.union( + { + name: 'Target Type', + required: { default: 'dropbox' }, + }, + Variants.of({ dropbox: { name: 'Dropbox', - spec: DropboxSpec, + spec: dropboxSpec, }, 'google-drive': { name: 'Google Drive', - spec: GoogleDriveSpec, + spec: googleDriveSpec, }, cifs: { name: 'Network Folder', - spec: CifsSpec, + spec: cifsSpec, }, - }, - default: 'dropbox', - }, -} + }), + ), +}) -export const DiskBackupTargetSpec: InputSpec = { - name: { - type: 'text', - inputmode: 'text', - minLength: null, - maxLength: null, - patterns: [], +export const diskBackupTargetSpec = Config.of({ + name: Value.text({ name: 'Name', description: 'A friendly name for this physical target', placeholder: 'My Physical Target', - required: true, - masked: false, - warning: null, - default: null, - }, - path: { - type: 'text', - inputmode: 'text', - minLength: null, - maxLength: null, - patterns: [], + required: { default: null }, + }), + path: Value.text({ name: 'Path', description: 'The fully qualified path to the backup directory', placeholder: 'e.g. /Backups/my-folder', - required: true, - masked: false, - warning: null, - default: null, - }, -} + required: { default: null }, + }), +}) diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.module.ts b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.module.ts deleted file mode 100644 index 649ab7dfc..000000000 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { RouterModule, Routes } from '@angular/router' -import { DevConfigPage } from './dev-config.page' -import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' -import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module' -import { FormsModule } from '@angular/forms' -import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor' - -const routes: Routes = [ - { - path: '', - component: DevConfigPage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - BadgeMenuComponentModule, - BackupReportPageModule, - FormsModule, - MonacoEditorModule, - ], - declarations: [DevConfigPage], -}) -export class DevConfigPageModule {} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.html b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.html deleted file mode 100644 index 688639533..000000000 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - Config - - - Preview - - - - - - - diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.scss b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts deleted file mode 100644 index 742214122..000000000 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-config/dev-config.page.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { Component } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { ModalController } from '@ionic/angular' -import { debounce, ErrorService } from '@start9labs/shared' -import * as yaml from 'js-yaml' -import { filter, take } from 'rxjs/operators' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { PatchDB } from 'patch-db-client' -import { getProjectId } from 'src/app/util/get-project-id' -import { DataModel } from 'src/app/services/patch-db/data-model' -import { FormDialogService } from '../../../services/form-dialog.service' -import { FormPage } from '../../../modals/form/form.page' - -@Component({ - selector: 'dev-config', - templateUrl: 'dev-config.page.html', - styleUrls: ['dev-config.page.scss'], -}) -export class DevConfigPage { - readonly projectId = getProjectId(this.route) - editorOptions = { theme: 'vs-dark', language: 'yaml' } - code: string = '' - saving: boolean = false - - constructor( - private readonly formDialog: FormDialogService, - private readonly errorHandler: ErrorService, - private readonly route: ActivatedRoute, - private readonly modalCtrl: ModalController, - private readonly patch: PatchDB, - private readonly api: ApiService, - ) {} - - ngOnInit() { - this.patch - .watch$('ui', 'dev', this.projectId, 'config') - .pipe(filter(Boolean), take(1)) - .subscribe(config => { - this.code = config - }) - } - - async preview() { - let doc: any - try { - doc = yaml.load(this.code) - } catch (e: any) { - this.errorHandler.handleError(e) - } - - this.formDialog.open(FormPage, { - label: 'Config Sample', - data: { - spec: JSON.parse(JSON.stringify(doc, null, 2)), - buttons: [ - { - text: 'OK', - handler: async () => true, - }, - ], - }, - }) - } - - @debounce(1000) - async save() { - this.saving = true - try { - await this.api.setDbValue( - ['dev', this.projectId, 'config'], - this.code, - ) - } catch (e: any) { - this.errorHandler.handleError(e) - } finally { - this.saving = false - } - } -} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.module.ts b/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.module.ts deleted file mode 100644 index ce15130e5..000000000 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { RouterModule, Routes } from '@angular/router' -import { DevInstructionsPage } from './dev-instructions.page' -import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' -import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module' -import { FormsModule } from '@angular/forms' -import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor' - -const routes: Routes = [ - { - path: '', - component: DevInstructionsPage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - BadgeMenuComponentModule, - BackupReportPageModule, - FormsModule, - MonacoEditorModule, - ], - declarations: [DevInstructionsPage], -}) -export class DevInstructionsPageModule {} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.html b/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.html deleted file mode 100644 index 6d8bee796..000000000 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.html +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - Instructions - - - Preview - - - - - - - diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.scss b/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts deleted file mode 100644 index bf3fa0b56..000000000 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-instructions/dev-instructions.page.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { Component } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { ModalController } from '@ionic/angular' -import { filter, take } from 'rxjs/operators' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { - debounce, - ErrorToastService, - MarkdownComponent, -} from '@start9labs/shared' -import { PatchDB } from 'patch-db-client' -import { getProjectId } from 'src/app/util/get-project-id' -import { DataModel } from 'src/app/services/patch-db/data-model' - -@Component({ - selector: 'dev-instructions', - templateUrl: 'dev-instructions.page.html', - styleUrls: ['dev-instructions.page.scss'], -}) -export class DevInstructionsPage { - readonly projectId = getProjectId(this.route) - editorOptions = { theme: 'vs-dark', language: 'markdown' } - code = '' - saving = false - - constructor( - private readonly route: ActivatedRoute, - private readonly errToast: ErrorToastService, - private readonly modalCtrl: ModalController, - private readonly patch: PatchDB, - private readonly api: ApiService, - ) {} - - ngOnInit() { - this.patch - .watch$('ui', 'dev', this.projectId, 'instructions') - .pipe(filter(Boolean), take(1)) - .subscribe(config => { - this.code = config - }) - } - - async preview() { - const modal = await this.modalCtrl.create({ - componentProps: { - title: 'Instructions Sample', - content: this.code, - }, - component: MarkdownComponent, - }) - - await modal.present() - } - - @debounce(1000) - async save() { - this.saving = true - try { - await this.api.setDbValue( - ['dev', this.projectId, 'instructions'], - this.code, - ) - } catch (e: any) { - this.errToast.present(e) - } finally { - this.saving = false - } - } -} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.module.ts b/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.module.ts deleted file mode 100644 index 49d738fe8..000000000 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.module.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { RouterModule, Routes } from '@angular/router' -import { DevManifestPage } from './dev-manifest.page' -import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' -import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module' -import { FormsModule } from '@angular/forms' -import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor' - -const routes: Routes = [ - { - path: '', - component: DevManifestPage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - BadgeMenuComponentModule, - BackupReportPageModule, - FormsModule, - MonacoEditorModule, - ], - declarations: [DevManifestPage], -}) -export class DevManifestPageModule {} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.html b/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.html deleted file mode 100644 index 599afca14..000000000 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.html +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - Manifest - - - - - - diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.scss b/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.ts deleted file mode 100644 index f039abbd8..000000000 --- a/frontend/projects/ui/src/app/pages/developer-routes/dev-manifest/dev-manifest.page.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { Component } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import * as yaml from 'js-yaml' -import { take } from 'rxjs/operators' -import { PatchDB } from 'patch-db-client' -import { getProjectId } from 'src/app/util/get-project-id' -import { DataModel } from 'src/app/services/patch-db/data-model' - -@Component({ - selector: 'dev-manifest', - templateUrl: 'dev-manifest.page.html', - styleUrls: ['dev-manifest.page.scss'], -}) -export class DevManifestPage { - readonly projectId = getProjectId(this.route) - editorOptions = { theme: 'vs-dark', language: 'yaml', readOnly: true } - manifest: string = '' - - constructor( - private readonly route: ActivatedRoute, - private readonly patch: PatchDB, - ) {} - - ngOnInit() { - this.patch - .watch$('ui', 'dev', this.projectId) - .pipe(take(1)) - .subscribe(devData => { - this.manifest = yaml.dump(devData['basic-info']) - }) - } -} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.module.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.module.ts deleted file mode 100644 index ea0c78423..000000000 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { RouterModule, Routes } from '@angular/router' -import { DeveloperListPage } from './developer-list.page' -import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' -import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module' - -const routes: Routes = [ - { - path: '', - component: DeveloperListPage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - BadgeMenuComponentModule, - BackupReportPageModule, - ], - declarations: [DeveloperListPage], -}) -export class DeveloperListPageModule {} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html deleted file mode 100644 index e00330448..000000000 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.html +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - Developer Tools - - - - - - - - Projects - - - - - Create project - - - - -

{{ entry.value.name }}

- - - -
-
diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.scss b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts deleted file mode 100644 index f5c5bcbb6..000000000 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-list/developer-list.page.ts +++ /dev/null @@ -1,279 +0,0 @@ -import { Component } from '@angular/core' -import { - ActionSheetButton, - ActionSheetController, - AlertController, - LoadingController, - ModalController, -} from '@ionic/angular' -import { - GenericInputComponent, - GenericInputOptions, -} from 'src/app/modals/generic-input/generic-input.component' -import { PatchDB } from 'patch-db-client' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { InputSpec } from 'start-sdk/lib/config/configTypes' -import * as yaml from 'js-yaml' -import { v4 } from 'uuid' -import { DataModel, DevData } from 'src/app/services/patch-db/data-model' -import { ErrorToastService } from '@start9labs/shared' -import { TuiDestroyService } from '@taiga-ui/cdk' -import { takeUntil } from 'rxjs/operators' - -@Component({ - selector: 'developer-list', - templateUrl: 'developer-list.page.html', - styleUrls: ['developer-list.page.scss'], - providers: [TuiDestroyService], -}) -export class DeveloperListPage { - devData: DevData = {} - - constructor( - private readonly modalCtrl: ModalController, - private readonly api: ApiService, - private readonly loadingCtrl: LoadingController, - private readonly errToast: ErrorToastService, - private readonly alertCtrl: AlertController, - private readonly destroy$: TuiDestroyService, - private readonly patch: PatchDB, - private readonly actionCtrl: ActionSheetController, - ) {} - - ngOnInit() { - this.patch - .watch$('ui', 'dev') - .pipe(takeUntil(this.destroy$)) - .subscribe(dd => { - this.devData = dd - }) - } - - async openCreateProjectModal() { - const projNumber = Object.keys(this.devData).length + 1 - const options: GenericInputOptions = { - title: 'Add new project', - message: 'Create a new dev project.', - label: 'New project', - useMask: false, - placeholder: `Project ${projNumber}`, - required: false, - initialValue: `Project ${projNumber}`, - buttonText: 'Save', - submitFn: (value: string) => this.createProject(value), - } - - const modal = await this.modalCtrl.create({ - componentProps: { options }, - cssClass: 'alertlike-modal', - presentingElement: await this.modalCtrl.getTop(), - component: GenericInputComponent, - }) - - await modal.present() - } - - async presentAction(id: string, event: Event) { - event.stopPropagation() - event.preventDefault() - const buttons: ActionSheetButton[] = [ - { - text: 'Edit Name', - icon: 'pencil', - handler: () => { - this.openEditNameModal(id) - }, - }, - { - text: 'Delete', - icon: 'trash', - role: 'destructive', - handler: () => { - this.presentAlertDelete(id) - }, - }, - ] - - const action = await this.actionCtrl.create({ - header: this.devData[id].name, - subHeader: 'Manage project', - mode: 'ios', - buttons, - }) - - await action.present() - } - - async openEditNameModal(id: string) { - const curName = this.devData[id].name - const options: GenericInputOptions = { - title: 'Edit Name', - message: 'Edit the name of your project.', - label: 'Name', - useMask: false, - placeholder: curName, - required: false, - initialValue: curName, - buttonText: 'Save', - submitFn: (value: string) => this.editName(id, value), - } - - const modal = await this.modalCtrl.create({ - componentProps: { options }, - cssClass: 'alertlike-modal', - presentingElement: await this.modalCtrl.getTop(), - component: GenericInputComponent, - }) - - await modal.present() - } - - async createProject(name: string) { - // fail silently if duplicate project name - if ( - Object.values(this.devData) - .map(v => v.name) - .includes(name) - ) - return - - const loader = await this.loadingCtrl.create({ - message: 'Creating Project...', - }) - await loader.present() - - try { - const id = v4() - const config = yaml - .dump(SAMPLE_CONFIG) - .replace(/warning:/g, '# Optional\n warning:') - - const def = { name, config, instructions: SAMPLE_INSTUCTIONS } - await this.api.setDbValue<{ - name: string - config: string - instructions: string - }>(['dev', id], def) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - } - - async presentAlertDelete(id: string) { - const alert = await this.alertCtrl.create({ - header: 'Caution', - message: `Are you sure you want to delete this project?`, - buttons: [ - { - text: 'Cancel', - role: 'cancel', - }, - { - text: 'Delete', - handler: () => { - this.delete(id) - }, - cssClass: 'enter-click', - }, - ], - }) - await alert.present() - } - - async editName(id: string, newName: string) { - const loader = await this.loadingCtrl.create({ - message: 'Saving...', - }) - await loader.present() - - try { - await this.api.setDbValue(['dev', id, 'name'], newName) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - } - - async delete(id: string) { - const loader = await this.loadingCtrl.create({ - message: 'Removing Project...', - }) - await loader.present() - - try { - const devDataToSave: DevData = JSON.parse(JSON.stringify(this.devData)) - delete devDataToSave[id] - await this.api.setDbValue(['dev'], devDataToSave) - } catch (e: any) { - this.errToast.present(e) - } finally { - loader.dismiss() - } - } -} - -const SAMPLE_INSTUCTIONS = `# Create Instructions using Markdown! :)` - -const SAMPLE_CONFIG: InputSpec = { - 'sample-string': { - type: 'text', - name: 'Example String Input', - inputmode: 'text', - required: true, - masked: false, - // optional - description: 'Example description for required string input.', - placeholder: 'Enter string value', - patterns: [ - { - regex: '^[a-zA-Z0-9! _]+$', - description: 'Must be alphanumeric (may contain underscore).', - }, - ], - minLength: null, - maxLength: null, - default: null, - warning: null, - }, - 'sample-number': { - type: 'number', - name: 'Example Number Input', - required: true, - min: 5, - max: 1000000, - step: '5', - integer: true, - // optional - warning: 'Example warning to display when changing this number value.', - units: 'ms', - description: 'Example description for optional number input.', - placeholder: 'Enter number value', - default: null, - }, - 'sample-boolean': { - type: 'toggle', - name: 'Example Boolean Toggle', - // optional - description: 'Example description for boolean toggle', - default: true, - warning: null, - }, - 'sample-select': { - type: 'multiselect', - name: 'Example Enum Select', - values: { - red: 'Red', - blue: 'Blue', - green: 'Green', - }, - // optional - warning: 'Example warning to display when changing this select value.', - description: 'Example description for select select', - minLength: null, - maxLength: 2, - default: ['red'], - }, -} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.module.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.module.ts deleted file mode 100644 index 286ab8178..000000000 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.module.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { NgModule } from '@angular/core' -import { CommonModule } from '@angular/common' -import { IonicModule } from '@ionic/angular' -import { RouterModule, Routes } from '@angular/router' -import { DeveloperMenuPage } from './developer-menu.page' -import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module' -import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module' -import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor' -import { FormsModule } from '@angular/forms' -import { SharedPipesModule } from '@start9labs/shared' - -const routes: Routes = [ - { - path: '', - component: DeveloperMenuPage, - }, -] - -@NgModule({ - imports: [ - CommonModule, - IonicModule, - RouterModule.forChild(routes), - BadgeMenuComponentModule, - BackupReportPageModule, - FormsModule, - MonacoEditorModule, - SharedPipesModule, - ], - declarations: [DeveloperMenuPage], -}) -export class DeveloperMenuPageModule {} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html deleted file mode 100644 index ee10e7fc6..000000000 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - {{ (projectData$ | async)?.name || '' }} - - View Manifest - - - - - - - - -

Basic Info

-

Complete basic info for your package

-
- - -
- - - -

Instructions Generator

-

Create instructions and see how they will appear to the end user

-
-
- - - -

Config Generator

-

Edit the config with YAML and see it in real time

-
-
-
diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.scss b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts deleted file mode 100644 index 7ebba9d90..000000000 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/developer-menu.page.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { ChangeDetectionStrategy, Component } from '@angular/core' -import { ActivatedRoute } from '@angular/router' -import { BasicInfo, getBasicInfoSpec } from './form-info' -import { PatchDB } from 'patch-db-client' -import { ApiService } from 'src/app/services/api/embassy-api.service' -import { ErrorService } from '@start9labs/shared' -import { getProjectId } from 'src/app/util/get-project-id' -import { DataModel, DevProjectData } from 'src/app/services/patch-db/data-model' -import { FormDialogService } from '../../../services/form-dialog.service' -import { FormPage } from '../../../modals/form/form.page' -import { LoadingService } from '../../../modals/loading/loading.service' - -@Component({ - selector: 'developer-menu', - templateUrl: 'developer-menu.page.html', - styleUrls: ['developer-menu.page.scss'], - changeDetection: ChangeDetectionStrategy.OnPush, -}) -export class DeveloperMenuPage { - readonly projectId = getProjectId(this.route) - readonly projectData$ = this.patch.watch$('ui', 'dev', this.projectId) - - constructor( - private readonly formDialog: FormDialogService, - private readonly loader: LoadingService, - private readonly errorHandler: ErrorService, - private readonly route: ActivatedRoute, - private readonly api: ApiService, - private readonly patch: PatchDB, - ) {} - - async openBasicInfoModal(data: DevProjectData) { - this.formDialog.open(FormPage, { - label: 'Basic Info', - data: { - spec: getBasicInfoSpec(data), - buttons: [ - { - text: 'Save', - handler: async (basicInfo: BasicInfo) => - this.saveBasicInfo(basicInfo), - }, - ], - }, - }) - } - - async saveBasicInfo(basicInfo: BasicInfo): Promise { - const loader = this.loader.open('Saving...').subscribe() - - try { - await this.api.setDbValue( - ['dev', this.projectId, 'basic-info'], - basicInfo, - ) - return true - } catch (e: any) { - this.errorHandler.handleError(e) - return false - } finally { - loader.unsubscribe() - } - } -} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/form-info.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/form-info.ts deleted file mode 100644 index 95bd29b1a..000000000 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-menu/form-info.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { InputSpec } from 'start-sdk/lib/config/configTypes' -import { DevProjectData } from 'src/app/services/patch-db/data-model' - -export type BasicInfo = { - id: string - title: string - 'service-version-number': string - 'release-notes': string - license: string - 'wrapper-repo': string - 'upstream-repo'?: string - 'support-site'?: string - 'marketing-site'?: string - description: { - short: string - long: string - } -} - -export function getBasicInfoSpec(devData: DevProjectData): InputSpec { - const basicInfo = devData['basic-info'] - return { - id: { - type: 'text', - inputmode: 'text', - name: 'ID', - description: 'The package identifier used by the OS', - placeholder: 'e.g. bitcoind', - required: true, - masked: false, - minLength: null, - maxLength: null, - patterns: [ - { - regex: '^([a-z][a-z0-9]*)(-[a-z0-9]+)*$', - description: 'Must be kebab case', - }, - ], - default: basicInfo?.id || '', - warning: null, - }, - title: { - type: 'text', - inputmode: 'text', - name: 'Service Name', - description: 'A human readable service title', - placeholder: 'e.g. Bitcoin Core', - required: true, - masked: false, - minLength: null, - maxLength: null, - patterns: [], - default: basicInfo ? basicInfo.title : devData.name, - warning: null, - }, - 'service-version-number': { - type: 'text', - inputmode: 'text', - name: 'Service Version', - description: - 'Service version - accepts up to four digits, where the last confirms to revisions necessary for StartOS - see documentation: https://github.com/Start9Labs/emver-rs. This value will change with each release of the service', - placeholder: 'e.g. 0.1.2.3', - required: true, - masked: false, - minLength: null, - maxLength: null, - patterns: [ - { - regex: '^([0-9]+).([0-9]+).([0-9]+).([0-9]+)$', - description: 'Must be valid Emver version', - }, - ], - default: basicInfo?.['service-version-number'] || '', - warning: null, - }, - description: { - type: 'object', - name: 'Marketplace Descriptions', - description: null, - warning: null, - spec: { - short: { - type: 'text', - inputmode: 'text', - name: 'Short Description', - description: - 'This is the first description visible to the user in the marketplace', - placeholder: null, - required: true, - masked: false, - default: basicInfo?.description?.short || '', - minLength: null, - maxLength: 320, - patterns: [], - warning: null, - }, - long: { - type: 'textarea', - name: 'Long Description', - description: `This description will display with additional details in the service's individual marketplace page`, - placeholder: null, - minLength: 20, - maxLength: 1000, - required: true, - warning: null, - }, - }, - }, - 'release-notes': { - type: 'text', - inputmode: 'text', - name: 'Release Notes', - description: - 'Markdown supported release notes for this version of this service.', - placeholder: 'e.g. Markdown _release notes_ for **Bitcoin Core**', - required: true, - minLength: null, - maxLength: null, - masked: false, - patterns: [], - default: basicInfo?.['release-notes'] || '', - warning: null, - }, - license: { - type: 'select', - name: 'License', - warning: null, - values: { - 'gnu-agpl-v3': 'GNU AGPLv3', - 'gnu-gpl-v3': 'GNU GPLv3', - 'gnu-lgpl-v3': 'GNU LGPLv3', - 'mozilla-public-license-2.0': 'Mozilla Public License 2.0', - 'apache-license-2.0': 'Apache License 2.0', - mit: 'mit', - 'boost-software-license-1.0': 'Boost Software License 1.0', - 'the-unlicense': 'The Unlicense', - custom: 'Custom', - }, - description: 'Example description for select', - required: true, - default: 'mit', - }, - 'wrapper-repo': { - type: 'text', - inputmode: 'url', - name: 'Wrapper Repo', - description: - 'The Start9 wrapper repository URL for the package. This repo contains the manifest file (this), any scripts necessary for configuration, backups, actions, or health checks', - placeholder: 'e.g. www.github.com/example', - patterns: [], - minLength: null, - maxLength: null, - required: true, - masked: false, - default: basicInfo?.['wrapper-repo'] || '', - warning: null, - }, - 'upstream-repo': { - type: 'text', - inputmode: 'url', - name: 'Upstream Repo', - description: 'The original project repository URL', - placeholder: 'e.g. www.github.com/example', - patterns: [], - minLength: null, - maxLength: null, - required: false, - masked: false, - default: basicInfo?.['upstream-repo'] || '', - warning: null, - }, - 'support-site': { - type: 'text', - inputmode: 'url', - name: 'Support Site', - description: 'URL to the support site / channel for the project', - placeholder: 'e.g. start9.com/support', - patterns: [], - minLength: null, - maxLength: null, - required: false, - masked: false, - default: basicInfo?.['support-site'] || '', - warning: null, - }, - 'marketing-site': { - type: 'text', - inputmode: 'url', - name: 'Website', - description: 'URL to the marketing site / channel for the project', - placeholder: 'e.g. start9.com', - patterns: [], - minLength: null, - maxLength: null, - required: false, - masked: false, - default: basicInfo?.['marketing-site'] || '', - warning: null, - }, - } -} diff --git a/frontend/projects/ui/src/app/pages/developer-routes/developer-routing.module.ts b/frontend/projects/ui/src/app/pages/developer-routes/developer-routing.module.ts deleted file mode 100644 index 03720d87d..000000000 --- a/frontend/projects/ui/src/app/pages/developer-routes/developer-routing.module.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { NgModule } from '@angular/core' -import { Routes, RouterModule } from '@angular/router' - -const routes: Routes = [ - { - path: '', - redirectTo: 'projects', - pathMatch: 'full', - }, - { - path: 'projects', - loadChildren: () => - import('./developer-list/developer-list.module').then( - m => m.DeveloperListPageModule, - ), - }, - { - path: 'projects/:projectId', - loadChildren: () => - import('./developer-menu/developer-menu.module').then( - m => m.DeveloperMenuPageModule, - ), - }, - { - path: 'projects/:projectId/config', - loadChildren: () => - import('./dev-config/dev-config.module').then(m => m.DevConfigPageModule), - }, - { - path: 'projects/:projectId/instructions', - loadChildren: () => - import('./dev-instructions/dev-instructions.module').then( - m => m.DevInstructionsPageModule, - ), - }, - { - path: 'projects/:projectId/manifest', - loadChildren: () => - import('./dev-manifest/dev-manifest.module').then( - m => m.DevManifestPageModule, - ), - }, -] - -@NgModule({ - imports: [RouterModule.forChild(routes)], - exports: [RouterModule], -}) -export class DeveloperRoutingModule {} diff --git a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts index 089b66139..ab76282c0 100644 --- a/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts +++ b/frontend/projects/ui/src/app/pages/marketplace-routes/marketplace-list/marketplace-list.page.ts @@ -1,7 +1,8 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core' import { ActivatedRoute } from '@angular/router' -import { ModalController } from '@ionic/angular' import { AbstractMarketplaceService } from '@start9labs/marketplace' +import { TuiDialogService } from '@taiga-ui/core' +import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus' import { PatchDB } from 'patch-db-client' import { map } from 'rxjs' import { MarketplaceSettingsPage } from 'src/app/modals/marketplace-settings/marketplace-settings.page' @@ -73,7 +74,7 @@ export class MarketplaceListPage { private readonly patch: PatchDB, @Inject(AbstractMarketplaceService) private readonly marketplaceService: MarketplaceService, - private readonly modalCtrl: ModalController, + private readonly dialogs: TuiDialogService, private readonly config: ConfigService, private readonly route: ActivatedRoute, ) {} @@ -81,11 +82,12 @@ export class MarketplaceListPage { category = 'featured' query = '' - async presentModalMarketplaceSettings() { - const modal = await this.modalCtrl.create({ - component: MarketplaceSettingsPage, - }) - await modal.present() + presentModalMarketplaceSettings() { + this.dialogs + .open(new PolymorpheusComponent(MarketplaceSettingsPage), { + label: 'Change Registry', + }) + .subscribe() } onCategoryChange(category: string): void { diff --git a/frontend/projects/ui/src/app/pages/notifications/notifications.page.html b/frontend/projects/ui/src/app/pages/notifications/notifications.page.html index f9e697107..a09112cd5 100644 --- a/frontend/projects/ui/src/app/pages/notifications/notifications.page.html +++ b/frontend/projects/ui/src/app/pages/notifications/notifications.page.html @@ -87,7 +87,6 @@

- {{ $any(packageData[pkgId])?.manifest.title || pkgId }} - {{ not.title }} diff --git a/frontend/projects/ui/src/app/pages/server-routes/email/email.module.ts b/frontend/projects/ui/src/app/pages/server-routes/email/email.module.ts new file mode 100644 index 000000000..483c8605d --- /dev/null +++ b/frontend/projects/ui/src/app/pages/server-routes/email/email.module.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core' +import { CommonModule } from '@angular/common' +import { IonicModule } from '@ionic/angular' +import { TuiButtonModule, TuiNotificationModule } from '@taiga-ui/core' +import { EmailPage } from './email.page' +import { Routes, RouterModule } from '@angular/router' +import { FormsModule, ReactiveFormsModule } from '@angular/forms' +import { FormModule } from 'src/app/components/form/form.module' +import { TuiInputModule } from '@taiga-ui/kit' + +const routes: Routes = [ + { + path: '', + component: EmailPage, + }, +] + +@NgModule({ + imports: [ + CommonModule, + IonicModule, + TuiButtonModule, + FormModule, + FormsModule, + ReactiveFormsModule, + TuiInputModule, + TuiNotificationModule, + RouterModule.forChild(routes), + ], + declarations: [EmailPage], +}) +export class EmailPageModule {} diff --git a/frontend/projects/ui/src/app/pages/server-routes/email/email.page.html b/frontend/projects/ui/src/app/pages/server-routes/email/email.page.html new file mode 100644 index 000000000..4ccb053d9 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/server-routes/email/email.page.html @@ -0,0 +1,63 @@ + + + + + + Email + + + + +
+ + Adding SMTP credentials to StartOS enables StartOS and some services to + send you emails. + + View instructions + + + +
+ SMTP Credentials + +
+ +
+
+
+ Test Email + + Firstname Lastname <email@example.com> + + +
+ +
+
+
+
diff --git a/frontend/projects/ui/src/app/pages/server-routes/email/email.page.scss b/frontend/projects/ui/src/app/pages/server-routes/email/email.page.scss new file mode 100644 index 000000000..4cceda182 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/server-routes/email/email.page.scss @@ -0,0 +1,11 @@ +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; +} diff --git a/frontend/projects/ui/src/app/pages/server-routes/email/email.page.ts b/frontend/projects/ui/src/app/pages/server-routes/email/email.page.ts new file mode 100644 index 000000000..f55682b87 --- /dev/null +++ b/frontend/projects/ui/src/app/pages/server-routes/email/email.page.ts @@ -0,0 +1,76 @@ +import { Component } from '@angular/core' +import { ApiService } from 'src/app/services/api/embassy-api.service' +import { ErrorService } from '@start9labs/shared' +import { PatchDB } from 'patch-db-client' +import { DataModel } from 'src/app/services/patch-db/data-model' +import { FormService } from 'src/app/services/form.service' +import { LoadingService } from '../../../modals/loading/loading.service' +import { TuiDialogService } from '@taiga-ui/core' +import { switchMap } from 'rxjs/operators' +import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes' +import { configBuilderToSpec } from 'src/app/util/configBuilderToSpec' +import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants' +import { UntypedFormGroup } from '@angular/forms' + +@Component({ + selector: 'email', + templateUrl: './email.page.html', + styleUrls: ['./email.page.scss'], +}) +export class EmailPage { + spec: Promise = configBuilderToSpec(customSmtp) + testAddress = '' + readonly form$ = this.patch + .watch$('server-info', 'smtp') + .pipe( + switchMap(async value => + this.formService.createForm(await this.spec, value), + ), + ) + + constructor( + private readonly dialogs: TuiDialogService, + private readonly loader: LoadingService, + private readonly errorService: ErrorService, + private readonly patch: PatchDB, + private readonly api: ApiService, + private readonly formService: FormService, + ) {} + + async save(value: unknown): Promise { + const loader = this.loader.open('Saving...').subscribe() + + try { + await this.api.configureEmail(customSmtp.validator.unsafeCast(value)) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + } + + async sendTestEmail(form: UntypedFormGroup) { + const loader = this.loader.open('Sending...').subscribe() + + try { + await this.api.testEmail({ + to: this.testAddress, + ...form.value, + }) + } catch (e: any) { + this.errorService.handleError(e) + } finally { + loader.unsubscribe() + } + + this.dialogs + .open( + `A test email has been sent to ${this.testAddress}.

Check your spam folder and mark as not spam`, + { + label: 'Success', + size: 's', + }, + ) + .subscribe() + } +} diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-routing.module.ts b/frontend/projects/ui/src/app/pages/server-routes/server-routing.module.ts index d6b42bfec..269ef563a 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-routing.module.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-routing.module.ts @@ -73,6 +73,11 @@ const routes: Routes = [ m => m.ExperimentalFeaturesPageModule, ), }, + { + path: 'email', + loadChildren: () => + import('./email/email.module').then(m => m.EmailPageModule), + }, ] @NgModule({ diff --git a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts index 262e06f9c..556f67372 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/server-show/server-show.page.ts @@ -387,6 +387,15 @@ export class ServerShowPage { detail: true, disabled$: of(false), }, + { + title: 'Email', + description: 'Provide an external SMTP server for sending emails', + icon: 'mail-outline', + action: () => + this.navCtrl.navigateForward(['email'], { relativeTo: this.route }), + detail: true, + disabled$: of(false), + }, { title: 'WiFi', description: 'Add or remove WiFi networks', diff --git a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifiSpec.ts b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.const.ts similarity index 81% rename from frontend/projects/ui/src/app/pages/server-routes/wifi/wifiSpec.ts rename to frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.const.ts index 5945a6bfb..7c9e9540c 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifiSpec.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.const.ts @@ -1,4 +1,4 @@ -import { ValueSpecObject } from 'start-sdk/lib/config/configTypes' +import { ValueSpecObject } from '@start9labs/start-sdk/lib/config/configTypes' export const wifiSpec: ValueSpecObject = { type: 'object', @@ -20,6 +20,9 @@ export const wifiSpec: ValueSpecObject = { masked: false, default: null, warning: null, + disabled: false, + immutable: false, + generate: null, }, password: { type: 'text', @@ -39,6 +42,9 @@ export const wifiSpec: ValueSpecObject = { masked: true, default: null, warning: null, + disabled: false, + immutable: false, + generate: null, }, }, } diff --git a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.module.ts b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.module.ts index 7e2f6285f..902824ff9 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.module.ts +++ b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.module.ts @@ -4,6 +4,7 @@ import { IonicModule } from '@ionic/angular' import { RouterModule, Routes } from '@angular/router' import { WifiPage, ToWifiIconPipe } from './wifi.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: [WifiPage, ToWifiIconPipe], }) diff --git a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html index 625d11264..5680a3696 100644 --- a/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html +++ b/frontend/projects/ui/src/app/pages/server-routes/wifi/wifi.page.html @@ -8,13 +8,13 @@ -
+

- Adding WiFi credentials to your Embassy allows you to remove the - Ethernet cable and move the device anywhere you want. Embassy will + Adding WiFi credentials to StartOS allows you to remove the Ethernet + cable and move the device anywhere you want. StartOS will automatically connect to available networks. = { - version: 2, - data: { - lndconnect: { - type: 'text', - inputmode: 'text', - description: 'This is some information about the thing.', - copyable: true, - qr: true, - masked: true, - value: - 'lndconnect://udlyfq2mxa4355pt7cqlrdipnvk2tsl4jtsdw7zaeekenufwcev2wlad.onion:10009?cert=MIICJTCCAcugAwIBAgIRAOyq85fqAiA3U3xOnwhH678wCgYIKoZIzj0EAwIwODEfMB0GAkUEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMB4XDTIwMTAyNjA3MzEyN1oXDTIxMTIyMTA3MzEyN1owODEfMB0GA1UEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKqfhAMMZdY-eFnU5P4bGrQTSx0lo7m8u4V0yYkzUM6jlql_u31_mU2ovLTj56wnZApkEjoPl6fL2yasZA2wiy6OBtTCBsjAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH_BAUwAwEB_zAdBgNVHQ4EFgQUYQ9uIO6spltnVCx4rLFL5BvBF9IwWwYDVR0RBFQwUoIMNTc0OTkwMzIyYzZlgglsb2NhbGhvc3SCBHVuaXiCCnVuaXhwYWNrZXSCB2J1ZmNvbm6HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAGHBKwSAAswCgYIKoZIzj0EAwIDSAAwRQIgVZH2Z2KlyAVY2Q2aIQl0nsvN-OEN49wreFwiBqlxNj4CIQD5_JbpuBFJuf81I5J0FQPtXY-4RppWOPZBb-y6-rkIUQ&macaroon=AgEDbG5kAusBAwoQuA8OUMeQ8Fr2h-f65OdXdRIBMBoWCgdhZGRyZXNzEgRyZWFkEgV3cml0ZRoTCgRpbmZvEgRyZWFkEgV3cml0ZRoXCghpbnZvaWNlcxIEcmVhZBIFd3JpdGUaFAoIbWFjYXJvb24SCGdlbmVyYXRlGhYKB21lc3NhZ2USBHJlYWQSBXdyaXRlGhcKCG9mZmNoYWluEgRyZWFkEgV3cml0ZRoWCgdvbmNoYWluEgRyZWFkEgV3cml0ZRoUCgVwZWVycxIEcmVhZBIFd3JpdGUaGAoGc2lnbmVyEghnZW5lcmF0ZRIEcmVhZAAABiCYsRUoUWuAHAiCSLbBR7b_qULDSl64R8LIU2aqNIyQfA', - }, - Nested: { - type: 'object', - description: 'This is a nested thing metric', - value: { - 'Last Name': { - type: 'text', - inputmode: 'text', - description: 'The last name of the user', - copyable: true, - qr: true, - masked: false, - value: 'Hill', + export const getInputSpec = async (): Promise< + RR.GetPackageConfigRes['spec'] + > => + configBuilderToSpec( + Config.of({ + bitcoin: Value.object( + { + name: 'Bitcoin Settings', + description: + 'RPC and P2P interface configuration options for Bitcoin Core', }, - Age: { - type: 'text', - inputmode: 'text', - description: 'The age of the user', - copyable: false, - qr: false, - masked: false, - value: '35', - }, - Password: { - type: 'text', - inputmode: 'text', - description: 'A secret password', - copyable: true, - qr: false, - masked: true, - value: 'password123', - }, - }, - }, - 'Another Value': { - type: 'text', - inputmode: 'text', - description: 'Some more information about the service.', - copyable: false, - qr: true, - masked: false, - value: 'https://guessagain.com', - }, - }, - } as any // @TODO why is this necessary? - - export const InputSpec: RR.GetPackageConfigRes['spec'] = { - bitcoin: { - type: 'object', - name: 'Bitcoin Settings', - description: - 'RPC and P2P interface configuration options for Bitcoin Core', - warning: null, - spec: { - 'bitcoind-p2p': { - type: 'union', - name: 'Bitcoin Core P2P', - description: - '

The Bitcoin Core node to connect to over the peer-to-peer (P2P) interface:

  • Bitcoin Core: The Bitcoin Core service installed on this device
  • External Node: A Bitcoin node running on a different device
', - warning: null, - default: null, - required: true, - variants: { - internal: { name: 'Internal', spec: {} }, - external: { - name: 'External', - spec: { - 'p2p-host': { - type: 'text', - inputmode: 'text', - name: 'Public Address', - description: 'The public address of your Bitcoin Core server', - required: true, - masked: false, - placeholder: null, - minLength: 4, - maxLength: 20, - patterns: [], - warning: null, - default: null, - }, - 'p2p-port': { - type: 'number', - name: 'P2P Port', - description: - 'The port that your Bitcoin Core P2P server is bound to', - required: true, - min: 0, - max: 65535, - integer: true, - step: '1', - default: 8333, - placeholder: null, - warning: null, - units: null, - }, - }, - }, - }, - }, - }, - }, - background: { - name: 'Background', - type: 'color', - description: 'Background color for the service', - warning: null, - required: false, - default: '#000000', - }, - chronos: { - name: 'Chronos', - type: 'object', - description: 'Various time related settings', - warning: null, - spec: { - time: { - name: 'Time', - type: 'datetime', - inputmode: 'time', - description: 'Time of day', - warning: null, - required: true, - min: '12:00', - max: '16:00', - step: null, - default: '12:00', - }, - date: { - name: 'Date', - type: 'datetime', - inputmode: 'date', - description: 'Just a date', - warning: null, - required: true, - min: '2023-01-01', - max: '2023-12-31', - step: null, - default: '2023-05-01', - }, - datetime: { - name: 'Date and time', - type: 'datetime', - inputmode: 'datetime-local', - description: 'Both date and time', - warning: null, - required: true, - min: '2023-01-01T12:00', - max: '2023-12-31T16:00', - step: null, - default: '2023-05-01T18:30', - }, - }, - }, - advanced: { - name: 'Advanced', - type: 'object', - description: 'Advanced settings', - warning: null, - spec: { - rpcsettings: { - name: 'RPC Settings', - type: 'object', - description: 'rpc username and password', - warning: - 'Adding RPC users gives them special permissions on your node.', - spec: { - rpcuser2: { - name: 'RPC Username', - type: 'text', - inputmode: 'text', - description: 'rpc username', - warning: null, - placeholder: null, - minLength: null, - maxLength: null, - required: true, - default: 'defaultrpcusername', - patterns: [ - { - regex: '^[a-zA-Z]+$', - description: 'must contain only letters.', - }, - ], - masked: false, - }, - rpcuser: { - name: 'RPC Username', - type: 'text', - inputmode: 'text', - description: 'rpc username', - warning: null, - placeholder: null, - required: true, - minLength: null, - maxLength: null, - default: 'defaultrpcusername', - patterns: [ - { - regex: '^[a-zA-Z]+$', - description: 'must contain only letters.', - }, - ], - masked: false, - }, - rpcpass: { - name: 'RPC User Password', - type: 'text', - inputmode: 'text', - description: 'rpc password', - placeholder: null, - minLength: null, - maxLength: null, - warning: null, - required: true, - default: { - charset: 'a-z,A-Z,2-9', - len: 20, - }, - masked: true, - patterns: [], - }, - rpcpass2: { - name: 'RPC User Password', - type: 'text', - inputmode: 'text', - description: 'rpc password', - warning: null, - placeholder: null, - minLength: null, - maxLength: null, - required: true, - default: { - charset: 'a-z,A-Z,2-9', - len: 20, - }, - masked: true, - patterns: [], - }, - }, - }, - }, - }, - bio: { - name: 'Bio', - type: 'textarea', - description: 'Your personal bio', - placeholder: 'Tell the world about yourself', - minLength: null, - maxLength: null, - warning: null, - required: false, - }, - testnet: { - name: 'Testnet', - type: 'toggle', - description: - '
  • determines whether your node is running on testnet or mainnet
', - warning: 'Chain will have to resync!', - default: true, - }, - document: { - name: 'Needed File', - type: 'file', - description: 'A file we need', - warning: 'Testing warning', - required: true, - extensions: ['.png'], - }, - 'object-list': { - name: 'Object List', - type: 'list', - description: 'This is a list of objects, like users or something', - warning: null, - minLength: null, - maxLength: 4, - default: [ - { - 'first-name': 'Admin', - 'last-name': 'User', - age: 40, - }, - { - 'first-name': 'Admin2', - 'last-name': 'User', - age: 40, - }, - ], - // the outer spec here, at the list level, says that what's inside (the inner spec) pertains to its inner elements. - // it just so happens that ValueSpecObject's have the field { spec: InputSpec } - spec: { - type: 'object', - uniqueBy: 'last-name', - displayAs: `I'm {{last-name}}, {{first-name}} {{last-name}}`, - spec: { - 'first-name': { - name: 'First Name', - type: 'text', - inputmode: 'text', - description: 'User first name', - required: false, - masked: false, - minLength: 4, - maxLength: 15, - placeholder: null, - patterns: [], - warning: null, - default: null, - }, - 'last-name': { - name: 'Last Name', - type: 'text', - inputmode: 'text', - description: 'User first name', - minLength: null, - maxLength: null, - required: false, - default: { - charset: 'a-g,2-9', - len: 12, - }, - patterns: [ + Config.of({ + 'bitcoind-p2p': Value.union( { - regex: '^[a-zA-Z]+$', - description: 'must contain only letters.', + name: 'P2P Settings', + description: + '

The Bitcoin Core node to connect to over the peer-to-peer (P2P) interface:

  • Bitcoin Core: The Bitcoin Core service installed on this device
  • External Node: A Bitcoin node running on a different device
', + required: { default: 'internal' }, }, - ], - masked: false, - placeholder: null, - warning: null, - }, - age: { - name: 'Age', - type: 'number', - description: 'The age of the user', - required: false, - integer: false, - warning: 'User must be at least 18.', - min: 18, - max: null, - step: null, - units: null, - placeholder: null, - default: null, - }, - }, - }, - }, - 'random-select': { - name: 'Random Select', - type: 'select', - values: { - hello: 'Hello', - goodbye: 'Goodbye', - sup: 'Sup', - }, - default: 'sup', - description: 'This is not even real.', - warning: 'Be careful changing this!', - required: true, - }, - notifications: { - name: 'Notification Preferences', - type: 'multiselect', - description: 'how you want to be notified', - warning: null, - minLength: 2, - maxLength: 3, - values: { - email: 'EEEEmail', - text: 'Texxxt', - call: 'Ccccall', - push: 'PuuuusH', - webhook: 'WebHooookkeee', - }, - default: ['email'], - }, - 'favorite-number': { - name: 'Favorite Number', - type: 'number', - integer: false, - description: 'Your favorite number of all time', - placeholder: null, - warning: - 'Once you set this number, it can never be changed without severe consequences.', - required: false, - default: 7, - min: -99, - max: 100, - step: 'all', - units: 'BTC', - }, - 'unlucky-numbers': { - name: 'Unlucky Numbers', - type: 'list', - description: 'Numbers that you like but are not your top favorite.', - warning: null, - spec: { - type: 'number', - integer: false, - min: -10, - max: 10, - step: null, - units: null, - placeholder: null, - }, - minLength: null, - maxLength: 10, - default: [2, 3], - }, - rpcsettings: { - name: 'RPC Settings', - type: 'object', - description: 'rpc username and password', - warning: 'Adding RPC users gives them special permissions on your node.', - spec: { - laws: { - name: 'Laws', - type: 'object', - description: 'the law of the realm', - warning: null, - spec: { - law1: { - name: 'First Law', - type: 'text', - inputmode: 'text', - description: 'the first law', - required: false, - masked: false, - minLength: null, - maxLength: null, - placeholder: null, - patterns: [], - warning: null, - default: null, - }, - law2: { - name: 'Second Law', - type: 'text', - inputmode: 'text', - description: 'the second law', - required: false, - masked: false, - minLength: null, - maxLength: null, - placeholder: null, - patterns: [], - warning: null, - default: null, - }, - }, - }, - rulemakers: { - name: 'Rule Makers', - type: 'list', - description: 'the people who make the rules', - warning: null, - minLength: 1, - maxLength: 3, - default: [], - spec: { - type: 'object', - uniqueBy: null, - displayAs: null, - spec: { - rulemakername: { - name: 'Rulemaker Name', - type: 'text', - inputmode: 'text', - description: 'the name of the rule maker', - required: true, - minLength: null, - maxLength: 30, - default: { - charset: 'a-g,2-9', - len: 12, + Variants.of({ + internal: { name: 'Bitcoin Core', spec: Config.of({}) }, + external: { + name: 'External Node', + spec: Config.of({ + 'p2p-host': Value.text({ + name: 'Public Address', + required: { + default: null, + }, + description: + 'The public address of your Bitcoin Core server', + }), + 'p2p-port': Value.number({ + name: 'P2P Port', + description: + 'The port that your Bitcoin Core P2P server is bound to', + required: { + default: 8333, + }, + min: 0, + max: 65535, + integer: true, + }), + }), }, - masked: false, - placeholder: null, - patterns: [], - warning: null, + }), + ), + }), + ), + advanced: Value.object( + { + name: 'Advanced', + description: 'Advanced settings', + }, + Config.of({ + rpcsettings: Value.object( + { + name: 'RPC Settings', + description: 'rpc username and password', + warning: + 'Adding RPC users gives them special permissions on your node.', }, - rulemakerip: { - name: 'Rulemaker IP', - type: 'text', - inputmode: 'text', - description: 'the ip of the rule maker', - required: true, - default: '192.168.1.0', - minLength: 4, - maxLength: 20, - patterns: [ - { - regex: - '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$', - description: 'may only contain numbers and periods', + Config.of({ + rpcuser2: Value.text({ + name: 'RPC Username', + required: { + default: 'defaultrpcusername', }, - ], - masked: false, - placeholder: null, - warning: null, - }, - }, - }, - }, - rpcuser: { - name: 'RPC Username', - type: 'text', - inputmode: 'text', - description: 'rpc username', - required: true, - minLength: null, - maxLength: null, - default: 'defaultrpcusername', - patterns: [ - { - regex: '^[a-zA-Z]+$', - description: 'must contain only letters.', - }, - ], - masked: false, - placeholder: null, - warning: null, - }, - rpcpass: { - name: 'RPC User Password', - type: 'text', - inputmode: 'text', - description: 'rpc password', - minLength: null, - maxLength: null, - required: true, - default: { - charset: 'a-z,A-Z,2-9', - len: 20, - }, - masked: true, - placeholder: null, - patterns: [], - warning: null, - }, - }, - }, - 'bitcoin-node': { - type: 'union', - default: 'internal', - name: 'Bitcoin Node Settings', - description: 'Options
  • Item 1
  • Item 2
', - warning: 'Careful changing this', - required: true, - variants: { - dummy: { - name: 'Dummy', - spec: { - name: { - type: 'text', - inputmode: 'text', - name: 'Name', - description: null, - minLength: null, - maxLength: null, - required: true, - masked: false, - patterns: [ - { - regex: '^[a-zA-Z]+$', - description: 'must contain only letters.', - }, - ], - placeholder: null, - warning: null, - default: null, - }, - }, - }, - internal: { name: 'Internal', spec: {} }, - external: { - name: 'External', - spec: { - 'emergency-contact': { - name: 'Emergency Contact', - type: 'object', - description: 'The person to contact in case of emergency.', - warning: null, - spec: { - name: { - type: 'text', - inputmode: 'text', - name: 'Name', - description: null, - required: true, - minLength: null, - maxLength: null, - masked: false, + description: 'rpc username', patterns: [ { regex: '^[a-zA-Z]+$', description: 'must contain only letters.', }, ], - placeholder: null, - warning: null, - default: null, - }, - email: { - type: 'text', - inputmode: 'text', - name: 'Email', - description: null, - required: true, - minLength: null, - maxLength: null, - masked: false, - placeholder: null, - patterns: [], - warning: null, - default: null, - }, - }, - }, - 'public-domain': { - name: 'Public Domain', - type: 'text', - inputmode: 'text', - description: 'the public address of the node', - required: true, - default: 'bitcoinnode.com', - minLength: null, - maxLength: null, - patterns: [], - masked: false, - placeholder: null, - warning: null, - }, - 'private-domain': { - name: 'Private Domain', - type: 'text', - inputmode: 'text', - description: 'the private address of the node', - required: true, - minLength: null, - maxLength: null, - masked: true, - placeholder: null, - patterns: [], - warning: null, - default: null, - }, - }, - }, - }, - }, - port: { - name: 'Port', - type: 'number', - integer: true, - description: - 'the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444', - warning: null, - required: true, - min: 1, - max: 9999, - step: '1', - units: null, - placeholder: null, - default: null, - }, - 'favorite-slogan': { - name: 'Favorite Slogan', - type: 'text', - inputmode: 'text', - description: - 'You most favorite slogan in the whole world, used for paying you.', - required: false, - masked: true, - minLength: null, - maxLength: null, - placeholder: null, - patterns: [], - warning: null, - default: null, - }, - rpcallowip: { - name: 'RPC Allowed IPs', - type: 'list', - description: - 'external ip addresses that are authorized to access your Bitcoin node', - warning: - 'Any IP you allow here will have RPC access to your Bitcoin node.', - minLength: 1, - maxLength: 10, - default: ['192.168.1.1'], - spec: { - type: 'text', - inputmode: 'text', - masked: false, - placeholder: null, - minLength: 4, - maxLength: 20, - patterns: [ - { - regex: - '((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))', - description: 'must be a valid ipv4, ipv6, or domain name', - }, - ], - }, - }, - rpcauth: { - name: 'RPC Auth', - type: 'list', - description: 'api keys that are authorized to access your Bitcoin node.', - warning: null, - minLength: null, - maxLength: null, - default: [], - spec: { - type: 'text', - inputmode: 'text', - masked: false, - minLength: null, - maxLength: null, - placeholder: null, - patterns: [], - }, - }, - 'more-advanced': { - name: 'More Advanced', - type: 'object', - description: 'Advanced settings', - warning: null, - spec: { - rpcsettings: { - name: 'RPC Settings', - type: 'object', - description: 'rpc username and password', - warning: - 'Adding RPC users gives them special permissions on your node.', - spec: { - laws: { - name: 'Laws', - type: 'object', - description: 'the law of the realm', - warning: null, - spec: { - law1: { - name: 'First Law', - type: 'text', - inputmode: 'text', - description: 'the first law', - required: false, - masked: false, - placeholder: null, - patterns: [], - minLength: null, - maxLength: null, - warning: null, - default: null, - }, - law2: { - name: 'Second Law', - type: 'text', - inputmode: 'text', - description: 'the second law', - required: false, - masked: false, - placeholder: null, - patterns: [], - minLength: null, - maxLength: null, - warning: null, - default: null, - }, - law4: { - name: 'Fourth Law', - type: 'text', - inputmode: 'text', - description: 'the fourth law', - required: false, - masked: false, - placeholder: null, - patterns: [], - minLength: null, - maxLength: null, - warning: null, - default: null, - }, - law3: { - name: 'Third Law', - type: 'list', - description: 'the third law', - warning: null, - minLength: null, - maxLength: 2, - default: [], - spec: { - type: 'object', - uniqueBy: null, - displayAs: null, - spec: { - lawname: { - name: 'Law Name', - type: 'text', - inputmode: 'text', - description: 'the name of the law maker', - required: true, - default: { - charset: 'a-g,2-9', - len: 12, - }, - masked: false, - placeholder: null, - patterns: [], - minLength: null, - maxLength: null, - warning: null, - }, - lawagency: { - name: 'Law agency', - type: 'text', - inputmode: 'text', - description: 'the ip of the law maker', - required: true, - default: '192.168.1.0', - minLength: null, - maxLength: null, - patterns: [ - { - regex: - '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$', - description: 'may only contain numbers and periods', - }, - ], - masked: false, - placeholder: null, - warning: null, - }, + }), + rpcuser: Value.text({ + name: 'RPC Username', + required: { + default: 'defaultrpcusername', + }, + description: 'rpc username', + patterns: [ + { + regex: '^[a-zA-Z]+$', + description: 'must contain only letters.', + }, + ], + }), + rpcpass: Value.text({ + name: 'RPC User Password', + required: { + default: { + charset: 'a-z,A-Z,2-9', + len: 20, }, }, - }, - law5: { - name: 'Fifth Law', - type: 'text', - inputmode: 'text', - description: 'the fifth law', - required: false, - masked: false, - placeholder: null, - minLength: null, - maxLength: null, - patterns: [], - warning: null, - default: null, - }, - }, + description: 'rpc password', + }), + rpcpass2: Value.text({ + name: 'RPC User Password', + required: { + default: { + charset: 'a-z,A-Z,2-9', + len: 20, + }, + }, + description: 'rpc password', + }), + }), + ), + }), + ), + testnet: Value.toggle({ + name: 'Testnet', + default: true, + description: + '
  • determines whether your node is running on testnet or mainnet
', + warning: 'Chain will have to resync!', + }), + 'object-list': Value.list( + List.obj( + { + name: 'Object List', + minLength: 0, + maxLength: 4, + default: [ + // { 'first-name': 'Admin', 'last-name': 'User', age: 40 }, + // { 'first-name': 'Admin2', 'last-name': 'User', age: 40 }, + ], + description: 'This is a list of objects, like users or something', }, - rulemakers: { - name: 'Rule Makers', - type: 'list', - description: 'the people who make the rules', - warning: null, - minLength: null, - maxLength: 2, - default: [], - spec: { - type: 'object', - uniqueBy: null, - displayAs: null, - spec: { - rulemakername: { - name: 'Rulemaker Name', - type: 'text', - inputmode: 'text', - description: 'the name of the rule maker', - required: true, + { + spec: Config.of({ + 'first-name': Value.text({ + name: 'First Name', + required: false, + description: 'User first name', + }), + 'last-name': Value.text({ + name: 'Last Name', + required: { default: { charset: 'a-g,2-9', len: 12, }, - masked: false, - placeholder: null, - patterns: [], - minLength: null, - maxLength: null, - warning: null, }, - rulemakerip: { - name: 'Rulemaker IP', - type: 'text', - inputmode: 'text', - description: 'the ip of the rule maker', - required: true, - default: '192.168.1.0', - minLength: null, - maxLength: null, - patterns: [ - { - regex: - '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$', - description: 'may only contain numbers and periods', - }, - ], - masked: false, - placeholder: null, - warning: null, - }, - }, - }, + description: 'User first name', + patterns: [ + { + regex: '^[a-zA-Z]+$', + description: 'must contain only letters.', + }, + ], + }), + age: Value.number({ + name: 'Age', + description: 'The age of the user', + warning: 'User must be at least 18.', + required: false, + min: 18, + integer: false, + }), + }), + displayAs: 'I\'m {{last-name}}, {{first-name}} {{last-name}}', + uniqueBy: 'last-name', }, - rpcuser: { + ), + ), + 'union-list': Value.list( + List.obj( + { + name: 'Union List', + minLength: 0, + maxLength: 2, + default: [], + description: 'This is a sample list of unions', + warning: 'If you change this, things may work.', + }, + { + spec: Config.of({ + /* TODO: Convert range for this value ([0, 2])*/ + union: Value.union( + { + name: 'Preference', + description: null, + warning: null, + required: { default: 'summer' }, + }, + Variants.of({ + summer: { + name: 'summer', + spec: Config.of({ + 'favorite-tree': Value.text({ + name: 'Favorite Tree', + required: { + default: 'Maple', + }, + description: 'What is your favorite tree?', + }), + 'favorite-flower': Value.select({ + name: 'Favorite Flower', + description: 'Select your favorite flower', + required: { + default: 'none', + }, + values: { + none: 'none', + red: 'red', + blue: 'blue', + purple: 'purple', + }, + }), + }), + }, + winter: { + name: 'winter', + spec: Config.of({ + 'like-snow': Value.toggle({ + name: 'Like Snow?', + default: true, + description: 'Do you like snow or not?', + }), + }), + }, + }), + ), + }), + uniqueBy: 'preference', + }, + ), + ), + 'random-enum': Value.select({ + name: 'Random Enum', + description: 'This is not even real.', + warning: 'Be careful changing this!', + required: { + default: 'null', + }, + values: { + null: 'null', + option1: 'option1', + option2: 'option2', + option3: 'option3', + }, + }), + 'favorite-number': + /* TODO: Convert range for this value ((-100,100])*/ Value.number({ + name: 'Favorite Number', + description: 'Your favorite number of all time', + warning: + 'Once you set this number, it can never be changed without severe consequences.', + required: { + default: 7, + }, + integer: false, + units: 'BTC', + }), + 'unlucky-numbers': Value.list( + List.number( + { + name: 'Unlucky Numbers', + minLength: 0, + maxLength: 10, + // default: [2, 3], + description: + 'Numbers that you like but are not your top favorite.', + }, + { + integer: false, + }, + ), + ), + rpcsettings: Value.object( + { + name: 'RPC Settings', + description: 'rpc username and password', + warning: + 'Adding RPC users gives them special permissions on your node.', + }, + Config.of({ + laws: Value.object( + { + name: 'Laws', + description: 'the law of the realm', + }, + Config.of({ + law1: Value.text({ + name: 'First Law', + required: false, + description: 'the first law', + }), + law2: Value.text({ + name: 'Second Law', + required: false, + description: 'the second law', + }), + }), + ), + rulemakers: Value.list( + List.obj( + { + name: 'Rule Makers', + minLength: 0, + maxLength: 2, + description: 'the people who make the rules', + }, + { + spec: Config.of({ + rulemakername: Value.text({ + name: 'Rulemaker Name', + required: { + default: { + charset: 'a-g,2-9', + len: 12, + }, + }, + description: 'the name of the rule maker', + }), + rulemakerip: Value.text({ + name: 'Rulemaker IP', + required: { + default: '192.168.1.0', + }, + description: 'the ip of the rule maker', + patterns: [ + { + regex: + '^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\\.){3}([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$', + description: 'may only contain numbers and periods', + }, + ], + }), + }), + }, + ), + ), + rpcuser: Value.text({ name: 'RPC Username', - type: 'text', - inputmode: 'text', + required: { + default: 'defaultrpcusername', + }, description: 'rpc username', - required: true, - default: 'defaultrpcusername', - minLength: null, - maxLength: null, patterns: [ { regex: '^[a-zA-Z]+$', description: 'must contain only letters.', }, ], - masked: false, - placeholder: null, - warning: null, - }, - rpcpass: { + }), + rpcpass: Value.text({ name: 'RPC User Password', - type: 'text', - inputmode: 'text', - description: 'rpc password', - required: true, - default: { - charset: 'a-z,A-Z,2-9', - len: 20, + required: { + default: { + charset: 'a-z,A-Z,2-9', + len: 20, + }, }, + description: 'rpc password', masked: true, - placeholder: null, - patterns: [], - minLength: null, - maxLength: null, - warning: null, - }, + }), + }), + ), + 'bitcoin-node': Value.union( + { + name: 'Bitcoin Node', + description: 'Options
  • Item 1
  • Item 2
', + warning: 'Careful changing this', + required: { default: 'internal' }, }, - }, - }, - }, - } + Variants.of({ + internal: { + name: 'Internal', + spec: Config.of({}), + }, + external: { + name: 'External', + spec: Config.of({ + 'emergency-contact': Value.object( + { + name: 'Emergency Contact', + description: 'The person to contact in case of emergency.', + }, + Config.of({ + name: Value.text({ + name: 'Name', + required: { + default: null, + }, + patterns: [ + { + regex: '^[a-zA-Z]+$', + description: 'Must contain only letters.', + }, + ], + }), + email: Value.text({ + name: 'Email', + inputmode: 'email', + required: { + default: null, + }, + }), + }), + ), + 'public-domain': Value.text({ + name: 'Public Domain', + required: { + default: 'bitcoinnode.com', + }, + description: 'the public address of the node', + patterns: [ + { + regex: '.*', + description: 'anything', + }, + ], + }), + 'private-domain': Value.text({ + name: 'Private Domain', + required: { + default: null, + }, + description: 'the private address of the node', + masked: true, + inputmode: 'url', + }), + }), + }, + }), + ), + port: Value.number({ + name: 'Port', + description: + 'the default port for your Bitcoin node. default: 8333, testnet: 18333, regtest: 18444', + required: { + default: 8333, + }, + min: 1, + max: 9998, + step: '1', + integer: true, + }), + 'favorite-slogan': Value.text({ + name: 'Favorite Slogan', + required: false, + description: + 'You most favorite slogan in the whole world, used for paying you.', + masked: true, + }), + rpcallowip: Value.list( + List.text( + { + name: 'RPC Allowed IPs', + minLength: 1, + maxLength: 10, + default: ['192.168.1.1'], + description: + 'external ip addresses that are authorized to access your Bitcoin node', + warning: + 'Any IP you allow here will have RPC access to your Bitcoin node.', + }, + { + patterns: [ + { + regex: + '((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|((^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]).){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$)|(^[a-z2-7]{16}\\.onion$)|(^([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\\.)+[a-z0-9][a-z0-9-]{0,61}[a-z0-9]$))', + description: 'must be a valid ipv4, ipv6, or domain name', + }, + ], + }, + ), + ), + rpcauth: Value.list( + List.text( + { + name: 'RPC Auth', + description: + 'api keys that are authorized to access your Bitcoin node.', + }, + { + patterns: [], + }, + ), + ), + }), + ) export const MockConfig = { testnet: undefined, @@ -1668,16 +1199,41 @@ export module Mock { export const bitcoind: PackageDataEntry = { state: PackageState.Installed, + icon: '/assets/img/service-icons/bitcoind.svg', manifest: MockManifestBitcoind, - icon: '/assets/img/service-icons/bitcoind.png', installed: { 'last-backup': null, status: { configured: true, main: { status: PackageMainStatus.Running, - started: new Date().toISOString(), - health: {}, + started: '2021-06-14T20:49:17.774Z', + health: { + 'ephemeral-health-check': { + name: 'Ephemeral Health Check', + result: HealthResult.Starting, + }, + 'chain-state': { + name: 'Chain State', + result: HealthResult.Loading, + message: 'Bitcoin is syncing from genesis', + }, + 'p2p-interface': { + name: 'P2P Interface', + result: HealthResult.Success, + message: 'the health check ran successfully', + }, + 'rpc-interface': { + name: 'RPC Interface', + result: HealthResult.Failure, + error: 'RPC interface unreachable.', + }, + 'unnecessary-health-check': { + name: 'Totally Unnecessary', + result: HealthResult.Disabled, + reason: 'You disabled this on purpose', + }, + }, }, 'dependency-errors': {}, }, @@ -1734,6 +1290,9 @@ export module Mock { ], warning: null, default: null, + disabled: false, + immutable: false, + generate: null, }, }, }, @@ -1795,7 +1354,8 @@ export module Mock { }, 'dependency-errors': { 'btc-rpc-proxy': { - type: DependencyErrorType.NotInstalled, + type: DependencyErrorType.ConfigUnsatisfied, + error: 'This is a config unsatisfied error', }, }, }, 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 c1f38fae4..d39d3a0b9 100644 --- a/frontend/projects/ui/src/app/services/api/api.types.ts +++ b/frontend/projects/ui/src/app/services/api/api.types.ts @@ -1,12 +1,13 @@ import { Dump, Revision } from 'patch-db-client' import { MarketplacePkg, StoreInfo, Manifest } from '@start9labs/marketplace' import { PackagePropertiesVersioned } from 'src/app/util/properties.util' -import { InputSpec } from 'start-sdk/lib/config/configTypes' +import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes' import { DataModel, DependencyError, } from 'src/app/services/patch-db/data-model' import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared' +import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants' export module RR { // DB @@ -136,6 +137,14 @@ export module RR { export type DeleteWifiReq = { ssid: string } // wifi.delete export type DeleteWifiRes = null + // email + + export type ConfigureEmailReq = typeof customSmtp.validator._TYPE // email.configure + export type ConfigureEmailRes = null + + export type TestEmailReq = ConfigureEmailReq & { to: string } // email.test + export type TestEmailRes = null + // ssh export type GetSSHKeysReq = {} // ssh.list 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 5703d2939..362eb9c3a 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 @@ -160,6 +160,14 @@ export abstract class ApiService { abstract deleteWifi(params: RR.DeleteWifiReq): Promise + // email + + abstract testEmail(params: RR.TestEmailReq): Promise + + abstract configureEmail( + params: RR.ConfigureEmailReq, + ): Promise + // ssh abstract getSshKeys(params: RR.GetSSHKeysReq): Promise 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 b96ad3368..61c96cfa4 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 @@ -31,7 +31,7 @@ export class LiveApiService extends ApiService { private readonly patch: PatchDB, ) { super() - ;(window as any).rpcClient = this + ; (window as any).rpcClient = this } // for getting static files: ex icons, instructions, licenses @@ -295,6 +295,18 @@ export class LiveApiService extends ApiService { return this.rpcRequest({ method: 'wifi.delete', params }) } + // email + + async testEmail(params: RR.TestEmailReq): Promise { + return this.rpcRequest({ method: 'email.test', params }) + } + + async configureEmail( + params: RR.ConfigureEmailReq, + ): Promise { + return this.rpcRequest({ method: 'email.configure', params }) + } + // ssh async getSshKeys(params: RR.GetSSHKeysReq): 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 0c1ed4f96..70ff824bc 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 @@ -459,6 +459,28 @@ export class MockApiService extends ApiService { return null } + // email + + async testEmail(params: RR.TestEmailReq): Promise { + await pauseFor(2000) + return null + } + + async configureEmail( + params: RR.ConfigureEmailReq, + ): Promise { + await pauseFor(2000) + const patch = [ + { + op: PatchOp.REPLACE, + path: '/server-info/email', + value: params, + }, + ] + + return this.withRevision(patch) + } + // ssh async getSshKeys(params: RR.GetSSHKeysReq): Promise { @@ -649,7 +671,8 @@ export class MockApiService extends ApiService { params: RR.GetPackagePropertiesReq, ): Promise['data']> { await pauseFor(2000) - return parsePropertiesPermissive(Mock.PackageProperties) + return '' as any + // return parsePropertiesPermissive(Mock.PackageProperties) } async getPackageLogs( @@ -730,7 +753,7 @@ export class MockApiService extends ApiService { await pauseFor(2000) return { config: Mock.MockConfig, - spec: Mock.InputSpec, + spec: await Mock.getInputSpec(), } } @@ -965,7 +988,7 @@ export class MockApiService extends ApiService { return { 'old-config': Mock.MockConfig, 'new-config': Mock.MockDependencyConfig, - spec: Mock.InputSpec, + spec: await Mock.getInputSpec(), } } 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 83018491e..b24fc8753 100644 --- a/frontend/projects/ui/src/app/services/api/mock-patch.ts +++ b/frontend/projects/ui/src/app/services/api/mock-patch.ts @@ -1,11 +1,6 @@ -import { - DataModel, - DependencyErrorType, - HealthResult, - PackageMainStatus, - PackageState, -} from 'src/app/services/patch-db/data-model' +import { DataModel } from 'src/app/services/patch-db/data-model' import { BUILT_IN_WIDGETS } from '../../pages/widgets/built-in/widgets' +import { Mock } from './api.fixures' export const mockPatchData: DataModel = { ui: { @@ -31,7 +26,6 @@ export const mockPatchData: DataModel = { }, }, }, - dev: {}, gaming: { snake: { 'high-score': 0, @@ -71,213 +65,17 @@ export const mockPatchData: DataModel = { '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(), zram: false, + smtp: { + server: '', + port: 587, + from: '', + login: '', + password: '', + tls: true, + }, }, 'package-data': { - bitcoind: { - state: PackageState.Installed, - icon: '/assets/img/service-icons/bitcoind.svg', - manifest: { - id: 'bitcoind', - title: 'Bitcoin Core', - version: '0.20.0', - 'git-hash': 'abcdefgh', - description: { - short: 'A Bitcoin full node by Bitcoin Core.', - long: 'Bitcoin is a decentralized consensus protocol and settlement network.', - }, - assets: { - icon: 'icon.png', - }, - 'release-notes': 'Taproot, Schnorr, and more.', - license: 'MIT', - 'wrapper-repo': 'https://github.com/start9labs/bitcoind-wrapper', - 'upstream-repo': 'https://github.com/bitcoin/bitcoin', - 'support-site': 'https://bitcoin.org', - 'marketing-site': 'https://bitcoin.org', - 'donation-url': 'https://start9.com', - alerts: { - install: 'Bitcoin can take over a week to sync.', - uninstall: - 'Chain state will be lost, as will any funds stored on your Bitcoin Core waller that have not been backed up.', - restore: null, - start: 'Starting Bitcoin is good for your health.', - stop: null, - }, - dependencies: {}, - 'os-version': '0.4.0', - }, - installed: { - 'last-backup': null, - status: { - configured: true, - main: { - status: PackageMainStatus.Running, - started: '2021-06-14T20:49:17.774Z', - health: { - 'ephemeral-health-check': { - name: 'Ephemeral Health Check', - result: HealthResult.Starting, - }, - 'chain-state': { - name: 'Chain State', - result: HealthResult.Loading, - message: 'Bitcoin is syncing from genesis', - }, - 'p2p-interface': { - name: 'P2P Interface', - result: HealthResult.Success, - message: 'the health check ran successfully', - }, - 'rpc-interface': { - name: 'RPC Interface', - result: HealthResult.Failure, - error: 'RPC interface unreachable.', - }, - 'unnecessary-health-check': { - name: 'Totally Unnecessary', - result: HealthResult.Disabled, - reason: 'You disabled this on purpose', - }, - }, - }, - 'dependency-errors': {}, - }, - 'address-info': { - rpc: { - name: 'Bitcoin RPC', - description: `Bitcoin's RPC interface`, - addresses: [ - 'http://bitcoind-rpc-address.onion', - 'https://bitcoind-rpc-address.local', - 'https://192.168.1.1:8332', - ], - ui: true, - }, - p2p: { - name: 'Bitcoin P2P', - description: `Bitcoin's P2P interface`, - addresses: [ - 'bitcoin://bitcoind-rpc-address.onion', - 'bitcoin://192.168.1.1:8333', - ], - ui: true, - }, - }, - 'current-dependencies': {}, - 'dependency-info': {}, - 'marketplace-url': 'https://registry.start9.com/', - 'developer-key': 'developer-key', - 'has-config': true, - }, - }, - lnd: { - state: PackageState.Installed, - icon: '/assets/img/service-icons/lnd.png', - manifest: { - id: 'lnd', - title: 'Lightning Network Daemon', - version: '0.11.1', - description: { - short: 'A bolt spec compliant client.', - long: 'More info about LND. More info about LND. More info about LND.', - }, - assets: { - icon: 'icon.png', - }, - 'release-notes': 'Dual funded channels!', - license: 'MIT', - 'wrapper-repo': 'https://github.com/start9labs/lnd-wrapper', - 'upstream-repo': 'https://github.com/lightningnetwork/lnd', - 'support-site': 'https://lightning.engineering/', - 'marketing-site': 'https://lightning.engineering/', - 'donation-url': null, - alerts: { - install: null, - uninstall: null, - restore: - 'If this is a duplicate instance of the same LND node, you may loose your funds.', - start: 'Starting LND is good for your health.', - stop: null, - }, - dependencies: { - bitcoind: { - version: '=0.21.0', - description: 'LND needs bitcoin to live.', - requirement: { - type: 'opt-out', - how: 'You can use an external node from your server if you prefer.', - }, - }, - 'btc-rpc-proxy': { - version: '>=0.2.2', - description: - 'As long as Bitcoin is pruned, LND needs Bitcoin Proxy to fetch block over the P2P network.', - requirement: { - type: 'opt-in', - how: `To use Proxy's user management system, go to LND config and select Bitcoin Proxy under Bitcoin config.`, - }, - }, - }, - 'os-version': '0.4.0', - }, - installed: { - 'last-backup': null, - status: { - configured: true, - main: { - status: PackageMainStatus.Stopped, - }, - 'dependency-errors': { - 'btc-rpc-proxy': { - type: DependencyErrorType.ConfigUnsatisfied, - error: 'This is a config unsatisfied error', - }, - }, - }, - 'address-info': { - ui: { - name: 'Web UI', - description: 'The browser web interface for LND', - addresses: [ - 'http://lnd-ui-address.onion', - 'https://lnd-ui-address.local', - 'https://192.168.1.1:3449', - ], - ui: true, - }, - grpc: { - name: 'gRPC', - description: 'For connecting to LND gRPC interface', - addresses: [ - 'http://lnd-grpc-address.onion', - 'https://lnd-grpc-address.local', - 'https://192.168.1.1:3449', - ], - ui: true, - }, - }, - 'current-dependencies': { - bitcoind: { - 'health-checks': [], - }, - 'btc-rpc-proxy': { - 'health-checks': [], - }, - }, - 'dependency-info': { - bitcoind: { - title: 'Bitcoin Core', - icon: 'assets/img/service-icons/bitcoind.svg', - }, - 'btc-rpc-proxy': { - title: 'Bitcoin Proxy', - icon: 'assets/img/service-icons/btc-rpc-proxy.png', - }, - }, - 'marketplace-url': 'https://registry.start9.com/', - 'developer-key': 'developer-key', - 'has-config': true, - }, - }, + bitcoind: Mock.bitcoind, + lnd: Mock.lnd, }, } diff --git a/frontend/projects/ui/src/app/services/form.service.ts b/frontend/projects/ui/src/app/services/form.service.ts index e74177dba..b50763208 100644 --- a/frontend/projects/ui/src/app/services/form.service.ts +++ b/frontend/projects/ui/src/app/services/form.service.ts @@ -10,7 +10,6 @@ import { import { getDefaultString } from '../util/config-utilities' import { InputSpec, - isValueSpecListOf, ListValueSpecNumber, ListValueSpecObject, ListValueSpecOf, @@ -25,12 +24,13 @@ import { ValueSpecObject, ValueSpecText, ValueSpecUnion, - unionSelectKey, ValueSpecTextarea, - unionValueKey, ValueSpecColor, ValueSpecDatetime, -} from 'start-sdk/lib/config/configTypes' + unionSelectKey, + unionValueKey, + isValueSpecListOf, +} from '@start9labs/start-sdk/lib/config/configTypes' const Mustache = require('mustache') @Injectable({ @@ -52,6 +52,8 @@ export class FormService { ): ValueSpecSelect { return { ...spec, + // TODO: implement disabled + disabled: false, type: 'select', default: selection, values: Object.fromEntries( 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 e1db7eaad..d73581e72 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 @@ -1,8 +1,8 @@ -import { InputSpec } from 'start-sdk/lib/config/configTypes' +import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes' import { Url } from '@start9labs/shared' import { Manifest } from '@start9labs/marketplace' -import { BasicInfo } from 'src/app/pages/developer-routes/developer-menu/form-info' import { BackupJob } from '../api/api.types' +import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants' export interface DataModel { 'server-info': ServerInfo @@ -14,7 +14,6 @@ export interface UIData { name: string | null 'ack-welcome': string // emver marketplace: UIMarketplaceData - dev: DevData gaming: { snake: { 'high-score': number @@ -51,17 +50,6 @@ export interface UIStore { name?: string } -export interface DevData { - [id: string]: DevProjectData -} - -export interface DevProjectData { - name: string - instructions: string - config: string - 'basic-info'?: BasicInfo -} - export interface ServerInfo { id: string version: string @@ -80,6 +68,7 @@ export interface ServerInfo { 'ca-fingerprint': string 'system-start-time': string zram: boolean + smtp: typeof customSmtp.validator._TYPE } export interface IpInfo { diff --git a/frontend/projects/ui/src/app/util/config-utilities.ts b/frontend/projects/ui/src/app/util/config-utilities.ts index 9c1130d18..b105f56a0 100644 --- a/frontend/projects/ui/src/app/util/config-utilities.ts +++ b/frontend/projects/ui/src/app/util/config-utilities.ts @@ -1,4 +1,4 @@ -import { DefaultString } from 'start-sdk/lib/config/configTypes' +import { DefaultString } from '@start9labs/start-sdk/lib/config/configTypes' export class Range { min?: number diff --git a/frontend/projects/ui/src/app/util/configBuilderToSpec.ts b/frontend/projects/ui/src/app/util/configBuilderToSpec.ts new file mode 100644 index 000000000..fd4573e99 --- /dev/null +++ b/frontend/projects/ui/src/app/util/configBuilderToSpec.ts @@ -0,0 +1,9 @@ +import { Config } from '@start9labs/start-sdk/lib/config/builder/config' + +export async function configBuilderToSpec( + builder: + | Config, unknown, unknown> + | Config, never, never>, +) { + return builder.build({} as any) +} diff --git a/frontend/projects/ui/src/styles.scss b/frontend/projects/ui/src/styles.scss index 223a10aa0..7159d6806 100644 --- a/frontend/projects/ui/src/styles.scss +++ b/frontend/projects/ui/src/styles.scss @@ -215,6 +215,11 @@ ion-button { --color: var(--ion-color-dark) !important; } +.cap-width { + padding: 32px; + max-width: 800px; +} + .text-ellipses { overflow: hidden; text-overflow: ellipsis;