Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Graph Navigator #603

Open
wants to merge 20 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions packages/graph-editor/src/components/panels/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ export * from './nodeSettings/index.js';
export * from './output/index.js';
export * from './play/index.js';
export * from './settings/index.js';
export * from './navigation/index.js';
109 changes: 109 additions & 0 deletions packages/graph-editor/src/components/panels/navigation/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React from 'react';

import { MAIN_GRAPH_ID } from '@/constants.js';
import { Stack } from '@tokens-studio/ui';
import { TreeNode, graphNodesSelector } from '@/redux/selectors/graph.js';
import { currentPanelIdSelector } from '@/redux/selectors/graph.js';
import { dockerSelector } from '@/redux/selectors/refs.js';
import { flow, get, size } from 'lodash-es';
import { useSelector } from 'react-redux';
import { useSubgraphExplorerCallback } from '@/hooks/useSubgraphExplorerCallback.js';
import styles from './index.module.css';

type ListItemProps = {
label: string;
count?: number;
isSelected?: boolean;
onClick?: () => void;
depth?: number;
};

const ListItem = function ({
label,
count,
isSelected,
onClick,
depth = 0,
}: ListItemProps) {
const style = {
'--tree-depth': depth,
fontWeight: isSelected && 'bold',
floscr marked this conversation as resolved.
Show resolved Hide resolved
} as React.CSSProperties;

return (
<li className={styles.listItem} onClick={onClick} style={style}>
<span>{label}</span>
{count && <span className={styles.listItemCount}>({count})</span>}
</li>
);
};

const SubgraphNodeItem = function ({ node, isSelected, depth }) {
const nodeType = node.factory.title || node.nodeType();
const onNodeClick = useSubgraphExplorerCallback(node);
const childNodesCount = flow(
(x) => get(x, ['_innerGraph', 'nodes'], []),
size,
)(node);

return (
<ListItem
label={nodeType}
onClick={onNodeClick}
isSelected={isSelected}
depth={depth}
count={childNodesCount > 0 && childNodesCount}
/>
);
};

const RootGraphNodeItem = function () {
const dockerRef = useSelector(dockerSelector);
const activeGraphId = useSelector(currentPanelIdSelector);

return (
<ListItem
label={'Root'}
isSelected={activeGraphId === MAIN_GRAPH_ID}
onClick={() => dockerRef.current.updateTab(MAIN_GRAPH_ID, null, true)}
/>
);
};

export const NavigationPanel = () => {
const nodes = useSelector(graphNodesSelector);
const activeGraphId = useSelector(currentPanelIdSelector);

return (
<Stack
direction="column"
gap={4}
style={{
floscr marked this conversation as resolved.
Show resolved Hide resolved
height: '100%',
flex: 1,
padding: 'var(--component-spacing-md)',
overflow: 'auto',
}}
>
<div style={{ padding: 'var(--component-spacing-md)' }}>
<ul className={styles.listWrapper}>
<RootGraphNodeItem />
{Object.values(nodes || {}).map(({ node, depth }: TreeNode) => {
const innerGraph = node['_innerGraph'];
if (!innerGraph) return null;
Comment on lines +89 to +90
Copy link
Author

@floscr floscr Jan 3, 2025

Choose a reason for hiding this comment

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

I don't know how we can get this type out of packages/graph-engine/src/nodes/generic/subgraph.ts so we can do a proper typescript type check.

I would have liked to do a check like

if (node instanceof SubgraphNode) {
  const innerGraph = node._innerGraph;
  if (!innerGraph) return null;
}

but I'm not sure how importing for types/classes works for such packages

return (
<SubgraphNodeItem
key={node.id}
isSelected={
activeGraphId === innerGraph?.annotations['engine.id']
}
node={node}
depth={depth}
/>
);
})}
</ul>
</div>
</Stack>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@ export const LayoutDropdown = () => {
<DropdownMenu.Item onSelect={() => onClick('dropPanel')}>
Nodes
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={() => onClick('navigationPanel')}>
Navigator
</DropdownMenu.Item>

<DropdownMenu.Separator />
<DropdownMenu.Item onSelect={saveLayout}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { GraphPanel } from '../panels/graph/index.js';
import { Inputsheet } from '../panels/inputs/index.js';
import { Legend } from '../panels/legend/index.js';
import { LogsPanel } from '../panels/logs/index.js';
import { NavigationPanel } from '../panels/navigation/index.js';
import { NodeSettingsPanel } from '../panels/nodeSettings/index.js';
import { OutputSheet } from '../panels/output/index.js';
import { Settings } from '../panels/settings/index.js';
Expand Down Expand Up @@ -55,6 +56,11 @@ export const layoutButtons = {
title: 'Nodes',
content: <DropPanel />,
},
navigationPanel: {
id: 'navigationPanel',
title: 'Navigation',
content: <NavigationPanel />,
},
};

export type LayoutButtons = keyof typeof layoutButtons;
2 changes: 1 addition & 1 deletion packages/graph-editor/src/data/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export const version = '4.3.9';
export const version = '4.3.6';
14 changes: 14 additions & 0 deletions packages/graph-editor/src/editor/graphEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { EditorApp } from './graph.js';
import { ErrorBoundary } from 'react-error-boundary';
import { ErrorBoundaryContent } from '@/components/ErrorBoundaryContent.js';
import { GraphEditorProps, ImperativeEditorRef } from './editorTypes.js';
import { ReactFlowProvider } from 'reactflow';
import React from 'react';
Expand All @@ -16,3 +18,15 @@ export const GraphEditor = React.forwardRef<
</ReactFlowProvider>
);
});

// HACK: Workaround for circular dependency not allowed for nextjs
// E.g.: when trying to create a new graph editor instance as a tab
if (typeof window !== 'undefined') {
window['newGraphEditor'] = function (ref, id) {
return (
<ErrorBoundary fallback={<ErrorBoundaryContent />}>
<GraphEditor ref={ref} id={id} />
</ErrorBoundary>
);
};
}
Comment on lines +24 to +32
Copy link
Author

Choose a reason for hiding this comment

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

NextJS was erroring because there's a circular dependency.
I think this is not too bad if we have some interface for global methods.

48 changes: 48 additions & 0 deletions packages/graph-editor/src/hooks/useSubgraphExplorerCallback.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { ImperativeEditorRef } from '../index.js';
import { title as annotatedTitle } from '@/annotations/index.js';
import { dockerSelector } from '@/redux/selectors/refs.js';
import { useCallback } from 'react';
import { useSelector } from 'react-redux';

export const useSubgraphExplorerCallback = (node) => {
const dockerRef = useSelector(dockerSelector);

const callback = useCallback(() => {
if (!dockerRef?.current) {
return;
}

let oneShot = false;
const innerGraph = node['_innerGraph'];
const graphId = innerGraph.annotations['engine.id'];
const title =
node.annotations[annotatedTitle] ||
innerGraph.annotations['engine.title'] ||
'Subgraph';
const existing = dockerRef.current.find(graphId);

if (!existing) {
const ref = (o: ImperativeEditorRef) => {
if (o && !oneShot) {
o.load(innerGraph);
oneShot = true;
}
};

const newTab = {
cached: true,
closable: true,
id: graphId,
group: 'graph',
title,
content: window && window['newGraphEditor'](ref, graphId),
};
console.log(newTab);
dockerRef.current.dockMove(newTab, 'graphs', 'middle');
} else {
dockerRef.current.updateTab(graphId, null, true);
}
}, [dockerRef, node['_innerGraph'], node.annotations]);

return callback;
};
31 changes: 31 additions & 0 deletions packages/graph-editor/src/redux/selectors/graph.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { Graph, Node } from '@tokens-studio/graph-engine';
import { MAIN_GRAPH_ID } from '@/constants.js';
import { createSelector } from 'reselect';
import { graph } from './roots.js';
Expand All @@ -18,6 +19,36 @@ export const mainGraphSelector = createSelector(
(state) => state.panels[MAIN_GRAPH_ID],
);

export type TreeNode = {
node: Node;
depth: number;
};

const collectNodes = function (
graph: Graph,
coll: Record<string, TreeNode> = {},
depth = 1,
): Record<string, TreeNode> {
for (const id in graph.nodes) {
const node: Node = graph.nodes[id];
const innerGraph = node['_innerGraph'];
coll[id] = { node, depth } as TreeNode;

if (innerGraph) {
collectNodes(innerGraph, coll, ++depth);
}
}
return coll;
};

export const graphNodesSelector = createSelector(graph, (state) => {
const graph = state.panels[MAIN_GRAPH_ID]?.graph;

if (!graph) return;

return collectNodes(graph);
});

export const graphEditorSelector = createSelector(
graph,
(state) => state.currentPanel?.ref,
Expand Down
33 changes: 19 additions & 14 deletions packages/graph-editor/src/registry/specifics.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,31 @@ import { useSelector } from 'react-redux';
import Eye from '@tokens-studio/icons/Eye.js';
import React, { useCallback } from 'react';

const SubgraphExplorer = ({ node }) => {
export const useSubgraphExplorerCallback = (node) => {
const dockerRef = useSelector(dockerSelector);
const onToggle = useCallback(() => {

const callback = useCallback(() => {
if (!dockerRef?.current) {
return;
}

let oneShot = false;
const innerGraph = node._innerGraph;
const innerGraph = node['_innerGraph'];
const graphId = innerGraph.annotations['engine.id'];
const title =
node.annotations[annotatedTitle] ||
innerGraph.annotations['engine.title'] ||
'Subgraph';
//Find the container
const existing = dockerRef.current.find(graphId);

const ref = (o: ImperativeEditorRef) => {
if (o && !oneShot) {
o.load(innerGraph);
oneShot = true;
}
};

if (!existing) {
const ref = (o: ImperativeEditorRef) => {
if (o && !oneShot) {
o.load(innerGraph);
oneShot = true;
}
};

const newTab = {
cached: true,
closable: true,
Expand All @@ -53,15 +53,20 @@ const SubgraphExplorer = ({ node }) => {
</ErrorBoundary>
),
};

dockerRef.current.dockMove(newTab, 'graphs', 'middle');
} else {
dockerRef.current.updateTab(graphId, null, true);
}
}, [dockerRef, node._innerGraph, node.annotations]);
}, [dockerRef, node['_innerGraph'], node.annotations]);

return callback;
};

const SubgraphExplorer = ({ node }: { node: Node }) => {
const onClick = useSubgraphExplorerCallback(node);

return (
<Button emphasis="high" icon={<Eye />} onClick={onToggle}>
<Button emphasis="high" icon={<Eye />} onClick={onClick}>
Subgraph Explorer
</Button>
);
Expand Down
Loading