diff --git a/site/package.json b/site/package.json index 56b7969b80a58..c881560a360a0 100644 --- a/site/package.json +++ b/site/package.json @@ -58,7 +58,7 @@ "@novnc/novnc": "^1.5.0", "@pierre/diffs": "1.2.7", "@pierre/trees": "1.0.0-beta.4", - "@tanstack/react-query-devtools": "5.77.0", + "@tanstack/react-query-devtools": "5.82.0", "@xterm/addon-canvas": "0.7.0", "@xterm/addon-fit": "0.11.0", "@xterm/addon-unicode11": "0.9.0", @@ -96,7 +96,7 @@ "react-dom": "19.2.8", "react-infinite-scroll-component": "7.1.0", "react-markdown": "9.1.0", - "react-query": "npm:@tanstack/react-query@5.77.0", + "react-query": "npm:@tanstack/react-query@5.82.0", "react-resizable-panels": "3.0.6", "react-router": "7.18.0", "react-syntax-highlighter": "15.6.6", diff --git a/site/pnpm-lock.yaml b/site/pnpm-lock.yaml index b3c6980da951a..9665f4e7b2e03 100644 --- a/site/pnpm-lock.yaml +++ b/site/pnpm-lock.yaml @@ -96,8 +96,8 @@ importers: specifier: 1.0.0-beta.4 version: 1.0.0-beta.4(react-dom@19.2.8(react@19.2.8))(react@19.2.8) '@tanstack/react-query-devtools': - specifier: 5.77.0 - version: 5.77.0(@tanstack/react-query@5.77.0(react@19.2.8))(react@19.2.8) + specifier: 5.82.0 + version: 5.82.0(@tanstack/react-query@5.82.0(react@19.2.8))(react@19.2.8) '@xterm/addon-canvas': specifier: 0.7.0 version: 0.7.0(@xterm/xterm@5.5.0) @@ -210,8 +210,8 @@ importers: specifier: 9.1.0 version: 9.1.0(@types/react@19.2.17)(react@19.2.8) react-query: - specifier: npm:@tanstack/react-query@5.77.0 - version: '@tanstack/react-query@5.77.0(react@19.2.8)' + specifier: npm:@tanstack/react-query@5.82.0 + version: '@tanstack/react-query@5.82.0(react@19.2.8)' react-resizable-panels: specifier: 3.0.6 version: 3.0.6(react-dom@19.2.8(react@19.2.8))(react@19.2.8) @@ -2575,20 +2575,20 @@ packages: peerDependencies: tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' - '@tanstack/query-core@5.77.0': - resolution: {integrity: sha512-PFeWjgMQjOsnxBwnW/TJoO0pCja2dzuMQoZ3Diho7dPz7FnTUwTrjNmdf08evrhSE5nvPIKeqV6R0fvQfmhGeg==, tarball: https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.77.0.tgz} + '@tanstack/query-core@5.82.0': + resolution: {integrity: sha512-JrjoVuaajBQtnoWSg8iaPHaT4mW73lK2t+exxHNOSMqy0+13eKLqJgTKXKImLejQIfdAHQ6Un0njEhOvUtOd5w==, tarball: https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.82.0.tgz} - '@tanstack/query-devtools@5.76.0': - resolution: {integrity: sha512-1p92nqOBPYVqVDU0Ua5nzHenC6EGZNrLnB2OZphYw8CNA1exuvI97FVgIKON7Uug3uQqvH/QY8suUKpQo8qHNQ==, tarball: https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.76.0.tgz} + '@tanstack/query-devtools@5.81.2': + resolution: {integrity: sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg==, tarball: https://registry.npmjs.org/@tanstack/query-devtools/-/query-devtools-5.81.2.tgz} - '@tanstack/react-query-devtools@5.77.0': - resolution: {integrity: sha512-Dwvs+ksXiK1tW4YnTtHwYPO5+d8IUk1l8QQJ4aGEIqKz6uTLu/67NIo7EnUF0G/Edv+UOn9P1V3tYWuVfvhbmg==, tarball: https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.77.0.tgz} + '@tanstack/react-query-devtools@5.82.0': + resolution: {integrity: sha512-MC05Zq3zr/59jhgF7dL6JSGPg1krbasDSizmRxjNcvxgh/sUTwRFD9CGN10YYX7LB6jq0ZpFtCjSVGdLiFrKAA==, tarball: https://registry.npmjs.org/@tanstack/react-query-devtools/-/react-query-devtools-5.82.0.tgz} peerDependencies: - '@tanstack/react-query': ^5.77.0 + '@tanstack/react-query': ^5.82.0 react: ^18 || ^19 - '@tanstack/react-query@5.77.0': - resolution: {integrity: sha512-jX52ot8WxWzWnAknpRSEWj6PTR/7nkULOfoiaVPk6nKu0otwt30UMBC9PTg/m1x0uhz1g71/imwjViTm/oYHxA==, tarball: https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.77.0.tgz} + '@tanstack/react-query@5.82.0': + resolution: {integrity: sha512-mnk8/ofKEthFeMdhV1dV8YXRf+9HqvXAcciXkoo755d/ocfWq7N/Y9jGOzS3h7ZW9dDGwSIhs3/HANWUBsyqYg==, tarball: https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.82.0.tgz} peerDependencies: react: ^18 || ^19 @@ -8681,19 +8681,19 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.19(tsx@4.22.4)(yaml@2.8.3) - '@tanstack/query-core@5.77.0': {} + '@tanstack/query-core@5.82.0': {} - '@tanstack/query-devtools@5.76.0': {} + '@tanstack/query-devtools@5.81.2': {} - '@tanstack/react-query-devtools@5.77.0(@tanstack/react-query@5.77.0(react@19.2.8))(react@19.2.8)': + '@tanstack/react-query-devtools@5.82.0(@tanstack/react-query@5.82.0(react@19.2.8))(react@19.2.8)': dependencies: - '@tanstack/query-devtools': 5.76.0 - '@tanstack/react-query': 5.77.0(react@19.2.8) + '@tanstack/query-devtools': 5.81.2 + '@tanstack/react-query': 5.82.0(react@19.2.8) react: 19.2.8 - '@tanstack/react-query@5.77.0(react@19.2.8)': + '@tanstack/react-query@5.82.0(react@19.2.8)': dependencies: - '@tanstack/query-core': 5.77.0 + '@tanstack/query-core': 5.82.0 react: 19.2.8 '@testing-library/dom@10.4.0': diff --git a/site/src/api/queries/chats.ts b/site/src/api/queries/chats.ts index b9b02b1f19124..34d2e7bf0b5de 100644 --- a/site/src/api/queries/chats.ts +++ b/site/src/api/queries/chats.ts @@ -1799,25 +1799,6 @@ export const updateChatAutoArchiveDays = (queryClient: QueryClient) => ({ }, }); -const chatTemplateAllowlistKey = [ - ...chatConfigKey, - "template-allowlist", -] as const; - -export const chatTemplateAllowlist = () => ({ - queryKey: chatTemplateAllowlistKey, - queryFn: () => API.experimental.getChatTemplateAllowlist(), -}); - -export const updateChatTemplateAllowlist = (queryClient: QueryClient) => ({ - mutationFn: API.experimental.updateChatTemplateAllowlist, - onSuccess: async () => { - await queryClient.invalidateQueries({ - queryKey: chatTemplateAllowlistKey, - }); - }, -}); - const chatUserCustomPromptKey = [...chatConfigKey, "prompt", "me"] as const; export const chatUserCustomPrompt = () => ({ diff --git a/site/src/api/queries/templates.ts b/site/src/api/queries/templates.ts index 2a01eff1a4589..f242b8b85afaa 100644 --- a/site/src/api/queries/templates.ts +++ b/site/src/api/queries/templates.ts @@ -1,4 +1,9 @@ -import type { MutationOptions, QueryClient, QueryOptions } from "react-query"; +import { + type MutationOptions, + mutationOptions, + type QueryClient, + type QueryOptions, +} from "react-query"; import { API, type GetTemplatesOptions, @@ -12,12 +17,14 @@ import type { Template, TemplateRole, TemplateVersion, + UpdateTemplateMeta, UsersRequest, } from "#/api/typesGenerated"; import { delay } from "#/utils/delay"; import { getTemplateVersionFiles } from "#/utils/templateVersion"; const templateKey = (templateId: string) => ["template", templateId]; +const templateListsKey = ["templates", "list"] as const; export const template = (templateId: string) => { return { @@ -41,7 +48,7 @@ export const templateByName = (organization: string, name: string) => { export const getTemplatesQueryKey = ( options?: GetTemplatesOptions | GetTemplatesQuery, -) => ["templates", options]; +) => [...templateListsKey, options]; export const templates = ( options?: GetTemplatesOptions | GetTemplatesQuery, @@ -52,6 +59,38 @@ export const templates = ( }; }; +export const invalidateTemplateListQueries = (queryClient: QueryClient) => + queryClient.invalidateQueries({ + queryKey: templateListsKey, + refetchType: "all", + }); + +type UpdateTemplateMetaVariables = { + template: Template; + data: UpdateTemplateMeta; +}; + +export const updateTemplateMeta = (queryClient: QueryClient) => + mutationOptions({ + mutationFn: ({ template, data }: UpdateTemplateMetaVariables) => + API.updateTemplateMeta(template.id, data), + onSuccess: async (result, { template }) => { + const updatedTemplate = result ?? template; + await Promise.all([ + invalidateTemplateListQueries(queryClient), + queryClient.invalidateQueries({ + queryKey: templateKey(template.id), + }), + queryClient.invalidateQueries({ + queryKey: templateByNameKey( + updatedTemplate.organization_name, + updatedTemplate.name, + ), + }), + ]); + }, + }); + export const templateACL = (templateId: string) => { return { queryKey: ["templateAcl", templateId], diff --git a/site/src/modules/management/AISettingsSidebarView.stories.tsx b/site/src/modules/management/AISettingsSidebarView.stories.tsx index a11f1097c726e..70a976d2110cb 100644 --- a/site/src/modules/management/AISettingsSidebarView.stories.tsx +++ b/site/src/modules/management/AISettingsSidebarView.stories.tsx @@ -75,6 +75,26 @@ export const NoDeploymentConfig: Story = { editDeploymentConfig: false, }, }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(canvas.queryByText("Coder Agents")).not.toBeInTheDocument(); + expect(canvas.queryByText("Templates")).not.toBeInTheDocument(); + }, +}; + +export const NoUpdateTemplates: Story = { + args: { + permissions: { + ...MockPermissions, + updateTemplates: false, + }, + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + expect(await canvas.findByText("Coder Agents")).toBeVisible(); + expect(canvas.queryByText("Templates")).not.toBeInTheDocument(); + expect(canvas.getByText("Models")).toBeVisible(); + }, }; export const NoPermissions: Story = { diff --git a/site/src/modules/management/AISettingsSidebarView.tsx b/site/src/modules/management/AISettingsSidebarView.tsx index 37ac31bc41bec..3cf88f3e4f5c0 100644 --- a/site/src/modules/management/AISettingsSidebarView.tsx +++ b/site/src/modules/management/AISettingsSidebarView.tsx @@ -62,7 +62,9 @@ const AISettingsSidebarView: FC = ({ MCP servers - Templates + {permissions.updateTemplates && ( + Templates + )} Instructions diff --git a/site/src/pages/AISettingsPage/TemplatesPage/TemplatesPage.stories.tsx b/site/src/pages/AISettingsPage/TemplatesPage/TemplatesPage.stories.tsx new file mode 100644 index 0000000000000..1cf4f88462b7a --- /dev/null +++ b/site/src/pages/AISettingsPage/TemplatesPage/TemplatesPage.stories.tsx @@ -0,0 +1,305 @@ +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { expect, spyOn, userEvent, waitFor, within } from "storybook/test"; +import { API } from "#/api/api"; +import { getTemplatesQueryKey } from "#/api/queries/templates"; +import type { Template } from "#/api/typesGenerated"; +import { createDeferred, type Deferred } from "#/testHelpers/deferred"; +import { + MockTemplate, + MockUserOwner, + mockApiError, +} from "#/testHelpers/entities"; +import { + withAuthProvider, + withDashboardProvider, + withToaster, +} from "#/testHelpers/storybook"; +import TemplatesPage from "./TemplatesPage"; + +const mockSecondTemplate: Template = { + ...MockTemplate, + id: "second-template", + name: "second-template", + display_name: "Second Template", +}; + +type ToggleDeferreds = { + first: Deferred