Skip to content

Commit 2c8a1dd

Browse files
committed
Hide deleted items
1 parent ce773aa commit 2c8a1dd

8 files changed

Lines changed: 88 additions & 13 deletions

File tree

client/src/api/schema/schema.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13368,6 +13368,8 @@ export interface components {
1336813368
GraphNode: {
1336913369
/** Collection Type */
1337013370
collection_type?: string | null;
13371+
/** Deleted */
13372+
deleted?: boolean | null;
1337113373
/** Extension */
1337213374
extension?: string | null;
1337313375
/** Hid */
@@ -13387,6 +13389,8 @@ export interface components {
1338713389
* @enum {string}
1338813390
*/
1338913391
type: "dataset" | "collection" | "tool_request";
13392+
/** Visible */
13393+
visible?: boolean | null;
1339013394
};
1339113395
/**
1339213396
* GroupCreatePayload
@@ -34947,6 +34951,8 @@ export interface operations {
3494734951
depth?: number;
3494834952
/** @description Maximum number of nodes returned. Must be 1-2000. */
3494934953
limit?: number;
34954+
/** @description Include deleted datasets and collections in the graph. */
34955+
include_deleted?: boolean;
3495034956
};
3495134957
header?: {
3495234958
/** @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. */

client/src/components/Graph/GraphNode.vue

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,7 @@ const iconSpin = computed(() => Boolean(props.node.data?.stateSpin));
3232
:class="[node.cssClass, { 'node-highlight': selected }]"
3333
:style="nodeStyle"
3434
@click.stop="emit('select', node.id)">
35-
<div
36-
class="graph-node-header card-header unselectable py-1 px-2"
37-
:data-state="node.data?.state ?? undefined">
35+
<div class="graph-node-header card-header unselectable py-1 px-2" :data-state="node.data?.state ?? undefined">
3836
<FontAwesomeIcon :icon="node.icon" class="graph-node-icon mr-1" :spin="iconSpin" />
3937
<span class="graph-node-label" :title="node.label">{{ node.label }}</span>
4038
<span v-if="hasPorts && node.badge" class="badge badge-light ml-auto">{{ node.badge }}</span>

client/src/components/History/Graph/HistoryGraphView.vue

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script setup lang="ts">
2-
import { faBezierCurve, faProjectDiagram } from "@fortawesome/free-solid-svg-icons";
2+
import { faBezierCurve, faProjectDiagram, faTrash } from "@fortawesome/free-solid-svg-icons";
33
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
44
import { BButton, BButtonGroup } from "bootstrap-vue";
55
import { computed, ref, toRef } from "vue";
@@ -28,8 +28,16 @@ const { seed, seedLoading, noLineage } = useHistoryGraphSeed(toRef(props, "histo
2828
const direction = ref("both");
2929
const depth = ref(20);
3030
const limit = ref(500);
31+
const includeDeleted = ref(false);
3132
32-
const { graphData, loading, error } = useHistoryGraphData(toRef(props, "historyId"), seed, direction, depth, limit);
33+
const { graphData, loading, error } = useHistoryGraphData(
34+
toRef(props, "historyId"),
35+
seed,
36+
direction,
37+
depth,
38+
limit,
39+
includeDeleted,
40+
);
3341
3442
// Layout
3543
const edgeStyle = ref<EdgeStyle>("orthogonal");
@@ -80,6 +88,15 @@ const isTruncated = computed(() => graphData.value?.truncated?.capped === true);
8088
<FontAwesomeIcon :icon="faBezierCurve" />
8189
</BButton>
8290
</BButtonGroup>
91+
<BButtonGroup size="sm" class="ml-2">
92+
<BButton
93+
:pressed="includeDeleted"
94+
variant="outline-primary"
95+
title="Show deleted items"
96+
@click="includeDeleted = !includeDeleted">
97+
<FontAwesomeIcon :icon="faTrash" />
98+
</BButton>
99+
</BButtonGroup>
83100
</div>
84101
<GraphView :layout="layout" :focus-node-id="seed" :edge-style="edgeStyle" @node-selected="onNodeSelected" />
85102
<GraphDetails :node="selectedNode" :details="selectedDetails" />

client/src/components/History/Graph/useHistoryGraphData.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export function useHistoryGraphData(
1717
direction: Ref<string>,
1818
depth: Ref<number>,
1919
limit: Ref<number>,
20+
includeDeleted: Ref<boolean>,
2021
) {
2122
const graphData = ref<HistoryGraphResponse | null>(null);
2223
const loading = ref(false);
@@ -40,6 +41,7 @@ export function useHistoryGraphData(
4041
direction: direction.value as "backward" | "forward" | "both",
4142
depth: depth.value,
4243
limit: limit.value,
44+
include_deleted: includeDeleted.value,
4345
},
4446
},
4547
});
@@ -58,7 +60,9 @@ export function useHistoryGraphData(
5860
}
5961
}
6062

61-
watch([historyId, seed, direction, depth, limit], () => fetchGraph(), { immediate: true });
63+
watch([historyId, seed, direction, depth, limit, includeDeleted], () => fetchGraph(), {
64+
immediate: true,
65+
});
6266

6367
return { graphData, loading, error };
6468
}

lib/galaxy/managers/history_graph.py

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ def __init__(
6969
depth: int,
7070
limit: int,
7171
toolbox=None,
72+
include_deleted: bool = False,
7273
):
7374
self.sa_session = sa_session
7475
self.security = security
@@ -78,6 +79,7 @@ def __init__(
7879
self.depth = depth
7980
self.limit = limit
8081
self.toolbox = toolbox
82+
self.include_deleted = include_deleted
8183

8284
# Global visited set: (node_type, db_id) — prevents re-expansion and preserves BFS depth semantics
8385
self.visited: set[tuple[str, int]] = set()
@@ -221,6 +223,8 @@ def _fetch_dataset_node(self, db_id: int) -> tuple[Optional[GraphNode], bool]:
221223
HistoryDatasetAssociation._state,
222224
Dataset.state.label("dataset_state"),
223225
HistoryDatasetAssociation.extension,
226+
HistoryDatasetAssociation.deleted,
227+
HistoryDatasetAssociation.visible,
224228
)
225229
.join(Dataset, HistoryDatasetAssociation.dataset_id == Dataset.id)
226230
.where(HistoryDatasetAssociation.id == db_id)
@@ -238,8 +242,10 @@ def _fetch_dataset_node(self, db_id: int) -> tuple[Optional[GraphNode], bool]:
238242
)
239243
)
240244
return None, True
245+
if not self._should_include(row.deleted, row.visible):
246+
return None, False
241247
state = row._state if row._state else row.dataset_state
242-
return GraphNode(id=encoded, type="dataset", name=row.name, hid=row.hid, state=state, extension=row.extension), False
248+
return GraphNode(id=encoded, type="dataset", name=row.name, hid=row.hid, state=state, extension=row.extension, deleted=row.deleted, visible=row.visible), False
243249

244250
def _fetch_collection_node(self, db_id: int) -> tuple[Optional[GraphNode], bool]:
245251
stmt = (
@@ -248,6 +254,8 @@ def _fetch_collection_node(self, db_id: int) -> tuple[Optional[GraphNode], bool]
248254
HistoryDatasetCollectionAssociation.history_id,
249255
HistoryDatasetCollectionAssociation.name,
250256
HistoryDatasetCollectionAssociation.hid,
257+
HistoryDatasetCollectionAssociation.deleted,
258+
HistoryDatasetCollectionAssociation.visible,
251259
DatasetCollection.collection_type,
252260
DatasetCollection.populated_state,
253261
)
@@ -267,8 +275,10 @@ def _fetch_collection_node(self, db_id: int) -> tuple[Optional[GraphNode], bool]
267275
)
268276
)
269277
return None, True
278+
if not self._should_include(row.deleted, row.visible):
279+
return None, False
270280
state = row.populated_state
271-
return GraphNode(id=encoded, type="collection", name=row.name, hid=row.hid, state=state, collection_type=row.collection_type), False
281+
return GraphNode(id=encoded, type="collection", name=row.name, hid=row.hid, state=state, collection_type=row.collection_type, deleted=row.deleted, visible=row.visible), False
272282

273283
def _fetch_tool_request_node(self, db_id: int) -> tuple[Optional[GraphNode], bool]:
274284
# Get tool_request and tool_id from first associated job
@@ -309,6 +319,8 @@ def _fetch_dataset_nodes_batch(self, db_ids: list[int]) -> list[GraphNode]:
309319
HistoryDatasetAssociation._state,
310320
Dataset.state.label("dataset_state"),
311321
HistoryDatasetAssociation.extension,
322+
HistoryDatasetAssociation.deleted,
323+
HistoryDatasetAssociation.visible,
312324
)
313325
.join(Dataset, HistoryDatasetAssociation.dataset_id == Dataset.id)
314326
.where(HistoryDatasetAssociation.id.in_(chunk))
@@ -325,9 +337,11 @@ def _fetch_dataset_nodes_batch(self, db_ids: list[int]) -> list[GraphNode]:
325337
history_id=self.security.encode_id(row.history_id) if row.history_id else None,
326338
)
327339
)
340+
elif not self._should_include(row.deleted, row.visible):
341+
pass # Filtered out — not admitted
328342
else:
329343
state = row._state if row._state else row.dataset_state
330-
nodes.append(GraphNode(id=encoded, type="dataset", name=row.name, hid=row.hid, state=state, extension=row.extension))
344+
nodes.append(GraphNode(id=encoded, type="dataset", name=row.name, hid=row.hid, state=state, extension=row.extension, deleted=row.deleted, visible=row.visible))
331345
for missing_id in set(chunk) - found:
332346
self._add_external_ref(
333347
ExternalRef(id=self._encode_node_id("dataset", missing_id), type="dataset")
@@ -343,6 +357,8 @@ def _fetch_collection_nodes_batch(self, db_ids: list[int]) -> list[GraphNode]:
343357
HistoryDatasetCollectionAssociation.history_id,
344358
HistoryDatasetCollectionAssociation.name,
345359
HistoryDatasetCollectionAssociation.hid,
360+
HistoryDatasetCollectionAssociation.deleted,
361+
HistoryDatasetCollectionAssociation.visible,
346362
DatasetCollection.collection_type,
347363
DatasetCollection.populated_state,
348364
)
@@ -361,9 +377,11 @@ def _fetch_collection_nodes_batch(self, db_ids: list[int]) -> list[GraphNode]:
361377
history_id=self.security.encode_id(row.history_id) if row.history_id else None,
362378
)
363379
)
380+
elif not self._should_include(row.deleted, row.visible):
381+
pass
364382
else:
365383
state = row.populated_state
366-
nodes.append(GraphNode(id=encoded, type="collection", name=row.name, hid=row.hid, state=state, collection_type=row.collection_type))
384+
nodes.append(GraphNode(id=encoded, type="collection", name=row.name, hid=row.hid, state=state, collection_type=row.collection_type, deleted=row.deleted, visible=row.visible))
367385
for missing_id in set(chunk) - found:
368386
self._add_external_ref(
369387
ExternalRef(id=self._encode_node_id("collection", missing_id), type="collection")
@@ -493,10 +511,21 @@ def _expand_level(
493511
self.external_refs = self.external_refs[:external_before]
494512

495513
# Build admitted keys from materialized nodes only. External refs and
496-
# filtered nodes (e.g., __DATA_FETCH__) are terminal — they must not
497-
# appear in the next frontier.
514+
# filtered nodes (e.g., __DATA_FETCH__, deleted, hidden) are terminal —
515+
# they must not appear in the next frontier.
498516
admitted_keys = [encoded_to_key[n.id] for n in new_nodes if n.id in encoded_to_key]
499517

518+
# Remove edges that reference filtered-out nodes (deleted/hidden items
519+
# that were discovered but not admitted). An edge is valid if both its
520+
# source and target exist as either: previously admitted nodes, newly
521+
# admitted nodes, external refs, or frontier nodes from this expansion.
522+
valid_ids: set[str] = {n.id for n in self.nodes} # previously admitted
523+
valid_ids.update(n.id for n in new_nodes) # newly admitted
524+
valid_ids.update(r.id for r in new_external) # external refs
525+
# Also include frontier nodes (they were admitted in a previous level)
526+
valid_ids.update(self._encode_node_id(t, d) for t, d in frontier)
527+
new_edges = [e for e in new_edges if e.source in valid_ids and e.target in valid_ids]
528+
500529
return new_nodes, admitted_keys, new_edges, new_external
501530

502531
# ── Edge discovery queries ──
@@ -763,6 +792,19 @@ def _find_outputs_for_trs(self, tr_ids: list[int]) -> list[tuple[str, int, str,
763792
results.append(("collection", row3.dataset_collection_id, tr_enc, c_enc, "collection_output"))
764793
return results
765794

795+
# ── Visibility filtering ──
796+
797+
def _should_include(self, deleted: bool, visible: bool) -> bool:
798+
"""Check if a dataset/collection should be included based on deleted/visible flags.
799+
800+
Hidden items are always included in the graph because intermediate
801+
tool outputs are commonly hidden in Galaxy — they are essential
802+
lineage nodes. Only deleted items are filtered by default.
803+
"""
804+
if deleted and not self.include_deleted:
805+
return False
806+
return True
807+
766808
# ── Tool name resolution ──
767809

768810
def _resolve_tool_names(self):

lib/galaxy/schema/history_graph.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ class GraphNode(BaseModel):
1717
state: Optional[str] = None
1818
extension: Optional[str] = None
1919
collection_type: Optional[str] = None
20+
deleted: Optional[bool] = None
21+
visible: Optional[bool] = None
2022
tool_id: Optional[str] = None
2123
tool_name: Optional[str] = None
2224

lib/galaxy/webapps/galaxy/api/histories.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -363,9 +363,13 @@ def graph(
363363
ge=1,
364364
le=2000,
365365
),
366+
include_deleted: bool = Query(
367+
default=False,
368+
description="Include deleted datasets and collections in the graph.",
369+
),
366370
trans: ProvidesHistoryContext = DependsOnTrans,
367371
) -> HistoryGraphResponse:
368-
return self.service.graph(trans, history_id, seed=seed, direction=direction, depth=depth, limit=limit)
372+
return self.service.graph(trans, history_id, seed=seed, direction=direction, depth=depth, limit=limit, include_deleted=include_deleted)
369373

370374
@router.post(
371375
"/api/histories/{history_id}/prepare_store_download",

lib/galaxy/webapps/galaxy/services/histories.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ def graph(
380380
direction: str,
381381
depth: int,
382382
limit: int,
383+
include_deleted: bool = False,
383384
):
384385
from galaxy.managers.history_graph import HistoryGraphBuilder
385386

@@ -394,6 +395,7 @@ def graph(
394395
depth=depth,
395396
limit=limit,
396397
toolbox=toolbox,
398+
include_deleted=include_deleted,
397399
)
398400
return builder.build()
399401

0 commit comments

Comments
 (0)