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
19 changes: 9 additions & 10 deletions src/components/Editor/PipelineDetails.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const PipelineDetails = () => {
const {
componentSpec,
graphSpec,
digest,
isComponentTreeValid,
globalValidationIssues,
} = useComponentSpec();
Expand All @@ -54,7 +55,6 @@ const PipelineDetails = () => {
creationTime?: Date;
modificationTime?: Date;
createdBy?: string;
digest?: string;
}>({});

// Fetch file metadata on mount or when componentSpec.name changes
Expand All @@ -72,7 +72,6 @@ const PipelineDetails = () => {
createdBy: file.componentRef.spec.metadata?.annotations?.author as
| string
| undefined,
digest: file.componentRef.digest,
});
}
};
Expand Down Expand Up @@ -109,6 +108,11 @@ const PipelineDetails = () => {
notify("Input value copied to clipboard", "success");
};

const handleDigestCopy = () => {
navigator.clipboard.writeText(digest);
notify("Digest copied to clipboard", "success");
};

if (!componentSpec) {
return (
<div className="flex flex-col gap-8 items-center justify-center h-full">
Expand Down Expand Up @@ -181,21 +185,16 @@ const PipelineDetails = () => {
)}

{/* Component Digest */}
{fileMeta.digest && (
{digest && (
<div className="mb-2">
<h3 className="text-md font-medium mb-1">Digest</h3>
<Button
className="bg-gray-100 border border-gray-300 rounded p-2 h-fit text-xs w-full text-left hover:bg-gray-200 active:bg-gray-300 transition cursor-pointer"
onClick={() => {
if (fileMeta.digest) {
navigator.clipboard.writeText(fileMeta.digest);
notify("Digest copied to clipboard", "success");
}
}}
onClick={handleDigestCopy}
variant="ghost"
>
<span className="font-mono break-all w-full text-wrap">
{fileMeta.digest}
{digest}
</span>
</Button>
</div>
Expand Down
32 changes: 31 additions & 1 deletion src/providers/ComponentSpecProvider.tsx
Copy link
Collaborator

@Mbeaulne Mbeaulne Nov 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think digest is technically part of the component spec. I wonder if we just update the component spec component to generate a component spec for itself instead of having it in context, that way we keep the context a little cleaner and it removes the effect in favour of tanstackQuery.

Here is a vibe coded example. Also this isn't a hill im going to die on, just a suggestion

// src/components/Editor/ComponentDigest.tsx
import { useSuspenseQuery } from "@tanstack/react-query";
import { Suspense, useMemo } from "react";

import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import useToastNotification from "@/hooks/useToastNotification";
import { useComponentSpec } from "@/providers/ComponentSpecProvider";
import { generateDigest } from "@/services/componentService";
import { componentSpecToYaml } from "@/utils/componentStore";

function useComponentSpecDigest() {
const { componentSpec } = useComponentSpec();

const yamlText = useMemo(
() => componentSpecToYaml(componentSpec),
[componentSpec],
);

const { data } = useSuspenseQuery({
queryKey: ["componentSpec", "digest", yamlText],
queryFn: () => generateDigest(yamlText),
staleTime: Infinity,
});

return data;
}

const DigestSkeleton = () => (

  <div class="">
    <h3 class="">Digest</h3>
  </div>
);

const DigestContent = () => {
const digest = useComponentSpecDigest();
const notify = useToastNotification();

return (
<div class=""><h3 class="">Digest</h3>&#x3C;ButtonclassName="bg-gray-100 border border-gray-300 rounded p-2 h-fit text-xs w-full text-left hover:bg-gray-200 active:bg-gray-300 transition cursor-pointer"onClick={() => {navigator.clipboard.writeText(digest);notify("Digest copied to clipboard", "success");}}variant="ghost"><span class="">{digest}</span></div>
);
};

export const ComponentDigest = () => (
\<Suspense fallback={\<DigestSkeleton />}>
\<DigestContent />
\</Suspense>
);  

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did consider whether to do this calculation in the component itself vs the provider. I ultimately put it in the provider because any given component spec is going to have its own unique digest so I thought it might be useful to have one source of truth for the digest.

I'm not sure about using useQuery because the digest is constantly changing so the benefits of caching is low. I think for now I'm going to keep it as-is and we can take a closer look later.

Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import equal from "fast-deep-equal";
import { type ReactNode, useCallback, useMemo, useRef, useState } from "react";
import {
type ReactNode,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";

import { type UndoRedo, useUndoRedo } from "@/hooks/useUndoRedo";
import { loadPipelineByName } from "@/services/pipelineService";
Expand Down Expand Up @@ -28,6 +35,7 @@ import {
import {
type ComponentReferenceWithSpec,
componentSpecToYaml,
generateDigest,
writeComponentToFileListFromText,
} from "../utils/componentStore";

Expand All @@ -47,6 +55,7 @@ interface ComponentSpecContextType {
graphSpec: GraphSpec;
currentGraphSpec: GraphSpec;
currentSubgraphSpec: ComponentSpec;
digest: string;
isLoading: boolean;
isValid: boolean;
errors: ValidationError[];
Expand Down Expand Up @@ -80,6 +89,7 @@ export const ComponentSpecProvider = ({
const [componentSpec, setComponentSpec] = useState<ComponentSpec>(
spec ?? EMPTY_GRAPH_COMPONENT_SPEC,
);
const [digest, setDigest] = useState<string>("");

const [isLoading, setIsLoading] = useState(!!spec);

Expand Down Expand Up @@ -118,6 +128,24 @@ export const ComponentSpecProvider = ({
);
const isComponentTreeValid = globalValidationIssues.length === 0;

useEffect(() => {
let isCancelled = false;

const computeDigest = async () => {
const text = componentSpecToYaml(componentSpec);
const newDigest = await generateDigest(text);
if (!isCancelled) {
setDigest(newDigest);
}
};

computeDigest();

return () => {
isCancelled = true;
};
}, [componentSpec]);

const clearComponentSpec = useCallback(() => {
setComponentSpec(EMPTY_GRAPH_COMPONENT_SPEC);
setIsLoading(false);
Expand Down Expand Up @@ -239,6 +267,7 @@ export const ComponentSpecProvider = ({
graphSpec,
currentGraphSpec,
currentSubgraphSpec,
digest,
isLoading,
isValid,
errors,
Expand All @@ -262,6 +291,7 @@ export const ComponentSpecProvider = ({
graphSpec,
currentGraphSpec,
currentSubgraphSpec,
digest,
isLoading,
isValid,
errors,
Expand Down