Skip to content

Commit e7ee991

Browse files
committed
Add history graph UI with interactive visualization and menu integration
1 parent 5e845bf commit e7ee991

9 files changed

Lines changed: 851 additions & 1 deletion

File tree

client/src/api/schema/schema.ts

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2803,6 +2803,23 @@ export interface paths {
28032803
patch?: never;
28042804
trace?: never;
28052805
};
2806+
"/api/histories/{history_id}/graph": {
2807+
parameters: {
2808+
query?: never;
2809+
header?: never;
2810+
path?: never;
2811+
cookie?: never;
2812+
};
2813+
/** Returns a history-scoped structural graph. */
2814+
get: operations["graph_api_histories__history_id__graph_get"];
2815+
put?: never;
2816+
post?: never;
2817+
delete?: never;
2818+
options?: never;
2819+
head?: never;
2820+
patch?: never;
2821+
trace?: never;
2822+
};
28062823
"/api/histories/{history_id}/jobs_summary": {
28072824
parameters: {
28082825
query?: never;
@@ -13371,6 +13388,46 @@ export interface components {
1337113388
*/
1337213389
type: "genomebuild";
1337313390
};
13391+
/** GraphEdge */
13392+
GraphEdge: {
13393+
/** Source */
13394+
source: string;
13395+
/** Target */
13396+
target: string;
13397+
/**
13398+
* Type
13399+
* @enum {string}
13400+
*/
13401+
type: "dataset_input" | "dataset_output" | "collection_input" | "collection_output";
13402+
};
13403+
/** GraphNode */
13404+
GraphNode: {
13405+
/** Collection Type */
13406+
collection_type?: string | null;
13407+
/** Deleted */
13408+
deleted?: boolean | null;
13409+
/** Extension */
13410+
extension?: string | null;
13411+
/** Hid */
13412+
hid?: number | null;
13413+
/** Id */
13414+
id: string;
13415+
/** Name */
13416+
name?: string | null;
13417+
/** State */
13418+
state?: string | null;
13419+
/** Tool Id */
13420+
tool_id?: string | null;
13421+
/** Tool Name */
13422+
tool_name?: string | null;
13423+
/**
13424+
* Type
13425+
* @enum {string}
13426+
*/
13427+
type: "dataset" | "collection" | "tool_request";
13428+
/** Visible */
13429+
visible?: boolean | null;
13430+
};
1337413431
/**
1337513432
* GroupCreatePayload
1337613433
* @description Payload schema for creating a group.
@@ -15635,6 +15692,14 @@ export interface components {
1563515692
*/
1563615693
username_and_slug?: string | null;
1563715694
};
15695+
/** HistoryGraphResponse */
15696+
HistoryGraphResponse: {
15697+
/** Edges */
15698+
edges: components["schemas"]["GraphEdge"][];
15699+
/** Nodes */
15700+
nodes: components["schemas"]["GraphNode"][];
15701+
truncated: components["schemas"]["TruncationInfo"];
15702+
};
1563815703
/**
1563915704
* HistorySummary
1564015705
* @description History summary information.
@@ -23774,6 +23839,24 @@ export interface components {
2377423839
*/
2377523840
title?: string | null;
2377623841
};
23842+
/** TruncationInfo */
23843+
TruncationInfo: {
23844+
/**
23845+
* Item Count Capped
23846+
* @default false
23847+
*/
23848+
item_count_capped: boolean;
23849+
/**
23850+
* Tool Request Count Capped
23851+
* @default false
23852+
*/
23853+
tool_request_count_capped: boolean;
23854+
/**
23855+
* Tool Requests Omitted
23856+
* @default 0
23857+
*/
23858+
tool_requests_omitted: number;
23859+
};
2377723860
/** UndeleteHistoriesPayload */
2377823861
UndeleteHistoriesPayload: {
2377923862
/**
@@ -35113,6 +35196,61 @@ export interface operations {
3511335196
};
3511435197
};
3511535198
};
35199+
graph_api_histories__history_id__graph_get: {
35200+
parameters: {
35201+
query?: {
35202+
/** @description Maximum number of nodes. Applied at history scope. */
35203+
limit?: number;
35204+
/** @description Include deleted datasets and collections. */
35205+
include_deleted?: boolean;
35206+
/** @description Optional: focus on subgraph reachable from this node (e.g. d<encoded_id>). */
35207+
seed?: string | null;
35208+
/** @description Direction for seed-based subgraph extraction. */
35209+
direction?: "backward" | "forward" | "both";
35210+
/** @description Max depth for seed-based subgraph extraction. */
35211+
depth?: number;
35212+
};
35213+
header?: {
35214+
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
35215+
"run-as"?: string | null;
35216+
};
35217+
path: {
35218+
/** @description The encoded database identifier of the History. */
35219+
history_id: string;
35220+
};
35221+
cookie?: never;
35222+
};
35223+
requestBody?: never;
35224+
responses: {
35225+
/** @description Successful Response */
35226+
200: {
35227+
headers: {
35228+
[name: string]: unknown;
35229+
};
35230+
content: {
35231+
"application/json": components["schemas"]["HistoryGraphResponse"];
35232+
};
35233+
};
35234+
/** @description Request Error */
35235+
"4XX": {
35236+
headers: {
35237+
[name: string]: unknown;
35238+
};
35239+
content: {
35240+
"application/json": components["schemas"]["MessageExceptionModel"];
35241+
};
35242+
};
35243+
/** @description Server Error */
35244+
"5XX": {
35245+
headers: {
35246+
[name: string]: unknown;
35247+
};
35248+
content: {
35249+
"application/json": components["schemas"]["MessageExceptionModel"];
35250+
};
35251+
};
35252+
};
35253+
};
3511635254
index_jobs_summary_api_histories__history_id__jobs_summary_get: {
3511735255
parameters: {
3511835256
query?: {

client/src/components/History/Content/model/states.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export type State =
4040
| "new_populated_state"
4141
| "inaccessible";
4242

43-
interface StateRepresentation {
43+
export interface StateRepresentation {
4444
status: "success" | "warning" | "info" | "danger" | "secondary";
4545
text?: string;
4646
displayName?: string;
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
<script setup lang="ts">
2+
import { faBezierCurve, faProjectDiagram, faTrash } from "@fortawesome/free-solid-svg-icons";
3+
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
4+
import { BButton, BButtonGroup } from "bootstrap-vue";
5+
import { computed, ref, toRef } from "vue";
6+
7+
import GraphDetails from "@/components/Graph/GraphDetails.vue";
8+
import GraphView from "@/components/Graph/GraphView.vue";
9+
import type { GraphNode } from "@/components/Graph/types";
10+
import LoadingSpan from "@/components/LoadingSpan.vue";
11+
12+
import { buildDetails } from "./historyGraphMapper";
13+
import { useHistoryGraphData } from "./useHistoryGraphData";
14+
import { type EdgeStyle, useHistoryGraphLayout } from "./useHistoryGraphLayout";
15+
16+
interface Props {
17+
historyId: string;
18+
seedNodeId?: string;
19+
}
20+
21+
const props = defineProps<Props>();
22+
23+
// Fetch params — product decisions owned here
24+
const limit = ref(500);
25+
const includeDeleted = ref(false);
26+
27+
const { graphData, loading, error } = useHistoryGraphData(
28+
toRef(props, "historyId"),
29+
limit,
30+
includeDeleted,
31+
toRef(props, "seedNodeId"),
32+
);
33+
34+
// Layout
35+
const edgeStyle = ref<EdgeStyle>("orthogonal");
36+
const { layout, layoutLoading } = useHistoryGraphLayout(graphData, edgeStyle);
37+
38+
// Selection
39+
const selectedNode = ref<GraphNode | null>(null);
40+
const selectedDetails = computed(() => {
41+
if (!selectedNode.value) {
42+
return [];
43+
}
44+
return buildDetails(selectedNode.value);
45+
});
46+
47+
function onNodeSelected(node: GraphNode | null) {
48+
selectedNode.value = node;
49+
}
50+
51+
const isLoading = computed(() => loading.value || layoutLoading.value);
52+
const isTruncated = computed(() => {
53+
const t = graphData.value?.truncated;
54+
return t?.item_count_capped || t?.tool_request_count_capped || (t?.tool_requests_omitted ?? 0) > 0;
55+
});
56+
</script>
57+
58+
<template>
59+
<div class="history-graph-view">
60+
<LoadingSpan v-if="isLoading" message="Loading history graph" />
61+
62+
<div v-else-if="error" class="history-graph-error alert alert-danger m-3">{{ error }}</div>
63+
64+
<template v-else>
65+
<div class="history-graph-toolbar">
66+
<BButtonGroup size="sm">
67+
<BButton
68+
:pressed="edgeStyle === 'orthogonal'"
69+
variant="outline-primary"
70+
title="Orthogonal edges"
71+
@click="edgeStyle = 'orthogonal'">
72+
<FontAwesomeIcon :icon="faProjectDiagram" />
73+
</BButton>
74+
<BButton
75+
:pressed="edgeStyle === 'curved'"
76+
variant="outline-primary"
77+
title="Curved edges"
78+
@click="edgeStyle = 'curved'">
79+
<FontAwesomeIcon :icon="faBezierCurve" />
80+
</BButton>
81+
</BButtonGroup>
82+
<BButtonGroup size="sm" class="ml-2">
83+
<BButton
84+
:pressed="includeDeleted"
85+
variant="outline-primary"
86+
title="Show deleted items"
87+
@click="includeDeleted = !includeDeleted">
88+
<FontAwesomeIcon :icon="faTrash" />
89+
</BButton>
90+
</BButtonGroup>
91+
</div>
92+
<GraphView
93+
:layout="layout"
94+
:focus-node-id="seedNodeId ?? null"
95+
:edge-style="edgeStyle"
96+
@node-selected="onNodeSelected" />
97+
<GraphDetails :node="selectedNode" :details="selectedDetails" />
98+
<div v-if="isTruncated" class="history-graph-truncation">
99+
Showing a partial graph. Not all connections are visible.
100+
</div>
101+
</template>
102+
</div>
103+
</template>
104+
105+
<style lang="scss" scoped>
106+
@import "@/style/scss/theme/blue.scss";
107+
108+
.history-graph-view {
109+
display: flex;
110+
flex-direction: column;
111+
height: 100%;
112+
min-height: 400px;
113+
}
114+
115+
.history-graph-toolbar {
116+
position: absolute;
117+
top: 0.5rem;
118+
right: 0.5rem;
119+
z-index: 20;
120+
}
121+
122+
.history-graph-truncation {
123+
padding: 0.375rem 1rem;
124+
background: $state-warning-bg;
125+
color: $state-warning-text;
126+
font-size: $h6-font-size;
127+
text-align: center;
128+
border-top: 1px solid $state-warning-border;
129+
}
130+
131+
/* Tool request nodes use primary header (no dataset state) */
132+
:deep(.node-tool-request) .graph-node-header {
133+
background: $brand-primary;
134+
color: $white;
135+
}
136+
137+
/* Dataset/collection nodes use state-driven coloring via data-state attribute */
138+
:deep(.node-dataset) .graph-node-header,
139+
:deep(.node-collection) .graph-node-header {
140+
color: $text-color;
141+
}
142+
143+
:deep(.edge-collection) {
144+
stroke: $brand-info;
145+
stroke-dasharray: 6 3;
146+
}
147+
148+
:deep(.edge-dataset) {
149+
stroke: $brand-primary;
150+
}
151+
</style>

0 commit comments

Comments
 (0)