Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,9 @@ export const RerunPipelineButton = ({
componentSpec,
),
),
// The generated API types don't include SecretArgument but the backend supports it
taskArguments: (executionData?.rootDetails?.task_spec.arguments ??
undefined) as Record<string, ArgumentType> | undefined,
// Generated API definitions slightly differs from the componentSpec
Comment thread
camielvs marked this conversation as resolved.
taskArguments: executionData?.rootDetails?.task_spec
.arguments as Record<string, ArgumentType>,
authorizationToken,
runNameOverride,
onSuccess: resolve,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ import { Spinner } from "@/components/ui/spinner";
import { Text } from "@/components/ui/typography";

import { AddSecretForm } from "./components/AddSecretForm";
import { getSecrets, SecretsQueryKeys } from "./secretsStorage";
import type { Secret } from "./types";
import { fetchSecretsList } from "./secretsStorage";
import { type Secret, SecretsQueryKeys } from "./types";

type DialogMode = "select" | "add";

Expand Down Expand Up @@ -99,7 +99,7 @@ function SelectSecretDialogContentInternal({
}: SelectSecretDialogContentProps) {
const { data: secrets } = useSuspenseQuery({
queryKey: SecretsQueryKeys.All(),
queryFn: getSecrets,
queryFn: fetchSecretsList,
});

const [mode, setMode] = useState<DialogMode>("select");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import useToastNotification from "@/hooks/useToastNotification";

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

interface AddSecretButtonProps {
secret: Pick<Secret, "name" | "value">;
Expand All @@ -22,7 +23,7 @@ export function AddSecretButton({
const queryClient = useQueryClient();

const { mutate: saveSecret, isPending } = useMutation({
mutationFn: () => addSecret(secret.name, secret.value),
mutationFn: () => addSecret(secret),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: SecretsQueryKeys.All() });
onSuccess();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import { Button } from "@/components/ui/button";
import { Icon } from "@/components/ui/icon";
import useToastNotification from "@/hooks/useToastNotification";

import { removeSecret, SecretsQueryKeys } from "../secretsStorage";
import { removeSecret } from "../secretsStorage";
import type { Secret } from "../types";
import { SecretsQueryKeys } from "../types";

interface RemoveSecretButtonProps {
secret: Pick<Secret, "id">;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import { Text } from "@/components/ui/typography";
import { formatRelativeTime } from "@/utils/date";

import { withSuspenseWrapper } from "../../SuspenseWrapper";
import { getSecrets, SecretsQueryKeys } from "../secretsStorage";
import type { Secret } from "../types";
import { fetchSecretsList } from "../secretsStorage";
import { type Secret, SecretsQueryKeys } from "../types";
import { RemoveSecretButton } from "./RemoveSecretButton";

interface SecretsListProps {
Expand All @@ -21,7 +21,7 @@ interface SecretsListProps {
function SecretsListInternal({ onReplace, onRemoveSuccess }: SecretsListProps) {
const { data: secrets } = useSuspenseQuery({
queryKey: SecretsQueryKeys.All(),
queryFn: getSecrets,
queryFn: fetchSecretsList,
});

if (secrets.length === 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
import useToastNotification from "@/hooks/useToastNotification";

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

interface UpdateSecretButtonProps {
secret: Pick<Secret, "id" | "value">;
Expand All @@ -22,7 +22,7 @@ export function UpdateSecretButton({
const queryClient = useQueryClient();

const { mutate: updateSecretMutation, isPending } = useMutation({
mutationFn: () => updateSecret(secret.id, { value: secret.value }),
mutationFn: () => updateSecret(secret.id, secret),
onSuccess: () => {
void queryClient.invalidateQueries({ queryKey: SecretsQueryKeys.All() });
onSuccess();
Expand Down
180 changes: 85 additions & 95 deletions src/components/shared/SecretsManagement/secretsStorage.ts
Original file line number Diff line number Diff line change
@@ -1,120 +1,110 @@
import {
createSecretApiSecretsPost,
deleteSecretApiSecretsSecretNameDelete,
listSecretsApiSecretsGet,
updateSecretApiSecretsSecretNamePut,
} from "@/api/sdk.gen";

import type { Secret } from "./types";

/**
* In-memory mocked storage for secrets.
* This will be replaced with an async API storage layer later.
* Parses a date string, assuming UTC if no timezone is specified.
* Handles cases where the server may or may not include timezone info.
*/
function parseAsUtc(dateString: string): Date {
// Already has UTC indicator
if (dateString.endsWith("Z")) {
return new Date(dateString);
}

// In-memory storage
const secretsStore = new Map<string, Secret>();

// Subscribers for reactive updates
type Subscriber = () => void;
const subscribers = new Set<Subscriber>();

function notifySubscribers() {
subscribers.forEach((callback) => callback());
}
// Has positive timezone offset (e.g., +05:00)
if (dateString.includes("+")) {
return new Date(dateString);
}

/**
* Generates a unique ID for a new secret.
*/
function generateId(): string {
return `secret_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`;
}
// Check for negative timezone offset by looking for - after the date portion
// ISO date format: YYYY-MM-DD (positions 0-9), so any - after index 9 is timezone
if (dateString.lastIndexOf("-") > 9) {
return new Date(dateString);
}

/**
* Gets all secrets from the store.
* Returns a promise to simulate async API behavior.
*/
export async function getSecrets(): Promise<Secret[]> {
// Simulate network delay
await new Promise((resolve) => setTimeout(resolve, 50));
return Array.from(secretsStore.values()).sort(
(a, b) => b.createdAt.getTime() - a.createdAt.getTime(),
);
// No timezone info detected, assume UTC
return new Date(dateString + "Z");
}

/**
* Adds a new secret to the store.
*/
export async function addSecret(name: string, value: string): Promise<Secret> {
await new Promise((resolve) => setTimeout(resolve, 100));
export async function fetchSecretsList() {
const response = await listSecretsApiSecretsGet();

// Check for duplicate names
const existing = Array.from(secretsStore.values()).find(
(s) => s.name === name,
);
if (existing) {
throw new Error(`A secret with name "${name}" already exists`);
if (response.response.status !== 200) {
throw new Error(`Failed to fetch secrets: ${response.response.body}`);
}

const secret: Secret = {
id: generateId(),
name,
value,
createdAt: new Date(),
};

secretsStore.set(secret.id, secret);
notifySubscribers();

return secret;
return (
response.data?.secrets.map(
(secret) =>
({
id: secret.secret_name,
name: secret.secret_name,
createdAt: parseAsUtc(secret.created_at),
updatedAt: parseAsUtc(secret.updated_at),
expiresAt: secret.expires_at
? parseAsUtc(secret.expires_at)
: undefined,
description: secret.description ?? undefined,
}) satisfies Secret,
) ?? []
);
}

/**
* Updates an existing secret.
*/
export async function updateSecret(
id: string,
updates: Partial<Pick<Secret, "name" | "value">>,
): Promise<Secret> {
await new Promise((resolve) => setTimeout(resolve, 100));

const existing = secretsStore.get(id);
if (!existing) {
throw new Error(`Secret with id "${id}" not found`);
}

// Check for name conflicts if name is being updated
if (updates.name && updates.name !== existing.name) {
const nameConflict = Array.from(secretsStore.values()).find(
(s) => s.name === updates.name && s.id !== id,
);
if (nameConflict) {
throw new Error(`A secret with name "${updates.name}" already exists`);
}
secretId: string,
secret: Partial<Secret> & Pick<Secret, "value">,
) {
const response = await updateSecretApiSecretsSecretNamePut({
path: {
secret_name: secretId,
},
body: {
secret_value: secret.value ?? "",
},
});

if (response.response.status !== 200) {
throw new Error(`Failed to update secret: ${response.response.body}`);
}

const updated: Secret = {
...existing,
...updates,
};
return true;
}

secretsStore.set(id, updated);
notifySubscribers();
export async function addSecret(
secret: Partial<Secret> & Pick<Secret, "name" | "value">,
) {
const response = await createSecretApiSecretsPost({
query: {
secret_name: secret.name ?? "",
},
body: {
secret_value: secret.value ?? "",
},
});

if (response.response.status !== 200) {
throw new Error(`Failed to add secret: ${response.response.body}`);
}

return updated;
return true;
}

/**
* Removes a secret from the store.
*/
export async function removeSecret(id: string): Promise<void> {
await new Promise((resolve) => setTimeout(resolve, 100));
export async function removeSecret(secretId: string) {
const response = await deleteSecretApiSecretsSecretNameDelete({
path: {
secret_name: secretId,
},
});

if (!secretsStore.has(id)) {
throw new Error(`Secret with id "${id}" not found`);
if (response.response.status !== 200) {
throw new Error(`Failed to remove secret: ${response.response.body}`);
}

secretsStore.delete(id);
notifySubscribers();
return true;
}

/**
* Query keys for React Query.
*/
export const SecretsQueryKeys = {
All: () => ["secrets"] as const,
Id: (id: string) => ["secrets", id] as const,
} as const;
13 changes: 12 additions & 1 deletion src/components/shared/SecretsManagement/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ import type { DynamicDataArgument } from "@/utils/componentSpec";
export interface Secret {
id: string;
name: string;
value: string;
value?: string;
createdAt: Date;
updatedAt: Date;
expiresAt?: Date;
description?: string;
}

/**
Expand Down Expand Up @@ -41,3 +44,11 @@ export function createSecretArgument(secretName: string): DynamicDataArgument {
export function extractSecretName(arg: DynamicDataArgument): string {
return arg.dynamicData.secret.name;
}

/**
* Query keys for React Query.
*/
export const SecretsQueryKeys = {
All: () => ["secrets"] as const,
Id: (id: string) => ["secrets", id] as const,
} as const;
Loading