• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

stacklok / codegate-ui / 13903674755

17 Mar 2025 03:42PM UTC coverage: 61.874% (-4.6%) from 66.452%
13903674755

Pull #345

github

web-flow
Merge ce2550358 into 5d463ef04
Pull Request #345: feat: react-hook-form field array for provider muxes

443 of 798 branches covered (55.51%)

Branch coverage included in aggregate %.

63 of 93 new or added lines in 21 files covered. (67.74%)

50 existing lines in 4 files now uncovered.

904 of 1379 relevant lines covered (65.55%)

36.88 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.91
/src/features/workspace/components/form-mux-rules.tsx
1
import {
2
  Alert,
3
  Card,
4
  CardBody,
5
  CardFooter,
6
  FormSubmitButton,
7
  FormV2,
8
  Link,
9
  LinkButton,
10
  Text,
11
} from '@stacklok/ui-kit'
12
import { twMerge } from 'tailwind-merge'
13
import { useMutationPreferredModelWorkspace } from '../hooks/use-mutation-preferred-model-workspace'
14
import { MuxMatcherType, V1GetWorkspaceMuxesResponse } from '@/api/generated'
15
import {
16
  FlipBackward,
17
  LayersThree01,
18
  LinkExternal01,
19
} from '@untitled-ui/icons-react'
20
import { useQueryListAllModelsForAllProviders } from '@/hooks/use-query-list-all-models-for-all-providers'
21
import { useQueryMuxingRulesWorkspace } from '../hooks/use-query-muxing-rules-workspace'
22
import { FormMuxFieldsArray } from './form-mux-fields-array'
23
import {
24
  schemaWorkspaceConfig,
25
  WorkspaceConfigFieldValues,
26
} from '../lib/schema-mux'
27
import { zodResolver } from '@hookform/resolvers/zod'
28
import { FormMuxButtonAddRow } from './form-mux-button-add-row'
29
import { FormMuxRulesContextProvider } from './form-mux-context-provider'
30
import { deserializeMuxModel, serializeMuxModel } from '../lib/mux-model-serde'
31
import { SubmitHandler } from 'react-hook-form'
32
import { handleMuxFormErrors } from '../lib/handle-mux-form-errors'
33
import { FormDiscardChangesButton } from './tmp/form-discard-changes-button'
34

35
const DEFAULT_VALUES: WorkspaceConfigFieldValues = {
4✔
36
  muxing_rules: [
37
    {
38
      // @ts-expect-error - we start with invalid state
39
      model: undefined,
40
      matcher: 'Catch-all',
41
      matcher_type: MuxMatcherType.CATCH_ALL,
42
    },
43
  ],
44
}
45

46
const fromApiMuxingRules = (
4✔
47
  rules: V1GetWorkspaceMuxesResponse
48
): WorkspaceConfigFieldValues => {
NEW
49
  return {
×
50
    muxing_rules: rules.map(
NEW
51
      ({ matcher_type, model, matcher, provider_name, provider_type }) => ({
×
52
        model: serializeMuxModel({
53
          name: model,
54
          provider_type,
55
          provider_name: provider_name as string,
56
        }),
57
        matcher_type,
58
        matcher: matcher ?? '',
×
59
      })
60
    ),
61
  }
62
}
63

64
function MissingProviderBanner() {
65
  return (
66
    // TODO needs to update the related ui-kit component that diverges from the design
67
    <Alert
68
      variant="warning"
69
      className="bg-brand-200 font-normal text-primary dark:bg-[#272472]"
70
      title="You must configure at least one provider before selecting your desired model."
71
    >
72
      <LinkButton
73
        className="mt-4 text-white dark:bg-[#7D7DED]"
74
        href="/providers"
75
      >
76
        Configure a provider
77
      </LinkButton>
78
    </Alert>
79
  )
80
}
81

82
export function FormMuxRules({
83
  className,
84
  workspaceName,
85
  isDisabled,
86
}: {
87
  className?: string
88
  workspaceName: string
89
  isDisabled: boolean
90
}) {
91
  const { data: muxRulesFromApi, isPending } =
92
    useQueryMuxingRulesWorkspace(workspaceName)
20✔
93

94
  const { mutateAsync } = useMutationPreferredModelWorkspace()
20✔
95
  const { data: providerModels = [] } = useQueryListAllModelsForAllProviders()
20✔
96
  const isModelsEmpty = !isPending && providerModels.length === 0
20✔
97

98
  const handleSubmit: SubmitHandler<WorkspaceConfigFieldValues> = (data) => {
20✔
99
    mutateAsync({
1✔
100
      path: { workspace_name: workspaceName },
101
      body: data.muxing_rules.map(({ matcher, matcher_type, model }) => {
102
        const {
103
          name: modelName,
104
          provider_name,
105
          provider_type,
106
        } = deserializeMuxModel(model)
2✔
107
        return {
2✔
108
          matcher_type,
109
          model: modelName,
110
          provider_type,
111
          matcher,
112
          provider_name,
113
        }
114
      }),
115
    })
116
  }
117

118
  if (isModelsEmpty) {
20!
119
    return (
120
      <Card className={twMerge(className, 'shrink-0')}>
121
        <CardBody className="flex flex-col gap-2">
122
          <Text className="text-primary">Model Muxing</Text>
123
          <MissingProviderBanner />
124
        </CardBody>
125
      </Card>
126
    )
127
  }
128

129
  const defaultValues =
130
    muxRulesFromApi.length > 0
20!
131
      ? fromApiMuxingRules(muxRulesFromApi)
132
      : DEFAULT_VALUES
133

134
  return (
135
    <FormV2<WorkspaceConfigFieldValues>
136
      onSubmit={handleSubmit}
137
      onError={handleMuxFormErrors}
138
      options={{
139
        values: defaultValues,
140
        resolver: zodResolver(schemaWorkspaceConfig),
141
        shouldFocusError: true,
142
      }}
143
    >
144
      <FormMuxRulesContextProvider>
145
        <Card className={twMerge(className, 'shrink-0')}>
146
          <CardBody>
147
            <div className="flex flex-col justify-start">
148
              <Text className="text-primary">Model Muxing</Text>
149
              <Text className="mb-6 flex items-center gap-1 text-balance text-secondary">
150
                Select the model you would like to use in this workspace. This
151
                section applies only if you are using the MUX endpoint.
152
                <Link
153
                  variant="primary"
154
                  className="flex items-center gap-1 no-underline"
155
                  href="https://docs.codegate.ai/features/muxing"
156
                  target="_blank"
157
                >
158
                  Learn more <LinkExternal01 className="size-4" />
159
                </Link>
160
              </Text>
161
            </div>
162

163
            <FormMuxFieldsArray isDisabled={isDisabled} />
164
          </CardBody>
165

166
          <CardFooter className="justify-between">
167
            <div className="flex gap-2">
168
              <FormMuxButtonAddRow isDisabled={isDisabled} />
169
              <LinkButton variant="tertiary" href="/providers">
170
                <LayersThree01 /> Manage providers
171
              </LinkButton>
172
            </div>
173
            <div className="flex gap-2">
174
              <FormDiscardChangesButton
175
                aria-label="Revert changes"
176
                isDisabled={isDisabled}
177
                defaultValues={defaultValues}
178
              >
179
                <FlipBackward />
180
                Revert changes
181
              </FormDiscardChangesButton>
182
              <FormSubmitButton aria-label="Save" isDisabled={isDisabled}>
183
                Save
184
              </FormSubmitButton>
185
            </div>
186
          </CardFooter>
187
        </Card>
188
      </FormMuxRulesContextProvider>
189
    </FormV2>
190
  )
191
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc