Action Request updates + misc fixes (#2818)

* fix web manifest format error

* fix setting optional dependencies

* rework dependency actions to be nested

* fix styling

* fix styles

* combine action requests into same component

* only display actions header if they exist

* fix storing polyfill dependencies

* fix styling and button propagation

* fixes for setting polyfill dependencies

* revert to test

* revert required deps setting logic

* add logs and adjust logic

* test

* fix deps logic when changing config

* remove logs; deps working as expected
This commit is contained in:
Lucy
2025-02-08 20:11:26 -05:00
committed by GitHub
parent 4e22f13007
commit 3047dae703
14 changed files with 326 additions and 137 deletions

View File

@@ -64,7 +64,7 @@
}, },
"../sdk/dist": { "../sdk/dist": {
"name": "@start9labs/start-sdk", "name": "@start9labs/start-sdk",
"version": "0.3.6-alpha8", "version": "0.3.6-beta.4",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@iarna/toml": "^2.2.5", "@iarna/toml": "^2.2.5",
@@ -72,8 +72,8 @@
"@noble/hashes": "^1.4.0", "@noble/hashes": "^1.4.0",
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"mime": "^4.0.3", "mime-types": "^2.1.35",
"ts-matches": "^5.5.1", "ts-matches": "^6.2.1",
"yaml": "^2.2.2" "yaml": "^2.2.2"
}, },
"devDependencies": { "devDependencies": {
@@ -6799,11 +6799,11 @@
"isomorphic-fetch": "^3.0.0", "isomorphic-fetch": "^3.0.0",
"jest": "^29.4.3", "jest": "^29.4.3",
"lodash.merge": "^4.6.2", "lodash.merge": "^4.6.2",
"mime": "^4.0.3", "mime-types": "^2.1.35",
"peggy": "^3.0.2", "peggy": "^3.0.2",
"prettier": "^3.2.5", "prettier": "^3.2.5",
"ts-jest": "^29.0.5", "ts-jest": "^29.0.5",
"ts-matches": "^5.5.1", "ts-matches": "^6.2.1",
"ts-node": "^10.9.1", "ts-node": "^10.9.1",
"ts-pegjs": "^4.2.1", "ts-pegjs": "^4.2.1",
"tsx": "^4.7.1", "tsx": "^4.7.1",

View File

@@ -51,6 +51,7 @@ function todo(): never {
const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json" const MANIFEST_LOCATION = "/usr/lib/startos/package/embassyManifest.json"
export const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js" export const EMBASSY_JS_LOCATION = "/usr/lib/startos/package/embassy.js"
const EMBASSY_POINTER_PATH_PREFIX = "/embassyConfig" as utils.StorePath const EMBASSY_POINTER_PATH_PREFIX = "/embassyConfig" as utils.StorePath
const EMBASSY_DEPENDS_ON_PATH_PREFIX = "/embassyDependsOn" as utils.StorePath
const matchResult = object({ const matchResult = object({
result: any, result: any,
@@ -314,7 +315,7 @@ export class SystemForEmbassy implements System {
) )
.catch(() => []), .catch(() => []),
) )
await this.setDependencies(effects, oldDeps) await this.setDependencies(effects, oldDeps, false)
} }
async exit(): Promise<void> { async exit(): Promise<void> {
@@ -664,7 +665,7 @@ export class SystemForEmbassy implements System {
), ),
) )
const dependsOn = answer["depends-on"] ?? answer.dependsOn ?? {} const dependsOn = answer["depends-on"] ?? answer.dependsOn ?? {}
await this.setDependencies(effects, dependsOn) await this.setDependencies(effects, dependsOn, true)
return return
} else if (setConfigValue.type === "script") { } else if (setConfigValue.type === "script") {
const moduleCode = await this.moduleCode const moduleCode = await this.moduleCode
@@ -687,48 +688,47 @@ export class SystemForEmbassy implements System {
}), }),
) )
const dependsOn = answer["depends-on"] ?? answer.dependsOn ?? {} const dependsOn = answer["depends-on"] ?? answer.dependsOn ?? {}
await this.setDependencies(effects, dependsOn) await this.setDependencies(effects, dependsOn, true)
return return
} }
} }
private async setDependencies( private async setDependencies(
effects: Effects, effects: Effects,
rawDepends: { [x: string]: readonly string[] }, rawDepends: { [x: string]: readonly string[] },
configuring: boolean,
) { ) {
const dependsOn: Record<string, readonly string[] | null> = { const storedDependsOn = (await effects.store.get({
packageId: this.manifest.id,
path: EMBASSY_DEPENDS_ON_PATH_PREFIX,
})) as Record<string, readonly string[]>
const requiredDeps = {
...Object.fromEntries( ...Object.fromEntries(
Object.entries(this.manifest.dependencies || {})?.map((x) => [ Object.entries(this.manifest.dependencies || {})
x[0], ?.filter((x) => x[1].requirement.type === "required")
null, .map((x) => [x[0], []]) || [],
]) || [],
), ),
...rawDepends,
} }
const dependsOn: Record<string, readonly string[]> = configuring
? {
...requiredDeps,
...rawDepends,
}
: storedDependsOn
? storedDependsOn
: requiredDeps
await effects.store.set({
path: EMBASSY_DEPENDS_ON_PATH_PREFIX,
value: dependsOn,
})
await effects.setDependencies({ await effects.setDependencies({
dependencies: Object.entries(dependsOn).flatMap( dependencies: Object.entries(dependsOn).flatMap(
([key, value]): T.Dependencies => { ([key, value]): T.Dependencies => {
const dependency = this.manifest.dependencies?.[key] const dependency = this.manifest.dependencies?.[key]
if (!dependency) return [] if (!dependency) return []
if (value == null) {
const versionRange = dependency.version
if (dependency.requirement.type === "required") {
return [
{
id: key,
versionRange,
kind: "running",
healthChecks: [],
},
]
}
return [
{
kind: "exists",
id: key,
versionRange,
},
]
}
const versionRange = dependency.version const versionRange = dependency.version
const kind = "running" const kind = "running"
return [ return [

View File

@@ -11,7 +11,7 @@ new RpcListener(getDependencies)
/** /**
So, this is going to be sent into a running comtainer along with any of the other node modules that are going to be needed and used. So, this is going to be sent into a running container along with any of the other node modules that are going to be needed and used.
Once the container is started, we will go into a loading/ await state. Once the container is started, we will go into a loading/ await state.
This is the init system, and it will always be running, and it will be waiting for a command to be sent to it. This is the init system, and it will always be running, and it will be waiting for a command to be sent to it.
@@ -38,5 +38,5 @@ There are
/** /**
TODO: TODO:
Should I seperate those adapter in/out? Should I separate those adapter in/out?
*/ */

View File

@@ -35,6 +35,9 @@
<app-show-dependencies <app-show-dependencies
*ngIf="pkgPlus.dependencies.length" *ngIf="pkgPlus.dependencies.length"
[dependencies]="pkgPlus.dependencies" [dependencies]="pkgPlus.dependencies"
[allPkgs]="pkgPlus.allPkgs"
[pkg]="pkg"
[manifest]="pkgPlus.manifest"
></app-show-dependencies> ></app-show-dependencies>
<!-- ** menu ** --> <!-- ** menu ** -->
<app-show-menu [buttons]="pkg | toButtons"></app-show-menu> <app-show-menu [buttons]="pkg | toButtons"></app-show-menu>

View File

@@ -143,7 +143,7 @@ export class AppShowPage {
fixText = 'Update' fixText = 'Update'
fixAction = () => this.installDep(pkg, manifest, depId) fixAction = () => this.installDep(pkg, manifest, depId)
} else if (depError.type === 'actionRequired') { } else if (depError.type === 'actionRequired') {
errorText = 'Action Required (see above)' errorText = 'Action Required (see below)'
} else if (depError.type === 'notRunning') { } else if (depError.type === 'notRunning') {
errorText = 'Not running' errorText = 'Not running'
fixText = 'Start' fixText = 'Start'

View File

@@ -1,45 +1,42 @@
<ng-container *ngIf="actionRequests.critical.length"> <ng-container *ngIf="actionRequests[pkgId]?.length">
<ion-item-divider>Required Actions</ion-item-divider> <ion-item-divider
<ion-item *ngIf="!dep"
*ngFor="let request of actionRequests.critical" style="--background: unset; z-index: 11; position: relative"
button
(click)="handleAction(request)"
> >
<ion-icon slot="start" name="warning-outline" color="warning"></ion-icon> Action Requests
<ion-label> </ion-item-divider>
<h2 class="highlighted">{{ request.actionName }}</h2> <div class="indent">
<p *ngIf="request.dependency" class="dependency"> <ion-item
<span class="light">Service:</span> class="line"
<img [src]="request.dependency.icon" alt="" /> lines="none"
{{ request.dependency.title }} *ngFor="let request of actionRequests[pkgId]"
</p> button
<p> (click)="handleAction(request, $event)"
<span class="light">Reason:</span> >
{{ request.reason || 'no reason provided' }} <ion-icon
</p> slot="start"
</ion-label> [name]="
</ion-item> request.severity === 'critical' ? 'warning-outline' : 'play-outline'
</ng-container> "
[color]="request.severity === 'critical' ? 'warning' : 'dark'"
<ng-container *ngIf="actionRequests.important.length"> ></ion-icon>
<ion-item-divider>Requested Actions</ion-item-divider> <ion-label>
<ion-item <h2 class="highlighted">{{ request.actionName }}</h2>
*ngFor="let request of actionRequests.important" <p>
button {{ request.reason || 'no reason provided' }} |
(click)="handleAction(request)" <span
> class="severity"
<ion-icon slot="start" name="play-outline" color="warning"></ion-icon> [ngStyle]="{
<ion-label> color:
<h2 class="highlighted">{{ request.actionName }}</h2> request.severity === 'critical'
<p *ngIf="request.dependency" class="dependency"> ? 'var(--ion-color-warning)'
<span class="light">Service:</span> : 'var(--ion-color-dark)'
<img [src]="request.dependency.icon" alt="" /> }"
{{ request.dependency.title }} >
</p> {{ request.severity === 'critical' ? 'Required' : 'Requested' }}
<p> </span>
<span class="light">Reason:</span> </p>
{{ request.reason || 'no reason provided' }} </ion-label>
</p> </ion-item>
</ion-label> </div>
</ion-item>
</ng-container> </ng-container>

View File

@@ -1,5 +1,5 @@
.light { ion-icon {
color: var(--ion-color-dark); margin-right: 32px;
} }
.highlighted { .highlighted {
@@ -7,10 +7,33 @@
font-weight: bold; font-weight: bold;
} }
.dependency { .severity {
display: inline-flex; font-variant-caps: all-small-caps;
img { font-weight: bold;
max-width: 16px; letter-spacing: 0.2px;
margin: 0 2px 0 5px; font-size: 16px;
}
.line {
&:after {
content: '';
display: block;
border-left: 1px solid var(--border-color);
border-bottom: 1px solid var(--border-color);
height: 100%;
width: 24px;
position: absolute;
left: -20px;
top: -33px;
} }
}
.indent {
margin-left: 41px
}
:host ::ng-deep ion-item {
display: table-row;
width: fit-content;
} }

View File

@@ -1,6 +1,7 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { T } from '@start9labs/start-sdk' import { T } from '@start9labs/start-sdk'
import { ActionService } from 'src/app/services/action.service' import { ActionService } from 'src/app/services/action.service'
import { DependencyInfo } from 'src/app/pages/apps-routes/app-show/app-show.page'
import { getDepDetails } from 'src/app/util/dep-info' import { getDepDetails } from 'src/app/util/dep-info'
@Component({ @Component({
@@ -19,22 +20,21 @@ export class AppShowActionRequestsComponent {
@Input() @Input()
manifest!: T.Manifest manifest!: T.Manifest
get actionRequests() { @Input()
const critical: (T.ActionRequest & { dep?: DependencyInfo
actionName: string
dependency: {
title: string
icon: string
} | null
})[] = []
const important: (T.ActionRequest & {
actionName: string
dependency: {
title: string
icon: string
} | null
})[] = []
pkgId!: string
ngOnInit() {
this.pkgId = this.dep ? this.dep?.id : this.manifest.id
}
get actionRequests() {
const reqs: {
[key: string]: (T.ActionRequest & {
actionName: string
})[]
} = {}
Object.values(this.pkg.requestedActions) Object.values(this.pkg.requestedActions)
.filter(r => r.active) .filter(r => r.active)
.forEach(r => { .forEach(r => {
@@ -49,20 +49,17 @@ export class AppShowActionRequestsComponent {
? null ? null
: getDepDetails(this.pkg, this.allPkgs, r.request.packageId), : getDepDetails(this.pkg, this.allPkgs, r.request.packageId),
} }
if (!reqs[r.request.packageId]) {
if (r.request.severity === 'critical') { reqs[r.request.packageId] = []
critical.push(toReturn)
} else {
important.push(toReturn)
} }
reqs[r.request.packageId].push(toReturn)
}) })
return reqs
return { critical, important }
} }
constructor(private readonly actionService: ActionService) {} constructor(private readonly actionService: ActionService) {}
async handleAction(request: T.ActionRequest) { async handleAction(request: T.ActionRequest, e: Event) {
e.stopPropagation()
const self = request.packageId === this.manifest.id const self = request.packageId === this.manifest.id
this.actionService.present({ this.actionService.present({
pkgInfo: { pkgInfo: {

View File

@@ -5,27 +5,37 @@
*ngFor="let dep of dependencies" *ngFor="let dep of dependencies"
(click)="dep.action()" (click)="dep.action()"
> >
<ion-thumbnail slot="start"> <div class="container">
<img [src]="dep.icon" alt="" /> <div class="dep-details">
</ion-thumbnail> <ion-thumbnail slot="start">
<ion-label> <img [src]="dep.icon" alt="" />
<h2 class="montserrat"> </ion-thumbnail>
<ion-icon <ion-label>
*ngIf="!!dep.errorText" <h2 class="montserrat">
class="icon" <ion-icon
slot="start" *ngIf="!!dep.errorText"
name="warning-outline" class="icon"
color="warning" slot="start"
></ion-icon> name="warning-outline"
{{ dep.title }} color="warning"
</h2> ></ion-icon>
<p>{{ dep.version }}</p> {{ dep.title }}
<p> </h2>
<ion-text [color]="dep.errorText ? 'warning' : 'success'"> <p>{{ dep.version }}</p>
{{ dep.errorText || 'satisfied' }} <p>
</ion-text> <ion-text [color]="dep.errorText ? 'warning' : 'success'">
</p> {{ dep.errorText || 'satisfied' }}
</ion-label> </ion-text>
</p>
</ion-label>
</div>
<app-show-action-requests
[allPkgs]="allPkgs"
[pkg]="pkg"
[dep]="dep"
[manifest]="manifest"
></app-show-action-requests>
</div>
<ion-button *ngIf="dep.actionText" slot="end" fill="clear"> <ion-button *ngIf="dep.actionText" slot="end" fill="clear">
{{ dep.actionText }} {{ dep.actionText }}
<ion-icon slot="end" name="arrow-forward"></ion-icon> <ion-icon slot="end" name="arrow-forward"></ion-icon>

View File

@@ -1,3 +1,26 @@
.icon { .icon {
padding-right: 4px; padding-right: 4px;
} }
img {
position: relative;
z-index: 10;
}
.container {
display: flex;
flex-direction: column;
margin: 8px;
}
.dep-details {
display: flex;
align-items: center;
flex-direction: row;
gap: 1.2rem;
}
ion-label h2 {
display: flex;
align-items: center;
}

View File

@@ -1,5 +1,10 @@
import { ChangeDetectionStrategy, Component, Input } from '@angular/core' import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { DependencyInfo } from '../../app-show.page' import { DependencyInfo } from '../../app-show.page'
import { T } from '@start9labs/start-sdk'
import {
PackageDataEntry,
StateInfo,
} from 'src/app/services/patch-db/data-model'
@Component({ @Component({
selector: 'app-show-dependencies', selector: 'app-show-dependencies',
@@ -10,4 +15,15 @@ import { DependencyInfo } from '../../app-show.page'
export class AppShowDependenciesComponent { export class AppShowDependenciesComponent {
@Input() @Input()
dependencies: DependencyInfo[] = [] dependencies: DependencyInfo[] = []
@Input()
allPkgs!: NonNullable<
T.AllPackageData & Record<string, PackageDataEntry<StateInfo>>
>
@Input()
pkg!: T.PackageDataEntry & { stateInfo: StateInfo }
@Input()
manifest!: T.Manifest
} }

View File

@@ -1739,6 +1739,15 @@ export module Mock {
hasInput: true, hasInput: true,
group: null, group: null,
}, },
rpc: {
name: 'Set RPC',
description: 'Create RPC Credentials',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
},
properties: { properties: {
name: 'View Properties', name: 'View Properties',
description: 'view important information about Bitcoin', description: 'view important information about Bitcoin',
@@ -2037,7 +2046,26 @@ export module Mock {
status: { status: {
main: 'stopped', main: 'stopped',
}, },
actions: {}, actions: {
config: {
name: 'Config',
description: 'LND needs configuration before starting',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
},
connect: {
name: 'Connect',
description: 'View LND connection details',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
},
},
serviceInterfaces: { serviceInterfaces: {
grpc: { grpc: {
id: 'grpc', id: 'grpc',
@@ -2108,6 +2136,24 @@ export module Mock {
registry: 'https://registry.start9.com/', registry: 'https://registry.start9.com/',
developerKey: 'developer-key', developerKey: 'developer-key',
requestedActions: { requestedActions: {
config: {
active: true,
request: {
packageId: 'lnd',
actionId: 'config',
severity: 'critical',
reason: 'LND needs configuration before starting',
},
},
connect: {
active: true,
request: {
packageId: 'lnd',
actionId: 'connect',
severity: 'important',
reason: 'View LND connection details',
},
},
'bitcoind/config': { 'bitcoind/config': {
active: true, active: true,
request: { request: {
@@ -2119,10 +2165,24 @@ export module Mock {
kind: 'partial', kind: 'partial',
value: { value: {
color: '#ffffff', color: '#ffffff',
testnet: false,
},
},
},
},
'bitcoind/rpc': {
active: true,
request: {
packageId: 'bitcoind',
actionId: 'rpc',
severity: 'important',
reason: `LND want's its own RPC credentials`,
input: {
kind: 'partial',
value: {
rpcsettings: { rpcsettings: {
rpcuser: 'lnd', rpcuser: 'lnd',
}, },
testnet: false,
}, },
}, },
}, },

View File

@@ -232,6 +232,15 @@ export const mockPatchData: DataModel = {
hasInput: true, hasInput: true,
group: null, group: null,
}, },
rpc: {
name: 'Set RPC',
description: 'Create RPC Credentials',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
},
properties: { properties: {
name: 'View Properties', name: 'View Properties',
description: 'view important information about Bitcoin', description: 'view important information about Bitcoin',
@@ -487,7 +496,26 @@ export const mockPatchData: DataModel = {
status: { status: {
main: 'stopped', main: 'stopped',
}, },
actions: {}, actions: {
config: {
name: 'Config',
description: 'LND needs configuration before starting',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
},
connect: {
name: 'Connect',
description: 'View LND connection details',
warning: null,
visibility: 'enabled',
allowedStatuses: 'any',
hasInput: true,
group: null,
},
},
serviceInterfaces: { serviceInterfaces: {
grpc: { grpc: {
id: 'grpc', id: 'grpc',
@@ -559,6 +587,24 @@ export const mockPatchData: DataModel = {
registry: 'https://registry.start9.com/', registry: 'https://registry.start9.com/',
developerKey: 'developer-key', developerKey: 'developer-key',
requestedActions: { requestedActions: {
config: {
active: true,
request: {
packageId: 'lnd',
actionId: 'config',
severity: 'critical',
reason: 'LND needs configuration before starting',
},
},
connect: {
active: true,
request: {
packageId: 'lnd',
actionId: 'connect',
severity: 'important',
reason: 'View LND connection details',
},
},
'bitcoind/config': { 'bitcoind/config': {
active: true, active: true,
request: { request: {
@@ -570,10 +616,24 @@ export const mockPatchData: DataModel = {
kind: 'partial', kind: 'partial',
value: { value: {
color: '#ffffff', color: '#ffffff',
testnet: false,
},
},
},
},
'bitcoind/rpc': {
active: true,
request: {
packageId: 'bitcoind',
actionId: 'rpc',
severity: 'important',
reason: `LND want's its own RPC credentials`,
input: {
kind: 'partial',
value: {
rpcsettings: { rpcsettings: {
rpcuser: 'lnd', rpcuser: 'lnd',
}, },
testnet: false,
}, },
}, },
}, },

View File

@@ -20,5 +20,5 @@
"type": "image/png", "type": "image/png",
"purpose": "any" "purpose": "any"
} }
], ]
} }