diff --git a/docs/user-guides/user-secrets.md b/docs/user-guides/user-secrets.md index d3b6cc45b6..70e2f2bc78 100644 --- a/docs/user-guides/user-secrets.md +++ b/docs/user-guides/user-secrets.md @@ -79,6 +79,9 @@ distinct paths to avoid the collision. ## Create a secret +You can create, edit, and delete user secrets in the Coder dashboard. Click your +avatar, select **Account**, then select **Secrets**. + Use `coder secret create ` to create a user secret. For sensitive values, provide the value through non-interactive stdin with a pipe or redirect. This keeps the value out of your shell history and process arguments. diff --git a/site/e2e/helpers.ts b/site/e2e/helpers.ts index 38205be20d..dc68cba15f 100644 --- a/site/e2e/helpers.ts +++ b/site/e2e/helpers.ts @@ -432,7 +432,7 @@ export const startWorkspaceWithEphemeralParameters = async ( await fillParameters(page, richParameters, buildParameters); - await page.getByRole("button", { name: /update and start/i }).click(); + await clickWorkspaceUpdateSubmit(page, /update and start/i); await page.waitForSelector("text=Workspace status: Running", { state: "visible", @@ -1107,6 +1107,12 @@ const fillParameters = async ( } }; +const clickWorkspaceUpdateSubmit = async (page: Page, name: RegExp) => { + const submitButton = page.getByRole("button", { name }); + await expect(submitButton).toBeEnabled({ timeout: 30_000 }); + await submitButton.click(); +}; + export const updateTemplate = async ( page: Page, organization: string, @@ -1205,11 +1211,11 @@ export const updateWorkspace = async ( await fillParameters(page, richParameters, buildParameters); if (workspaceStatus === "running") { - await page.getByRole("button", { name: /update and restart/i }).click(); + await clickWorkspaceUpdateSubmit(page, /update and restart/i); // Confirmation dialog. await page.getByRole("button", { name: /restart/i }).click(); } else { - await page.getByRole("button", { name: /update and start/i }).click(); + await clickWorkspaceUpdateSubmit(page, /update and start/i); } }; @@ -1228,11 +1234,11 @@ export const updateWorkspaceParameters = async ( await fillParameters(page, richParameters, buildParameters); if (workspaceStatus === "running") { - await page.getByRole("button", { name: /update and restart/i }).click(); + await clickWorkspaceUpdateSubmit(page, /update and restart/i); // Confirmation dialog. await page.getByRole("button", { name: /restart/i }).click(); } else { - await page.getByRole("button", { name: /update and start/i }).click(); + await clickWorkspaceUpdateSubmit(page, /update and start/i); } await page.waitForSelector("text=Workspace status: Running", { diff --git a/site/src/api/queries/userSecrets.ts b/site/src/api/queries/userSecrets.ts new file mode 100644 index 0000000000..40463d7851 --- /dev/null +++ b/site/src/api/queries/userSecrets.ts @@ -0,0 +1,52 @@ +import type { QueryClient } from "react-query"; +import { API } from "#/api/api"; +import type * as TypesGen from "#/api/typesGenerated"; + +const userSecretsKey = (userId: string) => ["users", userId, "secrets"]; + +export const userSecrets = (userId: string) => { + return { + queryKey: userSecretsKey(userId), + queryFn: () => API.getUserSecrets(userId), + }; +}; + +export const createUserSecret = (queryClient: QueryClient, userId: string) => { + return { + mutationFn: (request: TypesGen.CreateUserSecretRequest) => + API.createUserSecret(userId, request), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: userSecretsKey(userId), + }); + }, + }; +}; + +export const updateUserSecret = (queryClient: QueryClient, userId: string) => { + return { + mutationFn: ({ + name, + request, + }: { + name: string; + request: TypesGen.UpdateUserSecretRequest; + }) => API.updateUserSecret(userId, name, request), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: userSecretsKey(userId), + }); + }, + }; +}; + +export const deleteUserSecret = (queryClient: QueryClient, userId: string) => { + return { + mutationFn: (name: string) => API.deleteUserSecret(userId, name), + onSuccess: async () => { + await queryClient.invalidateQueries({ + queryKey: userSecretsKey(userId), + }); + }, + }; +}; diff --git a/site/src/components/FeatureStageBadge/FeatureStageBadge.stories.tsx b/site/src/components/FeatureStageBadge/FeatureStageBadge.stories.tsx index 520970cd54..fe1c848988 100644 --- a/site/src/components/FeatureStageBadge/FeatureStageBadge.stories.tsx +++ b/site/src/components/FeatureStageBadge/FeatureStageBadge.stories.tsx @@ -1,9 +1,11 @@ import type { Meta, StoryObj } from "@storybook/react-vite"; +import { chromatic } from "#/testHelpers/chromatic"; import { FeatureStageBadge } from "./FeatureStageBadge"; const meta: Meta = { title: "components/FeatureStageBadge", component: FeatureStageBadge, + parameters: { chromatic }, args: { contentType: "beta", }, @@ -19,6 +21,13 @@ export const ExtraSmallBeta: Story = { }, }; +export const ExtraSmallEarlyAccess: Story = { + args: { + size: "xs", + contentType: "early_access", + }, +}; + export const SmallBeta: Story = { args: { size: "sm", diff --git a/site/src/components/FeatureStageBadge/FeatureStageBadge.tsx b/site/src/components/FeatureStageBadge/FeatureStageBadge.tsx index 5c76219696..bedfd6222c 100644 --- a/site/src/components/FeatureStageBadge/FeatureStageBadge.tsx +++ b/site/src/components/FeatureStageBadge/FeatureStageBadge.tsx @@ -13,8 +13,8 @@ import { docs } from "#/utils/docs"; * ensure that we can't accidentally make typos when writing the badge text. */ const featureStageBadgeTypes = { - early_access: "early access", - beta: "beta", + early_access: "Early Access", + beta: "Beta", } as const satisfies Record; type FeatureStageBadgeProps = Readonly< @@ -26,14 +26,21 @@ type FeatureStageBadgeProps = Readonly< >; const badgeColorClasses = { - early_access: "bg-surface-orange text-content-warning", + early_access: "border-border-pending bg-surface-sky text-highlight-sky", beta: "bg-surface-sky text-highlight-sky", } as const; const badgeSizeClasses = { - xs: "text-2xs font-normal px-1.5 py-0.5 h-[18px] rounded border-0", - sm: "text-xs font-medium px-2 py-1", - md: "text-base px-2 py-1", + early_access: { + xs: "rounded-[5px] px-1.5 py-0.5 text-2xs font-normal leading-4", + sm: "rounded-[5px] px-2 py-0.5 text-[10px] font-normal leading-4", + md: "rounded-[5px] px-[7px] py-[3.5px] text-xs font-normal leading-4", + }, + beta: { + xs: "text-2xs font-normal px-1.5 py-0.5 h-[18px] rounded border-0", + sm: "text-xs font-medium px-2 py-1", + md: "text-base px-2 py-1", + }, } as const; export const FeatureStageBadge: FC = ({ @@ -44,21 +51,23 @@ export const FeatureStageBadge: FC = ({ ...delegatedProps }) => { const colorClasses = badgeColorClasses[contentType]; - const sizeClasses = badgeSizeClasses[size]; + const sizeClasses = badgeSizeClasses[contentType][size]; return ( - (This is a + + {` (This is ${contentType === "early_access" ? "an" : "a"} `} + {labelText && `${labelText} `} {featureStageBadgeTypes[contentType]} diff --git a/site/src/components/Textarea/Textarea.tsx b/site/src/components/Textarea/Textarea.tsx index 51a248e5ad..f735b73d4c 100644 --- a/site/src/components/Textarea/Textarea.tsx +++ b/site/src/components/Textarea/Textarea.tsx @@ -11,7 +11,7 @@ export const Textarea: React.FC> = ({ return (