diff --git a/ui/src/core/api/client.ts b/ui/src/core/api/client.ts index 6fec427a..f8d114c6 100644 --- a/ui/src/core/api/client.ts +++ b/ui/src/core/api/client.ts @@ -8,6 +8,8 @@ import type { InitAgentRequestBody, ListAgentsQueryParams, PatchControlRequest, + RenderControlTemplateRequest, + RenderControlTemplateResponse, SetControlDataRequest, ValidateControlDataRequest, ValidateControlDataResponse, @@ -209,6 +211,23 @@ export const api = { }, }), }, + controlTemplates: { + render: async (data: RenderControlTemplateRequest) => { + const res = await fetch(`${API_URL}/api/v1/control-templates/render`, { + method: 'POST', + credentials: 'include', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }); + const body = await res.json(); + if (!res.ok) return { data: undefined, error: body, response: res }; + return { + data: body as RenderControlTemplateResponse, + error: undefined, + response: res, + }; + }, + }, policies: { create: (name: string) => apiClient.PUT('/api/v1/policies', { body: { name } }), diff --git a/ui/src/core/api/types.ts b/ui/src/core/api/types.ts index 136d54eb..dfdfc3fa 100644 --- a/ui/src/core/api/types.ts +++ b/ui/src/core/api/types.ts @@ -111,6 +111,75 @@ export type ListControlsResponse = export type AgentRef = components['schemas']['AgentRef']; +// ============================================================================= +// Template Types (manual until API types are regenerated) +// ============================================================================= + +export type TemplateValue = string | boolean | string[]; + +export type TemplateParameterBase = { + label: string; + description?: string | null; + required?: boolean; + ui_hint?: string | null; +}; + +export type StringTemplateParameter = TemplateParameterBase & { + type: 'string'; + default?: string | null; + placeholder?: string | null; +}; + +export type StringListTemplateParameter = TemplateParameterBase & { + type: 'string_list'; + default?: string[] | null; + placeholder?: string[] | null; +}; + +export type EnumTemplateParameter = TemplateParameterBase & { + type: 'enum'; + allowed_values: string[]; + default?: string | null; +}; + +export type BooleanTemplateParameter = TemplateParameterBase & { + type: 'boolean'; + default?: boolean | null; +}; + +export type RegexTemplateParameter = TemplateParameterBase & { + type: 'regex_re2'; + default?: string | null; + placeholder?: string | null; +}; + +export type TemplateParameterDefinition = + | StringTemplateParameter + | StringListTemplateParameter + | EnumTemplateParameter + | BooleanTemplateParameter + | RegexTemplateParameter; + +export type TemplateDefinition = { + description?: string | null; + parameters: Record; + definition_template: unknown; +}; + +export type TemplateControlInput = { + template: TemplateDefinition; + template_values: Record; +}; + +export type RenderControlTemplateRequest = { + template: TemplateDefinition; + template_values: Record; +}; + +export type RenderControlTemplateResponse = { + control: ControlDefinition; +}; + // Helper type to extract query parameters from operations type ExtractQueryParams = T extends { parameters: { query?: infer Q } } ? Q diff --git a/ui/src/core/components/template-param-form.tsx b/ui/src/core/components/template-param-form.tsx new file mode 100644 index 00000000..00539c9f --- /dev/null +++ b/ui/src/core/components/template-param-form.tsx @@ -0,0 +1,172 @@ +import { + Select, + Stack, + Switch, + TagsInput, + Text, + TextInput, +} from '@mantine/core'; +import { useMemo } from 'react'; + +import type { + TemplateDefinition, + TemplateParameterDefinition, + TemplateValue, +} from '@/core/api/types'; + +import { labelPropsInline, LabelWithTooltip } from './label-with-tooltip'; + +type TemplateParamFormProps = { + template: TemplateDefinition; + values: Record; + onChange: (values: Record) => void; + errors?: Record; +}; + +function paramLabel( + name: string, + param: TemplateParameterDefinition +): React.ReactNode { + if (param.description) { + return ; + } + return param.label; +} + +function ParameterInput({ + name, + param, + value, + error, + onChangeValue, +}: { + name: string; + param: TemplateParameterDefinition; + value: TemplateValue | undefined; + error?: string; + onChangeValue: (name: string, value: TemplateValue) => void; +}) { + const label = paramLabel(name, param); + const isRequired = param.required !== false; + + switch (param.type) { + case 'string': + return ( + onChangeValue(name, e.currentTarget.value)} + error={error} + size="sm" + /> + ); + + case 'string_list': + return ( + onChangeValue(name, val)} + error={error} + size="sm" + /> + ); + + case 'enum': + return ( +