mirror of
https://github.com/Start9Labs/start-os.git
synced 2026-04-04 14:29:45 +00:00
Merge branch 'next/minor' of github.com:Start9Labs/start-os into next/major
This commit is contained in:
@@ -5,6 +5,7 @@ import { LoadingService } from '@start9labs/shared'
|
||||
import { TuiDialogService } from '@taiga-ui/core'
|
||||
import { filter } from 'rxjs'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
|
||||
@Component({
|
||||
selector: 'diagnostic-home',
|
||||
@@ -25,6 +26,7 @@ export class HomePage {
|
||||
private readonly api: ApiService,
|
||||
private readonly dialogs: TuiDialogService,
|
||||
@Inject(WA_WINDOW) private readonly window: Window,
|
||||
readonly config: ConfigService,
|
||||
) {}
|
||||
|
||||
async ngOnInit() {
|
||||
|
||||
@@ -8,8 +8,7 @@ import {
|
||||
} from '@angular/core'
|
||||
import { FormGroup, ReactiveFormsModule } from '@angular/forms'
|
||||
import { RouterModule } from '@angular/router'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import {
|
||||
tuiMarkControlAsTouchedAndValidate,
|
||||
TuiValueChanges,
|
||||
@@ -29,10 +28,10 @@ export interface ActionButton<T> {
|
||||
}
|
||||
|
||||
export interface FormContext<T> {
|
||||
spec: CT.InputSpec
|
||||
spec: IST.InputSpec
|
||||
buttons: ActionButton<T>[]
|
||||
value?: T
|
||||
patch?: Operation[]
|
||||
operations?: Operation[]
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -110,7 +109,7 @@ export class FormComponent<T extends Record<string, any>> implements OnInit {
|
||||
|
||||
@Input() spec = this.context?.data.spec || {}
|
||||
@Input() buttons = this.context?.data.buttons || []
|
||||
@Input() patch = this.context?.data.patch || []
|
||||
@Input() operations = this.context?.data.operations || []
|
||||
@Input() value?: T = this.context?.data.value
|
||||
|
||||
form = new FormGroup({})
|
||||
@@ -118,7 +117,7 @@ export class FormComponent<T extends Record<string, any>> implements OnInit {
|
||||
ngOnInit() {
|
||||
this.confirmService.markAsPristine()
|
||||
this.form = this.formService.createForm(this.spec, this.value)
|
||||
this.process(this.patch)
|
||||
this.process(this.operations)
|
||||
}
|
||||
|
||||
onReset() {
|
||||
@@ -147,15 +146,16 @@ export class FormComponent<T extends Record<string, any>> implements OnInit {
|
||||
this.context?.$implicit.complete()
|
||||
}
|
||||
|
||||
private process(patch: Operation[]) {
|
||||
patch.forEach(({ op, path }) => {
|
||||
const control = this.form.get(path.substring(1).split('/'))
|
||||
private process(operations: Operation[]) {
|
||||
operations.forEach(operation => {
|
||||
const control = this.form.get(operation.path.substring(1).split('/'))
|
||||
|
||||
if (!control || !control.parent) return
|
||||
|
||||
if (op !== 'remove') {
|
||||
if (operation.op === 'add' || operation.op === 'replace') {
|
||||
control.markAsDirty()
|
||||
control.markAsTouched()
|
||||
control.setValue(operation.value)
|
||||
}
|
||||
|
||||
control.parent.markAsDirty()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { inject } from '@angular/core'
|
||||
import { FormControlComponent } from './form-control/form-control.component'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
|
||||
export abstract class Control<Spec extends CT.ValueSpec, Value> {
|
||||
export abstract class Control<Spec extends Exclude<IST.ValueSpec, IST.ValueSpecHidden>, Value> {
|
||||
private readonly control: FormControlComponent<Spec, Value> =
|
||||
inject(FormControlComponent)
|
||||
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { KeyValue } from '@angular/common'
|
||||
|
||||
@Pipe({
|
||||
name: 'filterHidden',
|
||||
})
|
||||
export class FilterHiddenPipe implements PipeTransform {
|
||||
transform(value: KeyValue<string, IST.ValueSpec>[]) {
|
||||
return value.filter(x => x.value.type !== 'hidden') as KeyValue<
|
||||
string,
|
||||
Exclude<IST.ValueSpec, IST.ValueSpecHidden>
|
||||
>[]
|
||||
}
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '@taiga-ui/core'
|
||||
import { TUI_CONFIRM } from '@taiga-ui/kit'
|
||||
import { filter } from 'rxjs'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
import { ERRORS } from '../form-group/form-group.component'
|
||||
|
||||
@@ -29,7 +30,7 @@ import { ERRORS } from '../form-group/form-group.component'
|
||||
})
|
||||
export class FormArrayComponent {
|
||||
@Input({ required: true })
|
||||
spec!: CT.ValueSpecList
|
||||
spec!: IST.ValueSpecList
|
||||
|
||||
@HostBinding('@tuiParentStop')
|
||||
readonly animation = tuiToAnimationOptions(inject(TUI_ANIMATIONS_SPEED))
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
import { MaskitoOptions } from '@maskito/core'
|
||||
|
||||
@@ -8,7 +8,7 @@ import { MaskitoOptions } from '@maskito/core'
|
||||
templateUrl: './form-color.component.html',
|
||||
styleUrls: ['./form-color.component.scss'],
|
||||
})
|
||||
export class FormColorComponent extends Control<CT.ValueSpecColor, string> {
|
||||
export class FormColorComponent extends Control<IST.ValueSpecColor, string> {
|
||||
readonly mask: MaskitoOptions = {
|
||||
mask: ['#', ...Array(6).fill(/[0-9a-f]/i)],
|
||||
}
|
||||
|
||||
@@ -37,4 +37,4 @@
|
||||
Accept
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ng-template>
|
||||
@@ -7,10 +7,10 @@ import {
|
||||
ViewChild,
|
||||
} from '@angular/core'
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { TuiAlertService, TuiDialogContext } from '@taiga-ui/core'
|
||||
import { AbstractTuiNullableControl } from '@taiga-ui/legacy'
|
||||
import { filter } from 'rxjs'
|
||||
import { TuiAlertService, TuiDialogContext } from '@taiga-ui/core'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { ERRORS } from '../form-group/form-group.component'
|
||||
import { FORM_CONTROL_PROVIDERS } from './form-control.providers'
|
||||
|
||||
@@ -22,7 +22,7 @@ import { FORM_CONTROL_PROVIDERS } from './form-control.providers'
|
||||
providers: FORM_CONTROL_PROVIDERS,
|
||||
})
|
||||
export class FormControlComponent<
|
||||
T extends CT.ValueSpec,
|
||||
T extends Exclude<IST.ValueSpec, IST.ValueSpecHidden>,
|
||||
V,
|
||||
> extends AbstractTuiNullableControl<V> {
|
||||
@Input({ required: true })
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { forwardRef, Provider } from '@angular/core'
|
||||
import { TUI_VALIDATION_ERRORS } from '@taiga-ui/kit'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { FormControlComponent } from './form-control.component'
|
||||
|
||||
interface ValidatorsPatternError {
|
||||
@@ -12,7 +12,7 @@ export const FORM_CONTROL_PROVIDERS: Provider[] = [
|
||||
{
|
||||
provide: TUI_VALIDATION_ERRORS,
|
||||
deps: [forwardRef(() => FormControlComponent)],
|
||||
useFactory: (control: FormControlComponent<CT.ValueSpec, string>) => ({
|
||||
useFactory: (control: FormControlComponent<Exclude<IST.ValueSpec, IST.ValueSpecHidden>, string>) => ({
|
||||
required: 'Required',
|
||||
pattern: ({ requiredPattern }: ValidatorsPatternError) =>
|
||||
('patterns' in control.spec &&
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
tuiPure,
|
||||
TuiTime,
|
||||
} from '@taiga-ui/cdk'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
@@ -14,7 +14,7 @@ import { Control } from '../control'
|
||||
templateUrl: './form-datetime.component.html',
|
||||
})
|
||||
export class FormDatetimeComponent extends Control<
|
||||
CT.ValueSpecDatetime,
|
||||
IST.ValueSpecDatetime,
|
||||
string
|
||||
> {
|
||||
readonly min = TUI_FIRST_DAY
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<ng-container
|
||||
*ngFor="let entry of spec | keyvalue: asIsOrder"
|
||||
*ngFor="let entry of spec | keyvalue: asIsOrder | filterHidden"
|
||||
[ngSwitch]="entry.value.type"
|
||||
[tuiTextfieldCleaner]="true"
|
||||
>
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
Input,
|
||||
ViewEncapsulation,
|
||||
} from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { FORM_GROUP_PROVIDERS } from './form-group.providers'
|
||||
|
||||
export const ERRORS = [
|
||||
@@ -27,7 +27,7 @@ export const ERRORS = [
|
||||
viewProviders: [FORM_GROUP_PROVIDERS],
|
||||
})
|
||||
export class FormGroupComponent {
|
||||
@Input() spec: CT.InputSpec = {}
|
||||
@Input() spec: IST.InputSpec = {}
|
||||
|
||||
asIsOrder() {
|
||||
return 0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
import { invert } from '@start9labs/shared'
|
||||
@@ -9,7 +9,7 @@ import { invert } from '@start9labs/shared'
|
||||
templateUrl: './form-multiselect.component.html',
|
||||
})
|
||||
export class FormMultiselectComponent extends Control<
|
||||
CT.ValueSpecMultiselect,
|
||||
IST.ValueSpecMultiselect,
|
||||
readonly string[]
|
||||
> {
|
||||
private readonly inverted = invert(this.spec.values)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
selector: 'form-number',
|
||||
templateUrl: './form-number.component.html',
|
||||
})
|
||||
export class FormNumberComponent extends Control<CT.ValueSpecNumber, number> {
|
||||
export class FormNumberComponent extends Control<IST.ValueSpecNumber, number> {
|
||||
protected readonly Infinity = Infinity
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
Output,
|
||||
} from '@angular/core'
|
||||
import { ControlContainer } from '@angular/forms'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
|
||||
@Component({
|
||||
selector: 'form-object',
|
||||
@@ -17,7 +17,7 @@ import { CT } from '@start9labs/start-sdk'
|
||||
})
|
||||
export class FormObjectComponent {
|
||||
@Input({ required: true })
|
||||
spec!: CT.ValueSpecObject
|
||||
spec!: IST.ValueSpecObject
|
||||
|
||||
@Input()
|
||||
open = false
|
||||
|
||||
@@ -2,13 +2,12 @@
|
||||
[tuiHintContent]="spec | hint"
|
||||
[disabled]="disabled"
|
||||
[readOnly]="readOnly"
|
||||
[tuiTextfieldCleaner]="!spec.required"
|
||||
[tuiTextfieldCleaner]="false"
|
||||
[pseudoInvalid]="invalid"
|
||||
[(ngModel)]="selected"
|
||||
(focusedChange)="onFocus($event)"
|
||||
>
|
||||
{{ spec.name }}
|
||||
<span *ngIf="spec.required">*</span>
|
||||
{{ spec.name }}*
|
||||
<select
|
||||
tuiSelect
|
||||
[placeholder]="spec.name"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { invert } from '@start9labs/shared'
|
||||
import { Control } from '../control'
|
||||
|
||||
@@ -7,7 +7,7 @@ import { Control } from '../control'
|
||||
selector: 'form-select',
|
||||
templateUrl: './form-select.component.html',
|
||||
})
|
||||
export class FormSelectComponent extends Control<CT.ValueSpecSelect, string> {
|
||||
export class FormSelectComponent extends Control<IST.ValueSpecSelect, string> {
|
||||
private readonly inverted = invert(this.spec.values)
|
||||
|
||||
readonly items = Object.values(this.spec.values)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT, utils } from '@start9labs/start-sdk'
|
||||
import { IST, utils } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
@@ -7,7 +7,7 @@ import { Control } from '../control'
|
||||
templateUrl: './form-text.component.html',
|
||||
styleUrls: ['./form-text.component.scss'],
|
||||
})
|
||||
export class FormTextComponent extends Control<CT.ValueSpecText, string> {
|
||||
export class FormTextComponent extends Control<IST.ValueSpecText, string> {
|
||||
masked = true
|
||||
|
||||
generate() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
@@ -7,6 +7,6 @@ import { Control } from '../control'
|
||||
templateUrl: './form-textarea.component.html',
|
||||
})
|
||||
export class FormTextareaComponent extends Control<
|
||||
CT.ValueSpecTextarea,
|
||||
IST.ValueSpecTextarea,
|
||||
string
|
||||
> {}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Component } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { Control } from '../control'
|
||||
|
||||
@Component({
|
||||
@@ -7,4 +7,7 @@ import { Control } from '../control'
|
||||
templateUrl: './form-toggle.component.html',
|
||||
host: { class: 'g-toggle' },
|
||||
})
|
||||
export class FormToggleComponent extends Control<CT.ValueSpecToggle, boolean> {}
|
||||
export class FormToggleComponent extends Control<
|
||||
IST.ValueSpecToggle,
|
||||
boolean
|
||||
> {}
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
OnChanges,
|
||||
} from '@angular/core'
|
||||
import { ControlContainer, FormGroupName } from '@angular/forms'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
import { FormService } from 'src/app/services/form.service'
|
||||
import { tuiPure } from '@taiga-ui/cdk'
|
||||
|
||||
@@ -24,9 +24,9 @@ import { tuiPure } from '@taiga-ui/cdk'
|
||||
})
|
||||
export class FormUnionComponent implements OnChanges {
|
||||
@Input({ required: true })
|
||||
spec!: CT.ValueSpecUnion
|
||||
spec!: IST.ValueSpecUnion
|
||||
|
||||
selectSpec!: CT.ValueSpecSelect
|
||||
selectSpec!: IST.ValueSpecSelect
|
||||
|
||||
private readonly form = inject(FormGroupName)
|
||||
private readonly formService = inject(FormService)
|
||||
|
||||
@@ -49,6 +49,7 @@ import { FormToggleComponent } from './form-toggle/form-toggle.component'
|
||||
import { FormUnionComponent } from './form-union/form-union.component'
|
||||
import { HintPipe } from './hint.pipe'
|
||||
import { MustachePipe } from './mustache.pipe'
|
||||
import { FilterHiddenPipe } from './filter-hidden.pipe'
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -100,6 +101,7 @@ import { MustachePipe } from './mustache.pipe'
|
||||
MustachePipe,
|
||||
HintPipe,
|
||||
ControlDirective,
|
||||
FilterHiddenPipe,
|
||||
],
|
||||
exports: [FormGroupComponent],
|
||||
})
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { CT } from '@start9labs/start-sdk'
|
||||
import { IST } from '@start9labs/start-sdk'
|
||||
|
||||
@Pipe({
|
||||
name: 'hint',
|
||||
})
|
||||
export class HintPipe implements PipeTransform {
|
||||
transform(spec: CT.ValueSpec): string {
|
||||
transform(spec: Exclude<IST.ValueSpec, IST.ValueSpecHidden>): string {
|
||||
const hint = []
|
||||
|
||||
if (spec.description) {
|
||||
|
||||
@@ -2,50 +2,46 @@ import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
} from '@angular/core'
|
||||
import { TuiNotification } from '@taiga-ui/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'
|
||||
|
||||
@Component({
|
||||
selector: 'config-dep',
|
||||
selector: 'action-request-info',
|
||||
template: `
|
||||
<tui-notification>
|
||||
<h3 style="margin: 0 0 0.5rem; font-size: 1.25rem;">
|
||||
{{ package }}
|
||||
</h3>
|
||||
The following modifications have been made to {{ package }} to satisfy
|
||||
{{ dep }}:
|
||||
<tui-notification *ngIf="diff.length">
|
||||
The following modifications were made:
|
||||
<ul>
|
||||
<li *ngFor="let d of diff" [innerHTML]="d"></li>
|
||||
</ul>
|
||||
To accept these modifications, click "Save".
|
||||
</tui-notification>
|
||||
`,
|
||||
standalone: true,
|
||||
imports: [CommonModule, TuiNotification],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
styles: [
|
||||
`
|
||||
tui-notification {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
`,
|
||||
],
|
||||
})
|
||||
export class ConfigDepComponent implements OnChanges {
|
||||
export class ActionRequestInfoComponent implements OnInit {
|
||||
@Input()
|
||||
package = ''
|
||||
originalValue: object = {}
|
||||
|
||||
@Input()
|
||||
dep = ''
|
||||
|
||||
@Input()
|
||||
original: 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)}`,
|
||||
)
|
||||
}
|
||||
@@ -69,20 +65,20 @@ export class ConfigDepComponent implements OnChanges {
|
||||
private getMessage(operation: Operation): string {
|
||||
switch (operation.op) {
|
||||
case 'add':
|
||||
return `Added ${this.getNewValue(operation.value)}`
|
||||
return `added ${this.getNewValue(operation.value)}`
|
||||
case 'remove':
|
||||
return `Removed ${this.getOldValue(operation.path)}`
|
||||
return `removed ${this.getOldValue(operation.path)}`
|
||||
case 'replace':
|
||||
return `Changed from ${this.getOldValue(
|
||||
return `changed from ${this.getOldValue(
|
||||
operation.path,
|
||||
)} to ${this.getNewValue(operation.value)}`
|
||||
default:
|
||||
return `Unknown operation`
|
||||
return `Unknown operation` // unreachable
|
||||
}
|
||||
}
|
||||
|
||||
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)) {
|
||||
|
||||
@@ -1,257 +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 {
|
||||
TuiDialogContext,
|
||||
TuiDialogService,
|
||||
TuiLoader,
|
||||
TuiButton,
|
||||
TuiNotification,
|
||||
} from '@taiga-ui/core'
|
||||
import { TuiConfirmData, TUI_CONFIRM } from '@taiga-ui/kit'
|
||||
import { POLYMORPHEUS_CONTEXT } from '@taiga-ui/polymorpheus'
|
||||
import { compare, Operation } from 'fast-json-patch'
|
||||
import { PatchDB } from 'patch-db-client'
|
||||
import { endWith, firstValueFrom, Subscription } from 'rxjs'
|
||||
import { ConfigDepComponent } from 'src/app/routes/portal/modals/config-dep.component'
|
||||
import { ApiService } from 'src/app/services/api/embassy-api.service'
|
||||
import {
|
||||
DataModel,
|
||||
PackageDataEntry,
|
||||
} from 'src/app/services/patch-db/data-model'
|
||||
import { hasCurrentDeps } from 'src/app/utils/has-deps'
|
||||
import {
|
||||
getAllPackages,
|
||||
getManifest,
|
||||
getPackage,
|
||||
} from 'src/app/utils/get-package-data'
|
||||
import { Breakages } from 'src/app/services/api/api.types'
|
||||
import { InvalidService } from 'src/app/routes/portal/components/form/invalid.service'
|
||||
import {
|
||||
ActionButton,
|
||||
FormComponent,
|
||||
} from 'src/app/routes/portal/components/form.component'
|
||||
import { DependentInfo } from 'src/app/types/dependent-info'
|
||||
import { ToManifestPipe } from '../pipes/to-manifest'
|
||||
|
||||
export interface PackageConfigData {
|
||||
readonly pkgId: string
|
||||
readonly dependentInfo?: DependentInfo
|
||||
}
|
||||
|
||||
@Component({
|
||||
template: `
|
||||
<tui-loader *ngIf="loadingText" size="l" [textContent]="loadingText" />
|
||||
|
||||
<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"
|
||||
/>
|
||||
|
||||
<tui-notification *ngIf="!manifest.hasConfig" status="warning">
|
||||
No config options for {{ manifest.title }} {{ manifest.version }}.
|
||||
</tui-notification>
|
||||
|
||||
<app-form
|
||||
[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,
|
||||
TuiLoader,
|
||||
TuiNotification,
|
||||
TuiButton,
|
||||
ConfigDepComponent,
|
||||
ToManifestPipe,
|
||||
],
|
||||
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 = 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: TuiConfirmData = { content, yes: 'Continue', no: 'Cancel' }
|
||||
|
||||
return firstValueFrom(
|
||||
this.dialogs.open<boolean>(TUI_CONFIRM, { data }).pipe(endWith(false)),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,14 @@ import { PackageBackupInfo } from 'src/app/services/api/api.types'
|
||||
import { ConfigService } from 'src/app/services/config.service'
|
||||
import { PackageDataEntry } from 'src/app/services/patch-db/data-model'
|
||||
import { RecoverOption } from '../types/recover-option'
|
||||
import { Version } from '@start9labs/start-sdk'
|
||||
|
||||
export interface AppRecoverOption extends PackageBackupInfo {
|
||||
id: string
|
||||
checked: boolean
|
||||
installed: boolean
|
||||
newerOS: boolean
|
||||
}
|
||||
|
||||
@Pipe({
|
||||
name: 'toOptions',
|
||||
@@ -26,7 +34,10 @@ export class ToOptionsPipe implements PipeTransform {
|
||||
id,
|
||||
installed: !!packageData[id],
|
||||
checked: false,
|
||||
newerStartOs: this.compare(packageBackups[id].osVersion),
|
||||
newerOS:
|
||||
Version.parse(packageBackups[id].osVersion).compare(
|
||||
Version.parse(this.config.version),
|
||||
) === 'greater',
|
||||
}))
|
||||
.sort((a, b) =>
|
||||
b.title.toLowerCase() > a.title.toLowerCase() ? -1 : 1,
|
||||
@@ -34,11 +45,4 @@ export class ToOptionsPipe implements PipeTransform {
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
private compare(version: string): boolean {
|
||||
// checks to see if backup was made on a newer version of startOS
|
||||
return (
|
||||
this.exver.compareOsVersion(version, this.config.version) === 'greater'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,5 +4,5 @@ export interface RecoverOption extends PackageBackupInfo {
|
||||
id: string
|
||||
checked: boolean
|
||||
installed: boolean
|
||||
newerStartOs: boolean
|
||||
newerOs: boolean
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user