mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-01 21:13:09 +00:00
rebased and compiling again
This commit is contained in:
@@ -50,9 +50,8 @@ Valid values for "maskAs" are `tor` and `lan`.
|
||||
|
||||
```sh
|
||||
npm run start:ui
|
||||
npm run start:install-wiz
|
||||
npm run start:install
|
||||
npm run start:setup
|
||||
npm run start:dui
|
||||
```
|
||||
|
||||
## Running locally with proxied backend
|
||||
|
||||
@@ -36,6 +36,11 @@
|
||||
"input": "node_modules/monaco-editor",
|
||||
"output": "assets/monaco-editor/"
|
||||
},
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "node_modules/@taiga-ui/icons/src",
|
||||
"output": "assets/taiga-ui/icons"
|
||||
},
|
||||
"projects/ui/src/manifest.webmanifest",
|
||||
{
|
||||
"glob": "ngsw.json",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
{
|
||||
"useMocks": true,
|
||||
"enableWidgets": false,
|
||||
"packageArch": "aarch64",
|
||||
"osArch": "raspberrypi",
|
||||
"ui": {
|
||||
"api": {
|
||||
"url": "rpc",
|
||||
@@ -14,6 +12,7 @@
|
||||
},
|
||||
"mocks": {
|
||||
"maskAs": "tor",
|
||||
"maskAsHttps": true,
|
||||
"skipStartupAlerts": true
|
||||
}
|
||||
},
|
||||
|
||||
@@ -4,6 +4,6 @@ module.exports = {
|
||||
'projects/ui/**/*.ts': () => 'npm run check:ui',
|
||||
'projects/shared/**/*.ts': () => 'npm run check:shared',
|
||||
'projects/marketplace/**/*.ts': () => 'npm run check:marketplace',
|
||||
'projects/install-wizard/**/*.ts': () => 'npm run check:install-wiz',
|
||||
'projects/install-wizard/**/*.ts': () => 'npm run check:install',
|
||||
'projects/setup-wizard/**/*.ts': () => 'npm run check:setup',
|
||||
}
|
||||
|
||||
2070
frontend/package-lock.json
generated
2070
frontend/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,29 @@
|
||||
{
|
||||
"name": "startos-ui",
|
||||
"version": "0.3.4.4",
|
||||
"version": "0.3.5",
|
||||
"author": "Start9 Labs, Inc",
|
||||
"homepage": "https://start9.com/",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"check": "npm run check:shared && npm run check:marketplace && npm run check:ui && npm run check:install-wiz && npm run check:setup && npm run check:dui",
|
||||
"check": "npm run check:shared && npm run check:marketplace && npm run check:ui && npm run check:install && npm run check:setup",
|
||||
"check:shared": "tsc --project projects/shared/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:marketplace": "tsc --project projects/marketplace/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:install-wiz": "tsc --project projects/install-wizard/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:install": "tsc --project projects/install-wizard/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:setup": "tsc --project projects/setup-wizard/tsconfig.json --noEmit --skipLibCheck",
|
||||
"check:ui": "tsc --project projects/ui/tsconfig.json --noEmit --skipLibCheck",
|
||||
"build:deps": "rm -rf .angular/cache && cd ../patch-db/client && npm ci && npm run build",
|
||||
"build:install-wiz": "ng run install-wizard:build",
|
||||
"build:install": "ng run install-wizard:build",
|
||||
"build:setup": "ng run setup-wizard:build",
|
||||
"build:ui": "ng run ui:build",
|
||||
"build:ui:dev": "ng run ui:build:development",
|
||||
"build:ui:stats": "ng run ui:build --stats-json",
|
||||
"build:all": "npm run build:deps && npm run build:dui && npm run build:setup && npm run build:ui && npm run build:install-wiz",
|
||||
"build:all": "npm run build:deps && npm run build:setup && npm run build:ui && npm run build:install",
|
||||
"build:shared": "ng build shared",
|
||||
"build:marketplace": "npm run build:shared && ng build marketplace",
|
||||
"analyze:ui": "webpack-bundle-analyzer dist/raw/ui/stats.json",
|
||||
"publish:shared": "npm run build:shared && npm publish ./dist/shared --access public",
|
||||
"publish:marketplace": "npm run build:marketplace && npm publish ./dist/marketplace --access public",
|
||||
"start:install-wiz": "npm run-script build-config && ionic serve --project install-wizard --host 0.0.0.0",
|
||||
"start:install": "npm run-script build-config && ionic serve --project install-wizard --host 0.0.0.0",
|
||||
"start:setup": "npm run-script build-config && ionic serve --project setup-wizard --host 0.0.0.0",
|
||||
"start:ui": "npm run-script build-config && ionic serve --project ui --ip --host 0.0.0.0",
|
||||
"start:ui:proxy": "npm run-script build-config && ionic serve --project ui --ip --host 0.0.0.0 -- --proxy-config proxy.conf.json",
|
||||
@@ -44,13 +44,14 @@
|
||||
"@materia-ui/ngx-monaco-editor": "^6.0.0",
|
||||
"@start9labs/argon2": "^0.1.0",
|
||||
"@start9labs/emver": "^0.1.5",
|
||||
"@taiga-ui/addon-charts": "3.52.0",
|
||||
"@taiga-ui/cdk": "3.52.0",
|
||||
"@taiga-ui/core": "3.52.0",
|
||||
"@taiga-ui/experimental": "3.52.0",
|
||||
"@taiga-ui/icons": "3.52.0",
|
||||
"@taiga-ui/kit": "3.52.0",
|
||||
"@taiga-ui/styles": "3.52.0",
|
||||
"@start9labs/start-sdk": "0.4.0-rev0.lib0.rc8.beta2",
|
||||
"@taiga-ui/addon-charts": "3.53.0",
|
||||
"@taiga-ui/cdk": "3.53.0",
|
||||
"@taiga-ui/core": "3.53.0",
|
||||
"@taiga-ui/experimental": "3.53.0",
|
||||
"@taiga-ui/icons": "3.53.0",
|
||||
"@taiga-ui/kit": "3.53.0",
|
||||
"@taiga-ui/styles": "3.53.0",
|
||||
"@tinkoff/ng-dompurify": "4.0.0",
|
||||
"ansi-to-html": "^0.7.2",
|
||||
"base64-js": "^1.5.1",
|
||||
@@ -72,7 +73,6 @@
|
||||
"patch-db-client": "file: ../../../patch-db/client",
|
||||
"pbkdf2": "^3.1.2",
|
||||
"rxjs": "^7.5.6",
|
||||
"@start9labs/start-sdk": "0.4.0-rev0.lib0.rc8.beta2",
|
||||
"swiper": "^8.2.4",
|
||||
"ts-matches": "^5.2.1",
|
||||
"tslib": "^2.3.0",
|
||||
@@ -85,7 +85,6 @@
|
||||
"@angular/compiler-cli": "^16.1.4",
|
||||
"@angular/language-service": "^16.1.4",
|
||||
"@ionic/cli": "^6.19.0",
|
||||
"@types/cron": "^2.0.0",
|
||||
"@types/dompurify": "^2.3.3",
|
||||
"@types/estree": "^0.0.51",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
<ion-grid>
|
||||
<ion-row>
|
||||
<ion-col class="ion-text-center">
|
||||
<div style="padding-bottom: 32px">
|
||||
<img src="assets/img/logo.png" style="max-width: 240px" />
|
||||
<div style="padding: 64px 0 32px 0">
|
||||
<img src="assets/img/icon.png" style="max-width: 100px" />
|
||||
</div>
|
||||
|
||||
<ion-card color="dark">
|
||||
|
||||
@@ -4,13 +4,13 @@
|
||||
<ion-col class="ion-text-center">
|
||||
<div style="padding-bottom: 32px">
|
||||
<img
|
||||
src="assets/img/logo.png"
|
||||
src="assets/img/icon.png"
|
||||
class="pb-1"
|
||||
style="max-width: 220px"
|
||||
style="max-width: 100px"
|
||||
/>
|
||||
</div>
|
||||
<ion-card color="dark">
|
||||
<ion-card-header>
|
||||
<ion-card-header style="padding-bottom: 0">
|
||||
<ion-button
|
||||
*ngIf="swiper?.activeIndex === 1"
|
||||
class="back-button"
|
||||
|
||||
@@ -11,7 +11,3 @@
|
||||
text-align: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
ion-card-title {
|
||||
font-variant-caps: all-small-caps;
|
||||
}
|
||||
@@ -37,7 +37,7 @@
|
||||
<p>
|
||||
Download your server's Root CA and
|
||||
<a
|
||||
href="https://docs.start9.com/latest/user-manual/connecting/connecting-lan"
|
||||
href="https://docs.start9.com/0.3.5.x/user-manual/connecting-lan"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style="color: #6866cc; font-weight: bold; text-decoration: none"
|
||||
@@ -104,7 +104,7 @@
|
||||
<span style="font-weight: bold">Note:</span>
|
||||
This address will only work from a Tor-enabled browser.
|
||||
<a
|
||||
href="https://docs.start9.com/latest/user-manual/connecting/connecting-tor"
|
||||
href="https://docs.start9.com/0.3.5.x/user-manual/connecting-tor"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
style="color: #6866cc; font-weight: bold; text-decoration: none"
|
||||
|
||||
@@ -8,23 +8,21 @@
|
||||
<ion-card>
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col responsiveCol sizeXs="12" class="ion-text-center">
|
||||
<div class="inline" style="margin-bottom: 3rem">
|
||||
<div class="inline mb-12">
|
||||
<ion-icon
|
||||
name="checkmark-circle-outline"
|
||||
color="success"
|
||||
></ion-icon>
|
||||
<h1>Setup Complete!</h1>
|
||||
</div>
|
||||
<div class="card-container">
|
||||
<ion-card id="exit" (click)="exitKiosk()">
|
||||
<div class="container">
|
||||
<div class="inline">
|
||||
<p>Continue to login</p>
|
||||
<ion-icon name="log-in-outline"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
</ion-card>
|
||||
</div>
|
||||
<ion-button
|
||||
shape="round"
|
||||
class="login-button mb-12"
|
||||
(click)="exitKiosk()"
|
||||
>
|
||||
Continue to Login
|
||||
<ion-icon name="log-in-outline" slot="end"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-card>
|
||||
@@ -34,8 +32,8 @@
|
||||
<ion-card *ngIf="lanAddress">
|
||||
<ion-row class="ion-align-items-center">
|
||||
<ion-col responsiveCol sizeXs="12" class="ion-text-center">
|
||||
<div style="margin-bottom: 4rem">
|
||||
<div class="inline">
|
||||
<div class="mb-12">
|
||||
<div class="inline-container setup">
|
||||
<ion-icon
|
||||
name="checkmark-circle-outline"
|
||||
color="success"
|
||||
@@ -52,35 +50,40 @@
|
||||
<div class="card-container">
|
||||
<ion-card id="information" (click)="download()">
|
||||
<ion-card-content>
|
||||
<ion-card-title>
|
||||
Download permanent address info
|
||||
</ion-card-title>
|
||||
<ion-card-title>Download address info</ion-card-title>
|
||||
<p>
|
||||
start.local was for setup purposes only. It will no
|
||||
longer work.
|
||||
</p>
|
||||
</ion-card-content>
|
||||
<ion-footer>
|
||||
<div class="container">
|
||||
<div class="inline">
|
||||
<p>Download</p>
|
||||
<ion-icon name="download-outline"></ion-icon>
|
||||
</div>
|
||||
<div class="inline-container">
|
||||
<p class="action-text">Download</p>
|
||||
<ion-icon slot="end" name="download-outline"></ion-icon>
|
||||
</div>
|
||||
</ion-footer>
|
||||
</ion-card>
|
||||
<ion-card
|
||||
[disabled]="disableLogin"
|
||||
id="launch"
|
||||
[disabled]="disableLogin"
|
||||
href="{{ lanAddress }}"
|
||||
target="_blank"
|
||||
>
|
||||
<div class="container">
|
||||
<div class="inline">
|
||||
<p>Login to StartOS</p>
|
||||
<ion-icon name="open-outline"></ion-icon>
|
||||
<ion-card-content>
|
||||
<ion-card-title>Trust your Root CA</ion-card-title>
|
||||
<p>
|
||||
In the new tab, follow instructions to trust your
|
||||
server's Root CA and log in.
|
||||
</p>
|
||||
</ion-card-content>
|
||||
<ion-footer>
|
||||
<div class="container">
|
||||
<div class="inline-container">
|
||||
<p class="action-text">Open</p>
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ion-footer>
|
||||
</ion-card>
|
||||
</div>
|
||||
</ion-col>
|
||||
|
||||
@@ -18,19 +18,24 @@ ion-content {
|
||||
|
||||
ion-grid {
|
||||
max-width: 760px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.grid-center-wrapper {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
.inline-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
ion-card {
|
||||
padding: 3rem;
|
||||
padding: 2.4rem;
|
||||
|
||||
h1 {
|
||||
color: var(--ion-color-success);
|
||||
@@ -44,14 +49,14 @@ ion-card {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
// download info card
|
||||
ion-card {
|
||||
max-width: 91%;
|
||||
min-width: 91%;
|
||||
min-height: 260px;
|
||||
width: 80%;
|
||||
background: #615F5F;
|
||||
color: var(--ion-text-color);
|
||||
box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 44px;
|
||||
margin: auto;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
@@ -70,14 +75,6 @@ ion-card {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
ion-card-content {
|
||||
padding-bottom: 4rem;
|
||||
|
||||
p {
|
||||
padding: 1rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
ion-footer {
|
||||
position: absolute;
|
||||
bottom: 10px;
|
||||
@@ -100,19 +97,24 @@ ion-card {
|
||||
}
|
||||
}
|
||||
|
||||
.container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#exit {
|
||||
background: var(--color-accent);
|
||||
height: 100%;
|
||||
|
||||
.container p {
|
||||
font-size: 1.4rem !important;
|
||||
font-weight: bold;
|
||||
.login-button {
|
||||
--background: var(--color-accent);
|
||||
--padding-bottom: 2.5rem;
|
||||
--padding-top: 2.5rem;
|
||||
--padding-start: 2.5rem;
|
||||
--padding-end: 2.5rem;
|
||||
--border-radius: 44px;
|
||||
font-size: 1.4rem !important;
|
||||
font-weight: bold;
|
||||
text-transform: none;
|
||||
letter-spacing: normal;
|
||||
transition: all 350ms ease;
|
||||
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
&:hover {
|
||||
transition-property: transform;
|
||||
transform: scale(1.05);
|
||||
transition-delay: 40ms;
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
@@ -120,40 +122,62 @@ ion-card {
|
||||
}
|
||||
}
|
||||
|
||||
#launch {
|
||||
background: var(--alt-blue);
|
||||
height: 100%;
|
||||
|
||||
.container p {
|
||||
font-size: 1.4rem !important;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
ion-icon {
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
.launch-button {
|
||||
--background: var(--alt-blue);
|
||||
}
|
||||
|
||||
#information:after {
|
||||
#information:after, #launch:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 80%;
|
||||
top: 79%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: var(--color-accent);
|
||||
}
|
||||
|
||||
#launch:after {
|
||||
background: var(--alt-blue);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.card-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
.mb-12 {
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.emphasis-warn {
|
||||
font-weight: 600;
|
||||
color: var(--ion-color-warning);
|
||||
.pb-2 {
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.pt-1 {
|
||||
padding-top: 0.25rem;
|
||||
}
|
||||
|
||||
|
||||
.action-text {
|
||||
font-variant-caps: all-small-caps;
|
||||
padding-right: 0.5rem;
|
||||
font-size: 1.5rem !important;
|
||||
letter-spacing: 0.03rem;
|
||||
padding-bottom: 0.1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.setup {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
ion-card {
|
||||
ion-card {
|
||||
width: 100%;
|
||||
padding-bottom: unset;
|
||||
}
|
||||
#information:after {
|
||||
top: 84%;
|
||||
}
|
||||
#launch:after {
|
||||
top: 85%;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,18 +46,7 @@ export class SuccessPage {
|
||||
|
||||
async ngAfterViewInit() {
|
||||
this.ngZone.runOutsideAngular(() => this.initMatrix())
|
||||
try {
|
||||
const ret = await this.api.complete()
|
||||
if (!this.isKiosk) {
|
||||
this.torAddress = ret['tor-address']
|
||||
this.lanAddress = ret['lan-address'].replace(/^https:/, 'http:')
|
||||
this.cert = ret['root-ca']
|
||||
|
||||
await this.api.exit()
|
||||
}
|
||||
} catch (e: any) {
|
||||
await this.errorService.handleError(e)
|
||||
}
|
||||
setTimeout(() => this.complete(), 1000)
|
||||
}
|
||||
|
||||
download() {
|
||||
@@ -84,6 +73,21 @@ export class SuccessPage {
|
||||
this.api.exit()
|
||||
}
|
||||
|
||||
private async complete() {
|
||||
try {
|
||||
const ret = await this.api.complete()
|
||||
if (!this.isKiosk) {
|
||||
this.torAddress = ret['tor-address'].replace(/^https:/, 'http:')
|
||||
this.lanAddress = ret['lan-address'].replace(/^https:/, 'http:')
|
||||
this.cert = ret['root-ca']
|
||||
|
||||
await this.api.exit()
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
private initMatrix() {
|
||||
this.ctx = this.canvas.nativeElement.getContext('2d')!
|
||||
this.canvas.nativeElement.width = window.innerWidth
|
||||
|
||||
@@ -44,14 +44,14 @@ export class MockApiService extends ApiService {
|
||||
await pauseFor(1000)
|
||||
return [
|
||||
{
|
||||
logicalname: 'abcd',
|
||||
vendor: 'Samsung',
|
||||
model: 'T5',
|
||||
logicalname: '/dev/nvme0n1p3',
|
||||
vendor: 'Unknown Vendor',
|
||||
model: 'Samsung SSD - 970 EVO Plus 2TB',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: 'pabcd',
|
||||
label: null,
|
||||
capacity: 73264762332,
|
||||
capacity: 1979120929996,
|
||||
used: null,
|
||||
'embassy-os': {
|
||||
version: '0.2.17',
|
||||
@@ -63,13 +63,13 @@ export class MockApiService extends ApiService {
|
||||
guid: null,
|
||||
},
|
||||
],
|
||||
capacity: 123456789123,
|
||||
capacity: 1979120929996,
|
||||
guid: 'uuid-uuid-uuid-uuid',
|
||||
},
|
||||
{
|
||||
logicalname: 'dcba',
|
||||
vendor: 'Crucial',
|
||||
model: 'MX500',
|
||||
vendor: 'CT1000MX',
|
||||
model: '500SSD1',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: 'pbcba',
|
||||
@@ -86,13 +86,13 @@ export class MockApiService extends ApiService {
|
||||
guid: null,
|
||||
},
|
||||
],
|
||||
capacity: 124456789123,
|
||||
capacity: 1000190509056,
|
||||
guid: null,
|
||||
},
|
||||
{
|
||||
logicalname: 'wxyz',
|
||||
vendor: 'SanDisk',
|
||||
model: 'Specialness',
|
||||
logicalname: '/dev/sda',
|
||||
vendor: 'ASMT',
|
||||
model: '2115',
|
||||
partitions: [
|
||||
{
|
||||
logicalname: 'pbcba',
|
||||
@@ -109,7 +109,7 @@ export class MockApiService extends ApiService {
|
||||
guid: 'guid-guid-guid-guid',
|
||||
},
|
||||
],
|
||||
capacity: 123459789123,
|
||||
capacity: 1000190509056,
|
||||
guid: null,
|
||||
},
|
||||
]
|
||||
|
||||
@@ -218,7 +218,7 @@ ion-toast {
|
||||
* {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
padding: 0.3rem;
|
||||
padding-left: 0px 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 32 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 52 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 40 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 42 KiB |
@@ -1,6 +1,6 @@
|
||||
import { Directive, HostListener, Inject } from '@angular/core'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { debounce } from '@start9labs/shared'
|
||||
import { debounce } from '../../util/misc.util'
|
||||
|
||||
@Directive({
|
||||
selector: '[appEnter]',
|
||||
@@ -21,6 +21,8 @@ export * from './components/ticker/ticker.module'
|
||||
export * from './directives/drag-scroller.directive'
|
||||
export * from './directives/responsive-col.directive'
|
||||
export * from './directives/safe-links.directive'
|
||||
export * from './directives/enter/enter.directive'
|
||||
export * from './directives/enter/enter.module'
|
||||
|
||||
export * from './mocks/get-setup-status'
|
||||
|
||||
|
||||
@@ -38,12 +38,13 @@ export class HttpService {
|
||||
|
||||
async rpcRequest<T>(
|
||||
opts: RPCOptions,
|
||||
fullUrl?: string,
|
||||
): Promise<LocalHttpResponse<RPCResponse<T>>> {
|
||||
const { method, headers, params, timeout } = opts
|
||||
|
||||
return this.httpRequest<RPCResponse<T>>({
|
||||
method: Method.POST,
|
||||
url: this.relativeUrl,
|
||||
url: fullUrl || this.relativeUrl,
|
||||
headers,
|
||||
body: { method, params },
|
||||
timeout,
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
export type WorkspaceConfig = {
|
||||
packageArch: 'aarch64' | 'x86_64'
|
||||
osArch: 'aarch64' | 'x86_64' | 'raspberrypi'
|
||||
gitHash: string
|
||||
useMocks: boolean
|
||||
enableWidgets: boolean
|
||||
@@ -13,6 +11,7 @@ export type WorkspaceConfig = {
|
||||
marketplace: MarketplaceConfig
|
||||
mocks: {
|
||||
maskAs: 'tor' | 'local' | 'localhost' | 'ipv4' | 'ipv6' | 'clearnet'
|
||||
maskAsHttps: boolean
|
||||
skipStartupAlerts: boolean
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,10 +27,7 @@
|
||||
type="overlay"
|
||||
side="end"
|
||||
class="right-menu container"
|
||||
[class.container_offline]="
|
||||
(authService.isVerified$ | async) &&
|
||||
!(connection.connected$ | async)
|
||||
"
|
||||
[class.container_offline]="offline$ | async"
|
||||
[class.right-menu_hidden]="!drawer.open"
|
||||
[style.--side-width.px]="drawer.width"
|
||||
>
|
||||
@@ -48,10 +45,7 @@
|
||||
[responsiveColViewport]="viewport"
|
||||
id="main-content"
|
||||
class="container"
|
||||
[class.container_offline]="
|
||||
(authService.isVerified$ | async) &&
|
||||
!(connection.connected$ | async)
|
||||
"
|
||||
[class.container_offline]="offline$ | async"
|
||||
>
|
||||
<ion-content
|
||||
#viewport="viewport"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Component, inject, OnDestroy } from '@angular/core'
|
||||
import { Router } from '@angular/router'
|
||||
import { combineLatest, map, merge } from 'rxjs'
|
||||
import { combineLatest, map, merge, startWith } from 'rxjs'
|
||||
import { AuthService } from './services/auth.service'
|
||||
import { SplitPaneTracker } from './services/split-pane.service'
|
||||
import { PatchDataService } from './services/patch-data.service'
|
||||
@@ -34,11 +34,26 @@ export class AppComponent implements OnDestroy {
|
||||
readonly sidebarOpen$ = this.splitPane.sidebarOpen$
|
||||
readonly widgetDrawer$ = this.clientStorageService.widgetDrawer$
|
||||
readonly theme$ = inject(THEME)
|
||||
|
||||
readonly navigation$ = combineLatest([
|
||||
this.authService.isVerified$,
|
||||
this.router.events.pipe(map(() => hasNavigation(this.router.url))),
|
||||
]).pipe(map(([isVerified, hasNavigation]) => isVerified && hasNavigation))
|
||||
|
||||
readonly offline$ = combineLatest([
|
||||
this.authService.isVerified$,
|
||||
this.connection.connected$,
|
||||
this.patch
|
||||
.watch$('server-info', 'status-info')
|
||||
.pipe(startWith({ restarting: false, 'shutting-down': false })),
|
||||
]).pipe(
|
||||
map(
|
||||
([verified, connected, status]) =>
|
||||
verified &&
|
||||
(!connected || status.restarting || status['shutting-down']),
|
||||
),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly router: Router,
|
||||
private readonly titleService: Title,
|
||||
|
||||
@@ -11,12 +11,13 @@ import { BrowserAnimationsModule } from '@angular/platform-browser/animations'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { MonacoEditorModule } from '@materia-ui/ngx-monaco-editor'
|
||||
import {
|
||||
MarkdownModule,
|
||||
DarkThemeModule,
|
||||
SharedPipesModule,
|
||||
LightThemeModule,
|
||||
LoadingModule,
|
||||
ResponsiveColViewportDirective,
|
||||
EnterModule,
|
||||
MarkdownModule,
|
||||
} from '@start9labs/shared'
|
||||
|
||||
import { AppComponent } from './app.component'
|
||||
@@ -26,7 +27,6 @@ import { QRComponentModule } from './common/qr/qr.module'
|
||||
import { PreloaderModule } from './app/preloader/preloader.module'
|
||||
import { FooterModule } from './app/footer/footer.module'
|
||||
import { MenuModule } from './app/menu/menu.module'
|
||||
import { EnterModule } from './app/enter/enter.module'
|
||||
import { APP_PROVIDERS } from './app.providers'
|
||||
import { PatchDbModule } from './services/patch-db/patch-db.module'
|
||||
import { ToastContainerModule } from './common/toast-container/toast-container.module'
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { combineLatest, map, Observable, startWith } from 'rxjs'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Component({
|
||||
selector: 'connection-bar',
|
||||
@@ -19,8 +21,11 @@ export class ConnectionBarComponent {
|
||||
}> = combineLatest([
|
||||
this.connectionService.networkConnected$,
|
||||
this.websocket$.pipe(startWith(false)),
|
||||
this.patch
|
||||
.watch$('server-info', 'status-info')
|
||||
.pipe(startWith({ restarting: false, 'shutting-down': false })),
|
||||
]).pipe(
|
||||
map(([network, websocket]) => {
|
||||
map(([network, websocket, status]) => {
|
||||
if (!network)
|
||||
return {
|
||||
message: 'No Internet',
|
||||
@@ -35,6 +40,20 @@ export class ConnectionBarComponent {
|
||||
icon: 'cloud-offline-outline',
|
||||
dots: true,
|
||||
}
|
||||
if (status['shutting-down'])
|
||||
return {
|
||||
message: 'Shutting Down',
|
||||
color: 'dark',
|
||||
icon: 'power',
|
||||
dots: true,
|
||||
}
|
||||
if (status.restarting)
|
||||
return {
|
||||
message: 'Restarting',
|
||||
color: 'dark',
|
||||
icon: 'power',
|
||||
dots: true,
|
||||
}
|
||||
|
||||
return {
|
||||
message: 'Connected',
|
||||
@@ -45,5 +64,8 @@ export class ConnectionBarComponent {
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(private readonly connectionService: ConnectionService) {}
|
||||
constructor(
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
<a class="logo ion-padding" routerLink="/home">
|
||||
<img
|
||||
alt="Start9"
|
||||
src="assets/img/{{
|
||||
(theme$ | async) === 'Dark' ? 'logo' : 'logo_dark'
|
||||
}}.png"
|
||||
/>
|
||||
<a class="logo" routerLink="/home">
|
||||
<img alt="StartOS" src="assets/img/icon.png" />
|
||||
</a>
|
||||
<ion-item-group class="menu">
|
||||
<ion-menu-toggle *ngFor="let page of pages" auto-hide="false">
|
||||
@@ -27,11 +22,17 @@
|
||||
<ion-label class="label montserrat" routerLinkActive="label_selected">
|
||||
{{ page.title }}
|
||||
</ion-label>
|
||||
<ion-icon
|
||||
*ngIf="page.url === '/system' && (warning$ | async)"
|
||||
color="warning"
|
||||
size="small"
|
||||
name="warning"
|
||||
></ion-icon>
|
||||
<ion-icon
|
||||
*ngIf="page.url === '/system' && (showEOSUpdate$ | async)"
|
||||
color="success"
|
||||
size="small"
|
||||
name="rocket-outline"
|
||||
name="rocket"
|
||||
></ion-icon>
|
||||
<ion-badge
|
||||
*ngIf="page.url === '/updates' && (updateCount$ | async) as updateCount"
|
||||
|
||||
@@ -4,8 +4,9 @@
|
||||
|
||||
.logo {
|
||||
display: block;
|
||||
width: 60%;
|
||||
width: 36%;
|
||||
margin: 0 auto;
|
||||
padding: 16px 16px 0 16px;
|
||||
}
|
||||
|
||||
.menu {
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
filter,
|
||||
first,
|
||||
map,
|
||||
merge,
|
||||
Observable,
|
||||
of,
|
||||
pairwise,
|
||||
startWith,
|
||||
switchMap,
|
||||
@@ -22,6 +24,7 @@ import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { SplitPaneTracker } from 'src/app/services/split-pane.service'
|
||||
import { Emver, THEME } from '@start9labs/shared'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
|
||||
@Component({
|
||||
selector: 'app-menu',
|
||||
@@ -110,6 +113,11 @@ export class MenuComponent {
|
||||
|
||||
readonly theme$ = inject(THEME)
|
||||
|
||||
readonly warning$ = merge(
|
||||
of(this.config.isTorHttp()),
|
||||
this.patch.watch$('server-info', 'ntp-synced').pipe(map(synced => !synced)),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly eosService: EOSService,
|
||||
@@ -118,5 +126,6 @@ export class MenuComponent {
|
||||
private readonly splitPane: SplitPaneTracker,
|
||||
private readonly emver: Emver,
|
||||
private readonly connectionService: ConnectionService,
|
||||
private readonly config: ConfigService,
|
||||
) {}
|
||||
}
|
||||
|
||||
@@ -60,10 +60,7 @@
|
||||
<ion-toolbar></ion-toolbar>
|
||||
|
||||
<!-- images -->
|
||||
<img src="assets/img/icons/bitcoin.svg" />
|
||||
<img src="assets/img/icon.png" />
|
||||
<img src="assets/img/logo.png" />
|
||||
<img src="assets/img/icon_transparent.png" />
|
||||
<img src="assets/img/community-store.png" />
|
||||
<img src="assets/img/icons/snek.png" />
|
||||
<img src="assets/img/icons/wifi-1.png" />
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
<div class="center-container">
|
||||
<ng-container *ngIf="!caTrusted; else trusted">
|
||||
<ion-card id="untrusted" class="text-center">
|
||||
<ion-icon name="lock-closed-outline" class="wiz-icon"></ion-icon>
|
||||
<h1>Trust Your Root CA</h1>
|
||||
<p>
|
||||
Download and trust your server's Root Certificate Authority to establish
|
||||
a secure (HTTPS) connection. You will need to repeat this on every
|
||||
device you use to connect to your server.
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
<b>Bookmark this page</b>
|
||||
- Save this page so you can access it later. You can also find the
|
||||
address in the
|
||||
<code>StartOS-info.html</code>
|
||||
file downloaded at the end of initial setup.
|
||||
</li>
|
||||
<li>
|
||||
<b>Download your server's Root CA</b>
|
||||
- Your server uses its Root CA to generate SSL/TLS certificates for
|
||||
itself and installed services. These certificates are then used to
|
||||
encrypt network traffic with your client devices.
|
||||
<br />
|
||||
<ion-button
|
||||
strong
|
||||
size="small"
|
||||
shape="round"
|
||||
color="tertiary"
|
||||
(click)="download()"
|
||||
>
|
||||
Download
|
||||
<ion-icon slot="end" name="download-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</li>
|
||||
<li>
|
||||
<b>Trust your server's Root CA</b>
|
||||
- Follow instructions for your OS. By trusting your server's Root CA,
|
||||
your device can verify the authenticity of encrypted communications
|
||||
with your server.
|
||||
<br />
|
||||
<ion-button
|
||||
strong
|
||||
size="small"
|
||||
shape="round"
|
||||
color="primary"
|
||||
href="https://docs.start9.com/0.3.5.x/user-manual/trust-ca#establishing-trust"
|
||||
target="_blank"
|
||||
noreferrer
|
||||
>
|
||||
View Instructions
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</li>
|
||||
<li>
|
||||
<b>Test</b>
|
||||
- Refresh the page. If refreshing the page does not work, you may need
|
||||
to quit and re-open your browser, then revisit this page.
|
||||
<br />
|
||||
<ion-button
|
||||
strong
|
||||
size="small"
|
||||
shape="round"
|
||||
class="refresh"
|
||||
(click)="refresh()"
|
||||
>
|
||||
Refresh
|
||||
<ion-icon slot="end" name="refresh"></ion-icon>
|
||||
</ion-button>
|
||||
</li>
|
||||
</ol>
|
||||
<ion-button fill="clear" (click)="launchHttps()" [disabled]="caTrusted">
|
||||
Skip
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
<span class="skip_detail">(not recommended)</span>
|
||||
</ion-card>
|
||||
</ng-container>
|
||||
|
||||
<ng-template #trusted>
|
||||
<ion-card id="trusted" class="text-center">
|
||||
<ion-icon
|
||||
name="shield-checkmark-outline"
|
||||
class="wiz-icon"
|
||||
color="success"
|
||||
></ion-icon>
|
||||
<h1>Root CA Trusted!</h1>
|
||||
<p>
|
||||
You have successfully trusted your server's Root CA and may now log in
|
||||
securely.
|
||||
</p>
|
||||
<ion-button strong (click)="launchHttps()" color="tertiary" shape="round">
|
||||
Go to login
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-card>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<a
|
||||
id="install-cert"
|
||||
href="/eos/local.crt"
|
||||
[download]="
|
||||
config.isLocal() ? document.location.hostname + '.crt' : 'startos.crt'
|
||||
"
|
||||
></a>
|
||||
@@ -0,0 +1,83 @@
|
||||
#trusted {
|
||||
max-width: 40%;
|
||||
}
|
||||
|
||||
#untrusted {
|
||||
max-width: 50%;
|
||||
}
|
||||
|
||||
.center-container {
|
||||
padding: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
ion-card {
|
||||
color: var(--ion-color-dark);
|
||||
background: #414141;
|
||||
box-shadow: 0 4px 4px rgba(17, 17, 17, 0.144);
|
||||
border-radius: 35px;
|
||||
padding: 1.5rem;
|
||||
width: 100%;
|
||||
|
||||
h1 {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
padding-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 21px;
|
||||
line-height: 25px;
|
||||
margin-bottom: 30px;
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ol {
|
||||
font-size: 17px;
|
||||
line-height: 25px;
|
||||
text-align: left;
|
||||
|
||||
li {
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
ion-button {
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.refresh {
|
||||
--background: var(--ion-color-success-shade);
|
||||
}
|
||||
|
||||
.wiz-icon {
|
||||
font-size: 64px;
|
||||
}
|
||||
|
||||
.skip_detail {
|
||||
display: block;
|
||||
font-size: 0.8rem;
|
||||
margin-top: -13px;
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
#trusted, #untrusted {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 701px) and (max-width: 1200px) {
|
||||
#trusted, #untrusted {
|
||||
max-width: 75%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { RELATIVE_URL } from '@start9labs/shared'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { WINDOW } from '@ng-web-apis/common'
|
||||
|
||||
@Component({
|
||||
selector: 'ca-wizard',
|
||||
templateUrl: './ca-wizard.component.html',
|
||||
styleUrls: ['./ca-wizard.component.scss'],
|
||||
})
|
||||
export class CAWizardComponent {
|
||||
caTrusted = false
|
||||
|
||||
constructor(
|
||||
private readonly api: ApiService,
|
||||
public readonly config: ConfigService,
|
||||
@Inject(RELATIVE_URL) private readonly relativeUrl: string,
|
||||
@Inject(DOCUMENT) public readonly document: Document,
|
||||
@Inject(WINDOW) private readonly windowRef: Window,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
await this.testHttps().catch(e =>
|
||||
console.warn('Failed Https connection attempt'),
|
||||
)
|
||||
}
|
||||
|
||||
download() {
|
||||
this.document.getElementById('install-cert')?.click()
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.document.location.reload()
|
||||
}
|
||||
|
||||
launchHttps() {
|
||||
const host = this.config.getHost()
|
||||
this.windowRef.open(`https://${host}`, '_self')
|
||||
}
|
||||
|
||||
private async testHttps() {
|
||||
const url = `https://${this.document.location.host}${this.relativeUrl}`
|
||||
await this.api.echo({ message: 'ping' }, url).then(() => {
|
||||
this.caTrusted = true
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,10 @@ import { RouterModule, Routes } from '@angular/router'
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { FormsModule } from '@angular/forms'
|
||||
import { IonicModule } from '@ionic/angular'
|
||||
import { SharedPipesModule } from '@start9labs/shared'
|
||||
import { LoginPage } from './login.page'
|
||||
import { CAWizardComponent } from './ca-wizard/ca-wizard.component'
|
||||
import { SharedPipesModule } from '@start9labs/shared'
|
||||
import { TuiHintModule, TuiTooltipModule } from '@taiga-ui/core'
|
||||
|
||||
const routes: Routes = [
|
||||
{
|
||||
@@ -20,7 +22,9 @@ const routes: Routes = [
|
||||
IonicModule,
|
||||
SharedPipesModule,
|
||||
RouterModule.forChild(routes),
|
||||
TuiTooltipModule,
|
||||
TuiHintModule,
|
||||
],
|
||||
declarations: [LoginPage],
|
||||
declarations: [LoginPage, CAWizardComponent],
|
||||
})
|
||||
export class LoginPageModule {}
|
||||
|
||||
@@ -1,20 +1,54 @@
|
||||
<ion-content class="content">
|
||||
<ion-grid class="grid">
|
||||
<ion-row class="row">
|
||||
<ion-col>
|
||||
<img src="assets/img/logo.png" alt="Start9" class="logo" />
|
||||
<!-- Local HTTP -->
|
||||
<ng-container *ngIf="config.isLanHttp(); else notLanHttp">
|
||||
<ca-wizard></ca-wizard>
|
||||
</ng-container>
|
||||
|
||||
<ion-card class="card">
|
||||
<ion-card-header>
|
||||
<ion-card-title class="title">StartOS Login</ion-card-title>
|
||||
</ion-card-header>
|
||||
<!-- not Local HTTP -->
|
||||
<ng-template #notLanHttp>
|
||||
<div *ngIf="config.isTorHttp()" class="banner">
|
||||
<ion-item color="warning">
|
||||
<ion-icon slot="start" name="warning-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2 style="font-weight: bold">Http detected</h2>
|
||||
<p style="font-weight: 600">
|
||||
Tor is faster over https. Your Root CA must be trusted.
|
||||
<a
|
||||
href="https://docs.start9.com/0.3.5.x/user-manual/trust-ca"
|
||||
target="_blank"
|
||||
noreferrer
|
||||
style="color: black"
|
||||
>
|
||||
View instructions
|
||||
</a>
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" color="light" (click)="launchHttps()">
|
||||
Open Https
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</div>
|
||||
|
||||
<ion-card-content class="ion-margin">
|
||||
<form class="form" (submit)="submit()">
|
||||
<ion-item-group>
|
||||
<ion-item color="dark">
|
||||
<ion-grid class="grid">
|
||||
<ion-row class="row">
|
||||
<ion-col>
|
||||
<ion-card>
|
||||
<img
|
||||
alt="StartOS Icon"
|
||||
class="header-icon"
|
||||
src="assets/img/icon.png"
|
||||
/>
|
||||
<ion-card-header>
|
||||
<ion-card-title class="title">Login to StartOS</ion-card-title>
|
||||
</ion-card-header>
|
||||
<ion-card-content class="ion-margin">
|
||||
<form (submit)="submit()">
|
||||
<ion-item color="dark" fill="solid">
|
||||
<ion-icon
|
||||
slot="start"
|
||||
size="small"
|
||||
color="base"
|
||||
name="key-outline"
|
||||
style="margin-right: 16px"
|
||||
></ion-icon>
|
||||
@@ -25,7 +59,12 @@
|
||||
[(ngModel)]="password"
|
||||
(ionChange)="error = ''"
|
||||
></ion-input>
|
||||
<ion-button fill="clear" color="light" (click)="toggleMask()">
|
||||
<ion-button
|
||||
slot="end"
|
||||
fill="clear"
|
||||
color="dark"
|
||||
(click)="unmasked = !unmasked"
|
||||
>
|
||||
<ion-icon
|
||||
slot="icon-only"
|
||||
size="small"
|
||||
@@ -33,22 +72,22 @@
|
||||
></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
<ion-button
|
||||
class="login-button"
|
||||
type="submit"
|
||||
expand="block"
|
||||
color="tertiary"
|
||||
>
|
||||
Login
|
||||
</ion-button>
|
||||
</form>
|
||||
<p class="error">
|
||||
<ion-text color="danger">{{ error }}</ion-text>
|
||||
</p>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
<p class="error ion-text-center">
|
||||
<ion-text color="danger">{{ error }}</ion-text>
|
||||
</p>
|
||||
<ion-button
|
||||
class="login-button"
|
||||
type="submit"
|
||||
expand="block"
|
||||
color="tertiary"
|
||||
>
|
||||
Login
|
||||
</ion-button>
|
||||
</form>
|
||||
</ion-card-content>
|
||||
</ion-card>
|
||||
</ion-col>
|
||||
</ion-row>
|
||||
</ion-grid>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
.content {
|
||||
--background: #222428;
|
||||
}
|
||||
|
||||
.card {
|
||||
background: #414141;
|
||||
}
|
||||
|
||||
.title {
|
||||
margin: 24px 0 16px;
|
||||
color: #e0e0e0;
|
||||
text-transform: uppercase;
|
||||
--background: #333333;
|
||||
}
|
||||
|
||||
.grid {
|
||||
@@ -18,74 +8,69 @@
|
||||
}
|
||||
|
||||
.row {
|
||||
height: 90%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
max-width: 240px;
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
.banner {
|
||||
position: absolute;
|
||||
padding: 20px;
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
|
||||
.error {
|
||||
display: block;
|
||||
text-align: left;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
ion-button {
|
||||
--border-radius: 0 4px 4px 0;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
--border-style: solid;
|
||||
--border-color: var(--ion-color-light);
|
||||
--border-radius: 4px 0 0 4px;
|
||||
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14),
|
||||
0 1px 5px 0 rgba(0, 0, 0, 0.12);
|
||||
|
||||
ion-button {
|
||||
--border-radius: 4px;
|
||||
ion-item {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
}
|
||||
}
|
||||
|
||||
ion-card {
|
||||
background: var(--ion-color-step-200);
|
||||
box-shadow: 0 4px 4px rgba(0, 0, 0, 0.25);
|
||||
border-radius: 44px;
|
||||
background: #414141;
|
||||
box-shadow: 0 4px 4px rgba(17, 17, 17, 0.144);
|
||||
border-radius: 35px;
|
||||
min-height: 16rem;
|
||||
contain: unset;
|
||||
overflow: unset;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
--background: transparent;
|
||||
--border-radius: 0px;
|
||||
}
|
||||
|
||||
.title {
|
||||
padding-top: 55px;
|
||||
color: #e0e0e0;
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
&-icon {
|
||||
width: 100px;
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
margin-left: -50px;
|
||||
top: -17%;
|
||||
z-index: 100;
|
||||
}
|
||||
}
|
||||
|
||||
.login-button {
|
||||
margin-inline-start: 0;
|
||||
margin-inline-end: 0;
|
||||
height: 49px;
|
||||
font-size: larger;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.form {
|
||||
margin-bottom: 12px;
|
||||
|
||||
* {
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
height: 45px;
|
||||
width: 120px;
|
||||
--border-radius: 50px;
|
||||
margin: 0 auto;
|
||||
margin-top: 27px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.item-interactive {
|
||||
--highlight-background: #5260ff !important;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 500px) {
|
||||
ion-button {
|
||||
--border-radius: 4px;
|
||||
margin-top: 0.7rem;
|
||||
}
|
||||
|
||||
ion-item {
|
||||
--border-radius: 4px;
|
||||
}
|
||||
.error {
|
||||
display: block;
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { LoadingService } from '@start9labs/shared'
|
||||
import { TuiDestroyService } from '@taiga-ui/cdk'
|
||||
import { takeUntil } from 'rxjs'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { WINDOW } from '@ng-web-apis/common'
|
||||
|
||||
@Component({
|
||||
selector: 'login',
|
||||
@@ -19,30 +20,21 @@ export class LoginPage {
|
||||
password = ''
|
||||
unmasked = false
|
||||
error = ''
|
||||
secure = this.config.isSecure()
|
||||
|
||||
constructor(
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
private readonly destroy$: TuiDestroyService,
|
||||
private readonly router: Router,
|
||||
private readonly authService: AuthService,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly api: ApiService,
|
||||
private readonly config: ConfigService,
|
||||
public readonly config: ConfigService,
|
||||
@Inject(DOCUMENT) public readonly document: Document,
|
||||
@Inject(WINDOW) private readonly windowRef: Window,
|
||||
) {}
|
||||
|
||||
async ionViewDidEnter() {
|
||||
if (!this.secure) {
|
||||
try {
|
||||
await this.api.getPubKey()
|
||||
} catch (e: any) {
|
||||
this.error = e.message
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
toggleMask() {
|
||||
this.unmasked = !this.unmasked
|
||||
launchHttps() {
|
||||
const host = this.config.getHost()
|
||||
this.windowRef.open(`https://${host}`, '_self')
|
||||
}
|
||||
|
||||
async submit() {
|
||||
@@ -60,9 +52,7 @@ export class LoginPage {
|
||||
return
|
||||
}
|
||||
await this.api.login({
|
||||
password: this.secure
|
||||
? this.password
|
||||
: await this.api.encrypt(this.password),
|
||||
password: this.password,
|
||||
metadata: { platforms: getPlatforms() },
|
||||
})
|
||||
|
||||
|
||||
@@ -3,26 +3,18 @@ import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { ErrorService, LoadingService } from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { TuiButtonModule } from '@taiga-ui/experimental'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter } from 'rxjs'
|
||||
import {
|
||||
PackageStatus,
|
||||
PrimaryStatus,
|
||||
renderPkgStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
import {
|
||||
DataModel,
|
||||
InterfaceInfo,
|
||||
PackageDataEntry,
|
||||
PackageMainStatus,
|
||||
PackagePlus,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
import { ServiceConfigModal } from '../modals/config.component'
|
||||
import { PackageConfigData } from '../types/package-config-data'
|
||||
import { ToDependenciesPipe } from '../pipes/to-dependencies.pipe'
|
||||
|
||||
@Component({
|
||||
selector: 'service-actions',
|
||||
@@ -69,12 +61,11 @@ import { ToDependenciesPipe } from '../pipes/to-dependencies.pipe'
|
||||
styles: [':host { display: flex; gap: 1rem }'],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
providers: [ToDependenciesPipe],
|
||||
imports: [CommonModule, TuiButtonModule],
|
||||
})
|
||||
export class ServiceActionsComponent {
|
||||
@Input({ required: true })
|
||||
service!: PackageDataEntry
|
||||
service!: PackagePlus
|
||||
|
||||
constructor(
|
||||
private readonly dialogs: TuiDialogService,
|
||||
@@ -82,51 +73,51 @@ export class ServiceActionsComponent {
|
||||
private readonly loader: LoadingService,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly formDialog: FormDialogService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly dependencies: ToDependenciesPipe,
|
||||
) {}
|
||||
|
||||
private get id(): string {
|
||||
return this.service.manifest.id
|
||||
return this.service.pkg.manifest.id
|
||||
}
|
||||
|
||||
get interfaceInfo(): Record<string, InterfaceInfo> {
|
||||
return this.service.installed!['interfaceInfo']
|
||||
return this.service.pkg.installed!['interfaceInfo']
|
||||
}
|
||||
|
||||
get isConfigured(): boolean {
|
||||
return this.service.installed!.status.configured
|
||||
return this.service.pkg.installed!.status.configured
|
||||
}
|
||||
|
||||
get isRunning(): boolean {
|
||||
return this.getStatus(this.service).primary === PrimaryStatus.Running
|
||||
return (
|
||||
this.service.pkg.installed?.status.main.status ===
|
||||
PackageMainStatus.Running
|
||||
)
|
||||
}
|
||||
|
||||
get isStopped(): boolean {
|
||||
return this.getStatus(this.service).primary === PrimaryStatus.Stopped
|
||||
}
|
||||
|
||||
@tuiPure
|
||||
getStatus(service: PackageDataEntry): PackageStatus {
|
||||
return renderPkgStatus(service)
|
||||
return (
|
||||
this.service.pkg.installed?.status.main.status ===
|
||||
PackageMainStatus.Stopped
|
||||
)
|
||||
}
|
||||
|
||||
presentModalConfig(): void {
|
||||
this.formDialog.open<PackageConfigData>(ServiceConfigModal, {
|
||||
label: `${this.service.manifest.title} configuration`,
|
||||
label: `${this.service.pkg.manifest.title} configuration`,
|
||||
data: { pkgId: this.id },
|
||||
})
|
||||
}
|
||||
|
||||
async tryStart(): Promise<void> {
|
||||
if (this.dependencies.transform(this.service)?.some(d => !!d.errorText)) {
|
||||
const depErrMsg = `${this.service.manifest.title} has unmet dependencies. It will not work as expected.`
|
||||
const pkg = this.service.pkg
|
||||
if (Object.values(this.service.dependencies).some(dep => !!dep.errorText)) {
|
||||
const depErrMsg = `${pkg.manifest.title} has unmet dependencies. It will not work as expected.`
|
||||
const proceed = await this.presentAlertStart(depErrMsg)
|
||||
|
||||
if (!proceed) return
|
||||
}
|
||||
|
||||
const alertMsg = this.service.manifest.alerts.start
|
||||
const alertMsg = pkg.manifest.alerts.start
|
||||
|
||||
if (alertMsg) {
|
||||
const proceed = await this.presentAlertStart(alertMsg)
|
||||
@@ -138,10 +129,10 @@ export class ServiceActionsComponent {
|
||||
}
|
||||
|
||||
async tryStop(): Promise<void> {
|
||||
const { title, alerts, id } = this.service.manifest
|
||||
const { title, alerts } = this.service.pkg.manifest
|
||||
|
||||
let content = alerts.stop || ''
|
||||
if (await hasCurrentDeps(this.patch, id)) {
|
||||
if (hasCurrentDeps(this.service.pkg)) {
|
||||
const depMessage = `Services that depend on ${title} will no longer work properly and may crash`
|
||||
content = content ? `${content}.\n\n${depMessage}` : depMessage
|
||||
}
|
||||
@@ -165,15 +156,13 @@ export class ServiceActionsComponent {
|
||||
}
|
||||
|
||||
async tryRestart(): Promise<void> {
|
||||
const { id, title } = this.service.manifest
|
||||
|
||||
if (await hasCurrentDeps(this.patch, id)) {
|
||||
if (hasCurrentDeps(this.service.pkg)) {
|
||||
this.dialogs
|
||||
.open(TUI_PROMPT, {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
content: `Services that depend on ${title} may temporarily experiences issues`,
|
||||
content: `Services that depend on ${this.service.pkg.manifest} may temporarily experiences issues`,
|
||||
yes: 'Restart',
|
||||
no: 'Cancel',
|
||||
},
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { NgForOf } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { PackagePlus } from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
PackageStatus,
|
||||
PrimaryStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
import { InterfaceInfoPipe } from '../pipes/interface-info.pipe'
|
||||
import { ToStatusPipe } from '../pipes/to-status.pipe'
|
||||
import { ServiceInterfaceComponent } from './interface.component'
|
||||
import { RouterLink } from '@angular/router'
|
||||
|
||||
@@ -15,26 +14,20 @@ import { RouterLink } from '@angular/router'
|
||||
template: `
|
||||
<h3 class="g-title">Interfaces</h3>
|
||||
<a
|
||||
*ngFor="let info of service | interfaceInfo"
|
||||
*ngFor="let info of service.pkg | interfaceInfo"
|
||||
class="g-action"
|
||||
[serviceInterface]="info"
|
||||
[disabled]="!isRunning(service | toStatus)"
|
||||
[disabled]="!isRunning(service.status)"
|
||||
[routerLink]="info.routerLink"
|
||||
></a>
|
||||
`,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
standalone: true,
|
||||
imports: [
|
||||
NgForOf,
|
||||
RouterLink,
|
||||
InterfaceInfoPipe,
|
||||
ServiceInterfaceComponent,
|
||||
ToStatusPipe,
|
||||
],
|
||||
imports: [NgForOf, RouterLink, InterfaceInfoPipe, ServiceInterfaceComponent],
|
||||
})
|
||||
export class ServiceInterfacesComponent {
|
||||
@Input({ required: true })
|
||||
service!: PackageDataEntry
|
||||
service!: PackagePlus
|
||||
|
||||
isRunning({ primary }: PackageStatus): boolean {
|
||||
return primary === PrimaryStatus.Running
|
||||
|
||||
@@ -192,7 +192,7 @@ export class ServiceConfigModal {
|
||||
try {
|
||||
await this.uploadFiles(config, loader)
|
||||
|
||||
if (await hasCurrentDeps(this.patchDb, this.pkgId)) {
|
||||
if (hasCurrentDeps(this.pkg!)) {
|
||||
await this.configureDeps(config, loader)
|
||||
} else {
|
||||
await this.configure(config, loader)
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { NavigationExtras, Router } from '@angular/router'
|
||||
import { Manifest } from '@start9labs/marketplace'
|
||||
import {
|
||||
DependencyErrorType,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import { ServiceConfigModal } from '../modals/config.component'
|
||||
import { DependencyInfo } from '../types/dependency-info'
|
||||
import { PackageConfigData } from '../types/package-config-data'
|
||||
import { NavigationService } from '../../../services/navigation.service'
|
||||
import { toRouterLink } from '../../../utils/to-router-link'
|
||||
|
||||
@Pipe({
|
||||
name: 'toDependencies',
|
||||
standalone: true,
|
||||
})
|
||||
export class ToDependenciesPipe implements PipeTransform {
|
||||
constructor(
|
||||
private readonly router: Router,
|
||||
private readonly formDialog: FormDialogService,
|
||||
private readonly navigation: NavigationService,
|
||||
) {}
|
||||
|
||||
transform(pkg: PackageDataEntry): DependencyInfo[] | null {
|
||||
if (!pkg.installed) return null
|
||||
|
||||
const deps = Object.keys(pkg.installed['current-dependencies'])
|
||||
.filter(depId => pkg.manifest.dependencies[depId])
|
||||
.map(depId => this.setDepValues(pkg, depId))
|
||||
|
||||
return deps.length ? deps : null
|
||||
}
|
||||
|
||||
private setDepValues(pkg: PackageDataEntry, id: string): DependencyInfo {
|
||||
const error = pkg.installed!.status['dependency-errors'][id]
|
||||
const depInfo = pkg.installed!['dependency-info'][id]
|
||||
const version = pkg.manifest.dependencies[id].version
|
||||
const title = depInfo?.title || id
|
||||
const icon = depInfo?.icon || ''
|
||||
|
||||
let errorText = ''
|
||||
let actionText = 'View'
|
||||
let action = () => {
|
||||
this.navigation.addTab({ icon, title, routerLink: toRouterLink(id) })
|
||||
this.router.navigate([`portal`, `service`, id])
|
||||
}
|
||||
|
||||
if (error) {
|
||||
// health checks failed
|
||||
if (error.type === DependencyErrorType.HealthChecksFailed) {
|
||||
errorText = 'Health check failed'
|
||||
// not installed
|
||||
} else if (error.type === DependencyErrorType.NotInstalled) {
|
||||
errorText = 'Not installed'
|
||||
actionText = 'Install'
|
||||
action = () => this.fixDep(pkg, 'install', id)
|
||||
// incorrect version
|
||||
} else if (error.type === DependencyErrorType.IncorrectVersion) {
|
||||
errorText = 'Incorrect version'
|
||||
actionText = 'Update'
|
||||
action = () => this.fixDep(pkg, 'update', id)
|
||||
// not running
|
||||
} else if (error.type === DependencyErrorType.NotRunning) {
|
||||
errorText = 'Not running'
|
||||
actionText = 'Start'
|
||||
// config unsatisfied
|
||||
} else if (error.type === DependencyErrorType.ConfigUnsatisfied) {
|
||||
errorText = 'Config not satisfied'
|
||||
actionText = 'Auto config'
|
||||
action = () => this.fixDep(pkg, 'configure', id)
|
||||
} else if (error.type === DependencyErrorType.Transitive) {
|
||||
errorText = 'Dependency has a dependency issue'
|
||||
}
|
||||
errorText = `${errorText}. ${pkg.manifest.title} will not work as expected.`
|
||||
}
|
||||
|
||||
return { id, icon, title, version, errorText, actionText, action }
|
||||
}
|
||||
|
||||
async fixDep(
|
||||
pkg: PackageDataEntry,
|
||||
action: 'install' | 'update' | 'configure',
|
||||
depId: string,
|
||||
): Promise<void> {
|
||||
switch (action) {
|
||||
case 'install':
|
||||
case 'update':
|
||||
return this.installDep(pkg.manifest, depId)
|
||||
case 'configure':
|
||||
return this.formDialog.open<PackageConfigData>(ServiceConfigModal, {
|
||||
label: `${
|
||||
pkg.installed!['dependency-info'][depId].title
|
||||
} configuration`,
|
||||
data: {
|
||||
pkgId: depId,
|
||||
dependentInfo: pkg.manifest,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async installDep(manifest: Manifest, depId: string): Promise<void> {
|
||||
const version = manifest.dependencies[depId].version
|
||||
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: manifest.id,
|
||||
title: manifest.title,
|
||||
version,
|
||||
}
|
||||
const navigationExtras: NavigationExtras = {
|
||||
state: { dependentInfo },
|
||||
}
|
||||
|
||||
await this.router.navigate(['marketplace', depId], navigationExtras)
|
||||
}
|
||||
|
||||
private async configureDep(
|
||||
manifest: Manifest,
|
||||
dependencyId: string,
|
||||
): Promise<void> {
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: manifest.id,
|
||||
title: manifest.title,
|
||||
}
|
||||
|
||||
return this.formDialog.open<PackageConfigData>(ServiceConfigModal, {
|
||||
label: 'Config',
|
||||
data: {
|
||||
pkgId: dependencyId,
|
||||
dependentInfo,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
PackageStatus,
|
||||
renderPkgStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
|
||||
@Pipe({
|
||||
name: 'toStatus',
|
||||
standalone: true,
|
||||
})
|
||||
export class ToStatusPipe implements PipeTransform {
|
||||
transform(pkg: PackageDataEntry): PackageStatus {
|
||||
return renderPkgStatus(pkg)
|
||||
}
|
||||
}
|
||||
@@ -136,13 +136,13 @@ export class ServiceActionsRoute {
|
||||
}
|
||||
|
||||
async tryUninstall(pkg: PackageDataEntry): Promise<void> {
|
||||
const { title, alerts, id } = pkg.manifest
|
||||
const { title, alerts } = pkg.manifest
|
||||
|
||||
let content =
|
||||
alerts.uninstall ||
|
||||
`Uninstalling ${title} will permanently delete its data`
|
||||
|
||||
if (await hasCurrentDeps(this.patch, id)) {
|
||||
if (hasCurrentDeps(pkg)) {
|
||||
content = `${content}. Services that depend on ${title} will no longer work properly and may crash`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { CommonModule } from '@angular/common'
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'
|
||||
import { getPkgId, isEmptyObject } from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs'
|
||||
import { combineLatest, map } from 'rxjs'
|
||||
import {
|
||||
DataModel,
|
||||
HealthCheckResult,
|
||||
InstalledPackageInfo,
|
||||
MainStatus,
|
||||
PackageDataEntry,
|
||||
PackageState,
|
||||
@@ -16,6 +17,7 @@ import {
|
||||
PrimaryRendering,
|
||||
PrimaryStatus,
|
||||
StatusRendering,
|
||||
renderPkgStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { ServiceProgressComponent } from '../components/progress.component'
|
||||
@@ -27,8 +29,19 @@ import { ServiceDependenciesComponent } from '../components/dependencies.compone
|
||||
import { ServiceMenuComponent } from '../components/menu.component'
|
||||
import { ServiceAdditionalComponent } from '../components/additional.component'
|
||||
import { ProgressDataPipe } from '../pipes/progress-data.pipe'
|
||||
import { ToDependenciesPipe } from '../pipes/to-dependencies.pipe'
|
||||
import { ToStatusPipe } from '../pipes/to-status.pipe'
|
||||
import {
|
||||
DepErrorService,
|
||||
DependencyErrorType,
|
||||
PkgDependencyErrors,
|
||||
} from 'src/app/services/dep-error.service'
|
||||
import { DependencyInfo } from '../types/dependency-info'
|
||||
import { Manifest } from '@start9labs/marketplace'
|
||||
import { NavigationService } from '../../../services/navigation.service'
|
||||
import { toRouterLink } from '../../../utils/to-router-link'
|
||||
import { PackageConfigData } from '../types/package-config-data'
|
||||
import { ServiceConfigModal } from '../modals/config.component'
|
||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
|
||||
const STATES = [
|
||||
PackageState.Installing,
|
||||
@@ -39,8 +52,8 @@ const STATES = [
|
||||
@Component({
|
||||
template: `
|
||||
<ng-container *ngIf="service$ | async as service">
|
||||
<ng-container *ngIf="showProgress(service); else installed">
|
||||
<ng-container *ngIf="service | progressData as progress">
|
||||
<ng-container *ngIf="showProgress(service.pkg); else installed">
|
||||
<ng-container *ngIf="service.pkg | progressData as progress">
|
||||
<p [progress]="progress.downloadProgress">Downloading</p>
|
||||
<p [progress]="progress.validateProgress">Validating</p>
|
||||
<p [progress]="progress.unpackProgress">Unpacking</p>
|
||||
@@ -48,31 +61,28 @@ const STATES = [
|
||||
</ng-container>
|
||||
|
||||
<ng-template #installed>
|
||||
<ng-container *ngIf="service | toStatus as status">
|
||||
<h3 class="g-title">Status</h3>
|
||||
<service-status
|
||||
[connected]="!!(connected$ | async)"
|
||||
[installProgress]="service['install-progress']"
|
||||
[rendering]="$any(getRendering(status))"
|
||||
/>
|
||||
<service-actions
|
||||
*ngIf="isInstalled(service) && (connected$ | async)"
|
||||
[service]="service"
|
||||
/>
|
||||
<h3 class="g-title">Status</h3>
|
||||
<service-status
|
||||
[connected]="!!(connected$ | async)"
|
||||
[installProgress]="service.pkg['install-progress']"
|
||||
[rendering]="$any(getRendering(service.status))"
|
||||
/>
|
||||
<service-actions
|
||||
*ngIf="isInstalled(service.pkg) && (connected$ | async)"
|
||||
[service]="service"
|
||||
/>
|
||||
|
||||
<ng-container *ngIf="isInstalled(service) && !isBackingUp(status)">
|
||||
<service-interfaces [service]="service" />
|
||||
<service-health-checks
|
||||
*ngIf="isRunning(status) && (health$ | async) as checks"
|
||||
[checks]="checks"
|
||||
/>
|
||||
<service-dependencies
|
||||
*ngIf="service | toDependencies as dependencies"
|
||||
[dependencies]="dependencies"
|
||||
/>
|
||||
<service-menu [service]="service" />
|
||||
<service-additional [service]="service" />
|
||||
</ng-container>
|
||||
<ng-container
|
||||
*ngIf="isInstalled(service.pkg) && !isBackingUp(service.status)"
|
||||
>
|
||||
<service-interfaces [service]="service" />
|
||||
<service-health-checks
|
||||
*ngIf="isRunning(service.status) && (health$ | async) as checks"
|
||||
[checks]="checks"
|
||||
/>
|
||||
<service-dependencies [dependencies]="service.dependencies" />
|
||||
<service-menu [service]="service.pkg" />
|
||||
<service-additional [service]="service.pkg" />
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
</ng-container>
|
||||
@@ -92,16 +102,29 @@ const STATES = [
|
||||
ServiceAdditionalComponent,
|
||||
|
||||
ProgressDataPipe,
|
||||
ToDependenciesPipe,
|
||||
ToStatusPipe,
|
||||
],
|
||||
})
|
||||
export class ServiceRoute {
|
||||
private readonly patch = inject(PatchDB<DataModel>)
|
||||
private readonly pkgId = getPkgId(inject(ActivatedRoute))
|
||||
private readonly depErrorService = inject(DepErrorService)
|
||||
private readonly navigation = inject(NavigationService)
|
||||
private readonly router = inject(Router)
|
||||
private readonly formDialog = inject(FormDialogService)
|
||||
|
||||
readonly connected$ = inject(ConnectionService).connected$
|
||||
readonly service$ = this.patch.watch$('package-data', this.pkgId)
|
||||
readonly service$ = combineLatest([
|
||||
this.patch.watch$('package-data', this.pkgId),
|
||||
this.depErrorService.getPkgDepErrors$(this.pkgId),
|
||||
]).pipe(
|
||||
map(([pkg, depErrors]) => {
|
||||
return {
|
||||
pkg,
|
||||
dependencies: this.getDepInfo(pkg, depErrors),
|
||||
status: renderPkgStatus(pkg, depErrors),
|
||||
}
|
||||
}),
|
||||
)
|
||||
readonly health$ = this.patch
|
||||
.watch$('package-data', this.pkgId, 'installed', 'status', 'main')
|
||||
.pipe(map(toHealthCheck))
|
||||
@@ -125,6 +148,141 @@ export class ServiceRoute {
|
||||
showProgress({ state }: PackageDataEntry): boolean {
|
||||
return STATES.includes(state)
|
||||
}
|
||||
|
||||
private getDepInfo(
|
||||
pkg: PackageDataEntry,
|
||||
depErrors: PkgDependencyErrors,
|
||||
): DependencyInfo[] {
|
||||
const pkgInstalled = pkg.installed
|
||||
|
||||
if (!pkgInstalled) return []
|
||||
|
||||
const pkgManifest = pkg.manifest
|
||||
|
||||
return Object.keys(pkgInstalled['current-dependencies'])
|
||||
.filter(depId => !!pkg.manifest.dependencies[depId])
|
||||
.map(depId =>
|
||||
this.getDepValues(pkgInstalled, pkgManifest, depId, depErrors),
|
||||
)
|
||||
}
|
||||
|
||||
private getDepValues(
|
||||
pkgInstalled: InstalledPackageInfo,
|
||||
pkgManifest: Manifest,
|
||||
depId: string,
|
||||
depErrors: PkgDependencyErrors,
|
||||
): DependencyInfo {
|
||||
const { errorText, fixText, fixAction } = this.getDepErrors(
|
||||
pkgInstalled,
|
||||
pkgManifest,
|
||||
depId,
|
||||
depErrors,
|
||||
)
|
||||
|
||||
const depInfo = pkgInstalled['dependency-info'][depId]
|
||||
|
||||
return {
|
||||
id: depId,
|
||||
version: pkgManifest.dependencies[depId].version, // do we want this version range?
|
||||
title: depInfo?.title || depId,
|
||||
icon: depInfo?.icon || '',
|
||||
errorText: errorText
|
||||
? `${errorText}. ${pkgManifest.title} will not work as expected.`
|
||||
: '',
|
||||
actionText: fixText || 'View',
|
||||
action:
|
||||
fixAction ||
|
||||
(() => {
|
||||
this.navigation.addTab({
|
||||
icon: depInfo.icon,
|
||||
title: depInfo.title,
|
||||
routerLink: toRouterLink(depId),
|
||||
})
|
||||
this.router.navigate([`portal`, `service`, depId])
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
private getDepErrors(
|
||||
pkgInstalled: InstalledPackageInfo,
|
||||
pkgManifest: Manifest,
|
||||
depId: string,
|
||||
depErrors: PkgDependencyErrors,
|
||||
) {
|
||||
const depError = (depErrors[pkgManifest.id] as any)?.[depId] // @TODO fix
|
||||
|
||||
let errorText: string | null = null
|
||||
let fixText: string | null = null
|
||||
let fixAction: (() => any) | null = null
|
||||
|
||||
if (depError) {
|
||||
if (depError.type === DependencyErrorType.NotInstalled) {
|
||||
errorText = 'Not installed'
|
||||
fixText = 'Install'
|
||||
fixAction = () =>
|
||||
this.fixDep(pkgInstalled, pkgManifest, 'install', depId)
|
||||
} else if (depError.type === DependencyErrorType.IncorrectVersion) {
|
||||
errorText = 'Incorrect version'
|
||||
fixText = 'Update'
|
||||
fixAction = () =>
|
||||
this.fixDep(pkgInstalled, pkgManifest, 'update', depId)
|
||||
} else if (depError.type === DependencyErrorType.ConfigUnsatisfied) {
|
||||
errorText = 'Config not satisfied'
|
||||
fixText = 'Auto config'
|
||||
fixAction = () =>
|
||||
this.fixDep(pkgInstalled, pkgManifest, 'configure', depId)
|
||||
} else if (depError.type === DependencyErrorType.NotRunning) {
|
||||
errorText = 'Not running'
|
||||
fixText = 'Start'
|
||||
} else if (depError.type === DependencyErrorType.HealthChecksFailed) {
|
||||
errorText = 'Required health check not passing'
|
||||
} else if (depError.type === DependencyErrorType.Transitive) {
|
||||
errorText = 'Dependency has a dependency issue'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
errorText,
|
||||
fixText,
|
||||
fixAction,
|
||||
}
|
||||
}
|
||||
|
||||
async fixDep(
|
||||
pkgInstalled: InstalledPackageInfo,
|
||||
pkgManifest: Manifest,
|
||||
action: 'install' | 'update' | 'configure',
|
||||
depId: string,
|
||||
): Promise<void> {
|
||||
switch (action) {
|
||||
case 'install':
|
||||
case 'update':
|
||||
return this.installDep(pkgManifest, depId)
|
||||
case 'configure':
|
||||
return this.formDialog.open<PackageConfigData>(ServiceConfigModal, {
|
||||
label: `${pkgInstalled!['dependency-info'][depId].title} config`,
|
||||
data: {
|
||||
pkgId: depId,
|
||||
dependentInfo: pkgManifest,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async installDep(manifest: Manifest, depId: string): Promise<void> {
|
||||
const version = manifest.dependencies[depId].version
|
||||
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: manifest.id,
|
||||
title: manifest.title,
|
||||
version,
|
||||
}
|
||||
const navigationExtras: NavigationExtras = {
|
||||
state: { dependentInfo },
|
||||
}
|
||||
|
||||
await this.router.navigate(['marketplace', depId], navigationExtras)
|
||||
}
|
||||
}
|
||||
|
||||
function toHealthCheck(main: MainStatus): HealthCheckResult[] | null {
|
||||
|
||||
@@ -8,8 +8,6 @@ import {
|
||||
} from '@start9labs/marketplace'
|
||||
import {
|
||||
EmverPipesModule,
|
||||
isEmptyObject,
|
||||
LoadingService,
|
||||
MarkdownPipeModule,
|
||||
SafeLinksDirective,
|
||||
SharedPipesModule,
|
||||
@@ -27,16 +25,9 @@ import {
|
||||
TuiProgressModule,
|
||||
} from '@taiga-ui/kit'
|
||||
import { NgDompurifyModule } from '@tinkoff/ng-dompurify'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { Breakages } from 'src/app/services/api/api.types'
|
||||
import { getAllPackages } from 'src/app/util/get-package-data'
|
||||
import { InstallProgressPipe } from '../pipes/install-progress.pipe'
|
||||
|
||||
@Component({
|
||||
@@ -44,15 +35,15 @@ import { InstallProgressPipe } from '../pipes/install-progress.pipe'
|
||||
template: `
|
||||
<tui-accordion-item borders="top-bottom">
|
||||
<div class="g-action">
|
||||
<tui-avatar size="s" [src]="pkg | mimeType | trustUrl" />
|
||||
<tui-avatar size="s" [src]="marketplacePkg | mimeType | trustUrl" />
|
||||
<div [style.flex]="1" [style.overflow]="'hidden'">
|
||||
<strong>{{ pkg.manifest.title }}</strong>
|
||||
<strong>{{ marketplacePkg.manifest.title }}</strong>
|
||||
<div>
|
||||
<!-- @TODO left side should be local['old-manifest'] (or whatever), not manifest. -->
|
||||
{{ local.manifest.version || '' | displayEmver }}
|
||||
{{ localPkg.manifest.version || '' | displayEmver }}
|
||||
<tui-svg src="tuiIconArrowRight"></tui-svg>
|
||||
<span [style.color]="'var(--tui-positive)'">
|
||||
{{ pkg.manifest.version | displayEmver }}
|
||||
{{ marketplacePkg.manifest.version | displayEmver }}
|
||||
</span>
|
||||
</div>
|
||||
<div [style.color]="'var(--tui-negative)'">
|
||||
@@ -60,10 +51,10 @@ import { InstallProgressPipe } from '../pipes/install-progress.pipe'
|
||||
</div>
|
||||
</div>
|
||||
<tui-progress-circle
|
||||
*ngIf="local.state === 'updating'; else button"
|
||||
*ngIf="localPkg.state === 'updating'; else button"
|
||||
style="color: var(--tui-positive)"
|
||||
[max]="100"
|
||||
[value]="local['install-progress'] | installProgress"
|
||||
[value]="localPkg['install-progress'] | installProgress"
|
||||
></tui-progress-circle>
|
||||
<ng-template #button>
|
||||
<button
|
||||
@@ -84,13 +75,15 @@ import { InstallProgressPipe } from '../pipes/install-progress.pipe'
|
||||
<strong>What's new</strong>
|
||||
<p
|
||||
safeLinks
|
||||
[innerHTML]="pkg.manifest['release-notes'] | markdown | dompurify"
|
||||
[innerHTML]="
|
||||
marketplacePkg.manifest['release-notes'] | markdown | dompurify
|
||||
"
|
||||
></p>
|
||||
<a
|
||||
tuiLink
|
||||
iconAlign="right"
|
||||
icon="tuiIconExternalLink"
|
||||
[routerLink]="'/marketplace/' + pkg.manifest.id"
|
||||
[routerLink]="'/marketplace/' + marketplacePkg.manifest.id"
|
||||
[queryParams]="{ url: url }"
|
||||
>
|
||||
View listing
|
||||
@@ -131,77 +124,49 @@ import { InstallProgressPipe } from '../pipes/install-progress.pipe'
|
||||
],
|
||||
})
|
||||
export class UpdatesItemComponent {
|
||||
private readonly api = inject(ApiService)
|
||||
private readonly dialogs = inject(TuiDialogService)
|
||||
private readonly loader = inject(LoadingService)
|
||||
private readonly patch = inject(PatchDB<DataModel>)
|
||||
private readonly marketplace = inject(
|
||||
AbstractMarketplaceService,
|
||||
) as MarketplaceService
|
||||
|
||||
@Input({ required: true })
|
||||
pkg!: MarketplacePkg
|
||||
marketplacePkg!: MarketplacePkg
|
||||
|
||||
@Input({ required: true })
|
||||
local!: PackageDataEntry
|
||||
localPkg!: PackageDataEntry
|
||||
|
||||
@Input({ required: true })
|
||||
url = ''
|
||||
|
||||
get errors(): string {
|
||||
return this.marketplace.updateErrors[this.pkg.manifest.id]
|
||||
return this.marketplace.updateErrors[this.marketplacePkg.manifest.id]
|
||||
}
|
||||
|
||||
get ready(): boolean {
|
||||
return !this.marketplace.updateQueue[this.pkg.manifest.id]
|
||||
return !this.marketplace.updateQueue[this.marketplacePkg.manifest.id]
|
||||
}
|
||||
|
||||
async onClick() {
|
||||
const { id, version } = this.pkg.manifest
|
||||
const { id } = this.marketplacePkg.manifest
|
||||
|
||||
delete this.marketplace.updateErrors[id]
|
||||
this.marketplace.updateQueue[id] = true
|
||||
|
||||
if (await hasCurrentDeps(this.patch, this.local.manifest.id)) {
|
||||
await this.dry()
|
||||
if (hasCurrentDeps(this.localPkg)) {
|
||||
const proceed = await this.alert()
|
||||
|
||||
if (proceed) {
|
||||
await this.update()
|
||||
} else {
|
||||
delete this.marketplace.updateQueue[id]
|
||||
}
|
||||
} else {
|
||||
await this.update()
|
||||
}
|
||||
}
|
||||
|
||||
private async dry() {
|
||||
const { id, version } = this.pkg.manifest
|
||||
const loader = this.loader
|
||||
.open('Checking dependent services...')
|
||||
.subscribe()
|
||||
|
||||
try {
|
||||
const breakages = await this.api.dryUpdatePackage({
|
||||
id,
|
||||
version,
|
||||
})
|
||||
loader.unsubscribe()
|
||||
|
||||
if (isEmptyObject(breakages)) {
|
||||
await this.update()
|
||||
} else {
|
||||
const proceed = await this.alert(breakages)
|
||||
|
||||
if (proceed) {
|
||||
await this.update()
|
||||
} else {
|
||||
delete this.marketplace.updateQueue[id]
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
delete this.marketplace.updateQueue[id]
|
||||
this.marketplace.updateErrors[id] = e.message
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async update() {
|
||||
const { id, version } = this.pkg.manifest
|
||||
const { id, version } = this.marketplacePkg.manifest
|
||||
|
||||
try {
|
||||
await this.marketplace.installPackage(id, version, this.url)
|
||||
@@ -212,20 +177,14 @@ export class UpdatesItemComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private async alert(breakages: Breakages): Promise<boolean> {
|
||||
const content: string = `As a result of updating ${this.pkg.manifest.title}, the following services will no longer work properly and may crash:<ul>`
|
||||
const local = await getAllPackages(this.patch)
|
||||
const bullets = Object.keys(breakages)
|
||||
.map(id => `<li><b>${local[id].manifest.title}</b></li>`)
|
||||
.join('')
|
||||
|
||||
private async alert(): Promise<boolean> {
|
||||
return new Promise(async resolve => {
|
||||
this.dialogs
|
||||
.open<boolean>(TUI_PROMPT, {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
content: `${content}${bullets}</ul>`,
|
||||
content: `Services that depend on ${this.localPkg.manifest.title} will no longer work properly and may crash`,
|
||||
yes: 'Continue',
|
||||
no: 'Cancel',
|
||||
},
|
||||
|
||||
@@ -25,11 +25,10 @@ import {
|
||||
import { ClientStorageService } from 'src/app/services/client-storage.service'
|
||||
import { MarketplaceService } from 'src/app/services/marketplace.service'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { Breakages } from 'src/app/services/api/api.types'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { getAllPackages } from 'src/app/util/get-package-data'
|
||||
import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||
import { dryUpdate } from 'src/app/util/dry-update'
|
||||
|
||||
@Component({
|
||||
selector: 'marketplace-show-controls',
|
||||
@@ -59,7 +58,6 @@ export class MarketplaceShowControlsComponent {
|
||||
private readonly loader: LoadingService,
|
||||
private readonly emver: Emver,
|
||||
private readonly errorService: ErrorService,
|
||||
private readonly embassyApi: ApiService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
@@ -86,10 +84,11 @@ export class MarketplaceShowControlsComponent {
|
||||
if (!proceed) return
|
||||
}
|
||||
|
||||
const { id, version } = this.pkg.manifest
|
||||
|
||||
const currentDeps = await hasCurrentDeps(this.patch, id)
|
||||
if (currentDeps && this.emver.compare(this.localVersion, version) !== 0) {
|
||||
const currentDeps = hasCurrentDeps(this.localPkg)
|
||||
if (
|
||||
currentDeps &&
|
||||
this.emver.compare(this.localVersion, this.pkg.manifest.version) !== 0
|
||||
) {
|
||||
this.dryInstall(url)
|
||||
} else {
|
||||
this.install(url)
|
||||
@@ -131,29 +130,19 @@ export class MarketplaceShowControlsComponent {
|
||||
}
|
||||
|
||||
private async dryInstall(url: string) {
|
||||
const loader = this.loader
|
||||
.open('Checking dependent services...')
|
||||
.subscribe()
|
||||
const breakages = dryUpdate(
|
||||
this.pkg.manifest,
|
||||
await getAllPackages(this.patch),
|
||||
this.emver,
|
||||
)
|
||||
|
||||
const { id, version } = this.pkg.manifest
|
||||
|
||||
try {
|
||||
const breakages = await this.embassyApi.dryUpdatePackage({
|
||||
id,
|
||||
version: `${version}`,
|
||||
})
|
||||
|
||||
if (isEmptyObject(breakages)) {
|
||||
this.install(url, loader)
|
||||
} else {
|
||||
loader.unsubscribe()
|
||||
const proceed = await this.presentAlertBreakages(breakages)
|
||||
if (proceed) {
|
||||
this.install(url)
|
||||
}
|
||||
if (isEmptyObject(breakages)) {
|
||||
this.install(url)
|
||||
} else {
|
||||
const proceed = await this.presentAlertBreakages(breakages)
|
||||
if (proceed) {
|
||||
this.install(url)
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,14 +189,10 @@ export class MarketplaceShowControlsComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private async presentAlertBreakages(breakages: Breakages): Promise<boolean> {
|
||||
private async presentAlertBreakages(breakages: string[]): Promise<boolean> {
|
||||
let content: string =
|
||||
'As a result of this update, the following services will no longer work properly and may crash:<ul>'
|
||||
const localPkgs = await getAllPackages(this.patch)
|
||||
const bullets = Object.keys(breakages).map(id => {
|
||||
const title = localPkgs[id].manifest.title
|
||||
return `<li><b>${title}</b></li>`
|
||||
})
|
||||
const bullets = breakages.map(title => `<li><b>${title}</b></li>`)
|
||||
content = `${content}${bullets.join('')}</ul>`
|
||||
|
||||
return new Promise(async resolve => {
|
||||
|
||||
@@ -103,7 +103,7 @@ export class AppActionsPage {
|
||||
alerts.uninstall ||
|
||||
`Uninstalling ${title} will permanently delete its data`
|
||||
|
||||
if (await hasCurrentDeps(this.patch, id)) {
|
||||
if (hasCurrentDeps(pkg)) {
|
||||
content = `${content}. Services that depend on ${title} will no longer work properly and may crash`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
.metric-note {
|
||||
ion-note {
|
||||
font-size: 16px;
|
||||
color: white;
|
||||
}
|
||||
@@ -27,7 +27,7 @@
|
||||
sizeMd="6"
|
||||
>
|
||||
<app-list-pkg
|
||||
*ngIf="pkg | packageInfo | async as info"
|
||||
*ngIf="pkg.manifest.id | packageInfo | async as info"
|
||||
[pkg]="info"
|
||||
></app-list-pkg>
|
||||
</ion-col>
|
||||
|
||||
@@ -1,22 +1,25 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { filter, map, startWith, Observable } from 'rxjs'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { Observable, combineLatest } from 'rxjs'
|
||||
import { filter, map } from 'rxjs/operators'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { getPackageInfo } from 'src/app/util/get-package-info'
|
||||
import { PkgInfo } from 'src/app/types/pkg-info'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DepErrorService } from 'src/app/services/dep-error.service'
|
||||
|
||||
@Pipe({
|
||||
name: 'packageInfo',
|
||||
})
|
||||
export class PackageInfoPipe implements PipeTransform {
|
||||
constructor(private readonly patch: PatchDB<DataModel>) {}
|
||||
constructor(
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly depErrorService: DepErrorService,
|
||||
) {}
|
||||
|
||||
transform(pkg: PackageDataEntry): Observable<PkgInfo> {
|
||||
return this.patch
|
||||
.watch$('package-data', pkg.manifest.id)
|
||||
.pipe(filter(Boolean), startWith(pkg), map(getPackageInfo))
|
||||
transform(pkgId: string): Observable<PkgInfo> {
|
||||
return combineLatest([
|
||||
this.patch.watch$('package-data', pkgId).pipe(filter(Boolean)),
|
||||
this.depErrorService.getPkgDepErrors$(pkgId),
|
||||
]).pipe(map(([pkg, depErrors]) => getPackageInfo(pkg, depErrors)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,8 +22,6 @@ import {
|
||||
import { AppShowHealthChecksComponent } from './components/app-show-health-checks/app-show-health-checks.component'
|
||||
import { AppShowAdditionalComponent } from './components/app-show-additional/app-show-additional.component'
|
||||
import { HealthColorPipe } from './pipes/health-color.pipe'
|
||||
import { ToDependenciesPipe } from './pipes/to-dependencies.pipe'
|
||||
import { ToStatusPipe } from './pipes/to-status.pipe'
|
||||
import { ProgressDataPipe } from './pipes/progress-data.pipe'
|
||||
import { InsecureWarningComponentModule } from 'src/app/common/insecure-warning/insecure-warning.module'
|
||||
import { LaunchMenuComponentModule } from '../app-list/app-list-pkg/launch-menu/launch-menu.module'
|
||||
@@ -40,8 +38,6 @@ const routes: Routes = [
|
||||
AppShowPage,
|
||||
HealthColorPipe,
|
||||
ProgressDataPipe,
|
||||
ToDependenciesPipe,
|
||||
ToStatusPipe,
|
||||
AppShowHeaderComponent,
|
||||
AppShowProgressComponent,
|
||||
AppShowStatusComponent,
|
||||
@@ -64,6 +60,6 @@ const routes: Routes = [
|
||||
InsecureWarningComponentModule,
|
||||
LaunchMenuComponentModule,
|
||||
],
|
||||
exports: [InterfaceInfoPipe, ToStatusPipe],
|
||||
exports: [InterfaceInfoPipe],
|
||||
})
|
||||
export class AppShowPageModule {}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<ng-container *ngIf="pkg$ | async as pkg">
|
||||
<ng-container *ngIf="pkgPlus$ | async as pkgPlus">
|
||||
<!-- header -->
|
||||
<app-show-header [pkg]="pkg"></app-show-header>
|
||||
<app-show-header [pkg]="pkgPlus.pkg"></app-show-header>
|
||||
|
||||
<!-- content -->
|
||||
<ion-content class="ion-padding with-widgets">
|
||||
<ion-content *ngIf="pkgPlus.pkg as pkg" class="ion-padding with-widgets">
|
||||
<!-- ** installing, updating, restoring ** -->
|
||||
<ng-container *ngIf="showProgress(pkg); else installed">
|
||||
<app-show-progress
|
||||
@@ -15,43 +15,27 @@
|
||||
|
||||
<!-- Installed -->
|
||||
<ng-template #installed>
|
||||
<!-- SECURE -->
|
||||
<ng-container *ngIf="secure; else insecure">
|
||||
<ng-container *ngIf="pkg | toDependencies as dependencies">
|
||||
<ion-item-group *ngIf="pkg | toStatus as status">
|
||||
<!-- ** status ** -->
|
||||
<app-show-status
|
||||
[pkg]="pkg"
|
||||
[dependencies]="dependencies"
|
||||
[status]="status"
|
||||
></app-show-status>
|
||||
<!-- ** installed && !backing-up ** -->
|
||||
<ng-container *ngIf="isInstalled(pkg) && !isBackingUp(status)">
|
||||
<!-- ** interfaces ** -->
|
||||
<app-show-interfaces [pkg]="pkg.installed!"></app-show-interfaces>
|
||||
<!-- ** health checks ** -->
|
||||
<app-show-health-checks
|
||||
*ngIf="isRunning(status)"
|
||||
[pkgId]="pkgId"
|
||||
></app-show-health-checks>
|
||||
<!-- ** dependencies ** -->
|
||||
<app-show-dependencies
|
||||
*ngIf="dependencies.length"
|
||||
[dependencies]="dependencies"
|
||||
></app-show-dependencies>
|
||||
<!-- ** menu ** -->
|
||||
<app-show-menu [pkg]="pkg"></app-show-menu>
|
||||
<!-- ** additional ** -->
|
||||
<app-show-additional [pkg]="pkg"></app-show-additional>
|
||||
</ng-container>
|
||||
</ion-item-group>
|
||||
<ion-item-group *ngIf="pkgPlus.status as status">
|
||||
<!-- ** status ** -->
|
||||
<app-show-status [pkg]="pkg" [status]="status"></app-show-status>
|
||||
<!-- ** installed && !backing-up ** -->
|
||||
<ng-container *ngIf="isInstalled(pkg) && !isBackingUp(status)">
|
||||
<!-- ** health checks ** -->
|
||||
<app-show-health-checks
|
||||
*ngIf="isRunning(status)"
|
||||
[pkgId]="pkgId"
|
||||
></app-show-health-checks>
|
||||
<!-- ** dependencies ** -->
|
||||
<app-show-dependencies
|
||||
*ngIf="pkgPlus.dependencies.length"
|
||||
[dependencies]="pkgPlus.dependencies"
|
||||
></app-show-dependencies>
|
||||
<!-- ** menu ** -->
|
||||
<app-show-menu [pkg]="pkg"></app-show-menu>
|
||||
<!-- ** additional ** -->
|
||||
<app-show-additional [pkg]="pkg"></app-show-additional>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
|
||||
<!-- INSECURE -->
|
||||
<ng-template #insecure>
|
||||
<insecure-warning></insecure-warning>
|
||||
</ng-template>
|
||||
</ion-item-group>
|
||||
</ng-template>
|
||||
</ion-content>
|
||||
</ng-container>
|
||||
|
||||
@@ -1,19 +1,43 @@
|
||||
import { ChangeDetectionStrategy, Component } from '@angular/core'
|
||||
import { ActivatedRoute } from '@angular/router'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { tap } from 'rxjs'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
PackageState,
|
||||
InstalledPackageInfo,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
PackageStatus,
|
||||
PrimaryStatus,
|
||||
renderPkgStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { map, tap } from 'rxjs/operators'
|
||||
import { ActivatedRoute, NavigationExtras } from '@angular/router'
|
||||
import { getPkgId } from '@start9labs/shared'
|
||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||
import {
|
||||
DepErrorService,
|
||||
DependencyErrorType,
|
||||
PkgDependencyErrors,
|
||||
} from 'src/app/services/dep-error.service'
|
||||
import { Observable, combineLatest } from 'rxjs'
|
||||
import { Manifest } from '@start9labs/marketplace'
|
||||
import {
|
||||
AppConfigPage,
|
||||
PackageConfigData,
|
||||
} from './modals/app-config/app-config.page'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
|
||||
export interface DependencyInfo {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
version: string
|
||||
errorText: string
|
||||
actionText: string
|
||||
action: () => any
|
||||
}
|
||||
|
||||
const STATES = [
|
||||
PackageState.Installing,
|
||||
@@ -27,22 +51,31 @@ const STATES = [
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class AppShowPage {
|
||||
readonly secure = this.config.isSecure()
|
||||
|
||||
readonly pkgId = getPkgId(this.route)
|
||||
|
||||
readonly pkg$ = this.patch.watch$('package-data', this.pkgId).pipe(
|
||||
tap(pkg => {
|
||||
readonly pkgPlus$ = combineLatest([
|
||||
this.patch.watch$('package-data', this.pkgId),
|
||||
this.depErrorService.getPkgDepErrors$(this.pkgId),
|
||||
]).pipe(
|
||||
tap(([pkg, _]) => {
|
||||
// if package disappears, navigate to list page
|
||||
if (!pkg) this.navCtrl.navigateRoot('/services')
|
||||
}),
|
||||
map(([pkg, depErrors]) => {
|
||||
return {
|
||||
pkg,
|
||||
dependencies: this.getDepInfo(pkg, depErrors),
|
||||
status: renderPkgStatus(pkg, depErrors),
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly route: ActivatedRoute,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly config: ConfigService,
|
||||
private readonly depErrorService: DepErrorService,
|
||||
private readonly formDialog: FormDialogService,
|
||||
) {}
|
||||
|
||||
isInstalled({ state }: PackageDataEntry): boolean {
|
||||
@@ -60,4 +93,140 @@ export class AppShowPage {
|
||||
showProgress({ state }: PackageDataEntry): boolean {
|
||||
return STATES.includes(state)
|
||||
}
|
||||
|
||||
private getDepInfo(
|
||||
pkg: PackageDataEntry,
|
||||
depErrors: PkgDependencyErrors,
|
||||
): DependencyInfo[] {
|
||||
const pkgInstalled = pkg.installed
|
||||
|
||||
if (!pkgInstalled) return []
|
||||
|
||||
const pkgManifest = pkg.manifest
|
||||
|
||||
return Object.keys(pkgInstalled['current-dependencies'])
|
||||
.filter(depId => !!pkg.manifest.dependencies[depId])
|
||||
.map(depId =>
|
||||
this.getDepValues(pkgInstalled, pkgManifest, depId, depErrors),
|
||||
)
|
||||
}
|
||||
|
||||
private getDepValues(
|
||||
pkgInstalled: InstalledPackageInfo,
|
||||
pkgManifest: Manifest,
|
||||
depId: string,
|
||||
depErrors: PkgDependencyErrors,
|
||||
): DependencyInfo {
|
||||
const { errorText, fixText, fixAction } = this.getDepErrors(
|
||||
pkgManifest,
|
||||
depId,
|
||||
depErrors,
|
||||
)
|
||||
|
||||
const depInfo = pkgInstalled['dependency-info'][depId]
|
||||
|
||||
return {
|
||||
id: depId,
|
||||
version: pkgManifest.dependencies[depId].version, // do we want this version range?
|
||||
title: depInfo?.title || depId,
|
||||
icon: depInfo?.icon || '',
|
||||
errorText: errorText
|
||||
? `${errorText}. ${pkgManifest.title} will not work as expected.`
|
||||
: '',
|
||||
actionText: fixText || 'View',
|
||||
action:
|
||||
fixAction || (() => this.navCtrl.navigateForward(`/services/${depId}`)),
|
||||
}
|
||||
}
|
||||
|
||||
private getDepErrors(
|
||||
pkgManifest: Manifest,
|
||||
depId: string,
|
||||
depErrors: PkgDependencyErrors,
|
||||
) {
|
||||
const depError = (depErrors[pkgManifest.id] as any)?.[depId] // @TODO fix
|
||||
|
||||
let errorText: string | null = null
|
||||
let fixText: string | null = null
|
||||
let fixAction: (() => any) | null = null
|
||||
|
||||
if (depError) {
|
||||
if (depError.type === DependencyErrorType.NotInstalled) {
|
||||
errorText = 'Not installed'
|
||||
fixText = 'Install'
|
||||
fixAction = () => this.fixDep(pkgManifest, 'install', depId)
|
||||
} else if (depError.type === DependencyErrorType.IncorrectVersion) {
|
||||
errorText = 'Incorrect version'
|
||||
fixText = 'Update'
|
||||
fixAction = () => this.fixDep(pkgManifest, 'update', depId)
|
||||
} else if (depError.type === DependencyErrorType.ConfigUnsatisfied) {
|
||||
errorText = 'Config not satisfied'
|
||||
fixText = 'Auto config'
|
||||
fixAction = () => this.fixDep(pkgManifest, 'configure', depId)
|
||||
} else if (depError.type === DependencyErrorType.NotRunning) {
|
||||
errorText = 'Not running'
|
||||
fixText = 'Start'
|
||||
} else if (depError.type === DependencyErrorType.HealthChecksFailed) {
|
||||
errorText = 'Required health check not passing'
|
||||
} else if (depError.type === DependencyErrorType.Transitive) {
|
||||
errorText = 'Dependency has a dependency issue'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
errorText,
|
||||
fixText,
|
||||
fixAction,
|
||||
}
|
||||
}
|
||||
|
||||
private async fixDep(
|
||||
pkgManifest: Manifest,
|
||||
action: 'install' | 'update' | 'configure',
|
||||
id: string,
|
||||
): Promise<void> {
|
||||
switch (action) {
|
||||
case 'install':
|
||||
case 'update':
|
||||
return this.installDep(pkgManifest, id)
|
||||
case 'configure':
|
||||
return this.configureDep(pkgManifest, id)
|
||||
}
|
||||
}
|
||||
|
||||
private async installDep(manifest: Manifest, depId: string): Promise<void> {
|
||||
const version = manifest.dependencies[depId].version
|
||||
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: manifest.id,
|
||||
title: manifest.title,
|
||||
version,
|
||||
}
|
||||
const navigationExtras: NavigationExtras = {
|
||||
state: { dependentInfo },
|
||||
}
|
||||
|
||||
await this.navCtrl.navigateForward(
|
||||
`/marketplace/${depId}`,
|
||||
navigationExtras,
|
||||
)
|
||||
}
|
||||
|
||||
private async configureDep(
|
||||
manifest: Manifest,
|
||||
dependencyId: string,
|
||||
): Promise<void> {
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: manifest.id,
|
||||
title: manifest.title,
|
||||
}
|
||||
|
||||
return this.formDialog.open<PackageConfigData>(AppConfigPage, {
|
||||
label: 'Config',
|
||||
data: {
|
||||
pkgId: dependencyId,
|
||||
dependentInfo,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
|
||||
import { DependencyInfo } from '../../pipes/to-dependencies.pipe'
|
||||
import { DependencyInfo } from '../../app-show.page'
|
||||
|
||||
@Component({
|
||||
selector: 'app-show-dependencies',
|
||||
|
||||
@@ -14,11 +14,16 @@
|
||||
<ion-grid>
|
||||
<ion-row style="padding-left: 12px">
|
||||
<ion-col>
|
||||
<ion-button
|
||||
*ngIf="canStop"
|
||||
class="action-button"
|
||||
color="danger"
|
||||
(click)="tryStop()"
|
||||
>
|
||||
<ion-icon slot="start" name="stop-outline"></ion-icon>
|
||||
Stop
|
||||
</ion-button>
|
||||
<ng-container *ngIf="isRunning">
|
||||
<ion-button class="action-button" color="danger" (click)="tryStop()">
|
||||
<ion-icon slot="start" name="stop-outline"></ion-icon>
|
||||
Stop
|
||||
</ion-button>
|
||||
<ion-button
|
||||
class="action-button"
|
||||
color="tertiary"
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
AppConfigPage,
|
||||
PackageConfigData,
|
||||
} from '../../modals/app-config/app-config.page'
|
||||
import { DependencyInfo } from '../../pipes/to-dependencies.pipe'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
import { ConnectionService } from 'src/app/services/connection.service'
|
||||
import { LaunchMenuComponent } from '../../../app-list/app-list-pkg/launch-menu/launch-menu.component'
|
||||
@@ -47,8 +46,7 @@ export class AppShowStatusComponent {
|
||||
@Input({ required: true })
|
||||
status!: PackageStatus
|
||||
|
||||
@Input()
|
||||
dependencies: DependencyInfo[] = []
|
||||
PR = PrimaryRendering
|
||||
|
||||
readonly connected$ = this.connectionService.connected$
|
||||
|
||||
@@ -82,6 +80,14 @@ export class AppShowStatusComponent {
|
||||
return this.status.primary === PrimaryStatus.Running
|
||||
}
|
||||
|
||||
get canStop(): boolean {
|
||||
return [
|
||||
PrimaryStatus.Running,
|
||||
PrimaryStatus.Starting,
|
||||
PrimaryStatus.Restarting,
|
||||
].includes(this.status.primary as PrimaryStatus)
|
||||
}
|
||||
|
||||
get isStopped(): boolean {
|
||||
return this.status.primary === PrimaryStatus.Stopped
|
||||
}
|
||||
@@ -98,7 +104,7 @@ export class AppShowStatusComponent {
|
||||
}
|
||||
|
||||
async tryStart(): Promise<void> {
|
||||
if (this.dependencies.some(d => !!d.errorText)) {
|
||||
if (this.status.dependency === 'warning') {
|
||||
const depErrMsg = `${this.pkg.manifest.title} has unmet dependencies. It will not work as expected.`
|
||||
const proceed = await this.presentAlertStart(depErrMsg)
|
||||
|
||||
@@ -117,10 +123,10 @@ export class AppShowStatusComponent {
|
||||
}
|
||||
|
||||
async tryStop(): Promise<void> {
|
||||
const { title, alerts, id } = this.pkg.manifest
|
||||
const { title, alerts } = this.pkg.manifest
|
||||
|
||||
let content = alerts.stop || ''
|
||||
if (await hasCurrentDeps(this.patch, id)) {
|
||||
if (hasCurrentDeps(this.pkg)) {
|
||||
const depMessage = `Services that depend on ${title} will no longer work properly and may crash`
|
||||
content = content ? `${content}.\n\n${depMessage}` : depMessage
|
||||
}
|
||||
@@ -144,15 +150,13 @@ export class AppShowStatusComponent {
|
||||
}
|
||||
|
||||
async tryRestart(): Promise<void> {
|
||||
const { id, title } = this.pkg.manifest
|
||||
|
||||
if (await hasCurrentDeps(this.patch, id)) {
|
||||
if (hasCurrentDeps(this.pkg)) {
|
||||
this.dialogs
|
||||
.open(TUI_PROMPT, {
|
||||
label: 'Warning',
|
||||
size: 's',
|
||||
data: {
|
||||
content: `Services that depend on ${title} may temporarily experiences issues`,
|
||||
content: `Services that depend on ${this.pkg.manifest} may temporarily experiences issues`,
|
||||
yes: 'Restart',
|
||||
no: 'Cancel',
|
||||
},
|
||||
|
||||
@@ -109,7 +109,7 @@ export class AppConfigPage {
|
||||
try {
|
||||
await this.uploadFiles(config, loader)
|
||||
|
||||
if (await hasCurrentDeps(this.patchDb, this.pkgId)) {
|
||||
if (hasCurrentDeps(this.pkg!)) {
|
||||
await this.configureDeps(config, loader)
|
||||
} else {
|
||||
await this.configure(config, loader)
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { NavigationExtras } from '@angular/router'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import {
|
||||
DependencyErrorType,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||
import { FormDialogService } from 'src/app/services/form-dialog.service'
|
||||
import {
|
||||
AppConfigPage,
|
||||
PackageConfigData,
|
||||
} from '../modals/app-config/app-config.page'
|
||||
import { Manifest } from '@start9labs/marketplace'
|
||||
|
||||
export interface DependencyInfo {
|
||||
id: string
|
||||
title: string
|
||||
icon: string
|
||||
version: string
|
||||
errorText: string
|
||||
actionText: string
|
||||
action: () => any
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'toDependencies',
|
||||
})
|
||||
export class ToDependenciesPipe implements PipeTransform {
|
||||
constructor(
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly formDialog: FormDialogService,
|
||||
) {}
|
||||
|
||||
transform(pkg: PackageDataEntry): DependencyInfo[] {
|
||||
if (!pkg.installed) return []
|
||||
|
||||
return Object.keys(pkg.installed['current-dependencies'])
|
||||
.filter(depId => !!pkg.manifest.dependencies[depId])
|
||||
.map(depId => this.setDepValues(pkg, depId))
|
||||
}
|
||||
|
||||
private setDepValues(pkg: PackageDataEntry, depId: string): DependencyInfo {
|
||||
let errorText = ''
|
||||
let actionText = 'View'
|
||||
let action: () => any = () =>
|
||||
this.navCtrl.navigateForward(`/services/${depId}`)
|
||||
|
||||
const error = pkg.installed!.status['dependency-errors'][depId]
|
||||
|
||||
if (error) {
|
||||
// health checks failed
|
||||
if (error.type === DependencyErrorType.HealthChecksFailed) {
|
||||
errorText = 'Health check failed'
|
||||
// not installed
|
||||
} else if (error.type === DependencyErrorType.NotInstalled) {
|
||||
errorText = 'Not installed'
|
||||
actionText = 'Install'
|
||||
action = () => this.fixDep(pkg, 'install', depId)
|
||||
// incorrect version
|
||||
} else if (error.type === DependencyErrorType.IncorrectVersion) {
|
||||
errorText = 'Incorrect version'
|
||||
actionText = 'Update'
|
||||
action = () => this.fixDep(pkg, 'update', depId)
|
||||
// not running
|
||||
} else if (error.type === DependencyErrorType.NotRunning) {
|
||||
errorText = 'Not running'
|
||||
actionText = 'Start'
|
||||
// config unsatisfied
|
||||
} else if (error.type === DependencyErrorType.ConfigUnsatisfied) {
|
||||
errorText = 'Config not satisfied'
|
||||
actionText = 'Auto config'
|
||||
action = () => this.fixDep(pkg, 'configure', depId)
|
||||
} else if (error.type === DependencyErrorType.Transitive) {
|
||||
errorText = 'Dependency has a dependency issue'
|
||||
}
|
||||
errorText = `${errorText}. ${pkg.manifest.title} will not work as expected.`
|
||||
}
|
||||
|
||||
const depInfo = pkg.installed!['dependency-info'][depId]
|
||||
|
||||
return {
|
||||
id: depId,
|
||||
version: pkg.manifest.dependencies[depId].version,
|
||||
title: depInfo?.title || depId,
|
||||
icon: depInfo?.icon || '',
|
||||
errorText,
|
||||
actionText,
|
||||
action,
|
||||
}
|
||||
}
|
||||
|
||||
async fixDep(
|
||||
pkg: PackageDataEntry,
|
||||
action: 'install' | 'update' | 'configure',
|
||||
depId: string,
|
||||
): Promise<void> {
|
||||
switch (action) {
|
||||
case 'install':
|
||||
case 'update':
|
||||
return this.installDep(pkg.manifest, depId)
|
||||
case 'configure':
|
||||
return this.formDialog.open<PackageConfigData>(AppConfigPage, {
|
||||
label: `${
|
||||
pkg.installed!['dependency-info'][depId].title
|
||||
} configuration`,
|
||||
data: {
|
||||
pkgId: depId,
|
||||
dependentInfo: pkg.manifest,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
private async installDep(manifest: Manifest, depId: string): Promise<void> {
|
||||
const version = manifest.dependencies[depId].version
|
||||
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: manifest.id,
|
||||
title: manifest.title,
|
||||
version,
|
||||
}
|
||||
const navigationExtras: NavigationExtras = {
|
||||
state: { dependentInfo },
|
||||
}
|
||||
|
||||
await this.navCtrl.navigateForward(
|
||||
`/marketplace/${depId}`,
|
||||
navigationExtras,
|
||||
)
|
||||
}
|
||||
|
||||
private async configureDep(
|
||||
manifest: Manifest,
|
||||
dependencyId: string,
|
||||
): Promise<void> {
|
||||
const dependentInfo: DependentInfo = {
|
||||
id: manifest.id,
|
||||
title: manifest.title,
|
||||
}
|
||||
|
||||
return this.formDialog.open<PackageConfigData>(AppConfigPage, {
|
||||
label: 'Config',
|
||||
data: {
|
||||
pkgId: dependencyId,
|
||||
dependentInfo,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import {
|
||||
PackageStatus,
|
||||
renderPkgStatus,
|
||||
} from 'src/app/services/pkg-status-rendering.service'
|
||||
|
||||
@Pipe({
|
||||
name: 'toStatus',
|
||||
})
|
||||
export class ToStatusPipe implements PipeTransform {
|
||||
transform(pkg: PackageDataEntry): PackageStatus {
|
||||
return renderPkgStatus(pkg)
|
||||
}
|
||||
}
|
||||
@@ -6,18 +6,25 @@
|
||||
[style.font-style]="style"
|
||||
[style.font-weight]="weight"
|
||||
>
|
||||
<span *ngIf="!installProgress">
|
||||
{{ (connected$ | async) ? rendering.display : 'Unknown' }}
|
||||
<span *ngIf="rendering.showDots" class="loading-dots"></span>
|
||||
{{ (connected$ | async) ? rendering.display : 'Unknown' }}
|
||||
|
||||
<span
|
||||
*ngIf="
|
||||
rendering.display === PR[PS.Stopping].display &&
|
||||
(sigtermTimeout | durationToSeconds) > 30
|
||||
"
|
||||
>
|
||||
this may take a while
|
||||
</span>
|
||||
|
||||
<span *ngIf="installProgress">
|
||||
<ion-text
|
||||
*ngIf="installProgress | installProgressDisplay as progress"
|
||||
color="primary"
|
||||
>
|
||||
Installing
|
||||
<span class="loading-dots"></span>
|
||||
{{ progress }}
|
||||
</ion-text>
|
||||
</span>
|
||||
|
||||
<span *ngIf="rendering.showDots" class="loading-dots"></span>
|
||||
</p>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<logs
|
||||
[fetchLogs]="fetchLogs()"
|
||||
[followLogs]="followLogs()"
|
||||
context="eos"
|
||||
context="start-os"
|
||||
defaultBack="system"
|
||||
pageTitle="OS Logs"
|
||||
class="ion-page"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { Metrics } from 'src/app/services/api/api.types'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { TimeInfo, TimeService } from 'src/app/services/time-service'
|
||||
import { TimeService } from 'src/app/services/time-service'
|
||||
import {
|
||||
catchError,
|
||||
combineLatest,
|
||||
@@ -29,9 +29,24 @@ export class ServerMetricsPage {
|
||||
private readonly connectionService: ConnectionService,
|
||||
) {}
|
||||
|
||||
private getServerData$(): Observable<[TimeInfo, Metrics]> {
|
||||
private getServerData$(): Observable<
|
||||
[
|
||||
{
|
||||
value: number
|
||||
synced: boolean
|
||||
},
|
||||
{
|
||||
days: number
|
||||
hours: number
|
||||
minutes: number
|
||||
seconds: number
|
||||
},
|
||||
Metrics,
|
||||
]
|
||||
> {
|
||||
return combineLatest([
|
||||
this.timeService.getTimeInfo$(),
|
||||
this.timeService.now$,
|
||||
this.timeService.uptime$,
|
||||
this.getMetrics$(),
|
||||
]).pipe(
|
||||
catchError(() => {
|
||||
|
||||
@@ -14,12 +14,52 @@
|
||||
</ng-template>
|
||||
|
||||
<!-- loaded -->
|
||||
<div
|
||||
*ngIf="server$ | async as server; else loading"
|
||||
class="cap-width"
|
||||
style="padding-top: 0"
|
||||
>
|
||||
<insecure-warning *ngIf="!secure"></insecure-warning>
|
||||
<ion-item-group *ngIf="server$ | async as server; else loading">
|
||||
<ion-item
|
||||
*ngIf="!server['ntp-synced']"
|
||||
color="warning"
|
||||
class="ion-margin-bottom"
|
||||
>
|
||||
<ion-icon slot="start" name="warning-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2 style="font-weight: bold">Clock sync failure</h2>
|
||||
<p style="font-weight: 600">
|
||||
This will cause connectivity issues. Refer to the StartOS docs to
|
||||
resolve the issue.
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-button
|
||||
slot="end"
|
||||
color="light"
|
||||
href="https://docs.start9.com/0.3.5.x/support/common-issues#clock-sync-failure"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Open Docs
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<ion-item *ngIf="isTorHttp" color="warning" class="ion-margin-bottom">
|
||||
<ion-icon slot="start" name="warning-outline"></ion-icon>
|
||||
<ion-label>
|
||||
<h2 style="font-weight: bold">Http detected</h2>
|
||||
<p style="font-weight: 600">
|
||||
Tor is faster over https.
|
||||
<a
|
||||
[routerLink]="['/system', 'root-ca']"
|
||||
style="color: var(--ion-color-light)"
|
||||
>
|
||||
Download and trust your server's Root CA
|
||||
</a>
|
||||
, then switch to https.
|
||||
</p>
|
||||
</ion-label>
|
||||
<ion-button slot="end" color="light" (click)="launchHttps()">
|
||||
Open Https
|
||||
<ion-icon slot="end" name="open-outline"></ion-icon>
|
||||
</ion-button>
|
||||
</ion-item>
|
||||
|
||||
<div *ngFor="let cat of settings | keyvalue : asIsOrder">
|
||||
<ion-item-divider>
|
||||
@@ -80,5 +120,5 @@
|
||||
</ion-item>
|
||||
</ion-item-group>
|
||||
</div>
|
||||
</div>
|
||||
</ion-item-group>
|
||||
</ion-content>
|
||||
|
||||
@@ -21,7 +21,7 @@ import { TuiAlertService, TuiDialogService } from '@taiga-ui/core'
|
||||
import { PROMPT } from 'src/app/apps/ui/modals/prompt/prompt.component'
|
||||
import { PolymorpheusComponent } from '@tinkoff/ng-polymorpheus'
|
||||
import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { WINDOW } from '@ng-web-apis/common'
|
||||
import { getServerInfo } from 'src/app/util/get-server-info'
|
||||
import * as argon2 from '@start9labs/argon2'
|
||||
import { ProxyService } from 'src/app/services/proxy.service'
|
||||
@@ -39,9 +39,7 @@ export class ServerShowPage {
|
||||
readonly showUpdate$ = this.eosService.showUpdate$
|
||||
readonly showDiskRepair$ = this.clientStorageService.showDiskRepair$
|
||||
|
||||
readonly secure = this.config.isSecure()
|
||||
readonly isTorHttp =
|
||||
this.config.isTor() && this.document.location.protocol === 'http:'
|
||||
readonly isTorHttp = this.config.isTorHttp()
|
||||
|
||||
constructor(
|
||||
private readonly dialogs: TuiDialogService,
|
||||
@@ -58,7 +56,7 @@ export class ServerShowPage {
|
||||
private readonly config: ConfigService,
|
||||
private readonly formDialog: FormDialogService,
|
||||
private readonly proxyService: ProxyService,
|
||||
@Inject(DOCUMENT) private readonly document: Document,
|
||||
@Inject(WINDOW) private readonly windowRef: Window,
|
||||
) {}
|
||||
|
||||
addClick(title: string) {
|
||||
@@ -254,6 +252,11 @@ export class ServerShowPage {
|
||||
.subscribe(() => this.systemRebuild())
|
||||
}
|
||||
|
||||
async launchHttps() {
|
||||
const info = await getServerInfo(this.patch)
|
||||
this.windowRef.open(`https://${info.ui.torHostname}`, '_self')
|
||||
}
|
||||
|
||||
private async setName(value: string | null): Promise<void> {
|
||||
const loader = this.loader.open('Saving...').subscribe()
|
||||
|
||||
@@ -276,7 +279,6 @@ export class ServerShowPage {
|
||||
|
||||
try {
|
||||
await this.api.restartServer({})
|
||||
this.presentAlertInProgress(action, ` until ${action} completes.`)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
@@ -290,10 +292,6 @@ export class ServerShowPage {
|
||||
|
||||
try {
|
||||
await this.api.shutdownServer({})
|
||||
this.presentAlertInProgress(
|
||||
action,
|
||||
'.<br /><br /><b>You will need to physically power cycle the device to regain connectivity.</b>',
|
||||
)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
@@ -307,7 +305,6 @@ export class ServerShowPage {
|
||||
|
||||
try {
|
||||
await this.api.systemRebuild({})
|
||||
this.presentAlertInProgress(action, ` until ${action} completes.`)
|
||||
} catch (e: any) {
|
||||
this.errorService.handleError(e)
|
||||
} finally {
|
||||
@@ -343,18 +340,6 @@ export class ServerShowPage {
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
private presentAlertInProgress(verb: string, message: string) {
|
||||
this.dialogs
|
||||
.open(
|
||||
`Stopping all services gracefully. This can take a while.<br /><br />If you have a speaker, your server will <b>♫ play a melody ♫</b> before shutting down. Your server will then become unreachable${message}`,
|
||||
{
|
||||
label: `${verb} In Progress...`,
|
||||
size: 's',
|
||||
},
|
||||
)
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
settings: ServerSettings = {
|
||||
General: [
|
||||
{
|
||||
@@ -404,7 +389,7 @@ export class ServerShowPage {
|
||||
icon: 'key-outline',
|
||||
action: () => this.presentAlertResetPassword(),
|
||||
detail: false,
|
||||
disabled$: of(!this.secure),
|
||||
disabled$: of(false),
|
||||
},
|
||||
{
|
||||
title: 'Experimental Features',
|
||||
@@ -559,8 +544,8 @@ export class ServerShowPage {
|
||||
description: 'Discover what StartOS can do',
|
||||
icon: 'map-outline',
|
||||
action: () =>
|
||||
window.open(
|
||||
'https://docs.start9.com/latest/user-manual',
|
||||
this.windowRef.open(
|
||||
'https://docs.start9.com/0.3.5.x/user-manual',
|
||||
'_blank',
|
||||
'noreferrer',
|
||||
),
|
||||
@@ -572,7 +557,11 @@ export class ServerShowPage {
|
||||
description: 'Get help from the Start9 team and community',
|
||||
icon: 'chatbubbles-outline',
|
||||
action: () =>
|
||||
window.open('https://start9.com/contact', '_blank', 'noreferrer'),
|
||||
this.windowRef.open(
|
||||
'https://start9.com/contact',
|
||||
'_blank',
|
||||
'noreferrer',
|
||||
),
|
||||
detail: true,
|
||||
disabled$: of(false),
|
||||
},
|
||||
@@ -581,7 +570,7 @@ export class ServerShowPage {
|
||||
description: `Support StartOS development`,
|
||||
icon: 'logo-bitcoin',
|
||||
action: () =>
|
||||
this.document.defaultView?.open(
|
||||
this.windowRef.open(
|
||||
'https://donate.start9.com',
|
||||
'_blank',
|
||||
'noreferrer',
|
||||
|
||||
@@ -13,7 +13,7 @@ import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||
styleUrls: ['ssh-keys.page.scss'],
|
||||
})
|
||||
export class SSHKeysPage {
|
||||
readonly docsUrl = 'https://docs.start9.com/latest/user-manual/ssh'
|
||||
readonly docsUrl = 'https://docs.start9.com/0.3.5.x/user-manual/ssh'
|
||||
sshKeys: SSHKey[] = []
|
||||
loading$ = new BehaviorSubject(true)
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Component, Inject } from '@angular/core'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
@@ -12,15 +11,16 @@ import {
|
||||
Marketplace,
|
||||
StoreIdentity,
|
||||
} from '@start9labs/marketplace'
|
||||
import { isEmptyObject, LoadingService } from '@start9labs/shared'
|
||||
import { LoadingService } from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { combineLatest, filter, Observable } from 'rxjs'
|
||||
import { NavController } from '@ionic/angular'
|
||||
import { hasCurrentDeps } from 'src/app/util/has-deps'
|
||||
import { getAllPackages } from 'src/app/util/get-package-data'
|
||||
import { Breakages } from 'src/app/services/api/api.types'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { TUI_PROMPT } from '@taiga-ui/kit'
|
||||
import { Emver, isEmptyObject } from '@start9labs/shared'
|
||||
import { combineLatest, Observable } from 'rxjs'
|
||||
import { dryUpdate } from 'src/app/util/dry-update'
|
||||
|
||||
interface UpdatesData {
|
||||
hosts: StoreIdentity[]
|
||||
@@ -45,11 +45,11 @@ export class UpdatesPage {
|
||||
constructor(
|
||||
@Inject(AbstractMarketplaceService)
|
||||
readonly marketplaceService: MarketplaceService,
|
||||
private readonly api: ApiService,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly navCtrl: NavController,
|
||||
private readonly loader: LoadingService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
private readonly emver: Emver,
|
||||
readonly config: ConfigService,
|
||||
) {}
|
||||
|
||||
@@ -74,55 +74,41 @@ export class UpdatesPage {
|
||||
delete this.marketplaceService.updateErrors[id]
|
||||
this.marketplaceService.updateQueue[id] = true
|
||||
|
||||
if (await hasCurrentDeps(this.patch, local.manifest.id)) {
|
||||
this.dryUpdate(manifest, url)
|
||||
if (hasCurrentDeps(local)) {
|
||||
this.dryInstall(manifest, url)
|
||||
} else {
|
||||
this.update(id, version, url)
|
||||
this.install(id, version, url)
|
||||
}
|
||||
}
|
||||
|
||||
private async dryUpdate(manifest: Manifest, url: string) {
|
||||
const loader = this.loader
|
||||
.open('Checking dependent services...')
|
||||
.subscribe()
|
||||
const { id, version } = manifest
|
||||
private async dryInstall(manifest: Manifest, url: string) {
|
||||
const { id, version, title } = manifest
|
||||
|
||||
try {
|
||||
const breakages = await this.api.dryUpdatePackage({
|
||||
id,
|
||||
version: `${version}`,
|
||||
})
|
||||
loader.unsubscribe()
|
||||
const breakages = dryUpdate(
|
||||
manifest,
|
||||
await getAllPackages(this.patch),
|
||||
this.emver,
|
||||
)
|
||||
|
||||
if (isEmptyObject(breakages)) {
|
||||
this.update(id, version, url)
|
||||
if (isEmptyObject(breakages)) {
|
||||
this.install(id, version, url)
|
||||
} else {
|
||||
const proceed = await this.presentAlertBreakages(title, breakages)
|
||||
if (proceed) {
|
||||
this.install(id, version, url)
|
||||
} else {
|
||||
const proceed = await this.presentAlertBreakages(
|
||||
manifest.title,
|
||||
breakages,
|
||||
)
|
||||
if (proceed) {
|
||||
this.update(id, version, url)
|
||||
} else {
|
||||
delete this.marketplaceService.updateQueue[id]
|
||||
}
|
||||
delete this.marketplaceService.updateQueue[id]
|
||||
}
|
||||
} catch (e: any) {
|
||||
delete this.marketplaceService.updateQueue[id]
|
||||
this.marketplaceService.updateErrors[id] = e.message
|
||||
loader.unsubscribe()
|
||||
}
|
||||
}
|
||||
|
||||
private async presentAlertBreakages(
|
||||
title: string,
|
||||
breakages: Breakages,
|
||||
breakages: string[],
|
||||
): Promise<boolean> {
|
||||
let content: string = `As a result of updating ${title}, the following services will no longer work properly and may crash:<ul>`
|
||||
const localPkgs = await getAllPackages(this.patch)
|
||||
const bullets = Object.keys(breakages).map(id => {
|
||||
const title = localPkgs[id].manifest.title
|
||||
return `<li><b>${title}</b></li>`
|
||||
const bullets = breakages.map(depTitle => {
|
||||
return `<li><b>${depTitle}</b></li>`
|
||||
})
|
||||
content = `${content}${bullets.join('')}</ul>`
|
||||
|
||||
@@ -141,7 +127,7 @@ export class UpdatesPage {
|
||||
})
|
||||
}
|
||||
|
||||
private async update(id: string, version: string, url: string) {
|
||||
private async install(id: string, version: string, url: string) {
|
||||
try {
|
||||
await this.marketplaceService.installPackage(id, version, url)
|
||||
delete this.marketplaceService.updateQueue[id]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectionStrategy, Component, inject } from '@angular/core'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { map } from 'rxjs'
|
||||
import { map } from 'rxjs/operators'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
import { PrimaryStatus } from 'src/app/services/pkg-status-rendering.service'
|
||||
import { getPackageInfo } from 'src/app/util/get-package-info'
|
||||
import { PkgInfo } from 'src/app/types/pkg-info'
|
||||
import { combineLatest } from 'rxjs'
|
||||
import { DepErrorService } from 'src/app/services/dep-error.service'
|
||||
|
||||
@Component({
|
||||
selector: 'widget-health',
|
||||
@@ -24,29 +26,32 @@ export class HealthComponent {
|
||||
'Transitioning',
|
||||
] as const
|
||||
|
||||
readonly data$ = inject(PatchDB<DataModel>)
|
||||
.watch$('package-data')
|
||||
.pipe(
|
||||
map(data => {
|
||||
const pkgs = Object.values<PackageDataEntry>(data).map(getPackageInfo)
|
||||
const result = this.labels.reduce<Record<string, number>>(
|
||||
(acc, label) => ({
|
||||
...acc,
|
||||
[label]: this.getCount(label, pkgs),
|
||||
}),
|
||||
{},
|
||||
)
|
||||
readonly data$ = combineLatest([
|
||||
inject(PatchDB<DataModel>).watch$('package-data'),
|
||||
inject(DepErrorService).depErrors$,
|
||||
]).pipe(
|
||||
map(([data, depErrors]) => {
|
||||
const pkgs = Object.values<PackageDataEntry>(data).map(pkg =>
|
||||
getPackageInfo(pkg, depErrors[pkg.manifest.id]),
|
||||
)
|
||||
const result = this.labels.reduce<Record<string, number>>(
|
||||
(acc, label) => ({
|
||||
...acc,
|
||||
[label]: this.getCount(label, pkgs),
|
||||
}),
|
||||
{},
|
||||
)
|
||||
|
||||
result['Healthy'] =
|
||||
pkgs.length -
|
||||
result['Error'] -
|
||||
result['Needs Attention'] -
|
||||
result['Stopped'] -
|
||||
result['Transitioning']
|
||||
result['Healthy'] =
|
||||
pkgs.length -
|
||||
result['Error'] -
|
||||
result['Needs Attention'] -
|
||||
result['Stopped'] -
|
||||
result['Transitioning']
|
||||
|
||||
return this.labels.map(label => result[label])
|
||||
}),
|
||||
)
|
||||
return this.labels.map(label => result[label])
|
||||
}),
|
||||
)
|
||||
|
||||
private getCount(label: string, pkgs: PkgInfo[]): number {
|
||||
switch (label) {
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
<h2>This Release</h2>
|
||||
|
||||
<h4>0.3.4.4</h4>
|
||||
<h4>0.3.5</h4>
|
||||
<p class="note-padding">
|
||||
View the complete
|
||||
<a
|
||||
href="https://github.com/Start9Labs/start-os/releases/tag/v0.3.4.4"
|
||||
href="https://github.com/Start9Labs/start-os/releases/tag/v0.3.5"
|
||||
target="_blank"
|
||||
noreferrer
|
||||
>
|
||||
@@ -14,81 +13,22 @@
|
||||
</p>
|
||||
<h6>Highlights</h6>
|
||||
<ul class="spaced-list">
|
||||
<li>Https over Tor for faster UI loading times</li>
|
||||
<li>Change password through UI</li>
|
||||
<li>Use IP address for Network Folder backups</li>
|
||||
<li>
|
||||
Multiple bug fixes, performance enhancements, and other small features
|
||||
This release contains significant under-the-hood improvements to performance
|
||||
and reliability
|
||||
</li>
|
||||
<li>Ditch Docker, replace with Podman</li>
|
||||
<li>Remove locking behavior from PatchDB and optimize</li>
|
||||
<li>Boost efficiency of service manager</li>
|
||||
<li>Require HTTPS on LAN, and improve setup flow for trusting Root CA</li>
|
||||
<li>Better default privacy settings for Firefox kiosk mode</li>
|
||||
<li>Eliminate memory leak from Javascript runtime</li>
|
||||
<li>Other small bug fixes</li>
|
||||
<li>Update license to MIT</li>
|
||||
</ul>
|
||||
|
||||
<h2>Previous Releases</h2>
|
||||
|
||||
<h4>0.3.4.3</h4>
|
||||
<p class="note-padding">
|
||||
View the complete
|
||||
<a
|
||||
href="https://github.com/Start9Labs/start-os/releases/tag/v0.3.4.3"
|
||||
target="_blank"
|
||||
noreferrer
|
||||
>
|
||||
release notes
|
||||
</a>
|
||||
for more details.
|
||||
</p>
|
||||
<h6>Highlights</h6>
|
||||
<ul class="spaced-list">
|
||||
<li>Improved Tor reliability</li>
|
||||
<li>Experimental features tab</li>
|
||||
<li>Multiple bugfixes and general performance enhancements</li>
|
||||
<li>Update branding</li>
|
||||
</ul>
|
||||
|
||||
<h4>0.3.4.2</h4>
|
||||
<p class="note-padding">
|
||||
View the complete
|
||||
<a
|
||||
href="https://github.com/Start9Labs/start-os/releases/tag/v0.3.4.2"
|
||||
target="_blank"
|
||||
noreferrer
|
||||
>
|
||||
release notes
|
||||
</a>
|
||||
for more details.
|
||||
</p>
|
||||
<h6>Highlights</h6>
|
||||
<ul class="spaced-list">
|
||||
<li>Update build system for Server Lite and NUC-based Server One</li>
|
||||
<li>Rename embassyOS to StartOS</li>
|
||||
<li>
|
||||
PWA support for StartOS web interface. You can now save StartOS to your
|
||||
phone as an app!
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h4>0.3.4</h4>
|
||||
<p class="note-padding">
|
||||
View the complete
|
||||
<a
|
||||
href="https://github.com/Start9Labs/start-os/releases/tag/v0.3.4"
|
||||
target="_blank"
|
||||
noreferrer
|
||||
>
|
||||
release notes
|
||||
</a>
|
||||
for more details.
|
||||
</p>
|
||||
<h6>Highlights</h6>
|
||||
<ul class="spaced-list">
|
||||
<li>Security patches</li>
|
||||
<li>Bug fixes</li>
|
||||
<li>Breakout services to Community Registry</li>
|
||||
<li>SSL support for IP access</li>
|
||||
<li>UI display improvements</li>
|
||||
<li>Better logs</li>
|
||||
<li>New system metrics</li>
|
||||
<li>EFI support</li>
|
||||
</ul>
|
||||
<button tuiButton class="begin" (click)="context.$implicit.complete()">
|
||||
Begin
|
||||
</button>
|
||||
<div class="ion-text-center ion-padding">
|
||||
<button tuiButton class="begin" (click)="context.$implicit.complete()">
|
||||
Begin
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -19,33 +19,35 @@ ion-card {
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
padding: 0.6rem;
|
||||
font-weight: 600;
|
||||
font-size: calc(12px + 0.5vw);
|
||||
height: 3rem;
|
||||
height: 2.4rem;
|
||||
}
|
||||
|
||||
ion-card-content {
|
||||
min-height: 9rem;
|
||||
min-height: 8rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
ion-icon {
|
||||
font-size: calc(90px + 0.5vw);
|
||||
font-size: calc(90px + 0.4vw);
|
||||
--ionicon-stroke-width: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
ion-footer {
|
||||
padding: 1rem;
|
||||
font-family: 'Open Sans', sans-serif;
|
||||
padding: 0 1rem;
|
||||
font-family: 'Open Sans';
|
||||
font-size: clamp(1rem, calc(12px + 0.5vw), 1.3rem);
|
||||
height: 9rem;
|
||||
height: 4.5rem;
|
||||
width: clamp(13rem, 80%, 18rem);
|
||||
margin: 0 auto;
|
||||
* {
|
||||
max-width: 100%;
|
||||
}
|
||||
p {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.footer-md::before {
|
||||
@@ -54,10 +56,6 @@ ion-card {
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
ion-card-title,
|
||||
ion-footer {
|
||||
height: auto !important;
|
||||
}
|
||||
ion-footer {
|
||||
width: 10rem;
|
||||
}
|
||||
|
||||
@@ -1,14 +1,7 @@
|
||||
<div #gridContent>
|
||||
<ion-grid>
|
||||
<ion-row class="ion-justify-content-center ion-align-items-center">
|
||||
<ion-col
|
||||
*ngFor="let card of cards"
|
||||
responsiveCol
|
||||
sizeLg="4"
|
||||
sizeSm="6"
|
||||
sizeXs="12"
|
||||
class="ion-align-self-center"
|
||||
>
|
||||
<ion-col *ngFor="let card of cards" sizeXs="12">
|
||||
<widget-card
|
||||
[cardDetails]="card"
|
||||
[containerDimensions]="containerDimensions"
|
||||
|
||||
@@ -3,11 +3,17 @@ ion-col {
|
||||
--ion-grid-column-padding: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1800px) {
|
||||
@media (min-width: 1700px) {
|
||||
div {
|
||||
padding: 0 20%;
|
||||
padding: 0 7%;
|
||||
}
|
||||
ion-col {
|
||||
max-width: 24rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 2000px) {
|
||||
div {
|
||||
padding: 0 12%;
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,8 @@ import { Card, Dimension } from './widget-card/widget-card.component'
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class WidgetListComponent {
|
||||
@ViewChild('gridContent') gridContent: ElementRef<HTMLElement> =
|
||||
{} as ElementRef<HTMLElement>
|
||||
@ViewChild('gridContent')
|
||||
gridContent: ElementRef<HTMLElement> = {} as ElementRef<HTMLElement>
|
||||
@HostListener('window:resize', ['$event'])
|
||||
onResize() {
|
||||
this.setContainerDimensions()
|
||||
@@ -38,46 +38,46 @@ export class WidgetListComponent {
|
||||
|
||||
cards: Card[] = [
|
||||
{
|
||||
title: 'Visit the Marketplace',
|
||||
title: 'Server Info',
|
||||
icon: 'information-circle-outline',
|
||||
color: 'var(--alt-green)',
|
||||
description: 'View information about your server',
|
||||
link: '/system/specs',
|
||||
},
|
||||
{
|
||||
title: 'Browse',
|
||||
icon: 'storefront-outline',
|
||||
color: 'var(--alt-blue)',
|
||||
description: 'Shop for your favorite open source services',
|
||||
color: 'var(--alt-purple)',
|
||||
description: 'Browse for services to install',
|
||||
link: '/marketplace',
|
||||
qp: { back: 'true' },
|
||||
},
|
||||
{
|
||||
title: 'Root CA',
|
||||
icon: 'ribbon-outline',
|
||||
color: 'var(--alt-orange)',
|
||||
description: `Download and trust your server's root certificate authority`,
|
||||
link: '/system/root-ca',
|
||||
},
|
||||
{
|
||||
title: 'Create Backup',
|
||||
icon: 'duplicate-outline',
|
||||
color: 'var(--alt-purple)',
|
||||
color: 'var(--alt-blue)',
|
||||
description: 'Back up StartOS and service data',
|
||||
link: '/system/backup',
|
||||
},
|
||||
{
|
||||
title: 'Server Info',
|
||||
icon: 'information-circle-outline',
|
||||
color: 'var(--alt-green)',
|
||||
description: 'View basic information about your server',
|
||||
link: '/system/specs',
|
||||
title: 'Monitor',
|
||||
icon: 'pulse-outline',
|
||||
color: 'var(--alt-orange)',
|
||||
description: `View your system resource usage`,
|
||||
link: '/system/metrics',
|
||||
},
|
||||
{
|
||||
title: 'User Manual',
|
||||
icon: 'map-outline',
|
||||
color: 'var(--alt-yellow)',
|
||||
description: 'Discover what StartOS can do',
|
||||
link: 'https://docs.start9.com/latest/user-manual/index',
|
||||
link: 'https://docs.start9.com/0.3.5.x/user-manual/index',
|
||||
},
|
||||
{
|
||||
title: 'Contact Support',
|
||||
icon: 'chatbubbles-outline',
|
||||
color: 'var(--alt-red)',
|
||||
description: 'Get help from the Start9 team and community',
|
||||
description: 'Get help from the Start9 community',
|
||||
link: 'https://start9.com/contact',
|
||||
},
|
||||
]
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
DependencyErrorType,
|
||||
HealthResult,
|
||||
PackageDataEntry,
|
||||
PackageMainStatus,
|
||||
@@ -32,12 +31,14 @@ export module Mock {
|
||||
'current-backup': null,
|
||||
'update-progress': null,
|
||||
updated: true,
|
||||
restarting: false,
|
||||
'shutting-down': false,
|
||||
}
|
||||
export const MarketplaceEos: RR.GetMarketplaceEosRes = {
|
||||
version: '0.3.4.4',
|
||||
version: '0.3.5',
|
||||
headline: 'Our biggest release ever.',
|
||||
'release-notes': {
|
||||
'0.3.5': 'Some **Markdown** release _notes_ for 0.3.5',
|
||||
'0.3.4.4': 'Some **Markdown** release _notes_ for 0.3.4.4',
|
||||
'0.3.4.3': 'Some **Markdown** release _notes_ for 0.3.4.3',
|
||||
'0.3.4.2': 'Some **Markdown** release _notes_ for 0.3.4.2',
|
||||
@@ -381,29 +382,80 @@ export module Mock {
|
||||
export function getMetrics(): Metrics {
|
||||
return {
|
||||
general: {
|
||||
temperature: (Math.random() * 100).toFixed(1),
|
||||
temperature: {
|
||||
value: '66.8',
|
||||
unit: '°C',
|
||||
},
|
||||
},
|
||||
memory: {
|
||||
'percentage-used': '20',
|
||||
total: (Math.random() * 100).toFixed(2),
|
||||
available: '18000',
|
||||
used: '4000',
|
||||
'swap-total': '1000',
|
||||
'swap-free': Math.random().toFixed(2),
|
||||
'swap-used': '0',
|
||||
'percentage-used': {
|
||||
value: '30.7',
|
||||
unit: '%',
|
||||
},
|
||||
total: {
|
||||
value: '31971.10',
|
||||
unit: 'MiB',
|
||||
},
|
||||
available: {
|
||||
value: '22150.66',
|
||||
unit: 'MiB',
|
||||
},
|
||||
used: {
|
||||
value: '8784.97',
|
||||
unit: 'MiB',
|
||||
},
|
||||
'zram-total': {
|
||||
value: '7992.00',
|
||||
unit: 'MiB',
|
||||
},
|
||||
'zram-available': {
|
||||
value: '7882.50',
|
||||
unit: 'MiB',
|
||||
},
|
||||
'zram-used': {
|
||||
value: '109.50',
|
||||
unit: 'MiB',
|
||||
},
|
||||
},
|
||||
cpu: {
|
||||
'user-space': '100',
|
||||
'kernel-space': '50',
|
||||
'io-wait': String(Math.random() * 50),
|
||||
idle: '80',
|
||||
usage: '30',
|
||||
'percentage-used': {
|
||||
value: '8.4',
|
||||
unit: '%',
|
||||
},
|
||||
'user-space': {
|
||||
value: '7.0',
|
||||
unit: '%',
|
||||
},
|
||||
'kernel-space': {
|
||||
value: '1.4',
|
||||
unit: '%',
|
||||
},
|
||||
wait: {
|
||||
value: '0.5',
|
||||
unit: '%',
|
||||
},
|
||||
idle: {
|
||||
value: '91.1',
|
||||
unit: '%',
|
||||
},
|
||||
},
|
||||
disk: {
|
||||
size: '1000',
|
||||
used: '900',
|
||||
available: '100',
|
||||
'percentage-used': '90',
|
||||
capacity: {
|
||||
value: '1851.60',
|
||||
unit: 'GB',
|
||||
},
|
||||
used: {
|
||||
value: '859.02',
|
||||
unit: 'GB',
|
||||
},
|
||||
available: {
|
||||
value: '992.59',
|
||||
unit: 'GB',
|
||||
},
|
||||
'percentage-used': {
|
||||
value: '46.4',
|
||||
unit: '%',
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1258,7 +1310,7 @@ export module Mock {
|
||||
},
|
||||
},
|
||||
},
|
||||
'dependency-errors': {},
|
||||
'dependency-config-errors': {},
|
||||
},
|
||||
interfaceInfo: {
|
||||
rpc: {
|
||||
@@ -1297,6 +1349,7 @@ export module Mock {
|
||||
},
|
||||
},
|
||||
'current-dependencies': {},
|
||||
'current-dependents': {},
|
||||
'dependency-info': {},
|
||||
'marketplace-url': 'https://registry.start9.com/',
|
||||
'developer-key': 'developer-key',
|
||||
@@ -1350,7 +1403,7 @@ export module Mock {
|
||||
main: {
|
||||
status: PackageMainStatus.Stopped,
|
||||
},
|
||||
'dependency-errors': {},
|
||||
'dependency-config-errors': {},
|
||||
},
|
||||
interfaceInfo: {
|
||||
rpc: {
|
||||
@@ -1371,6 +1424,7 @@ export module Mock {
|
||||
type: 'api',
|
||||
},
|
||||
},
|
||||
'current-dependents': {},
|
||||
'current-dependencies': {
|
||||
bitcoind: {
|
||||
'health-checks': [],
|
||||
@@ -1378,8 +1432,8 @@ export module Mock {
|
||||
},
|
||||
'dependency-info': {
|
||||
bitcoind: {
|
||||
title: 'Bitcoin Core',
|
||||
icon: 'assets/img/service-icons/bitcoind.png',
|
||||
title: Mock.MockManifestBitcoind.title,
|
||||
icon: 'assets/img/service-icons/bitcoind.svg',
|
||||
},
|
||||
},
|
||||
'marketplace-url': 'https://registry.start9.com/',
|
||||
@@ -1402,11 +1456,8 @@ export module Mock {
|
||||
main: {
|
||||
status: PackageMainStatus.Stopped,
|
||||
},
|
||||
'dependency-errors': {
|
||||
'btc-rpc-proxy': {
|
||||
type: DependencyErrorType.ConfigUnsatisfied,
|
||||
error: 'This is a config unsatisfied error',
|
||||
},
|
||||
'dependency-config-errors': {
|
||||
'btc-rpc-proxy': 'Username not found',
|
||||
},
|
||||
},
|
||||
interfaceInfo: {
|
||||
@@ -1453,13 +1504,14 @@ export module Mock {
|
||||
'health-checks': [],
|
||||
},
|
||||
},
|
||||
'current-dependents': {},
|
||||
'dependency-info': {
|
||||
bitcoind: {
|
||||
title: 'Bitcoin Core',
|
||||
title: Mock.MockManifestBitcoind.title,
|
||||
icon: 'assets/img/service-icons/bitcoind.svg',
|
||||
},
|
||||
'btc-rpc-proxy': {
|
||||
title: 'Bitcoin Proxy',
|
||||
title: Mock.MockManifestBitcoinProxy.title,
|
||||
icon: 'assets/img/service-icons/btc-rpc-proxy.png',
|
||||
},
|
||||
},
|
||||
|
||||
@@ -3,11 +3,11 @@ import { MarketplacePkg, StoreInfo, Manifest } from '@start9labs/marketplace'
|
||||
import { InputSpec } from '@start9labs/start-sdk/lib/config/configTypes'
|
||||
import {
|
||||
DataModel,
|
||||
DependencyError,
|
||||
DomainInfo,
|
||||
NetworkStrategy,
|
||||
OsOutboundProxy,
|
||||
ServiceOutboundProxy,
|
||||
HealthCheckResult,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { StartOSDiskInfo, LogsRes, ServerLogsReq } from '@start9labs/shared'
|
||||
import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants'
|
||||
@@ -25,7 +25,7 @@ export module RR {
|
||||
// auth
|
||||
|
||||
export type LoginReq = {
|
||||
password: Encrypted | string
|
||||
password: string
|
||||
metadata: SessionMetadata
|
||||
} // auth.login - unauthed
|
||||
export type loginRes = null
|
||||
@@ -41,11 +41,14 @@ export module RR {
|
||||
|
||||
// server
|
||||
|
||||
export type EchoReq = { message: string } // server.echo
|
||||
export type EchoReq = { message: string; timeout?: number } // server.echo
|
||||
export type EchoRes = string
|
||||
|
||||
export type GetSystemTimeReq = {} // server.time
|
||||
export type GetSystemTimeRes = string
|
||||
export type GetSystemTimeRes = {
|
||||
now: string
|
||||
uptime: number // seconds
|
||||
}
|
||||
|
||||
export type GetServerLogsReq = ServerLogsReq // server.logs & server.kernel-logs
|
||||
export type GetServerLogsRes = LogsRes
|
||||
@@ -309,9 +312,6 @@ export module RR {
|
||||
} // package.install
|
||||
export type InstallPackageRes = null
|
||||
|
||||
export type DryUpdatePackageReq = { id: string; version: string } // package.update.dry
|
||||
export type DryUpdatePackageRes = Breakages
|
||||
|
||||
export type GetPackageConfigReq = { id: string } // package.config.get
|
||||
export type GetPackageConfigRes = { spec: InputSpec; config: object }
|
||||
|
||||
@@ -421,31 +421,36 @@ export interface ActionResponse {
|
||||
qr: boolean
|
||||
}
|
||||
|
||||
interface MetricData {
|
||||
value: string
|
||||
unit: string
|
||||
}
|
||||
|
||||
export interface Metrics {
|
||||
general: {
|
||||
temperature: string
|
||||
temperature: MetricData | null
|
||||
}
|
||||
memory: {
|
||||
'percentage-used': string
|
||||
total: string
|
||||
available: string
|
||||
used: string
|
||||
'swap-total': string
|
||||
'swap-free': string
|
||||
'swap-used': string
|
||||
total: MetricData
|
||||
'percentage-used': MetricData
|
||||
used: MetricData
|
||||
available: MetricData
|
||||
'zram-total': MetricData
|
||||
'zram-used': MetricData
|
||||
'zram-available': MetricData
|
||||
}
|
||||
cpu: {
|
||||
'user-space': string
|
||||
'kernel-space': string
|
||||
'io-wait': string
|
||||
idle: string
|
||||
usage: string
|
||||
'percentage-used': MetricData
|
||||
idle: MetricData
|
||||
'user-space': MetricData
|
||||
'kernel-space': MetricData
|
||||
wait: MetricData
|
||||
}
|
||||
disk: {
|
||||
size: string
|
||||
used: string
|
||||
available: string
|
||||
'percentage-used': string
|
||||
capacity: MetricData
|
||||
'percentage-used': MetricData
|
||||
used: MetricData
|
||||
available: MetricData
|
||||
}
|
||||
}
|
||||
|
||||
@@ -621,3 +626,49 @@ export type Encrypted = {
|
||||
}
|
||||
|
||||
export type CloudProvider = 'dropbox' | 'google-drive'
|
||||
|
||||
export type DependencyError =
|
||||
| DependencyErrorNotInstalled
|
||||
| DependencyErrorNotRunning
|
||||
| DependencyErrorIncorrectVersion
|
||||
| DependencyErrorConfigUnsatisfied
|
||||
| DependencyErrorHealthChecksFailed
|
||||
| DependencyErrorTransitive
|
||||
|
||||
export enum DependencyErrorType {
|
||||
NotInstalled = 'not-installed',
|
||||
NotRunning = 'not-running',
|
||||
IncorrectVersion = 'incorrect-version',
|
||||
ConfigUnsatisfied = 'config-unsatisfied',
|
||||
HealthChecksFailed = 'health-checks-failed',
|
||||
InterfaceHealthChecksFailed = 'interface-health-checks-failed',
|
||||
Transitive = 'transitive',
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotInstalled {
|
||||
type: DependencyErrorType.NotInstalled
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotRunning {
|
||||
type: DependencyErrorType.NotRunning
|
||||
}
|
||||
|
||||
export interface DependencyErrorIncorrectVersion {
|
||||
type: DependencyErrorType.IncorrectVersion
|
||||
expected: string // version range
|
||||
received: string // version
|
||||
}
|
||||
|
||||
export interface DependencyErrorConfigUnsatisfied {
|
||||
type: DependencyErrorType.ConfigUnsatisfied
|
||||
error: string
|
||||
}
|
||||
|
||||
export interface DependencyErrorHealthChecksFailed {
|
||||
type: DependencyErrorType.HealthChecksFailed
|
||||
check: HealthCheckResult
|
||||
}
|
||||
|
||||
export interface DependencyErrorTransitive {
|
||||
type: DependencyErrorType.Transitive
|
||||
}
|
||||
|
||||
@@ -1,29 +1,11 @@
|
||||
import { BehaviorSubject, Observable } from 'rxjs'
|
||||
import { Observable } from 'rxjs'
|
||||
import { Update } from 'patch-db-client'
|
||||
import { RR, Encrypted, BackupTargetType, Metrics } from './api.types'
|
||||
import { RR, BackupTargetType, Metrics } from './api.types'
|
||||
import { DataModel } from 'src/app/services/patch-db/data-model'
|
||||
import { Log, SetupStatus } from '@start9labs/shared'
|
||||
import { WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import type { JWK } from 'node-jose'
|
||||
|
||||
export abstract class ApiService {
|
||||
protected readonly jose = import('node-jose')
|
||||
|
||||
readonly patchStream$ = new BehaviorSubject<Update<DataModel>[]>([])
|
||||
pubkey?: JWK.Key
|
||||
|
||||
async encrypt(toEncrypt: string): Promise<Encrypted> {
|
||||
const { pubkey } = this
|
||||
|
||||
if (!pubkey) throw new Error('No pubkey found!')
|
||||
|
||||
const encrypted = await this.jose.then(jose =>
|
||||
jose.JWE.createEncrypt(pubkey).update(toEncrypt).final(),
|
||||
)
|
||||
|
||||
return { encrypted }
|
||||
}
|
||||
|
||||
// http
|
||||
|
||||
// for getting static files: ex icons, instructions, licenses
|
||||
@@ -43,8 +25,6 @@ export abstract class ApiService {
|
||||
|
||||
// auth
|
||||
|
||||
abstract getPubKey(): Promise<void>
|
||||
|
||||
abstract login(params: RR.LoginReq): Promise<RR.loginRes>
|
||||
|
||||
abstract logout(params: RR.LogoutReq): Promise<RR.LogoutRes>
|
||||
@@ -59,7 +39,7 @@ export abstract class ApiService {
|
||||
|
||||
// server
|
||||
|
||||
abstract echo(params: RR.EchoReq): Promise<RR.EchoRes>
|
||||
abstract echo(params: RR.EchoReq, urlOverride?: string): Promise<RR.EchoRes>
|
||||
|
||||
abstract openPatchWebsocket$(): Observable<Update<DataModel>>
|
||||
|
||||
@@ -286,10 +266,6 @@ export abstract class ApiService {
|
||||
params: RR.InstallPackageReq,
|
||||
): Promise<RR.InstallPackageRes>
|
||||
|
||||
abstract dryUpdatePackage(
|
||||
params: RR.DryUpdatePackageReq,
|
||||
): Promise<RR.DryUpdatePackageRes>
|
||||
|
||||
abstract getPackageConfig(
|
||||
params: RR.GetPackageConfigReq,
|
||||
): Promise<RR.GetPackageConfigRes>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import {
|
||||
decodeBase64,
|
||||
HttpOptions,
|
||||
HttpService,
|
||||
isRpcError,
|
||||
@@ -14,7 +13,7 @@ import { ApiService } from './embassy-api.service'
|
||||
import { BackupTargetType, Metrics, RR } from './api.types'
|
||||
import { ConfigService } from '../config.service'
|
||||
import { webSocket, WebSocketSubjectConfig } from 'rxjs/webSocket'
|
||||
import { Observable } from 'rxjs'
|
||||
import { Observable, filter, firstValueFrom } from 'rxjs'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { DataModel } from '../patch-db/data-model'
|
||||
@@ -77,20 +76,8 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
// auth
|
||||
|
||||
/**
|
||||
* We want to update the pubkey, which means that we will call in clearnet the
|
||||
* getPubKey, and all the information is never in the clear, and only public
|
||||
* information is sent across the network.
|
||||
*/
|
||||
async getPubKey() {
|
||||
this.pubkey = await this.rpcRequest({
|
||||
method: 'auth.get-pubkey',
|
||||
params: {},
|
||||
})
|
||||
}
|
||||
|
||||
async login(params: RR.LoginReq): Promise<RR.loginRes> {
|
||||
return this.rpcRequest({ method: 'auth.login', params }, false)
|
||||
return this.rpcRequest({ method: 'auth.login', params })
|
||||
}
|
||||
|
||||
async logout(params: RR.LogoutReq): Promise<RR.LogoutRes> {
|
||||
@@ -113,8 +100,8 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
// server
|
||||
|
||||
async echo(params: RR.EchoReq): Promise<RR.EchoRes> {
|
||||
return this.rpcRequest({ method: 'echo', params }, false)
|
||||
async echo(params: RR.EchoReq, urlOverride?: string): Promise<RR.EchoRes> {
|
||||
return this.rpcRequest({ method: 'echo', params }, urlOverride)
|
||||
}
|
||||
|
||||
openPatchWebsocket$(): Observable<Update<DataModel>> {
|
||||
@@ -483,12 +470,6 @@ export class LiveApiService extends ApiService {
|
||||
return this.rpcRequest({ method: 'package.install', params })
|
||||
}
|
||||
|
||||
async dryUpdatePackage(
|
||||
params: RR.DryUpdatePackageReq,
|
||||
): Promise<RR.DryUpdatePackageRes> {
|
||||
return this.rpcRequest({ method: 'package.update.dry', params })
|
||||
}
|
||||
|
||||
async getPackageConfig(
|
||||
params: RR.GetPackageConfigReq,
|
||||
): Promise<RR.GetPackageConfigRes> {
|
||||
@@ -588,41 +569,28 @@ export class LiveApiService extends ApiService {
|
||||
|
||||
private async rpcRequest<T>(
|
||||
options: RPCOptions,
|
||||
addHeader = true,
|
||||
urlOverride?: string,
|
||||
): Promise<T> {
|
||||
if (addHeader) {
|
||||
options.headers = {
|
||||
'x-patch-sequence': String(this.patch.cache$.value.sequence),
|
||||
...(options.headers || {}),
|
||||
}
|
||||
}
|
||||
const res = await this.http.rpcRequest<T>(options, urlOverride)
|
||||
const body = res.body
|
||||
|
||||
const res = await this.http.rpcRequest<T>(options)
|
||||
const encodedUpdates = res.headers.get('x-patch-updates')
|
||||
const encodedError = res.headers.get('x-patch-error')
|
||||
|
||||
if (encodedUpdates) {
|
||||
const decoded = decodeBase64(encodedUpdates)
|
||||
const updates: Update<DataModel>[] = JSON.parse(decoded)
|
||||
this.patchStream$.next(updates)
|
||||
}
|
||||
|
||||
if (encodedError) {
|
||||
const error = decodeBase64(encodedError)
|
||||
console.error(error)
|
||||
}
|
||||
|
||||
const rpcRes = res.body
|
||||
|
||||
if (isRpcError(rpcRes)) {
|
||||
if (rpcRes.error.code === 34) {
|
||||
if (isRpcError(body)) {
|
||||
if (body.error.code === 34) {
|
||||
console.error('Unauthenticated, logging out')
|
||||
this.auth.setUnverified()
|
||||
}
|
||||
throw new RpcError(rpcRes.error)
|
||||
throw new RpcError(body.error)
|
||||
}
|
||||
|
||||
return rpcRes.result
|
||||
const patchSequence = res.headers.get('x-patch-sequence')
|
||||
if (patchSequence)
|
||||
await firstValueFrom(
|
||||
this.patch.cache$.pipe(
|
||||
filter(({ sequence }) => sequence >= Number(patchSequence)),
|
||||
),
|
||||
)
|
||||
|
||||
return body.result
|
||||
}
|
||||
|
||||
private async httpRequest<T>(opts: HttpOptions): Promise<T> {
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
} from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
DependencyErrorType,
|
||||
InstallProgress,
|
||||
PackageDataEntry,
|
||||
PackageMainStatus,
|
||||
@@ -26,7 +25,8 @@ import {
|
||||
interval,
|
||||
map,
|
||||
Observable,
|
||||
ReplaySubject,
|
||||
shareReplay,
|
||||
Subject,
|
||||
switchMap,
|
||||
tap,
|
||||
timer,
|
||||
@@ -50,8 +50,8 @@ const PROGRESS: InstallProgress = {
|
||||
|
||||
@Injectable()
|
||||
export class MockApiService extends ApiService {
|
||||
readonly mockWsSource$ = new ReplaySubject<Update<DataModel>>()
|
||||
private readonly revertTime = 2000
|
||||
readonly mockWsSource$ = new Subject<Update<DataModel>>()
|
||||
private readonly revertTime = 1800
|
||||
sequence = 0
|
||||
|
||||
constructor(
|
||||
@@ -64,7 +64,6 @@ export class MockApiService extends ApiService {
|
||||
.pipe(
|
||||
tap(() => {
|
||||
this.sequence = 0
|
||||
this.patchStream$.next([])
|
||||
}),
|
||||
switchMap(verified =>
|
||||
iif(
|
||||
@@ -111,29 +110,13 @@ export class MockApiService extends ApiService {
|
||||
value: params.value,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// auth
|
||||
|
||||
async getPubKey() {
|
||||
await pauseFor(1000)
|
||||
|
||||
// randomly generated
|
||||
// const keystore = jose.JWK.createKeyStore()
|
||||
// this.pubkey = await keystore.generate('EC', 'P-256')
|
||||
|
||||
// generated from backend
|
||||
this.pubkey = await this.jose.then(jose =>
|
||||
jose.JWK.asKey({
|
||||
kty: 'EC',
|
||||
crv: 'P-256',
|
||||
x: 'yHTDYSfjU809fkSv9MmN4wuojf5c3cnD7ZDN13n-jz4',
|
||||
y: '8Mpkn744A5KDag0DmX2YivB63srjbugYZzWc3JOpQXI',
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
async login(params: RR.LoginReq): Promise<RR.loginRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
@@ -168,13 +151,20 @@ export class MockApiService extends ApiService {
|
||||
|
||||
// server
|
||||
|
||||
async echo(params: RR.EchoReq): Promise<RR.EchoRes> {
|
||||
async echo(params: RR.EchoReq, url?: string): Promise<RR.EchoRes> {
|
||||
if (url) {
|
||||
const num = Math.floor(Math.random() * 10) + 1
|
||||
if (num > 8) return params.message
|
||||
throw new Error()
|
||||
}
|
||||
await pauseFor(2000)
|
||||
return params.message
|
||||
}
|
||||
|
||||
openPatchWebsocket$(): Observable<Update<DataModel>> {
|
||||
return this.mockWsSource$
|
||||
return this.mockWsSource$.pipe(
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
}
|
||||
|
||||
openLogsWebsocket$(config: WebSocketSubjectConfig<Log>): Observable<Log> {
|
||||
@@ -205,7 +195,10 @@ export class MockApiService extends ApiService {
|
||||
params: RR.GetSystemTimeReq,
|
||||
): Promise<RR.GetSystemTimeRes> {
|
||||
await pauseFor(2000)
|
||||
return new Date().toUTCString()
|
||||
return {
|
||||
now: new Date().toUTCString(),
|
||||
uptime: 1234567,
|
||||
}
|
||||
}
|
||||
|
||||
async getServerLogs(
|
||||
@@ -312,7 +305,9 @@ export class MockApiService extends ApiService {
|
||||
value: initialProgress,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, 'updating')
|
||||
this.mockRevision(patch)
|
||||
|
||||
return 'updating'
|
||||
}
|
||||
|
||||
async setServerClearnetAddress(
|
||||
@@ -326,13 +321,37 @@ export class MockApiService extends ApiService {
|
||||
value: params.domainInfo,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async restartServer(
|
||||
params: RR.RestartServerReq,
|
||||
): Promise<RR.RestartServerRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/status-info/restarting',
|
||||
value: true,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
setTimeout(() => {
|
||||
const patch2 = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/status-info/restarting',
|
||||
value: false,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch2)
|
||||
}, 2000)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -340,14 +359,34 @@ export class MockApiService extends ApiService {
|
||||
params: RR.ShutdownServerReq,
|
||||
): Promise<RR.ShutdownServerRes> {
|
||||
await pauseFor(2000)
|
||||
|
||||
const patch = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/status-info/shutting-down',
|
||||
value: true,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
setTimeout(() => {
|
||||
const patch2 = [
|
||||
{
|
||||
op: PatchOp.REPLACE,
|
||||
path: '/server-info/status-info/shutting-down',
|
||||
value: false,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch2)
|
||||
}, 2000)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async systemRebuild(
|
||||
params: RR.RestartServerReq,
|
||||
): Promise<RR.RestartServerRes> {
|
||||
await pauseFor(2000)
|
||||
return null
|
||||
params: RR.SystemRebuildReq,
|
||||
): Promise<RR.SystemRebuildRes> {
|
||||
return this.restartServer(params)
|
||||
}
|
||||
|
||||
async repairDisk(params: RR.RestartServerReq): Promise<RR.RestartServerRes> {
|
||||
@@ -369,7 +408,9 @@ export class MockApiService extends ApiService {
|
||||
value: params.enable,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async setOsOutboundProxy(
|
||||
@@ -384,7 +425,9 @@ export class MockApiService extends ApiService {
|
||||
value: params.proxy,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// marketplace URLs
|
||||
@@ -437,7 +480,9 @@ export class MockApiService extends ApiService {
|
||||
value: 0,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, Mock.Notifications)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return Mock.Notifications
|
||||
}
|
||||
|
||||
async deleteNotification(
|
||||
@@ -485,7 +530,9 @@ export class MockApiService extends ApiService {
|
||||
],
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async updateProxy(params: RR.UpdateProxyReq): Promise<RR.UpdateProxyRes> {
|
||||
@@ -500,7 +547,9 @@ export class MockApiService extends ApiService {
|
||||
value,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async deleteProxy(params: RR.DeleteProxyReq): Promise<RR.DeleteProxyRes> {
|
||||
@@ -512,7 +561,9 @@ export class MockApiService extends ApiService {
|
||||
value: [],
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// domains
|
||||
@@ -534,7 +585,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async deleteStart9ToDomain(
|
||||
@@ -548,7 +601,9 @@ export class MockApiService extends ApiService {
|
||||
value: null,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async addDomain(params: RR.AddDomainReq): Promise<RR.AddDomainRes> {
|
||||
@@ -569,7 +624,9 @@ export class MockApiService extends ApiService {
|
||||
],
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async deleteDomain(params: RR.DeleteDomainReq): Promise<RR.DeleteDomainRes> {
|
||||
@@ -581,7 +638,9 @@ export class MockApiService extends ApiService {
|
||||
value: [],
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// port forwards
|
||||
@@ -598,7 +657,9 @@ export class MockApiService extends ApiService {
|
||||
value: params.port,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// wifi
|
||||
@@ -612,7 +673,9 @@ export class MockApiService extends ApiService {
|
||||
value: params.enable,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async getWifi(params: RR.GetWifiReq): Promise<RR.GetWifiRes> {
|
||||
@@ -653,8 +716,9 @@ export class MockApiService extends ApiService {
|
||||
value: params,
|
||||
},
|
||||
]
|
||||
this.mockRevision(patch)
|
||||
|
||||
return this.withRevision(patch)
|
||||
return null
|
||||
}
|
||||
|
||||
// ssh
|
||||
@@ -838,7 +902,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
]
|
||||
|
||||
return this.withRevision(originalPatch)
|
||||
this.mockRevision(originalPatch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
// package
|
||||
@@ -905,23 +971,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch)
|
||||
}
|
||||
this.mockRevision(patch)
|
||||
|
||||
async dryUpdatePackage(
|
||||
params: RR.DryUpdatePackageReq,
|
||||
): Promise<RR.DryUpdatePackageRes> {
|
||||
await pauseFor(2000)
|
||||
return {
|
||||
lnd: {
|
||||
dependency: 'bitcoind',
|
||||
error: {
|
||||
type: DependencyErrorType.IncorrectVersion,
|
||||
expected: '>0.23.0',
|
||||
received: params.version,
|
||||
},
|
||||
},
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
async getPackageConfig(
|
||||
@@ -952,7 +1004,9 @@ export class MockApiService extends ApiService {
|
||||
value: true,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async restorePackages(
|
||||
@@ -976,7 +1030,9 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
})
|
||||
|
||||
return this.withRevision(patch)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async executePackageAction(
|
||||
@@ -1026,7 +1082,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
]
|
||||
|
||||
return this.withRevision(originalPatch)
|
||||
this.mockRevision(originalPatch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async restartPackage(
|
||||
@@ -1103,7 +1161,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
]
|
||||
|
||||
return this.withRevision(patch)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async stopPackage(params: RR.StopPackageReq): Promise<RR.StopPackageRes> {
|
||||
@@ -1129,7 +1189,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
]
|
||||
|
||||
return this.withRevision(patch)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async uninstallPackage(
|
||||
@@ -1155,7 +1217,9 @@ export class MockApiService extends ApiService {
|
||||
},
|
||||
]
|
||||
|
||||
return this.withRevision(patch)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async dryConfigureDependency(
|
||||
@@ -1196,7 +1260,9 @@ export class MockApiService extends ApiService {
|
||||
value: params.domainInfo,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
async setServiceOutboundProxy(
|
||||
@@ -1210,7 +1276,9 @@ export class MockApiService extends ApiService {
|
||||
value: params.proxy,
|
||||
},
|
||||
]
|
||||
return this.withRevision(patch, null)
|
||||
this.mockRevision(patch)
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
private async updateProgress(id: string): Promise<void> {
|
||||
@@ -1337,23 +1405,4 @@ export class MockApiService extends ApiService {
|
||||
}
|
||||
this.mockWsSource$.next(revision)
|
||||
}
|
||||
|
||||
private async withRevision<T>(
|
||||
patch: Operation<unknown>[],
|
||||
response: T | null = null,
|
||||
): Promise<T> {
|
||||
if (!this.sequence) {
|
||||
const { sequence } = this.bootstrapper.init()
|
||||
this.sequence = sequence
|
||||
}
|
||||
|
||||
this.patchStream$.next([
|
||||
{
|
||||
id: ++this.sequence,
|
||||
patch,
|
||||
},
|
||||
])
|
||||
|
||||
return response as T
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
'server-info': {
|
||||
id: 'abcdefgh',
|
||||
version: '0.3.4',
|
||||
version: '0.3.5',
|
||||
country: 'us',
|
||||
ui: {
|
||||
lanHostname: 'adjective-noun.local',
|
||||
@@ -94,11 +94,12 @@ export const mockPatchData: DataModel = {
|
||||
'current-backup': null,
|
||||
updated: false,
|
||||
'update-progress': null,
|
||||
restarting: false,
|
||||
'shutting-down': false,
|
||||
},
|
||||
pubkey: 'npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m',
|
||||
'ca-fingerprint': 'SHA-256: 63 2B 11 99 44 40 17 DF 37 FC C3 DF 0F 3D 15',
|
||||
'system-start-time': new Date(new Date().valueOf() - 360042).toUTCString(),
|
||||
'ntp-synced': false,
|
||||
zram: false,
|
||||
smtp: {
|
||||
server: '',
|
||||
@@ -109,6 +110,7 @@ export const mockPatchData: DataModel = {
|
||||
},
|
||||
'password-hash':
|
||||
'$argon2d$v=19$m=1024,t=1,p=1$YXNkZmFzZGZhc2RmYXNkZg$Ceev1I901G6UwU+hY0sHrFZ56D+o+LNJ',
|
||||
platform: 'x86_64-nonfree',
|
||||
},
|
||||
'package-data': {
|
||||
bitcoind: {
|
||||
|
||||
@@ -1,14 +1,9 @@
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { WorkspaceConfig } from '@start9labs/shared'
|
||||
import {
|
||||
InstalledPackageInfo,
|
||||
InterfaceInfo,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { InterfaceInfo } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
const {
|
||||
packageArch,
|
||||
osArch,
|
||||
gitHash,
|
||||
useMocks,
|
||||
ui: { api, marketplace, mocks },
|
||||
@@ -21,20 +16,20 @@ export class ConfigService {
|
||||
constructor(@Inject(DOCUMENT) private readonly document: Document) {}
|
||||
|
||||
hostname = this.document.location.hostname
|
||||
// includes port
|
||||
host = this.document.location.host
|
||||
// includes ":" (e.g. "http:")
|
||||
protocol = this.document.location.protocol
|
||||
version = require('../../../../../package.json').version as string
|
||||
useMocks = useMocks
|
||||
mocks = mocks
|
||||
packageArch = packageArch
|
||||
osArch = osArch
|
||||
gitHash = gitHash
|
||||
api = api
|
||||
marketplace = marketplace
|
||||
skipStartupAlerts = useMocks && mocks.skipStartupAlerts
|
||||
|
||||
isTor(): boolean {
|
||||
return (
|
||||
this.hostname.endsWith('.onion') || (useMocks && mocks.maskAs === 'tor')
|
||||
)
|
||||
return useMocks ? mocks.maskAs === 'tor' : this.hostname.endsWith('.onion')
|
||||
}
|
||||
|
||||
isLocal(): boolean {
|
||||
@@ -69,6 +64,14 @@ export class ConfigService {
|
||||
)
|
||||
}
|
||||
|
||||
isTorHttp(): boolean {
|
||||
return this.isTor() && !this.isHttps()
|
||||
}
|
||||
|
||||
isLanHttp(): boolean {
|
||||
return !this.isTor() && !this.isLocalhost() && !this.isHttps()
|
||||
}
|
||||
|
||||
isSecure(): boolean {
|
||||
return window.isSecureContext || this.isTor()
|
||||
}
|
||||
@@ -84,6 +87,14 @@ export class ConfigService {
|
||||
? `https://${info.addressInfo.domainInfo.subdomain}${info.addressInfo.domainInfo.domain}`
|
||||
: `https://${info.addressInfo.domainInfo?.domain}`
|
||||
}
|
||||
|
||||
getHost(): string {
|
||||
return this.host
|
||||
}
|
||||
|
||||
private isHttps(): boolean {
|
||||
return useMocks ? mocks.maskAsHttps : this.protocol === 'https:'
|
||||
}
|
||||
}
|
||||
|
||||
export function isValidIpv4(address: string): boolean {
|
||||
|
||||
222
frontend/projects/ui/src/app/services/dep-error.service.ts
Normal file
222
frontend/projects/ui/src/app/services/dep-error.service.ts
Normal file
@@ -0,0 +1,222 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { distinctUntilChanged, map, shareReplay } from 'rxjs/operators'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
DataModel,
|
||||
HealthResult,
|
||||
InstalledPackageInfo,
|
||||
PackageMainStatus,
|
||||
} from './patch-db/data-model'
|
||||
import * as deepEqual from 'fast-deep-equal'
|
||||
import { Manifest } from '@start9labs/marketplace'
|
||||
import { Observable } from 'rxjs'
|
||||
|
||||
export type AllDependencyErrors = Record<string, PkgDependencyErrors>
|
||||
export type PkgDependencyErrors = Record<string, DependencyError | null>
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class DepErrorService {
|
||||
readonly depErrors$: Observable<AllDependencyErrors> = this.patch
|
||||
.watch$('package-data')
|
||||
.pipe(
|
||||
map(pkgs =>
|
||||
Object.keys(pkgs)
|
||||
.map(id => ({
|
||||
id,
|
||||
depth: dependencyDepth(pkgs, id),
|
||||
}))
|
||||
.sort((a, b) => (b.depth > a.depth ? -1 : 1))
|
||||
.reduce(
|
||||
(errors, { id }): AllDependencyErrors => ({
|
||||
...errors,
|
||||
[id]: this.getDepErrors(pkgs, id, errors),
|
||||
}),
|
||||
{} as AllDependencyErrors,
|
||||
),
|
||||
),
|
||||
distinctUntilChanged(deepEqual),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly emver: Emver,
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
) {}
|
||||
|
||||
getPkgDepErrors$(pkgId: string): Observable<PkgDependencyErrors> {
|
||||
return this.depErrors$.pipe(
|
||||
map(depErrors => depErrors[pkgId]),
|
||||
distinctUntilChanged(deepEqual),
|
||||
)
|
||||
}
|
||||
|
||||
private getDepErrors(
|
||||
pkgs: DataModel['package-data'],
|
||||
pkgId: string,
|
||||
outerErrors: AllDependencyErrors,
|
||||
): PkgDependencyErrors {
|
||||
const pkgInstalled = pkgs[pkgId].installed
|
||||
|
||||
if (!pkgInstalled) return {}
|
||||
|
||||
return currentDeps(pkgs, pkgId).reduce(
|
||||
(innerErrors, depId): PkgDependencyErrors => ({
|
||||
...innerErrors,
|
||||
[depId]: this.getDepError(
|
||||
pkgs,
|
||||
pkgInstalled,
|
||||
pkgs[pkgId].manifest,
|
||||
depId,
|
||||
outerErrors,
|
||||
),
|
||||
}),
|
||||
{} as PkgDependencyErrors,
|
||||
)
|
||||
}
|
||||
|
||||
private getDepError(
|
||||
pkgs: DataModel['package-data'],
|
||||
pkgInstalled: InstalledPackageInfo,
|
||||
pkgManifest: Manifest,
|
||||
depId: string,
|
||||
outerErrors: AllDependencyErrors,
|
||||
): DependencyError | null {
|
||||
const depInstalled = pkgs[depId]?.installed
|
||||
const depManifest = pkgs[depId]?.manifest
|
||||
|
||||
// not installed
|
||||
if (!depInstalled) {
|
||||
return {
|
||||
type: DependencyErrorType.NotInstalled,
|
||||
}
|
||||
}
|
||||
|
||||
// incorrect version
|
||||
if (
|
||||
!this.emver.satisfies(
|
||||
depManifest.version,
|
||||
pkgManifest.dependencies[depId].version,
|
||||
)
|
||||
) {
|
||||
return {
|
||||
type: DependencyErrorType.IncorrectVersion,
|
||||
expected: pkgManifest.dependencies[depId].version,
|
||||
received: depManifest.version,
|
||||
}
|
||||
}
|
||||
|
||||
// invalid config
|
||||
if (
|
||||
Object.values(pkgInstalled.status['dependency-config-errors']).some(
|
||||
err => !!err,
|
||||
)
|
||||
) {
|
||||
return {
|
||||
type: DependencyErrorType.ConfigUnsatisfied,
|
||||
}
|
||||
}
|
||||
|
||||
const depStatus = depInstalled.status.main.status
|
||||
|
||||
// not running
|
||||
if (
|
||||
depStatus !== PackageMainStatus.Running &&
|
||||
depStatus !== PackageMainStatus.Starting
|
||||
) {
|
||||
return {
|
||||
type: DependencyErrorType.NotRunning,
|
||||
}
|
||||
}
|
||||
|
||||
// health check failure
|
||||
if (depStatus === PackageMainStatus.Running) {
|
||||
for (let id of pkgInstalled['current-dependencies'][depId][
|
||||
'health-checks'
|
||||
]) {
|
||||
if (
|
||||
depInstalled.status.main.health[id]?.result !== HealthResult.Success
|
||||
) {
|
||||
return {
|
||||
type: DependencyErrorType.HealthChecksFailed,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// transitive
|
||||
const transitiveError = currentDeps(pkgs, depId).some(transitiveId =>
|
||||
Object.values(outerErrors[transitiveId]).some(err => !!err),
|
||||
)
|
||||
|
||||
if (transitiveError) {
|
||||
return {
|
||||
type: DependencyErrorType.Transitive,
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function currentDeps(pkgs: DataModel['package-data'], id: string): string[] {
|
||||
return Object.keys(
|
||||
pkgs[id]?.installed?.['current-dependencies'] || {},
|
||||
).filter(depId => depId !== id)
|
||||
}
|
||||
|
||||
function dependencyDepth(
|
||||
pkgs: DataModel['package-data'],
|
||||
id: string,
|
||||
depth = 0,
|
||||
): number {
|
||||
return currentDeps(pkgs, id).reduce(
|
||||
(prev, depId) => dependencyDepth(pkgs, depId, prev + 1),
|
||||
depth,
|
||||
)
|
||||
}
|
||||
|
||||
export type DependencyError =
|
||||
| DependencyErrorNotInstalled
|
||||
| DependencyErrorNotRunning
|
||||
| DependencyErrorIncorrectVersion
|
||||
| DependencyErrorConfigUnsatisfied
|
||||
| DependencyErrorHealthChecksFailed
|
||||
| DependencyErrorTransitive
|
||||
|
||||
export enum DependencyErrorType {
|
||||
NotInstalled = 'notInstalled',
|
||||
NotRunning = 'notRunning',
|
||||
IncorrectVersion = 'incorrectVersion',
|
||||
ConfigUnsatisfied = 'configUnsatisfied',
|
||||
HealthChecksFailed = 'healthChecksFailed',
|
||||
Transitive = 'transitive',
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotInstalled {
|
||||
type: DependencyErrorType.NotInstalled
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotRunning {
|
||||
type: DependencyErrorType.NotRunning
|
||||
}
|
||||
|
||||
export interface DependencyErrorIncorrectVersion {
|
||||
type: DependencyErrorType.IncorrectVersion
|
||||
expected: string // version range
|
||||
received: string // version
|
||||
}
|
||||
|
||||
export interface DependencyErrorConfigUnsatisfied {
|
||||
type: DependencyErrorType.ConfigUnsatisfied
|
||||
}
|
||||
|
||||
export interface DependencyErrorHealthChecksFailed {
|
||||
type: DependencyErrorType.HealthChecksFailed
|
||||
}
|
||||
|
||||
export interface DependencyErrorTransitive {
|
||||
type: DependencyErrorType.Transitive
|
||||
}
|
||||
@@ -75,7 +75,7 @@ export class MarketplaceService implements AbstractMarketplaceService {
|
||||
map(({ 'selected-url': url, 'known-hosts': hosts }) =>
|
||||
toStoreIdentity(url, hosts[url]),
|
||||
),
|
||||
shareReplay(1),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
|
||||
private readonly marketplace$ = this.knownHosts$.pipe(
|
||||
@@ -103,7 +103,7 @@ export class MarketplaceService implements AbstractMarketplaceService {
|
||||
},
|
||||
{},
|
||||
),
|
||||
shareReplay(1),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
|
||||
private readonly filteredMarketplace$ = combineLatest([
|
||||
|
||||
@@ -4,6 +4,8 @@ import { Manifest } from '@start9labs/marketplace'
|
||||
import { BackupJob } from '../api/api.types'
|
||||
import { customSmtp } from '@start9labs/start-sdk/lib/config/configConstants'
|
||||
import { NetworkInterfaceType } from '@start9labs/start-sdk/lib/util/utils'
|
||||
import { DependencyInfo } from 'src/app/apps/portal/routes/service/types/dependency-info'
|
||||
import { PackageStatus } from '../pkg-status-rendering.service'
|
||||
|
||||
export interface DataModel {
|
||||
'server-info': ServerInfo
|
||||
@@ -64,10 +66,11 @@ export interface ServerInfo {
|
||||
'eos-version-compat': string
|
||||
pubkey: string
|
||||
'ca-fingerprint': string
|
||||
'system-start-time': string
|
||||
'ntp-synced': boolean
|
||||
zram: boolean
|
||||
smtp: typeof customSmtp.validator._TYPE
|
||||
'password-hash': string
|
||||
platform: string
|
||||
}
|
||||
|
||||
export type NetworkInfo = {
|
||||
@@ -156,9 +159,16 @@ export interface ServerStatusInfo {
|
||||
}
|
||||
updated: boolean
|
||||
'update-progress': { size: number | null; downloaded: number } | null
|
||||
restarting: boolean
|
||||
'shutting-down': boolean
|
||||
}
|
||||
|
||||
export enum ServerStatus {
|
||||
Running = 'running',
|
||||
Updated = 'updated',
|
||||
BackingUp = 'backing-up',
|
||||
}
|
||||
|
||||
export interface PackageDataEntry {
|
||||
state: PackageState
|
||||
manifest: Manifest
|
||||
@@ -168,6 +178,12 @@ export interface PackageDataEntry {
|
||||
'install-progress'?: InstallProgress // when: installing, updating, restoring
|
||||
}
|
||||
|
||||
export type PackagePlus = {
|
||||
pkg: PackageDataEntry
|
||||
status: PackageStatus
|
||||
dependencies: DependencyInfo[]
|
||||
}
|
||||
|
||||
// export type PackageDataEntry =
|
||||
// | PackageDataEntryInstalled
|
||||
// | PackageDataEntryNeedsUpdate
|
||||
@@ -224,6 +240,7 @@ export interface InstalledPackageInfo {
|
||||
'last-backup': string | null
|
||||
'installed-at': string
|
||||
'current-dependencies': Record<string, CurrentDependencyInfo>
|
||||
'current-dependents': Record<string, CurrentDependencyInfo>
|
||||
'dependency-info': Record<string, { title: string; icon: Url }>
|
||||
interfaceInfo: Record<string, InterfaceInfo>
|
||||
'marketplace-url': string | null
|
||||
@@ -262,7 +279,7 @@ export interface Action {
|
||||
export interface Status {
|
||||
configured: boolean
|
||||
main: MainStatus
|
||||
'dependency-errors': { [id: string]: DependencyError | null }
|
||||
'dependency-config-errors': { [id: string]: string | null }
|
||||
}
|
||||
|
||||
export type MainStatus =
|
||||
@@ -354,51 +371,6 @@ export interface HealthCheckResultFailure {
|
||||
error: string
|
||||
}
|
||||
|
||||
export type DependencyError =
|
||||
| DependencyErrorNotInstalled
|
||||
| DependencyErrorNotRunning
|
||||
| DependencyErrorIncorrectVersion
|
||||
| DependencyErrorConfigUnsatisfied
|
||||
| DependencyErrorHealthChecksFailed
|
||||
| DependencyErrorTransitive
|
||||
|
||||
export enum DependencyErrorType {
|
||||
NotInstalled = 'not-installed',
|
||||
NotRunning = 'not-running',
|
||||
IncorrectVersion = 'incorrect-version',
|
||||
ConfigUnsatisfied = 'config-unsatisfied',
|
||||
HealthChecksFailed = 'health-checks-failed',
|
||||
Transitive = 'transitive',
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotInstalled {
|
||||
type: DependencyErrorType.NotInstalled
|
||||
}
|
||||
|
||||
export interface DependencyErrorNotRunning {
|
||||
type: DependencyErrorType.NotRunning
|
||||
}
|
||||
|
||||
export interface DependencyErrorIncorrectVersion {
|
||||
type: DependencyErrorType.IncorrectVersion
|
||||
expected: string // version range
|
||||
received: string // version
|
||||
}
|
||||
|
||||
export interface DependencyErrorConfigUnsatisfied {
|
||||
type: DependencyErrorType.ConfigUnsatisfied
|
||||
error: string
|
||||
}
|
||||
|
||||
export interface DependencyErrorHealthChecksFailed {
|
||||
type: DependencyErrorType.HealthChecksFailed
|
||||
check: HealthCheckResult
|
||||
}
|
||||
|
||||
export interface DependencyErrorTransitive {
|
||||
type: DependencyErrorType.Transitive
|
||||
}
|
||||
|
||||
export interface InstallProgress {
|
||||
readonly size: number | null
|
||||
readonly downloaded: number
|
||||
|
||||
@@ -11,13 +11,13 @@ import {
|
||||
EMPTY,
|
||||
from,
|
||||
interval,
|
||||
merge,
|
||||
Observable,
|
||||
} from 'rxjs'
|
||||
import { DataModel } from './data-model'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { ConnectionService } from '../connection.service'
|
||||
import { ApiService } from '../api/embassy-api.service'
|
||||
import { ConfigService } from '../config.service'
|
||||
|
||||
export const PATCH_SOURCE = new InjectionToken<Observable<Update<DataModel>[]>>(
|
||||
'',
|
||||
@@ -31,6 +31,9 @@ export function sourceFactory(
|
||||
const api = injector.get(ApiService)
|
||||
const authService = injector.get(AuthService)
|
||||
const connectionService = injector.get(ConnectionService)
|
||||
const configService = injector.get(ConfigService)
|
||||
const isTor = configService.isTor()
|
||||
const timeout = isTor ? 16000 : 4000
|
||||
|
||||
const websocket$ = api.openPatchWebsocket$().pipe(
|
||||
bufferTime(250),
|
||||
@@ -38,9 +41,11 @@ export function sourceFactory(
|
||||
catchError((_, watch$) => {
|
||||
connectionService.websocketConnected$.next(false)
|
||||
|
||||
return interval(4000).pipe(
|
||||
return interval(timeout).pipe(
|
||||
switchMap(() =>
|
||||
from(api.echo({ message: 'ping' })).pipe(catchError(() => EMPTY)),
|
||||
from(api.echo({ message: 'ping', timeout })).pipe(
|
||||
catchError(() => EMPTY),
|
||||
),
|
||||
),
|
||||
take(1),
|
||||
switchMap(() => watch$),
|
||||
@@ -50,9 +55,7 @@ export function sourceFactory(
|
||||
)
|
||||
|
||||
return authService.isVerified$.pipe(
|
||||
switchMap(verified =>
|
||||
verified ? merge(websocket$, api.patchStream$) : EMPTY,
|
||||
),
|
||||
switchMap(verified => (verified ? websocket$ : EMPTY)),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -12,13 +12,9 @@ import { LocalStorageBootstrap } from './patch-db/local-storage-bootstrap'
|
||||
export class PatchMonitorService extends Observable<any> {
|
||||
// @TODO not happy with Observable<void>
|
||||
private readonly stream$ = this.authService.isVerified$.pipe(
|
||||
tap(verified => {
|
||||
if (verified) {
|
||||
this.patch.start(this.bootstrapper)
|
||||
} else {
|
||||
this.patch.stop()
|
||||
}
|
||||
}),
|
||||
tap(verified =>
|
||||
verified ? this.patch.start(this.bootstrapper) : this.patch.stop(),
|
||||
),
|
||||
)
|
||||
|
||||
constructor(
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { isEmptyObject } from '@start9labs/shared'
|
||||
import {
|
||||
InstalledPackageInfo,
|
||||
PackageDataEntry,
|
||||
PackageMainStatus,
|
||||
PackagePlus,
|
||||
PackageState,
|
||||
Status,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { PkgDependencyErrors } from './dep-error.service'
|
||||
|
||||
export interface PackageStatus {
|
||||
primary: PrimaryStatus | PackageState | PackageMainStatus
|
||||
@@ -13,14 +13,17 @@ export interface PackageStatus {
|
||||
health: HealthStatus | null
|
||||
}
|
||||
|
||||
export function renderPkgStatus(pkg: PackageDataEntry): PackageStatus {
|
||||
export function renderPkgStatus(
|
||||
pkg: PackageDataEntry,
|
||||
depErrors: PkgDependencyErrors,
|
||||
): PackageStatus {
|
||||
let primary: PrimaryStatus | PackageState | PackageMainStatus
|
||||
let dependency: DependencyStatus | null = null
|
||||
let health: HealthStatus | null = null
|
||||
|
||||
if (pkg.state === PackageState.Installed && pkg.installed) {
|
||||
primary = getPrimaryStatus(pkg.installed.status)
|
||||
dependency = getDependencyStatus(pkg.installed)
|
||||
dependency = getDependencyStatus(depErrors)
|
||||
health = getHealthStatus(pkg.installed.status)
|
||||
} else {
|
||||
primary = pkg.state
|
||||
@@ -37,15 +40,10 @@ function getPrimaryStatus(status: Status): PrimaryStatus | PackageMainStatus {
|
||||
}
|
||||
}
|
||||
|
||||
function getDependencyStatus(
|
||||
installed: InstalledPackageInfo,
|
||||
): DependencyStatus | null {
|
||||
if (isEmptyObject(installed['current-dependencies'])) return null
|
||||
|
||||
const depErrors = installed.status['dependency-errors']
|
||||
const depIds = Object.keys(depErrors).filter(key => !!depErrors[key])
|
||||
|
||||
return depIds.length ? DependencyStatus.Warning : DependencyStatus.Satisfied
|
||||
function getDependencyStatus(depErrors: PkgDependencyErrors): DependencyStatus {
|
||||
return Object.values(depErrors).some(err => !!err)
|
||||
? DependencyStatus.Warning
|
||||
: DependencyStatus.Satisfied
|
||||
}
|
||||
|
||||
function getHealthStatus(status: Status): HealthStatus | null {
|
||||
|
||||
@@ -1,85 +1,59 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { map, shareReplay, startWith, switchMap } from 'rxjs/operators'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import {
|
||||
map,
|
||||
startWith,
|
||||
switchMap,
|
||||
combineLatest,
|
||||
from,
|
||||
Observable,
|
||||
timer,
|
||||
} from 'rxjs'
|
||||
import { DataModel } from './patch-db/data-model'
|
||||
import { ApiService } from './api/embassy-api.service'
|
||||
|
||||
export interface TimeInfo {
|
||||
systemStartTime: number
|
||||
systemCurrentTime: number
|
||||
systemUptime: {
|
||||
days: number
|
||||
hours: number
|
||||
minutes: number
|
||||
seconds: number
|
||||
}
|
||||
}
|
||||
import { combineLatest, interval, of } from 'rxjs'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class TimeService {
|
||||
private readonly systemStartTime$ = this.patch
|
||||
.watch$('server-info', 'system-start-time')
|
||||
.pipe(map(startTime => new Date(startTime).valueOf()))
|
||||
private readonly time$ = of({}).pipe(
|
||||
switchMap(() => this.apiService.getSystemTime({})),
|
||||
switchMap(({ now, uptime }) => {
|
||||
const current = new Date(now).valueOf()
|
||||
return interval(1000).pipe(
|
||||
map(index => {
|
||||
const incremented = index + 1
|
||||
return {
|
||||
now: current + 1000 * incremented,
|
||||
uptime: uptime + incremented,
|
||||
}
|
||||
}),
|
||||
startWith({
|
||||
now: current,
|
||||
uptime,
|
||||
}),
|
||||
)
|
||||
}),
|
||||
shareReplay({ bufferSize: 1, refCount: true }),
|
||||
)
|
||||
|
||||
readonly now$ = combineLatest([
|
||||
this.time$,
|
||||
this.patch.watch$('server-info', 'ntp-synced'),
|
||||
]).pipe(
|
||||
map(([time, synced]) => ({
|
||||
value: time.now,
|
||||
synced,
|
||||
})),
|
||||
)
|
||||
|
||||
readonly uptime$ = this.time$.pipe(
|
||||
map(({ uptime }) => {
|
||||
const days = Math.floor(uptime / (24 * 60 * 60))
|
||||
const daysSec = uptime % (24 * 60 * 60)
|
||||
const hours = Math.floor(daysSec / (60 * 60))
|
||||
const hoursSec = uptime % (60 * 60)
|
||||
const minutes = Math.floor(hoursSec / 60)
|
||||
const seconds = uptime % 60
|
||||
return { days, hours, minutes, seconds }
|
||||
}),
|
||||
)
|
||||
|
||||
constructor(
|
||||
private readonly patch: PatchDB<DataModel>,
|
||||
private readonly apiService: ApiService,
|
||||
) {}
|
||||
|
||||
getTimeInfo$(): Observable<TimeInfo> {
|
||||
return combineLatest([
|
||||
this.systemStartTime$.pipe(),
|
||||
this.getSystemCurrentTime$(),
|
||||
]).pipe(
|
||||
map(([systemStartTime, systemCurrentTime]) => ({
|
||||
systemStartTime,
|
||||
systemCurrentTime,
|
||||
systemUptime: this.getSystemUptime(systemStartTime, systemCurrentTime),
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
private getSystemCurrentTime$() {
|
||||
return from(this.apiService.getSystemTime({})).pipe(
|
||||
switchMap(utcStr => {
|
||||
const dateObj = new Date(utcStr)
|
||||
const current = dateObj.valueOf()
|
||||
return timer(0, 1000).pipe(
|
||||
map(index => {
|
||||
const incremented = index + 1
|
||||
const msToAdd = 1000 * incremented
|
||||
return current + msToAdd
|
||||
}),
|
||||
startWith(current),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
private getSystemUptime(systemStartTime: number, systemCurrentTime: number) {
|
||||
const ms = systemCurrentTime - systemStartTime
|
||||
|
||||
const days = Math.floor(ms / (24 * 60 * 60 * 1000))
|
||||
const daysms = ms % (24 * 60 * 60 * 1000)
|
||||
|
||||
const hours = Math.floor(daysms / (60 * 60 * 1000))
|
||||
const hoursms = ms % (60 * 60 * 1000)
|
||||
|
||||
const minutes = Math.floor(hoursms / (60 * 1000))
|
||||
const minutesms = ms % (60 * 1000)
|
||||
|
||||
const seconds = Math.floor(minutesms / 1000)
|
||||
|
||||
return { days, hours, minutes, seconds }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
import { Inject, Injectable } from '@angular/core'
|
||||
import { DOCUMENT } from '@angular/common'
|
||||
import { InstalledPackageInfo } from 'src/app/services/patch-db/data-model'
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class UiLauncherService {
|
||||
constructor(@Inject(DOCUMENT) private readonly document: Document) {}
|
||||
|
||||
launch(addressInfo: InstalledPackageInfo['address-info']): void {
|
||||
const UIs = Object.values(addressInfo)
|
||||
.filter(info => info.ui)
|
||||
.map(info => ({
|
||||
name: info.name,
|
||||
addresses: info.addresses,
|
||||
}))
|
||||
|
||||
if (UIs.length === 1 && UIs[0].addresses.length === 1) {
|
||||
this.document.defaultView?.open(
|
||||
UIs[0].addresses[0],
|
||||
'_blank',
|
||||
'noreferrer',
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
17
frontend/projects/ui/src/app/util/dry-update.ts
Normal file
17
frontend/projects/ui/src/app/util/dry-update.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Emver } from '@start9labs/shared'
|
||||
import { DataModel } from '../services/patch-db/data-model'
|
||||
|
||||
export function dryUpdate(
|
||||
{ id, version }: { id: string; version: string },
|
||||
pkgs: DataModel['package-data'],
|
||||
emver: Emver,
|
||||
): string[] {
|
||||
return Object.values(pkgs)
|
||||
.filter(
|
||||
pkg =>
|
||||
Object.keys(pkg.installed?.['current-dependencies'] || {}).some(
|
||||
pkgId => pkgId === id,
|
||||
) && !emver.satisfies(version, pkg.manifest.dependencies[id].version),
|
||||
)
|
||||
.map(pkg => pkg.manifest.title)
|
||||
}
|
||||
@@ -14,6 +14,6 @@ export async function getPackage(
|
||||
|
||||
export async function getAllPackages(
|
||||
patch: PatchDB<DataModel>,
|
||||
): Promise<Record<string, PackageDataEntry>> {
|
||||
): Promise<DataModel['package-data']> {
|
||||
return firstValueFrom(patch.watch$('package-data'))
|
||||
}
|
||||
|
||||
@@ -8,9 +8,13 @@ import {
|
||||
} from '../services/pkg-status-rendering.service'
|
||||
import { PkgInfo } from '../types/pkg-info'
|
||||
import { packageLoadingProgress } from './package-loading-progress'
|
||||
import { PkgDependencyErrors } from '../services/dep-error.service'
|
||||
|
||||
export function getPackageInfo(entry: PackageDataEntry): PkgInfo {
|
||||
const statuses = renderPkgStatus(entry)
|
||||
export function getPackageInfo(
|
||||
entry: PackageDataEntry,
|
||||
depErrors: PkgDependencyErrors,
|
||||
): PkgInfo {
|
||||
const statuses = renderPkgStatus(entry, depErrors)
|
||||
const primaryRendering = PrimaryRendering[statuses.primary]
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { DataModel } from '../services/patch-db/data-model'
|
||||
import { getAllPackages } from './get-package-data'
|
||||
import { PackageDataEntry } from '../services/patch-db/data-model'
|
||||
|
||||
export async function hasCurrentDeps(
|
||||
patch: PatchDB<DataModel>,
|
||||
id: string,
|
||||
): Promise<boolean> {
|
||||
const pkgs = await getAllPackages(patch)
|
||||
return !!Object.keys(pkgs)
|
||||
.filter(pkgId => pkgId !== id)
|
||||
.find(pkgId => pkgs[pkgId].installed?.['current-dependencies'][pkgId])
|
||||
export function hasCurrentDeps(pkg: PackageDataEntry): boolean {
|
||||
return !!Object.keys(pkg.installed?.['current-dependents'] || {}).filter(
|
||||
depId => depId !== pkg.manifest.id,
|
||||
).length
|
||||
}
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
"background_color": "#1e1e1e",
|
||||
"display": "standalone",
|
||||
"scope": ".",
|
||||
"start_url": "/?version=0344",
|
||||
"id": "/?version=0344",
|
||||
"start_url": "/?version=035",
|
||||
"id": "/?version=035",
|
||||
"icons": [
|
||||
{
|
||||
"src": "assets/img/icon_pwa.png",
|
||||
"src": "assets/img/icon.png",
|
||||
"sizes": "256x256",
|
||||
"type": "image/png",
|
||||
"purpose": "any"
|
||||
|
||||
@@ -53,8 +53,7 @@
|
||||
*/
|
||||
|
||||
(window as any).global = window
|
||||
global.Buffer = global.Buffer || require('buffer').Buffer;
|
||||
(window as any).process = { env: { DEBUG: undefined }, browser: true }
|
||||
; (window as any).process = { env: { DEBUG: undefined }, browser: true }
|
||||
|
||||
import './zone-flags'
|
||||
|
||||
@@ -62,8 +61,7 @@ import './zone-flags'
|
||||
* Zone JS is required by default for Angular itself.
|
||||
*/
|
||||
|
||||
import 'zone.js/dist/zone' // Included with Angular CLI.
|
||||
|
||||
import 'zone.js/dist/zone' // Included with Angular CLI.
|
||||
|
||||
/***************************************************************************************************
|
||||
* APPLICATION IMPORTS
|
||||
|
||||
@@ -474,3 +474,11 @@ button.g-action {
|
||||
@include scrollbar-hidden;
|
||||
overflow: auto !important;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
svg:not(:root) {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user