From 6e5b8ec33da05dc2ebdf4e2287748e1c838171f7 Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Fri, 21 Feb 2025 16:04:54 +0000 Subject: [PATCH 1/9] wip on react-hook-form useFieldArray --- package-lock.json | 46 ++- package.json | 3 +- .../workspace-muxes-fields-array.tsx | 286 ++++++++++++++++++ .../components/workspace-muxing-model.tsx | 111 ++++--- .../workspace/lib/workspace-config-schema.ts | 21 ++ ...query-list-all-models-for-all-providers.ts | 10 +- 6 files changed, 409 insertions(+), 68 deletions(-) create mode 100644 src/features/workspace/components/workspace-muxes-fields-array.tsx create mode 100644 src/features/workspace/lib/workspace-config-schema.ts diff --git a/package-lock.json b/package-lock.json index fe141cf9..ef316d44 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,6 +11,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@hey-api/client-fetch": "^0.7.1", + "@hookform/resolvers": "^4.1.0", "@jsonforms/core": "^3.5.1", "@jsonforms/react": "^3.5.1", "@jsonforms/vanilla-renderers": "^3.5.1", @@ -18,7 +19,7 @@ "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", - "@stacklok/ui-kit": "^1.0.1-4", + "@stacklok/ui-kit": "^1.0.1-9", "@tanstack/react-query": "^5.64.1", "@tanstack/react-query-devtools": "^5.66.0", "@types/lodash": "^4.17.15", @@ -1185,6 +1186,18 @@ "node": ">=18" } }, + "node_modules/@hookform/resolvers": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-4.1.0.tgz", + "integrity": "sha512-fX/uHKb+OOCpACLc6enuTQsf0ZpRrKbeBBPETg5PCPLCIYV6osP2Bw6ezuclM61lH+wBF9eXcuC0+BFh9XOEnQ==", + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001698" + }, + "peerDependencies": { + "react-hook-form": "^7.0.0" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -4076,18 +4089,20 @@ } }, "node_modules/@stacklok/ui-kit": { - "version": "1.0.1-4", - "resolved": "https://registry.npmjs.org/@stacklok/ui-kit/-/ui-kit-1.0.1-4.tgz", - "integrity": "sha512-Az5mQmb+0P7pMUKVyhJLEpDCzGGtSFi0H0C2Us32gM4Fsz78FL92MHvwdWMZgcepdwbdQoR/C6JASwSHteAbmA==", + "version": "1.0.1-9", + "resolved": "https://registry.npmjs.org/@stacklok/ui-kit/-/ui-kit-1.0.1-9.tgz", + "integrity": "sha512-TM7ajXb43bCx/b/SKuvkFS2MKBo6xThrt6mmBMMYpbgM1aD74nwSaSXuNdYW7/PaTU2hFnzdC7T7wNEHMAJQNw==", "license": "ISC", "dependencies": { "@fontsource-variable/figtree": "^5.1.1", "@fontsource-variable/inter": "^5.1.0", "@fontsource-variable/source-code-pro": "^5.1.0", + "@hookform/resolvers": "4.1.0", "@untitled-ui/icons-react": "^0.1.4", "postcss": "^8.4.47", "react-aria": "3.36.0", "react-aria-components": "1.5.0", + "react-hook-form": "7.47.0", "react-stately": "3.34.0", "sonner": "^1.7.1", "tailwind-merge": "^2.5.2", @@ -5846,10 +5861,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001690", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", - "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", - "dev": true, + "version": "1.0.30001700", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001700.tgz", + "integrity": "sha512-2S6XIXwaE7K7erT8dY+kLQcpa5ms63XlRkMkReXjle+kf6c5g38vyMl+Z5y8dSxOFDhcFe+nxnn261PLxBSQsQ==", "funding": [ { "type": "opencollective", @@ -11886,6 +11900,22 @@ "react": "^19.0.0" } }, + "node_modules/react-hook-form": { + "version": "7.47.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.47.0.tgz", + "integrity": "sha512-F/TroLjTICipmHeFlMrLtNLceO2xr1jU3CyiNla5zdwsGUGu2UOxxR4UyJgLlhMwLW/Wzp4cpJ7CPfgJIeKdSg==", + "license": "MIT", + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-markdown": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-9.0.1.tgz", diff --git a/package.json b/package.json index 4478ea30..a7254410 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "@dnd-kit/core": "^6.3.1", "@dnd-kit/sortable": "^10.0.0", "@hey-api/client-fetch": "^0.7.1", + "@hookform/resolvers": "^4.1.0", "@jsonforms/core": "^3.5.1", "@jsonforms/react": "^3.5.1", "@jsonforms/vanilla-renderers": "^3.5.1", @@ -31,7 +32,7 @@ "@radix-ui/react-dialog": "^1.1.4", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", - "@stacklok/ui-kit": "^1.0.1-4", + "@stacklok/ui-kit": "^1.0.1-9", "@tanstack/react-query": "^5.64.1", "@tanstack/react-query-devtools": "^5.66.0", "@types/lodash": "^4.17.15", diff --git a/src/features/workspace/components/workspace-muxes-fields-array.tsx b/src/features/workspace/components/workspace-muxes-fields-array.tsx new file mode 100644 index 00000000..3ea03a99 --- /dev/null +++ b/src/features/workspace/components/workspace-muxes-fields-array.tsx @@ -0,0 +1,286 @@ +import { + Button, + ComboBoxButton, + ComboBoxClearButton, + ComboBoxFieldGroup, + ComboBoxInput, + FormComboBox, + FormTextField, + Input, + Label, + OptionsSchema, + TextField, + Tooltip, + TooltipInfoButton, + TooltipTrigger, +} from '@stacklok/ui-kit' +import { useFieldArray, useFormContext } from 'react-hook-form' +import { + MUX_FIELD_NAME, + WORKSPACE_CONFIG_FIELD_NAME, + WorkspaceMuxFieldValues, +} from '../lib/workspace-config-schema' +import { useQueryListAllModelsForAllProviders } from '@/hooks/use-query-list-all-models-for-all-providers' +import { ModelByProvider, MuxMatcherType, MuxRule } from '@/api/generated' +import { groupBy, map, uniqueId } from 'lodash' +import { + BracketsSlash, + DotsGrid, + GridDotsTop, + Plus, + SearchMd, + Trash01, +} from '@untitled-ui/icons-react' +import { tv } from 'tailwind-variants' +import { + SortableContext, + sortableKeyboardCoordinates, + useSortable, + verticalListSortingStrategy, +} from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import { + closestCenter, + DndContext, + DragEndEvent, + KeyboardSensor, + PointerSensor, + UniqueIdentifier, + useSensor, + useSensors, +} from '@dnd-kit/core' +import { ReactNode, useCallback } from 'react' +import { twMerge } from 'tailwind-merge' + +function getMuxComponentName({ + field, + index, +}: { + index: number + field: (typeof MUX_FIELD_NAME)[keyof typeof MUX_FIELD_NAME] +}) { + return `${MUX_FIELD_NAME}.${index}.${field}` +} + +function groupModels( + models: ModelByProvider[] = [] +): OptionsSchema<'listbox'>[] { + return map(groupBy(models, 'provider_name'), (items, providerName) => ({ + id: providerName, + textValue: providerName, + items: items.map((item) => ({ + id: `${item.provider_id}/${item.name}`, + textValue: item.name, + })), + })) +} + +function getIndicesOnDragEnd( + event: DragEndEvent, + items: T[] +): { + from: number + to: number +} | null { + const { active, over } = event + + if (over == null || active.id || over.id) return null // no-op + + const from = items.findIndex(({ id }) => id === active.id) + const to = items.findIndex(({ id }) => id === over.id) + + return { + from, + to, + } +} + +function DndSortProvider({ + children, + onDragEnd, + items, +}: { + children: ReactNode + onDragEnd: (event: DragEndEvent) => void + items: T[] +}) { + const sensors = useSensors( + useSensor(PointerSensor), + useSensor(KeyboardSensor, { + coordinateGetter: sortableKeyboardCoordinates, + }) + ) + + return ( + + + {children} + + + ) +} + +const gridStyles = tv({ + base: 'grid grid-cols-[2fr_1fr_2.5rem] items-center gap-2', +}) + +function Labels() { + return ( +
+ + +
+ ) +} + +function DragHandle({ item }: { item: MuxRule & { id: string } }) { + const { attributes, listeners, setNodeRef } = useSortable({ id: item.id }) + + return ( +
+ +
+ ) +} + +function MuxRuleRow({ + index, + item, + models, + hasDragDisabled, +}: { + index: number + item: MuxRule & { id: string } + models: OptionsSchema<'listbox'>[] + hasDragDisabled: boolean +}) { + console.debug('👉 item:', item) + + const isCatchAll = item.matcher_type === MuxMatcherType.CATCH_ALL + + const { transform, transition } = useSortable({ id: item.id }) + const style = { + transform: CSS.Transform.toString(transform), + transition, + } + + return ( +
  • + + } /> + + + + + } + isBorderless + placeholder="Type to search..." + /> + + + + + +
  • + ) +} + +export function WorkspaceMuxesFieldsArray() { + const { control } = useFormContext() + + const { fields, swap, prepend } = useFieldArray({ + control, + name: WORKSPACE_CONFIG_FIELD_NAME.muxing_rules, + }) + + const { data: models = [] } = useQueryListAllModelsForAllProviders({ + select: groupModels, + }) + + const onDragEnd = useCallback( + (event: DragEndEvent) => { + const { from, to } = getIndicesOnDragEnd(event, fields) || {} + if (from && to) swap(from, to) + }, + [fields, swap] + ) + + return ( + <> + + +
      + {fields.map((item, index) => ( + + ))} +
    +
    + +
    + + + {/* + Manage providers + */} +
    + + ) +} diff --git a/src/features/workspace/components/workspace-muxing-model.tsx b/src/features/workspace/components/workspace-muxing-model.tsx index 44e8fe6b..fc48f093 100644 --- a/src/features/workspace/components/workspace-muxing-model.tsx +++ b/src/features/workspace/components/workspace-muxing-model.tsx @@ -4,7 +4,8 @@ import { Card, CardBody, CardFooter, - Form, + FormSubmitButton, + FormV2, Input, Label, Link, @@ -21,7 +22,6 @@ import { MuxMatcherType, V1ListAllModelsForAllProvidersResponse, } from '@/api/generated' -import { FormEvent } from 'react' import { LayersThree01, LinkExternal01, @@ -37,6 +37,24 @@ import { useMuxingRulesFormState, } from '../hooks/use-muxing-rules-form-workspace' import { FormButtons } from '@/components/FormButtons' +import { WorkspaceMuxesFieldsArray } from './workspace-muxes-fields-array' +import { + schemaWorkspaceConfig, + WorkspaceConfigFieldValues, +} from '../lib/workspace-config-schema' +import { zodResolver } from '@hookform/resolvers/zod' + +const DEFAULT_VALUES: WorkspaceConfigFieldValues = { + muxing_rules: [ + { + provider_id: '', + provider_name: '', + model: '', + matcher: '', + matcher_type: MuxMatcherType.CATCH_ALL, + }, + ], +} function MissingProviderBanner() { return ( @@ -80,19 +98,17 @@ function SortableItem({ const placeholder = isDefaultRule ? 'Catch-all' : 'e.g. file type, file name' return (
    -
    - { - setRuleItem({ ...rule, matcher }) - }} - > - - -
    + { + setRuleItem({ ...rule, matcher }) + }} + > + +
    1 - const handleSubmit = (event: FormEvent) => { - event.preventDefault() - mutateAsync( - { - path: { workspace_name: workspaceName }, - body: rules.map(({ id, ...rest }) => { - void id - - return rest.matcher - ? { ...rest, matcher_type: MuxMatcherType.FILENAME_MATCH } - : { ...rest } - }), - }, - { - onSuccess: () => { - formState.setInitialValues({ rules }) - }, - } - ) + const handleSubmit = (data: WorkspaceConfigFieldValues) => { + mutateAsync({ + path: { workspace_name: workspaceName }, + body: data.muxing_rules.map((rule) => { + return rule.matcher + ? { ...rule, matcher_type: MuxMatcherType.FILENAME_MATCH } + : { ...rule } + }), + }) } if (isModelsEmpty) { @@ -174,10 +180,13 @@ export function WorkspaceMuxingModel({ } return ( -
    onSubmit={handleSubmit} - validationBehavior="aria" data-testid="preferred-model" + options={{ + defaultValues: DEFAULT_VALUES, + resolver: zodResolver(schemaWorkspaceConfig), + }} > @@ -198,25 +207,8 @@ export function WorkspaceMuxingModel({
    -
    -
     
    -
    - -
    -
    - -
    -
    + +
    - +
    + +
    + {/*
    + + {/* + Manage providers + */} +
    + + ) +} diff --git a/src/features/workspace/components/form-mux-rule-row.tsx b/src/features/workspace/components/form-mux-rule-row.tsx new file mode 100644 index 00000000..32b5b50a --- /dev/null +++ b/src/features/workspace/components/form-mux-rule-row.tsx @@ -0,0 +1,53 @@ +import { MuxMatcherType } from '@/api/generated' +import { useSortable } from '@dnd-kit/sortable' +import { CSS } from '@dnd-kit/utilities' +import { Button } from '@stacklok/ui-kit' +import { tv } from 'tailwind-variants' +import { twMerge } from 'tailwind-merge' +import { FormMuxComboboxModel } from './form-mux-combobox-model' +import { Trash01 } from '@untitled-ui/icons-react' +import { FormMuxTextFieldMatcher } from './form-mux-text-field-matcher' +import { FieldValuesMuxRow } from '../lib/schema-mux' + +const gridStyles = tv({ + base: 'grid grid-cols-[2fr_1fr_2.5rem] items-center gap-2', +}) + +export function FormMuxRuleRow({ + index, + item, +}: { + index: number + item: FieldValuesMuxRow +}) { + const isCatchAll = item.matcher_type === MuxMatcherType.CATCH_ALL + + const { transform, transition } = useSortable({ id: item.id }) + const style = { + transform: CSS.Transform.toString(transform), + transition, + } + + return ( +
  • + + + + +
  • + ) +} diff --git a/src/features/workspace/components/form-mux-select-matcher-type.tsx b/src/features/workspace/components/form-mux-select-matcher-type.tsx new file mode 100644 index 00000000..d4bcef09 --- /dev/null +++ b/src/features/workspace/components/form-mux-select-matcher-type.tsx @@ -0,0 +1,31 @@ +import { MuxMatcherType } from '@/api/generated' +import { FormSelect, SelectButton } from '@stacklok/ui-kit' +import { getMuxFieldName } from '../lib/get-mux-field-name' + +export function FormMuxComboboxModel({ index }: { index: number }) { + return ( + + + + ) +} diff --git a/src/features/workspace/components/form-mux-text-field-matcher.tsx b/src/features/workspace/components/form-mux-text-field-matcher.tsx new file mode 100644 index 00000000..6c77e363 --- /dev/null +++ b/src/features/workspace/components/form-mux-text-field-matcher.tsx @@ -0,0 +1,33 @@ +import { FormTextField, Input } from '@stacklok/ui-kit' +import { getMuxFieldName } from '../lib/get-mux-field-name' +import { FormMuxDragToReorderButton } from './form-mux-drag-to-reorder-button' +import { FieldValuesMuxRow } from '../lib/schema-mux' + +export function FormMuxTextFieldMatcher({ + index, + isCatchAll, + item, +}: { + index: number + isCatchAll: boolean + item: FieldValuesMuxRow +}) { + console.debug('👉 isCatchAll:', isCatchAll) + return ( + + + } + /> + + ) +} diff --git a/src/features/workspace/components/form-mux.tsx b/src/features/workspace/components/form-mux.tsx new file mode 100644 index 00000000..11a9fecb --- /dev/null +++ b/src/features/workspace/components/form-mux.tsx @@ -0,0 +1,131 @@ +import { + Alert, + Card, + CardBody, + CardFooter, + FormSubmitButton, + FormV2, + Link, + LinkButton, + Text, +} from '@stacklok/ui-kit' +import { twMerge } from 'tailwind-merge' +import { useMutationPreferredModelWorkspace } from '../hooks/use-mutation-preferred-model-workspace' +import { MuxMatcherType } from '@/api/generated' +import { LinkExternal01 } from '@untitled-ui/icons-react' +import { useQueryListAllModelsForAllProviders } from '@/hooks/use-query-list-all-models-for-all-providers' +import { useQueryMuxingRulesWorkspace } from '../hooks/use-query-muxing-rules-workspace' +import { FormMuxFieldsArray } from './form-mux-fields-array' +import { + schemaWorkspaceConfig, + WorkspaceConfigFieldValues, +} from '../lib/schema-mux' +import { zodResolver } from '@hookform/resolvers/zod' + +const DEFAULT_VALUES: WorkspaceConfigFieldValues = { + muxing_rules: [ + { + id: MuxMatcherType.CATCH_ALL, + model: '', + matcher: '', + matcher_type: MuxMatcherType.CATCH_ALL, + }, + ], +} + +function MissingProviderBanner() { + return ( + // TODO needs to update the related ui-kit component that diverges from the design + + + Configure a provider + + + ) +} + +export function WorkspaceMuxingModel({ + className, + workspaceName, + isArchived, +}: { + className?: string + workspaceName: string + isArchived: boolean | undefined +}) { + const { data: muxingRules, isPending } = + useQueryMuxingRulesWorkspace(workspaceName) + + const { mutateAsync } = useMutationPreferredModelWorkspace() + const { data: providerModels = [] } = useQueryListAllModelsForAllProviders() + const isModelsEmpty = !isPending && providerModels.length === 0 + + const handleSubmit = (data: WorkspaceConfigFieldValues) => { + mutateAsync({ + path: { workspace_name: workspaceName }, + body: data.muxing_rules.map((rule) => { + return rule.matcher + ? { ...rule, matcher_type: MuxMatcherType.FILENAME_MATCH } + : { ...rule } + }), + }) + } + + if (isModelsEmpty) { + return ( + + + Model Muxing + + + + ) + } + + return ( + + onSubmit={handleSubmit} + data-testid="preferred-model" + options={{ + defaultValues: DEFAULT_VALUES, + resolver: zodResolver(schemaWorkspaceConfig), + }} + > + + +
    + Model Muxing + + Select the model you would like to use in this workspace. This + section applies only if you are using the MUX endpoint. + + Learn more + + +
    + +
    + +
    +
    +
    + + + + +
    + + ) +} diff --git a/src/features/workspace/components/workspace-muxes-fields-array.tsx b/src/features/workspace/components/workspace-muxes-fields-array.tsx deleted file mode 100644 index 3ea03a99..00000000 --- a/src/features/workspace/components/workspace-muxes-fields-array.tsx +++ /dev/null @@ -1,286 +0,0 @@ -import { - Button, - ComboBoxButton, - ComboBoxClearButton, - ComboBoxFieldGroup, - ComboBoxInput, - FormComboBox, - FormTextField, - Input, - Label, - OptionsSchema, - TextField, - Tooltip, - TooltipInfoButton, - TooltipTrigger, -} from '@stacklok/ui-kit' -import { useFieldArray, useFormContext } from 'react-hook-form' -import { - MUX_FIELD_NAME, - WORKSPACE_CONFIG_FIELD_NAME, - WorkspaceMuxFieldValues, -} from '../lib/workspace-config-schema' -import { useQueryListAllModelsForAllProviders } from '@/hooks/use-query-list-all-models-for-all-providers' -import { ModelByProvider, MuxMatcherType, MuxRule } from '@/api/generated' -import { groupBy, map, uniqueId } from 'lodash' -import { - BracketsSlash, - DotsGrid, - GridDotsTop, - Plus, - SearchMd, - Trash01, -} from '@untitled-ui/icons-react' -import { tv } from 'tailwind-variants' -import { - SortableContext, - sortableKeyboardCoordinates, - useSortable, - verticalListSortingStrategy, -} from '@dnd-kit/sortable' -import { CSS } from '@dnd-kit/utilities' -import { - closestCenter, - DndContext, - DragEndEvent, - KeyboardSensor, - PointerSensor, - UniqueIdentifier, - useSensor, - useSensors, -} from '@dnd-kit/core' -import { ReactNode, useCallback } from 'react' -import { twMerge } from 'tailwind-merge' - -function getMuxComponentName({ - field, - index, -}: { - index: number - field: (typeof MUX_FIELD_NAME)[keyof typeof MUX_FIELD_NAME] -}) { - return `${MUX_FIELD_NAME}.${index}.${field}` -} - -function groupModels( - models: ModelByProvider[] = [] -): OptionsSchema<'listbox'>[] { - return map(groupBy(models, 'provider_name'), (items, providerName) => ({ - id: providerName, - textValue: providerName, - items: items.map((item) => ({ - id: `${item.provider_id}/${item.name}`, - textValue: item.name, - })), - })) -} - -function getIndicesOnDragEnd( - event: DragEndEvent, - items: T[] -): { - from: number - to: number -} | null { - const { active, over } = event - - if (over == null || active.id || over.id) return null // no-op - - const from = items.findIndex(({ id }) => id === active.id) - const to = items.findIndex(({ id }) => id === over.id) - - return { - from, - to, - } -} - -function DndSortProvider({ - children, - onDragEnd, - items, -}: { - children: ReactNode - onDragEnd: (event: DragEndEvent) => void - items: T[] -}) { - const sensors = useSensors( - useSensor(PointerSensor), - useSensor(KeyboardSensor, { - coordinateGetter: sortableKeyboardCoordinates, - }) - ) - - return ( - - - {children} - - - ) -} - -const gridStyles = tv({ - base: 'grid grid-cols-[2fr_1fr_2.5rem] items-center gap-2', -}) - -function Labels() { - return ( -
    - - -
    - ) -} - -function DragHandle({ item }: { item: MuxRule & { id: string } }) { - const { attributes, listeners, setNodeRef } = useSortable({ id: item.id }) - - return ( -
    - -
    - ) -} - -function MuxRuleRow({ - index, - item, - models, - hasDragDisabled, -}: { - index: number - item: MuxRule & { id: string } - models: OptionsSchema<'listbox'>[] - hasDragDisabled: boolean -}) { - console.debug('👉 item:', item) - - const isCatchAll = item.matcher_type === MuxMatcherType.CATCH_ALL - - const { transform, transition } = useSortable({ id: item.id }) - const style = { - transform: CSS.Transform.toString(transform), - transition, - } - - return ( -
  • - - } /> - - - - - } - isBorderless - placeholder="Type to search..." - /> - - - - - -
  • - ) -} - -export function WorkspaceMuxesFieldsArray() { - const { control } = useFormContext() - - const { fields, swap, prepend } = useFieldArray({ - control, - name: WORKSPACE_CONFIG_FIELD_NAME.muxing_rules, - }) - - const { data: models = [] } = useQueryListAllModelsForAllProviders({ - select: groupModels, - }) - - const onDragEnd = useCallback( - (event: DragEndEvent) => { - const { from, to } = getIndicesOnDragEnd(event, fields) || {} - if (from && to) swap(from, to) - }, - [fields, swap] - ) - - return ( - <> - - -
      - {fields.map((item, index) => ( - - ))} -
    -
    - -
    - - - {/* - Manage providers - */} -
    - - ) -} diff --git a/src/features/workspace/components/workspace-muxing-model.tsx b/src/features/workspace/components/workspace-muxing-model.tsx deleted file mode 100644 index fc48f093..00000000 --- a/src/features/workspace/components/workspace-muxing-model.tsx +++ /dev/null @@ -1,263 +0,0 @@ -import { - Alert, - Button, - Card, - CardBody, - CardFooter, - FormSubmitButton, - FormV2, - Input, - Label, - Link, - LinkButton, - Text, - TextField, - Tooltip, - TooltipInfoButton, - TooltipTrigger, -} from '@stacklok/ui-kit' -import { twMerge } from 'tailwind-merge' -import { useMutationPreferredModelWorkspace } from '../hooks/use-mutation-preferred-model-workspace' -import { - MuxMatcherType, - V1ListAllModelsForAllProvidersResponse, -} from '@/api/generated' -import { - LayersThree01, - LinkExternal01, - Plus, - Trash01, -} from '@untitled-ui/icons-react' -import { SortableArea } from '@/components/SortableArea' -import { WorkspaceModelsDropdown } from './workspace-models-dropdown' -import { useQueryListAllModelsForAllProviders } from '@/hooks/use-query-list-all-models-for-all-providers' -import { useQueryMuxingRulesWorkspace } from '../hooks/use-query-muxing-rules-workspace' -import { - PreferredMuxRule, - useMuxingRulesFormState, -} from '../hooks/use-muxing-rules-form-workspace' -import { FormButtons } from '@/components/FormButtons' -import { WorkspaceMuxesFieldsArray } from './workspace-muxes-fields-array' -import { - schemaWorkspaceConfig, - WorkspaceConfigFieldValues, -} from '../lib/workspace-config-schema' -import { zodResolver } from '@hookform/resolvers/zod' - -const DEFAULT_VALUES: WorkspaceConfigFieldValues = { - muxing_rules: [ - { - provider_id: '', - provider_name: '', - model: '', - matcher: '', - matcher_type: MuxMatcherType.CATCH_ALL, - }, - ], -} - -function MissingProviderBanner() { - return ( - // TODO needs to update the related ui-kit component that diverges from the design - - - Configure a provider - - - ) -} - -type SortableItemProps = { - index: number - rule: PreferredMuxRule - models: V1ListAllModelsForAllProvidersResponse - isArchived: boolean - showRemoveButton: boolean - isDefaultRule: boolean - setRuleItem: (rule: PreferredMuxRule) => void - removeRule: (index: number) => void -} - -function SortableItem({ - rule, - index, - setRuleItem, - removeRule, - models, - showRemoveButton, - isArchived, - isDefaultRule, -}: SortableItemProps) { - const placeholder = isDefaultRule ? 'Catch-all' : 'e.g. file type, file name' - return ( -
    - { - setRuleItem({ ...rule, matcher }) - }} - > - - -
    - - setRuleItem({ ...rule, provider_id, model }) - } - /> - {showRemoveButton && !isDefaultRule ? ( - - ) : ( -
    - )} -
    -
    - ) -} - -export function WorkspaceMuxingModel({ - className, - workspaceName, - isArchived, -}: { - className?: string - workspaceName: string - isArchived: boolean | undefined -}) { - const { data: muxingRules, isPending } = - useQueryMuxingRulesWorkspace(workspaceName) - const { addRule, setRules, setRuleItem, removeRule, formState } = - useMuxingRulesFormState(muxingRules) - const { - values: { rules }, - } = formState - - const { mutateAsync } = useMutationPreferredModelWorkspace() - const { data: providerModels = [] } = useQueryListAllModelsForAllProviders() - const isModelsEmpty = !isPending && providerModels.length === 0 - const showRemoveButton = rules.length > 1 - - const handleSubmit = (data: WorkspaceConfigFieldValues) => { - mutateAsync({ - path: { workspace_name: workspaceName }, - body: data.muxing_rules.map((rule) => { - return rule.matcher - ? { ...rule, matcher_type: MuxMatcherType.FILENAME_MATCH } - : { ...rule } - }), - }) - } - - if (isModelsEmpty) { - return ( - - - Model Muxing - - - - ) - } - - return ( - - onSubmit={handleSubmit} - data-testid="preferred-model" - options={{ - defaultValues: DEFAULT_VALUES, - resolver: zodResolver(schemaWorkspaceConfig), - }} - > - - -
    - Model Muxing - - Select the model you would like to use in this workspace. This - section applies only if you are using the MUX endpoint. - - Learn more - - -
    - -
    - - - - {(rule, index) => { - const isDefaultRule = rules.length - 1 === index - return ( - - ) - }} - -
    -
    -
    - -
    - {/* -
    - - - - Manage providers - -
    - -
    */} -
    - - ) -} diff --git a/src/features/workspace/lib/get-mux-field-name.ts b/src/features/workspace/lib/get-mux-field-name.ts new file mode 100644 index 00000000..a1b39038 --- /dev/null +++ b/src/features/workspace/lib/get-mux-field-name.ts @@ -0,0 +1,11 @@ +import { MUX_FIELD_NAME } from './schema-mux' + +export function getMuxFieldName({ + field, + index, +}: { + index: number + field: (typeof MUX_FIELD_NAME)[keyof typeof MUX_FIELD_NAME] +}) { + return `${MUX_FIELD_NAME}.${index}.${field}` +} diff --git a/src/features/workspace/lib/schema-mux.ts b/src/features/workspace/lib/schema-mux.ts new file mode 100644 index 00000000..81a06b6d --- /dev/null +++ b/src/features/workspace/lib/schema-mux.ts @@ -0,0 +1,24 @@ +import { MuxMatcherType } from '@/api/generated' +import { z } from 'zod' + +const schemaMuxCatchAllId = z.literal(MuxMatcherType.CATCH_ALL) + +export type MuxCatchallId = z.infer + +const schemaMuxRow = z.object({ + model: z.string().optional(), + matcher_type: z.nativeEnum(MuxMatcherType), + matcher: z.string(), + id: z.union([z.string().uuid(), schemaMuxCatchAllId]), +}) + +export type FieldValuesMuxRow = z.infer + +export const schemaWorkspaceConfig = z.object({ + muxing_rules: z.array(schemaMuxRow), +}) + +export type WorkspaceConfigFieldValues = z.infer + +export const WORKSPACE_CONFIG_FIELD_NAME = schemaWorkspaceConfig.keyof().Enum +export const MUX_FIELD_NAME = schemaMuxRow.keyof().Enum diff --git a/src/features/workspace/lib/workspace-config-schema.ts b/src/features/workspace/lib/workspace-config-schema.ts deleted file mode 100644 index e53317f3..00000000 --- a/src/features/workspace/lib/workspace-config-schema.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { MuxMatcherType } from '@/api/generated' -import { z } from 'zod' - -const schemaWorkspaceMux = z.object({ - provider_name: z.string().nullable(), - provider_id: z.string().uuid(), - model: z.string(), - matcher_type: z.nativeEnum(MuxMatcherType), - matcher: z.string().nullable(), -}) - -export type WorkspaceMuxFieldValues = z.infer - -export const schemaWorkspaceConfig = z.object({ - muxing_rules: z.array(schemaWorkspaceMux), -}) - -export type WorkspaceConfigFieldValues = z.infer - -export const WORKSPACE_CONFIG_FIELD_NAME = schemaWorkspaceConfig.keyof().Enum -export const MUX_FIELD_NAME = schemaWorkspaceMux.keyof().Enum diff --git a/src/routes/route-workspace.tsx b/src/routes/route-workspace.tsx index d5df90ed..0ff8e3da 100644 --- a/src/routes/route-workspace.tsx +++ b/src/routes/route-workspace.tsx @@ -7,7 +7,7 @@ import { useParams } from 'react-router-dom' import { useArchivedWorkspaces } from '@/features/workspace/hooks/use-archived-workspaces' import { useRestoreWorkspaceButton } from '@/features/workspace/hooks/use-restore-workspace-button' import { WorkspaceCustomInstructions } from '@/features/workspace/components/workspace-custom-instructions' -import { WorkspaceMuxingModel } from '@/features/workspace/components/workspace-muxing-model' +import { WorkspaceMuxingModel } from '@/features/workspace/components/form-mux' import { PageContainer } from '@/components/page-container' import { WorkspaceActivateButton } from '@/features/workspace/components/workspace-activate-button' From 9f4778d42c0374ae885a75214c09e9ba4f33bef2 Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Mon, 10 Mar 2025 23:47:07 +0000 Subject: [PATCH 3/9] finish wiring up muxing w. react-hook-form --- .../components/form-mux-button-add-row.tsx | 25 ++++ .../form-mux-button-delete-rule.tsx | 28 ++++ .../form-mux-button-drag-to-reorder.tsx | 34 +++++ .../components/form-mux-combobox-model.tsx | 8 +- .../components/form-mux-context-provider.tsx | 35 +++++ .../form-mux-drag-to-reorder-button.tsx | 22 --- .../components/form-mux-fields-array.tsx | 127 ++++-------------- .../components/form-mux-fields-labels.tsx | 38 ++++++ .../components/form-mux-rule-row.tsx | 54 +++----- .../form-mux-select-matcher-type.tsx | 17 ++- .../form-mux-text-field-matcher.tsx | 25 ++-- .../workspace/components/form-mux.tsx | 112 ++++++++++----- .../components/tmp/form-checkbox-group.tsx | 60 +++++++++ .../components/tmp/form-combobox.tsx | 59 ++++++++ .../components/tmp/form-radio-group.tsx | 59 ++++++++ .../components/tmp/form-reset-on-submit.tsx | 23 ++++ .../workspace/components/tmp/form-select.tsx | 57 ++++++++ .../components/tmp/form-submit-button.tsx | 27 ++++ .../components/tmp/form-text-field.tsx | 55 ++++++++ .../lib/__tests__/mux-model-serde.test.ts | 39 ++++++ .../workspace/lib/get-mux-field-name.ts | 6 +- .../workspace/lib/handle-mux-form-errors.ts | 16 +++ src/features/workspace/lib/mux-model-serde.ts | 19 +++ .../workspace/lib/mux-row-grid-styles.ts | 5 + src/features/workspace/lib/schema-mux.ts | 13 +- 25 files changed, 736 insertions(+), 227 deletions(-) create mode 100644 src/features/workspace/components/form-mux-button-add-row.tsx create mode 100644 src/features/workspace/components/form-mux-button-delete-rule.tsx create mode 100644 src/features/workspace/components/form-mux-button-drag-to-reorder.tsx create mode 100644 src/features/workspace/components/form-mux-context-provider.tsx delete mode 100644 src/features/workspace/components/form-mux-drag-to-reorder-button.tsx create mode 100644 src/features/workspace/components/form-mux-fields-labels.tsx create mode 100644 src/features/workspace/components/tmp/form-checkbox-group.tsx create mode 100644 src/features/workspace/components/tmp/form-combobox.tsx create mode 100644 src/features/workspace/components/tmp/form-radio-group.tsx create mode 100644 src/features/workspace/components/tmp/form-reset-on-submit.tsx create mode 100644 src/features/workspace/components/tmp/form-select.tsx create mode 100644 src/features/workspace/components/tmp/form-submit-button.tsx create mode 100644 src/features/workspace/components/tmp/form-text-field.tsx create mode 100644 src/features/workspace/lib/__tests__/mux-model-serde.test.ts create mode 100644 src/features/workspace/lib/handle-mux-form-errors.ts create mode 100644 src/features/workspace/lib/mux-model-serde.ts create mode 100644 src/features/workspace/lib/mux-row-grid-styles.ts diff --git a/src/features/workspace/components/form-mux-button-add-row.tsx b/src/features/workspace/components/form-mux-button-add-row.tsx new file mode 100644 index 00000000..d6a61800 --- /dev/null +++ b/src/features/workspace/components/form-mux-button-add-row.tsx @@ -0,0 +1,25 @@ +import { Button } from '@stacklok/ui-kit' +import { MuxMatcherType } from '@/api/generated' +import { Plus } from '@untitled-ui/icons-react' +import { useFormMuxRulesContext } from './form-mux-context-provider' + +export function FormMuxButtonAddRow() { + const { prepend } = useFormMuxRulesContext() + + return ( + + ) +} diff --git a/src/features/workspace/components/form-mux-button-delete-rule.tsx b/src/features/workspace/components/form-mux-button-delete-rule.tsx new file mode 100644 index 00000000..326231c1 --- /dev/null +++ b/src/features/workspace/components/form-mux-button-delete-rule.tsx @@ -0,0 +1,28 @@ +import { Button } from '@stacklok/ui-kit' +import { Trash01 } from '@untitled-ui/icons-react' +import { useFormMuxRulesContext } from './form-mux-context-provider' +import { FieldValuesMuxRow } from '../lib/schema-mux' +import { MuxMatcherType } from '@/api/generated' + +export function FormMuxButtonDeleteRow({ + index, + row, +}: { + index: number + row: FieldValuesMuxRow & { id: string } +}) { + const { remove } = useFormMuxRulesContext() + + return ( + + ) +} diff --git a/src/features/workspace/components/form-mux-button-drag-to-reorder.tsx b/src/features/workspace/components/form-mux-button-drag-to-reorder.tsx new file mode 100644 index 00000000..483afadf --- /dev/null +++ b/src/features/workspace/components/form-mux-button-drag-to-reorder.tsx @@ -0,0 +1,34 @@ +import { useSortable } from '@dnd-kit/sortable' +import { DotsGrid } from '@untitled-ui/icons-react' +import { FieldValuesMuxRow } from '../lib/schema-mux' +import { twMerge } from 'tailwind-merge' + +export function FormMuxButtonDragToReorder({ + row, +}: { + row: FieldValuesMuxRow & { id: string } +}) { + const isDisabled = row.matcher === 'Catch-all' + + const { + attributes, + // @ts-expect-error - typedefs say `eventListeners` it is actually `listeners` + listeners, + } = useSortable({ id: row.id, disabled: isDisabled }) + return ( +
    + +
    + ) +} diff --git a/src/features/workspace/components/form-mux-combobox-model.tsx b/src/features/workspace/components/form-mux-combobox-model.tsx index 36682ae4..97eec560 100644 --- a/src/features/workspace/components/form-mux-combobox-model.tsx +++ b/src/features/workspace/components/form-mux-combobox-model.tsx @@ -5,12 +5,13 @@ import { ComboBoxClearButton, ComboBoxFieldGroup, ComboBoxInput, - FormComboBox, OptionsSchema, } from '@stacklok/ui-kit' import { groupBy, map } from 'lodash' import { getMuxFieldName } from '../lib/get-mux-field-name' import { SearchMd } from '@untitled-ui/icons-react' +import { FormComboBox } from './tmp/form-combobox' +import { serializeMuxModel } from '../lib/mux-model-serde' function groupModels( models: ModelByProvider[] = [] @@ -19,7 +20,7 @@ function groupModels( id: providerName, textValue: providerName, items: items.map((item) => ({ - id: `${item.provider_id}/${item.name}`, + id: serializeMuxModel(item), textValue: item.name, })), })) @@ -35,9 +36,10 @@ export function FormMuxComboboxModel({ index }: { index: number }) { aria-label="Matcher" items={models} name={getMuxFieldName({ - index, field: 'model', + index, })} + shouldShowValidationError={false} > +> + +const FormMuxRulesContext = createContext(null) + +export const useFormMuxRulesContext = (): FormMuxRulesContextValue => { + const context = useContext(FormMuxRulesContext) + if (!context) + throw Error( + 'useFormMuxRulesContext must be used inside a FormMuxRulesContextProvider' + ) + + return context +} + +export function FormMuxRulesContextProvider({ + children, +}: { + children: ReactNode +}) { + const value = useFieldArray({ + name: 'muxing_rules', + }) + + return ( + + {children} + + ) +} diff --git a/src/features/workspace/components/form-mux-drag-to-reorder-button.tsx b/src/features/workspace/components/form-mux-drag-to-reorder-button.tsx deleted file mode 100644 index 30c30099..00000000 --- a/src/features/workspace/components/form-mux-drag-to-reorder-button.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { useSortable } from '@dnd-kit/sortable' -import { DotsGrid } from '@untitled-ui/icons-react' -import { FieldValuesMuxRow } from '../lib/schema-mux' - -export function FormMuxDragToReorderButton({ - item, -}: { - item: FieldValuesMuxRow -}) { - const { attributes, listeners, setNodeRef } = useSortable({ id: item.id }) - - return ( -
    - -
    - ) -} diff --git a/src/features/workspace/components/form-mux-fields-array.tsx b/src/features/workspace/components/form-mux-fields-array.tsx index a200f012..5ece7966 100644 --- a/src/features/workspace/components/form-mux-fields-array.tsx +++ b/src/features/workspace/components/form-mux-fields-array.tsx @@ -1,131 +1,50 @@ -import { - Button, - Label, - Tooltip, - TooltipInfoButton, - TooltipTrigger, -} from '@stacklok/ui-kit' -import { useFieldArray, useFormContext } from 'react-hook-form' -import { - WORKSPACE_CONFIG_FIELD_NAME, - WorkspaceConfigFieldValues, -} from '../lib/schema-mux' -import { MuxMatcherType } from '@/api/generated' -import { uniqueId } from 'lodash' -import { Plus } from '@untitled-ui/icons-react' -import { tv } from 'tailwind-variants' -import { - SortableContext, - sortableKeyboardCoordinates, - verticalListSortingStrategy, -} from '@dnd-kit/sortable' -import { - closestCenter, - DndContext, - DragEndEvent, - KeyboardSensor, - PointerSensor, - UniqueIdentifier, - useSensor, - useSensors, -} from '@dnd-kit/core' -import { ReactNode, useCallback } from 'react' +import { DragEndEvent } from '@dnd-kit/core' +import { useCallback } from 'react' import { FormMuxRuleRow } from './form-mux-rule-row' import { DndSortProvider } from './form-mux-dnd-provider' +import { useFormMuxRulesContext } from './form-mux-context-provider' +import { FormMuxFieldsLabels } from './form-mux-fields-labels' -function getIndicesOnDragEnd( - event: DragEndEvent, - items: T[] -): { +function getIndicesOnDragEnd(event: DragEndEvent): { from: number to: number } | null { const { active, over } = event - - if (over == null || active.id || over.id) return null // no-op - - const from = items.findIndex(({ id }) => id === active.id) - const to = items.findIndex(({ id }) => id === over.id) + if (over == null || active.id == null || over.id == null) return null + // If trying to drag over the last item, no-op + if ( + over.data.current?.sortable.index >= + over.data.current?.sortable.items.length - 1 + ) + return null return { - from, - to, + from: active.data.current?.sortable.index, + to: over.data.current?.sortable.index, } } -const gridStyles = tv({ - base: 'grid grid-cols-[2fr_1fr_2.5rem] items-center gap-2', -}) - -function Labels() { - return ( -
    - - -
    - ) -} - export function FormMuxFieldsArray() { - const { control } = useFormContext() - - const { fields, swap, prepend } = useFieldArray({ - control, - name: WORKSPACE_CONFIG_FIELD_NAME.muxing_rules, - }) - console.debug('👉 fields:', fields) + const { fields, move } = useFormMuxRulesContext() const onDragEnd = useCallback( (event: DragEndEvent) => { - const { from, to } = getIndicesOnDragEnd(event, fields) || {} - if (from && to) swap(from, to) + const { from, to } = getIndicesOnDragEnd(event) || {} + if (typeof from === 'number' && typeof to === 'number') move(from, to) }, - [fields, swap] + [move] ) return ( <> - - + +
      - {fields.map((item, index) => { - console.debug('👉 MAPPED item:', item) - return - })} + {fields.map((item, index) => ( + + ))}
    - -
    - - - {/* - Manage providers - */} -
    ) } diff --git a/src/features/workspace/components/form-mux-fields-labels.tsx b/src/features/workspace/components/form-mux-fields-labels.tsx new file mode 100644 index 00000000..34b47ab6 --- /dev/null +++ b/src/features/workspace/components/form-mux-fields-labels.tsx @@ -0,0 +1,38 @@ +import { + Label, + Tooltip, + TooltipInfoButton, + TooltipTrigger, +} from '@stacklok/ui-kit' +import { muxRowGridStyles } from '../lib/mux-row-grid-styles' + +export function FormMuxFieldsLabels() { + return ( +
    +
    + + + +
    + ) +} diff --git a/src/features/workspace/components/form-mux-rule-row.tsx b/src/features/workspace/components/form-mux-rule-row.tsx index 32b5b50a..574c05e9 100644 --- a/src/features/workspace/components/form-mux-rule-row.tsx +++ b/src/features/workspace/components/form-mux-rule-row.tsx @@ -1,53 +1,43 @@ -import { MuxMatcherType } from '@/api/generated' import { useSortable } from '@dnd-kit/sortable' import { CSS } from '@dnd-kit/utilities' -import { Button } from '@stacklok/ui-kit' -import { tv } from 'tailwind-variants' import { twMerge } from 'tailwind-merge' import { FormMuxComboboxModel } from './form-mux-combobox-model' -import { Trash01 } from '@untitled-ui/icons-react' import { FormMuxTextFieldMatcher } from './form-mux-text-field-matcher' import { FieldValuesMuxRow } from '../lib/schema-mux' - -const gridStyles = tv({ - base: 'grid grid-cols-[2fr_1fr_2.5rem] items-center gap-2', -}) +import { FormSelectMatcherType } from './form-mux-select-matcher-type' +import { FormMuxButtonDragToReorder } from './form-mux-button-drag-to-reorder' +import { muxRowGridStyles } from '../lib/mux-row-grid-styles' +import { FormMuxButtonDeleteRow } from './form-mux-button-delete-rule' export function FormMuxRuleRow({ index, - item, + row, }: { index: number - item: FieldValuesMuxRow + row: FieldValuesMuxRow & { id: string } }) { - const isCatchAll = item.matcher_type === MuxMatcherType.CATCH_ALL - - const { transform, transition } = useSortable({ id: item.id }) + const { transform, transition, setNodeRef } = useSortable({ id: row.id }) const style = { - transform: CSS.Transform.toString(transform), + transform: CSS.Transform.toString({ + y: transform?.y || 0, + scaleX: 1, + scaleY: 1, + x: 0, + }), transition, } return ( -
  • - - +
  • + + + - +
  • ) } diff --git a/src/features/workspace/components/form-mux-select-matcher-type.tsx b/src/features/workspace/components/form-mux-select-matcher-type.tsx index d4bcef09..36c7fbe8 100644 --- a/src/features/workspace/components/form-mux-select-matcher-type.tsx +++ b/src/features/workspace/components/form-mux-select-matcher-type.tsx @@ -1,8 +1,16 @@ import { MuxMatcherType } from '@/api/generated' -import { FormSelect, SelectButton } from '@stacklok/ui-kit' +import { SelectButton } from '@stacklok/ui-kit' import { getMuxFieldName } from '../lib/get-mux-field-name' +import { FormSelect } from './tmp/form-select' +import { FieldValuesMuxRow } from '../lib/schema-mux' -export function FormMuxComboboxModel({ index }: { index: number }) { +export function FormSelectMatcherType({ + index, + row, +}: { + index: number + row: FieldValuesMuxRow & { id: string } +}) { return ( diff --git a/src/features/workspace/components/form-mux-text-field-matcher.tsx b/src/features/workspace/components/form-mux-text-field-matcher.tsx index 6c77e363..126983b7 100644 --- a/src/features/workspace/components/form-mux-text-field-matcher.tsx +++ b/src/features/workspace/components/form-mux-text-field-matcher.tsx @@ -1,33 +1,28 @@ -import { FormTextField, Input } from '@stacklok/ui-kit' +import { Input } from '@stacklok/ui-kit' import { getMuxFieldName } from '../lib/get-mux-field-name' -import { FormMuxDragToReorderButton } from './form-mux-drag-to-reorder-button' import { FieldValuesMuxRow } from '../lib/schema-mux' +import { MuxMatcherType } from '@/api/generated' +import { FormTextField } from './tmp/form-text-field' export function FormMuxTextFieldMatcher({ index, - isCatchAll, - item, + row, }: { index: number - isCatchAll: boolean - item: FieldValuesMuxRow + row: FieldValuesMuxRow & { id: string } }) { - console.debug('👉 isCatchAll:', isCatchAll) return ( - - } - /> + ) } diff --git a/src/features/workspace/components/form-mux.tsx b/src/features/workspace/components/form-mux.tsx index 11a9fecb..3ba80be7 100644 --- a/src/features/workspace/components/form-mux.tsx +++ b/src/features/workspace/components/form-mux.tsx @@ -11,7 +11,7 @@ import { } from '@stacklok/ui-kit' import { twMerge } from 'tailwind-merge' import { useMutationPreferredModelWorkspace } from '../hooks/use-mutation-preferred-model-workspace' -import { MuxMatcherType } from '@/api/generated' +import { MuxMatcherType, V1GetWorkspaceMuxesResponse } from '@/api/generated' import { LinkExternal01 } from '@untitled-ui/icons-react' import { useQueryListAllModelsForAllProviders } from '@/hooks/use-query-list-all-models-for-all-providers' import { useQueryMuxingRulesWorkspace } from '../hooks/use-query-muxing-rules-workspace' @@ -21,18 +21,41 @@ import { WorkspaceConfigFieldValues, } from '../lib/schema-mux' import { zodResolver } from '@hookform/resolvers/zod' +import { FormMuxButtonAddRow } from './form-mux-button-add-row' +import { FormMuxRulesContextProvider } from './form-mux-context-provider' +import { deserializeMuxModel, serializeMuxModel } from '../lib/mux-model-serde' +import { SubmitHandler } from 'react-hook-form' +import { handleMuxFormErrors } from '../lib/handle-mux-form-errors' const DEFAULT_VALUES: WorkspaceConfigFieldValues = { muxing_rules: [ { - id: MuxMatcherType.CATCH_ALL, - model: '', - matcher: '', + // @ts-expect-error - we start with invalid state + model: undefined, + matcher: 'Catch-all', matcher_type: MuxMatcherType.CATCH_ALL, }, ], } +const fromApiMuxingRules = ( + rules: V1GetWorkspaceMuxesResponse +): WorkspaceConfigFieldValues => { + return { + muxing_rules: rules.map( + ({ matcher_type, model, matcher, provider_name, provider_id }) => ({ + model: serializeMuxModel({ + name: model, + provider_id, + provider_name: provider_name as string, + }), + matcher_type, + matcher: matcher ?? '', + }) + ), + } +} + function MissingProviderBanner() { return ( // TODO needs to update the related ui-kit component that diverges from the design @@ -60,20 +83,29 @@ export function WorkspaceMuxingModel({ workspaceName: string isArchived: boolean | undefined }) { - const { data: muxingRules, isPending } = + const { data: muxRulesFromApi, isPending } = useQueryMuxingRulesWorkspace(workspaceName) const { mutateAsync } = useMutationPreferredModelWorkspace() const { data: providerModels = [] } = useQueryListAllModelsForAllProviders() const isModelsEmpty = !isPending && providerModels.length === 0 - const handleSubmit = (data: WorkspaceConfigFieldValues) => { + const handleSubmit: SubmitHandler = (data) => { mutateAsync({ path: { workspace_name: workspaceName }, - body: data.muxing_rules.map((rule) => { - return rule.matcher - ? { ...rule, matcher_type: MuxMatcherType.FILENAME_MATCH } - : { ...rule } + body: data.muxing_rules.map(({ matcher, matcher_type, model }) => { + const { + name: modelName, + provider_id, + provider_name, + } = deserializeMuxModel(model) + return { + matcher_type, + model: modelName, + provider_id, + matcher, + provider_name, + } }), }) } @@ -89,43 +121,49 @@ export function WorkspaceMuxingModel({ ) } + const defaultValues = + muxRulesFromApi.length > 0 + ? fromApiMuxingRules(muxRulesFromApi) + : DEFAULT_VALUES + return ( onSubmit={handleSubmit} - data-testid="preferred-model" + onError={handleMuxFormErrors} options={{ - defaultValues: DEFAULT_VALUES, + values: defaultValues, resolver: zodResolver(schemaWorkspaceConfig), + shouldFocusError: true, }} > - - -
    - Model Muxing - - Select the model you would like to use in this workspace. This - section applies only if you are using the MUX endpoint. - - Learn more - - -
    + + + +
    + Model Muxing + + Select the model you would like to use in this workspace. This + section applies only if you are using the MUX endpoint. + + Learn more + + +
    -
    -
    -
    -
    +
    - - - -
    + + + + + + ) } diff --git a/src/features/workspace/components/tmp/form-checkbox-group.tsx b/src/features/workspace/components/tmp/form-checkbox-group.tsx new file mode 100644 index 00000000..c848263f --- /dev/null +++ b/src/features/workspace/components/tmp/form-checkbox-group.tsx @@ -0,0 +1,60 @@ +import { CheckboxGroup, FieldError } from '@stacklok/ui-kit' +import type { ComponentProps } from 'react' +import { useController, useFormContext } from 'react-hook-form' + +/** + * A `FormCheckboxGroup` connects a `CheckboxGroup` to a `Form` component using `react-hook-form`. + * + * [React Aria Documentation](https://react-spectrum.adobe.com/react-aria/CheckboxGroup.html) + */ +export function FormCheckboxGroup({ + children, + ...props +}: ComponentProps) { + if (props.name == null) + throw new Error('FormCheckboxGroup requires a name prop') + + const { control } = useFormContext() + + const { + field: { + disabled: isDisabledByForm, + name, + onBlur, + onChange, + ref, + value = '', + }, + fieldState: { error, invalid }, + } = useController({ + control, + defaultValue: props.value ?? props.defaultValue ?? [], + name: props.name, + }) + + return ( + { + onChange(k) + props.onChange?.(k) + }} + ref={ref} + value={value} + validationBehavior="aria" // Let React Hook Form handle validation instead of the browser. + > + {(renderProps) => { + return ( + <> + {typeof children === 'function' ? children(renderProps) : children} + {error?.message} + + ) + }} + + ) +} diff --git a/src/features/workspace/components/tmp/form-combobox.tsx b/src/features/workspace/components/tmp/form-combobox.tsx new file mode 100644 index 00000000..33053db8 --- /dev/null +++ b/src/features/workspace/components/tmp/form-combobox.tsx @@ -0,0 +1,59 @@ +import { ComboBox, FieldError, OptionsSchema } from '@stacklok/ui-kit' +import type { ComponentProps } from 'react' +import { useController, useFormContext } from 'react-hook-form' + +/** + * A `FormComboBox` connects a `ComboBox` to a `Form` component using `react-hook-form`. + * + * [React Aria Documentation](https://react-spectrum.adobe.com/react-aria/ComboBox.html) + */ +export function FormComboBox< + T extends OptionsSchema<'listbox'> = OptionsSchema<'listbox'>, +>({ + children, + shouldShowValidationError = true, + ...props +}: ComponentProps> & { + shouldShowValidationError?: boolean +}) { + if (props.name == null) throw new Error('FormComboBox requires a name prop') + + const { control } = useFormContext() + + const { + field: { disabled: isDisabledByForm, name, onBlur, onChange, ref, value }, + fieldState: { error, invalid }, + } = useController({ + control, + defaultValue: props.selectedKey ?? props.defaultSelectedKey, + name: props.name, + }) + + return ( + { + onChange(k) + props.onSelectionChange?.(k) + }} + ref={ref} + selectedKey={value ?? ''} + validationBehavior="aria" // Let React Hook Form handle validation instead of the browser. + > + {(rp) => { + return ( + <> + {typeof children === 'function' ? children(rp) : children} + {shouldShowValidationError ? ( + {error?.message} + ) : null} + + ) + }} + + ) +} diff --git a/src/features/workspace/components/tmp/form-radio-group.tsx b/src/features/workspace/components/tmp/form-radio-group.tsx new file mode 100644 index 00000000..c43ad942 --- /dev/null +++ b/src/features/workspace/components/tmp/form-radio-group.tsx @@ -0,0 +1,59 @@ +import { FieldError, RadioGroup } from '@stacklok/ui-kit' +import type { ComponentProps } from 'react' +import { useController, useFormContext } from 'react-hook-form' + +/** + * A `FormRadioGroup` connects a `RadioGroup` to a `Form` component using `react-hook-form`. + * + * [React Aria Documentation](https://react-spectrum.adobe.com/react-aria/RadioGroup.html) + */ +export function FormRadioGroup({ + children, + ...props +}: ComponentProps) { + if (props.name == null) throw new Error('FormRadioGroup requires a name prop') + + const { control } = useFormContext() + + const { + field: { + disabled: isDisabledByForm, + name, + onBlur, + onChange, + ref, + value = '', + }, + fieldState: { error, invalid }, + } = useController({ + control, + defaultValue: props.value ?? props.defaultValue, + name: props.name, + }) + + return ( + { + onChange(k) + props.onChange?.(k) + }} + ref={ref} + value={value} + validationBehavior="aria" // Let React Hook Form handle validation instead of the browser. + > + {(renderProps) => { + return ( + <> + {typeof children === 'function' ? children(renderProps) : children} + {error?.message} + + ) + }} + + ) +} diff --git a/src/features/workspace/components/tmp/form-reset-on-submit.tsx b/src/features/workspace/components/tmp/form-reset-on-submit.tsx new file mode 100644 index 00000000..3a8774ff --- /dev/null +++ b/src/features/workspace/components/tmp/form-reset-on-submit.tsx @@ -0,0 +1,23 @@ +import { useEffect } from 'react' +import { useFormContext } from 'react-hook-form' + +/** + * A component that resets the form after a successful submission. + * Used to test that form fields are reset after a successful submission. + */ +export function FormResetOnSubmit() { + const { + formState: { isSubmitSuccessful }, + reset, + } = useFormContext() + + // It is recommended in the React Hook Form documentation to use `useEffect` to + // handle side effects like resetting the form after a successful submission. + useEffect(() => { + if (isSubmitSuccessful) { + reset() + } + }, [isSubmitSuccessful, reset]) + + return null +} diff --git a/src/features/workspace/components/tmp/form-select.tsx b/src/features/workspace/components/tmp/form-select.tsx new file mode 100644 index 00000000..c42fab9a --- /dev/null +++ b/src/features/workspace/components/tmp/form-select.tsx @@ -0,0 +1,57 @@ +import { FieldError, OptionsSchema, Select } from '@stacklok/ui-kit' +import type { ComponentProps } from 'react' +import { useController, useFormContext } from 'react-hook-form' + +/** + * A `FormSelect` connects a `Select` to a `Form` component using `react-hook-form`. + * + * [React Aria Documentation](https://react-spectrum.adobe.com/react-aria/Select.html) + */ +export function FormSelect< + T extends OptionsSchema<'listbox'> = OptionsSchema<'listbox'>, +>({ + children, + shouldShowValidationError = true, + ...props +}: ComponentProps> & { shouldShowValidationError?: boolean }) { + if (props.name == null) throw new Error('FormSelect requires a name prop') + + const { control } = useFormContext() + + const { + field: { disabled: isDisabledByForm, name, onBlur, onChange, ref, value }, + fieldState: { error, invalid }, + } = useController({ + control, + defaultValue: props.selectedKey ?? props.defaultSelectedKey, + name: props.name, + }) + + return ( + + ) +} diff --git a/src/features/workspace/components/tmp/form-submit-button.tsx b/src/features/workspace/components/tmp/form-submit-button.tsx new file mode 100644 index 00000000..0e0d1d7b --- /dev/null +++ b/src/features/workspace/components/tmp/form-submit-button.tsx @@ -0,0 +1,27 @@ +import { Button } from '@stacklok/ui-kit' +import type { ComponentProps } from 'react' +import { useFormState } from 'react-hook-form' + +export function FormSubmitButton({ + children = 'Submit', + variant = 'primary', + ...props +}: ComponentProps) { + const { isSubmitting, isValidating, isDirty } = useFormState() + + return ( + + ) +} diff --git a/src/features/workspace/components/tmp/form-text-field.tsx b/src/features/workspace/components/tmp/form-text-field.tsx new file mode 100644 index 00000000..26e20b8d --- /dev/null +++ b/src/features/workspace/components/tmp/form-text-field.tsx @@ -0,0 +1,55 @@ +import { FieldError, TextField } from '@stacklok/ui-kit' +import type { ComponentProps } from 'react' +import { useController, useFormContext } from 'react-hook-form' + +/** + * A form text field connects a `TextField` to a `Form` component using `react-hook-form`. + * + * [React Aria Documentation](https://react-spectrum.adobe.com/react-aria/TextField.html) + */ +export function FormTextField({ + children, + shouldShowValidationError = true, + ...props +}: ComponentProps & { shouldShowValidationError?: boolean }) { + if (props.name == null) throw new Error('FormTextField requires a name prop') + + const { control } = useFormContext() + + const { + field: { disabled: isDisabledByForm, name, onBlur, onChange, ref, value }, + fieldState: { error, invalid }, + } = useController({ + control, + defaultValue: props.value ?? props.defaultValue, + name: props.name, + }) + + return ( + { + onChange(v) + props.onChange?.(v) + }} + ref={ref} + validationBehavior="aria" // Let React Hook Form handle validation instead of the browser. + value={value} + > + {(renderProps) => { + return ( + <> + {typeof children === 'function' ? children(renderProps) : children} + {shouldShowValidationError ? ( + {error?.message} + ) : null} + + ) + }} + + ) +} diff --git a/src/features/workspace/lib/__tests__/mux-model-serde.test.ts b/src/features/workspace/lib/__tests__/mux-model-serde.test.ts new file mode 100644 index 00000000..b4cb37cc --- /dev/null +++ b/src/features/workspace/lib/__tests__/mux-model-serde.test.ts @@ -0,0 +1,39 @@ +import { describe, it, expect } from 'vitest' +import { serializeMuxModel, deserializeMuxModel } from '../mux-model-serde' +import { ModelByProvider } from '@/api/generated' + +describe('mux-model serialization/deserialization', () => { + it.each([ + { + name: 'deepseek-r1:1.5b', + // provider_type: 'ollama', + provider_name: 'ollama', + provider_id: 'deepseek-r1', + }, + { + name: 'mistral-nemo:latest', + // provider_type: 'ollama', + provider_name: 'ollama_muxing', + provider_id: 'mistral-nemo', + }, + { + name: '01-ai/yi-large', + // provider_type: 'openrouter', + provider_name: 'openrouter_muxing', + provider_id: '01-ai', + }, + { + name: 'anthropic/claude-3-opus:beta', + // provider_type: 'openrouter', + provider_name: 'openrouter_muxing', + provider_id: 'anthropic', + }, + ] as ModelByProvider[])( + 'should correctly serialize and deserialize model: $name', + (model) => { + const serialized = serializeMuxModel(model) + const deserialized = deserializeMuxModel(serialized) + expect(deserialized).toEqual(model) + } + ) +}) diff --git a/src/features/workspace/lib/get-mux-field-name.ts b/src/features/workspace/lib/get-mux-field-name.ts index a1b39038..3d9a41c4 100644 --- a/src/features/workspace/lib/get-mux-field-name.ts +++ b/src/features/workspace/lib/get-mux-field-name.ts @@ -1,4 +1,4 @@ -import { MUX_FIELD_NAME } from './schema-mux' +import { MUX_FIELD_NAME, WORKSPACE_CONFIG_FIELD_NAME } from './schema-mux' export function getMuxFieldName({ field, @@ -6,6 +6,6 @@ export function getMuxFieldName({ }: { index: number field: (typeof MUX_FIELD_NAME)[keyof typeof MUX_FIELD_NAME] -}) { - return `${MUX_FIELD_NAME}.${index}.${field}` +}): string { + return `${WORKSPACE_CONFIG_FIELD_NAME.muxing_rules}.${index}.${field}` as const } diff --git a/src/features/workspace/lib/handle-mux-form-errors.ts b/src/features/workspace/lib/handle-mux-form-errors.ts new file mode 100644 index 00000000..912ad01a --- /dev/null +++ b/src/features/workspace/lib/handle-mux-form-errors.ts @@ -0,0 +1,16 @@ +import { SubmitErrorHandler } from 'react-hook-form' +import { WorkspaceConfigFieldValues } from './schema-mux' +import { toast } from '@stacklok/ui-kit' + +export const handleMuxFormErrors: SubmitErrorHandler< + WorkspaceConfigFieldValues +> = (errors) => { + console.debug('👉 errors:', errors) + if (!Array.isArray(errors.muxing_rules) || errors.muxing_rules[0] == null) + return + Object.entries(errors.muxing_rules[0]).forEach((error) => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [_, { message }] = error as [string, { message: string }] + toast.error(message) + }) +} diff --git a/src/features/workspace/lib/mux-model-serde.ts b/src/features/workspace/lib/mux-model-serde.ts new file mode 100644 index 00000000..e833c7d3 --- /dev/null +++ b/src/features/workspace/lib/mux-model-serde.ts @@ -0,0 +1,19 @@ +import { ModelByProvider } from '@/api/generated' + +export function serializeMuxModel(model: ModelByProvider): string { + return `${model.provider_id}___${model.name}` + // return `${model.provider_name}___${model.name}` +} + +export function deserializeMuxModel(str: string): ModelByProvider { + const [provider_id, name] = str.split('___') + if (!provider_id || !name) throw new Error('Invalid model') + return { + // provider_type: z.nativeEnum(ProviderType).parse(provider_type), + provider_id, + // @ts-expect-error - the API is not returning this right now, this will + // change soon anyway + provider_name: null, + name, + } +} diff --git a/src/features/workspace/lib/mux-row-grid-styles.ts b/src/features/workspace/lib/mux-row-grid-styles.ts new file mode 100644 index 00000000..7716b669 --- /dev/null +++ b/src/features/workspace/lib/mux-row-grid-styles.ts @@ -0,0 +1,5 @@ +import { tv } from 'tailwind-variants' + +export const muxRowGridStyles = tv({ + base: 'grid grid-cols-[2.5rem_1fr_1fr_2fr_2.5rem] items-center gap-2', +}) diff --git a/src/features/workspace/lib/schema-mux.ts b/src/features/workspace/lib/schema-mux.ts index 81a06b6d..fd1d3a81 100644 --- a/src/features/workspace/lib/schema-mux.ts +++ b/src/features/workspace/lib/schema-mux.ts @@ -1,15 +1,12 @@ import { MuxMatcherType } from '@/api/generated' import { z } from 'zod' -const schemaMuxCatchAllId = z.literal(MuxMatcherType.CATCH_ALL) - -export type MuxCatchallId = z.infer - const schemaMuxRow = z.object({ - model: z.string().optional(), - matcher_type: z.nativeEnum(MuxMatcherType), - matcher: z.string(), - id: z.union([z.string().uuid(), schemaMuxCatchAllId]), + model: z.string({ message: 'Muxing: Model is required' }), + matcher_type: z.nativeEnum(MuxMatcherType, { + message: 'Muxing: Matcher type is required', + }), + matcher: z.string({ message: 'Muxing: Matcher cannot be empty' }), }) export type FieldValuesMuxRow = z.infer From 7f75f43bb33dfa3d89fdd2b8c8d921d767f98f4c Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 11 Mar 2025 08:04:54 +0000 Subject: [PATCH 4/9] update placeholder --- .../workspace/components/form-mux-text-field-matcher.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/features/workspace/components/form-mux-text-field-matcher.tsx b/src/features/workspace/components/form-mux-text-field-matcher.tsx index 126983b7..1917c4c1 100644 --- a/src/features/workspace/components/form-mux-text-field-matcher.tsx +++ b/src/features/workspace/components/form-mux-text-field-matcher.tsx @@ -22,7 +22,10 @@ export function FormMuxTextFieldMatcher({ })} shouldShowValidationError={false} > - + ) } From f571cd68ebfc98ae67d92b06f738d9fbd38ed8fb Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 11 Mar 2025 09:17:37 +0000 Subject: [PATCH 5/9] tidy ups --- .../generated/@tanstack/react-query.gen.ts | 183 +++++ src/api/generated/sdk.gen.ts | 141 ++++ src/api/generated/types.gen.ts | 139 +++- src/api/openapi.json | 683 ++++++++++++------ .../form-mux-button-delete-rule.tsx | 1 - .../components/form-mux-combobox-model.tsx | 10 +- .../components/form-mux-fields-labels.tsx | 25 +- .../components/form-mux-rule-row.tsx | 7 +- .../form-mux-select-matcher-type.tsx | 16 +- .../form-mux-text-field-matcher.tsx | 6 +- .../workspace/components/form-mux.tsx | 4 +- .../components/tmp/form-combobox.tsx | 2 +- src/features/workspace/lib/schema-mux.ts | 4 +- 13 files changed, 948 insertions(+), 273 deletions(-) diff --git a/src/api/generated/@tanstack/react-query.gen.ts b/src/api/generated/@tanstack/react-query.gen.ts index a22a7421..9675647d 100644 --- a/src/api/generated/@tanstack/react-query.gen.ts +++ b/src/api/generated/@tanstack/react-query.gen.ts @@ -17,11 +17,13 @@ import { v1CreateWorkspace, v1ListActiveWorkspaces, v1ActivateWorkspace, + v1UpdateWorkspace, v1DeleteWorkspace, v1ListArchivedWorkspaces, v1RecoverWorkspace, v1HardDeleteWorkspace, v1GetWorkspaceAlerts, + v1GetWorkspaceAlertsSummary, v1GetWorkspaceMessages, v1GetWorkspaceCustomInstructions, v1SetWorkspaceCustomInstructions, @@ -32,6 +34,11 @@ import { v1StreamSse, v1VersionCheck, v1GetWorkspaceTokenUsage, + v1ListPersonas, + v1CreatePersona, + v1GetPersona, + v1UpdatePersona, + v1DeletePersona, } from '../sdk.gen' import type { V1ListProviderEndpointsData, @@ -55,6 +62,9 @@ import type { V1ActivateWorkspaceData, V1ActivateWorkspaceError, V1ActivateWorkspaceResponse, + V1UpdateWorkspaceData, + V1UpdateWorkspaceError, + V1UpdateWorkspaceResponse, V1DeleteWorkspaceData, V1DeleteWorkspaceError, V1DeleteWorkspaceResponse, @@ -65,6 +75,7 @@ import type { V1HardDeleteWorkspaceError, V1HardDeleteWorkspaceResponse, V1GetWorkspaceAlertsData, + V1GetWorkspaceAlertsSummaryData, V1GetWorkspaceMessagesData, V1GetWorkspaceCustomInstructionsData, V1SetWorkspaceCustomInstructionsData, @@ -79,6 +90,16 @@ import type { V1SetWorkspaceMuxesResponse, V1ListWorkspacesByProviderData, V1GetWorkspaceTokenUsageData, + V1CreatePersonaData, + V1CreatePersonaError, + V1CreatePersonaResponse, + V1GetPersonaData, + V1UpdatePersonaData, + V1UpdatePersonaError, + V1UpdatePersonaResponse, + V1DeletePersonaData, + V1DeletePersonaError, + V1DeletePersonaResponse, } from '../types.gen' type QueryKey = [ @@ -441,6 +462,26 @@ export const v1ActivateWorkspaceMutation = ( return mutationOptions } +export const v1UpdateWorkspaceMutation = ( + options?: Partial> +) => { + const mutationOptions: UseMutationOptions< + V1UpdateWorkspaceResponse, + V1UpdateWorkspaceError, + OptionsLegacyParser + > = { + mutationFn: async (localOptions) => { + const { data } = await v1UpdateWorkspace({ + ...options, + ...localOptions, + throwOnError: true, + }) + return data + }, + } + return mutationOptions +} + export const v1DeleteWorkspaceMutation = ( options?: Partial> ) => { @@ -564,6 +605,27 @@ export const v1GetWorkspaceAlertsOptions = ( }) } +export const v1GetWorkspaceAlertsSummaryQueryKey = ( + options: OptionsLegacyParser +) => [createQueryKey('v1GetWorkspaceAlertsSummary', options)] + +export const v1GetWorkspaceAlertsSummaryOptions = ( + options: OptionsLegacyParser +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await v1GetWorkspaceAlertsSummary({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }) + return data + }, + queryKey: v1GetWorkspaceAlertsSummaryQueryKey(options), + }) +} + export const v1GetWorkspaceMessagesQueryKey = ( options: OptionsLegacyParser ) => [createQueryKey('v1GetWorkspaceMessages', options)] @@ -768,3 +830,124 @@ export const v1GetWorkspaceTokenUsageOptions = ( queryKey: v1GetWorkspaceTokenUsageQueryKey(options), }) } + +export const v1ListPersonasQueryKey = (options?: OptionsLegacyParser) => [ + createQueryKey('v1ListPersonas', options), +] + +export const v1ListPersonasOptions = (options?: OptionsLegacyParser) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await v1ListPersonas({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }) + return data + }, + queryKey: v1ListPersonasQueryKey(options), + }) +} + +export const v1CreatePersonaQueryKey = ( + options: OptionsLegacyParser +) => [createQueryKey('v1CreatePersona', options)] + +export const v1CreatePersonaOptions = ( + options: OptionsLegacyParser +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await v1CreatePersona({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }) + return data + }, + queryKey: v1CreatePersonaQueryKey(options), + }) +} + +export const v1CreatePersonaMutation = ( + options?: Partial> +) => { + const mutationOptions: UseMutationOptions< + V1CreatePersonaResponse, + V1CreatePersonaError, + OptionsLegacyParser + > = { + mutationFn: async (localOptions) => { + const { data } = await v1CreatePersona({ + ...options, + ...localOptions, + throwOnError: true, + }) + return data + }, + } + return mutationOptions +} + +export const v1GetPersonaQueryKey = ( + options: OptionsLegacyParser +) => [createQueryKey('v1GetPersona', options)] + +export const v1GetPersonaOptions = ( + options: OptionsLegacyParser +) => { + return queryOptions({ + queryFn: async ({ queryKey, signal }) => { + const { data } = await v1GetPersona({ + ...options, + ...queryKey[0], + signal, + throwOnError: true, + }) + return data + }, + queryKey: v1GetPersonaQueryKey(options), + }) +} + +export const v1UpdatePersonaMutation = ( + options?: Partial> +) => { + const mutationOptions: UseMutationOptions< + V1UpdatePersonaResponse, + V1UpdatePersonaError, + OptionsLegacyParser + > = { + mutationFn: async (localOptions) => { + const { data } = await v1UpdatePersona({ + ...options, + ...localOptions, + throwOnError: true, + }) + return data + }, + } + return mutationOptions +} + +export const v1DeletePersonaMutation = ( + options?: Partial> +) => { + const mutationOptions: UseMutationOptions< + V1DeletePersonaResponse, + V1DeletePersonaError, + OptionsLegacyParser + > = { + mutationFn: async (localOptions) => { + const { data } = await v1DeletePersona({ + ...options, + ...localOptions, + throwOnError: true, + }) + return data + }, + } + return mutationOptions +} diff --git a/src/api/generated/sdk.gen.ts b/src/api/generated/sdk.gen.ts index e9d216d9..d2fb6c05 100644 --- a/src/api/generated/sdk.gen.ts +++ b/src/api/generated/sdk.gen.ts @@ -41,6 +41,9 @@ import type { V1ActivateWorkspaceData, V1ActivateWorkspaceError, V1ActivateWorkspaceResponse, + V1UpdateWorkspaceData, + V1UpdateWorkspaceError, + V1UpdateWorkspaceResponse, V1DeleteWorkspaceData, V1DeleteWorkspaceError, V1DeleteWorkspaceResponse, @@ -55,6 +58,9 @@ import type { V1GetWorkspaceAlertsData, V1GetWorkspaceAlertsError, V1GetWorkspaceAlertsResponse, + V1GetWorkspaceAlertsSummaryData, + V1GetWorkspaceAlertsSummaryError, + V1GetWorkspaceAlertsSummaryResponse, V1GetWorkspaceMessagesData, V1GetWorkspaceMessagesError, V1GetWorkspaceMessagesResponse, @@ -83,6 +89,20 @@ import type { V1GetWorkspaceTokenUsageData, V1GetWorkspaceTokenUsageError, V1GetWorkspaceTokenUsageResponse, + V1ListPersonasError, + V1ListPersonasResponse, + V1CreatePersonaData, + V1CreatePersonaError, + V1CreatePersonaResponse, + V1GetPersonaData, + V1GetPersonaError, + V1GetPersonaResponse, + V1UpdatePersonaData, + V1UpdatePersonaError, + V1UpdatePersonaResponse, + V1DeletePersonaData, + V1DeletePersonaError, + V1DeletePersonaResponse, } from './types.gen' export const client = createClient(createConfig()) @@ -312,6 +332,23 @@ export const v1ActivateWorkspace = ( }) } +/** + * Update Workspace + * Update a workspace. + */ +export const v1UpdateWorkspace = ( + options: OptionsLegacyParser +) => { + return (options?.client ?? client).put< + V1UpdateWorkspaceResponse, + V1UpdateWorkspaceError, + ThrowOnError + >({ + ...options, + url: '/api/v1/workspaces/{workspace_name}', + }) +} + /** * Delete Workspace * Delete a workspace by name. @@ -397,6 +434,25 @@ export const v1GetWorkspaceAlerts = ( }) } +/** + * Get Workspace Alerts Summary + * Get alert summary for a workspace. + */ +export const v1GetWorkspaceAlertsSummary = < + ThrowOnError extends boolean = false, +>( + options: OptionsLegacyParser +) => { + return (options?.client ?? client).get< + V1GetWorkspaceAlertsSummaryResponse, + V1GetWorkspaceAlertsSummaryError, + ThrowOnError + >({ + ...options, + url: '/api/v1/workspaces/{workspace_name}/alerts-summary', + }) +} + /** * Get Workspace Messages * Get messages for a workspace. @@ -583,3 +639,88 @@ export const v1GetWorkspaceTokenUsage = ( url: '/api/v1/workspaces/{workspace_name}/token-usage', }) } + +/** + * List Personas + * List all personas. + */ +export const v1ListPersonas = ( + options?: OptionsLegacyParser +) => { + return (options?.client ?? client).get< + V1ListPersonasResponse, + V1ListPersonasError, + ThrowOnError + >({ + ...options, + url: '/api/v1/personas', + }) +} + +/** + * Create Persona + * Create a new persona. + */ +export const v1CreatePersona = ( + options: OptionsLegacyParser +) => { + return (options?.client ?? client).post< + V1CreatePersonaResponse, + V1CreatePersonaError, + ThrowOnError + >({ + ...options, + url: '/api/v1/personas', + }) +} + +/** + * Get Persona + * Get a persona by name. + */ +export const v1GetPersona = ( + options: OptionsLegacyParser +) => { + return (options?.client ?? client).get< + V1GetPersonaResponse, + V1GetPersonaError, + ThrowOnError + >({ + ...options, + url: '/api/v1/personas/{persona_name}', + }) +} + +/** + * Update Persona + * Update an existing persona. + */ +export const v1UpdatePersona = ( + options: OptionsLegacyParser +) => { + return (options?.client ?? client).put< + V1UpdatePersonaResponse, + V1UpdatePersonaError, + ThrowOnError + >({ + ...options, + url: '/api/v1/personas/{persona_name}', + }) +} + +/** + * Delete Persona + * Delete a persona. + */ +export const v1DeletePersona = ( + options: OptionsLegacyParser +) => { + return (options?.client ?? client).delete< + V1DeletePersonaResponse, + V1DeletePersonaError, + ThrowOnError + >({ + ...options, + url: '/api/v1/personas/{persona_name}', + }) +} diff --git a/src/api/generated/types.gen.ts b/src/api/generated/types.gen.ts index 50634d20..195e0d85 100644 --- a/src/api/generated/types.gen.ts +++ b/src/api/generated/types.gen.ts @@ -64,6 +64,15 @@ export enum AlertSeverity { CRITICAL = 'critical', } +/** + * Represents a set of summary alerts + */ +export type AlertSummary = { + malicious_packages: number + pii: number + secrets: number +} + /** * Represents a chat message. */ @@ -109,14 +118,18 @@ export type Conversation = { alerts?: Array } -export type CreateOrRenameWorkspaceRequest = { +export type CustomInstructions = { + prompt: string +} + +export type FullWorkspace_Input = { name: string - config?: WorkspaceConfig | null - rename_to?: string | null + config?: WorkspaceConfig_Input | null } -export type CustomInstructions = { - prompt: string +export type FullWorkspace_Output = { + name: string + config?: WorkspaceConfig_Output | null } export type HTTPValidationError = { @@ -144,11 +157,23 @@ export type ModelByProvider = { /** * Represents the different types of matchers we support. + * + * The 3 rules present match filenames and request types. They're used in conjunction with the + * matcher field in the MuxRule model. + * E.g. + * - catch_all-> Always match + * - filename_match and match: requests.py -> Match the request if the filename is requests.py + * - fim_filename and match: main.py -> Match the request if the request type is fim + * and the filename is main.py + * + * NOTE: Removing or updating fields from this enum will require a migration. + * Adding new fields is safe. */ export enum MuxMatcherType { CATCH_ALL = 'catch_all', FILENAME_MATCH = 'filename_match', - REQUEST_TYPE_MATCH = 'request_type_match', + FIM_FILENAME = 'fim_filename', + CHAT_FILENAME = 'chat_filename', } /** @@ -162,6 +187,31 @@ export type MuxRule = { matcher?: string | null } +/** + * Represents a persona object. + */ +export type Persona = { + id: string + name: string + description: string +} + +/** + * Model for creating a new Persona. + */ +export type PersonaRequest = { + name: string + description: string +} + +/** + * Model for updating a Persona. + */ +export type PersonaUpdateRequest = { + new_name: string + new_description: string +} + /** * Represents the different types of auth we support for providers. */ @@ -253,8 +303,13 @@ export type Workspace = { is_active: boolean } -export type WorkspaceConfig = { - system_prompt: string +export type WorkspaceConfig_Input = { + custom_instructions: string + muxing_rules: Array +} + +export type WorkspaceConfig_Output = { + custom_instructions: string muxing_rules: Array } @@ -350,10 +405,10 @@ export type V1ListWorkspacesResponse = ListWorkspacesResponse export type V1ListWorkspacesError = unknown export type V1CreateWorkspaceData = { - body: CreateOrRenameWorkspaceRequest + body: FullWorkspace_Input } -export type V1CreateWorkspaceResponse = Workspace +export type V1CreateWorkspaceResponse = FullWorkspace_Output export type V1CreateWorkspaceError = HTTPValidationError @@ -372,6 +427,17 @@ export type V1ActivateWorkspaceResponse = unknown export type V1ActivateWorkspaceError = HTTPValidationError +export type V1UpdateWorkspaceData = { + body: FullWorkspace_Input + path: { + workspace_name: string + } +} + +export type V1UpdateWorkspaceResponse = FullWorkspace_Output + +export type V1UpdateWorkspaceError = HTTPValidationError + export type V1DeleteWorkspaceData = { path: { workspace_name: string @@ -416,6 +482,16 @@ export type V1GetWorkspaceAlertsResponse = Array export type V1GetWorkspaceAlertsError = HTTPValidationError +export type V1GetWorkspaceAlertsSummaryData = { + path: { + workspace_name: string + } +} + +export type V1GetWorkspaceAlertsSummaryResponse = AlertSummary + +export type V1GetWorkspaceAlertsSummaryError = HTTPValidationError + export type V1GetWorkspaceMessagesData = { path: { workspace_name: string @@ -505,3 +581,46 @@ export type V1GetWorkspaceTokenUsageData = { export type V1GetWorkspaceTokenUsageResponse = TokenUsageAggregate export type V1GetWorkspaceTokenUsageError = HTTPValidationError + +export type V1ListPersonasResponse = Array + +export type V1ListPersonasError = unknown + +export type V1CreatePersonaData = { + body: PersonaRequest +} + +export type V1CreatePersonaResponse = Persona + +export type V1CreatePersonaError = HTTPValidationError + +export type V1GetPersonaData = { + path: { + persona_name: string + } +} + +export type V1GetPersonaResponse = Persona + +export type V1GetPersonaError = HTTPValidationError + +export type V1UpdatePersonaData = { + body: PersonaUpdateRequest + path: { + persona_name: string + } +} + +export type V1UpdatePersonaResponse = Persona + +export type V1UpdatePersonaError = HTTPValidationError + +export type V1DeletePersonaData = { + path: { + persona_name: string + } +} + +export type V1DeletePersonaResponse = void + +export type V1DeletePersonaError = HTTPValidationError diff --git a/src/api/openapi.json b/src/api/openapi.json index a6d16753..5fe0b42a 100644 --- a/src/api/openapi.json +++ b/src/api/openapi.json @@ -8,9 +8,7 @@ "paths": { "/health": { "get": { - "tags": [ - "System" - ], + "tags": ["System"], "summary": "Health Check", "operationId": "health_check_health_get", "responses": { @@ -27,10 +25,7 @@ }, "/api/v1/provider-endpoints": { "get": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "List Provider Endpoints", "description": "List all provider endpoints.", "operationId": "v1_list_provider_endpoints", @@ -80,10 +75,7 @@ } }, "post": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "Add Provider Endpoint", "description": "Add a provider endpoint.", "operationId": "v1_add_provider_endpoint", @@ -123,10 +115,7 @@ }, "/api/v1/provider-endpoints/models": { "get": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "List All Models For All Providers", "description": "List all models for all providers.", "operationId": "v1_list_all_models_for_all_providers", @@ -150,10 +139,7 @@ }, "/api/v1/provider-endpoints/{provider_id}/models": { "get": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "List Models By Provider", "description": "List models by provider.", "operationId": "v1_list_models_by_provider", @@ -199,10 +185,7 @@ }, "/api/v1/provider-endpoints/{provider_id}": { "get": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "Get Provider Endpoint", "description": "Get a provider endpoint by ID.", "operationId": "v1_get_provider_endpoint", @@ -242,10 +225,7 @@ } }, "put": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "Update Provider Endpoint", "description": "Update a provider endpoint by ID.", "operationId": "v1_update_provider_endpoint", @@ -295,10 +275,7 @@ } }, "delete": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "Delete Provider Endpoint", "description": "Delete a provider endpoint by id.", "operationId": "v1_delete_provider_endpoint", @@ -338,10 +315,7 @@ }, "/api/v1/provider-endpoints/{provider_id}/auth-material": { "put": { - "tags": [ - "CodeGate API", - "Providers" - ], + "tags": ["CodeGate API", "Providers"], "summary": "Configure Auth Material", "description": "Configure auth material for a provider.", "operationId": "v1_configure_auth_material", @@ -386,10 +360,7 @@ }, "/api/v1/workspaces": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "List Workspaces", "description": "List all workspaces.", "operationId": "v1_list_workspaces", @@ -407,10 +378,7 @@ } }, "post": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Create Workspace", "description": "Create a new workspace.", "operationId": "v1_create_workspace", @@ -418,7 +386,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateOrRenameWorkspaceRequest" + "$ref": "#/components/schemas/FullWorkspace-Input" } } }, @@ -430,7 +398,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/Workspace" + "$ref": "#/components/schemas/FullWorkspace-Output" } } } @@ -450,10 +418,7 @@ }, "/api/v1/workspaces/active": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "List Active Workspaces", "description": "List all active workspaces.\n\nIn it's current form, this function will only return one workspace. That is,\nthe globally active workspace.", "operationId": "v1_list_active_workspaces", @@ -471,10 +436,7 @@ } }, "post": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Activate Workspace", "description": "Activate a workspace by name.", "operationId": "v1_activate_workspace", @@ -522,11 +484,57 @@ } }, "/api/v1/workspaces/{workspace_name}": { - "delete": { - "tags": [ - "CodeGate API", - "Workspaces" + "put": { + "tags": ["CodeGate API", "Workspaces"], + "summary": "Update Workspace", + "description": "Update a workspace.", + "operationId": "v1_update_workspace", + "parameters": [ + { + "name": "workspace_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Workspace Name" + } + } ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FullWorkspace-Input" + } + } + } + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FullWorkspace-Output" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["CodeGate API", "Workspaces"], "summary": "Delete Workspace", "description": "Delete a workspace by name.", "operationId": "v1_delete_workspace", @@ -565,10 +573,7 @@ }, "/api/v1/workspaces/archive": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "List Archived Workspaces", "description": "List all archived workspaces.", "operationId": "v1_list_archived_workspaces", @@ -588,10 +593,7 @@ }, "/api/v1/workspaces/archive/{workspace_name}/recover": { "post": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Recover Workspace", "description": "Recover an archived workspace by name.", "operationId": "v1_recover_workspace", @@ -625,10 +627,7 @@ }, "/api/v1/workspaces/archive/{workspace_name}": { "delete": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Hard Delete Workspace", "description": "Hard delete an archived workspace by name.", "operationId": "v1_hard_delete_workspace", @@ -667,10 +666,7 @@ }, "/api/v1/workspaces/{workspace_name}/alerts": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Get Workspace Alerts", "description": "Get alerts for a workspace.", "operationId": "v1_get_workspace_alerts", @@ -720,12 +716,50 @@ } } }, - "/api/v1/workspaces/{workspace_name}/messages": { + "/api/v1/workspaces/{workspace_name}/alerts-summary": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" + "tags": ["CodeGate API", "Workspaces"], + "summary": "Get Workspace Alerts Summary", + "description": "Get alert summary for a workspace.", + "operationId": "v1_get_workspace_alerts_summary", + "parameters": [ + { + "name": "workspace_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Workspace Name" + } + } ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/AlertSummary" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/workspaces/{workspace_name}/messages": { + "get": { + "tags": ["CodeGate API", "Workspaces"], "summary": "Get Workspace Messages", "description": "Get messages for a workspace.", "operationId": "v1_get_workspace_messages", @@ -770,10 +804,7 @@ }, "/api/v1/workspaces/{workspace_name}/custom-instructions": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Get Workspace Custom Instructions", "description": "Get the custom instructions of a workspace.", "operationId": "v1_get_workspace_custom_instructions", @@ -812,10 +843,7 @@ } }, "put": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Set Workspace Custom Instructions", "operationId": "v1_set_workspace_custom_instructions", "parameters": [ @@ -856,10 +884,7 @@ } }, "delete": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "Delete Workspace Custom Instructions", "operationId": "v1_delete_workspace_custom_instructions", "parameters": [ @@ -892,11 +917,7 @@ }, "/api/v1/workspaces/{workspace_name}/muxes": { "get": { - "tags": [ - "CodeGate API", - "Workspaces", - "Muxes" - ], + "tags": ["CodeGate API", "Workspaces", "Muxes"], "summary": "Get Workspace Muxes", "description": "Get the mux rules of a workspace.\n\nThe list is ordered in order of priority. That is, the first rule in the list\nhas the highest priority.", "operationId": "v1_get_workspace_muxes", @@ -939,11 +960,7 @@ } }, "put": { - "tags": [ - "CodeGate API", - "Workspaces", - "Muxes" - ], + "tags": ["CodeGate API", "Workspaces", "Muxes"], "summary": "Set Workspace Muxes", "description": "Set the mux rules of a workspace.", "operationId": "v1_set_workspace_muxes", @@ -991,10 +1008,7 @@ }, "/api/v1/workspaces/{provider_id}": { "get": { - "tags": [ - "CodeGate API", - "Workspaces" - ], + "tags": ["CodeGate API", "Workspaces"], "summary": "List Workspaces By Provider", "description": "List workspaces by provider ID.", "operationId": "v1_list_workspaces_by_provider", @@ -1040,10 +1054,7 @@ }, "/api/v1/alerts_notification": { "get": { - "tags": [ - "CodeGate API", - "Dashboard" - ], + "tags": ["CodeGate API", "Dashboard"], "summary": "Stream Sse", "description": "Send alerts event", "operationId": "v1_stream_sse", @@ -1061,10 +1072,7 @@ }, "/api/v1/version": { "get": { - "tags": [ - "CodeGate API", - "Dashboard" - ], + "tags": ["CodeGate API", "Dashboard"], "summary": "Version Check", "operationId": "v1_version_check", "responses": { @@ -1081,11 +1089,7 @@ }, "/api/v1/workspaces/{workspace_name}/token-usage": { "get": { - "tags": [ - "CodeGate API", - "Workspaces", - "Token Usage" - ], + "tags": ["CodeGate API", "Workspaces", "Token Usage"], "summary": "Get Workspace Token Usage", "description": "Get the token usage of a workspace.", "operationId": "v1_get_workspace_token_usage", @@ -1123,6 +1127,190 @@ } } } + }, + "/api/v1/personas": { + "get": { + "tags": ["CodeGate API", "Personas"], + "summary": "List Personas", + "description": "List all personas.", + "operationId": "v1_list_personas", + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/Persona" + }, + "type": "array", + "title": "Response V1 List Personas" + } + } + } + } + } + }, + "post": { + "tags": ["CodeGate API", "Personas"], + "summary": "Create Persona", + "description": "Create a new persona.", + "operationId": "v1_create_persona", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonaRequest" + } + } + }, + "required": true + }, + "responses": { + "201": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Persona" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } + }, + "/api/v1/personas/{persona_name}": { + "get": { + "tags": ["CodeGate API", "Personas"], + "summary": "Get Persona", + "description": "Get a persona by name.", + "operationId": "v1_get_persona", + "parameters": [ + { + "name": "persona_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Persona Name" + } + } + ], + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Persona" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "put": { + "tags": ["CodeGate API", "Personas"], + "summary": "Update Persona", + "description": "Update an existing persona.", + "operationId": "v1_update_persona", + "parameters": [ + { + "name": "persona_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Persona Name" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonaUpdateRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Successful Response", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Persona" + } + } + } + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + }, + "delete": { + "tags": ["CodeGate API", "Personas"], + "summary": "Delete Persona", + "description": "Delete a persona.", + "operationId": "v1_delete_persona", + "parameters": [ + { + "name": "persona_name", + "in": "path", + "required": true, + "schema": { + "type": "string", + "title": "Persona Name" + } + } + ], + "responses": { + "204": { + "description": "Successful Response" + }, + "422": { + "description": "Validation Error", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HTTPValidationError" + } + } + } + } + } + } } }, "components": { @@ -1135,9 +1323,7 @@ } }, "type": "object", - "required": [ - "name" - ], + "required": ["name"], "title": "ActivateWorkspaceRequest" }, "ActiveWorkspace": { @@ -1155,11 +1341,7 @@ } }, "type": "object", - "required": [ - "name", - "is_active", - "last_updated" - ], + "required": ["name", "is_active", "last_updated"], "title": "ActiveWorkspace" }, "AddProviderEndpointRequest": { @@ -1210,10 +1392,7 @@ } }, "type": "object", - "required": [ - "name", - "provider_type" - ], + "required": ["name", "provider_type"], "title": "AddProviderEndpointRequest", "description": "Represents a request to add a provider endpoint." }, @@ -1346,12 +1525,29 @@ }, "AlertSeverity": { "type": "string", - "enum": [ - "info", - "critical" - ], + "enum": ["info", "critical"], "title": "AlertSeverity" }, + "AlertSummary": { + "properties": { + "malicious_packages": { + "type": "integer", + "title": "Malicious Packages" + }, + "pii": { + "type": "integer", + "title": "Pii" + }, + "secrets": { + "type": "integer", + "title": "Secrets" + } + }, + "type": "object", + "required": ["malicious_packages", "pii", "secrets"], + "title": "AlertSummary", + "description": "Represents a set of summary alerts" + }, "ChatMessage": { "properties": { "message": { @@ -1369,11 +1565,7 @@ } }, "type": "object", - "required": [ - "message", - "timestamp", - "message_id" - ], + "required": ["message", "timestamp", "message_id"], "title": "ChatMessage", "description": "Represents a chat message." }, @@ -1426,11 +1618,7 @@ } }, "type": "object", - "required": [ - "code", - "language", - "filepath" - ], + "required": ["code", "language", "filepath"], "title": "CodeSnippet", "description": "Represents a code snippet with its programming language.\n\nArgs:\n language: The programming language identifier (e.g., 'python', 'javascript')\n code: The actual code content" }, @@ -1452,9 +1640,7 @@ } }, "type": "object", - "required": [ - "auth_type" - ], + "required": ["auth_type"], "title": "ConfigureAuthMaterial", "description": "Represents a request to configure auth material for a provider." }, @@ -1521,7 +1707,18 @@ "title": "Conversation", "description": "Represents a conversation." }, - "CreateOrRenameWorkspaceRequest": { + "CustomInstructions": { + "properties": { + "prompt": { + "type": "string", + "title": "Prompt" + } + }, + "type": "object", + "required": ["prompt"], + "title": "CustomInstructions" + }, + "FullWorkspace-Input": { "properties": { "name": { "type": "string", @@ -1530,43 +1727,38 @@ "config": { "anyOf": [ { - "$ref": "#/components/schemas/WorkspaceConfig" + "$ref": "#/components/schemas/WorkspaceConfig-Input" }, { "type": "null" } ] + } + }, + "type": "object", + "required": ["name"], + "title": "FullWorkspace" + }, + "FullWorkspace-Output": { + "properties": { + "name": { + "type": "string", + "title": "Name" }, - "rename_to": { + "config": { "anyOf": [ { - "type": "string" + "$ref": "#/components/schemas/WorkspaceConfig-Output" }, { "type": "null" } - ], - "title": "Rename To" - } - }, - "type": "object", - "required": [ - "name" - ], - "title": "CreateOrRenameWorkspaceRequest" - }, - "CustomInstructions": { - "properties": { - "prompt": { - "type": "string", - "title": "Prompt" + ] } }, "type": "object", - "required": [ - "prompt" - ], - "title": "CustomInstructions" + "required": ["name"], + "title": "FullWorkspace" }, "HTTPValidationError": { "properties": { @@ -1592,9 +1784,7 @@ } }, "type": "object", - "required": [ - "workspaces" - ], + "required": ["workspaces"], "title": "ListActiveWorkspacesResponse" }, "ListWorkspacesResponse": { @@ -1608,9 +1798,7 @@ } }, "type": "object", - "required": [ - "workspaces" - ], + "required": ["workspaces"], "title": "ListWorkspacesResponse" }, "ModelByProvider": { @@ -1629,11 +1817,7 @@ } }, "type": "object", - "required": [ - "name", - "provider_id", - "provider_name" - ], + "required": ["name", "provider_id", "provider_name"], "title": "ModelByProvider", "description": "Represents a model supported by a provider.\n\nNote that these are auto-discovered by the provider." }, @@ -1642,10 +1826,11 @@ "enum": [ "catch_all", "filename_match", - "request_type_match" + "fim_filename", + "chat_filename" ], "title": "MuxMatcherType", - "description": "Represents the different types of matchers we support." + "description": "Represents the different types of matchers we support.\n\nThe 3 rules present match filenames and request types. They're used in conjunction with the\nmatcher field in the MuxRule model.\nE.g.\n- catch_all-> Always match\n- filename_match and match: requests.py -> Match the request if the filename is requests.py\n- fim_filename and match: main.py -> Match the request if the request type is fim\nand the filename is main.py\n\nNOTE: Removing or updating fields from this enum will require a migration.\nAdding new fields is safe." }, "MuxRule": { "properties": { @@ -1684,21 +1869,65 @@ } }, "type": "object", - "required": [ - "provider_id", - "model", - "matcher_type" - ], + "required": ["provider_id", "model", "matcher_type"], "title": "MuxRule", "description": "Represents a mux rule for a provider." }, + "Persona": { + "properties": { + "id": { + "type": "string", + "title": "Id" + }, + "name": { + "type": "string", + "title": "Name" + }, + "description": { + "type": "string", + "title": "Description" + } + }, + "type": "object", + "required": ["id", "name", "description"], + "title": "Persona", + "description": "Represents a persona object." + }, + "PersonaRequest": { + "properties": { + "name": { + "type": "string", + "title": "Name" + }, + "description": { + "type": "string", + "title": "Description" + } + }, + "type": "object", + "required": ["name", "description"], + "title": "PersonaRequest", + "description": "Model for creating a new Persona." + }, + "PersonaUpdateRequest": { + "properties": { + "new_name": { + "type": "string", + "title": "New Name" + }, + "new_description": { + "type": "string", + "title": "New Description" + } + }, + "type": "object", + "required": ["new_name", "new_description"], + "title": "PersonaUpdateRequest", + "description": "Model for updating a Persona." + }, "ProviderAuthType": { "type": "string", - "enum": [ - "none", - "passthrough", - "api_key" - ], + "enum": ["none", "passthrough", "api_key"], "title": "ProviderAuthType", "description": "Represents the different types of auth we support for providers." }, @@ -1739,10 +1968,7 @@ } }, "type": "object", - "required": [ - "name", - "provider_type" - ], + "required": ["name", "provider_type"], "title": "ProviderEndpoint", "description": "Represents a provider's endpoint configuration. This\nallows us to persist the configuration for each provider,\nso we can use this for muxing messages." }, @@ -1777,19 +2003,13 @@ } }, "type": "object", - "required": [ - "question", - "answer" - ], + "required": ["question", "answer"], "title": "QuestionAnswer", "description": "Represents a question and answer pair." }, "QuestionType": { "type": "string", - "enum": [ - "chat", - "fim" - ], + "enum": ["chat", "fim"], "title": "QuestionType" }, "TokenUsage": { @@ -1833,10 +2053,7 @@ } }, "type": "object", - "required": [ - "tokens_by_model", - "token_usage" - ], + "required": ["tokens_by_model", "token_usage"], "title": "TokenUsageAggregate", "description": "Represents the tokens used. Includes the information of the tokens used by model.\n`used_tokens` are the total tokens used in the `tokens_by_model` list." }, @@ -1854,11 +2071,7 @@ } }, "type": "object", - "required": [ - "provider_type", - "model", - "token_usage" - ], + "required": ["provider_type", "model", "token_usage"], "title": "TokenUsageByModel", "description": "Represents the tokens used by a model." }, @@ -1888,11 +2101,7 @@ } }, "type": "object", - "required": [ - "loc", - "msg", - "type" - ], + "required": ["loc", "msg", "type"], "title": "ValidationError" }, "Workspace": { @@ -1907,17 +2116,14 @@ } }, "type": "object", - "required": [ - "name", - "is_active" - ], + "required": ["name", "is_active"], "title": "Workspace" }, - "WorkspaceConfig": { + "WorkspaceConfig-Input": { "properties": { - "system_prompt": { + "custom_instructions": { "type": "string", - "title": "System Prompt" + "title": "Custom Instructions" }, "muxing_rules": { "items": { @@ -1928,10 +2134,25 @@ } }, "type": "object", - "required": [ - "system_prompt", - "muxing_rules" - ], + "required": ["custom_instructions", "muxing_rules"], + "title": "WorkspaceConfig" + }, + "WorkspaceConfig-Output": { + "properties": { + "custom_instructions": { + "type": "string", + "title": "Custom Instructions" + }, + "muxing_rules": { + "items": { + "$ref": "#/components/schemas/MuxRule" + }, + "type": "array", + "title": "Muxing Rules" + } + }, + "type": "object", + "required": ["custom_instructions", "muxing_rules"], "title": "WorkspaceConfig" }, "WorkspaceWithModel": { @@ -1951,11 +2172,7 @@ } }, "type": "object", - "required": [ - "id", - "name", - "provider_model_name" - ], + "required": ["id", "name", "provider_model_name"], "title": "WorkspaceWithModel", "description": "Returns a workspace ID with model name" } diff --git a/src/features/workspace/components/form-mux-button-delete-rule.tsx b/src/features/workspace/components/form-mux-button-delete-rule.tsx index 326231c1..f60cc24c 100644 --- a/src/features/workspace/components/form-mux-button-delete-rule.tsx +++ b/src/features/workspace/components/form-mux-button-delete-rule.tsx @@ -18,7 +18,6 @@ export function FormMuxButtonDeleteRow({ aria-label="Delete" isIcon isDisabled={row.matcher_type === MuxMatcherType.CATCH_ALL} - isDestructive variant="secondary" onPress={() => remove(index)} > diff --git a/src/features/workspace/components/form-mux-combobox-model.tsx b/src/features/workspace/components/form-mux-combobox-model.tsx index 97eec560..e6789abf 100644 --- a/src/features/workspace/components/form-mux-combobox-model.tsx +++ b/src/features/workspace/components/form-mux-combobox-model.tsx @@ -12,6 +12,7 @@ import { getMuxFieldName } from '../lib/get-mux-field-name' import { SearchMd } from '@untitled-ui/icons-react' import { FormComboBox } from './tmp/form-combobox' import { serializeMuxModel } from '../lib/mux-model-serde' +import { FieldValuesMuxRow } from '../lib/schema-mux' function groupModels( models: ModelByProvider[] = [] @@ -26,7 +27,13 @@ function groupModels( })) } -export function FormMuxComboboxModel({ index }: { index: number }) { +export function FormMuxComboboxModel({ + index, + row, +}: { + index: number + row: FieldValuesMuxRow & { id: string } +}) { const { data: models = [] } = useQueryListAllModelsForAllProviders({ select: groupModels, }) @@ -39,6 +46,7 @@ export function FormMuxComboboxModel({ index }: { index: number }) { field: 'model', index, })} + key={row.id} shouldShowValidationError={false} > diff --git a/src/features/workspace/components/form-mux-fields-labels.tsx b/src/features/workspace/components/form-mux-fields-labels.tsx index 34b47ab6..7f189dd3 100644 --- a/src/features/workspace/components/form-mux-fields-labels.tsx +++ b/src/features/workspace/components/form-mux-fields-labels.tsx @@ -11,28 +11,33 @@ export function FormMuxFieldsLabels() {
    - +
    ) } diff --git a/src/features/workspace/components/form-mux-rule-row.tsx b/src/features/workspace/components/form-mux-rule-row.tsx index 574c05e9..04550216 100644 --- a/src/features/workspace/components/form-mux-rule-row.tsx +++ b/src/features/workspace/components/form-mux-rule-row.tsx @@ -8,7 +8,7 @@ import { FormSelectMatcherType } from './form-mux-select-matcher-type' import { FormMuxButtonDragToReorder } from './form-mux-button-drag-to-reorder' import { muxRowGridStyles } from '../lib/mux-row-grid-styles' import { FormMuxButtonDeleteRow } from './form-mux-button-delete-rule' - +import { useWatch } from 'react-hook-form' export function FormMuxRuleRow({ index, row, @@ -27,6 +27,9 @@ export function FormMuxRuleRow({ transition, } + const watched = useWatch() + console.debug('👉 watched:', watched) + return (
  • - +
  • ) diff --git a/src/features/workspace/components/form-mux-select-matcher-type.tsx b/src/features/workspace/components/form-mux-select-matcher-type.tsx index 36c7fbe8..7edb00d1 100644 --- a/src/features/workspace/components/form-mux-select-matcher-type.tsx +++ b/src/features/workspace/components/form-mux-select-matcher-type.tsx @@ -16,16 +16,20 @@ export function FormSelectMatcherType({ aria-label="Matcher type" items={[ { - id: MuxMatcherType.CATCH_ALL, - textValue: 'Catch-all', + id: MuxMatcherType.FILENAME_MATCH, + textValue: 'FIM & Chat', }, { - id: MuxMatcherType.FILENAME_MATCH, - textValue: 'Filename', + id: MuxMatcherType.FIM_FILENAME, + textValue: 'FIM', }, { - id: MuxMatcherType.REQUEST_TYPE_MATCH, - textValue: 'Request type', + id: MuxMatcherType.CHAT_FILENAME, + textValue: 'Chat', + }, + { + id: MuxMatcherType.CATCH_ALL, + textValue: 'Catch-all', }, ]} name={getMuxFieldName({ diff --git a/src/features/workspace/components/form-mux-text-field-matcher.tsx b/src/features/workspace/components/form-mux-text-field-matcher.tsx index 1917c4c1..0117e27b 100644 --- a/src/features/workspace/components/form-mux-text-field-matcher.tsx +++ b/src/features/workspace/components/form-mux-text-field-matcher.tsx @@ -15,17 +15,13 @@ export function FormMuxTextFieldMatcher({ - + ) } diff --git a/src/features/workspace/components/form-mux.tsx b/src/features/workspace/components/form-mux.tsx index 3ba80be7..74cfbab5 100644 --- a/src/features/workspace/components/form-mux.tsx +++ b/src/features/workspace/components/form-mux.tsx @@ -77,11 +77,9 @@ function MissingProviderBanner() { export function WorkspaceMuxingModel({ className, workspaceName, - isArchived, }: { className?: string workspaceName: string - isArchived: boolean | undefined }) { const { data: muxRulesFromApi, isPending } = useQueryMuxingRulesWorkspace(workspaceName) @@ -158,7 +156,7 @@ export function WorkspaceMuxingModel({ - + diff --git a/src/features/workspace/components/tmp/form-combobox.tsx b/src/features/workspace/components/tmp/form-combobox.tsx index 33053db8..db0f9a3b 100644 --- a/src/features/workspace/components/tmp/form-combobox.tsx +++ b/src/features/workspace/components/tmp/form-combobox.tsx @@ -41,7 +41,7 @@ export function FormComboBox< props.onSelectionChange?.(k) }} ref={ref} - selectedKey={value ?? ''} + selectedKey={value} validationBehavior="aria" // Let React Hook Form handle validation instead of the browser. > {(rp) => { diff --git a/src/features/workspace/lib/schema-mux.ts b/src/features/workspace/lib/schema-mux.ts index fd1d3a81..45f1c2fd 100644 --- a/src/features/workspace/lib/schema-mux.ts +++ b/src/features/workspace/lib/schema-mux.ts @@ -6,7 +6,9 @@ const schemaMuxRow = z.object({ matcher_type: z.nativeEnum(MuxMatcherType, { message: 'Muxing: Matcher type is required', }), - matcher: z.string({ message: 'Muxing: Matcher cannot be empty' }), + matcher: z.string({ message: 'Muxing: Matcher cannot be empty' }).nonempty({ + message: 'Muxing: Matcher cannot be empty', + }), }) export type FieldValuesMuxRow = z.infer From 48e7ed2ab54e80e6e4dee434e770ff0d3ec6e528 Mon Sep 17 00:00:00 2001 From: alex-mcgovern Date: Tue, 11 Mar 2025 09:58:11 +0000 Subject: [PATCH 6/9] tidy ups --- .../components/form-mux-button-add-row.tsx | 3 +- .../components/form-mux-combobox-model.tsx | 10 +----- .../components/form-mux-rule-row.tsx | 2 +- .../workspace/components/form-mux.tsx | 15 +++++++-- .../components/tmp/form-checkbox-group.tsx | 11 ++----- .../components/tmp/form-combobox.tsx | 5 +-- .../tmp/form-discard-changes-button.tsx | 31 +++++++++++++++++++ .../components/tmp/form-radio-group.tsx | 5 +-- .../workspace/components/tmp/form-select.tsx | 5 +-- .../components/tmp/form-text-field.tsx | 5 +-- .../workspace/lib/get-mux-field-name.ts | 6 ++-- src/features/workspace/lib/schema-mux.ts | 4 +-- 12 files changed, 65 insertions(+), 37 deletions(-) create mode 100644 src/features/workspace/components/tmp/form-discard-changes-button.tsx diff --git a/src/features/workspace/components/form-mux-button-add-row.tsx b/src/features/workspace/components/form-mux-button-add-row.tsx index d6a61800..d0ad3e78 100644 --- a/src/features/workspace/components/form-mux-button-add-row.tsx +++ b/src/features/workspace/components/form-mux-button-add-row.tsx @@ -12,8 +12,7 @@ export function FormMuxButtonAddRow() { variant="tertiary" onPress={() => prepend({ - // @ts-expect-error - intentionally prepended with invalid state - model: undefined, + model: '', matcher: '', matcher_type: MuxMatcherType.FILENAME_MATCH, }) diff --git a/src/features/workspace/components/form-mux-combobox-model.tsx b/src/features/workspace/components/form-mux-combobox-model.tsx index e6789abf..97eec560 100644 --- a/src/features/workspace/components/form-mux-combobox-model.tsx +++ b/src/features/workspace/components/form-mux-combobox-model.tsx @@ -12,7 +12,6 @@ import { getMuxFieldName } from '../lib/get-mux-field-name' import { SearchMd } from '@untitled-ui/icons-react' import { FormComboBox } from './tmp/form-combobox' import { serializeMuxModel } from '../lib/mux-model-serde' -import { FieldValuesMuxRow } from '../lib/schema-mux' function groupModels( models: ModelByProvider[] = [] @@ -27,13 +26,7 @@ function groupModels( })) } -export function FormMuxComboboxModel({ - index, - row, -}: { - index: number - row: FieldValuesMuxRow & { id: string } -}) { +export function FormMuxComboboxModel({ index }: { index: number }) { const { data: models = [] } = useQueryListAllModelsForAllProviders({ select: groupModels, }) @@ -46,7 +39,6 @@ export function FormMuxComboboxModel({ field: 'model', index, })} - key={row.id} shouldShowValidationError={false} > diff --git a/src/features/workspace/components/form-mux-rule-row.tsx b/src/features/workspace/components/form-mux-rule-row.tsx index 04550216..1abb3d08 100644 --- a/src/features/workspace/components/form-mux-rule-row.tsx +++ b/src/features/workspace/components/form-mux-rule-row.tsx @@ -39,7 +39,7 @@ export function FormMuxRuleRow({ - + ) diff --git a/src/features/workspace/components/form-mux.tsx b/src/features/workspace/components/form-mux.tsx index 74cfbab5..e57a76b8 100644 --- a/src/features/workspace/components/form-mux.tsx +++ b/src/features/workspace/components/form-mux.tsx @@ -12,7 +12,7 @@ import { import { twMerge } from 'tailwind-merge' import { useMutationPreferredModelWorkspace } from '../hooks/use-mutation-preferred-model-workspace' import { MuxMatcherType, V1GetWorkspaceMuxesResponse } from '@/api/generated' -import { LinkExternal01 } from '@untitled-ui/icons-react' +import { LayersThree01, LinkExternal01 } from '@untitled-ui/icons-react' import { useQueryListAllModelsForAllProviders } from '@/hooks/use-query-list-all-models-for-all-providers' import { useQueryMuxingRulesWorkspace } from '../hooks/use-query-muxing-rules-workspace' import { FormMuxFieldsArray } from './form-mux-fields-array' @@ -26,6 +26,7 @@ import { FormMuxRulesContextProvider } from './form-mux-context-provider' import { deserializeMuxModel, serializeMuxModel } from '../lib/mux-model-serde' import { SubmitHandler } from 'react-hook-form' import { handleMuxFormErrors } from '../lib/handle-mux-form-errors' +import { FormDiscardChangesButton } from './tmp/form-discard-changes-button' const DEFAULT_VALUES: WorkspaceConfigFieldValues = { muxing_rules: [ @@ -157,8 +158,16 @@ export function WorkspaceMuxingModel({ - - +
    + + + Manage providers + +
    +
    + + Save +
    diff --git a/src/features/workspace/components/tmp/form-checkbox-group.tsx b/src/features/workspace/components/tmp/form-checkbox-group.tsx index c848263f..c3dec8c5 100644 --- a/src/features/workspace/components/tmp/form-checkbox-group.tsx +++ b/src/features/workspace/components/tmp/form-checkbox-group.tsx @@ -17,14 +17,7 @@ export function FormCheckboxGroup({ const { control } = useFormContext() const { - field: { - disabled: isDisabledByForm, - name, - onBlur, - onChange, - ref, - value = '', - }, + field: { disabled: isDisabledByForm, name, onBlur, onChange, ref, value }, fieldState: { error, invalid }, } = useController({ control, @@ -44,7 +37,7 @@ export function FormCheckboxGroup({ props.onChange?.(k) }} ref={ref} - value={value} + defaultValue={value} validationBehavior="aria" // Let React Hook Form handle validation instead of the browser. > {(renderProps) => { diff --git a/src/features/workspace/components/tmp/form-combobox.tsx b/src/features/workspace/components/tmp/form-combobox.tsx index db0f9a3b..dea8ada1 100644 --- a/src/features/workspace/components/tmp/form-combobox.tsx +++ b/src/features/workspace/components/tmp/form-combobox.tsx @@ -32,6 +32,7 @@ export function FormComboBox< return ( {(rp) => { return ( diff --git a/src/features/workspace/components/tmp/form-discard-changes-button.tsx b/src/features/workspace/components/tmp/form-discard-changes-button.tsx new file mode 100644 index 00000000..ff0d06a3 --- /dev/null +++ b/src/features/workspace/components/tmp/form-discard-changes-button.tsx @@ -0,0 +1,31 @@ +import { Button } from '@stacklok/ui-kit' +import { FlipBackward } from '@untitled-ui/icons-react' +import type { ComponentProps } from 'react' +import { type FieldValues, useFormContext } from 'react-hook-form' + +export function FormDiscardChangesButton({ + children = ( + <> + + Discard changes + + ), + variant = 'tertiary', + defaultValues, + ...props +}: ComponentProps & { defaultValues: T }) { + const { reset, formState } = useFormContext() + const { isDirty, isValidating } = formState || {} + + return ( + + ) +} diff --git a/src/features/workspace/components/tmp/form-radio-group.tsx b/src/features/workspace/components/tmp/form-radio-group.tsx index c43ad942..ad654c84 100644 --- a/src/features/workspace/components/tmp/form-radio-group.tsx +++ b/src/features/workspace/components/tmp/form-radio-group.tsx @@ -34,6 +34,7 @@ export function FormRadioGroup({ return ( {(renderProps) => { return ( diff --git a/src/features/workspace/components/tmp/form-select.tsx b/src/features/workspace/components/tmp/form-select.tsx index c42fab9a..abf9dca5 100644 --- a/src/features/workspace/components/tmp/form-select.tsx +++ b/src/features/workspace/components/tmp/form-select.tsx @@ -30,6 +30,7 @@ export function FormSelect< return (