sdk tweaks (#2760)

* sdk tweaks

* update action result types

* accommodate new action response types

* fix: show action value labels

* Feature/get status effect (#2765)

* wip: get status

* feat: Add the get_status for effects

* feat: Do a callback

---------

Co-authored-by: J H <dragondef@gmail.com>

---------

Co-authored-by: Matt Hill <mattnine@protonmail.com>
Co-authored-by: waterplea <alexander@inkin.ru>
Co-authored-by: J H <dragondef@gmail.com>
This commit is contained in:
Aiden McClelland
2024-10-28 12:12:36 -06:00
committed by GitHub
parent 42cfd69463
commit 26ae0bf207
28 changed files with 871 additions and 456 deletions

View File

@@ -1,23 +1,23 @@
import { CommonModule } from '@angular/common'
import { ChangeDetectionStrategy, Component, Input } from '@angular/core'
import { T } from '@start9labs/start-sdk'
import { TuiFadeModule, TuiTitleModule } from '@taiga-ui/experimental'
import { TuiAccordionModule } from '@taiga-ui/kit'
import { ActionSuccessItemComponent } from './action-success-item.component'
import { ActionSuccessMemberComponent } from './action-success-member.component'
import { GroupResult } from './types'
@Component({
standalone: true,
selector: 'app-action-success-group',
template: `
<p *ngFor="let item of value?.value">
<app-action-success-item
*ngIf="isSingle(item)"
[value]="item"
></app-action-success-item>
<tui-accordion-item *ngIf="!isSingle(item)" size="s">
<div tuiFade>{{ item.name }}</div>
<p *ngFor="let member of group.value">
<app-action-success-member
*ngIf="member.type === 'single'"
[member]="member"
></app-action-success-member>
<tui-accordion-item *ngIf="member.type === 'group'">
<div tuiFade>{{ member.name }}</div>
<ng-template tuiAccordionItemContent>
<app-action-success-group [value]="item"></app-action-success-group>
<app-action-success-group [group]="member"></app-action-success-group>
</ng-template>
</tui-accordion-item>
</p>
@@ -37,18 +37,12 @@ import { ActionSuccessItemComponent } from './action-success-item.component'
imports: [
CommonModule,
TuiTitleModule,
ActionSuccessItemComponent,
ActionSuccessMemberComponent,
TuiAccordionModule,
TuiFadeModule,
],
})
export class ActionSuccessGroupComponent {
@Input()
value?: T.ActionResultV1 & { type: 'object' }
isSingle(
value: T.ActionResultV1,
): value is T.ActionResultV1 & { type: 'string' } {
return value.type === 'string'
}
group!: GroupResult
}

View File

@@ -9,41 +9,38 @@ import {
ViewChild,
} from '@angular/core'
import { FormsModule } from '@angular/forms'
import { T } from '@start9labs/start-sdk'
import {
TuiDialogService,
TuiLabelModule,
TuiTextfieldComponent,
TuiTextfieldControllerModule,
} from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { TuiButtonModule, TuiTitleModule } from '@taiga-ui/experimental'
import { TuiInputModule } from '@taiga-ui/kit'
import { QrCodeModule } from 'ng-qrcode'
import { ActionSuccessGroupComponent } from './action-success-group.component'
import { T } from '@start9labs/start-sdk'
@Component({
standalone: true,
selector: 'app-action-success-item',
selector: 'app-action-success-member',
template: `
<p *ngIf="!parent" class="qr">
<ng-container *ngTemplateOutlet="qr"></ng-container>
</p>
<label [tuiLabel]="value.description">
<tui-input
[readOnly]="true"
[ngModel]="value.value"
[tuiTextfieldCustomContent]="actions"
>
<input
tuiTextfield
[style.border-inline-end-width.rem]="border"
[type]="value.masked && masked ? 'password' : 'text'"
/>
</tui-input>
<tui-input
[readOnly]="true"
[ngModel]="member.value"
[tuiTextfieldCustomContent]="actions"
>
{{ member.name }}
<input
tuiTextfield
[style.border-inline-end-width.rem]="border"
[type]="member.masked && masked ? 'password' : 'text'"
/>
</tui-input>
<label *ngIf="member.description" [style.padding-top.rem]="0.25" tuiTitle>
<span tuiSubtitle [style.opacity]="0.8">{{ member.description }}</span>
</label>
<ng-template #actions>
<button
*ngIf="value.masked"
*ngIf="member.masked"
tuiIconButton
appearance="icon"
size="s"
@@ -56,7 +53,7 @@ import { ActionSuccessGroupComponent } from './action-success-group.component'
Reveal/Hide
</button>
<button
*ngIf="value.copyable"
*ngIf="member.copyable"
tuiIconButton
appearance="icon"
size="s"
@@ -69,7 +66,7 @@ import { ActionSuccessGroupComponent } from './action-success-group.component'
Copy
</button>
<button
*ngIf="value.qr && parent"
*ngIf="member.qr"
tuiIconButton
appearance="icon"
size="s"
@@ -84,12 +81,12 @@ import { ActionSuccessGroupComponent } from './action-success-group.component'
</ng-template>
<ng-template #qr>
<qr-code
[value]="value.value"
[style.filter]="value.masked && masked ? 'blur(0.5rem)' : null"
[value]="member.value"
[style.filter]="member.masked && masked ? 'blur(0.5rem)' : null"
size="350"
></qr-code>
<button
*ngIf="value.masked && masked"
*ngIf="member.masked && masked"
tuiIconButton
class="reveal"
iconLeft="tuiIconEye"
@@ -122,37 +119,25 @@ import { ActionSuccessGroupComponent } from './action-success-group.component'
TuiTextfieldControllerModule,
TuiButtonModule,
QrCodeModule,
TuiLabelModule,
TuiTitleModule,
],
})
export class ActionSuccessItemComponent {
export class ActionSuccessMemberComponent {
@ViewChild(TuiTextfieldComponent, { read: ElementRef })
private readonly input!: ElementRef<HTMLInputElement>
private readonly dialogs = inject(TuiDialogService)
readonly parent = inject(ActionSuccessGroupComponent, {
optional: true,
})
@Input()
value!: T.ActionResultV1 & { type: 'string' }
member!: T.ActionResultMember & { type: 'single' }
masked = true
get border(): number {
let border = 0
if (this.value.masked) {
border += 2
}
if (this.value.copyable) {
border += 2
}
if (this.value.qr && this.parent) {
border += 2
}
if (this.member.masked) border += 2
if (this.member.copyable) border += 2
if (this.member.qr) border += 2
return border
}
@@ -160,7 +145,7 @@ export class ActionSuccessItemComponent {
show(template: TemplateRef<any>) {
const masked = this.masked
this.masked = this.value.masked
this.masked = this.member.masked
this.dialogs
.open(template, { label: 'Scan this QR', size: 's' })
.subscribe({
@@ -179,6 +164,6 @@ export class ActionSuccessItemComponent {
el.focus()
el.select()
el.ownerDocument.execCommand('copy')
el.type = this.masked && this.value.masked ? 'password' : 'text'
el.type = this.masked && this.member.masked ? 'password' : 'text'
}
}

View File

@@ -0,0 +1,145 @@
import { CommonModule } from '@angular/common'
import {
ChangeDetectionStrategy,
Component,
ElementRef,
inject,
Input,
TemplateRef,
ViewChild,
} from '@angular/core'
import { FormsModule } from '@angular/forms'
import {
TuiDialogService,
TuiLabelModule,
TuiTextfieldComponent,
TuiTextfieldControllerModule,
} from '@taiga-ui/core'
import { TuiButtonModule } from '@taiga-ui/experimental'
import { TuiInputModule } from '@taiga-ui/kit'
import { QrCodeModule } from 'ng-qrcode'
import { SingleResult } from './types'
@Component({
standalone: true,
selector: 'app-action-success-single',
template: `
<p class="qr">
<ng-container *ngTemplateOutlet="qr"></ng-container>
</p>
<tui-input
[readOnly]="true"
[ngModel]="single.value"
[tuiTextfieldLabelOutside]="true"
[tuiTextfieldCustomContent]="actions"
>
<input
tuiTextfield
[style.border-inline-end-width.rem]="border"
[type]="single.masked && masked ? 'password' : 'text'"
/>
</tui-input>
<ng-template #actions>
<button
*ngIf="single.masked"
tuiIconButton
appearance="icon"
size="s"
type="button"
tabindex="-1"
[iconLeft]="masked ? 'tuiIconEye' : 'tuiIconEyeOff'"
[style.pointer-events]="'auto'"
(click)="masked = !masked"
>
Reveal/Hide
</button>
<button
*ngIf="single.copyable"
tuiIconButton
appearance="icon"
size="s"
type="button"
tabindex="-1"
iconLeft="tuiIconCopy"
[style.pointer-events]="'auto'"
(click)="copy()"
>
Copy
</button>
</ng-template>
<ng-template #qr>
<qr-code
[value]="single.value"
[style.filter]="single.masked && masked ? 'blur(0.5rem)' : null"
size="350"
></qr-code>
<button
*ngIf="single.masked && masked"
tuiIconButton
class="reveal"
iconLeft="tuiIconEye"
[style.border-radius.%]="100"
(click)="masked = false"
>
Reveal
</button>
</ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
styles: [
`
@import '@taiga-ui/core/styles/taiga-ui-local';
.reveal {
@include center-all();
}
.qr {
position: relative;
text-align: center;
}
`,
],
imports: [
CommonModule,
FormsModule,
TuiInputModule,
TuiTextfieldControllerModule,
TuiButtonModule,
QrCodeModule,
TuiLabelModule,
],
})
export class ActionSuccessSingleComponent {
@ViewChild(TuiTextfieldComponent, { read: ElementRef })
private readonly input!: ElementRef<HTMLInputElement>
private readonly dialogs = inject(TuiDialogService)
@Input()
single!: SingleResult
masked = true
get border(): number {
let border = 0
if (this.single.masked) border += 2
if (this.single.copyable) border += 2
return border
}
copy() {
const el = this.input.nativeElement
if (!el) {
return
}
el.type = 'text'
el.focus()
el.select()
el.ownerDocument.execCommand('copy')
el.type = this.masked && this.single.masked ? 'password' : 'text'
}
}

View File

@@ -1,36 +1,36 @@
import { CommonModule } from '@angular/common'
import { Component, inject } from '@angular/core'
import { TuiDialogContext, TuiTextfieldControllerModule } from '@taiga-ui/core'
import { TuiDialogContext } from '@taiga-ui/core'
import { POLYMORPHEUS_CONTEXT } from '@tinkoff/ng-polymorpheus'
import { RR } from 'src/app/services/api/api.types'
import { ActionSuccessGroupComponent } from './action-success-group.component'
import { ActionSuccessItemComponent } from './action-success-item.component'
import { ActionSuccessSingleComponent } from './action-success-single.component'
import { ActionResponseWithResult } from './types'
@Component({
standalone: true,
template: `
<ng-container tuiTextfieldSize="m" [tuiTextfieldLabelOutside]="true">
<app-action-success-item
*ngIf="item"
[value]="item"
></app-action-success-item>
<app-action-success-group
*ngIf="group"
[value]="group"
></app-action-success-group>
</ng-container>
<p *ngIf="data.message">{{ data.message }}</p>
<app-action-success-single
*ngIf="single"
[single]="single"
></app-action-success-single>
<app-action-success-group
*ngIf="group"
[group]="group"
></app-action-success-group>
`,
imports: [
CommonModule,
ActionSuccessGroupComponent,
ActionSuccessItemComponent,
TuiTextfieldControllerModule,
ActionSuccessSingleComponent,
],
})
export class ActionSuccessPage {
readonly data =
inject<TuiDialogContext<void, RR.ActionRes>>(POLYMORPHEUS_CONTEXT).data
inject<TuiDialogContext<void, ActionResponseWithResult>>(
POLYMORPHEUS_CONTEXT,
).data
readonly item = this.data?.type === 'string' ? this.data : null
readonly group = this.data?.type === 'object' ? this.data : null
readonly single = this.data.result.type === 'single' ? this.data.result : null
readonly group = this.data.result.type === 'group' ? this.data.result : null
}

View File

@@ -0,0 +1,7 @@
import { RR } from 'src/app/services/api/api.types'
type ActionResponse = NonNullable<RR.ActionRes>
type ActionResult = NonNullable<ActionResponse['result']>
export type ActionResponseWithResult = ActionResponse & { result: ActionResult }
export type SingleResult = ActionResult & { type: 'single' }
export type GroupResult = ActionResult & { type: 'group' }

View File

@@ -118,13 +118,17 @@ export class ActionService {
input: input || null,
})
if (res) {
if (!res) return true
if (res.result) {
this.dialogs
.open(new PolymorpheusComponent(ActionSuccessPage), {
label: res.name,
label: res.title,
data: res,
})
.subscribe()
} else if (res.message) {
this.dialogs.open(res.message, { label: res.title }).subscribe()
}
return true // needed to dismiss original modal/alert
} catch (e: any) {

View File

@@ -1049,77 +1049,91 @@ export module Mock {
},
}
export const ActionRes: RR.ActionRes = {
export const ActionResMessage: RR.ActionRes = {
version: '1',
type: 'string',
name: 'New Password',
description:
'Action was run successfully Action was run successfully Action was run successfully Action was run successfully Action was run successfully',
copyable: true,
qr: true,
masked: true,
value: 'iwejdoiewdhbew',
title: 'New Password',
message:
'Action was run successfully and smoothly and fully and all is good on the western front.',
result: null,
}
export const ActionProperties: RR.ActionRes = {
export const ActionResSingle: RR.ActionRes = {
version: '1',
type: 'object',
name: 'Properties',
value: [
{
type: 'string',
name: 'LND Connect',
description: 'This is some information about the thing.',
copyable: true,
qr: true,
masked: true,
value:
'lndconnect://udlyfq2mxa4355pt7cqlrdipnvk2tsl4jtsdw7zaeekenufwcev2wlad.onion:10009?cert=MIICJTCCAcugAwIBAgIRAOyq85fqAiA3U3xOnwhH678wCgYIKoZIzj0EAwIwODEfMB0GAkUEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMB4XDTIwMTAyNjA3MzEyN1oXDTIxMTIyMTA3MzEyN1owODEfMB0GA1UEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKqfhAMMZdY-eFnU5P4bGrQTSx0lo7m8u4V0yYkzUM6jlql_u31_mU2ovLTj56wnZApkEjoPl6fL2yasZA2wiy6OBtTCBsjAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH_BAUwAwEB_zAdBgNVHQ4EFgQUYQ9uIO6spltnVCx4rLFL5BvBF9IwWwYDVR0RBFQwUoIMNTc0OTkwMzIyYzZlgglsb2NhbGhvc3SCBHVuaXiCCnVuaXhwYWNrZXSCB2J1ZmNvbm6HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAGHBKwSAAswCgYIKoZIzj0EAwIDSAAwRQIgVZH2Z2KlyAVY2Q2aIQl0nsvN-OEN49wreFwiBqlxNj4CIQD5_JbpuBFJuf81I5J0FQPtXY-4RppWOPZBb-y6-rkIUQ&macaroon=AgEDbG5kAusBAwoQuA8OUMeQ8Fr2h-f65OdXdRIBMBoWCgdhZGRyZXNzEgRyZWFkEgV3cml0ZRoTCgRpbmZvEgRyZWFkEgV3cml0ZRoXCghpbnZvaWNlcxIEcmVhZBIFd3JpdGUaFAoIbWFjYXJvb24SCGdlbmVyYXRlGhYKB21lc3NhZ2USBHJlYWQSBXdyaXRlGhcKCG9mZmNoYWluEgRyZWFkEgV3cml0ZRoWCgdvbmNoYWluEgRyZWFkEgV3cml0ZRoUCgVwZWVycxIEcmVhZBIFd3JpdGUaGAoGc2lnbmVyEghnZW5lcmF0ZRIEcmVhZAAABiCYsRUoUWuAHAiCSLbBR7b_qULDSl64R8LIU2aqNIyQfA',
},
{
type: 'object',
name: 'Nested Stuff',
description: 'This is a nested thing metric',
value: [
{
type: 'string',
name: 'Last Name',
description: 'The last name of the user',
copyable: true,
qr: true,
masked: false,
value: 'Hill',
},
{
type: 'string',
name: 'Age',
description: 'The age of the user',
copyable: false,
qr: false,
masked: false,
value: '35',
},
{
type: 'string',
name: 'Password',
description: 'A secret password',
copyable: true,
qr: false,
masked: true,
value: 'password123',
},
],
},
{
type: 'string',
name: 'Another Value',
description: 'Some more information about the service.',
copyable: false,
qr: true,
masked: false,
value: 'https://guessagain.com',
},
],
title: 'New Password',
message:
'Action was run successfully and smoothly and fully and all is good on the western front.',
result: {
type: 'single',
copyable: true,
qr: true,
masked: true,
value: 'iwejdoiewdhbew',
},
}
export const ActionResGroup: RR.ActionRes = {
version: '1',
title: 'Properties',
message:
'Successfully retrieved properties. Here is a bunch of useful information about this service.',
result: {
type: 'group',
value: [
{
type: 'single',
name: 'LND Connect',
description: 'This is some information about the thing.',
copyable: true,
qr: true,
masked: true,
value:
'lndconnect://udlyfq2mxa4355pt7cqlrdipnvk2tsl4jtsdw7zaeekenufwcev2wlad.onion:10009?cert=MIICJTCCAcugAwIBAgIRAOyq85fqAiA3U3xOnwhH678wCgYIKoZIzj0EAwIwODEfMB0GAkUEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMB4XDTIwMTAyNjA3MzEyN1oXDTIxMTIyMTA3MzEyN1owODEfMB0GA1UEChMWbG5kIGF1dG9nZW5lcmF0ZWQgY2VydDEVMBMGA1UEAxMMNTc0OTkwMzIyYzZlMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEKqfhAMMZdY-eFnU5P4bGrQTSx0lo7m8u4V0yYkzUM6jlql_u31_mU2ovLTj56wnZApkEjoPl6fL2yasZA2wiy6OBtTCBsjAOBgNVHQ8BAf8EBAMCAqQwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDwYDVR0TAQH_BAUwAwEB_zAdBgNVHQ4EFgQUYQ9uIO6spltnVCx4rLFL5BvBF9IwWwYDVR0RBFQwUoIMNTc0OTkwMzIyYzZlgglsb2NhbGhvc3SCBHVuaXiCCnVuaXhwYWNrZXSCB2J1ZmNvbm6HBH8AAAGHEAAAAAAAAAAAAAAAAAAAAAGHBKwSAAswCgYIKoZIzj0EAwIDSAAwRQIgVZH2Z2KlyAVY2Q2aIQl0nsvN-OEN49wreFwiBqlxNj4CIQD5_JbpuBFJuf81I5J0FQPtXY-4RppWOPZBb-y6-rkIUQ&macaroon=AgEDbG5kAusBAwoQuA8OUMeQ8Fr2h-f65OdXdRIBMBoWCgdhZGRyZXNzEgRyZWFkEgV3cml0ZRoTCgRpbmZvEgRyZWFkEgV3cml0ZRoXCghpbnZvaWNlcxIEcmVhZBIFd3JpdGUaFAoIbWFjYXJvb24SCGdlbmVyYXRlGhYKB21lc3NhZ2USBHJlYWQSBXdyaXRlGhcKCG9mZmNoYWluEgRyZWFkEgV3cml0ZRoWCgdvbmNoYWluEgRyZWFkEgV3cml0ZRoUCgVwZWVycxIEcmVhZBIFd3JpdGUaGAoGc2lnbmVyEghnZW5lcmF0ZRIEcmVhZAAABiCYsRUoUWuAHAiCSLbBR7b_qULDSl64R8LIU2aqNIyQfA',
},
{
type: 'group',
name: 'Nested Stuff',
description: 'This is a nested thing metric',
value: [
{
type: 'single',
name: 'Last Name',
description: 'The last name of the user',
copyable: true,
qr: true,
masked: false,
value: 'Hill',
},
{
type: 'single',
name: 'Age',
description: 'The age of the user',
copyable: false,
qr: false,
masked: false,
value: '35',
},
{
type: 'single',
name: 'Password',
description: 'A secret password',
copyable: true,
qr: false,
masked: true,
value: 'password123',
},
],
},
{
type: 'single',
name: 'Another Value',
description: 'Some more information about the service.',
copyable: false,
qr: true,
masked: false,
value: 'https://guessagain.com',
},
],
},
}
export const getActionInputSpec = async (): Promise<IST.InputSpec> =>

View File

@@ -784,7 +784,9 @@ export class MockApiService extends ApiService {
await pauseFor(2000)
if (params.actionId === 'properties') {
return Mock.ActionProperties
// return Mock.ActionResGroup
return Mock.ActionResMessage
// return Mock.ActionResSingle
} else if (params.actionId === 'config') {
const patch: RemoveOperation[] = [
{
@@ -795,7 +797,8 @@ export class MockApiService extends ApiService {
this.mockRevision(patch)
return null
} else {
return Mock.ActionRes
return Mock.ActionResMessage
// return Mock.ActionResSingle
}
}