Skip to content

Commit 3d1593e

Browse files
committed
feat: render state graph infos in main component
1 parent 4128803 commit 3d1593e

File tree

7 files changed

+114
-28
lines changed

7 files changed

+114
-28
lines changed

internal/runner/stategraph/build.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,7 @@ func BuildGraphFromState(data []byte) ([]byte, error) {
7474
Type: rsrc.Type,
7575
Name: rsrc.Name,
7676
Module: rsrc.Module,
77-
Provider: rsrc.Provider,
77+
Provider: cleanProvider(rsrc.Provider),
7878
}
7979
groups[base] = g
8080
byBase[base] = g.ID
@@ -243,6 +243,41 @@ func normalizeIndex(addr string) string {
243243
return addr[:i] + "[\"" + inner + "\"]"
244244
}
245245

246+
// cleanProvider extracts a short provider name from Terraform provider
247+
// address strings which usually look like:
248+
//
249+
// provider["registry.terraform.io/hashicorp/random"]
250+
//
251+
// It can also be other formats, e.g.:
252+
//
253+
// provider["random"]
254+
// provider['random']
255+
// provider[registry.terraform.io/hashicorp/random]
256+
//
257+
// ...
258+
func cleanProvider(in string) string {
259+
if in == "" {
260+
return ""
261+
}
262+
// Look for content inside the brackets: provider["..."]
263+
// We'll accept both single and double quotes.
264+
// First, find the first '[' and the last ']'.
265+
i := strings.Index(in, "[")
266+
j := strings.LastIndex(in, "]")
267+
var inside string
268+
if i >= 0 && j > i {
269+
inside = in[i+1 : j]
270+
inside = strings.Trim(inside, "'\" ")
271+
}
272+
// If we found something like registry.terraform.io/hashicorp/random
273+
// return the full path
274+
if inside != "" {
275+
return inside
276+
}
277+
// Fallback: return the trimmed original string (remove surrounding quotes/space)
278+
return strings.Trim(in, "'\" ")
279+
}
280+
246281
// uniqueEdges removes duplicate edges while preserving order.
247282
func uniqueEdges(in []Edge) []Edge {
248283
seen := make(map[string]struct{}, len(in))

ui/src/components/tools/LayerStateGraph.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,20 @@ import { fetchLayer, fetchStateGraph } from '@/clients/layers/client';
44
import { useQuery } from '@tanstack/react-query';
55
import ReactFlowView from './ReactFlowView';
66
import { buildReactFlow, type ReactFlowGraph } from '@/utils/stateGraph';
7+
import { StateGraphNode } from "@/clients/layers/types";
78

89
export interface LayerStateGraphProps {
910
variant?: 'light' | 'dark';
1011
namespace: string;
1112
name: string;
13+
onNodeClick?: (n: StateGraphNode) => void;
1214
}
1315

1416
const LayerStateGraph: React.FC<LayerStateGraphProps> = ({
1517
variant = 'light',
1618
namespace,
17-
name
19+
name,
20+
onNodeClick,
1821
}) => {
1922
const layerQuery = useQuery({
2023
queryKey: reactQueryKeys.layer(namespace, name),
@@ -92,7 +95,11 @@ const LayerStateGraph: React.FC<LayerStateGraphProps> = ({
9295

9396
return (
9497
<div className="h-full w-full">
95-
<ReactFlowView rf={rf} variant={variant} />
98+
<ReactFlowView
99+
rf={rf}
100+
variant={variant}
101+
onNodeClick={(id) => onNodeClick && onNodeClick(stateGraphQuery.data!.nodes.find(n => n.id === id)!)}
102+
/>
96103
</div>
97104
);
98105
};

ui/src/components/tools/ReactFlowView.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import React, { useEffect, useMemo } from 'react';
22
import ReactFlow, {
33
Background,
44
Controls,
5-
MiniMap,
65
MarkerType,
76
useEdgesState,
87
useNodesState,

ui/src/components/tools/ResourceNode.tsx

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ const ResourceNode: React.FC<ResourceNodeProps> = ({ data }) => {
1818
const count = data.count || 0;
1919

2020
return (
21-
<div className="rounded-xl border border-slate-300 bg-white px-3 py-2 shadow-sm relative">
21+
<div className="rounded-sm border border-slate-300 bg-white px-3 py-2 shadow-sm relative">
2222
{/* Provide handles on all sides with stable ids for edge anchoring */}
2323
<Handle
2424
id="left"
@@ -48,22 +48,21 @@ const ResourceNode: React.FC<ResourceNodeProps> = ({ data }) => {
4848
isConnectable={false}
4949
style={{ opacity: 0 }}
5050
/>
51-
<div className="text-[10px] tracking-wide text-slate-500">{data.type}</div>
52-
<div className="text-sm text-slate-800 font-medium flex items-center gap-2">
51+
<div className="text-[10px] tracking-wide text-primary-200">{data.type}</div>
52+
<div className="text-sm text-nuances-black text-lg font-semibold flex items-center gap-2">
5353
<span className="truncate max-w-[200px]" title={data.name}>
5454
{data.name}
5555
</span>
56-
<span className="ml-auto inline-flex items-center gap-1">
57-
{count > 1 && (
58-
<span
59-
className="inline-flex items-center justify-center h-5 min-w-[20px] px-1 rounded-full bg-sky-100 text-sky-700 text-[10px] font-semibold"
60-
title={`${count} instances`}
61-
>
62-
{count}
63-
</span>
64-
)}
65-
</span>
6656
</div>
57+
{count > 1 && (
58+
<span
59+
className="absolute -top-2 -right-2 inline-flex items-center justify-center h-5 min-w-[20px] px-1 rounded-full text-primary-100 bg-nuances-black text-[10px] font-semibold shadow"
60+
title={`${count} instances`}
61+
aria-label={`${count} instances`}
62+
>
63+
{count}
64+
</span>
65+
)}
6766
</div>
6867
);
6968
};

ui/src/modals/SlidingPane.tsx

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -46,30 +46,30 @@ const SlidingPane: React.FC<SlidingPaneProps> = ({
4646
<>
4747
{/* Background */}
4848
<div
49-
className={`fixed inset-0 flex bg-nuances-400 bg-opacity-50 z-9 duration-300 ease-in-out ${
50-
isOpen ? 'opacity-100 visible' : 'opacity-0 invisible'
49+
className={`fixed inset-0 bg-nuances-400/50 z-40 transition-opacity duration-300 ease-in-out ${
50+
isOpen ? 'opacity-100 visible' : 'opacity-0 invisible pointer-events-none'
5151
}`}
5252
onClick={onClose}
5353
aria-hidden={!isOpen}
54-
></div>
54+
/>
5555

5656
{/* Sliding Pane */}
5757
<FocusLock disabled={!isOpen}>
5858
<div
59-
className={`fixed top-0 right-0 h-screen z-10 shadow-lg transform transition-transform duration-300 ease-in-out ${
59+
className={`fixed top-0 right-0 h-screen z-50 shadow-lg transform transition-transform duration-300 ease-in-out ${
6060
isOpen ? 'translate-x-0' : 'translate-x-full'
6161
} ${width} ${variant === 'light' ? 'bg-primary-100' : 'bg-nuances-black'}`}
6262
>
6363
{/* Close Button */}
64-
<button
64+
<button
6565
aria-label="Close"
66-
className={`absolute top-4 right-8 text-2xl focus:outline-hidden ${
66+
className={`absolute top-4 right-8 text-2xl focus:outline-hidden cursor-pointer ${
6767
variant === 'light' ? 'text-gray-600' : 'text-nuances-50'
6868
}`}
6969
onClick={onClose}
70-
>
70+
>
7171
&times;
72-
</button>
72+
</button>
7373
{/* Content */}
7474
<div className="p-8 pt-12 overflow-y-auto h-full">{children}</div>
7575
</div>

ui/src/pages/Layer.tsx

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@ import { reactQueryKeys } from '@/clients/reactQueryConfig';
99
import { fetchLayer, syncLayer } from '@/clients/layers/client';
1010
import LayerStatus from '@/components/status/LayerStatus';
1111
import Button from '@/components/core/Button';
12-
import type { Layer } from '@/clients/layers/types';
12+
import type { Layer, StateGraphNode } from '@/clients/layers/types';
13+
import SlidingPane from '@/modals/SlidingPane';
1314

1415
const Layer: React.FC = () => {
1516
const { theme } = useContext(ThemeContext);
1617
const { namespace = '', name = '' } = useParams();
1718
const navigate = useNavigate();
18-
19+
const [showResourcePane, setShowResourcePane] = useState(false);
20+
const [selectedResourceData, setSelectedResourceData] = useState<StateGraphNode | null>(null);
1921
const [isManualSyncPending, setIsManualSyncPending] = useState<boolean>(false);
2022
const [isRefreshing, setIsRefreshing] = useState<boolean>(false);
2123

@@ -67,6 +69,46 @@ const Layer: React.FC = () => {
6769

6870
return (
6971
<div className="flex flex-col flex-1 h-screen min-w-0">
72+
<SlidingPane
73+
isOpen={showResourcePane}
74+
onClose={() => setShowResourcePane(false)}
75+
variant={theme}
76+
>
77+
<div className="p-6">
78+
<h2 className="text-2xl font-semibold mb-2">{selectedResourceData?.name}</h2>
79+
<h3 className="text-sm mb-2 uppercase">{selectedResourceData?.type}</h3>
80+
<p className="text-sm mb-2">Provider: <span className="font-medium">{selectedResourceData?.provider}</span></p>
81+
<p className="text-sm mb-2">Instance count: <span className="font-medium">{selectedResourceData?.instances_count}</span></p>
82+
{ selectedResourceData?.module !== undefined && <p className="text-sm mb-2">Module: <span className="font-medium">{selectedResourceData?.module || '(root)'}</span></p>}
83+
<p className="text-sm mb-2">Address: <span className="font-medium">{selectedResourceData?.addr}</span></p>
84+
<h3 className="text-lg font-semibold mt-4 mb-2">Instance{ (selectedResourceData?.instances_count ?? 0) > 1 ? 's' : '' } details</h3>
85+
<ul className="list-disc list-inside">
86+
{selectedResourceData?.instances?.map((inst) => (
87+
<li key={inst.addr} className="mb-2">
88+
<p className="text-sm">Address: <span className="font-medium">{inst.addr}</span></p>
89+
{ inst.created_at && <p className="text-sm">Created at: <span className="font-medium">{new Date(inst.created_at).toLocaleString()}</span></p> }
90+
{ inst.dependencies && inst.dependencies.length > 0 && (
91+
<p className="text-sm">Dependencies: <span className="font-medium">{inst.dependencies.join(', ')}</span></p>
92+
) }
93+
{ inst.attributes && (
94+
<details className="mt-1">
95+
<summary className="cursor-pointer text-sm text-primary-500">View attributes</summary>
96+
<pre className="bg-nuances-white p-2 rounded mt-1 overflow-auto text-xs text-nuances-black">
97+
{JSON.stringify(inst.attributes, null,
98+
99+
2)}
100+
</pre>
101+
</details>
102+
) }
103+
</li>
104+
)) }
105+
</ul>
106+
<h3 className="text-lg font-semibold mt-4 mb-2">Raw data</h3>
107+
<pre className="bg-nuances-white p-4 rounded-lg overflow-auto text-sm text-nuances-black">
108+
{JSON.stringify(selectedResourceData, null, 2)}
109+
</pre>
110+
</div>
111+
</SlidingPane>
70112
<div
71113
className={`
72114
p-6
@@ -110,6 +152,10 @@ const Layer: React.FC = () => {
110152
namespace={namespace}
111153
name={name}
112154
variant={theme === 'light' ? 'light' : 'dark'}
155+
onNodeClick={(n) => { setShowResourcePane(true)
156+
setSelectedResourceData(n);
157+
console.log('Clicked node', n);
158+
} }
113159
/>
114160
</div>
115161
</div>

ui/src/pages/Logs.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useContext, useCallback, useMemo, useState, useEffect } from 'react';
1+
import React, { useContext, useCallback, useMemo, useState } from 'react';
22
import { useQuery } from '@tanstack/react-query';
33
import { useParams, useSearchParams, useNavigate } from 'react-router-dom';
44

0 commit comments

Comments
 (0)