Skip to content

Commit 5cddbc8

Browse files
committed
Implement data landing UI.
1 parent fbadc9a commit 5cddbc8

File tree

11 files changed

+1394
-5
lines changed

11 files changed

+1394
-5
lines changed

client/src/api/tools.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@ import { type components, GalaxyApi } from "@/api";
22
import { ERROR_STATES, type ShowFullJobResponse } from "@/api/jobs";
33

44
export type HdcaUploadTarget = components["schemas"]["HdcaDataItemsTarget"];
5+
export type HdasUploadTarget = components["schemas"]["DataElementsTarget"];
56
export type FetchDataPayload = components["schemas"]["FetchDataPayload"];
67
export type UrlDataElement = components["schemas"]["UrlDataElement"];
78
export type NestedElement = components["schemas"]["NestedElement"];
9+
export type NestedElementItems = NestedElement["elements"];
10+
export type NestedElementItem = NestedElementItems[number];
11+
export type FetchTargets = FetchDataPayload["targets"];
12+
export type AnyFetchTarget = FetchTargets[number];
813

914
export function urlDataElement(identifier: string, uri: string): UrlDataElement {
1015
const element: UrlDataElement = {
Lines changed: 257 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,257 @@
1+
<script setup lang="ts">
2+
import type { ColDef } from "ag-grid-community";
3+
import { computed, ref, watch } from "vue";
4+
5+
import type { AnyFetchTarget, HdcaUploadTarget } from "@/api/tools";
6+
import type { ParsedFetchWorkbookColumnType } from "@/components/Collections/wizard/types";
7+
import type { CardAction } from "@/components/Common/GCard.types";
8+
import { useAgGrid } from "@/composables/useAgGrid";
9+
10+
import { type DerivedColumn, type FetchTable, fetchTargetToTable, type RowsType, tableToRequest } from "./fetchModels";
11+
import { enforceColumnUniqueness, useGridHelpers } from "./gridHelpers";
12+
13+
import GCard from "@/components/Common/GCard.vue";
14+
import DataFetchRequestParameter from "@/components/JobParameters/DataFetchRequestParameter.vue";
15+
16+
interface Props {
17+
target: AnyFetchTarget;
18+
}
19+
20+
const props = defineProps<Props>();
21+
22+
const { gridApi, AgGridVue, onGridReady, theme } = useAgGrid(resize);
23+
24+
function resize() {
25+
if (gridApi.value) {
26+
gridApi.value.sizeColumnsToFit();
27+
}
28+
}
29+
30+
const style = computed(() => {
31+
return { width: "100%" };
32+
});
33+
34+
type AgRowData = Record<string, unknown>;
35+
36+
const gridRowData = ref<AgRowData[]>([]);
37+
const gridColumns = ref<ColDef[]>([]);
38+
const richSupportForTarget = ref(false);
39+
const modified = ref(false);
40+
type ViewModeT = "table" | "raw";
41+
const viewMode = ref<ViewModeT>("raw");
42+
43+
const collectionTarget = computed(() => {
44+
if (props.target.destination.type == "hdca") {
45+
return props.target as HdcaUploadTarget;
46+
} else {
47+
throw Error("Not a collection target - logic error");
48+
}
49+
});
50+
51+
const { makeExtensionColumn, makeDbkeyColumn } = useGridHelpers();
52+
53+
const title = computed(() => {
54+
let title;
55+
if (props.target.destination.type == "hdas") {
56+
title = "Datasets";
57+
} else if (props.target.destination.type == "hdca") {
58+
title = `Collection: ${collectionTarget.value.name}`;
59+
} else {
60+
title = "Library";
61+
}
62+
if (modified.value) {
63+
title += " (modified)";
64+
}
65+
return title;
66+
});
67+
68+
function initializeRowData(rowData: AgRowData[], rows: RowsType) {
69+
for (const row of rows) {
70+
rowData.push({ ...row });
71+
}
72+
}
73+
74+
const BOOLEAN_COLUMNS: ParsedFetchWorkbookColumnType[] = [
75+
"to_posix_lines",
76+
"space_to_tab",
77+
"auto_decompress",
78+
"deferred",
79+
];
80+
81+
function derivedColumnToAgColumnDefinition(column: DerivedColumn): ColDef {
82+
const colDef: ColDef = {
83+
headerName: column.title,
84+
field: column.key(),
85+
sortable: false,
86+
filter: false,
87+
resizable: true,
88+
};
89+
if (column.type === "file_type") {
90+
makeExtensionColumn(colDef);
91+
} else if (column.type === "dbkey") {
92+
makeDbkeyColumn(colDef);
93+
} else if (column.type == "list_identifiers" && collectionTypeRef.value?.indexOf(":") === -1) {
94+
// flat list-like structure - lets make sure element names are unique.
95+
enforceColumnUniqueness(colDef);
96+
} else if (BOOLEAN_COLUMNS.indexOf(column.type) >= 0) {
97+
colDef.cellRenderer = "agCheckboxCellRenderer";
98+
colDef.cellEditor = "agCheckboxCellEditor";
99+
}
100+
return colDef;
101+
}
102+
103+
function initializeColumns(columns: DerivedColumn[]) {
104+
if (!columns || columns.length === 0) {
105+
gridColumns.value = [];
106+
return;
107+
}
108+
109+
gridColumns.value = columns.map(derivedColumnToAgColumnDefinition);
110+
}
111+
112+
const collectionTypeRef = ref<string | undefined>(undefined);
113+
const columnsRef = ref<DerivedColumn[]>([]);
114+
const autoDecompressRef = ref<boolean | undefined>(undefined);
115+
116+
function initializeTabularVersionOfTarget() {
117+
let table;
118+
try {
119+
table = fetchTargetToTable(props.target);
120+
} catch (error) {
121+
richSupportForTarget.value = false;
122+
return;
123+
}
124+
const { columns, rows, collectionType } = table;
125+
columnsRef.value = columns;
126+
collectionTypeRef.value = collectionType;
127+
autoDecompressRef.value = table.autoDecompress;
128+
initializeColumns(columns);
129+
gridRowData.value.splice(0, gridRowData.value.length);
130+
initializeRowData(gridRowData.value, rows);
131+
richSupportForTarget.value = true;
132+
viewMode.value = "table";
133+
}
134+
135+
function initialize() {
136+
initializeTabularVersionOfTarget();
137+
modified.value = false;
138+
}
139+
140+
// Default Column Properties
141+
const defaultColDef = ref<ColDef>({
142+
editable: true,
143+
sortable: true,
144+
filter: true,
145+
resizable: true,
146+
});
147+
148+
function asTarget(): AnyFetchTarget {
149+
if (modified.value) {
150+
const newTable: FetchTable = {
151+
columns: columnsRef.value,
152+
rows: gridRowData.value as RowsType,
153+
autoDecompress: autoDecompressRef.value,
154+
collectionType: collectionTypeRef.value,
155+
isCollection: collectionTypeRef.value !== undefined,
156+
};
157+
return tableToRequest(newTable);
158+
} else {
159+
return Object.assign({}, props.target);
160+
}
161+
}
162+
163+
defineExpose({
164+
asTarget,
165+
});
166+
167+
watch(
168+
() => {
169+
props.target;
170+
},
171+
() => {
172+
initialize();
173+
// is this block needed?
174+
if (gridApi.value) {
175+
const params = {
176+
force: true,
177+
suppressFlash: true,
178+
};
179+
gridApi.value!.refreshCells(params);
180+
}
181+
},
182+
{
183+
immediate: true,
184+
}
185+
);
186+
187+
const viewRequestAction: CardAction = {
188+
id: "source",
189+
label: "View Request",
190+
title: "View raw data request JSON",
191+
handler: () => (viewMode.value = "raw"),
192+
visible: true,
193+
};
194+
195+
const viewTableAction: CardAction = {
196+
id: "table",
197+
label: "View Table",
198+
title: "View data in table format",
199+
handler: () => (viewMode.value = "table"),
200+
visible: richSupportForTarget.value,
201+
};
202+
203+
const secondaryActions = computed<CardAction[]>(() => {
204+
if (!richSupportForTarget.value) {
205+
return [];
206+
}
207+
208+
const actions: CardAction[] = [];
209+
if (viewMode.value === "raw") {
210+
actions.push(viewTableAction);
211+
} else {
212+
actions.push(viewRequestAction);
213+
}
214+
return actions;
215+
});
216+
217+
function handleDataUpdated(event: any) {
218+
console.log(event);
219+
modified.value = true;
220+
}
221+
</script>
222+
223+
<template>
224+
<GCard :title="title" :secondary-actions="secondaryActions">
225+
<template v-slot:description>
226+
<div v-if="viewMode === 'raw'">
227+
<BAlert v-if="!richSupportForTarget" show dismissible variant="warning">
228+
This target is using advanced features that we don't yet support a rich tabular view for, an
229+
annotated request is shown here and can still be used to import the target data. If you would like
230+
this to see this kind of target supported, please
231+
<a href="https://github.com/galaxyproject/galaxy/issues">create an issue on GitHub</a>
232+
titled something like "Support Rich View of Data Fetch Request" and include this request as an
233+
example.
234+
</BAlert>
235+
<BAlert v-if="modified" show dismissible variant="warning">
236+
This shows the initial data import request, you have modified the import data and your modifications
237+
will be reflected in the final data import but not in this initial request.
238+
</BAlert>
239+
<DataFetchRequestParameter :parameter-value="props.target" />
240+
</div>
241+
<div v-else-if="viewMode === 'table'" :class="[theme]">
242+
<BAlert v-if="modified" show dismissible variant="info">
243+
You have modified the import data from the initial request, these modifications will be reflected in
244+
the final data import.
245+
</BAlert>
246+
<AgGridVue
247+
:row-data="gridRowData"
248+
:column-defs="gridColumns"
249+
:default-col-def="defaultColDef"
250+
:style="style"
251+
dom-layout="autoHeight"
252+
@cellValueChanged="handleDataUpdated"
253+
@gridReady="onGridReady" />
254+
</div>
255+
</template>
256+
</GCard>
257+
</template>
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<script setup lang="ts">
2+
import { ref } from "vue";
3+
4+
import type { AnyFetchTarget, FetchTargets } from "@/api/tools";
5+
6+
import FetchGrid from "./FetchGrid.vue";
7+
8+
const fetchGrids = ref<unknown[]>([]);
9+
10+
interface TargetComponent {
11+
asTarget(): AnyFetchTarget;
12+
}
13+
14+
interface Props {
15+
targets: FetchTargets;
16+
}
17+
18+
function asTargets() {
19+
const targets = fetchGrids.value.map((fetchGrid) => {
20+
const component = fetchGrid as TargetComponent;
21+
return component.asTarget();
22+
});
23+
return targets;
24+
}
25+
26+
defineExpose({ asTargets });
27+
28+
defineProps<Props>();
29+
</script>
30+
31+
<template>
32+
<div>
33+
<div v-for="(target, index) in targets" :key="index">
34+
<FetchGrid ref="fetchGrids" :target="target" />
35+
</div>
36+
</div>
37+
</template>
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
<script setup lang="ts">
2+
import { faUpload } from "@fortawesome/free-solid-svg-icons";
3+
import { ref } from "vue";
4+
5+
import type { FetchTargets } from "@/api/tools";
6+
import { useFetchJobMonitor } from "@/composables/fetch";
7+
import { useHistoryStore } from "@/stores/historyStore";
8+
9+
import FetchGrids from "./FetchGrids.vue";
10+
import ButtonSpinner from "@/components/Common/ButtonSpinner.vue";
11+
import FormCardSticky from "@/components/Form/FormCardSticky.vue";
12+
import LoadingSpan from "@/components/LoadingSpan.vue";
13+
14+
const { currentHistoryId } = useHistoryStore();
15+
16+
interface Props {
17+
targets: FetchTargets;
18+
}
19+
20+
const props = defineProps<Props>();
21+
const fetchGrids = ref();
22+
const errorText = ref<string | null>(null);
23+
const description = "(review data to be imported)";
24+
const title = "Import Data";
25+
const runTitle = "Import";
26+
const runTooltip = "Begin data import";
27+
28+
interface FetchGridComponent {
29+
asTargets(): FetchTargets;
30+
}
31+
32+
async function onExecute() {
33+
if (!currentHistoryId) {
34+
console.log("Logic error - no current history ID set, cannot execute fetch job.");
35+
return;
36+
}
37+
const component = fetchGrids.value as FetchGridComponent;
38+
const request = component.asTargets();
39+
fetchAndWatch({
40+
targets: request,
41+
history_id: currentHistoryId,
42+
});
43+
}
44+
45+
const { fetchAndWatch, fetchComplete, fetchError, waitingOnFetch } = useFetchJobMonitor();
46+
</script>
47+
48+
<template>
49+
<FormCardSticky
50+
:error-message="errorText || ''"
51+
:description="description"
52+
:name="title"
53+
:icon="faUpload"
54+
:version="undefined">
55+
<template v-slot:buttons>
56+
<b-button-group class="tool-card-buttons">
57+
<ButtonSpinner
58+
id="execute"
59+
class="text-nowrap"
60+
:title="runTitle"
61+
:disabled="!currentHistoryId"
62+
size="small"
63+
:wait="waitingOnFetch"
64+
:tooltip="runTooltip"
65+
@onClick="onExecute" />
66+
</b-button-group>
67+
</template>
68+
<template v-slot>
69+
<LoadingSpan v-if="!currentHistoryId" />
70+
<BAlert v-else-if="fetchComplete" variant="success" show> Data imported successfully. </BAlert>
71+
<BAlert v-else-if="fetchError" variant="danger" show> Error importing data: {{ fetchError }} </BAlert>
72+
<BAlert v-else-if="waitingOnFetch" variant="info" show>
73+
<LoadingSpan message="Importing data" />
74+
</BAlert>
75+
<FetchGrids v-else ref="fetchGrids" :targets="props.targets" />
76+
</template>
77+
</FormCardSticky>
78+
</template>
79+
80+
<style lang="scss" scoped>
81+
.tool-card-buttons {
82+
height: 2em;
83+
}
84+
</style>

0 commit comments

Comments
 (0)