Trying code editor (#1173)

Implement hidden dev service packaging tools.

Co-authored-by: Drew Ansbacher <drew.ansbacher@spiredigital.com>
Co-authored-by: Matt Hill <matthewonthemoon@gmail.com>
This commit is contained in:
Drew Ansbacher
2022-02-08 13:00:40 -07:00
committed by GitHub
parent 79e4c6880a
commit 9d3f0a9d2b
29 changed files with 1644 additions and 877 deletions

View File

@@ -30,6 +30,11 @@
"glob": "**/*.svg",
"input": "node_modules/ionicons/dist/ionicons/svg",
"output": "./svg"
},
{
"glob": "**/*",
"input": "node_modules/monaco-editor",
"output": "assets/monaco-editor/"
}
],
"styles": [

View File

@@ -18,10 +18,9 @@
"@angular/router": "^13.2.0",
"@ionic/angular": "^6.0.3",
"@ionic/storage-angular": "^3.0.6",
"@materia-ui/ngx-monaco-editor": "^6.0.0",
"@start9labs/argon2": "^0.1.0",
"@start9labs/emver": "^0.1.5",
"@types/aes-js": "^3.1.1",
"@types/pbkdf2": "^3.1.0",
"aes-js": "^3.1.2",
"ajv": "^6.12.6",
"ansi-to-html": "^0.7.2",
@@ -29,7 +28,9 @@
"dompurify": "^2.3.3",
"fast-json-patch": "^3.1.0",
"fuse.js": "^6.4.6",
"js-yaml": "^4.1.0",
"marked": "^4.0.0",
"monaco-editor": "^0.32.0",
"mustache": "^4.2.0",
"ng-qrcode": "^6.0.0",
"patch-db-client": "file: ../../../patch-db/client",
@@ -45,10 +46,13 @@
"@angular/compiler-cli": "^13.2.0",
"@angular/language-service": "^13.2.0",
"@ionic/cli": "^6.18.1",
"@types/aes-js": "^3.1.1",
"@types/dompurify": "^2.3.3",
"@types/js-yaml": "^4.0.5",
"@types/marked": "^4.0.0",
"@types/mustache": "^4.1.2",
"@types/node": "^16.9.1",
"@types/pbkdf2": "^3.1.0",
"@types/uuid": "^8.3.1",
"husky": "^4.3.8",
"lint-staged": "^12.1.2",
@@ -2900,6 +2904,28 @@
"node": ">=8"
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/@istanbuljs/schema": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
@@ -2918,6 +2944,18 @@
"node": ">=10.0.0"
}
},
"node_modules/@materia-ui/ngx-monaco-editor": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@materia-ui/ngx-monaco-editor/-/ngx-monaco-editor-6.0.0.tgz",
"integrity": "sha512-gTqNQjOGznZxOC0NlmKdKSGCJuTts8YmK4dsTQAGc5IgIV7cZdQWiW6AL742h0ruED6q0cAunEYjXT6jzHBoIQ==",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"@angular/core": ">=13.0.0",
"rxjs": ">=6.0.0"
}
},
"node_modules/@ngtools/webpack": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-13.2.0.tgz",
@@ -3267,7 +3305,8 @@
"node_modules/@types/aes-js": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/aes-js/-/aes-js-3.1.1.tgz",
"integrity": "sha512-SDSGgXT3LRCH6qMWk8OHT1vLSVNuHNvCpKCx2/TYtQMbMGGgxJC9fspwSkQjqzRagrWnCrxuLL3jMNXLXHHvSw=="
"integrity": "sha512-SDSGgXT3LRCH6qMWk8OHT1vLSVNuHNvCpKCx2/TYtQMbMGGgxJC9fspwSkQjqzRagrWnCrxuLL3jMNXLXHHvSw==",
"dev": true
},
"node_modules/@types/body-parser": {
"version": "1.19.2",
@@ -3374,6 +3413,12 @@
"@types/node": "*"
}
},
"node_modules/@types/js-yaml": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz",
"integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==",
"dev": true
},
"node_modules/@types/json-schema": {
"version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
@@ -3401,7 +3446,8 @@
"node_modules/@types/node": {
"version": "16.11.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.21.tgz",
"integrity": "sha512-Pf8M1XD9i1ksZEcCP8vuSNwooJ/bZapNmIzpmsMaL+jMI+8mEYU3PKvs+xDNuQcJWF/x24WzY4qxLtB0zNow9A=="
"integrity": "sha512-Pf8M1XD9i1ksZEcCP8vuSNwooJ/bZapNmIzpmsMaL+jMI+8mEYU3PKvs+xDNuQcJWF/x24WzY4qxLtB0zNow9A==",
"dev": true
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
@@ -3413,6 +3459,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.0.tgz",
"integrity": "sha512-Cf63Rv7jCQ0LaL8tNXmEyqTHuIJxRdlS5vMh1mj5voN4+QFhVZnlZruezqpWYDiJ8UTzhP0VmeLXCmBk66YrMQ==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
@@ -3925,13 +3972,9 @@
"dev": true
},
"node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"dependencies": {
"sprintf-js": "~1.0.2"
}
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"node_modules/array-flatten": {
"version": "2.1.2",
@@ -8154,13 +8197,11 @@
"dev": true
},
"node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
@@ -9312,6 +9353,11 @@
"node": ">=10"
}
},
"node_modules/monaco-editor": {
"version": "0.32.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.32.0.tgz",
"integrity": "sha512-r3xvo6XMA/fg3SuVJb+NMxf+fXHO8GPIOLoPFRO2LIf7GbqfV9W7FdddqT9ze+bCtnLd+s4IScnOGCgDQDg4BQ=="
},
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -12949,6 +12995,15 @@
"typescript": ">=2.3.0-dev || >=2.4.0-dev || >=2.5.0-dev || >=2.6.0-dev || >=2.7.0-dev || >=2.8.0-dev || >=2.9.0-dev || >=3.0.0-dev || >= 3.1.0-dev || >= 3.2.0-dev || >= 4.0.0-dev"
}
},
"node_modules/tslint/node_modules/argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"dependencies": {
"sprintf-js": "~1.0.2"
}
},
"node_modules/tslint/node_modules/builtin-modules": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
@@ -12964,6 +13019,19 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"node_modules/tslint/node_modules/js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"dependencies": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/tslint/node_modules/mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",
@@ -15914,6 +15982,27 @@
"get-package-type": "^0.1.0",
"js-yaml": "^3.13.1",
"resolve-from": "^5.0.0"
},
"dependencies": {
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"requires": {
"sprintf-js": "~1.0.2"
}
},
"js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
}
}
},
"@istanbuljs/schema": {
@@ -15928,6 +16017,14 @@
"integrity": "sha512-fuIOnc81C5iRNevb/XPiM8Khp9bVjreydRQ37rt0C/dY0PAW1DRvEM3WrKX/5rStS5lbgwS0FCgqSndh9tvK5w==",
"dev": true
},
"@materia-ui/ngx-monaco-editor": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/@materia-ui/ngx-monaco-editor/-/ngx-monaco-editor-6.0.0.tgz",
"integrity": "sha512-gTqNQjOGznZxOC0NlmKdKSGCJuTts8YmK4dsTQAGc5IgIV7cZdQWiW6AL742h0ruED6q0cAunEYjXT6jzHBoIQ==",
"requires": {
"tslib": "^2.0.0"
}
},
"@ngtools/webpack": {
"version": "13.2.0",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-13.2.0.tgz",
@@ -16191,7 +16288,8 @@
"@types/aes-js": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@types/aes-js/-/aes-js-3.1.1.tgz",
"integrity": "sha512-SDSGgXT3LRCH6qMWk8OHT1vLSVNuHNvCpKCx2/TYtQMbMGGgxJC9fspwSkQjqzRagrWnCrxuLL3jMNXLXHHvSw=="
"integrity": "sha512-SDSGgXT3LRCH6qMWk8OHT1vLSVNuHNvCpKCx2/TYtQMbMGGgxJC9fspwSkQjqzRagrWnCrxuLL3jMNXLXHHvSw==",
"dev": true
},
"@types/body-parser": {
"version": "1.19.2",
@@ -16298,6 +16396,12 @@
"@types/node": "*"
}
},
"@types/js-yaml": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.5.tgz",
"integrity": "sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==",
"dev": true
},
"@types/json-schema": {
"version": "7.0.9",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz",
@@ -16325,7 +16429,8 @@
"@types/node": {
"version": "16.11.21",
"resolved": "https://registry.npmjs.org/@types/node/-/node-16.11.21.tgz",
"integrity": "sha512-Pf8M1XD9i1ksZEcCP8vuSNwooJ/bZapNmIzpmsMaL+jMI+8mEYU3PKvs+xDNuQcJWF/x24WzY4qxLtB0zNow9A=="
"integrity": "sha512-Pf8M1XD9i1ksZEcCP8vuSNwooJ/bZapNmIzpmsMaL+jMI+8mEYU3PKvs+xDNuQcJWF/x24WzY4qxLtB0zNow9A==",
"dev": true
},
"@types/parse-json": {
"version": "4.0.0",
@@ -16337,6 +16442,7 @@
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/@types/pbkdf2/-/pbkdf2-3.1.0.tgz",
"integrity": "sha512-Cf63Rv7jCQ0LaL8tNXmEyqTHuIJxRdlS5vMh1mj5voN4+QFhVZnlZruezqpWYDiJ8UTzhP0VmeLXCmBk66YrMQ==",
"dev": true,
"requires": {
"@types/node": "*"
}
@@ -16782,13 +16888,9 @@
"dev": true
},
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"requires": {
"sprintf-js": "~1.0.2"
}
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"array-flatten": {
"version": "2.1.2",
@@ -19908,13 +20010,11 @@
"dev": true
},
"js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
"argparse": "^2.0.1"
}
},
"jsesc": {
@@ -20792,6 +20892,11 @@
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==",
"dev": true
},
"monaco-editor": {
"version": "0.32.0",
"resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.32.0.tgz",
"integrity": "sha512-r3xvo6XMA/fg3SuVJb+NMxf+fXHO8GPIOLoPFRO2LIf7GbqfV9W7FdddqT9ze+bCtnLd+s4IScnOGCgDQDg4BQ=="
},
"ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -23492,6 +23597,15 @@
"tsutils": "^2.29.0"
},
"dependencies": {
"argparse": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
"dev": true,
"requires": {
"sprintf-js": "~1.0.2"
}
},
"builtin-modules": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz",
@@ -23504,6 +23618,16 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true
},
"js-yaml": {
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"dev": true,
"requires": {
"argparse": "^1.0.7",
"esprima": "^4.0.0"
}
},
"mkdirp": {
"version": "0.5.5",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz",

View File

@@ -30,10 +30,9 @@
"@angular/router": "^13.2.0",
"@ionic/angular": "^6.0.3",
"@ionic/storage-angular": "^3.0.6",
"@materia-ui/ngx-monaco-editor": "^6.0.0",
"@start9labs/argon2": "^0.1.0",
"@start9labs/emver": "^0.1.5",
"@types/aes-js": "^3.1.1",
"@types/pbkdf2": "^3.1.0",
"aes-js": "^3.1.2",
"ajv": "^6.12.6",
"ansi-to-html": "^0.7.2",
@@ -41,7 +40,9 @@
"dompurify": "^2.3.3",
"fast-json-patch": "^3.1.0",
"fuse.js": "^6.4.6",
"js-yaml": "^4.1.0",
"marked": "^4.0.0",
"monaco-editor": "^0.32.0",
"mustache": "^4.2.0",
"ng-qrcode": "^6.0.0",
"patch-db-client": "file: ../../../patch-db/client",
@@ -57,10 +58,13 @@
"@angular/compiler-cli": "^13.2.0",
"@angular/language-service": "^13.2.0",
"@ionic/cli": "^6.18.1",
"@types/aes-js": "^3.1.1",
"@types/dompurify": "^2.3.3",
"@types/js-yaml": "^4.0.5",
"@types/marked": "^4.0.0",
"@types/mustache": "^4.1.2",
"@types/node": "^16.9.1",
"@types/pbkdf2": "^3.1.0",
"@types/uuid": "^8.3.1",
"husky": "^4.3.8",
"lint-staged": "^12.1.2",

View File

@@ -12,30 +12,52 @@ const routes: Routes = [
{
path: 'login',
canActivate: [UnauthGuard],
loadChildren: () => import('./pages/login/login.module').then(m => m.LoginPageModule),
loadChildren: () =>
import('./pages/login/login.module').then(m => m.LoginPageModule),
},
{
path: 'embassy',
canActivate: [AuthGuard],
canActivateChild: [AuthGuard],
loadChildren: () => import('./pages/server-routes/server-routing.module').then(m => m.ServerRoutingModule),
loadChildren: () =>
import('./pages/server-routes/server-routing.module').then(
m => m.ServerRoutingModule,
),
},
{
path: 'marketplace',
canActivate: [AuthGuard],
canActivateChild: [AuthGuard],
loadChildren: () => import('./pages/marketplace-routes/marketplace-routing.module').then(m => m.MarketplaceRoutingModule),
loadChildren: () =>
import('./pages/marketplace-routes/marketplace-routing.module').then(
m => m.MarketplaceRoutingModule,
),
},
{
path: 'notifications',
canActivate: [AuthGuard],
loadChildren: () => import('./pages/notifications/notifications.module').then(m => m.NotificationsPageModule),
loadChildren: () =>
import('./pages/notifications/notifications.module').then(
m => m.NotificationsPageModule,
),
},
{
path: 'services',
canActivate: [AuthGuard],
canActivateChild: [AuthGuard],
loadChildren: () => import('./pages/apps-routes/apps-routing.module').then(m => m.AppsRoutingModule),
loadChildren: () =>
import('./pages/apps-routes/apps-routing.module').then(
m => m.AppsRoutingModule,
),
},
{
path: 'developer',
canActivate: [AuthGuard],
canActivateChild: [AuthGuard],
loadChildren: () =>
import('./pages/developer-routes/developer-routing.module').then(
m => m.DeveloperRoutingModule,
),
},
]
@@ -50,4 +72,4 @@ const routes: Routes = [
],
exports: [RouterModule],
})
export class AppRoutingModule { }
export class AppRoutingModule {}

View File

@@ -1,16 +1,27 @@
<ion-app>
<ion-content>
<ion-split-pane [disabled]="!showMenu" (ionSplitPaneVisible)="splitPaneVisible($event)" contentId="main-content">
<ion-split-pane
[disabled]="!showMenu"
(ionSplitPaneVisible)="splitPaneVisible($event)"
contentId="main-content"
>
<ion-menu contentId="main-content" type="overlay">
<ion-content color="light" scrollY="false">
<div style="text-align: center;" class="ion-padding">
<img style="width: 45%; cursor: pointer;" src="assets/img/logo.png" (click)="goToWebsite()">
<div style="text-align: center" class="ion-padding">
<img
style="width: 45%; cursor: pointer"
src="assets/img/logo.png"
(click)="goToWebsite()"
/>
</div>
<div class="divider"></div>
<ion-item-group style="padding: 30px 0px;">
<ion-menu-toggle auto-hide="false" *ngFor="let page of appPages; let i = index">
<ion-item-group style="padding: 30px 0px">
<ion-menu-toggle
auto-hide="false"
*ngFor="let page of appPages; let i = index"
>
<ion-item
style="padding-left: 10px;"
style="padding-left: 10px"
color="transparent"
button
(click)="selectedIndex = i"
@@ -18,38 +29,69 @@
[routerLink]="[page.url]"
lines="none"
detail="false"
*ngIf="
page.url !== '/developer' ||
(localStorageService.showDevTools$ | async)
"
>
<ion-icon slot="start" [name]="page.icon" [class]="selectedIndex === i ? 'bold' : 'dim'"></ion-icon>
<ion-icon
slot="start"
[name]="page.icon"
[class]="selectedIndex === i ? 'bold' : 'dim'"
></ion-icon>
<ion-label
style="font-family: 'Montserrat';"
style="font-family: 'Montserrat'"
[class]="selectedIndex === i ? 'bold' : 'dim'"
>
{{ page.title }}
</ion-label>
<ion-badge *ngIf="page.url === '/notifications' && unreadCount" color="danger" style="margin-right: 3%;" [class.selected-badge]="selectedIndex == i">{{ unreadCount }}</ion-badge>
<ion-badge
*ngIf="page.url === '/notifications' && unreadCount"
color="danger"
style="margin-right: 3%"
[class.selected-badge]="selectedIndex == i"
>{{ unreadCount }}</ion-badge
>
</ion-item>
</ion-menu-toggle>
</ion-item-group>
<div style="text-align: center; height: 75px; position:absolute; bottom:0; right:0; width: 100%;">
<div class="divider" style="margin-bottom: 10px;"></div>
<div
style="
text-align: center;
height: 75px;
position: absolute;
bottom: 0;
right: 0;
width: 100%;
"
>
<div class="divider" style="margin-bottom: 10px"></div>
<ion-menu-toggle auto-hide="false">
<ion-item button lines="none" style="--background: transparent; margin-bottom: 86px; text-align: center;" fill="clear" (click)="presentAlertLogout()">
<ion-label><ion-text
style="font-family: 'Montserrat';"
color="dark"
>
<ion-item
button
lines="none"
style="
--background: transparent;
margin-bottom: 86px;
text-align: center;
"
fill="clear"
(click)="presentAlertLogout()"
>
<ion-label
><ion-text style="font-family: 'Montserrat'" color="dark">
Log Out
</ion-text></ion-label>
</ion-text></ion-label
>
</ion-item>
</ion-menu-toggle>
</div>
</ion-content>
</ion-menu>
<ion-router-outlet id="main-content"></ion-router-outlet>
</ion-split-pane>
<section id="preload" style="display: none;">
<section id="preload" style="display: none">
<!-- 3rd party components -->
<qr-code value="hello"></qr-code>
@@ -66,7 +108,8 @@
<ion-icon name="checkmark"></ion-icon>
<ion-icon name="chevron-down"></ion-icon>
<ion-icon name="chevron-up"></ion-icon>
<ion-icon name="chevron-forward"></ion-icon> <!-- needed for detail="true" on ion-item button -->
<ion-icon name="chevron-forward"></ion-icon>
<!-- needed for detail="true" on ion-item button -->
<ion-icon name="close"></ion-icon>
<ion-icon name="cloud-outline"></ion-icon>
<ion-icon name="cloud-done-outline"></ion-icon>
@@ -90,6 +133,7 @@
<ion-icon name="folder-open-outline"></ion-icon>
<ion-icon name="grid-outline"></ion-icon>
<ion-icon name="help-circle-outline"></ion-icon>
<ion-icon name="hammer-outline"></ion-icon>
<ion-icon name="home-outline"></ion-icon>
<ion-icon name="information-circle-outline"></ion-icon>
<ion-icon name="key-outline"></ion-icon>
@@ -119,7 +163,7 @@
<ion-icon name="trash-outline"></ion-icon>
<ion-icon name="warning-outline"></ion-icon>
<ion-icon name="wifi"></ion-icon>
<!-- Ionic components -->
<ion-action-sheet></ion-action-sheet>
<ion-alert></ion-alert>
@@ -141,7 +185,9 @@
<ion-refresher slot="fixed"></ion-refresher>
<ion-refresher-content pullingContent="lines"></ion-refresher-content>
<ion-infinite-scroll></ion-infinite-scroll>
<ion-infinite-scroll-content loadingSpinner="lines"></ion-infinite-scroll-content>
<ion-infinite-scroll-content
loadingSpinner="lines"
></ion-infinite-scroll-content>
</ion-content>
<ion-input></ion-input>
<ion-item></ion-item>
@@ -172,26 +218,38 @@
<ion-footer
[ngStyle]="{
'max-height': osUpdateProgress ? '100px' : '0px',
'overflow': 'hidden',
overflow: 'hidden',
'transition-property': 'max-height',
'transition-duration': '1s',
'transition-delay': '.05s'
}"
>
<ion-toolbar style="border-top: 1px solid var(--ion-color-dark);" color="light">
<ion-toolbar
style="border-top: 1px solid var(--ion-color-dark)"
color="light"
>
<ion-list>
<ion-list-header>
<ion-label>Downloading EOS: {{ (100 * (osUpdateProgress?.downloaded || 1) / (osUpdateProgress?.size || 1)).toFixed(0) }}%</ion-label>
<ion-label
>Downloading EOS:
{{
(
(100 * (osUpdateProgress?.downloaded || 1)) /
(osUpdateProgress?.size || 1)
).toFixed(0)
}}%</ion-label
>
</ion-list-header>
<div style="padding: 0 16px 16px 16px;">
<div style="padding: 0 16px 16px 16px">
<ion-progress-bar
color="secondary"
[value]="osUpdateProgress && osUpdateProgress.downloaded / osUpdateProgress.size"
[value]="
osUpdateProgress &&
osUpdateProgress.downloaded / osUpdateProgress.size
"
></ion-progress-bar>
</div>
</ion-list>
</ion-toolbar>
</ion-footer>
</ion-app>

View File

@@ -3,19 +3,33 @@ import { Storage } from '@ionic/storage-angular'
import { AuthService, AuthState } from './services/auth.service'
import { ApiService } from './services/api/embassy-api.service'
import { Router, RoutesRecognized } from '@angular/router'
import { debounceTime, distinctUntilChanged, filter, finalize, take, takeWhile } from 'rxjs/operators'
import { AlertController, IonicSafeString, LoadingController, ToastController } from '@ionic/angular'
import {
debounceTime,
distinctUntilChanged,
filter,
take,
} from 'rxjs/operators'
import {
AlertController,
IonicSafeString,
LoadingController,
ToastController,
} from '@ionic/angular'
import { Emver } from './services/emver.service'
import { SplitPaneTracker } from './services/split-pane.service'
import { ToastButton } from '@ionic/core'
import { PatchDbService } from './services/patch-db/patch-db.service'
import { ServerStatus } from './services/patch-db/data-model'
import { ConnectionFailure, ConnectionService } from './services/connection.service'
import {
ConnectionFailure,
ConnectionService,
} from './services/connection.service'
import { StartupAlertsService } from './services/startup-alerts.service'
import { ConfigService } from './services/config.service'
import { debounce, isEmptyObject, pauseFor } from './util/misc.util'
import { debounce, isEmptyObject } from './util/misc.util'
import { ErrorToastService } from './services/error-toast.service'
import { Subscription } from 'rxjs'
import { LocalStorageService } from './services/local-storage.service'
@Component({
selector: 'app-root',
@@ -25,7 +39,7 @@ import { Subscription } from 'rxjs'
export class AppComponent {
@HostListener('document:keydown.enter', ['$event'])
@debounce()
handleKeyboardEvent () {
handleKeyboardEvent() {
const elems = document.getElementsByClassName('enter-click')
const elem = elems[elems.length - 1] as HTMLButtonElement
if (!elem || elem.classList.contains('no-click') || elem.disabled) return
@@ -41,7 +55,7 @@ export class AppComponent {
serverName: string
unreadCount: number
subscriptions: Subscription[] = []
osUpdateProgress: { size: number, downloaded: number }
osUpdateProgress: { size: number; downloaded: number }
appPages = [
{
title: 'Services',
@@ -63,9 +77,14 @@ export class AppComponent {
url: '/notifications',
icon: 'notifications-outline',
},
{
title: 'Developer Tools',
url: '/developer',
icon: 'hammer-outline',
},
]
constructor (
constructor(
private readonly storage: Storage,
private readonly authService: AuthService,
private readonly router: Router,
@@ -77,23 +96,24 @@ export class AppComponent {
private readonly startupAlertsService: StartupAlertsService,
private readonly toastCtrl: ToastController,
private readonly errToast: ErrorToastService,
private readonly patch: PatchDbService,
private readonly config: ConfigService,
private readonly zone: NgZone,
readonly splitPane: SplitPaneTracker,
public readonly splitPane: SplitPaneTracker,
public readonly patch: PatchDbService,
public readonly localStorageService: LocalStorageService,
) {
this.init()
this.init()
}
async init () {
async init() {
await this.storage.create()
await this.authService.init()
await this.localStorageService.init()
this.router.initialNavigation()
// watch auth
this.authService.watch$()
.subscribe(async auth => {
this.authService.watch$().subscribe(async auth => {
// VERIFIED
if (auth === AuthState.VERIFIED) {
await this.patch.start()
@@ -114,26 +134,27 @@ export class AppComponent {
// watch status to display/hide maintenance page
])
this.patch.watch$()
.pipe(
filter(obj => !isEmptyObject(obj)),
take(1),
)
.subscribe(_ => {
this.subscriptions = this.subscriptions.concat([
// watch status to present toast for updated state
this.watchStatus(),
// watch update-progress to present progress bar when server is updating
this.watchUpdateProgress(),
// watch version to refresh browser window
this.watchVersion(),
// watch unread notification count to display toast
this.watchNotifications(),
// run startup alerts
this.startupAlertsService.runChecks(),
])
})
// UNVERIFIED
this.patch
.watch$()
.pipe(
filter(obj => !isEmptyObject(obj)),
take(1),
)
.subscribe(_ => {
this.subscriptions = this.subscriptions.concat([
// watch status to present toast for updated state
this.watchStatus(),
// watch update-progress to present progress bar when server is updating
this.watchUpdateProgress(),
// watch version to refresh browser window
this.watchVersion(),
// watch unread notification count to display toast
this.watchNotifications(),
// run startup alerts
this.startupAlertsService.runChecks(),
])
})
// UNVERIFIED
} else if (auth === AuthState.UNVERIFIED) {
this.subscriptions.forEach(sub => sub.unsubscribe())
this.subscriptions = []
@@ -151,20 +172,22 @@ export class AppComponent {
})
}
async goToWebsite (): Promise<void> {
async goToWebsite(): Promise<void> {
let url: string
if (this.config.isTor()) {
url = 'http://privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion'
url =
'http://privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion'
} else {
url = 'https://start9.com'
}
window.open(url, '_blank', 'noreferrer')
}
async presentAlertLogout () {
async presentAlertLogout() {
const alert = await this.alertCtrl.create({
header: 'Caution',
message: 'Do you know your password? If you log out and forget your password, you may permanently lose access to your Embassy.',
message:
'Do you know your password? If you log out and forget your password, you may permanently lose access to your Embassy.',
buttons: [
{
text: 'Cancel',
@@ -184,97 +207,97 @@ export class AppComponent {
}
// should wipe cache independant of actual BE logout
private async logout () {
this.embassyApi.logout({ })
private async logout() {
this.embassyApi.logout({})
this.authService.setUnverified()
}
private watchConnection (): Subscription {
return this.connectionService.watchFailure$()
.pipe(
distinctUntilChanged(),
debounceTime(500),
)
.subscribe(async connectionFailure => {
if (connectionFailure === ConnectionFailure.None) {
if (this.offlineToast) {
await this.offlineToast.dismiss()
this.offlineToast = undefined
private watchConnection(): Subscription {
return this.connectionService
.watchFailure$()
.pipe(distinctUntilChanged(), debounceTime(500))
.subscribe(async connectionFailure => {
if (connectionFailure === ConnectionFailure.None) {
if (this.offlineToast) {
await this.offlineToast.dismiss()
this.offlineToast = undefined
}
} else {
let message: string | IonicSafeString
let link: string
switch (connectionFailure) {
case ConnectionFailure.Network:
message = 'Phone or computer has no network connection.'
break
case ConnectionFailure.Tor:
message = 'Browser unable to connect over Tor.'
link =
'https://docs.start9.com/support/FAQ/troubleshooting.html#tor-failure'
break
case ConnectionFailure.Lan:
message = 'Embassy not found on Local Area Network.'
link =
'https://docs.start9.com/support/FAQ/troubleshooting.html#lan-failure'
break
}
await this.presentToastOffline(message, link)
}
} else {
let message: string | IonicSafeString
let link: string
switch (connectionFailure) {
case ConnectionFailure.Network:
message = 'Phone or computer has no network connection.'
break
case ConnectionFailure.Tor:
message = 'Browser unable to connect over Tor.'
link = 'https://docs.start9.com/support/FAQ/troubleshooting.html#tor-failure'
break
case ConnectionFailure.Lan:
message = 'Embassy not found on Local Area Network.'
link = 'https://docs.start9.com/support/FAQ/troubleshooting.html#lan-failure'
break
}
await this.presentToastOffline(message, link)
}
})
})
}
private watchRouter (): Subscription {
private watchRouter(): Subscription {
return this.router.events
.pipe(
filter((e: RoutesRecognized) => !!e.urlAfterRedirects),
)
.subscribe(e => {
const appPageIndex = this.appPages.findIndex(
appPage => e.urlAfterRedirects.startsWith(appPage.url),
)
if (appPageIndex > -1) this.selectedIndex = appPageIndex
})
.pipe(filter((e: RoutesRecognized) => !!e.urlAfterRedirects))
.subscribe(e => {
const appPageIndex = this.appPages.findIndex(appPage =>
e.urlAfterRedirects.startsWith(appPage.url),
)
if (appPageIndex > -1) this.selectedIndex = appPageIndex
})
}
private watchStatus (): Subscription {
return this.patch.watch$('server-info', 'status')
.subscribe(status => {
private watchStatus(): Subscription {
return this.patch.watch$('server-info', 'status').subscribe(status => {
if (status === ServerStatus.Updated && !this.updateToast) {
this.presentToastUpdated()
}
})
}
private watchUpdateProgress (): Subscription {
return this.patch.watch$('server-info', 'update-progress')
.subscribe(progress => {
this.osUpdateProgress = progress
})
private watchUpdateProgress(): Subscription {
return this.patch
.watch$('server-info', 'update-progress')
.subscribe(progress => {
this.osUpdateProgress = progress
})
}
private watchVersion (): Subscription {
return this.patch.watch$('server-info', 'version')
.subscribe(version => {
private watchVersion(): Subscription {
return this.patch.watch$('server-info', 'version').subscribe(version => {
if (this.emver.compare(this.config.version, version) !== 0) {
this.presentAlertRefreshNeeded()
}
})
}
private watchNotifications (): Subscription {
private watchNotifications(): Subscription {
let previous: number
return this.patch.watch$('server-info', 'unread-notification-count')
.subscribe(count => {
this.unreadCount = count
if (previous !== undefined && count > previous) this.presentToastNotifications()
previous = count
})
return this.patch
.watch$('server-info', 'unread-notification-count')
.subscribe(count => {
this.unreadCount = count
if (previous !== undefined && count > previous)
this.presentToastNotifications()
previous = count
})
}
private async presentAlertRefreshNeeded () {
private async presentAlertRefreshNeeded() {
const alert = await this.alertCtrl.create({
backdropDismiss: false,
header: 'Refresh Needed',
message: 'Your user interface is cached and out of date. Hard refresh the page to get the latest UI.',
message:
'Your user interface is cached and out of date. Hard refresh the page to get the latest UI.',
buttons: [
{
text: 'Refresh Page',
@@ -288,12 +311,13 @@ export class AppComponent {
await alert.present()
}
private async presentToastUpdated () {
private async presentToastUpdated() {
if (this.updateToast) return
this.updateToast = await this.toastCtrl.create({
header: 'EOS download complete!',
message: 'Restart your Embassy for these updates to take effect. It can take several minutes to come back online.',
message:
'Restart your Embassy for these updates to take effect. It can take several minutes to come back online.',
position: 'bottom',
duration: 0,
cssClass: 'success-toast',
@@ -317,7 +341,7 @@ export class AppComponent {
await this.updateToast.present()
}
private async presentToastNotifications () {
private async presentToastNotifications() {
if (this.notificationToast) return
this.notificationToast = await this.toastCtrl.create({
@@ -337,7 +361,9 @@ export class AppComponent {
side: 'end',
text: 'View',
handler: () => {
this.router.navigate(['/notifications'], { queryParams: { toast: true } })
this.router.navigate(['/notifications'], {
queryParams: { toast: true },
})
},
},
],
@@ -345,7 +371,10 @@ export class AppComponent {
await this.notificationToast.present()
}
private async presentToastOffline (message: string | IonicSafeString, link?: string) {
private async presentToastOffline(
message: string | IonicSafeString,
link?: string,
) {
if (this.offlineToast) {
this.offlineToast.message = message
return
@@ -362,16 +391,14 @@ export class AppComponent {
]
if (link) {
buttons.push(
{
side: 'end',
text: 'View solutions',
handler: () => {
window.open(link, '_blank', 'noreferrer')
return false
},
buttons.push({
side: 'end',
text: 'View solutions',
handler: () => {
window.open(link, '_blank', 'noreferrer')
return false
},
)
})
}
this.offlineToast = await this.toastCtrl.create({
@@ -385,7 +412,7 @@ export class AppComponent {
await this.offlineToast.present()
}
private async restart (): Promise<void> {
private async restart(): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Restarting...',
@@ -394,7 +421,7 @@ export class AppComponent {
await loader.present()
try {
await this.embassyApi.restartServer({ })
await this.embassyApi.restartServer({})
} catch (e) {
this.errToast.present(e)
} finally {
@@ -402,7 +429,7 @@ export class AppComponent {
}
}
splitPaneVisible (e: any) {
splitPaneVisible(e: any) {
this.splitPane.sidebarOpen$.next(e.detail.visible)
}
}

View File

@@ -23,6 +23,7 @@ import { GlobalErrorHandler } from './services/global-error-handler.service'
import { MockApiService } from './services/api/embassy-mock-api.service'
import { LiveApiService } from './services/api/embassy-live-api.service'
import { WorkspaceConfig } from '@shared'
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'
const { useMocks } = require('../../../../config.json') as WorkspaceConfig
@@ -47,6 +48,7 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
MarkdownPageModule,
GenericInputComponentModule,
SharingModule,
MonacoEditorModule,
],
providers: [
FormBuilder,

View File

@@ -9,20 +9,22 @@ import { getErrorMessage } from 'src/app/services/error-toast.service'
styleUrls: ['./markdown.page.scss'],
})
export class MarkdownPage {
@Input() contentUrl: string
@Input() contentUrl?: string
@Input() content?: string
@Input() title: string
content: string
loading = true
loadingError: string | IonicSafeString
constructor (
constructor(
private readonly modalCtrl: ModalController,
private readonly embassyApi: ApiService,
) { }
) {}
async ngOnInit () {
async ngOnInit() {
try {
this.content = await this.embassyApi.getStatic(this.contentUrl)
if (!this.content) {
this.content = await this.embassyApi.getStatic(this.contentUrl)
}
const links = document.links
for (let i = 0, linksLength = links.length; i < linksLength; i++) {
if (links[i].hostname != window.location.hostname) {
@@ -38,7 +40,7 @@ export class MarkdownPage {
}
}
async dismiss () {
async dismiss() {
return this.modalCtrl.dismiss(true)
}
}

View File

@@ -0,0 +1,32 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router'
import { DevConfigPage } from './dev-config.page'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { SharingModule } from 'src/app/modules/sharing.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,
SharingModule,
BackupReportPageModule,
FormsModule,
MonacoEditorModule,
],
declarations: [DevConfigPage],
})
export class DevConfigPageModule {}

View File

@@ -0,0 +1,18 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="/developer"></ion-back-button>
</ion-buttons>
<ion-title>Config</ion-title>
<ion-buttons slot="end">
<ion-button (click)="submit()">Preview</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ngx-monaco-editor
[options]="editorOptions"
[(ngModel)]="code"
></ngx-monaco-editor>
</ion-content>

View File

@@ -0,0 +1,106 @@
import { Component } from '@angular/core'
import { ModalController } from '@ionic/angular'
import * as yaml from 'js-yaml'
import { GenericFormPage } from '../../../modals/generic-form/generic-form.page'
import { ConfigSpec } from '../../../pkg-config/config-types'
import { ErrorToastService } from '../../../services/error-toast.service'
@Component({
selector: 'dev-config',
templateUrl: 'dev-config.page.html',
styleUrls: ['dev-config.page.scss'],
})
export class DevConfigPage {
editorOptions = { theme: 'vs-dark', language: 'yaml' }
code: string
constructor(
private readonly errToast: ErrorToastService,
private readonly modalCtrl: ModalController,
) {}
ngOnInit() {
this.code = yaml
.dump(SAMPLE_CODE)
.replace(/warning:/g, '# Optional\n warning:')
}
async submit() {
let doc: any
try {
doc = yaml.load(this.code)
} catch (e) {
this.errToast.present(e)
}
const modal = await this.modalCtrl.create({
component: GenericFormPage,
componentProps: {
title: 'Config Sample',
spec: JSON.parse(JSON.stringify(doc, null, 2)),
buttons: [
{
text: 'OK',
handler: () => {
return
},
isSubmit: true,
},
],
},
})
await modal.present()
}
}
const SAMPLE_CODE: ConfigSpec = {
'sample-string': {
type: 'string',
name: 'Example String Input',
nullable: false,
masked: false,
copyable: false,
// optional
warning: null,
description: 'Example description for required string input.',
default: null,
placeholder: 'Enter string value',
pattern: '^[a-zA-Z0-9! _]+$',
'pattern-description': 'Must be alphanumeric (may contain underscore).',
},
'sample-number': {
type: 'number',
name: 'Example Number Input',
nullable: false,
range: '[5,1000000]',
integral: true,
// optional
warning: 'Example warning to display when changing this number value.',
units: 'ms',
description: 'Example description for optional number input.',
default: null,
placeholder: 'Enter number value',
},
'sample-boolean': {
type: 'boolean',
name: 'Example Boolean Toggle',
// optional
warning: null,
description: 'Example description for boolean toggle',
default: true,
},
'sample-enum': {
type: 'enum',
name: 'Example Enum Select',
values: ['red', 'blue', 'green'],
'value-names': {
red: 'Red',
blue: 'Blue',
green: 'Green',
},
// optional
warning: 'Example warning to display when changing this enum value.',
description: 'Example description for enum select',
default: 'red',
},
}

View File

@@ -0,0 +1,32 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router'
import { DevInstructionsPage } from './dev-instructions.page'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { SharingModule } from 'src/app/modules/sharing.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,
SharingModule,
BackupReportPageModule,
FormsModule,
MonacoEditorModule,
],
declarations: [DevInstructionsPage],
})
export class DevInstructionsPageModule {}

View File

@@ -0,0 +1,18 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button defaultHref="/developer"></ion-back-button>
</ion-buttons>
<ion-title>Instructions</ion-title>
<ion-buttons slot="end">
<ion-button (click)="submit()">Preview</ion-button>
</ion-buttons>
</ion-toolbar>
</ion-header>
<ion-content>
<ngx-monaco-editor
[options]="editorOptions"
[(ngModel)]="code"
></ngx-monaco-editor>
</ion-content>

View File

@@ -0,0 +1,27 @@
import { Component } from '@angular/core'
import { ModalController } from '@ionic/angular'
import { MarkdownPage } from '../../../modals/markdown/markdown.page'
@Component({
selector: 'dev-instructions',
templateUrl: 'dev-instructions.page.html',
styleUrls: ['dev-instructions.page.scss'],
})
export class DevInstructionsPage {
editorOptions = { theme: 'vs-dark', language: 'markdown' }
code: string = `# Create Instructions using Markdown! :)`
constructor(private readonly modalCtrl: ModalController) {}
async submit() {
const modal = await this.modalCtrl.create({
componentProps: {
title: 'Instructions Sample',
content: this.code,
},
component: MarkdownPage,
})
await modal.present()
}
}

View File

@@ -0,0 +1,28 @@
import { NgModule } from '@angular/core'
import { CommonModule } from '@angular/common'
import { IonicModule } from '@ionic/angular'
import { RouterModule, Routes } from '@angular/router'
import { DeveloperPage } from './developer-list.page'
import { BadgeMenuComponentModule } from 'src/app/components/badge-menu-button/badge-menu.component.module'
import { SharingModule } from 'src/app/modules/sharing.module'
import { BackupReportPageModule } from 'src/app/modals/backup-report/backup-report.module'
const routes: Routes = [
{
path: '',
component: DeveloperPage,
},
]
@NgModule({
imports: [
CommonModule,
IonicModule,
RouterModule.forChild(routes),
BadgeMenuComponentModule,
SharingModule,
BackupReportPageModule,
],
declarations: [DeveloperPage],
})
export class DeveloperPageModule {}

View File

@@ -0,0 +1,26 @@
<ion-header>
<ion-toolbar>
<ion-buttons slot="start">
<ion-back-button></ion-back-button>
</ion-buttons>
<ion-title>Developer Tools</ion-title>
</ion-toolbar>
</ion-header>
<ion-content>
<ion-item-divider>Components</ion-item-divider>
<ion-item button detail (click)="navToInstructions()">
<ion-icon slot="start" name="list-outline"></ion-icon>
<ion-label>
<h2>Instructions Generator</h2>
<p>Create instructions and see how they will appear to the end user</p>
</ion-label>
</ion-item>
<ion-item button detail (click)="navToConfig()">
<ion-icon slot="start" name="construct-outline"></ion-icon>
<ion-label>
<h2>Config Generator</h2>
<p>Edit the config with YAML and see it in real time</p>
</ion-label>
</ion-item>
</ion-content>

View File

@@ -0,0 +1,23 @@
import { Component } from '@angular/core'
import { ActivatedRoute } from '@angular/router'
import { NavController } from '@ionic/angular'
@Component({
selector: 'developer-list',
templateUrl: 'developer-list.page.html',
styleUrls: ['developer-list.page.scss'],
})
export class DeveloperPage {
constructor(
private readonly navCtrl: NavController,
private readonly route: ActivatedRoute,
) {}
navToConfig() {
this.navCtrl.navigateForward(['config'], { relativeTo: this.route })
}
navToInstructions() {
this.navCtrl.navigateForward(['instructions'], { relativeTo: this.route })
}
}

View File

@@ -0,0 +1,30 @@
import { NgModule } from '@angular/core'
import { Routes, RouterModule } from '@angular/router'
const routes: Routes = [
{
path: '',
loadChildren: () =>
import('./developer-list/developer-list.module').then(
m => m.DeveloperPageModule,
),
},
{
path: 'config',
loadChildren: () =>
import('./dev-config/dev-config.module').then(m => m.DevConfigPageModule),
},
{
path: 'instructions',
loadChildren: () =>
import('./dev-instructions/dev-instructions.module').then(
m => m.DevInstructionsPageModule,
),
},
]
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule],
})
export class DeveloperRoutingModule {}

View File

@@ -3,22 +3,26 @@
<ion-buttons slot="start">
<ion-back-button defaultHref="embassy"></ion-back-button>
</ion-buttons>
<ion-title>Preferences</ion-title>
<ion-title (click)="addClick()">Preferences</ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding-top">
<ion-item-group *ngIf="patch.data['server-info'] as server">
<ion-item-divider>General</ion-item-divider>
<ion-item button (click)="presentModalName()">
<ion-label>{{ fields['name'].name }}</ion-label>
<ion-label>Device Name</ion-label>
<ion-note slot="end">{{ patch.data.ui.name || defaultName }}</ion-note>
</ion-item>
<ion-item button (click)="serverConfig.presentAlert('share-stats', server['share-stats'])">
<ion-item
button
(click)="serverConfig.presentAlert('share-stats', server['share-stats'])"
>
<ion-label>Auto Report Bugs</ion-label>
<ion-note slot="end">{{ server['share-stats'] ? 'Enabled' : 'Disabled' }}</ion-note>
<ion-note slot="end"
>{{ server['share-stats'] ? 'Enabled' : 'Disabled' }}</ion-note
>
</ion-item>
<!-- <ion-item button (click)="presentModalValueEdit('password')">
@@ -27,15 +31,15 @@
</ion-item> -->
<ion-item-divider>Marketplace</ion-item-divider>
<ion-item button (click)="serverConfig.presentAlert('auto-check-updates', patch.data.ui['auto-check-updates'])">
<ion-item
button
(click)="serverConfig.presentAlert('auto-check-updates', patch.data.ui['auto-check-updates'])"
>
<ion-label>Auto Check for Updates</ion-label>
<ion-note slot="end">{{ patch.data.ui['auto-check-updates'] ? 'Enabled' : 'Disabled' }}</ion-note>
<ion-note slot="end"
>{{ patch.data.ui['auto-check-updates'] ? 'Enabled' : 'Disabled'
}}</ion-note
>
</ion-item>
<!-- <ion-item button (click)="presentModalValueEdit('packageMarketplace', server['package-marketplace'])">
<ion-label>Package Marketplace</ion-label>
<ion-note slot="end">{{ server['package-marketplace'] }}</ion-note>
</ion-item> -->
</ion-item-group>
</ion-content>
</ion-content>

View File

@@ -1,10 +1,18 @@
import { Component, ViewChild } from '@angular/core'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { IonContent, LoadingController, ModalController } from '@ionic/angular'
import { GenericInputComponent, GenericInputOptions } from 'src/app/modals/generic-input/generic-input.component'
import { ConfigSpec } from 'src/app/pkg-config/config-types'
import {
IonContent,
LoadingController,
ModalController,
ToastController,
} from '@ionic/angular'
import {
GenericInputComponent,
GenericInputOptions,
} from 'src/app/modals/generic-input/generic-input.component'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { ServerConfigService } from 'src/app/services/server-config.service'
import { LocalStorageService } from '../../../services/local-storage.service'
@Component({
selector: 'preferences',
@@ -13,26 +21,28 @@ import { ServerConfigService } from 'src/app/services/server-config.service'
})
export class PreferencesPage {
@ViewChild(IonContent) content: IonContent
fields = fields
defaultName: string
clicks = 0
constructor (
constructor(
private readonly loadingCtrl: LoadingController,
private readonly modalCtrl: ModalController,
private readonly api: ApiService,
public readonly serverConfig: ServerConfigService,
private readonly toastCtrl: ToastController,
private readonly localStorageService: LocalStorageService,
public readonly patch: PatchDbService,
) { }
) {}
ngOnInit () {
ngOnInit() {
this.defaultName = `Embassy-${this.patch.getData()['server-info'].id}`
}
ngAfterViewInit () {
ngAfterViewInit() {
this.content.scrollToPoint(undefined, 1)
}
async presentModalName (): Promise<void> {
async presentModalName(): Promise<void> {
const options: GenericInputOptions = {
title: 'Edit Device Name',
message: 'This is for your reference only.',
@@ -42,7 +52,8 @@ export class PreferencesPage {
nullable: true,
initialValue: this.patch.getData().ui.name,
buttonText: 'Save',
submitFn: (value: string) => this.setDbValue('name', value || this.defaultName),
submitFn: (value: string) =>
this.setDbValue('name', value || this.defaultName),
}
const modal = await this.modalCtrl.create({
@@ -55,7 +66,7 @@ export class PreferencesPage {
await modal.present()
}
private async setDbValue (key: string, value: string): Promise<void> {
async setDbValue(key: string, value: any): Promise<void> {
const loader = await this.loadingCtrl.create({
spinner: 'lines',
message: 'Saving...',
@@ -69,14 +80,22 @@ export class PreferencesPage {
loader.dismiss()
}
}
}
const fields: ConfigSpec = {
'name': {
name: 'Device Name',
type: 'string',
nullable: false,
masked: false,
copyable: false,
},
async addClick() {
this.clicks++
if (this.clicks >= 5) {
this.clicks = 0
const newVal = await this.localStorageService.toggleShowDevTools()
const toast = await this.toastCtrl.create({
header: newVal ? 'Dev tools unlocked' : 'Dev tools hidden',
position: 'bottom',
duration: 1000,
})
await toast.present()
}
setTimeout(() => {
this.clicks = Math.max(this.clicks - 1, 0)
}, 10000)
}
}

View File

@@ -1,19 +1,36 @@
export interface ConfigSpec { [key: string]: ValueSpec }
export interface ConfigSpec {
[key: string]: ValueSpec
}
export type ValueType = 'string' | 'number' | 'boolean' | 'enum' | 'list' | 'object' | 'pointer' | 'union'
export type ValueType =
| 'string'
| 'number'
| 'boolean'
| 'enum'
| 'list'
| 'object'
| 'pointer'
| 'union'
export type ValueSpec = ValueSpecOf<ValueType>
// core spec types. These types provide the metadata for performing validations
export type ValueSpecOf<T extends ValueType> =
T extends 'string' ? ValueSpecString :
T extends 'number' ? ValueSpecNumber :
T extends 'boolean' ? ValueSpecBoolean :
T extends 'enum' ? ValueSpecEnum :
T extends 'list' ? ValueSpecList :
T extends 'object' ? ValueSpecObject :
T extends 'pointer' ? ValueSpecPointer :
T extends 'union' ? ValueSpecUnion :
never
export type ValueSpecOf<T extends ValueType> = T extends 'string'
? ValueSpecString
: T extends 'number'
? ValueSpecNumber
: T extends 'boolean'
? ValueSpecBoolean
: T extends 'enum'
? ValueSpecEnum
: T extends 'list'
? ValueSpecList
: T extends 'object'
? ValueSpecObject
: T extends 'pointer'
? ValueSpecPointer
: T extends 'union'
? ValueSpecUnion
: never
export interface ValueSpecString extends ListValueSpecString, WithStandalone {
type: 'string'
@@ -62,20 +79,30 @@ export interface WithStandalone {
}
// no lists of booleans, lists, pointers
export type ListValueSpecType = 'string' | 'number' | 'enum' | 'object' | 'union'
export type ListValueSpecType =
| 'string'
| 'number'
| 'enum'
| 'object'
| 'union'
// represents a spec for the values of a list
export type ListValueSpecOf<T extends ListValueSpecType> =
T extends 'string' ? ListValueSpecString :
T extends 'number' ? ListValueSpecNumber :
T extends 'enum' ? ListValueSpecEnum :
T extends 'object' ? ListValueSpecObject :
T extends 'union' ? ListValueSpecUnion :
never
export type ListValueSpecOf<T extends ListValueSpecType> = T extends 'string'
? ListValueSpecString
: T extends 'number'
? ListValueSpecNumber
: T extends 'enum'
? ListValueSpecEnum
: T extends 'object'
? ListValueSpecObject
: T extends 'union'
? ListValueSpecUnion
: never
// represents a spec for a list
export type ValueSpecList = ValueSpecListOf<ListValueSpecType>
export interface ValueSpecListOf<T extends ListValueSpecType> extends WithStandalone {
export interface ValueSpecListOf<T extends ListValueSpecType>
extends WithStandalone {
type: 'list'
subtype: T
spec: ListValueSpecOf<T>
@@ -84,7 +111,10 @@ export interface ValueSpecListOf<T extends ListValueSpecType> extends WithStanda
}
// sometimes the type checker needs just a little bit of help
export function isValueSpecListOf<S extends ListValueSpecType> (t: ValueSpecList, s: S): t is ValueSpecListOf<S> {
export function isValueSpecListOf<S extends ListValueSpecType>(
t: ValueSpecList,
s: S,
): t is ValueSpecListOf<S> {
return t.subtype === s
}
@@ -105,7 +135,6 @@ export interface ListValueSpecNumber {
export interface ListValueSpecEnum {
values: string[]
'values-set'?: Set<string>
'value-names': { [value: string]: string }
}
@@ -129,9 +158,10 @@ export interface UnionTagSpec {
id: string // The name of the field containing one of the union variants
name: string
description?: string
'variant-names': { // the name of each variant
'variant-names': {
// the name of each variant
[variant: string]: string
}
}
export type DefaultString = string | { charset: string, len: number }
export type DefaultString = string | { charset: string; len: number }

View File

@@ -2,7 +2,15 @@ import { Injectable } from '@angular/core'
import { pauseFor } from '../../util/misc.util'
import { ApiService } from './embassy-api.service'
import { PatchOp, Update, Operation, RemoveOperation } from 'patch-db-client'
import { DataModel, DependencyErrorType, InstallProgress, PackageDataEntry, PackageMainStatus, PackageState, ServerStatus } from 'src/app/services/patch-db/data-model'
import {
DataModel,
DependencyErrorType,
InstallProgress,
PackageDataEntry,
PackageMainStatus,
PackageState,
ServerStatus,
} from 'src/app/services/patch-db/data-model'
import { CifsBackupTarget, Log, RR, WithRevision } from './api.types'
import { parsePropertiesPermissive } from 'src/app/util/properties.util'
import { Mock } from './api.fixures'
@@ -17,24 +25,22 @@ export class MockApiService extends ApiService {
private readonly revertTime = 4000
sequence: number
constructor (
private readonly bootstrapper: LocalStorageBootstrap,
) {
constructor(private readonly bootstrapper: LocalStorageBootstrap) {
super()
}
async getStatic (url: string): Promise<string> {
async getStatic(url: string): Promise<string> {
await pauseFor(2000)
return markdown
}
// db
async getRevisions (since: number): Promise<RR.GetRevisionsRes> {
async getRevisions(since: number): Promise<RR.GetRevisionsRes> {
return this.getDump()
}
async getDump (): Promise<RR.GetDumpRes> {
async getDump(): Promise<RR.GetDumpRes> {
const cache = await this.bootstrapper.init()
return {
id: cache.sequence,
@@ -43,7 +49,7 @@ export class MockApiService extends ApiService {
}
}
async setDbValueRaw (params: RR.SetDBValueReq): Promise<RR.SetDBValueRes> {
async setDbValueRaw(params: RR.SetDBValueReq): Promise<RR.SetDBValueRes> {
await pauseFor(2000)
const patch = [
{
@@ -57,7 +63,7 @@ export class MockApiService extends ApiService {
// auth
async login (params: RR.LoginReq): Promise<RR.loginRes> {
async login(params: RR.LoginReq): Promise<RR.loginRes> {
await pauseFor(2000)
setTimeout(() => {
@@ -67,24 +73,26 @@ export class MockApiService extends ApiService {
return null
}
async logout (params: RR.LogoutReq): Promise<RR.LogoutRes> {
async logout(params: RR.LogoutReq): Promise<RR.LogoutRes> {
await pauseFor(2000)
return null
}
async getSessions (params: RR.GetSessionsReq): Promise<RR.GetSessionsRes> {
async getSessions(params: RR.GetSessionsReq): Promise<RR.GetSessionsRes> {
await pauseFor(2000)
return Mock.Sessions
}
async killSessions (params: RR.KillSessionsReq): Promise<RR.KillSessionsRes> {
async killSessions(params: RR.KillSessionsReq): Promise<RR.KillSessionsRes> {
await pauseFor(2000)
return null
}
// server
async setShareStatsRaw (params: RR.SetShareStatsReq): Promise<RR.SetShareStatsRes> {
async setShareStatsRaw(
params: RR.SetShareStatsReq,
): Promise<RR.SetShareStatsRes> {
await pauseFor(2000)
const patch = [
{
@@ -97,14 +105,20 @@ export class MockApiService extends ApiService {
return this.withRevision(patch)
}
async getServerLogs (params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> {
async getServerLogs(
params: RR.GetServerLogsReq,
): Promise<RR.GetServerLogsRes> {
await pauseFor(2000)
let entries: Log[]
if (Math.random() < .2) {
if (Math.random() < 0.2) {
entries = Mock.ServerLogs
} else {
const arrLength = params.limit ? Math.ceil(params.limit / Mock.ServerLogs.length) : 10
entries = new Array(arrLength).fill(Mock.ServerLogs).reduce((acc, val) => acc.concat(val), [])
const arrLength = params.limit
? Math.ceil(params.limit / Mock.ServerLogs.length)
: 10
entries = new Array(arrLength)
.fill(Mock.ServerLogs)
.reduce((acc, val) => acc.concat(val), [])
}
return {
entries,
@@ -113,17 +127,23 @@ export class MockApiService extends ApiService {
}
}
async getServerMetrics (params: RR.GetServerMetricsReq): Promise<RR.GetServerMetricsRes> {
async getServerMetrics(
params: RR.GetServerMetricsReq,
): Promise<RR.GetServerMetricsRes> {
await pauseFor(2000)
return Mock.getServerMetrics()
}
async getPkgMetrics (params: RR.GetServerMetricsReq): Promise<RR.GetPackageMetricsRes> {
async getPkgMetrics(
params: RR.GetServerMetricsReq,
): Promise<RR.GetPackageMetricsRes> {
await pauseFor(2000)
return Mock.getAppMetrics()
}
async updateServerRaw (params: RR.UpdateServerReq): Promise<RR.UpdateServerRes> {
async updateServerRaw(
params: RR.UpdateServerReq,
): Promise<RR.UpdateServerRes> {
await pauseFor(2000)
const initialProgress = {
size: 10000,
@@ -145,65 +165,77 @@ export class MockApiService extends ApiService {
return this.withRevision(patch, 'updating')
}
async restartServer (params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
async restartServer(
params: RR.RestartServerReq,
): Promise<RR.RestartServerRes> {
await pauseFor(2000)
return null
}
async shutdownServer (params: RR.ShutdownServerReq): Promise<RR.ShutdownServerRes> {
async shutdownServer(
params: RR.ShutdownServerReq,
): Promise<RR.ShutdownServerRes> {
await pauseFor(2000)
return null
}
async systemRebuild (params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
async systemRebuild(
params: RR.RestartServerReq,
): Promise<RR.RestartServerRes> {
await pauseFor(2000)
return null
}
// marketplace URLs
async getEos (params: RR.GetMarketplaceEOSReq): Promise<RR.GetMarketplaceEOSRes> {
async getEos(
params: RR.GetMarketplaceEOSReq,
): Promise<RR.GetMarketplaceEOSRes> {
await pauseFor(2000)
return Mock.MarketplaceEos
}
async getMarketplaceData (params: RR.GetMarketplaceDataReq): Promise<RR.GetMarketplaceDataRes> {
async getMarketplaceData(
params: RR.GetMarketplaceDataReq,
): Promise<RR.GetMarketplaceDataRes> {
await pauseFor(2000)
return {
categories: ['featured', 'bitcoin', 'lightning', 'data', 'messaging', 'social', 'alt coin'],
categories: [
'featured',
'bitcoin',
'lightning',
'data',
'messaging',
'social',
'alt coin',
],
}
}
async getMarketplacePkgs (params: RR.GetMarketplacePackagesReq): Promise<RR.GetMarketplacePackagesRes> {
async getMarketplacePkgs(
params: RR.GetMarketplacePackagesReq,
): Promise<RR.GetMarketplacePackagesRes> {
await pauseFor(2000)
return Mock.MarketplacePkgsList
}
async getReleaseNotes (params: RR.GetReleaseNotesReq): Promise<RR.GetReleaseNotesRes> {
async getReleaseNotes(
params: RR.GetReleaseNotesReq,
): Promise<RR.GetReleaseNotesRes> {
await pauseFor(2000)
return Mock.ReleaseNotes
}
async getLatestVersion (params: RR.GetLatestVersionReq): Promise<RR.GetLatestVersionRes> {
async getLatestVersion(
params: RR.GetLatestVersionReq,
): Promise<RR.GetLatestVersionRes> {
await pauseFor(2000)
return params.ids.reduce((obj, id) => {
obj[id] = '1.3.0'
return obj
}, { })
}, {})
}
// async setPackageMarketplaceRaw (params: RR.SetPackageMarketplaceReq): Promise<RR.SetPackageMarketplaceRes> {
// await pauseFor(2000)
// const patch = [
// {
// op: PatchOp.REPLACE,
// path: '/server-info/package-marketplace',
// value: params.url,
// },
// ]
// return this.http.rpcRequest<WithRevision<null>>({ method: 'db.patch', params: { patch } })
// }
// password
// async updatePassword (params: RR.UpdatePasswordReq): Promise<RR.UpdatePasswordRes> {
// await pauseFor(2000)
@@ -212,7 +244,9 @@ export class MockApiService extends ApiService {
// notification
async getNotificationsRaw (params: RR.GetNotificationsReq): Promise<RR.GetNotificationsRes> {
async getNotificationsRaw(
params: RR.GetNotificationsReq,
): Promise<RR.GetNotificationsRes> {
await pauseFor(2000)
const patch = [
{
@@ -225,72 +259,82 @@ export class MockApiService extends ApiService {
return this.withRevision(patch, Mock.Notifications)
}
async deleteNotification (params: RR.DeleteNotificationReq): Promise<RR.DeleteNotificationRes> {
async deleteNotification(
params: RR.DeleteNotificationReq,
): Promise<RR.DeleteNotificationRes> {
await pauseFor(2000)
return null
}
async deleteAllNotifications (params: RR.DeleteAllNotificationsReq): Promise<RR.DeleteAllNotificationsRes> {
async deleteAllNotifications(
params: RR.DeleteAllNotificationsReq,
): Promise<RR.DeleteAllNotificationsRes> {
await pauseFor(2000)
return null
}
// wifi
async getWifi (params: RR.GetWifiReq): Promise < RR.GetWifiRes > {
async getWifi(params: RR.GetWifiReq): Promise<RR.GetWifiRes> {
await pauseFor(2000)
return Mock.Wifi
}
async setWifiCountry (params: RR.SetWifiCountryReq): Promise <RR.SetWifiCountryRes> {
async setWifiCountry(
params: RR.SetWifiCountryReq,
): Promise<RR.SetWifiCountryRes> {
await pauseFor(2000)
return null
}
async addWifi (params: RR.AddWifiReq): Promise<RR.AddWifiRes> {
async addWifi(params: RR.AddWifiReq): Promise<RR.AddWifiRes> {
await pauseFor(2000)
return null
}
async connectWifi (params: RR.ConnectWifiReq): Promise<RR.ConnectWifiRes> {
async connectWifi(params: RR.ConnectWifiReq): Promise<RR.ConnectWifiRes> {
await pauseFor(2000)
return null
}
async deleteWifi (params: RR.DeleteWifiReq): Promise<RR.DeleteWifiRes> {
async deleteWifi(params: RR.DeleteWifiReq): Promise<RR.DeleteWifiRes> {
await pauseFor(2000)
return null
}
// ssh
async getSshKeys (params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes> {
async getSshKeys(params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes> {
await pauseFor(2000)
return Mock.SshKeys
}
async addSshKey (params: RR.AddSSHKeyReq): Promise<RR.AddSSHKeyRes> {
async addSshKey(params: RR.AddSSHKeyReq): Promise<RR.AddSSHKeyRes> {
await pauseFor(2000)
return Mock.SshKey
}
async deleteSshKey (params: RR.DeleteSSHKeyReq): Promise<RR.DeleteSSHKeyRes> {
async deleteSshKey(params: RR.DeleteSSHKeyReq): Promise<RR.DeleteSSHKeyRes> {
await pauseFor(2000)
return null
}
// backup
async getBackupTargets (params: RR.GetBackupTargetsReq): Promise<RR.GetBackupTargetsRes> {
async getBackupTargets(
params: RR.GetBackupTargetsReq,
): Promise<RR.GetBackupTargetsRes> {
await pauseFor(2000)
return Mock.BackupTargets
}
async addBackupTarget (params: RR.AddBackupTargetReq): Promise<RR.AddBackupTargetRes> {
async addBackupTarget(
params: RR.AddBackupTargetReq,
): Promise<RR.AddBackupTargetRes> {
await pauseFor(2000)
const { hostname, path, username } = params
return {
'latfgvwdbhjsndmk': {
latfgvwdbhjsndmk: {
type: 'cifs',
hostname,
path: path.replace(/\\/g, '/'),
@@ -301,12 +345,14 @@ export class MockApiService extends ApiService {
}
}
async updateBackupTarget (params: RR.UpdateBackupTargetReq): Promise<RR.UpdateBackupTargetRes> {
async updateBackupTarget(
params: RR.UpdateBackupTargetReq,
): Promise<RR.UpdateBackupTargetRes> {
await pauseFor(2000)
const { id, hostname, path, username } = params
return {
[id]: {
...Mock.BackupTargets[id] as CifsBackupTarget,
...(Mock.BackupTargets[id] as CifsBackupTarget),
hostname,
path,
username,
@@ -314,17 +360,23 @@ export class MockApiService extends ApiService {
}
}
async removeBackupTarget (params: RR.RemoveBackupTargetReq): Promise<RR.RemoveBackupTargetRes> {
async removeBackupTarget(
params: RR.RemoveBackupTargetReq,
): Promise<RR.RemoveBackupTargetRes> {
await pauseFor(2000)
return null
}
async getBackupInfo (params: RR.GetBackupInfoReq): Promise<RR.GetBackupInfoRes> {
async getBackupInfo(
params: RR.GetBackupInfoReq,
): Promise<RR.GetBackupInfoRes> {
await pauseFor(2000)
return Mock.BackupInfo
}
async createBackupRaw (params: RR.CreateBackupReq): Promise<RR.CreateBackupRes> {
async createBackupRaw(
params: RR.CreateBackupReq,
): Promise<RR.CreateBackupRes> {
await pauseFor(2000)
const path = '/server-info/status'
const ids = ['bitcoind', 'lnd']
@@ -373,19 +425,27 @@ export class MockApiService extends ApiService {
// package
async getPackageProperties (params: RR.GetPackagePropertiesReq): Promise<RR.GetPackagePropertiesRes<2>['data']> {
async getPackageProperties(
params: RR.GetPackagePropertiesReq,
): Promise<RR.GetPackagePropertiesRes<2>['data']> {
await pauseFor(2000)
return parsePropertiesPermissive(Mock.PackageProperties)
}
async getPackageLogs (params: RR.GetPackageLogsReq): Promise<RR.GetPackageLogsRes> {
async getPackageLogs(
params: RR.GetPackageLogsReq,
): Promise<RR.GetPackageLogsRes> {
await pauseFor(2000)
let entries
if (Math.random() < .2) {
entries = Mock.PackageLogs
if (Math.random() < 0.2) {
entries = Mock.PackageLogs
} else {
const arrLength = params.limit ? Math.ceil(params.limit / Mock.PackageLogs.length) : 10
entries = new Array(arrLength).fill(Mock.PackageLogs).reduce((acc, val) => acc.concat(val), [])
const arrLength = params.limit
? Math.ceil(params.limit / Mock.PackageLogs.length)
: 10
entries = new Array(arrLength)
.fill(Mock.PackageLogs)
.reduce((acc, val) => acc.concat(val), [])
}
return {
entries,
@@ -394,7 +454,9 @@ export class MockApiService extends ApiService {
}
}
async installPackageRaw (params: RR.InstallPackageReq): Promise<RR.InstallPackageRes> {
async installPackageRaw(
params: RR.InstallPackageReq,
): Promise<RR.InstallPackageRes> {
await pauseFor(2000)
const initialProgress: InstallProgress = {
size: 120,
@@ -427,12 +489,16 @@ export class MockApiService extends ApiService {
return this.withRevision(patch)
}
async dryUpdatePackage (params: RR.DryUpdatePackageReq): Promise<RR.DryUpdatePackageRes> {
async dryUpdatePackage(
params: RR.DryUpdatePackageReq,
): Promise<RR.DryUpdatePackageRes> {
await pauseFor(2000)
return { }
return {}
}
async getPackageConfig (params: RR.GetPackageConfigReq): Promise<RR.GetPackageConfigRes> {
async getPackageConfig(
params: RR.GetPackageConfigReq,
): Promise<RR.GetPackageConfigRes> {
await pauseFor(2000)
return {
config: Mock.MockConfig,
@@ -440,12 +506,16 @@ export class MockApiService extends ApiService {
}
}
async drySetPackageConfig (params: RR.DrySetPackageConfigReq): Promise<RR.DrySetPackageConfigRes> {
async drySetPackageConfig(
params: RR.DrySetPackageConfigReq,
): Promise<RR.DrySetPackageConfigRes> {
await pauseFor(2000)
return { }
return {}
}
async setPackageConfigRaw (params: RR.SetPackageConfigReq): Promise<RR.SetPackageConfigRes> {
async setPackageConfigRaw(
params: RR.SetPackageConfigReq,
): Promise<RR.SetPackageConfigRes> {
await pauseFor(2000)
const patch = [
{
@@ -457,10 +527,11 @@ export class MockApiService extends ApiService {
return this.withRevision(patch)
}
async restorePackagesRaw (params: RR.RestorePackagesReq): Promise<RR.RestorePackagesRes> {
async restorePackagesRaw(
params: RR.RestorePackagesReq,
): Promise<RR.RestorePackagesRes> {
await pauseFor(2000)
const patch: Operation[] = params.ids.map(id => {
const initialProgress: InstallProgress = {
size: 120,
downloaded: 120,
@@ -492,12 +563,16 @@ export class MockApiService extends ApiService {
return this.withRevision(patch)
}
async executePackageAction (params: RR.ExecutePackageActionReq): Promise<RR.ExecutePackageActionRes> {
async executePackageAction(
params: RR.ExecutePackageActionReq,
): Promise<RR.ExecutePackageActionRes> {
await pauseFor(2000)
return Mock.ActionResponse
}
async startPackageRaw (params: RR.StartPackageReq): Promise<RR.StartPackageRes> {
async startPackageRaw(
params: RR.StartPackageReq,
): Promise<RR.StartPackageRes> {
const path = `/package-data/${params.id}/installed/status/main`
await pauseFor(2000)
@@ -551,11 +626,11 @@ export class MockApiService extends ApiService {
message: 'Bitcoin is syncing from genesis',
},
'p2p-interface': {
result: 'success',
result: 'success',
},
'rpc-interface': {
result: 'failure',
error: 'RPC interface unreachable.',
result: 'failure',
error: 'RPC interface unreachable.',
},
},
},
@@ -574,10 +649,12 @@ export class MockApiService extends ApiService {
return this.withRevision(originalPatch)
}
async dryStopPackage (params: RR.DryStopPackageReq): Promise<RR.DryStopPackageRes> {
async dryStopPackage(
params: RR.DryStopPackageReq,
): Promise<RR.DryStopPackageRes> {
await pauseFor(2000)
return {
'lnd': {
lnd: {
dependency: 'bitcoind',
error: {
type: DependencyErrorType.NotRunning,
@@ -586,7 +663,7 @@ export class MockApiService extends ApiService {
}
}
async stopPackageRaw (params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
async stopPackageRaw(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
await pauseFor(2000)
const path = `/package-data/${params.id}/installed/status/main`
@@ -610,19 +687,23 @@ export class MockApiService extends ApiService {
{
op: PatchOp.REPLACE,
path: path + '/health',
value: { },
value: {},
},
]
return this.withRevision(patch)
}
async dryUninstallPackage (params: RR.DryUninstallPackageReq): Promise<RR.DryUninstallPackageRes> {
async dryUninstallPackage(
params: RR.DryUninstallPackageReq,
): Promise<RR.DryUninstallPackageRes> {
await pauseFor(2000)
return { }
return {}
}
async uninstallPackageRaw (params: RR.UninstallPackageReq): Promise<RR.UninstallPackageRes> {
async uninstallPackageRaw(
params: RR.UninstallPackageReq,
): Promise<RR.UninstallPackageRes> {
await pauseFor(2000)
setTimeout(async () => {
@@ -646,7 +727,9 @@ export class MockApiService extends ApiService {
return this.withRevision(patch)
}
async deleteRecoveredPackageRaw (params: RR.DeleteRecoveredPackageReq): Promise<RR.DeleteRecoveredPackageRes> {
async deleteRecoveredPackageRaw(
params: RR.DeleteRecoveredPackageReq,
): Promise<RR.DeleteRecoveredPackageRes> {
await pauseFor(2000)
const patch = [
{
@@ -657,7 +740,9 @@ export class MockApiService extends ApiService {
return this.withRevision(patch)
}
async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise<RR.DryConfigureDependencyRes> {
async dryConfigureDependency(
params: RR.DryConfigureDependencyReq,
): Promise<RR.DryConfigureDependencyRes> {
await pauseFor(2000)
return {
'old-config': Mock.MockConfig,
@@ -666,11 +751,14 @@ export class MockApiService extends ApiService {
}
}
private async updateProgress (id: string, initialProgress: InstallProgress): Promise<void> {
private async updateProgress(
id: string,
initialProgress: InstallProgress,
): Promise<void> {
const phases = [
{ progress: 'downloaded', completion: 'download-complete'},
{ progress: 'validated', completion: 'validation-complete'},
{ progress: 'unpacked', completion: 'unpack-complete'},
{ progress: 'downloaded', completion: 'download-complete' },
{ progress: 'validated', completion: 'validation-complete' },
{ progress: 'unpacked', completion: 'unpack-complete' },
]
for (let phase of phases) {
let i = initialProgress[phase.progress]
@@ -709,7 +797,7 @@ export class MockApiService extends ApiService {
}, 1000)
}
private async updateOSProgress (size: number) {
private async updateOSProgress(size: number) {
let downloaded = 0
while (downloaded < size) {
await pauseFor(250)
@@ -759,7 +847,7 @@ export class MockApiService extends ApiService {
}, 1000)
}
private async updateMock (patch: Operation[]): Promise<void> {
private async updateMock(patch: Operation[]): Promise<void> {
if (!this.sequence) {
const { sequence } = await this.bootstrapper.init()
this.sequence = sequence
@@ -772,7 +860,10 @@ export class MockApiService extends ApiService {
this.mockPatch$.next(revision)
}
private async withRevision<T> (patch: Operation[], response: T = null): Promise<WithRevision<T>> {
private async withRevision<T>(
patch: Operation[],
response: T = null,
): Promise<WithRevision<T>> {
if (!this.sequence) {
const { sequence } = await this.bootstrapper.init()
this.sequence = sequence

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,24 @@
import { Injectable } from '@angular/core'
import { Storage } from '@ionic/storage-angular'
import { BehaviorSubject } from 'rxjs'
const SHOW_DEV_TOOLS = 'SHOW_DEV_TOOLS'
@Injectable({
providedIn: 'root',
})
export class LocalStorageService {
showDevTools$: BehaviorSubject<boolean> = new BehaviorSubject(false)
constructor(private readonly storage: Storage) {}
async init() {
const val = await this.storage.get(SHOW_DEV_TOOLS)
this.showDevTools$.next(!!val)
}
async toggleShowDevTools(): Promise<boolean> {
const newVal = !(await this.storage.get(SHOW_DEV_TOOLS))
await this.storage.set(SHOW_DEV_TOOLS, newVal)
this.showDevTools$.next(newVal)
return newVal
}
}

View File

@@ -40,9 +40,9 @@ export enum ServerStatus {
BackingUp = 'backing-up',
}
export interface RecoveredPackageDataEntry {
title: string,
icon: URL,
version: string,
title: string
icon: URL
version: string
}
export interface PackageDataEntry {
@@ -53,8 +53,8 @@ export interface PackageDataEntry {
icon: URL
}
manifest: Manifest
installed?: InstalledPackageDataEntry, // exists when: installed, updating
'install-progress'?: InstallProgress, // exists when: installing, updating
installed?: InstalledPackageDataEntry // exists when: installed, updating
'install-progress'?: InstallProgress // exists when: installing, updating
}
export interface InstallProgress {
@@ -69,7 +69,7 @@ export interface InstallProgress {
export interface InstalledPackageDataEntry {
status: Status
manifest: Manifest,
manifest: Manifest
'last-backup': string | null
'system-pointers': any[]
'current-dependents': { [id: string]: CurrentDependencyInfo }
@@ -81,7 +81,7 @@ export interface InstalledPackageDataEntry {
}
}
'interface-addresses': {
[id: string]: { 'tor-address': string, 'lan-address': string }
[id: string]: { 'tor-address': string; 'lan-address': string }
}
}
@@ -121,7 +121,10 @@ export interface Manifest {
stop: string | null
}
main: ActionImpl
'health-checks': Record<string, ActionImpl & { name: string, description: string }>
'health-checks': Record<
string,
ActionImpl & { name: string; description: string }
>
config: ConfigActions | null
volumes: Record<string, Volume>
'min-os-version': string
@@ -209,7 +212,7 @@ export interface TorConfig {
}
export type LanConfig = {
[port: number]: { ssl: boolean, mapping: number }
[port: number]: { ssl: boolean; mapping: number }
}
export interface BackupActions {
@@ -237,7 +240,12 @@ export interface Status {
'dependency-errors': { [id: string]: DependencyError | null }
}
export type MainStatus = MainStatusStopped | MainStatusStopping | MainStatusStarting | MainStatusRunning | MainStatusBackingUp
export type MainStatus =
| MainStatusStopped
| MainStatusStopping
| MainStatusStarting
| MainStatusRunning
| MainStatusBackingUp
export interface MainStatusStopped {
status: PackageMainStatus.Stopped
@@ -270,11 +278,12 @@ export enum PackageMainStatus {
BackingUp = 'backing-up',
}
export type HealthCheckResult = HealthCheckResultStarting |
HealthCheckResultLoading |
HealthCheckResultDisabled |
HealthCheckResultSuccess |
HealthCheckResultFailure
export type HealthCheckResult =
| HealthCheckResultStarting
| HealthCheckResultLoading
| HealthCheckResultDisabled
| HealthCheckResultSuccess
| HealthCheckResultFailure
export enum HealthResult {
Starting = 'starting',
@@ -306,12 +315,13 @@ export interface HealthCheckResultFailure {
error: string
}
export type DependencyError = DependencyErrorNotInstalled |
DependencyErrorNotRunning |
DependencyErrorIncorrectVersion |
DependencyErrorConfigUnsatisfied |
DependencyErrorHealthChecksFailed |
DependencyErrorTransitive
export type DependencyError =
| DependencyErrorNotInstalled
| DependencyErrorNotRunning
| DependencyErrorIncorrectVersion
| DependencyErrorConfigUnsatisfied
| DependencyErrorHealthChecksFailed
| DependencyErrorTransitive
export enum DependencyErrorType {
NotInstalled = 'not-installed',
@@ -357,18 +367,21 @@ export interface DependencyInfo {
export interface DependencyEntry {
version: string
requirement: {
type: 'opt-in'
how: string
} | {
type: 'opt-out'
how: string
} | {
type: 'required'
}
requirement:
| {
type: 'opt-in'
how: string
}
| {
type: 'opt-out'
how: string
}
| {
type: 'required'
}
description: string | null
config: {
check: ActionImpl,
check: ActionImpl
'auto-configure': ActionImpl
}
}

View File

@@ -9,15 +9,14 @@ import { ErrorToastService } from './error-toast.service'
providedIn: 'root',
})
export class ServerConfigService {
constructor (
constructor(
private readonly loadingCtrl: LoadingController,
private readonly errToast: ErrorToastService,
private readonly alertCtrl: AlertController,
private readonly embassyApi: ApiService,
) { }
) {}
async presentAlert (key: string, current?: any): Promise<HTMLIonAlertElement> {
async presentAlert(key: string, current?: any): Promise<HTMLIonAlertElement> {
const spec = serverConfig[key]
let inputs: AlertInput[]
@@ -100,14 +99,11 @@ export class ServerConfigService {
saveFns: { [key: string]: (val: any) => Promise<any> } = {
'auto-check-updates': async (enabled: boolean) => {
return this.embassyApi.setDbValue({ pointer: '/auto-check-updates', value: enabled })
return this.embassyApi.setDbValue({
pointer: '/auto-check-updates',
value: enabled,
})
},
// 'eos-marketplace': async () => {
// return this.embassyApi.setEosMarketplace()
// },
// 'package-marketplace': async (url: string) => {
// return this.embassyApi.setPackageMarketplace({ url })
// },
'share-stats': async (enabled: boolean) => {
return this.embassyApi.setShareStats({ value: enabled })
},
@@ -121,31 +117,16 @@ export const serverConfig: ConfigSpec = {
'auto-check-updates': {
type: 'boolean',
name: 'Auto Check for Updates',
description: 'If enabled, EmbassyOS will automatically check for updates of itself and any installed services. Updating will still require your approval and action. Updates will never be performed automatically.',
description:
'If enabled, EmbassyOS will automatically check for updates of itself and any installed services. Updating will still require your approval and action. Updates will never be performed automatically.',
default: true,
},
// 'eos-marketplace': {
// type: 'boolean',
// name: 'Tor Only Marketplace',
// description: `Use Start9's Tor (instead of clearnet) Marketplace.`,
// warning: 'This will result in higher latency and slower download times.',
// default: false,
// },
// 'package-marketplace': {
// type: 'string',
// name: 'Package Marketplace',
// description: `Use for alternative embassy marketplace. Leave empty to use start9's marketplace.`,
// nullable: true,
// // @TODO regex for URL
// // pattern: '',
// 'pattern-description': 'Must be a valid URL.',
// masked: false,
// copyable: false,
// },
'share-stats': {
type: 'boolean',
name: 'Report Bugs',
description: new IonicSafeString(`If enabled, generic error codes will be anonymously transmitted over Tor to the Start9 team. This helps us identify and fix bugs quickly. <a href="https://docs.start9.com/user-manual/general/user-preferences/report-bugs.html" target="_blank" rel="noreferrer">Read more</a> `) as any,
description: new IonicSafeString(
`If enabled, generic error codes will be anonymously transmitted over Tor to the Start9 team. This helps us identify and fix bugs quickly. <a href="https://docs.start9.com/user-manual/general/user-preferences/report-bugs.html" target="_blank" rel="noreferrer">Read more</a> `,
) as any,
default: false,
},
// password: {