Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 23 additions & 2 deletions plugins/inspect/app/components/FunctionRow.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script setup lang="ts">
import type { RpcFunctionInfo } from '../connect'
import ActionButton from '@antfu/design/components/Action/ActionButton.vue'
import { computed, ref } from 'vue'
import FeedbackTip from '@antfu/design/components/Feedback/FeedbackTip.vue'
import { computed, ref, useId } from 'vue'
import FunctionName from './FunctionName.vue'
import JsonView from './JsonView.vue'

Expand All @@ -11,6 +12,7 @@ const props = defineProps<{
results: Record<string, any>
pending: Record<string, boolean>
isStatic: boolean
invokeError?: string
}>()

const emit = defineEmits<{
Expand All @@ -19,6 +21,7 @@ const emit = defineEmits<{
}>()

const expanded = ref(false)
const invokeErrorId = useId()

const clickable = computed(() =>
props.fn.invokable || !!props.fn.agent || props.fn.hasArgs || props.fn.hasReturns,
Expand Down Expand Up @@ -67,7 +70,25 @@ function toggle(): void {
<div class="label">
Invoke with positional args as a JSON array
</div>
<textarea :value="argsInput[fn.name]" class="args" spellcheck="false" placeholder="[]" @input="emit('updateArgs', fn.name, ($event.target as HTMLTextAreaElement).value)" />
<textarea
:value="argsInput[fn.name]"
class="args"
spellcheck="false"
placeholder="[]"
:aria-invalid="!!invokeError"
:aria-describedby="invokeError ? invokeErrorId : undefined"
@input="emit('updateArgs', fn.name, ($event.target as HTMLTextAreaElement).value)"
/>
<FeedbackTip
v-if="invokeError"
:id="invokeErrorId"
class="mt-2"
type="error"
icon="i-ph-warning-circle-duotone"
role="alert"
>
{{ invokeError }}
</FeedbackTip>
<div style="margin-top: 8px; display: flex; gap: 8px; align-items: center;">
<ActionButton
variant="primary"
Expand Down
70 changes: 70 additions & 0 deletions plugins/inspect/app/components/FunctionsView.stories.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import type { RpcFunctionInfo } from '../connect'
import { expect, fn, userEvent, within } from 'storybook/test'
import FunctionsView from './FunctionsView.vue'

const meta = {
Expand Down Expand Up @@ -119,3 +121,71 @@ export const WithResultsAndPending: Story = {
},
},
}

const query: RpcFunctionInfo = {
name: 'audit:echo',
type: 'query',
jsonSerializable: true,
snapshot: false,
cacheable: false,
hasArgs: true,
hasReturns: false,
hasDump: false,
hasSetup: true,
hasHandler: true,
invokable: true,
}

export const InvalidArguments: Story = {
args: {
...Default.args,
functions: [query],
onInvoke: fn(),
},
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement)
await userEvent.click(canvas.getByText('echo'))
const input = canvas.getByPlaceholderText('[]')
const invoke = canvas.getByRole('button', { name: 'Invoke' })

await userEvent.type(input, '{{')
await userEvent.click(invoke)
const error = canvas.getByRole('alert')
await expect(error).toHaveTextContent('Invalid JSON arguments:')
await expect(input).toHaveAttribute('aria-invalid', 'true')
await expect(input).toHaveAttribute('aria-describedby', error.id)
await expect(args.onInvoke).not.toHaveBeenCalled()

await userEvent.clear(input)
await expect(canvas.queryByRole('alert')).not.toBeInTheDocument()
await expect(input).toHaveAttribute('aria-invalid', 'false')
await userEvent.type(input, '[["recovered"]')
await userEvent.click(invoke)
await expect(args.onInvoke).toHaveBeenLastCalledWith(query, ['recovered'])

await userEvent.clear(input)
await userEvent.type(input, '42')
await userEvent.click(invoke)
await expect(args.onInvoke).toHaveBeenLastCalledWith(query, [42])
},
}

export const InvalidArgumentsAfterSuccess: Story = {
args: {
...InvalidArguments.args,
results: {
'audit:echo': { ok: true, result: 'previous result', durationMs: 1 },
},
onInvoke: fn(),
},
play: async ({ args, canvasElement }) => {
const canvas = within(canvasElement)
await userEvent.click(canvas.getByText('echo'))
const input = canvas.getByPlaceholderText('[]')
await userEvent.type(input, '{{')
await userEvent.click(canvas.getByRole('button', { name: 'Invoke' }))
await expect(canvas.getByRole('alert')).toHaveTextContent('Invalid JSON arguments:')
await expect(canvas.getByText('"previous result"')).toBeVisible()
await expect(args.onInvoke).not.toHaveBeenCalled()
},
}
13 changes: 11 additions & 2 deletions plugins/inspect/app/components/FunctionsView.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const emit = defineEmits<{

const search = ref('')
const argsInput = reactive<Record<string, string>>({})
const invokeErrors = reactive<Record<string, string>>({})

const filtered = computed(() => {
const list = props.functions ?? []
Expand All @@ -38,10 +39,17 @@ function invoke(fn: RpcFunctionInfo): void {
parsed = Array.isArray(raw) ? raw : [raw]
}
catch (e) {
throw new Error(`Invalid JSON args: ${(e as Error).message}`)
invokeErrors[fn.name] = `Invalid JSON arguments: ${(e as Error).message}`
return
}
delete invokeErrors[fn.name]
emit('invoke', fn, parsed)
}

function updateArgs(name: string, value: string): void {
argsInput[name] = value
delete invokeErrors[name]
}
</script>

<template>
Expand Down Expand Up @@ -70,11 +78,12 @@ function invoke(fn: RpcFunctionInfo): void {
:key="fn.name"
:fn="fn"
:args-input="argsInput"
:invoke-error="invokeErrors[fn.name]"
:results="results"
:pending="pending"
:is-static="isStatic"
@invoke="invoke"
@update-args="(n, v) => argsInput[n] = v"
@update-args="updateArgs"
/>
</div>
</div>
Expand Down
Loading