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", "glob": "**/*.svg",
"input": "node_modules/ionicons/dist/ionicons/svg", "input": "node_modules/ionicons/dist/ionicons/svg",
"output": "./svg" "output": "./svg"
},
{
"glob": "**/*",
"input": "node_modules/monaco-editor",
"output": "assets/monaco-editor/"
} }
], ],
"styles": [ "styles": [

View File

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

View File

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

View File

@@ -12,30 +12,52 @@ const routes: Routes = [
{ {
path: 'login', path: 'login',
canActivate: [UnauthGuard], canActivate: [UnauthGuard],
loadChildren: () => import('./pages/login/login.module').then(m => m.LoginPageModule), loadChildren: () =>
import('./pages/login/login.module').then(m => m.LoginPageModule),
}, },
{ {
path: 'embassy', path: 'embassy',
canActivate: [AuthGuard], canActivate: [AuthGuard],
canActivateChild: [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', path: 'marketplace',
canActivate: [AuthGuard], canActivate: [AuthGuard],
canActivateChild: [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', path: 'notifications',
canActivate: [AuthGuard], canActivate: [AuthGuard],
loadChildren: () => import('./pages/notifications/notifications.module').then(m => m.NotificationsPageModule), loadChildren: () =>
import('./pages/notifications/notifications.module').then(
m => m.NotificationsPageModule,
),
}, },
{ {
path: 'services', path: 'services',
canActivate: [AuthGuard], canActivate: [AuthGuard],
canActivateChild: [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], exports: [RouterModule],
}) })
export class AppRoutingModule { } export class AppRoutingModule {}

View File

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

View File

@@ -3,19 +3,33 @@ import { Storage } from '@ionic/storage-angular'
import { AuthService, AuthState } from './services/auth.service' import { AuthService, AuthState } from './services/auth.service'
import { ApiService } from './services/api/embassy-api.service' import { ApiService } from './services/api/embassy-api.service'
import { Router, RoutesRecognized } from '@angular/router' import { Router, RoutesRecognized } from '@angular/router'
import { debounceTime, distinctUntilChanged, filter, finalize, take, takeWhile } from 'rxjs/operators' import {
import { AlertController, IonicSafeString, LoadingController, ToastController } from '@ionic/angular' debounceTime,
distinctUntilChanged,
filter,
take,
} from 'rxjs/operators'
import {
AlertController,
IonicSafeString,
LoadingController,
ToastController,
} from '@ionic/angular'
import { Emver } from './services/emver.service' import { Emver } from './services/emver.service'
import { SplitPaneTracker } from './services/split-pane.service' import { SplitPaneTracker } from './services/split-pane.service'
import { ToastButton } from '@ionic/core' import { ToastButton } from '@ionic/core'
import { PatchDbService } from './services/patch-db/patch-db.service' import { PatchDbService } from './services/patch-db/patch-db.service'
import { ServerStatus } from './services/patch-db/data-model' 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 { StartupAlertsService } from './services/startup-alerts.service'
import { ConfigService } from './services/config.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 { ErrorToastService } from './services/error-toast.service'
import { Subscription } from 'rxjs' import { Subscription } from 'rxjs'
import { LocalStorageService } from './services/local-storage.service'
@Component({ @Component({
selector: 'app-root', selector: 'app-root',
@@ -25,7 +39,7 @@ import { Subscription } from 'rxjs'
export class AppComponent { export class AppComponent {
@HostListener('document:keydown.enter', ['$event']) @HostListener('document:keydown.enter', ['$event'])
@debounce() @debounce()
handleKeyboardEvent () { handleKeyboardEvent() {
const elems = document.getElementsByClassName('enter-click') const elems = document.getElementsByClassName('enter-click')
const elem = elems[elems.length - 1] as HTMLButtonElement const elem = elems[elems.length - 1] as HTMLButtonElement
if (!elem || elem.classList.contains('no-click') || elem.disabled) return if (!elem || elem.classList.contains('no-click') || elem.disabled) return
@@ -41,7 +55,7 @@ export class AppComponent {
serverName: string serverName: string
unreadCount: number unreadCount: number
subscriptions: Subscription[] = [] subscriptions: Subscription[] = []
osUpdateProgress: { size: number, downloaded: number } osUpdateProgress: { size: number; downloaded: number }
appPages = [ appPages = [
{ {
title: 'Services', title: 'Services',
@@ -63,9 +77,14 @@ export class AppComponent {
url: '/notifications', url: '/notifications',
icon: 'notifications-outline', icon: 'notifications-outline',
}, },
{
title: 'Developer Tools',
url: '/developer',
icon: 'hammer-outline',
},
] ]
constructor ( constructor(
private readonly storage: Storage, private readonly storage: Storage,
private readonly authService: AuthService, private readonly authService: AuthService,
private readonly router: Router, private readonly router: Router,
@@ -77,23 +96,24 @@ export class AppComponent {
private readonly startupAlertsService: StartupAlertsService, private readonly startupAlertsService: StartupAlertsService,
private readonly toastCtrl: ToastController, private readonly toastCtrl: ToastController,
private readonly errToast: ErrorToastService, private readonly errToast: ErrorToastService,
private readonly patch: PatchDbService,
private readonly config: ConfigService, private readonly config: ConfigService,
private readonly zone: NgZone, 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.storage.create()
await this.authService.init() await this.authService.init()
await this.localStorageService.init()
this.router.initialNavigation() this.router.initialNavigation()
// watch auth // watch auth
this.authService.watch$() this.authService.watch$().subscribe(async auth => {
.subscribe(async auth => {
// VERIFIED // VERIFIED
if (auth === AuthState.VERIFIED) { if (auth === AuthState.VERIFIED) {
await this.patch.start() await this.patch.start()
@@ -114,26 +134,27 @@ export class AppComponent {
// watch status to display/hide maintenance page // watch status to display/hide maintenance page
]) ])
this.patch.watch$() this.patch
.pipe( .watch$()
filter(obj => !isEmptyObject(obj)), .pipe(
take(1), filter(obj => !isEmptyObject(obj)),
) take(1),
.subscribe(_ => { )
this.subscriptions = this.subscriptions.concat([ .subscribe(_ => {
// watch status to present toast for updated state this.subscriptions = this.subscriptions.concat([
this.watchStatus(), // watch status to present toast for updated state
// watch update-progress to present progress bar when server is updating this.watchStatus(),
this.watchUpdateProgress(), // watch update-progress to present progress bar when server is updating
// watch version to refresh browser window this.watchUpdateProgress(),
this.watchVersion(), // watch version to refresh browser window
// watch unread notification count to display toast this.watchVersion(),
this.watchNotifications(), // watch unread notification count to display toast
// run startup alerts this.watchNotifications(),
this.startupAlertsService.runChecks(), // run startup alerts
]) this.startupAlertsService.runChecks(),
}) ])
// UNVERIFIED })
// UNVERIFIED
} else if (auth === AuthState.UNVERIFIED) { } else if (auth === AuthState.UNVERIFIED) {
this.subscriptions.forEach(sub => sub.unsubscribe()) this.subscriptions.forEach(sub => sub.unsubscribe())
this.subscriptions = [] this.subscriptions = []
@@ -151,20 +172,22 @@ export class AppComponent {
}) })
} }
async goToWebsite (): Promise<void> { async goToWebsite(): Promise<void> {
let url: string let url: string
if (this.config.isTor()) { if (this.config.isTor()) {
url = 'http://privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion' url =
'http://privacy34kn4ez3y3nijweec6w4g54i3g54sdv7r5mr6soma3w4begyd.onion'
} else { } else {
url = 'https://start9.com' url = 'https://start9.com'
} }
window.open(url, '_blank', 'noreferrer') window.open(url, '_blank', 'noreferrer')
} }
async presentAlertLogout () { async presentAlertLogout() {
const alert = await this.alertCtrl.create({ const alert = await this.alertCtrl.create({
header: 'Caution', 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: [ buttons: [
{ {
text: 'Cancel', text: 'Cancel',
@@ -184,97 +207,97 @@ export class AppComponent {
} }
// should wipe cache independant of actual BE logout // should wipe cache independant of actual BE logout
private async logout () { private async logout() {
this.embassyApi.logout({ }) this.embassyApi.logout({})
this.authService.setUnverified() this.authService.setUnverified()
} }
private watchConnection (): Subscription { private watchConnection(): Subscription {
return this.connectionService.watchFailure$() return this.connectionService
.pipe( .watchFailure$()
distinctUntilChanged(), .pipe(distinctUntilChanged(), debounceTime(500))
debounceTime(500), .subscribe(async connectionFailure => {
) if (connectionFailure === ConnectionFailure.None) {
.subscribe(async connectionFailure => { if (this.offlineToast) {
if (connectionFailure === ConnectionFailure.None) { await this.offlineToast.dismiss()
if (this.offlineToast) { this.offlineToast = undefined
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 return this.router.events
.pipe( .pipe(filter((e: RoutesRecognized) => !!e.urlAfterRedirects))
filter((e: RoutesRecognized) => !!e.urlAfterRedirects), .subscribe(e => {
) const appPageIndex = this.appPages.findIndex(appPage =>
.subscribe(e => { e.urlAfterRedirects.startsWith(appPage.url),
const appPageIndex = this.appPages.findIndex( )
appPage => e.urlAfterRedirects.startsWith(appPage.url), if (appPageIndex > -1) this.selectedIndex = appPageIndex
) })
if (appPageIndex > -1) this.selectedIndex = appPageIndex
})
} }
private watchStatus (): Subscription { private watchStatus(): Subscription {
return this.patch.watch$('server-info', 'status') return this.patch.watch$('server-info', 'status').subscribe(status => {
.subscribe(status => {
if (status === ServerStatus.Updated && !this.updateToast) { if (status === ServerStatus.Updated && !this.updateToast) {
this.presentToastUpdated() this.presentToastUpdated()
} }
}) })
} }
private watchUpdateProgress (): Subscription { private watchUpdateProgress(): Subscription {
return this.patch.watch$('server-info', 'update-progress') return this.patch
.subscribe(progress => { .watch$('server-info', 'update-progress')
this.osUpdateProgress = progress .subscribe(progress => {
}) this.osUpdateProgress = progress
})
} }
private watchVersion (): Subscription { private watchVersion(): Subscription {
return this.patch.watch$('server-info', 'version') return this.patch.watch$('server-info', 'version').subscribe(version => {
.subscribe(version => {
if (this.emver.compare(this.config.version, version) !== 0) { if (this.emver.compare(this.config.version, version) !== 0) {
this.presentAlertRefreshNeeded() this.presentAlertRefreshNeeded()
} }
}) })
} }
private watchNotifications (): Subscription { private watchNotifications(): Subscription {
let previous: number let previous: number
return this.patch.watch$('server-info', 'unread-notification-count') return this.patch
.subscribe(count => { .watch$('server-info', 'unread-notification-count')
this.unreadCount = count .subscribe(count => {
if (previous !== undefined && count > previous) this.presentToastNotifications() this.unreadCount = count
previous = count if (previous !== undefined && count > previous)
}) this.presentToastNotifications()
previous = count
})
} }
private async presentAlertRefreshNeeded () { private async presentAlertRefreshNeeded() {
const alert = await this.alertCtrl.create({ const alert = await this.alertCtrl.create({
backdropDismiss: false, backdropDismiss: false,
header: 'Refresh Needed', 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: [ buttons: [
{ {
text: 'Refresh Page', text: 'Refresh Page',
@@ -288,12 +311,13 @@ export class AppComponent {
await alert.present() await alert.present()
} }
private async presentToastUpdated () { private async presentToastUpdated() {
if (this.updateToast) return if (this.updateToast) return
this.updateToast = await this.toastCtrl.create({ this.updateToast = await this.toastCtrl.create({
header: 'EOS download complete!', 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', position: 'bottom',
duration: 0, duration: 0,
cssClass: 'success-toast', cssClass: 'success-toast',
@@ -317,7 +341,7 @@ export class AppComponent {
await this.updateToast.present() await this.updateToast.present()
} }
private async presentToastNotifications () { private async presentToastNotifications() {
if (this.notificationToast) return if (this.notificationToast) return
this.notificationToast = await this.toastCtrl.create({ this.notificationToast = await this.toastCtrl.create({
@@ -337,7 +361,9 @@ export class AppComponent {
side: 'end', side: 'end',
text: 'View', text: 'View',
handler: () => { 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() await this.notificationToast.present()
} }
private async presentToastOffline (message: string | IonicSafeString, link?: string) { private async presentToastOffline(
message: string | IonicSafeString,
link?: string,
) {
if (this.offlineToast) { if (this.offlineToast) {
this.offlineToast.message = message this.offlineToast.message = message
return return
@@ -362,16 +391,14 @@ export class AppComponent {
] ]
if (link) { if (link) {
buttons.push( buttons.push({
{ side: 'end',
side: 'end', text: 'View solutions',
text: 'View solutions', handler: () => {
handler: () => { window.open(link, '_blank', 'noreferrer')
window.open(link, '_blank', 'noreferrer') return false
return false
},
}, },
) })
} }
this.offlineToast = await this.toastCtrl.create({ this.offlineToast = await this.toastCtrl.create({
@@ -385,7 +412,7 @@ export class AppComponent {
await this.offlineToast.present() await this.offlineToast.present()
} }
private async restart (): Promise<void> { private async restart(): Promise<void> {
const loader = await this.loadingCtrl.create({ const loader = await this.loadingCtrl.create({
spinner: 'lines', spinner: 'lines',
message: 'Restarting...', message: 'Restarting...',
@@ -394,7 +421,7 @@ export class AppComponent {
await loader.present() await loader.present()
try { try {
await this.embassyApi.restartServer({ }) await this.embassyApi.restartServer({})
} catch (e) { } catch (e) {
this.errToast.present(e) this.errToast.present(e)
} finally { } finally {
@@ -402,7 +429,7 @@ export class AppComponent {
} }
} }
splitPaneVisible (e: any) { splitPaneVisible(e: any) {
this.splitPane.sidebarOpen$.next(e.detail.visible) 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 { MockApiService } from './services/api/embassy-mock-api.service'
import { LiveApiService } from './services/api/embassy-live-api.service' import { LiveApiService } from './services/api/embassy-live-api.service'
import { WorkspaceConfig } from '@shared' import { WorkspaceConfig } from '@shared'
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'
const { useMocks } = require('../../../../config.json') as WorkspaceConfig const { useMocks } = require('../../../../config.json') as WorkspaceConfig
@@ -47,6 +48,7 @@ const { useMocks } = require('../../../../config.json') as WorkspaceConfig
MarkdownPageModule, MarkdownPageModule,
GenericInputComponentModule, GenericInputComponentModule,
SharingModule, SharingModule,
MonacoEditorModule,
], ],
providers: [ providers: [
FormBuilder, FormBuilder,

View File

@@ -9,20 +9,22 @@ import { getErrorMessage } from 'src/app/services/error-toast.service'
styleUrls: ['./markdown.page.scss'], styleUrls: ['./markdown.page.scss'],
}) })
export class MarkdownPage { export class MarkdownPage {
@Input() contentUrl: string @Input() contentUrl?: string
@Input() content?: string
@Input() title: string @Input() title: string
content: string
loading = true loading = true
loadingError: string | IonicSafeString loadingError: string | IonicSafeString
constructor ( constructor(
private readonly modalCtrl: ModalController, private readonly modalCtrl: ModalController,
private readonly embassyApi: ApiService, private readonly embassyApi: ApiService,
) { } ) {}
async ngOnInit () { async ngOnInit() {
try { try {
this.content = await this.embassyApi.getStatic(this.contentUrl) if (!this.content) {
this.content = await this.embassyApi.getStatic(this.contentUrl)
}
const links = document.links const links = document.links
for (let i = 0, linksLength = links.length; i < linksLength; i++) { for (let i = 0, linksLength = links.length; i < linksLength; i++) {
if (links[i].hostname != window.location.hostname) { if (links[i].hostname != window.location.hostname) {
@@ -38,7 +40,7 @@ export class MarkdownPage {
} }
} }
async dismiss () { async dismiss() {
return this.modalCtrl.dismiss(true) 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-buttons slot="start">
<ion-back-button defaultHref="embassy"></ion-back-button> <ion-back-button defaultHref="embassy"></ion-back-button>
</ion-buttons> </ion-buttons>
<ion-title>Preferences</ion-title> <ion-title (click)="addClick()">Preferences</ion-title>
</ion-toolbar> </ion-toolbar>
</ion-header> </ion-header>
<ion-content class="ion-padding-top"> <ion-content class="ion-padding-top">
<ion-item-group *ngIf="patch.data['server-info'] as server"> <ion-item-group *ngIf="patch.data['server-info'] as server">
<ion-item-divider>General</ion-item-divider> <ion-item-divider>General</ion-item-divider>
<ion-item button (click)="presentModalName()"> <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-note slot="end">{{ patch.data.ui.name || defaultName }}</ion-note>
</ion-item> </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-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>
<!-- <ion-item button (click)="presentModalValueEdit('password')"> <!-- <ion-item button (click)="presentModalValueEdit('password')">
@@ -27,15 +31,15 @@
</ion-item> --> </ion-item> -->
<ion-item-divider>Marketplace</ion-item-divider> <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-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>
<!-- <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-item-group>
</ion-content>
</ion-content>

View File

@@ -1,10 +1,18 @@
import { Component, ViewChild } from '@angular/core' import { Component, ViewChild } from '@angular/core'
import { PatchDbService } from 'src/app/services/patch-db/patch-db.service' import { PatchDbService } from 'src/app/services/patch-db/patch-db.service'
import { IonContent, LoadingController, ModalController } from '@ionic/angular' import {
import { GenericInputComponent, GenericInputOptions } from 'src/app/modals/generic-input/generic-input.component' IonContent,
import { ConfigSpec } from 'src/app/pkg-config/config-types' 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 { ApiService } from 'src/app/services/api/embassy-api.service'
import { ServerConfigService } from 'src/app/services/server-config.service' import { ServerConfigService } from 'src/app/services/server-config.service'
import { LocalStorageService } from '../../../services/local-storage.service'
@Component({ @Component({
selector: 'preferences', selector: 'preferences',
@@ -13,26 +21,28 @@ import { ServerConfigService } from 'src/app/services/server-config.service'
}) })
export class PreferencesPage { export class PreferencesPage {
@ViewChild(IonContent) content: IonContent @ViewChild(IonContent) content: IonContent
fields = fields
defaultName: string defaultName: string
clicks = 0
constructor ( constructor(
private readonly loadingCtrl: LoadingController, private readonly loadingCtrl: LoadingController,
private readonly modalCtrl: ModalController, private readonly modalCtrl: ModalController,
private readonly api: ApiService, private readonly api: ApiService,
public readonly serverConfig: ServerConfigService, public readonly serverConfig: ServerConfigService,
private readonly toastCtrl: ToastController,
private readonly localStorageService: LocalStorageService,
public readonly patch: PatchDbService, public readonly patch: PatchDbService,
) { } ) {}
ngOnInit () { ngOnInit() {
this.defaultName = `Embassy-${this.patch.getData()['server-info'].id}` this.defaultName = `Embassy-${this.patch.getData()['server-info'].id}`
} }
ngAfterViewInit () { ngAfterViewInit() {
this.content.scrollToPoint(undefined, 1) this.content.scrollToPoint(undefined, 1)
} }
async presentModalName (): Promise<void> { async presentModalName(): Promise<void> {
const options: GenericInputOptions = { const options: GenericInputOptions = {
title: 'Edit Device Name', title: 'Edit Device Name',
message: 'This is for your reference only.', message: 'This is for your reference only.',
@@ -42,7 +52,8 @@ export class PreferencesPage {
nullable: true, nullable: true,
initialValue: this.patch.getData().ui.name, initialValue: this.patch.getData().ui.name,
buttonText: 'Save', 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({ const modal = await this.modalCtrl.create({
@@ -55,7 +66,7 @@ export class PreferencesPage {
await modal.present() 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({ const loader = await this.loadingCtrl.create({
spinner: 'lines', spinner: 'lines',
message: 'Saving...', message: 'Saving...',
@@ -69,14 +80,22 @@ export class PreferencesPage {
loader.dismiss() loader.dismiss()
} }
} }
}
const fields: ConfigSpec = { async addClick() {
'name': { this.clicks++
name: 'Device Name', if (this.clicks >= 5) {
type: 'string', this.clicks = 0
nullable: false, const newVal = await this.localStorageService.toggleShowDevTools()
masked: false, const toast = await this.toastCtrl.create({
copyable: false, 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> export type ValueSpec = ValueSpecOf<ValueType>
// core spec types. These types provide the metadata for performing validations // core spec types. These types provide the metadata for performing validations
export type ValueSpecOf<T extends ValueType> = export type ValueSpecOf<T extends ValueType> = T extends 'string'
T extends 'string' ? ValueSpecString : ? ValueSpecString
T extends 'number' ? ValueSpecNumber : : T extends 'number'
T extends 'boolean' ? ValueSpecBoolean : ? ValueSpecNumber
T extends 'enum' ? ValueSpecEnum : : T extends 'boolean'
T extends 'list' ? ValueSpecList : ? ValueSpecBoolean
T extends 'object' ? ValueSpecObject : : T extends 'enum'
T extends 'pointer' ? ValueSpecPointer : ? ValueSpecEnum
T extends 'union' ? ValueSpecUnion : : T extends 'list'
never ? ValueSpecList
: T extends 'object'
? ValueSpecObject
: T extends 'pointer'
? ValueSpecPointer
: T extends 'union'
? ValueSpecUnion
: never
export interface ValueSpecString extends ListValueSpecString, WithStandalone { export interface ValueSpecString extends ListValueSpecString, WithStandalone {
type: 'string' type: 'string'
@@ -62,20 +79,30 @@ export interface WithStandalone {
} }
// no lists of booleans, lists, pointers // 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 // represents a spec for the values of a list
export type ListValueSpecOf<T extends ListValueSpecType> = export type ListValueSpecOf<T extends ListValueSpecType> = T extends 'string'
T extends 'string' ? ListValueSpecString : ? ListValueSpecString
T extends 'number' ? ListValueSpecNumber : : T extends 'number'
T extends 'enum' ? ListValueSpecEnum : ? ListValueSpecNumber
T extends 'object' ? ListValueSpecObject : : T extends 'enum'
T extends 'union' ? ListValueSpecUnion : ? ListValueSpecEnum
never : T extends 'object'
? ListValueSpecObject
: T extends 'union'
? ListValueSpecUnion
: never
// represents a spec for a list // represents a spec for a list
export type ValueSpecList = ValueSpecListOf<ListValueSpecType> export type ValueSpecList = ValueSpecListOf<ListValueSpecType>
export interface ValueSpecListOf<T extends ListValueSpecType> extends WithStandalone { export interface ValueSpecListOf<T extends ListValueSpecType>
extends WithStandalone {
type: 'list' type: 'list'
subtype: T subtype: T
spec: ListValueSpecOf<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 // 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 return t.subtype === s
} }
@@ -105,7 +135,6 @@ export interface ListValueSpecNumber {
export interface ListValueSpecEnum { export interface ListValueSpecEnum {
values: string[] values: string[]
'values-set'?: Set<string>
'value-names': { [value: string]: 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 id: string // The name of the field containing one of the union variants
name: string name: string
description?: string description?: string
'variant-names': { // the name of each variant 'variant-names': {
// the name of each variant
[variant: string]: string [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 { pauseFor } from '../../util/misc.util'
import { ApiService } from './embassy-api.service' import { ApiService } from './embassy-api.service'
import { PatchOp, Update, Operation, RemoveOperation } from 'patch-db-client' 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 { CifsBackupTarget, Log, RR, WithRevision } from './api.types'
import { parsePropertiesPermissive } from 'src/app/util/properties.util' import { parsePropertiesPermissive } from 'src/app/util/properties.util'
import { Mock } from './api.fixures' import { Mock } from './api.fixures'
@@ -17,24 +25,22 @@ export class MockApiService extends ApiService {
private readonly revertTime = 4000 private readonly revertTime = 4000
sequence: number sequence: number
constructor ( constructor(private readonly bootstrapper: LocalStorageBootstrap) {
private readonly bootstrapper: LocalStorageBootstrap,
) {
super() super()
} }
async getStatic (url: string): Promise<string> { async getStatic(url: string): Promise<string> {
await pauseFor(2000) await pauseFor(2000)
return markdown return markdown
} }
// db // db
async getRevisions (since: number): Promise<RR.GetRevisionsRes> { async getRevisions(since: number): Promise<RR.GetRevisionsRes> {
return this.getDump() return this.getDump()
} }
async getDump (): Promise<RR.GetDumpRes> { async getDump(): Promise<RR.GetDumpRes> {
const cache = await this.bootstrapper.init() const cache = await this.bootstrapper.init()
return { return {
id: cache.sequence, 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) await pauseFor(2000)
const patch = [ const patch = [
{ {
@@ -57,7 +63,7 @@ export class MockApiService extends ApiService {
// auth // auth
async login (params: RR.LoginReq): Promise<RR.loginRes> { async login(params: RR.LoginReq): Promise<RR.loginRes> {
await pauseFor(2000) await pauseFor(2000)
setTimeout(() => { setTimeout(() => {
@@ -67,24 +73,26 @@ export class MockApiService extends ApiService {
return null return null
} }
async logout (params: RR.LogoutReq): Promise<RR.LogoutRes> { async logout(params: RR.LogoutReq): Promise<RR.LogoutRes> {
await pauseFor(2000) await pauseFor(2000)
return null return null
} }
async getSessions (params: RR.GetSessionsReq): Promise<RR.GetSessionsRes> { async getSessions(params: RR.GetSessionsReq): Promise<RR.GetSessionsRes> {
await pauseFor(2000) await pauseFor(2000)
return Mock.Sessions return Mock.Sessions
} }
async killSessions (params: RR.KillSessionsReq): Promise<RR.KillSessionsRes> { async killSessions(params: RR.KillSessionsReq): Promise<RR.KillSessionsRes> {
await pauseFor(2000) await pauseFor(2000)
return null return null
} }
// server // server
async setShareStatsRaw (params: RR.SetShareStatsReq): Promise<RR.SetShareStatsRes> { async setShareStatsRaw(
params: RR.SetShareStatsReq,
): Promise<RR.SetShareStatsRes> {
await pauseFor(2000) await pauseFor(2000)
const patch = [ const patch = [
{ {
@@ -97,14 +105,20 @@ export class MockApiService extends ApiService {
return this.withRevision(patch) return this.withRevision(patch)
} }
async getServerLogs (params: RR.GetServerLogsReq): Promise<RR.GetServerLogsRes> { async getServerLogs(
params: RR.GetServerLogsReq,
): Promise<RR.GetServerLogsRes> {
await pauseFor(2000) await pauseFor(2000)
let entries: Log[] let entries: Log[]
if (Math.random() < .2) { if (Math.random() < 0.2) {
entries = Mock.ServerLogs entries = Mock.ServerLogs
} else { } else {
const arrLength = params.limit ? Math.ceil(params.limit / Mock.ServerLogs.length) : 10 const arrLength = params.limit
entries = new Array(arrLength).fill(Mock.ServerLogs).reduce((acc, val) => acc.concat(val), []) ? Math.ceil(params.limit / Mock.ServerLogs.length)
: 10
entries = new Array(arrLength)
.fill(Mock.ServerLogs)
.reduce((acc, val) => acc.concat(val), [])
} }
return { return {
entries, 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) await pauseFor(2000)
return Mock.getServerMetrics() return Mock.getServerMetrics()
} }
async getPkgMetrics (params: RR.GetServerMetricsReq): Promise<RR.GetPackageMetricsRes> { async getPkgMetrics(
params: RR.GetServerMetricsReq,
): Promise<RR.GetPackageMetricsRes> {
await pauseFor(2000) await pauseFor(2000)
return Mock.getAppMetrics() return Mock.getAppMetrics()
} }
async updateServerRaw (params: RR.UpdateServerReq): Promise<RR.UpdateServerRes> { async updateServerRaw(
params: RR.UpdateServerReq,
): Promise<RR.UpdateServerRes> {
await pauseFor(2000) await pauseFor(2000)
const initialProgress = { const initialProgress = {
size: 10000, size: 10000,
@@ -145,65 +165,77 @@ export class MockApiService extends ApiService {
return this.withRevision(patch, 'updating') return this.withRevision(patch, 'updating')
} }
async restartServer (params: RR.RestartServerReq): Promise<RR.RestartServerRes> { async restartServer(
params: RR.RestartServerReq,
): Promise<RR.RestartServerRes> {
await pauseFor(2000) await pauseFor(2000)
return null return null
} }
async shutdownServer (params: RR.ShutdownServerReq): Promise<RR.ShutdownServerRes> { async shutdownServer(
params: RR.ShutdownServerReq,
): Promise<RR.ShutdownServerRes> {
await pauseFor(2000) await pauseFor(2000)
return null return null
} }
async systemRebuild (params: RR.RestartServerReq): Promise<RR.RestartServerRes> { async systemRebuild(
params: RR.RestartServerReq,
): Promise<RR.RestartServerRes> {
await pauseFor(2000) await pauseFor(2000)
return null return null
} }
// marketplace URLs // marketplace URLs
async getEos (params: RR.GetMarketplaceEOSReq): Promise<RR.GetMarketplaceEOSRes> { async getEos(
params: RR.GetMarketplaceEOSReq,
): Promise<RR.GetMarketplaceEOSRes> {
await pauseFor(2000) await pauseFor(2000)
return Mock.MarketplaceEos return Mock.MarketplaceEos
} }
async getMarketplaceData (params: RR.GetMarketplaceDataReq): Promise<RR.GetMarketplaceDataRes> { async getMarketplaceData(
params: RR.GetMarketplaceDataReq,
): Promise<RR.GetMarketplaceDataRes> {
await pauseFor(2000) await pauseFor(2000)
return { 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) await pauseFor(2000)
return Mock.MarketplacePkgsList return Mock.MarketplacePkgsList
} }
async getReleaseNotes (params: RR.GetReleaseNotesReq): Promise<RR.GetReleaseNotesRes> { async getReleaseNotes(
params: RR.GetReleaseNotesReq,
): Promise<RR.GetReleaseNotesRes> {
await pauseFor(2000) await pauseFor(2000)
return Mock.ReleaseNotes return Mock.ReleaseNotes
} }
async getLatestVersion (params: RR.GetLatestVersionReq): Promise<RR.GetLatestVersionRes> { async getLatestVersion(
params: RR.GetLatestVersionReq,
): Promise<RR.GetLatestVersionRes> {
await pauseFor(2000) await pauseFor(2000)
return params.ids.reduce((obj, id) => { return params.ids.reduce((obj, id) => {
obj[id] = '1.3.0' obj[id] = '1.3.0'
return obj 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 // password
// async updatePassword (params: RR.UpdatePasswordReq): Promise<RR.UpdatePasswordRes> { // async updatePassword (params: RR.UpdatePasswordReq): Promise<RR.UpdatePasswordRes> {
// await pauseFor(2000) // await pauseFor(2000)
@@ -212,7 +244,9 @@ export class MockApiService extends ApiService {
// notification // notification
async getNotificationsRaw (params: RR.GetNotificationsReq): Promise<RR.GetNotificationsRes> { async getNotificationsRaw(
params: RR.GetNotificationsReq,
): Promise<RR.GetNotificationsRes> {
await pauseFor(2000) await pauseFor(2000)
const patch = [ const patch = [
{ {
@@ -225,72 +259,82 @@ export class MockApiService extends ApiService {
return this.withRevision(patch, Mock.Notifications) 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) await pauseFor(2000)
return null return null
} }
async deleteAllNotifications (params: RR.DeleteAllNotificationsReq): Promise<RR.DeleteAllNotificationsRes> { async deleteAllNotifications(
params: RR.DeleteAllNotificationsReq,
): Promise<RR.DeleteAllNotificationsRes> {
await pauseFor(2000) await pauseFor(2000)
return null return null
} }
// wifi // wifi
async getWifi (params: RR.GetWifiReq): Promise < RR.GetWifiRes > { async getWifi(params: RR.GetWifiReq): Promise<RR.GetWifiRes> {
await pauseFor(2000) await pauseFor(2000)
return Mock.Wifi return Mock.Wifi
} }
async setWifiCountry (params: RR.SetWifiCountryReq): Promise <RR.SetWifiCountryRes> { async setWifiCountry(
params: RR.SetWifiCountryReq,
): Promise<RR.SetWifiCountryRes> {
await pauseFor(2000) await pauseFor(2000)
return null return null
} }
async addWifi (params: RR.AddWifiReq): Promise<RR.AddWifiRes> { async addWifi(params: RR.AddWifiReq): Promise<RR.AddWifiRes> {
await pauseFor(2000) await pauseFor(2000)
return null return null
} }
async connectWifi (params: RR.ConnectWifiReq): Promise<RR.ConnectWifiRes> { async connectWifi(params: RR.ConnectWifiReq): Promise<RR.ConnectWifiRes> {
await pauseFor(2000) await pauseFor(2000)
return null return null
} }
async deleteWifi (params: RR.DeleteWifiReq): Promise<RR.DeleteWifiRes> { async deleteWifi(params: RR.DeleteWifiReq): Promise<RR.DeleteWifiRes> {
await pauseFor(2000) await pauseFor(2000)
return null return null
} }
// ssh // ssh
async getSshKeys (params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes> { async getSshKeys(params: RR.GetSSHKeysReq): Promise<RR.GetSSHKeysRes> {
await pauseFor(2000) await pauseFor(2000)
return Mock.SshKeys return Mock.SshKeys
} }
async addSshKey (params: RR.AddSSHKeyReq): Promise<RR.AddSSHKeyRes> { async addSshKey(params: RR.AddSSHKeyReq): Promise<RR.AddSSHKeyRes> {
await pauseFor(2000) await pauseFor(2000)
return Mock.SshKey return Mock.SshKey
} }
async deleteSshKey (params: RR.DeleteSSHKeyReq): Promise<RR.DeleteSSHKeyRes> { async deleteSshKey(params: RR.DeleteSSHKeyReq): Promise<RR.DeleteSSHKeyRes> {
await pauseFor(2000) await pauseFor(2000)
return null return null
} }
// backup // backup
async getBackupTargets (params: RR.GetBackupTargetsReq): Promise<RR.GetBackupTargetsRes> { async getBackupTargets(
params: RR.GetBackupTargetsReq,
): Promise<RR.GetBackupTargetsRes> {
await pauseFor(2000) await pauseFor(2000)
return Mock.BackupTargets return Mock.BackupTargets
} }
async addBackupTarget (params: RR.AddBackupTargetReq): Promise<RR.AddBackupTargetRes> { async addBackupTarget(
params: RR.AddBackupTargetReq,
): Promise<RR.AddBackupTargetRes> {
await pauseFor(2000) await pauseFor(2000)
const { hostname, path, username } = params const { hostname, path, username } = params
return { return {
'latfgvwdbhjsndmk': { latfgvwdbhjsndmk: {
type: 'cifs', type: 'cifs',
hostname, hostname,
path: path.replace(/\\/g, '/'), 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) await pauseFor(2000)
const { id, hostname, path, username } = params const { id, hostname, path, username } = params
return { return {
[id]: { [id]: {
...Mock.BackupTargets[id] as CifsBackupTarget, ...(Mock.BackupTargets[id] as CifsBackupTarget),
hostname, hostname,
path, path,
username, 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) await pauseFor(2000)
return null return null
} }
async getBackupInfo (params: RR.GetBackupInfoReq): Promise<RR.GetBackupInfoRes> { async getBackupInfo(
params: RR.GetBackupInfoReq,
): Promise<RR.GetBackupInfoRes> {
await pauseFor(2000) await pauseFor(2000)
return Mock.BackupInfo return Mock.BackupInfo
} }
async createBackupRaw (params: RR.CreateBackupReq): Promise<RR.CreateBackupRes> { async createBackupRaw(
params: RR.CreateBackupReq,
): Promise<RR.CreateBackupRes> {
await pauseFor(2000) await pauseFor(2000)
const path = '/server-info/status' const path = '/server-info/status'
const ids = ['bitcoind', 'lnd'] const ids = ['bitcoind', 'lnd']
@@ -373,19 +425,27 @@ export class MockApiService extends ApiService {
// package // package
async getPackageProperties (params: RR.GetPackagePropertiesReq): Promise<RR.GetPackagePropertiesRes<2>['data']> { async getPackageProperties(
params: RR.GetPackagePropertiesReq,
): Promise<RR.GetPackagePropertiesRes<2>['data']> {
await pauseFor(2000) await pauseFor(2000)
return parsePropertiesPermissive(Mock.PackageProperties) return parsePropertiesPermissive(Mock.PackageProperties)
} }
async getPackageLogs (params: RR.GetPackageLogsReq): Promise<RR.GetPackageLogsRes> { async getPackageLogs(
params: RR.GetPackageLogsReq,
): Promise<RR.GetPackageLogsRes> {
await pauseFor(2000) await pauseFor(2000)
let entries let entries
if (Math.random() < .2) { if (Math.random() < 0.2) {
entries = Mock.PackageLogs entries = Mock.PackageLogs
} else { } else {
const arrLength = params.limit ? Math.ceil(params.limit / Mock.PackageLogs.length) : 10 const arrLength = params.limit
entries = new Array(arrLength).fill(Mock.PackageLogs).reduce((acc, val) => acc.concat(val), []) ? Math.ceil(params.limit / Mock.PackageLogs.length)
: 10
entries = new Array(arrLength)
.fill(Mock.PackageLogs)
.reduce((acc, val) => acc.concat(val), [])
} }
return { return {
entries, 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) await pauseFor(2000)
const initialProgress: InstallProgress = { const initialProgress: InstallProgress = {
size: 120, size: 120,
@@ -427,12 +489,16 @@ export class MockApiService extends ApiService {
return this.withRevision(patch) return this.withRevision(patch)
} }
async dryUpdatePackage (params: RR.DryUpdatePackageReq): Promise<RR.DryUpdatePackageRes> { async dryUpdatePackage(
params: RR.DryUpdatePackageReq,
): Promise<RR.DryUpdatePackageRes> {
await pauseFor(2000) await pauseFor(2000)
return { } return {}
} }
async getPackageConfig (params: RR.GetPackageConfigReq): Promise<RR.GetPackageConfigRes> { async getPackageConfig(
params: RR.GetPackageConfigReq,
): Promise<RR.GetPackageConfigRes> {
await pauseFor(2000) await pauseFor(2000)
return { return {
config: Mock.MockConfig, 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) await pauseFor(2000)
return { } return {}
} }
async setPackageConfigRaw (params: RR.SetPackageConfigReq): Promise<RR.SetPackageConfigRes> { async setPackageConfigRaw(
params: RR.SetPackageConfigReq,
): Promise<RR.SetPackageConfigRes> {
await pauseFor(2000) await pauseFor(2000)
const patch = [ const patch = [
{ {
@@ -457,10 +527,11 @@ export class MockApiService extends ApiService {
return this.withRevision(patch) return this.withRevision(patch)
} }
async restorePackagesRaw (params: RR.RestorePackagesReq): Promise<RR.RestorePackagesRes> { async restorePackagesRaw(
params: RR.RestorePackagesReq,
): Promise<RR.RestorePackagesRes> {
await pauseFor(2000) await pauseFor(2000)
const patch: Operation[] = params.ids.map(id => { const patch: Operation[] = params.ids.map(id => {
const initialProgress: InstallProgress = { const initialProgress: InstallProgress = {
size: 120, size: 120,
downloaded: 120, downloaded: 120,
@@ -492,12 +563,16 @@ export class MockApiService extends ApiService {
return this.withRevision(patch) return this.withRevision(patch)
} }
async executePackageAction (params: RR.ExecutePackageActionReq): Promise<RR.ExecutePackageActionRes> { async executePackageAction(
params: RR.ExecutePackageActionReq,
): Promise<RR.ExecutePackageActionRes> {
await pauseFor(2000) await pauseFor(2000)
return Mock.ActionResponse 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` const path = `/package-data/${params.id}/installed/status/main`
await pauseFor(2000) await pauseFor(2000)
@@ -551,11 +626,11 @@ export class MockApiService extends ApiService {
message: 'Bitcoin is syncing from genesis', message: 'Bitcoin is syncing from genesis',
}, },
'p2p-interface': { 'p2p-interface': {
result: 'success', result: 'success',
}, },
'rpc-interface': { 'rpc-interface': {
result: 'failure', result: 'failure',
error: 'RPC interface unreachable.', error: 'RPC interface unreachable.',
}, },
}, },
}, },
@@ -574,10 +649,12 @@ export class MockApiService extends ApiService {
return this.withRevision(originalPatch) return this.withRevision(originalPatch)
} }
async dryStopPackage (params: RR.DryStopPackageReq): Promise<RR.DryStopPackageRes> { async dryStopPackage(
params: RR.DryStopPackageReq,
): Promise<RR.DryStopPackageRes> {
await pauseFor(2000) await pauseFor(2000)
return { return {
'lnd': { lnd: {
dependency: 'bitcoind', dependency: 'bitcoind',
error: { error: {
type: DependencyErrorType.NotRunning, 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) await pauseFor(2000)
const path = `/package-data/${params.id}/installed/status/main` const path = `/package-data/${params.id}/installed/status/main`
@@ -610,19 +687,23 @@ export class MockApiService extends ApiService {
{ {
op: PatchOp.REPLACE, op: PatchOp.REPLACE,
path: path + '/health', path: path + '/health',
value: { }, value: {},
}, },
] ]
return this.withRevision(patch) return this.withRevision(patch)
} }
async dryUninstallPackage (params: RR.DryUninstallPackageReq): Promise<RR.DryUninstallPackageRes> { async dryUninstallPackage(
params: RR.DryUninstallPackageReq,
): Promise<RR.DryUninstallPackageRes> {
await pauseFor(2000) await pauseFor(2000)
return { } return {}
} }
async uninstallPackageRaw (params: RR.UninstallPackageReq): Promise<RR.UninstallPackageRes> { async uninstallPackageRaw(
params: RR.UninstallPackageReq,
): Promise<RR.UninstallPackageRes> {
await pauseFor(2000) await pauseFor(2000)
setTimeout(async () => { setTimeout(async () => {
@@ -646,7 +727,9 @@ export class MockApiService extends ApiService {
return this.withRevision(patch) return this.withRevision(patch)
} }
async deleteRecoveredPackageRaw (params: RR.DeleteRecoveredPackageReq): Promise<RR.DeleteRecoveredPackageRes> { async deleteRecoveredPackageRaw(
params: RR.DeleteRecoveredPackageReq,
): Promise<RR.DeleteRecoveredPackageRes> {
await pauseFor(2000) await pauseFor(2000)
const patch = [ const patch = [
{ {
@@ -657,7 +740,9 @@ export class MockApiService extends ApiService {
return this.withRevision(patch) return this.withRevision(patch)
} }
async dryConfigureDependency (params: RR.DryConfigureDependencyReq): Promise<RR.DryConfigureDependencyRes> { async dryConfigureDependency(
params: RR.DryConfigureDependencyReq,
): Promise<RR.DryConfigureDependencyRes> {
await pauseFor(2000) await pauseFor(2000)
return { return {
'old-config': Mock.MockConfig, '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 = [ const phases = [
{ progress: 'downloaded', completion: 'download-complete'}, { progress: 'downloaded', completion: 'download-complete' },
{ progress: 'validated', completion: 'validation-complete'}, { progress: 'validated', completion: 'validation-complete' },
{ progress: 'unpacked', completion: 'unpack-complete'}, { progress: 'unpacked', completion: 'unpack-complete' },
] ]
for (let phase of phases) { for (let phase of phases) {
let i = initialProgress[phase.progress] let i = initialProgress[phase.progress]
@@ -709,7 +797,7 @@ export class MockApiService extends ApiService {
}, 1000) }, 1000)
} }
private async updateOSProgress (size: number) { private async updateOSProgress(size: number) {
let downloaded = 0 let downloaded = 0
while (downloaded < size) { while (downloaded < size) {
await pauseFor(250) await pauseFor(250)
@@ -759,7 +847,7 @@ export class MockApiService extends ApiService {
}, 1000) }, 1000)
} }
private async updateMock (patch: Operation[]): Promise<void> { private async updateMock(patch: Operation[]): Promise<void> {
if (!this.sequence) { if (!this.sequence) {
const { sequence } = await this.bootstrapper.init() const { sequence } = await this.bootstrapper.init()
this.sequence = sequence this.sequence = sequence
@@ -772,7 +860,10 @@ export class MockApiService extends ApiService {
this.mockPatch$.next(revision) 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) { if (!this.sequence) {
const { sequence } = await this.bootstrapper.init() const { sequence } = await this.bootstrapper.init()
this.sequence = sequence 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', BackingUp = 'backing-up',
} }
export interface RecoveredPackageDataEntry { export interface RecoveredPackageDataEntry {
title: string, title: string
icon: URL, icon: URL
version: string, version: string
} }
export interface PackageDataEntry { export interface PackageDataEntry {
@@ -53,8 +53,8 @@ export interface PackageDataEntry {
icon: URL icon: URL
} }
manifest: Manifest manifest: Manifest
installed?: InstalledPackageDataEntry, // exists when: installed, updating installed?: InstalledPackageDataEntry // exists when: installed, updating
'install-progress'?: InstallProgress, // exists when: installing, updating 'install-progress'?: InstallProgress // exists when: installing, updating
} }
export interface InstallProgress { export interface InstallProgress {
@@ -69,7 +69,7 @@ export interface InstallProgress {
export interface InstalledPackageDataEntry { export interface InstalledPackageDataEntry {
status: Status status: Status
manifest: Manifest, manifest: Manifest
'last-backup': string | null 'last-backup': string | null
'system-pointers': any[] 'system-pointers': any[]
'current-dependents': { [id: string]: CurrentDependencyInfo } 'current-dependents': { [id: string]: CurrentDependencyInfo }
@@ -81,7 +81,7 @@ export interface InstalledPackageDataEntry {
} }
} }
'interface-addresses': { '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 stop: string | null
} }
main: ActionImpl main: ActionImpl
'health-checks': Record<string, ActionImpl & { name: string, description: string }> 'health-checks': Record<
string,
ActionImpl & { name: string; description: string }
>
config: ConfigActions | null config: ConfigActions | null
volumes: Record<string, Volume> volumes: Record<string, Volume>
'min-os-version': string 'min-os-version': string
@@ -209,7 +212,7 @@ export interface TorConfig {
} }
export type LanConfig = { export type LanConfig = {
[port: number]: { ssl: boolean, mapping: number } [port: number]: { ssl: boolean; mapping: number }
} }
export interface BackupActions { export interface BackupActions {
@@ -237,7 +240,12 @@ export interface Status {
'dependency-errors': { [id: string]: DependencyError | null } '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 { export interface MainStatusStopped {
status: PackageMainStatus.Stopped status: PackageMainStatus.Stopped
@@ -270,11 +278,12 @@ export enum PackageMainStatus {
BackingUp = 'backing-up', BackingUp = 'backing-up',
} }
export type HealthCheckResult = HealthCheckResultStarting | export type HealthCheckResult =
HealthCheckResultLoading | | HealthCheckResultStarting
HealthCheckResultDisabled | | HealthCheckResultLoading
HealthCheckResultSuccess | | HealthCheckResultDisabled
HealthCheckResultFailure | HealthCheckResultSuccess
| HealthCheckResultFailure
export enum HealthResult { export enum HealthResult {
Starting = 'starting', Starting = 'starting',
@@ -306,12 +315,13 @@ export interface HealthCheckResultFailure {
error: string error: string
} }
export type DependencyError = DependencyErrorNotInstalled | export type DependencyError =
DependencyErrorNotRunning | | DependencyErrorNotInstalled
DependencyErrorIncorrectVersion | | DependencyErrorNotRunning
DependencyErrorConfigUnsatisfied | | DependencyErrorIncorrectVersion
DependencyErrorHealthChecksFailed | | DependencyErrorConfigUnsatisfied
DependencyErrorTransitive | DependencyErrorHealthChecksFailed
| DependencyErrorTransitive
export enum DependencyErrorType { export enum DependencyErrorType {
NotInstalled = 'not-installed', NotInstalled = 'not-installed',
@@ -357,18 +367,21 @@ export interface DependencyInfo {
export interface DependencyEntry { export interface DependencyEntry {
version: string version: string
requirement: { requirement:
type: 'opt-in' | {
how: string type: 'opt-in'
} | { how: string
type: 'opt-out' }
how: string | {
} | { type: 'opt-out'
type: 'required' how: string
} }
| {
type: 'required'
}
description: string | null description: string | null
config: { config: {
check: ActionImpl, check: ActionImpl
'auto-configure': ActionImpl 'auto-configure': ActionImpl
} }
} }

View File

@@ -9,15 +9,14 @@ import { ErrorToastService } from './error-toast.service'
providedIn: 'root', providedIn: 'root',
}) })
export class ServerConfigService { export class ServerConfigService {
constructor(
constructor (
private readonly loadingCtrl: LoadingController, private readonly loadingCtrl: LoadingController,
private readonly errToast: ErrorToastService, private readonly errToast: ErrorToastService,
private readonly alertCtrl: AlertController, private readonly alertCtrl: AlertController,
private readonly embassyApi: ApiService, private readonly embassyApi: ApiService,
) { } ) {}
async presentAlert (key: string, current?: any): Promise<HTMLIonAlertElement> { async presentAlert(key: string, current?: any): Promise<HTMLIonAlertElement> {
const spec = serverConfig[key] const spec = serverConfig[key]
let inputs: AlertInput[] let inputs: AlertInput[]
@@ -100,14 +99,11 @@ export class ServerConfigService {
saveFns: { [key: string]: (val: any) => Promise<any> } = { saveFns: { [key: string]: (val: any) => Promise<any> } = {
'auto-check-updates': async (enabled: boolean) => { '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) => { 'share-stats': async (enabled: boolean) => {
return this.embassyApi.setShareStats({ value: enabled }) return this.embassyApi.setShareStats({ value: enabled })
}, },
@@ -121,31 +117,16 @@ export const serverConfig: ConfigSpec = {
'auto-check-updates': { 'auto-check-updates': {
type: 'boolean', type: 'boolean',
name: 'Auto Check for Updates', 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, 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': { 'share-stats': {
type: 'boolean', type: 'boolean',
name: 'Report Bugs', 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, default: false,
}, },
// password: { // password: {