Skip to content
This repository was archived by the owner on Feb 6, 2026. It is now read-only.
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
12 changes: 9 additions & 3 deletions app/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@
"date-fns": "^2.29.2",
"floating-vue": "^2.0.0-beta.16",
"fontfaceobserver": "^2.3.0",
"graphology": "^0.25.4",
"graphology-layout": "^0.6.1",
"graphology-layout-forceatlas2": "^0.10.1",
"graphology-layout-noverlap": "^0.4.2",
"is-promise": "^4.0.0",
"javascript-time-ago": "^2.5.7",
"js-base64": "^3.7.5",
Expand All @@ -70,6 +74,7 @@
"plur": "^5.1.0",
"posthog-js": "^1.76.0",
"reconnecting-websocket": "^4.4.0",
"sigma": "3.0.0-beta.2",
"tinycolor2": "^1.4.2",
"typescript": "^4.9.5",
"validator": "^13.7.0",
Expand All @@ -79,10 +84,10 @@
"vue-router": "^4.1.6",
"vue-safe-teleport": "^0.1.2",
"vue-toastification": "2.0.0-rc.5",
"yjs-codemirror-plugin": "workspace:*",
"y-indexeddb": "^9.0.12",
"y-websocket": "^1.5.0",
"yjs": "^13.6.8"
"yjs": "^13.6.8",
"yjs-codemirror-plugin": "workspace:*"
},
"devDependencies": {
"@iconify/json": "^2.2.135",
Expand All @@ -101,6 +106,7 @@
"cypress": "^9.6.0",
"eslint": "^8.36.0",
"faker": "^6.6.6",
"graphology-types": "^0.24.7",
"unplugin-icons": "^0.17.1",
"vite": "^4.4.9",
"vite-plugin-checker": "^0.6.1",
Expand All @@ -110,4 +116,4 @@
"volta": {
"extends": "../../package.json"
}
}
}
31 changes: 31 additions & 0 deletions app/web/src/components/Workspace/WorkspaceViz.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<template>
<Stack class="h-full w-full">
<VormInput
v-model="selectedSchemaVariant"
label="Schema Variants"
type="dropdown"
class="flex-1"
:options="schemaVariantOptions"
/>

<WorkspaceVizSchemaVariant :schemaVariantId="selectedSchemaVariant"
:key="selectedSchemaVariant" />
</Stack>
</template>

<script lang="ts" setup>
import { computed, ref } from "vue";
import { VormInput, Stack } from "@si/vue-lib/design-system";
import { useComponentsStore } from "@/store/components.store";
import WorkspaceVizSchemaVariant from "./WorkspaceVizSchemaVariant.vue";

const componentStore = useComponentsStore();

const selectedSchemaVariant = ref();
const schemaVariantOptions = computed(() =>
componentStore.schemaVariants.map((sv) => ({
label: sv.schemaName + (sv.builtin ? " (builtin)" : ""),
value: sv.id,
})),
);
</script>
95 changes: 95 additions & 0 deletions app/web/src/components/Workspace/WorkspaceVizSchemaVariant.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
<template>
<div id="vizDiv" class="h-full w-full m-0 p-0 overflow-hidden"></div>
</template>

<script lang="ts" setup>
import { onMounted } from "vue";
import { DirectedGraph } from "graphology";
import Sigma from "sigma";
import FA2Layout from "graphology-layout-forceatlas2/worker";
import forceAtlas2 from "graphology-layout-forceatlas2";
import noverlap, { NoverlapLayoutParameters } from "graphology-layout-noverlap";
import { useVizStore } from "@/store/viz.store";

const vizStore = useVizStore();

const props = defineProps<{ schemaVariantId?: string }>();

const getColor = (nodeKind: string, contentKind: string | null) => {
const kindMap: { [key: string]: string } = {
Category: "#2FA",
Func: "#F02",
Ordering: "#ABC",
Prop: "#0FF",
};

const contentKindMap: { [key: string]: string } = {
Root: "#F0F",
ActionPrototype: "#FFA",
AttributePrototype: "#00F",
InternalProvider: "#07A",
Schema: "#0F0",
FuncArg: "#777",
};

if (nodeKind === "Content") {
if (contentKind) {
return contentKindMap[contentKind] ?? "#AF0";
} else {
return "#AF0";
}
} else {
return kindMap[nodeKind] ?? "#AF0";
}
};

onMounted(async () => {
let nodesAndEdges;
let size: number;
if (!props.schemaVariantId) {
nodesAndEdges = await vizStore.FETCH_VIZ();
size = 2;
} else {
nodesAndEdges = await vizStore.FETCH_SCHEMA_VARIANT_VIZ(
props.schemaVariantId,
);
size = 6;
}

if (!nodesAndEdges.result.success) {
return;
}

const graph = new DirectedGraph();

const nodes = nodesAndEdges.result.data.nodes;
const edges = nodesAndEdges.result.data.edges;

for (const node of nodes) {
graph.addNode(node.id, {
color: getColor(node.nodeKind, node.contentKind),
label: `${node.contentKind ?? node.nodeKind}${
node.name ? `: ${node.name}` : ""
}`,
x: Math.floor(Math.random() * 1000),
y: Math.floor(Math.random() * 1000),
size,
});
}

for (const edge of edges) {
graph.addEdge(edge.from, edge.to);
}

const sensibleSettings = forceAtlas2.inferSettings(graph);
const fa2Layout = new FA2Layout(graph, {
settings: sensibleSettings,
});

fa2Layout.start();

const container = document.getElementById("vizDiv") as HTMLElement;
// tslint:disable-next-line
const _sigma = new Sigma(graph, container);
});
</script>
5 changes: 5 additions & 0 deletions app/web/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,11 @@ const routes: RouteRecordRaw[] = [
};
},
},
{
path: ":changeSetId/viz",
name: "workspace-viz",
component: () => import("@/components/Workspace/WorkspaceViz.vue"),
},
{
path: ":changeSetId/c",
name: "workspace-compose",
Expand Down
81 changes: 81 additions & 0 deletions app/web/src/store/viz.store.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import { defineStore } from "pinia";
import { ApiRequest, addStoreHooks } from "@si/vue-lib/pinia";
import { useWorkspacesStore } from "@/store/workspaces.store";
import { useChangeSetsStore } from "@/store/change_sets.store";
import { Visibility } from "@/api/sdf/dal/visibility";
import { nilId } from "@/utils/nilId";

export type NodeKind = "Category" | "Content" | "Func" | "Ordering" | "Prop";

export type ContentKind =
| "Root"
| "ActionPrototype"
| "AttributePrototype"
| "AttributePrototypeArgument"
| "AttributeValue"
| "Component"
| "ExternalProvider"
| "FuncArg"
| "Func"
| "InternalProvider"
| "Prop"
| "Schema"
| "SchemaVariant"
| "StaticArgumentValue"
| "ValidationPrototype";

export interface VizResponse {
edges: {
from: string;
to: string;
}[];

nodes: {
id: string;
nodeKind: NodeKind;
contentKind: ContentKind | null;
name: string | null;
}[];

rootNodeId: string;
}

export const useVizStore = () => {
const changeSetStore = useChangeSetsStore();
const selectedChangeSetId = changeSetStore.selectedChangeSetId;
const workspacesStore = useWorkspacesStore();
const workspaceId = workspacesStore.selectedWorkspacePk;
const visibility: Visibility = {
visibility_change_set_pk: selectedChangeSetId ?? nilId(),
};

return addStoreHooks(
defineStore(
`ws${workspaceId || "NONE"}/cs${selectedChangeSetId || "NONE"}/viz`,
{
state: () => ({
edges: [],
nodes: [],
}),
getters: {
nodes: (state) => state.nodes,
edges: (state) => state.edges,
},
actions: {
async FETCH_VIZ() {
return new ApiRequest<VizResponse>({
url: "/graphviz/nodes_edges",
params: { ...visibility },
});
},
async FETCH_SCHEMA_VARIANT_VIZ(schemaVariantId: string) {
return new ApiRequest<VizResponse>({
url: "/graphviz/schema_variant",
params: { schemaVariantId, ...visibility },
});
},
},
},
),
)();
};
52 changes: 52 additions & 0 deletions lib/dal/src/workspace_snapshot.rs
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ impl WorkspaceSnapshot {
self.id
}

pub fn root(&mut self) -> WorkspaceSnapshotResult<NodeIndex> {
Ok(self.working_copy()?.root())
}

fn working_copy(&mut self) -> WorkspaceSnapshotResult<&mut WorkspaceSnapshotGraph> {
if self.working_copy.is_none() {
self.working_copy = Some(postcard::from_bytes(&self.snapshot)?);
Expand Down Expand Up @@ -362,6 +366,18 @@ impl WorkspaceSnapshot {
Ok(())
}

pub fn nodes(
&mut self,
) -> WorkspaceSnapshotResult<impl Iterator<Item = (&NodeWeight, NodeIndex)>> {
Ok(self.working_copy()?.nodes())
}

pub fn edges(
&mut self,
) -> WorkspaceSnapshotResult<impl Iterator<Item = (&EdgeWeight, NodeIndex, NodeIndex)>> {
Ok(self.working_copy()?.edges())
}

pub fn dot(&mut self) {
self.working_copy()
.expect("failed on accessing or creating a working copy")
Expand Down Expand Up @@ -491,6 +507,42 @@ impl WorkspaceSnapshot {
.collect())
}

pub fn all_outgoing_targets(
&mut self,
id: impl Into<Ulid>,
) -> WorkspaceSnapshotResult<Vec<NodeWeight>> {
let mut result = vec![];
let target_idxs: Vec<NodeIndex> = self
.edges_directed(id, Direction::Outgoing)?
.map(|edge_ref| edge_ref.target())
.collect();

for target_idx in target_idxs {
let node_weight = self.get_node_weight(target_idx)?;
result.push(node_weight.to_owned());
}

Ok(result)
}

pub fn all_incoming_sources(
&mut self,
id: impl Into<Ulid>,
) -> WorkspaceSnapshotResult<Vec<NodeWeight>> {
let mut result = vec![];
let source_idxs: Vec<NodeIndex> = self
.edges_directed(id, Direction::Incoming)?
.map(|edge_ref| edge_ref.source())
.collect();

for source_idx in source_idxs {
let node_weight = self.get_node_weight(source_idx)?;
result.push(node_weight.to_owned());
}

Ok(result)
}

pub fn remove_incoming_edges_of_kind(
&mut self,
change_set: &ChangeSetPointer,
Expand Down
21 changes: 21 additions & 0 deletions lib/dal/src/workspace_snapshot/graph.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,27 @@ impl WorkspaceSnapshotGraph {
self.graph.edges_directed(node_index, direction)
}

pub fn nodes(&self) -> impl Iterator<Item = (&NodeWeight, NodeIndex)> {
self.graph.node_indices().filter_map(|node_idx| {
self.graph
.node_weight(node_idx)
.map(|weight| (weight, node_idx))
})
}

pub fn edges(&self) -> impl Iterator<Item = (&EdgeWeight, NodeIndex, NodeIndex)> {
self.graph.edge_indices().filter_map(|edge_idx| {
self.graph
.edge_weight(edge_idx)
.map(|weight| {
self.graph
.edge_endpoints(edge_idx)
.map(|(source, target)| (weight, source, target))
})
.flatten()
})
}

pub fn add_ordered_edge(
&mut self,
change_set: &ChangeSetPointer,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
use chrono::{DateTime, Utc};
use content_store::ContentHash;
use serde::{Deserialize, Serialize};
use strum::Display;
use ulid::Ulid;

use crate::change_set_pointer::ChangeSetPointer;
use crate::workspace_snapshot::vector_clock::VectorClockId;
use crate::workspace_snapshot::{node_weight::NodeWeightResult, vector_clock::VectorClock};

#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Display)]
pub enum CategoryNodeKind {
Component,
Func,
Expand Down
Loading