Refactor/actions (#2733)

* store, properties, manifest

* interfaces

* init and backups

* fix init and backups

* file models

* more versions

* dependencies

* config except dynamic types

* clean up config

* remove disabled from non-dynamic vaues

* actions

* standardize example code block formats

* wip: actions refactor

Co-authored-by: Jade <Blu-J@users.noreply.github.com>

* commit types

* fix types

* update types

* update action request type

* update apis

* add description to actionrequest

* clean up imports

* revert package json

* chore: Remove the recursive to the index

* chore: Remove the other thing I was testing

* flatten action requests

* update container runtime with new config paradigm

* new actions strategy

* seems to be working

* misc backend fixes

* fix fe bugs

* only show breakages if breakages

* only show success modal if result

* don't panic on failed removal

* hide config from actions page

* polyfill autoconfig

* use metadata strategy for actions instead of prev

* misc fixes

* chore: split the sdk into 2 libs (#2736)

* follow sideload progress (#2718)

* follow sideload progress

* small bugfix

* shareReplay with no refcount false

* don't wrap sideload progress in RPCResult

* dont present toast

---------

Co-authored-by: Aiden McClelland <me@drbonez.dev>

* chore: Add the initial of the creation of the two sdk

* chore: Add in the baseDist

* chore: Add in the baseDist

* chore: Get the web and the runtime-container running

* chore: Remove the empty file

* chore: Fix it so the container-runtime works

---------

Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
Co-authored-by: Aiden McClelland <me@drbonez.dev>

* misc fixes

* update todos

* minor clean up

* fix link script

* update node version in CI test

* fix node version syntax in ci build

* wip: fixing callbacks

* fix sdk makefile dependencies

* add support for const outside of main

* update apis

* don't panic!

* Chore: Capture weird case on rpc, and log that

* fix procedure id issue

* pass input value for dep auto config

* handle disabled and warning for actions

* chore: Fix for link not having node_modules

* sdk fixes

* fix build

* fix build

* fix build

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: Jade <Blu-J@users.noreply.github.com>
Co-authored-by: J H <dragondef@gmail.com>
Co-authored-by: Jade <2364004+Blu-J@users.noreply.github.com>
Co-authored-by: Matt Hill <MattDHill@users.noreply.github.com>
This commit is contained in:
Aiden McClelland
2024-09-25 16:12:52 -06:00
committed by GitHub
parent eec5cf6b65
commit db0695126f
469 changed files with 16218 additions and 10485 deletions

View File

@@ -2,50 +2,49 @@ import {
ChangeDetectionStrategy,
Component,
Input,
OnChanges,
OnInit,
} from '@angular/core'
import { compare, getValueByPointer, Operation } from 'fast-json-patch'
import { getValueByPointer, Operation } from 'fast-json-patch'
import { isObject } from '@start9labs/shared'
import { tuiIsNumber } from '@taiga-ui/cdk'
import { CommonModule } from '@angular/common'
import { TuiNotificationModule } from '@taiga-ui/core'
@Component({
selector: 'config-dep',
selector: 'action-dep',
template: `
<tui-notification>
<h3 style="margin: 0 0 0.5rem; font-size: 1.25rem;">
{{ package }}
{{ pkgTitle }}
</h3>
The following modifications have been made to {{ package }} to satisfy
{{ dep }}:
The following modifications have been made to {{ pkgTitle }} to satisfy
{{ depTitle }}:
<ul>
<li *ngFor="let d of diff" [innerHTML]="d"></li>
</ul>
To accept these modifications, click "Save".
</tui-notification>
`,
standalone: true,
imports: [CommonModule, TuiNotificationModule],
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ConfigDepComponent implements OnChanges {
export class ActionDepComponent implements OnInit {
@Input()
package = ''
pkgTitle = ''
@Input()
dep = ''
depTitle = ''
@Input()
original: object = {}
originalValue: object = {}
@Input()
value: object = {}
operations: Operation[] = []
diff: string[] = []
ngOnChanges() {
this.diff = compare(this.original, this.value).map(
ngOnInit() {
this.diff = this.operations.map(
op => `${this.getPath(op)}: ${this.getMessage(op)}`,
)
}
@@ -82,7 +81,7 @@ export class ConfigDepComponent implements OnChanges {
}
private getOldValue(path: any): string {
const val = getValueByPointer(this.original, path)
const val = getValueByPointer(this.originalValue, path)
if (['string', 'number', 'boolean'].includes(typeof val)) {
return val
} else if (isObject(val)) {

View File

@@ -0,0 +1,208 @@
import { CommonModule } from '@angular/common'
import { Component, Inject } from '@angular/core'
import { getErrorMessage } from '@start9labs/shared'
import { T, utils } from '@start9labs/start-sdk'
import { TuiButtonModule } from '@taiga-ui/experimental'
import {
TuiDialogContext,
TuiDialogService,
TuiLoaderModule,
TuiModeModule,
TuiNotificationModule,
} from '@taiga-ui/core'
import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
import { compare } from 'fast-json-patch'
import { PatchDB } from 'patch-db-client'
import { catchError, defer, EMPTY, endWith, firstValueFrom, map } from 'rxjs'
import { InvalidService } from 'src/app/components/form/invalid.service'
import { ActionDepComponent } from 'src/app/modals/action-dep.component'
import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import { DataModel } from 'src/app/services/patch-db/data-model'
import { getAllPackages, getManifest } from 'src/app/util/get-package-data'
import * as json from 'fast-json-patch'
import { ActionService } from '../services/action.service'
import { ActionButton, FormComponent } from '../components/form.component'
export interface PackageActionData {
readonly pkgInfo: {
id: string
title: string
}
readonly actionInfo: {
id: string
warning: string | null
}
readonly dependentInfo?: {
title: string
request: T.ActionRequest
}
}
@Component({
template: `
<ng-container *ngIf="res$ | async as res; else loading">
<tui-notification *ngIf="error" status="error">
<div [innerHTML]="error"></div>
</tui-notification>
<ng-container *ngIf="res">
<tui-notification *ngIf="warning" status="warning">
<div [innerHTML]="warning"></div>
</tui-notification>
<action-dep
*ngIf="dependentInfo"
[pkgTitle]="pkgInfo.title"
[depTitle]="dependentInfo.title"
[originalValue]="res.originalValue || {}"
[operations]="res.operations || []"
></action-dep>
<app-form
tuiMode="onDark"
[spec]="res.spec"
[value]="res.originalValue || {}"
[buttons]="buttons"
[operations]="res.operations || []"
>
<button
tuiButton
appearance="flat"
type="reset"
[style.margin-right]="'auto'"
>
Reset Defaults
</button>
</app-form>
</ng-container>
</ng-container>
<ng-template #loading>
<tui-loader size="l" textContent="loading"></tui-loader>
</ng-template>
`,
styles: [
`
tui-notification {
font-size: 1rem;
margin-bottom: 1rem;
}
`,
],
standalone: true,
imports: [
CommonModule,
TuiLoaderModule,
TuiNotificationModule,
TuiButtonModule,
TuiModeModule,
ActionDepComponent,
UiPipeModule,
FormComponent,
],
providers: [InvalidService],
})
export class ActionInputModal {
readonly actionId = this.context.data.actionInfo.id
readonly warning = this.context.data.actionInfo.warning
readonly pkgInfo = this.context.data.pkgInfo
readonly dependentInfo = this.context.data.dependentInfo
buttons: ActionButton<any>[] = [
{
text: 'Submit',
handler: value => this.execute(value),
},
]
error = ''
res$ = defer(() =>
this.api.getActionInput({
packageId: this.pkgInfo.id,
actionId: this.actionId,
}),
).pipe(
map(res => {
const originalValue = res.value || {}
return {
spec: res.spec,
originalValue,
operations: this.dependentInfo?.request.input
? compare(
originalValue,
utils.deepMerge(
originalValue,
this.dependentInfo.request.input.value,
) as object,
)
: null,
}
}),
catchError(e => {
this.error = String(getErrorMessage(e))
return EMPTY
}),
)
constructor(
@Inject(POLYMORPHEUS_CONTEXT)
private readonly context: TuiDialogContext<void, PackageActionData>,
private readonly dialogs: TuiDialogService,
private readonly api: ApiService,
private readonly patch: PatchDB<DataModel>,
private readonly actionService: ActionService,
) {}
async execute(input: object) {
if (await this.checkConflicts(input)) {
const res = await firstValueFrom(this.res$)
return this.actionService.execute(this.pkgInfo.id, this.actionId, {
prev: {
spec: res.spec,
value: res.originalValue,
},
curr: input,
})
}
}
private async checkConflicts(input: object): Promise<boolean> {
const packages = await getAllPackages(this.patch)
const breakages = Object.keys(packages)
.filter(
id =>
id !== this.pkgInfo.id &&
Object.values(packages[id].requestedActions).some(
({ request, active }) =>
!active &&
request.packageId === this.pkgInfo.id &&
request.actionId === this.actionId &&
request.when?.condition === 'input-not-matches' &&
request.input &&
json
.compare(input, request.input)
.some(op => op.op === 'add' || op.op === 'replace'),
),
)
.map(id => id)
if (!breakages.length) return true
const message =
'As a result of this change, the following services will no longer work properly and may crash:<ul>'
const content = `${message}${breakages.map(
id => `<li><b>${getManifest(packages[id]).title}</b></li>`,
)}</ul>`
const data: TuiPromptData = { content, yes: 'Continue', no: 'Cancel' }
return firstValueFrom(
this.dialogs.open<boolean>(TUI_PROMPT, { data }).pipe(endWith(false)),
)
}
}

View File

@@ -1,7 +1,7 @@
import { Component, Input } from '@angular/core'
import { ModalController, ToastController } from '@ionic/angular'
import { ActionResponse } from 'src/app/services/api/api.types'
import { copyToClipboard } from '@start9labs/shared'
import { T } from '@start9labs/start-sdk'
@Component({
selector: 'action-success',
@@ -10,7 +10,7 @@ import { copyToClipboard } from '@start9labs/shared'
})
export class ActionSuccessPage {
@Input()
actionRes!: ActionResponse
actionRes!: T.ActionResult
constructor(
private readonly modalCtrl: ModalController,

View File

@@ -1,261 +0,0 @@
import { CommonModule } from '@angular/common'
import { Component, Inject, ViewChild } from '@angular/core'
import {
ErrorService,
getErrorMessage,
isEmptyObject,
LoadingService,
} from '@start9labs/shared'
import { CT, T } from '@start9labs/start-sdk'
import { TuiButtonModule } from '@taiga-ui/experimental'
import {
TuiDialogContext,
TuiDialogService,
TuiLoaderModule,
TuiModeModule,
TuiNotificationModule,
} from '@taiga-ui/core'
import { TUI_PROMPT, TuiPromptData } from '@taiga-ui/kit'
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
import { compare, Operation } from 'fast-json-patch'
import { PatchDB } from 'patch-db-client'
import { endWith, firstValueFrom, Subscription } from 'rxjs'
import { ActionButton, FormComponent } from 'src/app/components/form.component'
import { InvalidService } from 'src/app/components/form/invalid.service'
import { ConfigDepComponent } from 'src/app/modals/config-dep.component'
import { UiPipeModule } from 'src/app/pipes/ui/ui.module'
import { ApiService } from 'src/app/services/api/embassy-api.service'
import {
DataModel,
PackageDataEntry,
} from 'src/app/services/patch-db/data-model'
import {
getAllPackages,
getManifest,
getPackage,
} from 'src/app/util/get-package-data'
import { hasCurrentDeps } from 'src/app/util/has-deps'
import { Breakages } from 'src/app/services/api/api.types'
import { DependentInfo } from 'src/app/types/dependent-info'
export interface PackageConfigData {
readonly pkgId: string
readonly dependentInfo?: DependentInfo
}
@Component({
template: `
<tui-loader
*ngIf="loadingText"
size="l"
[textContent]="loadingText"
></tui-loader>
<tui-notification
*ngIf="!loadingText && (loadingError || !pkg)"
status="error"
>
<div [innerHTML]="loadingError"></div>
</tui-notification>
<ng-container
*ngIf="
!loadingText && !loadingError && pkg && (pkg | toManifest) as manifest
"
>
<tui-notification *ngIf="success" status="success">
{{ manifest.title }} has been automatically configured with recommended
defaults. Make whatever changes you want, then click "Save".
</tui-notification>
<config-dep
*ngIf="dependentInfo && value && original"
[package]="manifest.title"
[dep]="dependentInfo.title"
[original]="original"
[value]="value"
></config-dep>
<tui-notification *ngIf="!manifest.hasConfig" status="warning">
No config options for {{ manifest.title }} {{ manifest.version }}.
</tui-notification>
<app-form
tuiMode="onDark"
[spec]="spec"
[value]="value || {}"
[buttons]="buttons"
[patch]="patch"
>
<button
tuiButton
appearance="flat"
type="reset"
[style.margin-right]="'auto'"
>
Reset Defaults
</button>
</app-form>
</ng-container>
`,
styles: [
`
tui-notification {
font-size: 1rem;
margin-bottom: 1rem;
}
`,
],
standalone: true,
imports: [
CommonModule,
FormComponent,
TuiLoaderModule,
TuiNotificationModule,
TuiButtonModule,
TuiModeModule,
ConfigDepComponent,
UiPipeModule,
],
providers: [InvalidService],
})
export class ConfigModal {
@ViewChild(FormComponent)
private readonly form?: FormComponent<Record<string, any>>
readonly pkgId = this.context.data.pkgId
readonly dependentInfo = this.context.data.dependentInfo
loadingError = ''
loadingText = this.dependentInfo
? `Setting properties to accommodate ${this.dependentInfo.title}`
: 'Loading Config'
pkg?: PackageDataEntry
spec: CT.InputSpec = {}
patch: Operation[] = []
buttons: ActionButton<any>[] = [
{
text: 'Save',
handler: value => this.save(value),
},
]
original: object | null = null
value: object | null = null
constructor(
@Inject(POLYMORPHEUS_CONTEXT)
private readonly context: TuiDialogContext<void, PackageConfigData>,
private readonly dialogs: TuiDialogService,
private readonly errorService: ErrorService,
private readonly loader: LoadingService,
private readonly embassyApi: ApiService,
private readonly patchDb: PatchDB<DataModel>,
) {}
get success(): boolean {
return (
!!this.form &&
!this.form.form.dirty &&
!this.original &&
!this.pkg?.status?.configured
)
}
async ngOnInit() {
try {
this.pkg = await getPackage(this.patchDb, this.pkgId)
if (!this.pkg) {
this.loadingError = 'This service does not exist'
return
}
if (this.dependentInfo) {
const depConfig = await this.embassyApi.dryConfigureDependency({
dependencyId: this.pkgId,
dependentId: this.dependentInfo.id,
})
this.original = depConfig.oldConfig
this.value = depConfig.newConfig || this.original
this.spec = depConfig.spec
this.patch = compare(this.original, this.value)
} else {
const { config, spec } = await this.embassyApi.getPackageConfig({
id: this.pkgId,
})
this.original = config
this.value = config
this.spec = spec
}
} catch (e: any) {
this.loadingError = String(getErrorMessage(e))
} finally {
this.loadingText = ''
}
}
private async save(config: any) {
const loader = new Subscription()
try {
if (hasCurrentDeps(this.pkgId, await getAllPackages(this.patchDb))) {
await this.configureDeps(config, loader)
} else {
await this.configure(config, loader)
}
} catch (e: any) {
this.errorService.handleError(e)
} finally {
loader.unsubscribe()
}
}
private async configureDeps(
config: Record<string, any>,
loader: Subscription,
) {
loader.unsubscribe()
loader.closed = false
loader.add(this.loader.open('Checking dependent services...').subscribe())
const breakages = await this.embassyApi.drySetPackageConfig({
id: this.pkgId,
config,
})
loader.unsubscribe()
loader.closed = false
if (isEmptyObject(breakages) || (await this.approveBreakages(breakages))) {
await this.configure(config, loader)
}
}
private async configure(config: Record<string, any>, loader: Subscription) {
loader.unsubscribe()
loader.closed = false
loader.add(this.loader.open('Saving...').subscribe())
await this.embassyApi.setPackageConfig({ id: this.pkgId, config })
this.context.$implicit.complete()
}
private async approveBreakages(breakages: T.PackageId[]): Promise<boolean> {
const packages = await getAllPackages(this.patchDb)
const message =
'As a result of this change, the following services will no longer work properly and may crash:<ul>'
const content = `${message}${breakages.map(
id => `<li><b>${getManifest(packages[id]).title}</b></li>`,
)}</ul>`
const data: TuiPromptData = { content, yes: 'Continue', no: 'Cancel' }
return firstValueFrom(
this.dialogs.open<boolean>(TUI_PROMPT, { data }).pipe(endWith(false)),
)
}
}

View File

@@ -7,7 +7,7 @@ import {
sameUrl,
toUrl,
} from '@start9labs/shared'
import { CT } from '@start9labs/start-sdk'
import { IST } from '@start9labs/start-sdk'
import { TuiDialogOptions, TuiDialogService } from '@taiga-ui/core'
import {
TuiButtonModule,
@@ -182,7 +182,7 @@ export const MARKETPLACE_REGISTRY = new PolymorpheusComponent(
MarketplaceSettingsPage,
)
function getMarketplaceValueSpec(): CT.ValueSpecObject {
function getMarketplaceValueSpec(): IST.ValueSpecObject {
return {
type: 'object',
name: 'Add Custom Registry',

View File

@@ -14,7 +14,10 @@
<h4>0.3.6-alpha.5</h4>
<h6>This is an ALPHA release! DO NOT use for production data!</h6>
<h6>Expect that any data you create or store on this version of the OS can be LOST FOREVER!</h6>
<h6>
Expect that any data you create or store on this version of the OS can be
LOST FOREVER!
</h6>
<div class="ion-text-center ion-padding">
<ion-button