Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Empower Users to Build More Kinds of Collections, More Intelligently #19377

Draft
wants to merge 37 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
02eeb09
Refactor collection name input out of CollectionCreator.
jmchilton Dec 22, 2024
bed1d57
Refactor the source options out of collection creator.
jmchilton Dec 22, 2024
e0fd0ad
Refactor collection creator help out to simplify.
jmchilton Dec 22, 2024
d6a19d0
Refactor showing of extensions into own component.
jmchilton Dec 22, 2024
6806a43
Refactor collection creator footer buttons into a component.
jmchilton Dec 22, 2024
f4a3e57
Refactor no items message out of collection creator for reuse.
jmchilton Dec 22, 2024
75bb1f5
Don't show upload files option when selecting datasets.
jmchilton Dec 22, 2024
265fed5
Remove unused mixin...
jmchilton Dec 26, 2024
2742343
Comment out unused style...
jmchilton Dec 22, 2024
00c9116
Refactor paired list creator for reuse, simplicity.
jmchilton Dec 21, 2024
3b78f3e
Simplification to paired list creator.
jmchilton Dec 21, 2024
56d4265
Migrate more of list collection builder out for reuse.
jmchilton Dec 21, 2024
4447a86
Composable to simplify CollectionCreator, reuse.
jmchilton Jan 2, 2025
1f53ef9
Code de-duplication for element filtering using composable.
jmchilton Dec 22, 2024
fe60a1f
Spelling fix in ListCollectionCreator.
jmchilton Dec 22, 2024
8571700
Cleanup list:paired builder comments...
jmchilton Dec 22, 2024
8d8658d
bugfix: list collection builder should strip extensions...
jmchilton Dec 24, 2024
28961a8
useCollectionCreator for de-duplication.
jmchilton Dec 24, 2024
9f19f26
Build dataset collection input definition on the client.
jmchilton Dec 10, 2024
2b5ce4b
Gray out repeat buttons that don't make sense.
jmchilton Dec 11, 2024
e487e5b
Reuse in FormData.vue.
jmchilton Dec 12, 2024
8fa4467
Decompose FormRepeat for reuse.
jmchilton Dec 18, 2024
8d46cbc
Event name: clicked-create -> on-create
jmchilton Dec 24, 2024
a6271bf
Reformat list collection creator...
jmchilton Dec 24, 2024
0445d06
Better typing around collection API calls in client.
jmchilton Dec 24, 2024
2deaec9
Allow swapping handsontable with AG Grid Community in Rule Builder.
jmchilton Dec 13, 2024
a8a7fe8
New components for seeding the rule builder.
jmchilton Dec 15, 2024
346a04e
Implement list building wizard.
jmchilton Dec 22, 2024
1275b58
[WIP] Implement records - heterogenous dataset collections.
jmchilton May 18, 2020
db4ef5d
record ui.
jmchilton Jan 4, 2025
906f674
Database migration for fixed length collection migraton.
jmchilton Dec 9, 2024
1e5bb8b
Migrate doctests to unit tests.
jmchilton Dec 19, 2024
f81f8ba
More dataset collection unit testing...
jmchilton Dec 21, 2024
6e097ed
Start trying to formalize dataset collection semantics.
jmchilton Dec 29, 2024
d109975
Implement paired_or_unpaired collections...
jmchilton Dec 30, 2024
416f5a8
Activity for rule builder imports.
jmchilton Jan 2, 2025
7f2b5cb
Remove individual collection builders from history dropdown.
jmchilton Jan 6, 2025
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
4 changes: 4 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@
"@types/jest": "^29.5.12",
"@vueuse/core": "^10.5.0",
"@vueuse/math": "^10.9.0",
"ag-grid-community": "^30",
"ag-grid-vue": "^30",
"assert": "^2.1.0",
"axios": "^1.6.2",
"babel-runtime": "^6.26.0",
Expand Down Expand Up @@ -106,11 +108,13 @@
"vega-embed": "^6.26.0",
"vega-lite": "^5.21.0",
"vue": "^2.7.14",
"vue-class-component": "^7.2.6",
"vue-echarts": "^7.0.3",
"vue-infinite-scroll": "^2.0.2",
"vue-multiselect": "^2.1.7",
"vue-observe-visibility": "^1.0.0",
"vue-prismjs": "^1.2.0",
"vue-property-decorator": "^9.1.2",
"vue-router": "^3.6.5",
"vue-rx": "^6.2.0",
"vue-virtual-scroll-list": "^2.3.5",
Expand Down
51 changes: 50 additions & 1 deletion client/src/api/datasetCollections.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { type CollectionEntry, type DCESummary, GalaxyApi, type HDCADetailed, type HDCASummary, isHDCA } from "@/api";
import {
type CollectionEntry,
type components,
type DCESummary,
GalaxyApi,
type HDCADetailed,
type HDCASummary,
isHDCA,
} from "@/api";
import { rethrowSimple } from "@/utils/simple-error";

const DEFAULT_LIMIT = 50;
Expand Down Expand Up @@ -76,3 +84,44 @@ export async function fetchElementsFromCollection(params: {
limit: params.limit ?? DEFAULT_LIMIT,
});
}

export type CollectionElementIdentifiers = components["schemas"]["CollectionElementIdentifier"][];
export type CreateNewCollectionPayload = components["schemas"]["CreateNewCollectionPayload"];

export type NewCollectionOptions = {
name: string;
element_identifiers: CollectionElementIdentifiers;
collection_type: string;
history_id: string;
copy_elements?: boolean;
hide_source_items?: boolean;
};

export function createCollectionPayload(options: NewCollectionOptions): CreateNewCollectionPayload {
return {
name: options.name,
history_id: options.history_id,
element_identifiers: options.element_identifiers,
collection_type: options.collection_type,
instance_type: "history",
fields: "auto",
copy_elements: options.copy_elements || true,
hide_source_items: options.hide_source_items || true,
};
}

export async function createHistoryDatasetCollectionInstanceSimple(options: NewCollectionOptions) {
const payload = createCollectionPayload(options);
return createHistoryDatasetCollectionInstanceFull(payload);
}

export async function createHistoryDatasetCollectionInstanceFull(payload: CreateNewCollectionPayload) {
const { data, error } = await GalaxyApi().POST("/api/dataset_collections", {
body: payload,
});

if (error) {
rethrowSimple(error);
}
return data;
}
1 change: 1 addition & 0 deletions client/src/api/datasets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export async function copyDataset(
// TODO: Investigate. These should be optional, but the API requires explicit null values?
type,
copy_elements: null,
fields: null,
hide_source_items: null,
instance_type: null,
},
Expand Down
3 changes: 3 additions & 0 deletions client/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,9 @@ export type DatasetTransform = {
*/
export type MessageException = components["schemas"]["MessageExceptionModel"];

export type FieldDict = components["schemas"]["FieldDict"];
export type FieldType = FieldDict["type"];

export type StoreExportPayload = components["schemas"]["StoreExportPayload"];
export type ModelStoreFormat = components["schemas"]["ModelStoreFormat"];
export type ObjectExportTaskResponse = components["schemas"]["ObjectExportTaskResponse"];
Expand Down
23 changes: 23 additions & 0 deletions client/src/api/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6948,6 +6948,12 @@ export interface components {
* @description List of elements that should be in the new collection.
*/
element_identifiers?: components["schemas"]["CollectionElementIdentifier"][] | null;
/**
* Fields
* @description List of fields to create for this collection. Set to 'auto' to guess fields from identifiers.
* @default []
*/
fields: string | components["schemas"]["FieldDict"][] | null;
/**
* Folder Id
* @description The ID of the library folder that will contain the collection. Required if `instance_type=library`.
Expand Down Expand Up @@ -7140,6 +7146,12 @@ export interface components {
* @description List of elements that should be in the new collection.
*/
element_identifiers?: components["schemas"]["CollectionElementIdentifier"][] | null;
/**
* Fields
* @description List of fields to create for this collection. Set to 'auto' to guess fields from identifiers.
* @default []
*/
fields: string | components["schemas"]["FieldDict"][] | null;
/**
* Folder Id
* @description The ID of the library folder that will contain the collection. Required if `instance_type=library`.
Expand Down Expand Up @@ -9080,6 +9092,17 @@ export interface components {
/** Hash Value */
hash_value: string;
};
/** FieldDict */
FieldDict: {
/** Format */
format?: string | null;
/** Name */
name: string;
/** Type */
type:
| ("File" | "null" | "boolean" | "int" | "float" | "string")
| ("File" | "null" | "boolean" | "int" | "float" | "string")[];
};
/** FileDataElement */
FileDataElement: {
/** Md5 */
Expand Down
220 changes: 220 additions & 0 deletions client/src/components/Collections/BuildFileSetWizard.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,220 @@
<script setup lang="ts">
import { BCardGroup } from "bootstrap-vue";
import { computed, ref } from "vue";

import { getGalaxyInstance } from "@/app";
import { attemptCreate, type CollectionCreatorComponent } from "@/components/Collections/common/useCollectionCreator";
import { rawToTable } from "@/components/Collections/tables";
import { useWizard } from "@/components/Common/Wizard/useWizard";
import { useToolRouting } from "@/composables/route";

import { type RemoteFile, type RulesCreatingWhat, type RulesSourceFrom } from "./wizard/types";
import { useFileSetSources } from "./wizard/useFileSetSources";

import CreatingWhat from "./wizard/CreatingWhat.vue";
import PasteData from "./wizard/PasteData.vue";
import SelectDataset from "./wizard/SelectDataset.vue";
import SelectFolder from "./wizard/SelectFolder.vue";
import SourceFromCollection from "./wizard/SourceFromCollection.vue";
import SourceFromDatasetAsTable from "./wizard/SourceFromDatasetAsTable.vue";
import SourceFromPastedData from "./wizard/SourceFromPastedData.vue";
import SourceFromRemoteFiles from "./wizard/SourceFromRemoteFiles.vue";
import GenericWizard from "@/components/Common/Wizard/GenericWizard.vue";
import RuleCollectionBuilder from "@/components/RuleCollectionBuilder.vue";

const isBusy = ref<boolean>(false);
const { pasteData, tabularDatasetContents, uris, setRemoteFilesFolder, onFtp, setDatasetContents, setPasteTable } =
useFileSetSources(isBusy);

interface Props {
fileSourcesConfigured: boolean;
ftpUploadSite?: string;
mode: "uploadModal" | "standalone";
}

const props = defineProps<Props>();

const ruleState = ref(false);
const creatingWhat = ref<RulesCreatingWhat>("datasets");
const creatingWhatTitle = computed(() => {
return creatingWhat.value == "datasets" ? "Datasets" : "Collections";
});
const sourceInstructions = computed(() => {
return `${creatingWhatTitle.value} can be created from a set or files, URIs, or existing datasets.`;
});
const collectionCreator = ref<CollectionCreatorComponent | undefined>();

const sourceFrom = ref<RulesSourceFrom>("remote_files");

const { routeToTool } = useToolRouting();

const wizard = useWizard({
"select-what": {
label: "What is being created?",
instructions: computed(() => {
return `Are you creating datasets or collections?`;
}),
isValid: () => true,
isSkippable: () => false,
},
"select-source": {
label: "Select source",
instructions: sourceInstructions,
isValid: () => true,
isSkippable: () => false,
},
"select-remote-files-folder": {
label: "Select folder",
instructions: "Select folder of files to import.",
isValid: () => sourceFrom.value === "remote_files" && Boolean(uris.value.length > 0),
isSkippable: () => sourceFrom.value !== "remote_files",
},
"paste-data": {
label: "Paste data",
instructions: "Paste data (or drop file) containing URIs and optional extra metadata.",
isValid: () => sourceFrom.value === "pasted_table" && pasteData.value.length > 0,
isSkippable: () => sourceFrom.value !== "pasted_table",
},
"select-dataset": {
label: "Select dataset",
instructions: "Select tabular dataset to load URIs and metadata from.",
isValid: () => sourceFrom.value === "dataset_as_table" && tabularDatasetContents.value.length > 0,
isSkippable: () => sourceFrom.value !== "dataset_as_table",
},
"rule-builder": {
label: "Specify Rules",
instructions: "Use this form to describe rules for importing",
isValid: () => ruleState.value,
isSkippable: () => props.mode == "uploadModal",
width: "100%",
},
});

const importButtonLabel = computed(() => {
if (sourceFrom.value == "collection") {
return "Transform";
} else {
return "Import";
}
});

const emit = defineEmits(["dismiss", "created"]);
type SelectionType = "raw" | "remote_files";
type ElementsType = RemoteFile[] | string[][];

// it would be nice to have a real type from the rule builder but
// it is older code. This is really outlining what this component can
// produce and not what the rule builder can consume which is a wide
// superset of this.
interface Entry {
dataType: RulesCreatingWhat;
ftpUploadSite?: string;
elements?: ElementsType | undefined;
content?: string;
selectionType: SelectionType;
}

const ruleBuilderModalEntryProps = computed(() => {
let elements: ElementsType | undefined = undefined;
let selectionType: SelectionType = "raw";
if (sourceFrom.value == "remote_files") {
elements = uris.value;
selectionType = "remote_files";
} else if (sourceFrom.value == "pasted_table") {
elements = pasteData.value;
} else if (sourceFrom.value == "dataset_as_table") {
elements = tabularDatasetContents.value;
}
const entry: Entry = {
dataType: creatingWhat.value,
ftpUploadSite: props.ftpUploadSite,
selectionType: selectionType,
elements: elements,
};
return entry;
});

const ruleBuilderElements = computed(() => {
const builderProps = ruleBuilderModalEntryProps.value;
let elements;
if (builderProps.elements) {
elements = builderProps.elements;
} else {
elements = rawToTable(builderProps.content || "");
}
return elements;
});

function launchRuleBuilder() {
const Galaxy = getGalaxyInstance();
const entry = ruleBuilderModalEntryProps.value;
Galaxy.currHistoryPanel.buildCollectionFromRules(entry, null, true);
}

function submit() {
if (sourceFrom.value == "collection") {
routeToTool("__APPLY_RULES__");
} else if (props.mode == "standalone") {
attemptCreate(collectionCreator);
} else {
launchRuleBuilder();
}
emit("dismiss");
}

function setCreatingWhat(what: RulesCreatingWhat) {
creatingWhat.value = what;
}

function setSourceForm(newValue: RulesSourceFrom) {
sourceFrom.value = newValue;
}

function onRuleState(newRuleState: boolean) {
ruleState.value = newRuleState;
}

function onRuleCreate() {
// axios response data for job currently sent, not really used but wanted to document what is available.
emit("created");
}
</script>

<template>
<GenericWizard :use="wizard" :submit-button-label="importButtonLabel" @submit="submit">
<div v-if="wizard.isCurrent('select-what')">
<CreatingWhat :creating-what="creatingWhat" @onChange="setCreatingWhat" />
</div>
<div v-else-if="wizard.isCurrent('select-source')">
<BCardGroup deck>
<SourceFromRemoteFiles :selected="sourceFrom === 'remote_files'" @select="setSourceForm" />
<SourceFromPastedData :selected="sourceFrom === 'pasted_table'" @select="setSourceForm" />
<SourceFromDatasetAsTable :selected="sourceFrom === 'dataset_as_table'" @select="setSourceForm" />
<SourceFromCollection
v-if="creatingWhat == 'collections'"
:selected="sourceFrom === 'collection'"
@select="setSourceForm" />
</BCardGroup>
</div>
<div v-else-if="wizard.isCurrent('paste-data')">
<PasteData @onChange="setPasteTable" />
</div>
<div v-else-if="wizard.isCurrent('select-remote-files-folder')">
<SelectFolder :ftp-upload-site="ftpUploadSite" @onChange="setRemoteFilesFolder" @onFtp="onFtp" />
</div>
<div v-else-if="wizard.isCurrent('select-dataset')">
<SelectDataset @onChange="setDatasetContents" />
</div>
<div v-else-if="wizard.isCurrent('rule-builder')" style="width: 100%">
<RuleCollectionBuilder
ref="collectionCreator"
grid-implementation="ag"
:import-type="creatingWhat"
:elements-type="ruleBuilderModalEntryProps.selectionType"
:initial-elements="ruleBuilderElements"
mode="wizard"
@onCreate="onRuleCreate"
@validInput="onRuleState" />
</div>
</GenericWizard>
</template>
Loading
Loading