Skip to content

Commit 3b13afc

Browse files
committed
Implement data landing UI.
1 parent b8414cf commit 3b13afc

File tree

14 files changed

+1480
-25
lines changed

14 files changed

+1480
-25
lines changed

client/src/api/jobs.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ export type JobInputSummary = components["schemas"]["JobInputSummary"];
88
export type JobDisplayParametersSummary = components["schemas"]["JobDisplayParametersSummary"];
99
export type JobMetric = components["schemas"]["JobMetric"];
1010

11+
export const NON_TERMINAL_STATES = ["new", "queued", "running", "waiting"];
12+
export const ERROR_STATES = ["error", "deleted"];
13+
export const TERMINAL_STATES = ["ok", "skipped"].concat(ERROR_STATES);
14+
1115
interface JobDef {
1216
tool_id: string;
1317
}

client/src/api/tools.ts

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { type components, GalaxyApi } from "@/api";
2+
import { ERROR_STATES, type ShowFullJobResponse } from "@/api/jobs";
3+
4+
export type HdcaUploadTarget = components["schemas"]["HdcaDataItemsTarget"];
5+
export type HdasUploadTarget = components["schemas"]["DataElementsTarget"];
6+
export type FetchDataPayload = components["schemas"]["FetchDataPayload"];
7+
export type UrlDataElement = components["schemas"]["UrlDataElement"];
8+
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];
13+
14+
export function urlDataElement(identifier: string, uri: string): UrlDataElement {
15+
const element: UrlDataElement = {
16+
src: "url",
17+
url: uri,
18+
name: identifier,
19+
// these shouldn't be required but the way our model -> ts stuff works it is...
20+
auto_decompress: false,
21+
dbkey: "?",
22+
ext: "auto",
23+
to_posix_lines: false,
24+
deferred: false,
25+
space_to_tab: false,
26+
};
27+
return element;
28+
}
29+
30+
export function nestedElement(identifier: string, elements: NestedElementItems) {
31+
const nestedElement: NestedElement = {
32+
name: identifier,
33+
elements: elements,
34+
auto_decompress: false,
35+
dbkey: "?",
36+
ext: "auto",
37+
to_posix_lines: false,
38+
deferred: false,
39+
space_to_tab: false,
40+
};
41+
return nestedElement;
42+
}
43+
44+
export async function fetch(payload: FetchDataPayload) {
45+
const { data } = await GalaxyApi().POST("/api/tools/fetch", {
46+
body: payload,
47+
});
48+
return fetchResponseToJobId(data as FetchResponseInterface);
49+
}
50+
51+
interface FetchResponseInterface {
52+
jobs: { id: string }[];
53+
}
54+
55+
function fetchResponseToJobId(response: FetchResponseInterface) {
56+
return response.jobs[0]!.id;
57+
}
58+
59+
export function fetchJobErrorMessage(jobDetails: ShowFullJobResponse): string | undefined {
60+
const stderr = jobDetails.stderr;
61+
let errorMessage: string | undefined = undefined;
62+
if (stderr) {
63+
errorMessage = "An error was encountered while running your upload job. ";
64+
if (stderr.indexOf("binary file contains inappropriate content") > -1) {
65+
errorMessage +=
66+
"The problem may be that the batch uploader will not automatically decompress your files the way the normal uploader does, please specify a correct extension or upload decompressed data.";
67+
}
68+
errorMessage += "Upload job completed with standard error: " + stderr;
69+
} else if (ERROR_STATES.indexOf(jobDetails.state) !== -1) {
70+
errorMessage =
71+
"Unknown error encountered while running your data import job, this could be a server issue or a problem with the upload definition.";
72+
}
73+
return errorMessage;
74+
}
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 { CardAttributes } 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: CardAttributes = {
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: CardAttributes = {
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<CardAttributes[]>(() => {
204+
if (!richSupportForTarget.value) {
205+
return [];
206+
}
207+
208+
const actions: CardAttributes[] = [];
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>

0 commit comments

Comments
 (0)