diff --git a/apps/wing-console/console/app/demo/main.w b/apps/wing-console/console/app/demo/main.w
index 035874567ca..f6ca4555b7d 100644
--- a/apps/wing-console/console/app/demo/main.w
+++ b/apps/wing-console/console/app/demo/main.w
@@ -165,10 +165,12 @@ test "Add fixtures" {
class WidgetService {
data: cloud.Bucket;
counter: cloud.Counter;
+ bucket: myBucket;
new() {
this.data = new cloud.Bucket();
this.counter = new cloud.Counter();
+ this.bucket = new myBucket() as "MyInternalBucket";
// a field displays a labeled value, with optional refreshing
new ui.Field(
diff --git a/apps/wing-console/console/design-system/src/headless/tree-item.tsx b/apps/wing-console/console/design-system/src/headless/tree-item.tsx
index e89df902931..e30b4891ead 100644
--- a/apps/wing-console/console/design-system/src/headless/tree-item.tsx
+++ b/apps/wing-console/console/design-system/src/headless/tree-item.tsx
@@ -121,6 +121,12 @@ export const TreeItem = ({
});
const canBeExpanded = !!children;
+ useEffect(() => {
+ if (selected) {
+ ref.current?.scrollIntoView();
+ }
+ }, [selected, ref]);
+
return (
void;
color?: string;
icon?: string;
+ collapsed?: boolean;
+ onCollapse?: (value: boolean) => void;
}
const Wrapper: FunctionComponent> = memo(
- ({ name, fqn, highlight, onClick, children, color, icon }) => {
+ ({
+ name,
+ fqn,
+ highlight,
+ onClick,
+ collapsed = false,
+ onCollapse = (value: boolean) => {},
+ children,
+ color,
+ icon,
+ }) => {
+ /* eslint-disable jsx-a11y/no-static-element-interactions */
+ /* eslint-disable jsx-a11y/click-events-have-key-events */
return (
- // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
> = memo(
0 &&
"border-b border-slate-200 dark:border-slate-800",
+ "cursor-pointer",
)}
>
> = memo(
>
{name}
+
+
{
+ onCollapse(!collapsed);
+ }}
+ >
+ {collapsed && (
+
+ )}
+ {!collapsed && (
+
+ )}
+
+
-
- {children}
+ {!collapsed && children}
);
},
@@ -113,6 +146,8 @@ interface ContainerNodeProps {
resourceType?: string;
highlight?: boolean;
onClick?: () => void;
+ collapsed?: boolean;
+ onCollapse?: (value: boolean) => void;
color?: string;
icon?: string;
}
@@ -136,6 +171,8 @@ const ContainerNode: FunctionComponent> =
fqn={props.resourceType!}
highlight={props.highlight}
onClick={props.onClick}
+ onCollapse={props.onCollapse}
+ collapsed={props.collapsed}
color={props.color}
icon={props.icon}
>
@@ -164,6 +201,8 @@ interface ConstructNodeProps {
hasChildNodes?: boolean;
onSelectedNodeIdChange: (id: string | undefined) => void;
color?: string;
+ onCollapse: (value: boolean) => void;
+ collapsed: boolean;
icon?: string;
}
@@ -179,6 +218,8 @@ const ConstructNode: FunctionComponent> =
children,
hasChildNodes,
color,
+ onCollapse,
+ collapsed,
icon,
}) => {
const select = useCallback(
@@ -245,6 +286,7 @@ const ConstructNode: FunctionComponent> =
/>
{!hasChildNodes && (
+ // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
> =
>
{name}
+ {collapsed && (
+
{
+ if (collapsed) {
+ onCollapse(false);
+ }
+ }}
+ >
+
+
+ )}
)}
@@ -334,6 +393,8 @@ const ConstructNode: FunctionComponent> =
resourceType={fqn}
highlight={highlight}
onClick={select}
+ onCollapse={onCollapse}
+ collapsed={collapsed}
color={color}
icon={icon}
>
@@ -541,6 +602,9 @@ export interface MapViewV2Props {
onSelectedNodeIdChange: (id: string | undefined) => void;
selectedEdgeId?: string;
onSelectedEdgeIdChange?: (id: string | undefined) => void;
+ onExpand: (path: string) => void;
+ onCollapse: (path: string) => void;
+ expandedItems: string[];
}
export const MapView = memo(
@@ -549,8 +613,13 @@ export const MapView = memo(
onSelectedNodeIdChange,
selectedEdgeId,
onSelectedEdgeIdChange,
+ onExpand,
+ onCollapse,
+ expandedItems,
}: MapViewV2Props) => {
- const { nodeInfo, isNodeHidden, rootNodes, edges } = useMap({});
+ const { nodeInfo, isNodeHidden, rootNodes, edges } = useMap({
+ expandedItems,
+ });
const RenderEdge = useCallback(
(props) => {
@@ -581,40 +650,53 @@ export const MapView = memo(
}>
>(
(props) => {
- if (isNodeHidden(props.constructTreeNode.path)) {
+ const node = props.constructTreeNode;
+ if (isNodeHidden(node.path)) {
return <>>;
}
- const info = nodeInfo?.get(props.constructTreeNode.path);
+ const info = nodeInfo?.get(node.path);
if (!info) {
return <>>;
}
- const childNodes = Object.values(
- props.constructTreeNode.children ?? {},
- ).filter((node) => !isNodeHidden(node.path));
+ const childNodes = Object.values(node.children ?? {}).filter(
+ (node) => !isNodeHidden(node.path),
+ );
- const fqn = props.constructTreeNode.constructInfo?.fqn;
+ const fqn = node.constructInfo?.fqn;
const cloudResourceType = fqn?.split(".").at(-1);
const name =
- props.constructTreeNode.display?.title === cloudResourceType
- ? props.constructTreeNode.id
- : props.constructTreeNode.display?.title ??
- props.constructTreeNode.id;
+ node.display?.title === cloudResourceType
+ ? node.id
+ : node.display?.title ?? node.id;
+
+ const children = Object.values(node.children ?? {});
+ const canBeExpanded =
+ !!node.children && children.some((child) => !child.display?.hidden);
+ const collapsed = canBeExpanded && !expandedItems.includes(node.path);
return (
0}
+ collapsed={collapsed}
+ onCollapse={(collapse) => {
+ if (collapse) {
+ onCollapse(node.path);
+ } else {
+ onExpand(node.path);
+ }
+ }}
>
{childNodes.map((child) => (
);
},
- [isNodeHidden, nodeInfo],
+ [isNodeHidden, nodeInfo, onCollapse, onExpand, expandedItems],
);
const { theme } = useTheme();
diff --git a/apps/wing-console/console/ui/src/layout/default-layout.tsx b/apps/wing-console/console/ui/src/layout/default-layout.tsx
index 1ad8578e8fa..9d5174937b5 100644
--- a/apps/wing-console/console/ui/src/layout/default-layout.tsx
+++ b/apps/wing-console/console/ui/src/layout/default-layout.tsx
@@ -77,6 +77,7 @@ export const DefaultLayout = ({
expandedItems,
setExpandedItems,
expand,
+ collapse,
expandAll,
collapseAll,
theme,
@@ -261,7 +262,7 @@ export const DefaultLayout = ({
return (
{panelComponent}
@@ -300,6 +301,9 @@ export const DefaultLayout = ({
onSelectedNodeIdChange={setSelectedItemSingle}
selectedEdgeId={selectedEdgeId}
onSelectedEdgeIdChange={setSelectedEdgeId}
+ onExpand={expand}
+ onCollapse={collapse}
+ expandedItems={expandedItems}
/>
{!layout.rightPanel?.hide && (
diff --git a/apps/wing-console/console/ui/src/layout/use-layout.tsx b/apps/wing-console/console/ui/src/layout/use-layout.tsx
index bd2e84b9caa..db27f0e4e52 100644
--- a/apps/wing-console/console/ui/src/layout/use-layout.tsx
+++ b/apps/wing-console/console/ui/src/layout/use-layout.tsx
@@ -21,6 +21,7 @@ export const useLayout = ({ cloudAppState }: UseLayoutProps) => {
expandedItems,
setExpandedItems,
expand,
+ collapse,
expandAll,
collapseAll,
} = useExplorer();
@@ -98,6 +99,7 @@ export const useLayout = ({ cloudAppState }: UseLayoutProps) => {
expandedItems,
setExpandedItems,
expand,
+ collapse,
expandAll,
collapseAll,
theme,
diff --git a/apps/wing-console/console/ui/src/services/use-explorer.tsx b/apps/wing-console/console/ui/src/services/use-explorer.tsx
index 357b8a6f1d6..de897acf893 100644
--- a/apps/wing-console/console/ui/src/services/use-explorer.tsx
+++ b/apps/wing-console/console/ui/src/services/use-explorer.tsx
@@ -37,6 +37,7 @@ export const useExplorer = () => {
expandedItems,
setExpandedItems,
expand,
+ collapse,
expandAll,
collapseAll,
} = useTreeMenuItems({
@@ -83,10 +84,6 @@ export const useExplorer = () => {
setSelectedItems([selectedNode]);
}, [selectedNode, setSelectedItems]);
- useEffect(() => {
- expandAll();
- }, [items, expandAll]);
-
return {
items,
selectedItems,
@@ -94,6 +91,7 @@ export const useExplorer = () => {
expandedItems,
setExpandedItems,
expand,
+ collapse,
expandAll,
collapseAll,
};
diff --git a/apps/wing-console/console/ui/src/services/use-map.ts b/apps/wing-console/console/ui/src/services/use-map.ts
index 54f13aa6147..1fb7009f98b 100644
--- a/apps/wing-console/console/ui/src/services/use-map.ts
+++ b/apps/wing-console/console/ui/src/services/use-map.ts
@@ -108,9 +108,11 @@ const getNodeInflights = (
}));
};
-export interface UseMapOptions {}
+export interface UseMapOptions {
+ expandedItems: string[];
+}
-export const useMap = ({}: UseMapOptions = {}) => {
+export const useMap = ({ expandedItems }: UseMapOptions) => {
const query = trpc["app.map"].useQuery();
const { tree: rawTree, connections: rawConnections } = query.data ?? {};
@@ -160,9 +162,16 @@ export const useMap = ({}: UseMapOptions = {}) => {
const hiddenMap = new Map();
const traverse = (node: ConstructTreeNode, forceHidden?: boolean) => {
const hidden = forceHidden || node.display?.hidden || false;
+
hiddenMap.set(node.path, hidden);
- for (const child of Object.values(node.children ?? {})) {
- traverse(child, hidden);
+
+ const children = Object.values(node.children ?? {});
+ const canBeExpanded =
+ !!node.children && children.some((child) => !child.display?.hidden);
+ const collapsed = canBeExpanded && !expandedItems.includes(node.path);
+
+ for (const child of children) {
+ traverse(child, hidden || collapsed);
}
};
const pseudoRoot = rawTree?.children?.["Default"];
@@ -170,7 +179,7 @@ export const useMap = ({}: UseMapOptions = {}) => {
traverse(child!);
}
return hiddenMap;
- }, [rawTree]);
+ }, [rawTree, expandedItems]);
const isNodeHidden = useCallback(
(path: string) => {
diff --git a/apps/wing-console/console/ui/src/ui/map-controls.tsx b/apps/wing-console/console/ui/src/ui/map-controls.tsx
index 7c724ac9e08..c7c30fdb77e 100644
--- a/apps/wing-console/console/ui/src/ui/map-controls.tsx
+++ b/apps/wing-console/console/ui/src/ui/map-controls.tsx
@@ -23,15 +23,15 @@ export const MapControls = ({
diff --git a/apps/wing-console/console/ui/src/ui/resource-metadata.tsx b/apps/wing-console/console/ui/src/ui/resource-metadata.tsx
index 0c155468164..fd552c35daa 100644
--- a/apps/wing-console/console/ui/src/ui/resource-metadata.tsx
+++ b/apps/wing-console/console/ui/src/ui/resource-metadata.tsx
@@ -1,10 +1,5 @@
+import { CubeIcon } from "@heroicons/react/20/solid";
import {
- CubeIcon,
- ArrowLeftOnRectangleIcon,
- ArrowRightOnRectangleIcon,
-} from "@heroicons/react/20/solid";
-import {
- ArrowPathRoundedSquareIcon,
CubeTransparentIcon,
CursorArrowRaysIcon,
} from "@heroicons/react/24/outline";
@@ -17,7 +12,6 @@ import {
getResourceIconComponent,
Attribute,
ScrollableArea,
- getResourceIconColors,
} from "@wingconsole/design-system";
import type { NodeDisplay } from "@wingconsole/server";
import classNames from "classnames";
@@ -216,7 +210,7 @@ export const ResourceMetadata = memo(
resourceGroup,
connectionsGroups: connectionsGroupsArray,
};
- }, [node, inbound, outbound]);
+ }, [node, inbound, outbound, icon]);
const nodeLabel = useMemo(() => {
const cloudResourceTypeName = node.type.split(".").at(-1) || "";
diff --git a/apps/wing-console/console/ui/src/ui/use-tree-menu-items.tsx b/apps/wing-console/console/ui/src/ui/use-tree-menu-items.tsx
index 4dfefd92d16..1dc481ccf8e 100644
--- a/apps/wing-console/console/ui/src/ui/use-tree-menu-items.tsx
+++ b/apps/wing-console/console/ui/src/ui/use-tree-menu-items.tsx
@@ -22,6 +22,7 @@ export function useTreeMenuItems(options?: {
const [expandedItems, setExpandedItems] = useState(
options?.openMenuItemIds ?? [],
);
+
const toggle = useCallback((itemId: string) => {
setExpandedItems(([...openedMenuItems]) => {
const index = openedMenuItems.indexOf(itemId);
@@ -70,6 +71,18 @@ export function useTreeMenuItems(options?: {
});
}, []);
+ const collapse = useCallback((itemId: string) => {
+ setExpandedItems(([...openedMenuItems]) => {
+ const index = openedMenuItems.indexOf(itemId);
+ if (index === -1) {
+ return openedMenuItems;
+ }
+
+ openedMenuItems.splice(index, 1);
+ return openedMenuItems;
+ });
+ }, []);
+
return {
items,
setItems,
@@ -81,5 +94,6 @@ export function useTreeMenuItems(options?: {
expandAll,
collapseAll,
expand,
+ collapse,
};
}
diff --git a/apps/wing-console/console/ui/src/ui/zoom-pane.tsx b/apps/wing-console/console/ui/src/ui/zoom-pane.tsx
index 439f886a676..07abaa1185b 100644
--- a/apps/wing-console/console/ui/src/ui/zoom-pane.tsx
+++ b/apps/wing-console/console/ui/src/ui/zoom-pane.tsx
@@ -395,8 +395,7 @@ export const ZoomPane = forwardRef((props, ref) => {