diff --git a/src/components/Learn/tours/registry.ts b/src/components/Learn/tours/registry.ts
index 5fa9891f7..f77b02992 100644
--- a/src/components/Learn/tours/registry.ts
+++ b/src/components/Learn/tours/registry.ts
@@ -18,7 +18,12 @@ export type TourStep = StepType & {
| "navigate-to-root"
| "unpack-subgraph"
| "multi-select-tasks"
- | "create-subgraph";
+ | "create-subgraph"
+ | "open-secret-dialog"
+ | "open-settings-panel"
+ | "open-submit-dialog"
+ | "assign-secret-argument"
+ | "assign-secret-submit";
targetWindowId?: string;
targetWindowName?: string;
targetFolderName?: string;
@@ -34,6 +39,7 @@ export type TourStep = StepType & {
targetPortName?: string;
};
ringSelectors?: string[];
+ tourPanel?: "secrets-manager";
resetLibrarySearch?: boolean;
ensureWindowRestored?: string;
requiresTaskSelected?: string;
@@ -45,6 +51,12 @@ export interface TourDefinition {
displayName?: string;
requiresEditor?: boolean;
starterPipelineUrl?: string;
+ /**
+ * When true, a tour running without a real backend mocks the secrets backend
+ * in-memory so its backend-dependent steps stay hands-on (see
+ * tourMockBackend). With a real backend this has no effect.
+ */
+ mockBackend?: boolean;
steps: TourStep[];
}
diff --git a/src/components/shared/SecretsManagement/SelectSecretDialog.tsx b/src/components/shared/SecretsManagement/SelectSecretDialog.tsx
index dc4bcb2a5..db4223c34 100644
--- a/src/components/shared/SecretsManagement/SelectSecretDialog.tsx
+++ b/src/components/shared/SecretsManagement/SelectSecretDialog.tsx
@@ -16,8 +16,11 @@ import { ScrollArea } from "@/components/ui/scroll-area";
import { Separator } from "@/components/ui/separator";
import { Spinner } from "@/components/ui/spinner";
import { Text } from "@/components/ui/typography";
+import { useBackend } from "@/providers/BackendProvider";
+import { useTourMockBackend } from "@/providers/TourProvider/tourMockBackend";
import { AddSecretForm } from "./components/AddSecretForm";
+import { SecretsBackendUnavailable } from "./components/SecretsBackendUnavailable";
import { fetchSecretsList } from "./secretsStorage";
import { type Secret, SecretsQueryKeys } from "./types";
@@ -189,6 +192,8 @@ export function SelectSecretDialog({
onOpenChange,
onSelect,
}: SelectSecretDialogProps) {
+ const { available } = useBackend();
+ const mockBackend = useTourMockBackend();
const handleSelect = (secretName: string) => {
onSelect(secretName);
onOpenChange(false);
@@ -198,7 +203,11 @@ export function SelectSecretDialog({
diff --git a/src/components/shared/SecretsManagement/components/SecretsBackendUnavailable.tsx b/src/components/shared/SecretsManagement/components/SecretsBackendUnavailable.tsx
new file mode 100644
index 000000000..de1016c31
--- /dev/null
+++ b/src/components/shared/SecretsManagement/components/SecretsBackendUnavailable.tsx
@@ -0,0 +1,25 @@
+import { Icon } from "@/components/ui/icon";
+import { BlockStack } from "@/components/ui/layout";
+import { Text } from "@/components/ui/typography";
+
+/**
+ * Shown in place of the secrets list/picker when no backend is available.
+ * Secrets live on the Tangle backend, so without one there is nothing to list
+ * and a friendly explanation is clearer than the generic error fallback.
+ */
+export function SecretsBackendUnavailable() {
+ return (
+
+
+ Backend not connected
+
+ Secrets are stored on your Tangle backend. Connect one in Settings to
+ manage secrets.
+
+
+ );
+}
diff --git a/src/components/shared/SecretsManagement/components/SecretsList.tsx b/src/components/shared/SecretsManagement/components/SecretsList.tsx
index 8f138b204..a11991290 100644
--- a/src/components/shared/SecretsManagement/components/SecretsList.tsx
+++ b/src/components/shared/SecretsManagement/components/SecretsList.tsx
@@ -6,18 +6,25 @@ import { Icon } from "@/components/ui/icon";
import { BlockStack, InlineStack } from "@/components/ui/layout";
import { Skeleton } from "@/components/ui/skeleton";
import { Text } from "@/components/ui/typography";
+import { useBackend } from "@/providers/BackendProvider";
+import { useTourMockBackend } from "@/providers/TourProvider/tourMockBackend";
import { formatRelativeTime } from "@/utils/date";
import { withSuspenseWrapper } from "../../SuspenseWrapper";
import { fetchSecretsList } from "../secretsStorage";
-import { SecretsQueryKeys } from "../types";
+import { type Secret, SecretsQueryKeys } from "../types";
import { RemoveSecretButton } from "./RemoveSecretButton";
+import { SecretsBackendUnavailable } from "./SecretsBackendUnavailable";
interface SecretsListProps {
onRemoveSuccess?: () => void;
+ onEditSecret?: (secret: Secret) => void;
}
-function SecretsListInternal({ onRemoveSuccess }: SecretsListProps) {
+function SecretsListInternal({
+ onRemoveSuccess,
+ onEditSecret,
+}: SecretsListProps) {
const { data: secrets } = useSuspenseQuery({
queryKey: SecretsQueryKeys.All(),
queryFn: fetchSecretsList,
@@ -67,16 +74,27 @@ function SecretsListInternal({ onRemoveSuccess }: SecretsListProps) {
-
-
@@ -96,7 +114,16 @@ function SecretsListSkeleton() {
);
}
-export const SecretsList = withSuspenseWrapper(
+const SecretsListWithSuspense = withSuspenseWrapper(
SecretsListInternal,
SecretsListSkeleton,
);
+
+export function SecretsList(props: SecretsListProps) {
+ const { available } = useBackend();
+ const mockBackend = useTourMockBackend();
+ if (!available && !mockBackend) {
+ return ;
+ }
+ return ;
+}
diff --git a/src/components/shared/SecretsManagement/components/SecretsListView.tsx b/src/components/shared/SecretsManagement/components/SecretsListView.tsx
index 9f8448407..ebe3665c4 100644
--- a/src/components/shared/SecretsManagement/components/SecretsListView.tsx
+++ b/src/components/shared/SecretsManagement/components/SecretsListView.tsx
@@ -9,9 +9,18 @@ import useToastNotification from "@/hooks/useToastNotification";
import { useAnalytics } from "@/providers/AnalyticsProvider";
import { tracking } from "@/utils/tracking";
+import type { Secret } from "../types";
import { SecretsList } from "./SecretsList";
-export function SecretsListView() {
+interface SecretsListViewProps {
+ onAddSecret?: () => void;
+ onEditSecret?: (secret: Secret) => void;
+}
+
+export function SecretsListView({
+ onAddSecret,
+ onEditSecret,
+}: SecretsListViewProps = {}) {
const notify = useToastNotification();
const { track } = useAnalytics();
@@ -32,22 +41,36 @@ export function SecretsListView() {
-
+
-
-
+ {onAddSecret ? (
+
Add Secret
-
+ ) : (
+
+
+
+ Add Secret
+
+
+ )}
);
diff --git a/src/components/shared/SecretsManagement/secretsStorage.ts b/src/components/shared/SecretsManagement/secretsStorage.ts
index 60bf25f29..484283874 100644
--- a/src/components/shared/SecretsManagement/secretsStorage.ts
+++ b/src/components/shared/SecretsManagement/secretsStorage.ts
@@ -4,6 +4,13 @@ import {
listSecretsApiSecretsGet,
updateSecretApiSecretsSecretNamePut,
} from "@/api/sdk.gen";
+import {
+ isTourMockActive,
+ mockAddSecret,
+ mockListSecrets,
+ mockRemoveSecret,
+ mockUpdateSecret,
+} from "@/providers/TourProvider/tourMockBackend";
import type { Secret } from "./types";
@@ -33,6 +40,10 @@ function parseAsUtc(dateString: string): Date {
}
export async function fetchSecretsList() {
+ if (isTourMockActive()) {
+ return mockListSecrets();
+ }
+
const response = await listSecretsApiSecretsGet();
if (response.response.status !== 200) {
@@ -60,6 +71,11 @@ export async function updateSecret(
secretId: string,
secret: Partial & Pick,
) {
+ if (isTourMockActive()) {
+ mockUpdateSecret(secretId, secret.value);
+ return true;
+ }
+
const response = await updateSecretApiSecretsSecretNamePut({
path: {
secret_name: secretId,
@@ -79,6 +95,11 @@ export async function updateSecret(
export async function addSecret(
secret: Partial & Pick,
) {
+ if (isTourMockActive()) {
+ mockAddSecret(secret.name ?? "", secret.value);
+ return true;
+ }
+
const response = await createSecretApiSecretsPost({
query: {
secret_name: secret.name ?? "",
@@ -96,6 +117,11 @@ export async function addSecret(
}
export async function removeSecret(secretId: string) {
+ if (isTourMockActive()) {
+ mockRemoveSecret(secretId);
+ return true;
+ }
+
const response = await deleteSecretApiSecretsSecretNameDelete({
path: {
secret_name: secretId,
diff --git a/src/components/shared/Submitters/Tangle/TangleSubmitter.tsx b/src/components/shared/Submitters/Tangle/TangleSubmitter.tsx
index afb94994b..93c523d3b 100644
--- a/src/components/shared/Submitters/Tangle/TangleSubmitter.tsx
+++ b/src/components/shared/Submitters/Tangle/TangleSubmitter.tsx
@@ -12,6 +12,7 @@ import useCooldownTimer from "@/hooks/useCooldownTimer";
import useToastNotification from "@/hooks/useToastNotification";
import { cn } from "@/lib/utils";
import { useBackend } from "@/providers/BackendProvider";
+import { useTourMockBackend } from "@/providers/TourProvider/tourMockBackend";
import { APP_ROUTES } from "@/routes/router";
import { updateRunAnnotation } from "@/services/pipelineRunService";
import type { PipelineRun } from "@/types/pipelineRun";
@@ -102,6 +103,10 @@ const TangleSubmitter = ({
}: TangleSubmitterProps) => {
const { isAuthorized } = useAwaitAuthorization();
const { backendUrl, configured, available } = useBackend();
+ // During a no-backend tour the secrets flow is mocked in-memory, so allow the
+ // run-arguments dialog to open (to assign a secret to an input). Real run
+ // submission stays gated on `available`.
+ const mockBackend = useTourMockBackend();
const { mutate: submit, isPending: isSubmitting } = useSubmitPipeline();
const isAutoRedirect = useFlagValue("redirect-on-new-pipeline-run");
@@ -310,7 +315,7 @@ const TangleSubmitter = ({
size="icon"
data-testid="run-with-arguments-button"
onClick={() => setIsArgumentsDialogOpen(true)}
- disabled={!available}
+ disabled={!available && !mockBackend}
>
diff --git a/src/components/shared/Submitters/Tangle/components/SubmitTaskArgumentsDialog.tsx b/src/components/shared/Submitters/Tangle/components/SubmitTaskArgumentsDialog.tsx
index ee90e8dec..9a3f183f1 100644
--- a/src/components/shared/Submitters/Tangle/components/SubmitTaskArgumentsDialog.tsx
+++ b/src/components/shared/Submitters/Tangle/components/SubmitTaskArgumentsDialog.tsx
@@ -36,6 +36,8 @@ import { Paragraph } from "@/components/ui/typography";
import useToastNotification from "@/hooks/useToastNotification";
import { cn } from "@/lib/utils";
import { useBackend } from "@/providers/BackendProvider";
+import { useTourMockBackend } from "@/providers/TourProvider/tourMockBackend";
+import { useTourMode } from "@/providers/TourProvider/TourModeContext";
import {
fetchExecutionDetails,
fetchPipelineRun,
@@ -66,6 +68,10 @@ export const SubmitTaskArgumentsDialog = ({
componentSpec,
}: SubmitTaskArgumentsDialogProps) => {
const notify = useToastNotification();
+ const tourMode = useTourMode();
+ // In a no-backend tour the secret-to-input assignment is demonstrated, but
+ // launching a real run isn't possible, so the submit confirm stays disabled.
+ const mockBackend = useTourMockBackend();
const initialArgs = getArgumentsFromInputs(componentSpec);
const [runNotes, setRunNotes] = useState("");
@@ -135,8 +141,16 @@ export const SubmitTaskArgumentsDialog = ({
const hasInputs = inputs.length > 0;
return (
-