Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
1c82d37
feat: add read-only configuration view for component sidebar
ropsii Jun 9, 2026
ce604aa
Merge branch 'main' into configuration-view-ux
ropsii Jun 9, 2026
42f22a5
fix: show customField content in read-only settings tab
ropsii Jun 9, 2026
8be5fd6
fix: satisfy prettier and ESLint budget for configuration view
ropsii Jun 9, 2026
9a1d10a
Merge branch 'main' into configuration-view-ux
ropsii Jun 9, 2026
d108f23
fix: refine sidebar tab switching when changing nodes
ropsii Jun 9, 2026
104f923
fix: avoid defaulting read-only integration to first instance
ropsii Jun 9, 2026
585f836
fix: use saved integrationRef in read-only configuration view
ropsii Jun 9, 2026
c068f2a
Merge branch 'main' into configuration-view-ux
ropsii Jun 9, 2026
a89a4f3
fix: skip integration auto-select in read-only settings
ropsii Jun 9, 2026
33a8c08
fix: address remaining read-only configuration view bugbot findings
ropsii Jun 9, 2026
6dd902c
fix: only link validated http(s) URLs in read-only config view
ropsii Jun 9, 2026
74f4658
Merge branch 'main' into configuration-view-ux
ropsii Jun 9, 2026
94f39e7
fix: reset sidebar to Runs when switching nodes in view mode
ropsii Jun 9, 2026
188bad0
test: guard read-only config links against unsafe URL schemes
ropsii Jun 9, 2026
f0c9a16
fix: preserve sidebar tab when switching nodes in view mode
ropsii Jun 9, 2026
c1b504c
Merge branch 'main' into configuration-view-ux
ropsii Jun 9, 2026
28d7fd8
Merge branch 'main' into configuration-view-ux
ropsii Jun 10, 2026
a43a2e0
refactor: simplify integration status variant mapping
ropsii Jun 10, 2026
7c986d1
fix: extract sidebar tab logic to satisfy ESLint max-statements budget
ropsii Jun 10, 2026
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
61 changes: 45 additions & 16 deletions web_src/src/ui/CanvasPage/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1604,6 +1604,9 @@ function CanvasPage(props: CanvasPageProps) {
canReadIntegrations={props.canReadIntegrations}
canCreateIntegrations={props.canCreateIntegrations}
canUpdateIntegrations={props.canUpdateIntegrations}
onEnterEditMode={props.onEnterEditMode}
enterEditModeDisabled={props.enterEditModeDisabled}
enterEditModeDisabledTooltip={props.enterEditModeDisabledTooltip}
/>
) : null}
</div>
Expand Down Expand Up @@ -1681,6 +1684,9 @@ function Sidebar({
canReadIntegrations,
canCreateIntegrations,
canUpdateIntegrations,
onEnterEditMode,
enterEditModeDisabled,
enterEditModeDisabledTooltip,
}: {
state: CanvasPageState;
getSidebarData?: (nodeId: string) => SidebarData | null;
Expand Down Expand Up @@ -1728,6 +1734,9 @@ function Sidebar({
canReadIntegrations?: boolean;
canCreateIntegrations?: boolean;
canUpdateIntegrations?: boolean;
onEnterEditMode?: () => void;
enterEditModeDisabled?: boolean;
enterEditModeDisabledTooltip?: string;
}) {
const sidebarData = useMemo(() => {
if (!state.componentSidebar.selectedNodeId || !getSidebarData) {
Expand Down Expand Up @@ -1892,6 +1901,9 @@ function Sidebar({
hideDocsTab={isAnnotationNode}
hideNodeId={isAnnotationNode}
readOnly={readOnly}
onEnterEditMode={onEnterEditMode}
enterEditModeDisabled={enterEditModeDisabled}
enterEditModeDisabledTooltip={enterEditModeDisabledTooltip}
/>
);
}
Expand Down Expand Up @@ -2107,6 +2119,35 @@ function resolveAbsoluteNodeRect(
};
}

type ComponentSidebarTab = "latest" | "settings" | "docs";

type NodeConfigurationWarningData = {
component?: { error?: string };
composite?: { error?: string };
trigger?: { error?: string };
} | null;

function shouldOpenSidebarSettingsTab(nodeData: NodeConfigurationWarningData, isEditMode: boolean): boolean {
return Boolean(nodeData?.component?.error || nodeData?.composite?.error || nodeData?.trigger?.error) || isEditMode;
}

function applySidebarTabOnNodeOpen(
setCurrentTab: ((tab: ComponentSidebarTab) => void) | undefined,
wasSidebarOpen: boolean,
shouldOpenSettings: boolean,
): void {
if (!setCurrentTab) {
return;
}
if (!wasSidebarOpen) {
setCurrentTab(shouldOpenSettings ? "settings" : "latest");
return;
}
if (shouldOpenSettings) {
setCurrentTab("settings");
}
}

function CanvasContent({
state,
onNodeEdit,
Expand Down Expand Up @@ -2384,24 +2425,12 @@ function CanvasContent({
} else if (onNodeClick) {
onNodeClick(nodeId);
} else {
const wasSidebarOpen = stateRef.current.componentSidebar.isOpen;
stateRef.current.componentSidebar.open(nodeId);

const nodeData = clickedNode?.data as {
component?: { error?: string };
composite?: { error?: string };
trigger?: { error?: string };
} | null;
const hasConfigurationWarning = Boolean(
nodeData?.component?.error || nodeData?.composite?.error || nodeData?.trigger?.error,
);

if (setCurrentTab) {
setCurrentTab(hasConfigurationWarning || isEditMode ? "settings" : "latest");
}

if (onBuildingBlocksSidebarToggle) {
onBuildingBlocksSidebarToggle(false);
}
const nodeData = clickedNode?.data as NodeConfigurationWarningData;
applySidebarTabOnNodeOpen(setCurrentTab, wasSidebarOpen, shouldOpenSidebarSettingsTab(nodeData, isEditMode));
onBuildingBlocksSidebarToggle?.(false);
}

stateRef.current.setNodes((nodes) =>
Expand Down
110 changes: 70 additions & 40 deletions web_src/src/ui/componentSidebar/DocsTab.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { ConfigurationField } from "@/api-client";
import { BookOpen, ExternalLink } from "lucide-react";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import { PayloadPreview } from "@/ui/BuildingBlocksSidebar/PayloadPreview";

function ConfigTable({ fields }: { fields: ConfigurationField[] }) {
function ConfigTable({ fields, showTopBorder }: { fields: ConfigurationField[]; showTopBorder?: boolean }) {
return (
<div className="w-full px-4 pt-3 pb-3 border-t border-gray-200">
<div className={cn("w-full px-4 pt-3 pb-3", showTopBorder && "border-t border-slate-950/15")}>
<span className="text-[13px] font-medium text-gray-500">Configuration</span>
<div className="overflow-x-auto mt-2">
<table className="text-xs w-full border-collapse">
Expand Down Expand Up @@ -39,6 +41,54 @@ function ConfigTable({ fields }: { fields: ConfigurationField[] }) {
);
}

function DocsReferenceSection({ documentationUrl }: { documentationUrl: string }) {
return (
<div className="bg-slate-100 px-4 py-2.5">
<Button variant="outline" size="xs" asChild>
<a href={documentationUrl} target="_blank" rel="noopener noreferrer">
<BookOpen className="size-3" aria-hidden />
Docs reference
<ExternalLink className="size-3" aria-hidden />
</a>
</Button>
</div>
);
}

function DocsDescriptionSection({ description, showBottomBorder }: { description: string; showBottomBorder: boolean }) {
return (
<div className={cn("w-full px-4 pt-3 pb-3", showBottomBorder && "border-b border-slate-950/15")}>
<span className="text-[13px] font-medium text-gray-500">Description</span>
<p className="text-[13px] text-gray-800 mt-1 leading-relaxed">{description}</p>
</div>
);
}

function DocsPayloadSection({
examplePayload,
payloadLabel,
showBottomBorder,
}: {
examplePayload: Record<string, unknown>;
payloadLabel: string;
showBottomBorder: boolean;
}) {
return (
<div className={cn("w-full px-2 py-2", showBottomBorder && "border-b border-slate-950/15")}>
<div className="px-2">
<PayloadPreview
value={examplePayload}
label={payloadLabel}
dialogTitle={payloadLabel}
maxHeight="max-h-64"
showCopy
labelSize="md"
/>
</div>
</div>
);
}

interface DocsTabProps {
description?: string;
examplePayload?: Record<string, unknown>;
Expand All @@ -56,8 +106,10 @@ export function DocsTab({
configurationFields = [],
}: DocsTabProps) {
const hasPayload = examplePayload && Object.keys(examplePayload).length > 0;
const hasFollowingContent = Boolean(hasPayload || configurationFields.length > 0);
const hasContent = Boolean(description || hasPayload || configurationFields.length > 0 || documentationUrl);

if (!description && !hasPayload && configurationFields.length === 0 && !documentationUrl) {
if (!hasContent) {
return (
<div className="flex flex-col items-center justify-center py-16 px-6 text-center">
<p className="text-sm text-gray-500">No documentation available for this component.</p>
Expand All @@ -67,43 +119,21 @@ export function DocsTab({

return (
<div className="pb-8">
{documentationUrl && (
<div className="border-b border-gray-200 dark:border-gray-700 px-4 py-2.5">
<a
href={documentationUrl}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-[11px] text-gray-500 dark:text-gray-400 hover:text-primary transition-colors"
>
<BookOpen size={12} className="shrink-0" aria-hidden />
<span>Docs reference</span>
<ExternalLink size={10} className="shrink-0" aria-hidden />
</a>
</div>
)}
{description && (
<div className="w-full px-4 pt-3 pb-3">
<span className="text-[13px] font-medium text-gray-500">Description</span>
<p className="text-[13px] text-gray-800 mt-1 leading-relaxed">{description}</p>
</div>
)}

{hasPayload && (
<div className="w-full px-2 py-2 border-t border-gray-200">
<div className="px-2">
<PayloadPreview
value={examplePayload!}
label={payloadLabel}
dialogTitle={payloadLabel}
maxHeight="max-h-64"
showCopy
labelSize="md"
/>
</div>
</div>
)}

{configurationFields.length > 0 && <ConfigTable fields={configurationFields} />}
{documentationUrl ? <DocsReferenceSection documentationUrl={documentationUrl} /> : null}
{description ? <DocsDescriptionSection description={description} showBottomBorder={hasFollowingContent} /> : null}
{hasPayload ? (
<DocsPayloadSection
examplePayload={examplePayload!}
payloadLabel={payloadLabel}
showBottomBorder={configurationFields.length > 0}
/>
) : null}
{configurationFields.length > 0 ? (
<ConfigTable
fields={configurationFields}
showTopBorder={!hasPayload && Boolean(description || documentationUrl)}
/>
) : null}
</div>
);
}
37 changes: 37 additions & 0 deletions web_src/src/ui/componentSidebar/SettingsTab.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,40 @@ export const RendererCoverage: Story = {
},
render: () => <SettingsTabPlayground />,
};

function ReadOnlyConfigurationPlayground() {
return (
<SettingsTab
mode="edit"
nodeId="node_renderer_coverage_readonly"
nodeName="Renderer Coverage Demo"
configuration={settingsTabConfiguration}
configurationFields={settingsTabFields}
onSave={() => undefined}
domainId={STORY_DOMAIN_ID}
domainType={STORY_DOMAIN_TYPE}
integrationName="github"
integrationRef={STORY_INTEGRATION_REF}
integrations={STORY_INTEGRATIONS}
integrationDefinition={{
name: "github",
label: "GitHub",
icon: "github",
}}
autocompleteExampleObj={STORY_AUTOCOMPLETE_CONTEXT}
configurationSaveMode="manual"
readOnly={true}
/>
);
}

export const ReadOnlyConfiguration: Story = {
parameters: {
docs: {
description: {
story: "Read-only configuration view shown when the component sidebar is not editable.",
},
},
},
render: () => <ReadOnlyConfigurationPlayground />,
};
Loading
Loading