Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions react-compiler.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export const REACT_COMPILER_ENABLED_DIRS = [
"src/services/projects/useProjectRuns.ts",
"src/services/projects/useWorkspaceChoice.ts",
"src/services/teams",
"src/components/shared/Teams",
"src/services/usePipelineSpec.ts",
"src/components/shared/FavoriteToggle.tsx",
"src/components/shared/LocalPipelineMigration.tsx",
Expand Down
2 changes: 1 addition & 1 deletion src/components/shared/InfoBox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Text } from "@/components/ui/typography";
import { cn } from "@/lib/utils";

interface InfoBoxProps {
title: string;
title: ReactNode;
width?: "full" | "fit" | "auto";
className?: string;
children: ReactNode;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,30 @@ import { SecretsQueryKeys } from "../types";

interface AddSecretButtonProps {
secret: Pick<Secret, "name" | "value" | "description" | "expiresAt">;
teamId?: string;
disabled?: boolean;
onSuccess: () => void;
}

export function AddSecretButton({
secret,
teamId,
disabled,
onSuccess,
}: AddSecretButtonProps) {
const notify = useToastNotification();
const queryClient = useQueryClient();

const { mutate: saveSecret, isPending } = useMutation({
mutationFn: () => addSecret(secret),
mutationFn: () => addSecret(secret, teamId),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: SecretsQueryKeys.All() });
void queryClient.invalidateQueries({
queryKey: SecretsQueryKeys.All(teamId),
});
onSuccess();
},
onError: () => {
notify("Failed to add secret", "error");
onError: (error) => {
notify(error.message, "error");
},
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import type { ReactNode } from "react";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { addSecret, updateSecret } from "../secretsStorage";
import type { Secret } from "../types";
import { type Secret, SecretsQueryKeys } from "../types";
import { AddSecretForm } from "./AddSecretForm";

vi.mock("../secretsStorage", () => ({
Expand All @@ -22,6 +22,8 @@ class ResizeObserverMock {
}
vi.stubGlobal("ResizeObserver", ResizeObserverMock);

const TEAM_ID = "ml-team@example.com";

const existing: Secret = {
id: "API_KEY",
name: "API_KEY",
Expand All @@ -30,14 +32,19 @@ const existing: Secret = {
updatedAt: new Date("2026-10-02T09:00:00Z"),
};

function renderForm(existingSecret?: Secret) {
function renderForm({
existingSecret,
teamId,
}: { existingSecret?: Secret; teamId?: string } = {}) {
const queryClient = new QueryClient({
defaultOptions: { queries: { retry: false }, mutations: { retry: false } },
});
const invalidateQueries = vi.spyOn(queryClient, "invalidateQueries");
const onSuccess = vi.fn();
render(
<AddSecretForm
existingSecret={existingSecret}
teamId={teamId}
onSuccess={onSuccess}
onCancel={vi.fn()}
/>,
Expand All @@ -49,7 +56,7 @@ function renderForm(existingSecret?: Secret) {
),
},
);
return { user: userEvent.setup(), onSuccess };
return { user: userEvent.setup(), onSuccess, invalidateQueries };
}

const field = (label: string) => screen.getByLabelText(label);
Expand All @@ -74,11 +81,30 @@ describe("AddSecretForm", () => {
await user.click(screen.getByRole("button", { name: "Add Secret" }));

await waitFor(() => expect(onSuccess).toHaveBeenCalled());
expect(addSecret).toHaveBeenCalledWith({
name: "API_KEY",
value: "s3cret",
description: "Model access",
expiresAt: undefined,
expect(addSecret).toHaveBeenCalledWith(
{
name: "API_KEY",
value: "s3cret",
description: "Model access",
expiresAt: undefined,
},
undefined,
);
});

it("adds a secret to the team it is given", async () => {
const { user, onSuccess, invalidateQueries } = renderForm({
teamId: TEAM_ID,
});

await user.type(field("Secret Name"), "API_KEY");
await user.type(field("Secret Value"), "s3cret");
await user.click(screen.getByRole("button", { name: "Add Secret" }));

await waitFor(() => expect(onSuccess).toHaveBeenCalled());
expect(addSecret).toHaveBeenCalledWith(expect.anything(), TEAM_ID);
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: SecretsQueryKeys.All(TEAM_ID),
});
});

Expand All @@ -92,6 +118,7 @@ describe("AddSecretForm", () => {
await waitFor(() =>
expect(addSecret).toHaveBeenCalledWith(
expect.objectContaining({ description: undefined }),
undefined,
),
);
});
Expand Down Expand Up @@ -143,6 +170,7 @@ describe("AddSecretForm", () => {
),
),
}),
undefined,
),
);
});
Expand All @@ -156,7 +184,7 @@ describe("AddSecretForm", () => {

describe("replacing", () => {
it("starts from the secret's description", () => {
renderForm(existing);
renderForm({ existingSecret: existing });

expect(field("Secret Name")).toBeDisabled();
expect(field("Description (optional)")).toHaveValue("Model access");
Expand All @@ -166,22 +194,46 @@ describe("AddSecretForm", () => {
});

it("sends only the new value when nothing else changed", async () => {
const { user, onSuccess } = renderForm(existing);
const { user, onSuccess } = renderForm({ existingSecret: existing });

await user.type(field("New Secret Value"), "n3w");
await user.click(screen.getByRole("button", { name: "Update Secret" }));

await waitFor(() => expect(onSuccess).toHaveBeenCalled());
expect(updateSecret).toHaveBeenCalledWith("API_KEY", {
id: "API_KEY",
value: "n3w",
description: undefined,
expiresAt: undefined,
expect(updateSecret).toHaveBeenCalledWith(
"API_KEY",
{
id: "API_KEY",
value: "n3w",
description: undefined,
expiresAt: undefined,
},
undefined,
);
});

it("replaces a team's secret as the team", async () => {
const { user, onSuccess, invalidateQueries } = renderForm({
existingSecret: existing,
teamId: TEAM_ID,
});

await user.type(field("New Secret Value"), "n3w");
await user.click(screen.getByRole("button", { name: "Update Secret" }));

await waitFor(() => expect(onSuccess).toHaveBeenCalled());
expect(updateSecret).toHaveBeenCalledWith(
"API_KEY",
expect.anything(),
TEAM_ID,
);
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: SecretsQueryKeys.All(TEAM_ID),
});
});

it("sends a description that was changed", async () => {
const { user } = renderForm(existing);
const { user } = renderForm({ existingSecret: existing });

await user.type(field("New Secret Value"), "n3w");
await user.clear(field("Description (optional)"));
Expand All @@ -192,20 +244,26 @@ describe("AddSecretForm", () => {
expect(updateSecret).toHaveBeenCalledWith(
"API_KEY",
expect.objectContaining({ description: "Agent model access" }),
undefined,
),
);
});

it("names the current expiry and keeps it when left empty", () => {
renderForm({ ...existing, expiresAt: new Date("2027-03-03T23:59:59Z") });
renderForm({
existingSecret: {
...existing,
expiresAt: new Date("2027-03-03T23:59:59Z"),
},
});

expect(field("Expires (optional)")).toHaveAccessibleDescription(
"Currently Mar 3, 2027. Leave empty to keep that date.",
);
});

it("still needs a new value", () => {
renderForm(existing);
renderForm({ existingSecret: existing });

expect(
screen.getByRole("button", { name: "Update Secret" }),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,12 +85,14 @@ function expiryHint(existingSecret?: Secret): string | null {

interface AddSecretFormProps {
existingSecret?: Secret;
teamId?: string;
onSuccess: () => void;
onCancel: () => void;
}

export function AddSecretForm({
existingSecret,
teamId,
onSuccess,
onCancel,
}: AddSecretFormProps) {
Expand Down Expand Up @@ -233,6 +235,7 @@ export function AddSecretForm({
description,
expiresAt,
}}
teamId={teamId}
disabled={!isFormValid}
onSuccess={onSuccess}
/>
Expand All @@ -244,6 +247,7 @@ export function AddSecretForm({
description,
expiresAt,
}}
teamId={teamId}
disabled={!isFormValid}
onSuccess={onSuccess}
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,25 @@ import type { ReactNode } from "react";
import { afterEach, describe, expect, it, vi } from "vitest";

import { removeSecret } from "../secretsStorage";
import { SecretsQueryKeys } from "../types";
import { RemoveSecretButton } from "./RemoveSecretButton";

vi.mock("../secretsStorage", () => ({ removeSecret: vi.fn() }));

vi.mock("@/hooks/useToastNotification", () => ({ default: () => vi.fn() }));

function renderButton() {
const TEAM_ID = "ml-team@example.com";

function renderButton(teamId?: string) {
const queryClient = new QueryClient({
defaultOptions: { mutations: { retry: false } },
});
const invalidateQueries = vi.spyOn(queryClient, "invalidateQueries");
const onSuccess = vi.fn();
render(
<RemoveSecretButton
secret={{ id: "API_KEY", name: "API_KEY" }}
teamId={teamId}
onSuccess={onSuccess}
/>,
{
Expand All @@ -29,7 +34,7 @@ function renderButton() {
),
},
);
return { user: userEvent.setup(), onSuccess };
return { user: userEvent.setup(), onSuccess, invalidateQueries };
}

describe("RemoveSecretButton", () => {
Expand Down Expand Up @@ -58,7 +63,21 @@ describe("RemoveSecretButton", () => {
await user.click(screen.getByRole("button", { name: "Continue" }));

await waitFor(() => expect(onSuccess).toHaveBeenCalled());
expect(removeSecret).toHaveBeenCalledWith("API_KEY");
expect(removeSecret).toHaveBeenCalledWith("API_KEY", undefined);
});

it("deletes a team's secret as the team", async () => {
vi.mocked(removeSecret).mockResolvedValue(true);
const { user, onSuccess, invalidateQueries } = renderButton(TEAM_ID);

await user.click(screen.getByRole("button", { name: "Delete API_KEY" }));
await user.click(screen.getByRole("button", { name: "Continue" }));

await waitFor(() => expect(onSuccess).toHaveBeenCalled());
expect(removeSecret).toHaveBeenCalledWith("API_KEY", TEAM_ID);
expect(invalidateQueries).toHaveBeenCalledWith({
queryKey: SecretsQueryKeys.All(TEAM_ID),
});
});

it("keeps the secret when cancelled", async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,24 +11,28 @@ import { SecretsQueryKeys } from "../types";

interface RemoveSecretButtonProps {
secret: Pick<Secret, "id" | "name">;
teamId?: string;
onSuccess?: () => void;
}

export function RemoveSecretButton({
secret,
teamId,
onSuccess,
}: RemoveSecretButtonProps) {
const notify = useToastNotification();
const queryClient = useQueryClient();

const { mutate: removeSecretMutation, isPending } = useMutation({
mutationFn: () => removeSecret(secret.id),
mutationFn: () => removeSecret(secret.id, teamId),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: SecretsQueryKeys.All() });
void queryClient.invalidateQueries({
queryKey: SecretsQueryKeys.All(teamId),
});
onSuccess?.();
},
onError: () => {
notify("Failed to remove secret", "error");
onError: (error) => {
notify(error.message, "error");
},
});

Expand Down
Loading
Loading