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

stacklok / codegate-ui / 13808282424

12 Mar 2025 09:43AM UTC coverage: 66.452% (+0.1%) from 66.347%
13808282424

Pull #379

github

web-flow
Merge d1ad42c49 into 0fb0ba74f
Pull Request #379: feat: shareable workspaces MVP (frontend)

428 of 707 branches covered (60.54%)

Branch coverage included in aggregate %.

36 of 72 new or added lines in 14 files covered. (50.0%)

1 existing line in 1 file now uncovered.

913 of 1311 relevant lines covered (69.64%)

40.19 hits per line

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

83.33
/src/features/workspace/components/workspace-muxing-model.tsx
1
import {
2
  Alert,
3
  Button,
4
  Card,
5
  CardBody,
6
  CardFooter,
7
  Form,
8
  Input,
9
  Label,
10
  Link,
11
  LinkButton,
12
  Select,
13
  SelectButton,
14
  Text,
15
  TextField,
16
  Tooltip,
17
  TooltipInfoButton,
18
  TooltipTrigger,
19
} from '@stacklok/ui-kit'
20
import { twMerge } from 'tailwind-merge'
21
import { useMutationPreferredModelWorkspace } from '../hooks/use-mutation-preferred-model-workspace'
22
import {
23
  ProviderType,
24
  V1ListAllModelsForAllProvidersResponse,
25
} from '@/api/generated'
26
import { FormEvent } from 'react'
27
import {
28
  LayersThree01,
29
  LinkExternal01,
30
  Plus,
31
  Trash01,
32
} from '@untitled-ui/icons-react'
33
import { SortableArea } from '@/components/SortableArea'
34
import { WorkspaceModelsDropdown } from './workspace-models-dropdown'
35
import { useQueryListAllModelsForAllProviders } from '@/hooks/use-query-list-all-models-for-all-providers'
36
import { useQueryMuxingRulesWorkspace } from '../hooks/use-query-muxing-rules-workspace'
37
import {
38
  PreferredMuxRule,
39
  useMuxingRulesFormState,
40
} from '../hooks/use-muxing-rules-form-workspace'
41
import { FormButtons } from '@/components/FormButtons'
42
import { getRuleData, isRequestType } from '../lib/utils'
43
import { z } from 'zod'
44

45
function MissingProviderBanner() {
46
  return (
47
    // TODO needs to update the related ui-kit component that diverges from the design
48
    <Alert
49
      variant="warning"
50
      className="bg-brand-200 font-normal text-primary dark:bg-[#272472]"
51
      title="You must configure at least one provider before selecting your desired model."
52
    >
53
      <LinkButton
54
        className="mt-4 text-white dark:bg-[#7D7DED]"
55
        href="/providers"
56
      >
57
        Configure a provider
58
      </LinkButton>
59
    </Alert>
60
  )
61
}
62

63
type SortableItemProps = {
64
  index: number
65
  rule: PreferredMuxRule
66
  models: V1ListAllModelsForAllProvidersResponse
67
  isArchived: boolean
68
  showRemoveButton: boolean
69
  isDefaultRule: boolean
70
  setRuleItem: (rule: PreferredMuxRule) => void
71
  removeRule: (index: number) => void
72
}
73

74
function SortableItem({
75
  rule,
76
  index,
77
  setRuleItem,
78
  removeRule,
79
  models,
80
  showRemoveButton,
81
  isArchived,
82
  isDefaultRule,
83
}: SortableItemProps) {
84
  const { selectedKey, placeholder, items } = getRuleData({
68✔
85
    isDefaultRule,
86
    matcher_type: rule.matcher_type,
87
  })
88

89
  return (
90
    <div className="flex items-center gap-2" key={rule.id}>
91
      <div className="flex w-2/5 justify-between">
92
        <Select
93
          aria-labelledby="request type"
94
          selectedKey={selectedKey}
95
          name="request_type"
96
          isRequired
97
          isDisabled={isDefaultRule}
98
          className="w-full"
99
          items={items}
100
          onSelectionChange={(matcher_type) => {
101
            if (isRequestType(matcher_type)) {
2!
102
              setRuleItem({ ...rule, matcher_type })
2✔
103
            }
104
          }}
105
        >
106
          <SelectButton />
107
        </Select>
108
      </div>
109
      <div className="flex w-full justify-between">
110
        <TextField
111
          aria-labelledby="filter-by-label-id"
112
          value={rule?.matcher ?? ''}
68!
113
          isDisabled={isArchived || isDefaultRule}
133✔
114
          name="matcher"
115
          onChange={(matcher) => {
116
            setRuleItem({ ...rule, matcher })
7✔
117
          }}
118
        >
119
          <Input placeholder={placeholder} />
120
        </TextField>
121
      </div>
122
      <div className="flex w-3/5 gap-2">
123
        <WorkspaceModelsDropdown
124
          rule={rule}
125
          isArchived={isArchived}
126
          models={models}
127
          onChange={({ model, provider_name, provider_type }) => {
128
            if (provider_type === undefined) return
4!
129
            setRuleItem({
4✔
130
              ...rule,
131
              provider_name,
132
              provider_type: z.nativeEnum(ProviderType).parse(provider_type),
133
              model,
134
            })
135
          }}
136
        />
137
        {showRemoveButton && !isDefaultRule ? (
103✔
138
          <Button
21✔
139
            aria-label="remove mux rule"
140
            isIcon
141
            variant="tertiary"
142
            onPress={() => removeRule(index)}
×
143
          >
144
            <Trash01 />
145
          </Button>
146
        ) : (
147
          <div className="min-w-10 max-w-10" />
148
        )}
149
      </div>
150
    </div>
151
  )
152
}
153

154
export function WorkspaceMuxingModel({
155
  className,
156
  workspaceName,
157
  isArchived,
158
}: {
159
  className?: string
160
  workspaceName: string
161
  isArchived: boolean | undefined
162
}) {
163
  const { data: muxingRules, isPending } =
164
    useQueryMuxingRulesWorkspace(workspaceName)
47✔
165
  const { addRule, setRules, setRuleItem, removeRule, formState } =
166
    useMuxingRulesFormState(muxingRules)
47✔
167
  const {
168
    values: { rules },
169
  } = formState
47✔
170

171
  const { mutateAsync } = useMutationPreferredModelWorkspace()
47✔
172
  const { data: providerModels = [] } = useQueryListAllModelsForAllProviders()
47✔
173
  const isModelsEmpty = !isPending && providerModels.length === 0
47✔
174
  const showRemoveButton = rules.length > 1
47✔
175

176
  const handleSubmit = (event: FormEvent) => {
47✔
177
    event.preventDefault()
1✔
178
    mutateAsync(
1✔
179
      {
180
        path: { workspace_name: workspaceName },
181
        body: rules.map(({ id, provider_type, ...rest }) => {
182
          void id
3✔
183
          if (provider_type === undefined)
3!
NEW
184
            throw new Error('provider_type is required')
×
185

186
          return { provider_type, ...rest }
3✔
187
        }),
188
      },
189
      {
190
        onSuccess: () => {
191
          formState.setInitialValues({ rules })
1✔
192
        },
193
      }
194
    )
195
  }
196

197
  if (isModelsEmpty) {
47!
198
    return (
199
      <Card className={twMerge(className, 'shrink-0')}>
200
        <CardBody className="flex flex-col gap-2">
201
          <Text className="text-primary">Model Muxing</Text>
202
          <MissingProviderBanner />
203
        </CardBody>
204
      </Card>
205
    )
206
  }
207

208
  return (
209
    <Form
210
      onSubmit={handleSubmit}
211
      validationBehavior="aria"
212
      data-testid="preferred-model"
213
    >
214
      <Card className={twMerge(className, 'shrink-0')}>
215
        <CardBody className="flex flex-col gap-6">
216
          <div className="flex flex-col justify-start">
217
            <Text className="text-primary">Model Muxing</Text>
218
            <Text className="mb-0 flex items-center gap-1 text-balance text-secondary">
219
              Select the model you would like to use in this workspace. This
220
              section applies only if you are using the MUX endpoint.
221
              <Link
222
                variant="primary"
223
                className="flex items-center gap-1 no-underline"
224
                href="https://docs.codegate.ai/features/muxing"
225
                target="_blank"
226
              >
227
                Learn more <LinkExternal01 className="size-4" />
228
              </Link>
229
            </Text>
230
          </div>
231

232
          <div className="flex w-full flex-col gap-2">
233
            <div className="flex gap-2">
234
              <div className="w-12">&nbsp;</div>
235
              <div className="w-2/5">Request Type</div>
236
              <div className="w-full">
237
                <Label id="filter-by-label-id" className="flex items-center">
238
                  Filter by
239
                  <TooltipTrigger delay={0}>
240
                    <TooltipInfoButton aria-label="Filter by description" />
241
                    <Tooltip>
242
                      Filters are applied in top-down order. The first rule that
243
                      matches each prompt determines the chosen model. An empty
244
                      filter applies to all prompts.
245
                    </Tooltip>
246
                  </TooltipTrigger>
247
                </Label>
248
              </div>
249
              <div className="w-3/5">
250
                <Label id="preferred-model-id">Model</Label>
251
              </div>
252
            </div>
253
            <SortableArea
254
              items={rules}
255
              setItems={setRules}
256
              disableDragByIndex={rules.length - 1}
257
            >
258
              {(rule, index) => {
259
                const isDefaultRule = rules.length - 1 === index
68✔
260
                return (
261
                  <SortableItem
262
                    key={rule.id}
263
                    index={index}
264
                    rule={rule}
265
                    setRuleItem={setRuleItem}
266
                    removeRule={removeRule}
267
                    models={providerModels}
268
                    showRemoveButton={showRemoveButton}
269
                    isArchived={!!isArchived}
270
                    isDefaultRule={isDefaultRule}
271
                  />
272
                )
273
              }}
274
            </SortableArea>
275
          </div>
276
        </CardBody>
277
        <CardFooter className="justify-between">
278
          <div className="flex gap-2">
279
            <Button
280
              className="w-fit"
281
              variant="tertiary"
282
              onPress={addRule}
283
              isDisabled={isArchived}
284
            >
285
              <Plus /> Add Filter
286
            </Button>
287

288
            <LinkButton className="w-fit" variant="tertiary" href="/providers">
289
              <LayersThree01 /> Manage providers
290
            </LinkButton>
291
          </div>
292
          <FormButtons
293
            isPending={isPending}
294
            formState={formState}
295
            canSubmit={!isArchived}
296
          />
297
        </CardFooter>
298
      </Card>
299
    </Form>
300
  )
301
}
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