From 76dce58af74e49c3d829b442099d9e4c09f94a0e Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Fri, 31 Jan 2025 13:59:18 +0100 Subject: [PATCH 01/17] feat: get preferred model by workspace --- .../components/workspace-preferred-model.tsx | 3 +- .../hooks/use-preferred-preferred-model.ts | 38 ++++++++++++++++++- 2 files changed, 38 insertions(+), 3 deletions(-) diff --git a/src/features/workspace/components/workspace-preferred-model.tsx b/src/features/workspace/components/workspace-preferred-model.tsx index f3a1c75b..a3aa2d21 100644 --- a/src/features/workspace/components/workspace-preferred-model.tsx +++ b/src/features/workspace/components/workspace-preferred-model.tsx @@ -23,7 +23,8 @@ export function WorkspacePreferredModel({ workspaceName: string; isArchived: boolean | undefined; }) { - const { preferredModel, setPreferredModel } = usePreferredModelWorkspace(); + const { preferredModel, setPreferredModel } = + usePreferredModelWorkspace(workspaceName); const { mutateAsync } = useMutationPreferredModelWorkspace(); const { data: providerModels = [] } = useModelsData(); const { model, provider_id } = preferredModel; diff --git a/src/features/workspace/hooks/use-preferred-preferred-model.ts b/src/features/workspace/hooks/use-preferred-preferred-model.ts index 555a2a23..cb74e4a9 100644 --- a/src/features/workspace/hooks/use-preferred-preferred-model.ts +++ b/src/features/workspace/hooks/use-preferred-preferred-model.ts @@ -1,4 +1,7 @@ -import { MuxRule } from "@/api/generated"; +import { MuxRule, V1GetWorkspaceMuxesData } from "@/api/generated"; +import { v1GetWorkspaceMuxesOptions } from "@/api/generated/@tanstack/react-query.gen"; +import { useQuery } from "@tanstack/react-query"; +import { useEffect, useMemo } from "react"; import { create } from "zustand"; export type ModelRule = Omit & {}; @@ -8,7 +11,7 @@ type State = { preferredModel: ModelRule; }; -export const usePreferredModelWorkspace = create((set) => ({ +const useModelValue = create((set) => ({ preferredModel: { provider_id: "", model: "", @@ -17,3 +20,34 @@ export const usePreferredModelWorkspace = create((set) => ({ set({ preferredModel: { provider_id, model } }); }, })); + +const usePreferredModel = (options: { + path: { + workspace_name: string; + }; +}) => { + return useQuery({ + ...v1GetWorkspaceMuxesOptions(options), + }); +}; + +export const usePreferredModelWorkspace = (workspaceName: string) => { + const options: V1GetWorkspaceMuxesData & + Omit = useMemo( + () => ({ + path: { workspace_name: workspaceName }, + }), + [workspaceName], + ); + const { data } = usePreferredModel(options); + const { preferredModel, setPreferredModel } = useModelValue(); + + useEffect(() => { + const providerModel = data?.[0]; + if (providerModel) { + setPreferredModel(providerModel); + } + }, [data, setPreferredModel]); + + return { preferredModel, setPreferredModel }; +}; From 482d922dab436d58fb15d2fac954c5dd05972b32 Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Mon, 3 Feb 2025 12:48:23 +0100 Subject: [PATCH 02/17] enable muxing --- .../components/workspace-preferred-model.tsx | 2 +- .../hooks/use-preferred-preferred-model.ts | 1 - src/hooks/use-models-data.ts | 8 +++++ src/hooks/useModelsData.ts | 32 ------------------- src/routes/route-workspace.tsx | 2 +- 5 files changed, 10 insertions(+), 35 deletions(-) create mode 100644 src/hooks/use-models-data.ts delete mode 100644 src/hooks/useModelsData.ts diff --git a/src/features/workspace/components/workspace-preferred-model.tsx b/src/features/workspace/components/workspace-preferred-model.tsx index a3aa2d21..47747732 100644 --- a/src/features/workspace/components/workspace-preferred-model.tsx +++ b/src/features/workspace/components/workspace-preferred-model.tsx @@ -12,7 +12,7 @@ import { MuxMatcherType } from "@/api/generated"; import { FormEvent } from "react"; import { usePreferredModelWorkspace } from "../hooks/use-preferred-preferred-model"; import { Select, SelectButton } from "@stacklok/ui-kit"; -import { useModelsData } from "@/hooks/useModelsData"; +import { useModelsData } from "@/hooks/use-models-data"; export function WorkspacePreferredModel({ className, diff --git a/src/features/workspace/hooks/use-preferred-preferred-model.ts b/src/features/workspace/hooks/use-preferred-preferred-model.ts index cb74e4a9..834a2ded 100644 --- a/src/features/workspace/hooks/use-preferred-preferred-model.ts +++ b/src/features/workspace/hooks/use-preferred-preferred-model.ts @@ -30,7 +30,6 @@ const usePreferredModel = (options: { ...v1GetWorkspaceMuxesOptions(options), }); }; - export const usePreferredModelWorkspace = (workspaceName: string) => { const options: V1GetWorkspaceMuxesData & Omit = useMemo( diff --git a/src/hooks/use-models-data.ts b/src/hooks/use-models-data.ts new file mode 100644 index 00000000..b9fc280b --- /dev/null +++ b/src/hooks/use-models-data.ts @@ -0,0 +1,8 @@ +import { useQuery } from "@tanstack/react-query"; +import { v1ListAllModelsForAllProvidersOptions } from "@/api/generated/@tanstack/react-query.gen"; + +export const useModelsData = () => { + return useQuery({ + ...v1ListAllModelsForAllProvidersOptions(), + }); +}; diff --git a/src/hooks/useModelsData.ts b/src/hooks/useModelsData.ts deleted file mode 100644 index b58fda4e..00000000 --- a/src/hooks/useModelsData.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useQuery } from "@tanstack/react-query"; -import { v1ListAllModelsForAllProvidersOptions } from "@/api/generated/@tanstack/react-query.gen"; -import { V1ListAllModelsForAllProvidersResponse } from "@/api/generated"; - -export const useModelsData = () => { - return useQuery({ - ...v1ListAllModelsForAllProvidersOptions(), - queryFn: async () => { - const response: V1ListAllModelsForAllProvidersResponse = [ - { - name: "claude-3.5", - provider_name: "anthropic", - provider_id: "anthropic", - }, - { - name: "claude-3.6", - provider_name: "anthropic", - provider_id: "anthropic", - }, - { - name: "claude-3.7", - provider_name: "anthropic", - provider_id: "anthropic", - }, - { name: "chatgpt-4o", provider_name: "openai", provider_id: "openai" }, - { name: "chatgpt-4p", provider_name: "openai", provider_id: "openai" }, - ]; - - return response; - }, - }); -}; diff --git a/src/routes/route-workspace.tsx b/src/routes/route-workspace.tsx index ba265320..b42ddd24 100644 --- a/src/routes/route-workspace.tsx +++ b/src/routes/route-workspace.tsx @@ -54,7 +54,7 @@ export function RouteWorkspace() { workspaceName={name} /> From c5ca1c0b263d7b31229e04059573a63625ce339a Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Tue, 4 Feb 2025 17:13:51 +0100 Subject: [PATCH 03/17] chore: update openapi --- src/api/generated/types.gen.ts | 18 +++++++-- src/api/openapi.json | 71 +++++++++++++++++++++++++++++++--- 2 files changed, 81 insertions(+), 8 deletions(-) diff --git a/src/api/generated/types.gen.ts b/src/api/generated/types.gen.ts index ec403510..2a202061 100644 --- a/src/api/generated/types.gen.ts +++ b/src/api/generated/types.gen.ts @@ -10,6 +10,19 @@ export type ActiveWorkspace = { last_updated: unknown; }; +/** + * Represents a request to add a provider endpoint. + */ +export type AddProviderEndpointRequest = { + id?: string | null; + name: string; + description?: string; + provider_type: ProviderType; + endpoint?: string; + auth_type?: ProviderAuthType | null; + api_key?: string | null; +}; + /** * Represents an alert with it's respective conversation. */ @@ -100,7 +113,6 @@ export type ModelByProvider = { * Represents the different types of matchers we support. */ export enum MuxMatcherType { - FILE_REGEX = "file_regex", CATCH_ALL = "catch_all", } @@ -133,7 +145,7 @@ export type ProviderEndpoint = { name: string; description?: string; provider_type: ProviderType; - endpoint: string; + endpoint?: string; auth_type?: ProviderAuthType | null; }; @@ -219,7 +231,7 @@ export type V1ListProviderEndpointsResponse = Array; export type V1ListProviderEndpointsError = HTTPValidationError; export type V1AddProviderEndpointData = { - body: ProviderEndpoint; + body: AddProviderEndpointRequest; }; export type V1AddProviderEndpointResponse = ProviderEndpoint; diff --git a/src/api/openapi.json b/src/api/openapi.json index 67d67ecb..6536af0f 100644 --- a/src/api/openapi.json +++ b/src/api/openapi.json @@ -92,7 +92,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProviderEndpoint" + "$ref": "#/components/schemas/AddProviderEndpointRequest" } } } @@ -1113,6 +1113,68 @@ ], "title": "ActiveWorkspace" }, + "AddProviderEndpointRequest": { + "properties": { + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Id", + "default": "" + }, + "name": { + "type": "string", + "title": "Name" + }, + "description": { + "type": "string", + "title": "Description", + "default": "" + }, + "provider_type": { + "$ref": "#/components/schemas/ProviderType" + }, + "endpoint": { + "type": "string", + "title": "Endpoint", + "default": "" + }, + "auth_type": { + "anyOf": [ + { + "$ref": "#/components/schemas/ProviderAuthType" + }, + { + "type": "null" + } + ], + "default": "none" + }, + "api_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "title": "Api Key" + } + }, + "type": "object", + "required": [ + "name", + "provider_type" + ], + "title": "AddProviderEndpointRequest", + "description": "Represents a request to add a provider endpoint." + }, "AlertConversation": { "properties": { "conversation": { @@ -1437,7 +1499,6 @@ "MuxMatcherType": { "type": "string", "enum": [ - "file_regex", "catch_all" ], "title": "MuxMatcherType", @@ -1515,7 +1576,8 @@ }, "endpoint": { "type": "string", - "title": "Endpoint" + "title": "Endpoint", + "default": "" }, "auth_type": { "anyOf": [ @@ -1532,8 +1594,7 @@ "type": "object", "required": [ "name", - "provider_type", - "endpoint" + "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." From ea22f3e6398440f9f0ec36a53bb5121fdb0072b0 Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Tue, 4 Feb 2025 17:14:30 +0100 Subject: [PATCH 04/17] chore: add @tanstack/react-query-devtools --- src/main.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main.tsx b/src/main.tsx index 4a874faf..7398fed0 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -27,6 +27,7 @@ createRoot(document.getElementById("root")!).render( }> + From 2ef4ab3080badb509f7408c0c5a5966022a0e30e Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Tue, 4 Feb 2025 17:15:42 +0100 Subject: [PATCH 05/17] feat: providers endpoint crud --- src/Page.tsx | 12 ++ .../components/provider-dialog-footer.tsx | 17 +++ .../providers/components/provider-dialog.tsx | 42 ++++++ .../providers/components/provider-form.tsx | 124 ++++++++++++++++++ .../providers/components/table-actions.tsx | 53 ++++++++ .../providers/components/table-providers.tsx | 78 +++++++++++ .../hooks/use-confirm-delete-provider.tsx | 33 +++++ .../hooks/use-invalidate-providers-queries.ts | 16 +++ .../hooks/use-mutation-create-provider.ts | 16 +++ .../hooks/use-mutation-delete-provider.ts | 13 ++ .../hooks/use-mutation-update-provider.ts | 58 ++++++++ src/features/providers/hooks/use-provider.ts | 28 ++++ src/features/providers/hooks/use-providers.ts | 10 ++ src/features/providers/lib/utils.ts | 29 ++++ src/hooks/use-models-data.ts | 3 + src/lib/utils.ts | 11 +- src/routes/route-provider-create.tsx | 41 ++++++ src/routes/route-provider-update.tsx | 47 +++++++ src/routes/route-providers.tsx | 44 +++++++ 19 files changed, 672 insertions(+), 3 deletions(-) create mode 100644 src/features/providers/components/provider-dialog-footer.tsx create mode 100644 src/features/providers/components/provider-dialog.tsx create mode 100644 src/features/providers/components/provider-form.tsx create mode 100644 src/features/providers/components/table-actions.tsx create mode 100644 src/features/providers/components/table-providers.tsx create mode 100644 src/features/providers/hooks/use-confirm-delete-provider.tsx create mode 100644 src/features/providers/hooks/use-invalidate-providers-queries.ts create mode 100644 src/features/providers/hooks/use-mutation-create-provider.ts create mode 100644 src/features/providers/hooks/use-mutation-delete-provider.ts create mode 100644 src/features/providers/hooks/use-mutation-update-provider.ts create mode 100644 src/features/providers/hooks/use-provider.ts create mode 100644 src/features/providers/hooks/use-providers.ts create mode 100644 src/features/providers/lib/utils.ts create mode 100644 src/routes/route-provider-create.tsx create mode 100644 src/routes/route-provider-update.tsx create mode 100644 src/routes/route-providers.tsx diff --git a/src/Page.tsx b/src/Page.tsx index 1abcfcde..65d5c1ce 100644 --- a/src/Page.tsx +++ b/src/Page.tsx @@ -8,6 +8,9 @@ import { RouteDashboard } from "./routes/route-dashboard"; import { RouteCertificateSecurity } from "./routes/route-certificate-security"; import { RouteWorkspaceCreation } from "./routes/route-workspace-creation"; import { RouteNotFound } from "./routes/route-not-found"; +import { RouteProvider } from "./routes/route-providers"; +import { RouteProviderCreate } from "./routes/route-provider-create"; +import { RouteProviderUpdate } from "./routes/route-provider-update"; export default function Page() { return ( @@ -22,6 +25,15 @@ export default function Page() { path="/certificates/security" element={} /> + + + } /> + }> + } /> + } /> + + + } /> ); diff --git a/src/features/providers/components/provider-dialog-footer.tsx b/src/features/providers/components/provider-dialog-footer.tsx new file mode 100644 index 00000000..3297c601 --- /dev/null +++ b/src/features/providers/components/provider-dialog-footer.tsx @@ -0,0 +1,17 @@ +import { Button, DialogFooter } from "@stacklok/ui-kit"; +import { useNavigate } from "react-router-dom"; + +export function ProviderDialogFooter() { + const navigate = useNavigate(); + + return ( + + + + + ); +} diff --git a/src/features/providers/components/provider-dialog.tsx b/src/features/providers/components/provider-dialog.tsx new file mode 100644 index 00000000..5fd64df9 --- /dev/null +++ b/src/features/providers/components/provider-dialog.tsx @@ -0,0 +1,42 @@ +import { + DialogModalOverlay, + DialogModal, + Dialog, + DialogHeader, + DialogTitle, + DialogCloseButton, +} from "@stacklok/ui-kit"; +import { useNavigate } from "react-router-dom"; +import { useState } from "react"; + +export function ProviderDialog({ + title, + children, +}: { + title: string; + children: React.ReactNode; +}) { + const [isModalOpen, setIsModalOpen] = useState(true); + const navigate = useNavigate(); + + return ( + { + setIsModalOpen(isOpen); + navigate("/providers"); + }} + > + + + + {title} + + + {children} + + + + ); +} diff --git a/src/features/providers/components/provider-form.tsx b/src/features/providers/components/provider-form.tsx new file mode 100644 index 00000000..7ab9a8b2 --- /dev/null +++ b/src/features/providers/components/provider-form.tsx @@ -0,0 +1,124 @@ +import { AddProviderEndpointRequest, ProviderAuthType } from "@/api/generated"; +import { + Label, + Select, + SelectButton, + Input, + TextField, +} from "@stacklok/ui-kit"; +import { + getAuthTypeOptions, + getProviderType, + isProviderAuthType, + isProviderType, +} from "../lib/utils"; + +interface Props { + provider: AddProviderEndpointRequest; + setProvider: (provider: AddProviderEndpointRequest) => void; +} + +export function ProviderForm({ provider, setProvider }: Props) { + return ( +
+
+ setProvider({ ...provider, name })} + > + + + +
+
+ + +
+
+ setProvider({ ...provider, description })} + > + + + +
+
+ setProvider({ ...provider, endpoint })} + > + + + +
+
+ + +
+ + {provider.auth_type === ProviderAuthType.API_KEY && ( +
+ setProvider({ ...provider, api_key })} + > + + + +
+ )} +
+ ); +} diff --git a/src/features/providers/components/table-actions.tsx b/src/features/providers/components/table-actions.tsx new file mode 100644 index 00000000..68c28fcc --- /dev/null +++ b/src/features/providers/components/table-actions.tsx @@ -0,0 +1,53 @@ +import { ProviderEndpoint } from "@/api/generated"; +import { + MenuTrigger, + Button, + Popover, + Menu, + OptionsSchema, +} from "@stacklok/ui-kit"; +import { DotsVertical, Settings04, Trash01 } from "@untitled-ui/icons-react"; +import { useMutationDeleteProvider } from "../hooks/use-mutation-delete-provider"; +import { useConfirmDeleteProvider } from "../hooks/use-confirm-delete-provider"; + +const getProviderActions = ({ + provider, + deleteProvider, +}: { + provider: ProviderEndpoint; + deleteProvider: ReturnType["mutateAsync"]; +}): OptionsSchema<"menu">[] => [ + { + textValue: "Edit", + icon: , + id: "edit", + href: `/providers/${provider.id}`, + }, + { + textValue: "Delete", + icon: , + id: "delete", + onAction: () => + deleteProvider({ path: { provider_id: provider.id as string } }), + }, +]; + +export function TableActions({ provider }: { provider: ProviderEndpoint }) { + const deleteProvider = useConfirmDeleteProvider(); + + return ( + + + + + + + ); +} diff --git a/src/features/providers/components/table-providers.tsx b/src/features/providers/components/table-providers.tsx new file mode 100644 index 00000000..3a140536 --- /dev/null +++ b/src/features/providers/components/table-providers.tsx @@ -0,0 +1,78 @@ +import { + Badge, + Cell, + Column, + Row, + Table, + TableBody, + LinkButton, + TableHeader, +} from "@stacklok/ui-kit"; +import { Globe02, Tool01 } from "@untitled-ui/icons-react"; +import { PROVIDER_AUTH_TYPE_MAP } from "../lib/utils"; +import { TableActions } from "./table-actions"; +import { useProviders } from "../hooks/use-providers"; + +export function TableProviders() { + const { data: providers = [] } = useProviders(); + + return ( + + + + + name & description + + + provider + + + endpoint + + + authentication + + + + + + {providers.map((provider) => ( + + +
{provider.name}
+
{provider.description}
+
+ {provider.provider_type} + +
+ + {provider.endpoint} +
+
+ +
+ {provider.auth_type ? ( + + {PROVIDER_AUTH_TYPE_MAP[provider.auth_type]} + + ) : ( + "N/A" + )} + + Manage + +
+
+ + + +
+ ))} +
+
+ ); +} diff --git a/src/features/providers/hooks/use-confirm-delete-provider.tsx b/src/features/providers/hooks/use-confirm-delete-provider.tsx new file mode 100644 index 00000000..ac6ad6b6 --- /dev/null +++ b/src/features/providers/hooks/use-confirm-delete-provider.tsx @@ -0,0 +1,33 @@ +import { useConfirm } from "@/hooks/use-confirm"; +import { useCallback } from "react"; +import { useMutationDeleteProvider } from "./use-mutation-delete-provider"; + +export function useConfirmDeleteProvider() { + const { mutateAsync: deleteProvider } = useMutationDeleteProvider(); + + const { confirm } = useConfirm(); + + return useCallback( + async (...params: Parameters) => { + const answer = await confirm( + <> +

+ Are you sure you want to permanently delete this provider? +

+ , + { + buttons: { + yes: "Delete", + no: "Cancel", + }, + title: "Permanently delete provider", + isDestructive: true, + } + ); + if (answer) { + return deleteProvider(...params); + } + }, + [confirm, deleteProvider] + ); +} diff --git a/src/features/providers/hooks/use-invalidate-providers-queries.ts b/src/features/providers/hooks/use-invalidate-providers-queries.ts new file mode 100644 index 00000000..36aee59c --- /dev/null +++ b/src/features/providers/hooks/use-invalidate-providers-queries.ts @@ -0,0 +1,16 @@ +import { v1ListProviderEndpointsQueryKey } from "@/api/generated/@tanstack/react-query.gen"; +import { useQueryClient } from "@tanstack/react-query"; +import { useCallback } from "react"; + +export function useInvalidateProvidersQueries() { + const queryClient = useQueryClient(); + + const invalidate = useCallback(async () => { + await queryClient.invalidateQueries({ + queryKey: v1ListProviderEndpointsQueryKey(), + refetchType: "all", + }); + }, [queryClient]); + + return invalidate; +} diff --git a/src/features/providers/hooks/use-mutation-create-provider.ts b/src/features/providers/hooks/use-mutation-create-provider.ts new file mode 100644 index 00000000..b420d940 --- /dev/null +++ b/src/features/providers/hooks/use-mutation-create-provider.ts @@ -0,0 +1,16 @@ +import { v1AddProviderEndpointMutation } from "@/api/generated/@tanstack/react-query.gen"; +import { useToastMutation } from "@/hooks/use-toast-mutation"; +import { useInvalidateProvidersQueries } from "./use-invalidate-providers-queries"; +import { useNavigate } from "react-router-dom"; + +export function useMutationCreateProvider() { + const navigate = useNavigate(); + const invalidate = useInvalidateProvidersQueries(); + return useToastMutation({ + ...v1AddProviderEndpointMutation(), + onSuccess: async () => { + await invalidate(); + navigate("/providers"); + }, + }); +} diff --git a/src/features/providers/hooks/use-mutation-delete-provider.ts b/src/features/providers/hooks/use-mutation-delete-provider.ts new file mode 100644 index 00000000..e647db5a --- /dev/null +++ b/src/features/providers/hooks/use-mutation-delete-provider.ts @@ -0,0 +1,13 @@ +import { useToastMutation } from "@/hooks/use-toast-mutation"; +import { useInvalidateProvidersQueries } from "./use-invalidate-providers-queries"; +import { v1DeleteProviderEndpointMutation } from "@/api/generated/@tanstack/react-query.gen"; + +export const useMutationDeleteProvider = () => { + const invalidate = useInvalidateProvidersQueries(); + + return useToastMutation({ + ...v1DeleteProviderEndpointMutation(), + onSuccess: () => invalidate(), + successMsg: () => "Successfully delete provider", + }); +}; diff --git a/src/features/providers/hooks/use-mutation-update-provider.ts b/src/features/providers/hooks/use-mutation-update-provider.ts new file mode 100644 index 00000000..54908ac2 --- /dev/null +++ b/src/features/providers/hooks/use-mutation-update-provider.ts @@ -0,0 +1,58 @@ +import { + v1ConfigureAuthMaterialMutation, + v1UpdateProviderEndpointMutation, +} from "@/api/generated/@tanstack/react-query.gen"; +import { useToastMutation } from "@/hooks/use-toast-mutation"; +import { useNavigate } from "react-router-dom"; +import { useInvalidateProvidersQueries } from "./use-invalidate-providers-queries"; + +// export function useMutationUpdateProvider() { +// return useToastMutation({ +// mutationFn: async (provider) => { +// console.log(provider); +// v1ConfigureAuthMaterialMutation(); +// v1UpdateProviderEndpointMutation(); + +// return; +// }, +// successMsg: () => { +// return "cioenn"; +// }, +// }); +// } + +// const mutationProvider = useMutation({ +// ...v1UpdateProviderEndpointMutation(), +// }); + +// if (mutationAuthMaterial.isSuccess && mutationProvider.isSuccess) { +// toast.success("sdkasdjk"); +// } + +// if (mutationAuthMaterial.isError && mutationProvider.isError) { +// toast.error("sdkasdjk"); +// } + +// return { mutationAuthMaterial, mutationProvider }; + +export function useMutationUpdateProvider() { + const navigate = useNavigate(); + const invalidate = useInvalidateProvidersQueries(); + + const mutationFn = async () => { + const configureAuthMaterialMutation = v1ConfigureAuthMaterialMutation(); + const updateProviderEndpointMutation = v1UpdateProviderEndpointMutation(); + + return { configureAuthMaterialMutation, updateProviderEndpointMutation }; + }; + + return useToastMutation({ + mutationFn, + successMsg: "Success", + errorMsg: "error", + onSuccess: async () => { + await invalidate(); + navigate("/providers"); + }, + }); +} diff --git a/src/features/providers/hooks/use-provider.ts b/src/features/providers/hooks/use-provider.ts new file mode 100644 index 00000000..1b685972 --- /dev/null +++ b/src/features/providers/hooks/use-provider.ts @@ -0,0 +1,28 @@ +import { useQuery } from "@tanstack/react-query"; +import { v1GetProviderEndpointOptions } from "@/api/generated/@tanstack/react-query.gen"; +import { AddProviderEndpointRequest, ProviderType } from "@/api/generated"; +import { useEffect, useState } from "react"; + +export function useProvider(providerId: string) { + const [provider, setProvider] = useState({ + name: "", + description: "", + auth_type: null, + provider_type: ProviderType.OPENAI, + endpoint: "", + api_key: "" + }); + + const { data, isPending, isError } = useQuery({ + ...v1GetProviderEndpointOptions({ path: { provider_id: providerId } }), + }); + + useEffect(() => { + if (data) { + setProvider(data); + } + }, [data]); + + console.log({ provider }); + return { isPending, isError, provider, setProvider }; +} diff --git a/src/features/providers/hooks/use-providers.ts b/src/features/providers/hooks/use-providers.ts new file mode 100644 index 00000000..e652714a --- /dev/null +++ b/src/features/providers/hooks/use-providers.ts @@ -0,0 +1,10 @@ +import { useQuery } from "@tanstack/react-query"; +import { + v1ListProviderEndpointsOptions, +} from "@/api/generated/@tanstack/react-query.gen"; + +export function useProviders() { + return useQuery({ + ...v1ListProviderEndpointsOptions(), + }); +} diff --git a/src/features/providers/lib/utils.ts b/src/features/providers/lib/utils.ts new file mode 100644 index 00000000..80a2e7df --- /dev/null +++ b/src/features/providers/lib/utils.ts @@ -0,0 +1,29 @@ +import { ProviderAuthType, ProviderType } from "@/api/generated"; + +export const PROVIDER_AUTH_TYPE_MAP = { + [ProviderAuthType.NONE]: "None", + [ProviderAuthType.PASSTHROUGH]: "Passthrough", + [ProviderAuthType.API_KEY]: "API Key", +}; + +export function getAuthTypeOptions() { + return Object.entries(PROVIDER_AUTH_TYPE_MAP).map(([id, textValue]) => ({ + id, + textValue, + })); +} + +export function getProviderType() { + return Object.values(ProviderType).map((textValue) => ({ + id: textValue, + textValue, + })); +} + +export function isProviderType(value: unknown): value is ProviderType { + return Object.values(ProviderType).includes(value as ProviderType); +} + +export function isProviderAuthType(value: unknown): value is ProviderAuthType { + return Object.values(ProviderAuthType).includes(value as ProviderAuthType); +} diff --git a/src/hooks/use-models-data.ts b/src/hooks/use-models-data.ts index b9fc280b..c423147c 100644 --- a/src/hooks/use-models-data.ts +++ b/src/hooks/use-models-data.ts @@ -4,5 +4,8 @@ import { v1ListAllModelsForAllProvidersOptions } from "@/api/generated/@tanstack export const useModelsData = () => { return useQuery({ ...v1ListAllModelsForAllProvidersOptions(), + refetchOnMount: true, + refetchOnReconnect: true, + refetchOnWindowFocus: true, }); }; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 07a499a6..976388a4 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -73,7 +73,7 @@ export function groupPromptsByRelativeDate(prompts: Conversation[]) { const promptsSorted = prompts.sort( (a, b) => new Date(b.conversation_timestamp).getTime() - - new Date(a.conversation_timestamp).getTime(), + new Date(a.conversation_timestamp).getTime() ); const grouped = promptsSorted.reduce( @@ -90,7 +90,7 @@ export function groupPromptsByRelativeDate(prompts: Conversation[]) { (groups[group] ?? []).push(prompt); return groups; }, - {} as Record, + {} as Record ); return grouped; @@ -125,10 +125,15 @@ export function sanitizeQuestionPrompt({ } export function getIssueDetectedType( - alert: AlertConversation, + alert: AlertConversation ): "malicious_package" | "leaked_secret" | null { if (isAlertMalicious(alert)) return "malicious_package"; if (isAlertSecret(alert)) return "leaked_secret"; return null; } + +export function capitalize(text: string) { + const [first, ...rest] = text; + return first ? first.toUpperCase() + rest.join("") : text; +} diff --git a/src/routes/route-provider-create.tsx b/src/routes/route-provider-create.tsx new file mode 100644 index 00000000..62cbbaac --- /dev/null +++ b/src/routes/route-provider-create.tsx @@ -0,0 +1,41 @@ +import { AddProviderEndpointRequest, ProviderType } from "@/api/generated"; +import { ProviderDialog } from "@/features/providers/components/provider-dialog"; +import { ProviderDialogFooter } from "@/features/providers/components/provider-dialog-footer"; +import { ProviderForm } from "@/features/providers/components/provider-form"; +import { useMutationCreateProvider } from "@/features/providers/hooks/use-mutation-create-provider"; +import { DialogContent, Form } from "@stacklok/ui-kit"; +import { useState } from "react"; + +const DEFAULT_PROVIDER_STATE = { + name: "", + description: "", + auth_type: null, + provider_type: ProviderType.OPENAI, + endpoint: "", + api_key: "", +}; + +export function RouteProviderCreate() { + const [provider, setProvider] = useState( + DEFAULT_PROVIDER_STATE + ); + const { mutateAsync } = useMutationCreateProvider(); + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + mutateAsync({ + body: provider, + }); + }; + + return ( + +
+ + + + + +
+ ); +} diff --git a/src/routes/route-provider-update.tsx b/src/routes/route-provider-update.tsx new file mode 100644 index 00000000..c3210d86 --- /dev/null +++ b/src/routes/route-provider-update.tsx @@ -0,0 +1,47 @@ +import { ProviderAuthType } from "@/api/generated"; +import { ProviderDialog } from "@/features/providers/components/provider-dialog"; +import { ProviderDialogFooter } from "@/features/providers/components/provider-dialog-footer"; +import { ProviderForm } from "@/features/providers/components/provider-form"; +import { useMutationUpdateProvider } from "@/features/providers/hooks/use-mutation-update-provider"; +import { useProvider } from "@/features/providers/hooks/use-provider"; +import { DialogContent, Form } from "@stacklok/ui-kit"; +import { useParams } from "react-router-dom"; + +export function RouteProviderUpdate() { + const { id } = useParams(); + if (id === undefined) { + throw new Error("Provider id is required"); + } + const { setProvider, provider } = useProvider(id); + const { mutateAsync } = useMutationUpdateProvider(); + + const handleSubmit = (event: React.FormEvent) => { + console.log(provider); + event.preventDefault(); + // await mutationProvider.mutateAsync({ + // path: { provider_id: id }, + // body: provider, + // }); + // await mutationAuthMaterial.mutateAsync({ + // path: { provider_id: id }, + // body: { + // auth_type: provider.auth_type as ProviderAuthType, + // api_key: provider.api_key, + // }, + // }); + }; + + // TODO add empty state and loading in a next step + if (provider === undefined) return; + + return ( + +
+ + + + + +
+ ); +} diff --git a/src/routes/route-providers.tsx b/src/routes/route-providers.tsx new file mode 100644 index 00000000..d0bc1c8e --- /dev/null +++ b/src/routes/route-providers.tsx @@ -0,0 +1,44 @@ +import { BreadcrumbHome } from "@/components/BreadcrumbHome"; +import { + Breadcrumbs, + Breadcrumb, + Heading, + Card, + LinkButton, + CardBody, + CardFooter, +} from "@stacklok/ui-kit"; +import { twMerge } from "tailwind-merge"; +import { Plus } from "@untitled-ui/icons-react"; +import { TableProviders } from "@/features/providers/components/table-providers"; +import { Outlet } from "react-router-dom"; + +export function RouteProvider({ className }: { className?: string }) { + return ( + <> + + + Providers + + + Providers + + + + + + + + Add Provider + + + + + + + ); +} From 48feb98c650bcb0f7bd38f38476f6a7ea7ec9d37 Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Tue, 4 Feb 2025 17:22:17 +0100 Subject: [PATCH 06/17] feat: update provider and auth --- .../hooks/use-mutation-update-provider.ts | 63 +++++++------------ src/hooks/use-toast-mutation.ts | 23 ++++++- src/routes/route-provider-update.tsx | 14 +---- 3 files changed, 44 insertions(+), 56 deletions(-) diff --git a/src/features/providers/hooks/use-mutation-update-provider.ts b/src/features/providers/hooks/use-mutation-update-provider.ts index 54908ac2..c879c935 100644 --- a/src/features/providers/hooks/use-mutation-update-provider.ts +++ b/src/features/providers/hooks/use-mutation-update-provider.ts @@ -1,55 +1,38 @@ -import { - v1ConfigureAuthMaterialMutation, - v1UpdateProviderEndpointMutation, -} from "@/api/generated/@tanstack/react-query.gen"; import { useToastMutation } from "@/hooks/use-toast-mutation"; import { useNavigate } from "react-router-dom"; import { useInvalidateProvidersQueries } from "./use-invalidate-providers-queries"; - -// export function useMutationUpdateProvider() { -// return useToastMutation({ -// mutationFn: async (provider) => { -// console.log(provider); -// v1ConfigureAuthMaterialMutation(); -// v1UpdateProviderEndpointMutation(); - -// return; -// }, -// successMsg: () => { -// return "cioenn"; -// }, -// }); -// } - -// const mutationProvider = useMutation({ -// ...v1UpdateProviderEndpointMutation(), -// }); - -// if (mutationAuthMaterial.isSuccess && mutationProvider.isSuccess) { -// toast.success("sdkasdjk"); -// } - -// if (mutationAuthMaterial.isError && mutationProvider.isError) { -// toast.error("sdkasdjk"); -// } - -// return { mutationAuthMaterial, mutationProvider }; +import { + AddProviderEndpointRequest, + ProviderAuthType, + v1ConfigureAuthMaterial, + v1UpdateProviderEndpoint, +} from "@/api/generated"; export function useMutationUpdateProvider() { const navigate = useNavigate(); const invalidate = useInvalidateProvidersQueries(); - const mutationFn = async () => { - const configureAuthMaterialMutation = v1ConfigureAuthMaterialMutation(); - const updateProviderEndpointMutation = v1UpdateProviderEndpointMutation(); - - return { configureAuthMaterialMutation, updateProviderEndpointMutation }; + const mutationFn = ({ api_key, ...rest }: AddProviderEndpointRequest) => { + return Promise.all([ + v1ConfigureAuthMaterial({ + path: { provider_id: rest.id as string }, + body: { + api_key: api_key, + auth_type: rest.auth_type as ProviderAuthType, + }, + throwOnError: true, + }), + + v1UpdateProviderEndpoint({ + path: { provider_id: rest.id as string }, + body: rest, + }), + ]); }; return useToastMutation({ mutationFn, - successMsg: "Success", - errorMsg: "error", + successMsg: "Successfully update provider", onSuccess: async () => { await invalidate(); navigate("/providers"); diff --git a/src/hooks/use-toast-mutation.ts b/src/hooks/use-toast-mutation.ts index 29b8686a..e9b935ce 100644 --- a/src/hooks/use-toast-mutation.ts +++ b/src/hooks/use-toast-mutation.ts @@ -1,3 +1,4 @@ +import { HTTPValidationError } from "@/api/generated"; import { toast } from "@stacklok/ui-kit"; import { DefaultError, @@ -31,7 +32,7 @@ export function useToastMutation< } = useMutation(options); const mutateAsync = useCallback( - async ( + async ( variables: Parameters[0], options: Parameters[1] = {}, ) => { @@ -41,8 +42,24 @@ export function useToastMutation< success: typeof successMsg === "function" ? successMsg(variables) : successMsg, loading: loadingMsg ?? "Loading...", - error: (e: TError) => - errorMsg ?? (e.detail ? e.detail : "An error occurred"), + error: (e: TError) => { + if (errorMsg) return errorMsg; + + if (typeof e.detail == "string") { + return e.detail ?? "An error occurred"; + } + + if (Array.isArray(e.detail)) { + const err = e.detail + ?.map((item) => `${item.msg} - ${JSON.stringify(item.loc)}`) + .filter(Boolean) + .join(", "); + + return err ?? "An error occurred"; + } + + return "An error occurred"; + }, }); }, [errorMsg, loadingMsg, originalMutateAsync, successMsg], diff --git a/src/routes/route-provider-update.tsx b/src/routes/route-provider-update.tsx index c3210d86..209ac483 100644 --- a/src/routes/route-provider-update.tsx +++ b/src/routes/route-provider-update.tsx @@ -1,4 +1,3 @@ -import { ProviderAuthType } from "@/api/generated"; import { ProviderDialog } from "@/features/providers/components/provider-dialog"; import { ProviderDialogFooter } from "@/features/providers/components/provider-dialog-footer"; import { ProviderForm } from "@/features/providers/components/provider-form"; @@ -16,19 +15,8 @@ export function RouteProviderUpdate() { const { mutateAsync } = useMutationUpdateProvider(); const handleSubmit = (event: React.FormEvent) => { - console.log(provider); event.preventDefault(); - // await mutationProvider.mutateAsync({ - // path: { provider_id: id }, - // body: provider, - // }); - // await mutationAuthMaterial.mutateAsync({ - // path: { provider_id: id }, - // body: { - // auth_type: provider.auth_type as ProviderAuthType, - // api_key: provider.api_key, - // }, - // }); + mutateAsync(provider); }; // TODO add empty state and loading in a next step From 25186f76e792caf2981d90f1df69b1bb8c4ad080 Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Wed, 5 Feb 2025 10:32:50 +0100 Subject: [PATCH 07/17] refactor: move add provider button on top --- .../providers/components/provider-form.tsx | 6 +++++- src/routes/route-providers.tsx | 15 ++++----------- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/features/providers/components/provider-form.tsx b/src/features/providers/components/provider-form.tsx index 7ab9a8b2..0614af40 100644 --- a/src/features/providers/components/provider-form.tsx +++ b/src/features/providers/components/provider-form.tsx @@ -113,7 +113,11 @@ export function ProviderForm({ provider, setProvider }: Props) { > diff --git a/src/routes/route-providers.tsx b/src/routes/route-providers.tsx index d0bc1c8e..7ea1aeea 100644 --- a/src/routes/route-providers.tsx +++ b/src/routes/route-providers.tsx @@ -6,10 +6,9 @@ import { Card, LinkButton, CardBody, - CardFooter, } from "@stacklok/ui-kit"; import { twMerge } from "tailwind-merge"; -import { Plus } from "@untitled-ui/icons-react"; +import { PlusSquare } from "@untitled-ui/icons-react"; import { TableProviders } from "@/features/providers/components/table-providers"; import { Outlet } from "react-router-dom"; @@ -22,20 +21,14 @@ export function RouteProvider({ className }: { className?: string }) { Providers + + Add Provider + - - - Add Provider - - From c350954d4e05527eb0b15793487b3a329e4ffd63 Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Wed, 5 Feb 2025 11:32:23 +0100 Subject: [PATCH 08/17] fix: msw handler --- .../msw/fixtures/GET_PROVIDERS_MODELS.json | 27 +++++++++++++++++ src/mocks/msw/handlers.ts | 29 ++++++------------- 2 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 src/mocks/msw/fixtures/GET_PROVIDERS_MODELS.json diff --git a/src/mocks/msw/fixtures/GET_PROVIDERS_MODELS.json b/src/mocks/msw/fixtures/GET_PROVIDERS_MODELS.json new file mode 100644 index 00000000..1a6ff4dd --- /dev/null +++ b/src/mocks/msw/fixtures/GET_PROVIDERS_MODELS.json @@ -0,0 +1,27 @@ +[ + { + "name": "claude-3.5", + "provider_id": "id_1", + "provider_name": "anthropic" + }, + { + "name": "claude-3.6", + "provider_id": "id_2", + "provider_name": "anthropic" + }, + { + "name": "claude-3.7", + "provider_id": "id_3", + "provider_name": "anthropic" + }, + { + "name": "chatgpt-4o", + "provider_id": "id_4", + "provider_name": "openai" + }, + { + "name": "chatgpt-4p", + "provider_id": "id_5", + "provider_name": "openai" + } +] \ No newline at end of file diff --git a/src/mocks/msw/handlers.ts b/src/mocks/msw/handlers.ts index be2bd963..2a4974cb 100644 --- a/src/mocks/msw/handlers.ts +++ b/src/mocks/msw/handlers.ts @@ -3,6 +3,7 @@ import mockedPrompts from "@/mocks/msw/fixtures/GET_MESSAGES.json"; import mockedAlerts from "@/mocks/msw/fixtures/GET_ALERTS.json"; import mockedWorkspaces from "@/mocks/msw/fixtures/GET_WORKSPACES.json"; import mockedProviders from "@/mocks/msw/fixtures/GET_PROVIDERS.json"; +import mockedProvidersModels from "@/mocks/msw/fixtures/GET_PROVIDERS_MODELS.json"; import { ProviderType } from "@/api/generated"; export const handlers = [ @@ -112,12 +113,18 @@ export const handlers = [ "*/api/v1/workspaces/:workspace_name/muxes", () => new HttpResponse(null, { status: 204 }), ), - http.get("*/api/v1/provider-endpoints", () => - HttpResponse.json(mockedProviders), + http.get("*/api/v1/provider-endpoints/:provider_name/models", () => + HttpResponse.json(mockedProvidersModels), + ), + http.get("*/api/v1/provider-endpoints/models", () => + HttpResponse.json(mockedProvidersModels), ), http.get("*/api/v1/provider-endpoints/:provider_id", () => HttpResponse.json(mockedProviders[0]), ), + http.get("*/api/v1/provider-endpoints", () => + HttpResponse.json(mockedProviders), + ), http.post( "*/api/v1/provider-endpoints", () => new HttpResponse(null, { status: 204 }), @@ -130,22 +137,4 @@ export const handlers = [ "*/api/v1/provider-endpoints", () => new HttpResponse(null, { status: 204 }), ), - http.get("*/api/v1/provider-endpoints/:provider_name/models", () => - HttpResponse.json([ - { name: "claude-3.5", provider: "anthropic" }, - { name: "claude-3.6", provider: "anthropic" }, - { name: "claude-3.7", provider: "anthropic" }, - { name: "chatgpt-4o", provider: "openai" }, - { name: "chatgpt-4p", provider: "openai" }, - ]), - ), - http.get("*/api/v1/provider-endpoints/models", () => - HttpResponse.json([ - { name: "claude-3.5", provider: "anthropic" }, - { name: "claude-3.6", provider: "anthropic" }, - { name: "claude-3.7", provider: "anthropic" }, - { name: "chatgpt-4o", provider: "openai" }, - { name: "chatgpt-4p", provider: "openai" }, - ]), - ), ]; From 5ba311a5625ec461ccd896b05b66073da754064e Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Wed, 5 Feb 2025 11:47:15 +0100 Subject: [PATCH 09/17] feat: add providers menu item --- src/features/header/components/header.tsx | 4 +- .../constants/certificate-menu-items.tsx | 17 -------- .../header/constants/settings-menu-items.tsx | 39 +++++++++++++++++++ 3 files changed, 41 insertions(+), 19 deletions(-) delete mode 100644 src/features/header/constants/certificate-menu-items.tsx create mode 100644 src/features/header/constants/settings-menu-items.tsx diff --git a/src/features/header/components/header.tsx b/src/features/header/components/header.tsx index ce7f7c2e..19eaf836 100644 --- a/src/features/header/components/header.tsx +++ b/src/features/header/components/header.tsx @@ -3,9 +3,9 @@ import { SidebarTrigger } from "../../../components/ui/sidebar"; import { DropdownMenu } from "../../../components/HoverPopover"; import { Separator, ButtonDarkMode } from "@stacklok/ui-kit"; import { WorkspacesSelection } from "@/features/workspace/components/workspaces-selection"; -import { CERTIFICATE_MENU_ITEMS } from "../constants/certificate-menu-items"; import { HELP_MENU_ITEMS } from "../constants/help-menu-items"; import { HeaderStatusMenu } from "./header-status-menu"; +import { SETTINGS_MENU_ITEMS } from "../constants/settings-menu-items"; function HomeLink() { return ( @@ -39,8 +39,8 @@ export function Header({ hasError }: { hasError?: boolean }) {
- +
diff --git a/src/features/header/constants/certificate-menu-items.tsx b/src/features/header/constants/certificate-menu-items.tsx deleted file mode 100644 index 64ce0b51..00000000 --- a/src/features/header/constants/certificate-menu-items.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { OptionsSchema } from "@stacklok/ui-kit"; -import { Download01, ShieldTick } from "@untitled-ui/icons-react"; - -export const CERTIFICATE_MENU_ITEMS = [ - { - icon: , - id: "about-certificate-security", - href: "/certificates/security", - textValue: "About certificate security", - }, - { - icon: , - id: "download-certificates", - href: "/certificates", - textValue: "Download certificates", - }, -] as const satisfies OptionsSchema<"menu">[]; diff --git a/src/features/header/constants/settings-menu-items.tsx b/src/features/header/constants/settings-menu-items.tsx new file mode 100644 index 00000000..f85a70f2 --- /dev/null +++ b/src/features/header/constants/settings-menu-items.tsx @@ -0,0 +1,39 @@ +import { OptionsSchema } from "@stacklok/ui-kit"; +import { + Download01, + LayersThree01, + ShieldTick, +} from "@untitled-ui/icons-react"; + +export const SETTINGS_MENU_ITEMS = [ + { + textValue: "Providers", + id: "providers", + items: [ + { + icon: , + id: "providers", + href: "/providers", + textValue: "Providers", + }, + ], + }, + { + textValue: "Certificates", + id: "certificates", + items: [ + { + icon: , + id: "about-certificate-security", + href: "/certificates/security", + textValue: "About certificate security", + }, + { + icon: , + id: "download-certificates", + href: "/certificates", + textValue: "Download certificates", + }, + ], + }, +] as const satisfies OptionsSchema<"menu">[]; From eb12cdfc9d97fe6cfa873df109230589bdb3600a Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Wed, 5 Feb 2025 11:49:18 +0100 Subject: [PATCH 10/17] test: add settings menu and providers items --- src/App.test.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/App.test.tsx b/src/App.test.tsx index 4a6affe3..4f1c8079 100644 --- a/src/App.test.tsx +++ b/src/App.test.tsx @@ -8,13 +8,17 @@ describe("App", () => { it("should render header", async () => { render(); expect(screen.getByText(/toggle sidebar/i)).toBeVisible(); - expect(screen.getByText("Certificates")).toBeVisible(); + expect(screen.getByText("Settings")).toBeVisible(); expect(screen.getByText("Help")).toBeVisible(); expect(screen.getByRole("banner", { name: "App header" })).toBeVisible(); expect(screen.getByRole("heading", { name: /codeGate/i })).toBeVisible(); - await userEvent.click(screen.getByText("Certificates")); - + await userEvent.click(screen.getByText("Settings")); + expect( + screen.getByRole("menuitem", { + name: /providers/i, + }), + ).toBeVisible(); expect( screen.getByRole("menuitem", { name: /certificate security/i, @@ -26,7 +30,7 @@ describe("App", () => { }), ).toBeVisible(); - await userEvent.click(screen.getByText("Certificates")); + await userEvent.click(screen.getByText("Settings")); await userEvent.click(screen.getByText("Help")); expect( From a4642e89c6db97b586a3ab640325ea2a279d97f4 Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Wed, 5 Feb 2025 12:00:28 +0100 Subject: [PATCH 11/17] leftover --- src/features/providers/hooks/use-provider.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/features/providers/hooks/use-provider.ts b/src/features/providers/hooks/use-provider.ts index 1b685972..87e5044c 100644 --- a/src/features/providers/hooks/use-provider.ts +++ b/src/features/providers/hooks/use-provider.ts @@ -10,7 +10,7 @@ export function useProvider(providerId: string) { auth_type: null, provider_type: ProviderType.OPENAI, endpoint: "", - api_key: "" + api_key: "", }); const { data, isPending, isError } = useQuery({ @@ -23,6 +23,5 @@ export function useProvider(providerId: string) { } }, [data]); - console.log({ provider }); return { isPending, isError, provider, setProvider }; } From 450e49411de997f1d9599c9f0441b29fabfcbc4b Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Wed, 5 Feb 2025 12:01:47 +0100 Subject: [PATCH 12/17] feat: add query config on refetch for providers list endpoint --- src/features/providers/hooks/use-providers.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/features/providers/hooks/use-providers.ts b/src/features/providers/hooks/use-providers.ts index e652714a..cf112f09 100644 --- a/src/features/providers/hooks/use-providers.ts +++ b/src/features/providers/hooks/use-providers.ts @@ -1,10 +1,11 @@ import { useQuery } from "@tanstack/react-query"; -import { - v1ListProviderEndpointsOptions, -} from "@/api/generated/@tanstack/react-query.gen"; +import { v1ListProviderEndpointsOptions } from "@/api/generated/@tanstack/react-query.gen"; export function useProviders() { return useQuery({ ...v1ListProviderEndpointsOptions(), + refetchOnMount: true, + refetchOnReconnect: true, + refetchOnWindowFocus: true, }); } From 7a808a635d01a57cf8330ae38e4b4ca438391412 Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Wed, 5 Feb 2025 12:03:30 +0100 Subject: [PATCH 13/17] fix: provider id type --- .../providers/hooks/use-mutation-update-provider.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/features/providers/hooks/use-mutation-update-provider.ts b/src/features/providers/hooks/use-mutation-update-provider.ts index c879c935..b8782e67 100644 --- a/src/features/providers/hooks/use-mutation-update-provider.ts +++ b/src/features/providers/hooks/use-mutation-update-provider.ts @@ -13,9 +13,11 @@ export function useMutationUpdateProvider() { const invalidate = useInvalidateProvidersQueries(); const mutationFn = ({ api_key, ...rest }: AddProviderEndpointRequest) => { + const provider_id = rest.id; + if (!provider_id) throw new Error("Provider is missing"); return Promise.all([ v1ConfigureAuthMaterial({ - path: { provider_id: rest.id as string }, + path: { provider_id }, body: { api_key: api_key, auth_type: rest.auth_type as ProviderAuthType, @@ -24,7 +26,7 @@ export function useMutationUpdateProvider() { }), v1UpdateProviderEndpoint({ - path: { provider_id: rest.id as string }, + path: { provider_id }, body: rest, }), ]); From 329d6e07d6db3d7a6848dbb3d371fcead32876ab Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Wed, 5 Feb 2025 12:07:57 +0100 Subject: [PATCH 14/17] fix: spelling --- src/features/providers/hooks/use-mutation-delete-provider.ts | 2 +- src/features/providers/hooks/use-mutation-update-provider.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/features/providers/hooks/use-mutation-delete-provider.ts b/src/features/providers/hooks/use-mutation-delete-provider.ts index e647db5a..6dacf200 100644 --- a/src/features/providers/hooks/use-mutation-delete-provider.ts +++ b/src/features/providers/hooks/use-mutation-delete-provider.ts @@ -8,6 +8,6 @@ export const useMutationDeleteProvider = () => { return useToastMutation({ ...v1DeleteProviderEndpointMutation(), onSuccess: () => invalidate(), - successMsg: () => "Successfully delete provider", + successMsg: () => "Successfully deleted provider", }); }; diff --git a/src/features/providers/hooks/use-mutation-update-provider.ts b/src/features/providers/hooks/use-mutation-update-provider.ts index b8782e67..b8c987bf 100644 --- a/src/features/providers/hooks/use-mutation-update-provider.ts +++ b/src/features/providers/hooks/use-mutation-update-provider.ts @@ -34,7 +34,7 @@ export function useMutationUpdateProvider() { return useToastMutation({ mutationFn, - successMsg: "Successfully update provider", + successMsg: "Successfully updated provider", onSuccess: async () => { await invalidate(); navigate("/providers"); From e97cdc78fcc9c84ee30ed6b41bb447370cdf3cec Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Wed, 5 Feb 2025 12:47:10 +0100 Subject: [PATCH 15/17] refadctor: providers table --- .../providers/components/table-providers.tsx | 146 +++++++++++------- 1 file changed, 91 insertions(+), 55 deletions(-) diff --git a/src/features/providers/components/table-providers.tsx b/src/features/providers/components/table-providers.tsx index 3a140536..197f5c37 100644 --- a/src/features/providers/components/table-providers.tsx +++ b/src/features/providers/components/table-providers.tsx @@ -7,72 +7,108 @@ import { TableBody, LinkButton, TableHeader, + ResizableTableContainer, } from "@stacklok/ui-kit"; import { Globe02, Tool01 } from "@untitled-ui/icons-react"; import { PROVIDER_AUTH_TYPE_MAP } from "../lib/utils"; import { TableActions } from "./table-actions"; import { useProviders } from "../hooks/use-providers"; +import { match } from "ts-pattern"; +import { ComponentProps } from "react"; +import { ProviderEndpoint } from "@/api/generated"; + +const COLUMN_MAP = { + provider: "provider", + type: "type", + endpoint: "endpoint", + auth: "auth", + configuration: "configuration", +} as const; + +type ColumnId = keyof typeof COLUMN_MAP; +type Column = { id: ColumnId } & Omit, "id">; +const COLUMNS: Column[] = [ + { + id: "provider", + isRowHeader: true, + children: "Name & Description", + width: "40%", + }, + { id: "type", children: "Provider", width: "10%", className: "capitalize" }, + { id: "endpoint", children: "Endpoint", width: "20%" }, + { id: "auth", children: "Authentication", width: "20%" }, + { id: "configuration", alignment: "end", width: "10%", children: "" }, +]; + +function CellRenderer({ + column, + row, +}: { + column: Column; + row: ProviderEndpoint; +}) { + return match(column.id) + .with(COLUMN_MAP.provider, () => ( + <> +
{row.name}
+
{row.description}
+ + )) + .with(COLUMN_MAP.type, () => row.provider_type) + .with(COLUMN_MAP.endpoint, () => ( +
+ + {row.endpoint} +
+ )) + .with(COLUMN_MAP.auth, () => ( +
+ {row.auth_type ? ( + + {PROVIDER_AUTH_TYPE_MAP[row.auth_type]} + + ) : ( + "N/A" + )} + + Manage + +
+ )) + .with(COLUMN_MAP.configuration, () => ) + .exhaustive(); +} export function TableProviders() { const { data: providers = [] } = useProviders(); return ( - - - - - name & description - - - provider - - - endpoint - - - authentication - - + +
+ + {(column) => } - - - {providers.map((provider) => ( - - -
{provider.name}
-
{provider.description}
-
- {provider.provider_type} - -
- - {provider.endpoint} -
-
- -
- {provider.auth_type ? ( - - {PROVIDER_AUTH_TYPE_MAP[provider.auth_type]} - - ) : ( - "N/A" - )} - + {(row) => ( + + {(column) => ( + - Manage - -
-
- - - -
- ))} -
-
+ + + )} + + )} + + + ); } From 85e769be586b43c83a206386658017e75b5f6d24 Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Wed, 5 Feb 2025 12:52:28 +0100 Subject: [PATCH 16/17] fix: dialog content overflow --- src/routes/route-provider-create.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/routes/route-provider-create.tsx b/src/routes/route-provider-create.tsx index 62cbbaac..197e5401 100644 --- a/src/routes/route-provider-create.tsx +++ b/src/routes/route-provider-create.tsx @@ -17,7 +17,7 @@ const DEFAULT_PROVIDER_STATE = { export function RouteProviderCreate() { const [provider, setProvider] = useState( - DEFAULT_PROVIDER_STATE + DEFAULT_PROVIDER_STATE, ); const { mutateAsync } = useMutationCreateProvider(); @@ -29,13 +29,13 @@ export function RouteProviderCreate() { }; return ( - -
+ + - -
+
+ ); } From 76b92e99ad1a3693c24ceae345521ee2501f90ca Mon Sep 17 00:00:00 2001 From: Giuseppe Scuglia Date: Wed, 5 Feb 2025 13:02:40 +0100 Subject: [PATCH 17/17] refactor: remove modal state --- src/features/providers/components/provider-dialog.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/features/providers/components/provider-dialog.tsx b/src/features/providers/components/provider-dialog.tsx index 5fd64df9..44ae7de9 100644 --- a/src/features/providers/components/provider-dialog.tsx +++ b/src/features/providers/components/provider-dialog.tsx @@ -7,7 +7,6 @@ import { DialogCloseButton, } from "@stacklok/ui-kit"; import { useNavigate } from "react-router-dom"; -import { useState } from "react"; export function ProviderDialog({ title, @@ -16,15 +15,13 @@ export function ProviderDialog({ title: string; children: React.ReactNode; }) { - const [isModalOpen, setIsModalOpen] = useState(true); const navigate = useNavigate(); return ( { - setIsModalOpen(isOpen); + isOpen + onOpenChange={() => { navigate("/providers"); }} > @@ -32,7 +29,7 @@ export function ProviderDialog({ {title} - + {children}