mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-10-28 02:58:18 +08:00
1901 lines
80 KiB
Vue
1901 lines
80 KiB
Vue
<template>
|
|
<state-div v-if="state == 'build'">
|
|
<!-- Different instructions if building up from individual datasets vs.
|
|
initial data import. -->
|
|
<rule-modal-header v-if="ruleView == 'source'">
|
|
Below is a raw JSON description of the rules to apply to the tabular data. This is an advanced setting.
|
|
</rule-modal-header>
|
|
<rule-modal-header v-else-if="elementsType == 'datasets' || elementsType == 'library_datasets'">
|
|
Use this form to describe rules for building collection(s) from the specified datasets. <b>Be sure to specify at least one column as a list identifier</b> - specify more to created nested list structures. Specify a column to serve as "collection name" to group datasets into multiple collections.
|
|
</rule-modal-header>
|
|
<!-- This modality allows importing individual datasets, multiple collections,
|
|
and requires a data source - note that. -->
|
|
<rule-modal-header v-else-if="importType == 'datasets'">
|
|
Use this form to describe rules for import datasets. At least one column should be defined to a source to fetch data from (URLs, FTP files, etc...).
|
|
</rule-modal-header>
|
|
<rule-modal-header v-else>
|
|
Use this form to describe rules for import datasets. At least one column should be defined to a source to fetch data from (URLs, FTP files, etc...). <b>Be sure to specify at least one column as a list identifier</b> - specify more to created nested list structures. Specify a column to serve as "collection name" to group datasets into multiple collections.
|
|
</rule-modal-header>
|
|
<rule-modal-middle v-if="ruleView == 'source'">
|
|
<p class="errormessagelarge" v-if="ruleSourceError">{{ ruleSourceError }}</p>
|
|
<textarea class="rule-source" v-model="ruleSource"></textarea>
|
|
</rule-modal-middle>
|
|
<rule-modal-middle v-else>
|
|
<!-- column-headers -->
|
|
<div class="rule-builder-body vertically-spaced"
|
|
v-bind:class="{ 'flex-column-container': vertical }" v-if="ruleView == 'normal'">
|
|
<!-- width: 30%; -->
|
|
<div class="rule-column" v-bind:class="orientation">
|
|
<div class="rules-container" v-bind:class="{'rules-container-vertical': initialElements && vertical, 'rules-container-horizontal': initialElements && horizontal, 'rules-container-full': initialElements == null}">
|
|
<rule-component rule-type="sort"
|
|
:display-rule-type="displayRuleType"
|
|
:builder="this">
|
|
<column-selector :target.sync="addSortingTarget" :col-headers="activeRuleColHeaders" />
|
|
<label v-b-tooltip.hover :title="titleNumericSort">
|
|
<input type="checkbox" v-model="addSortingNumeric" />
|
|
{{ l("Numeric sorting.") }}
|
|
</label>
|
|
</rule-component>
|
|
<rule-component rule-type="add_column_basename"
|
|
:display-rule-type="displayRuleType"
|
|
:builder="this">
|
|
<column-selector :target.sync="addColumnBasenameTarget" :col-headers="activeRuleColHeaders" />
|
|
</rule-component>
|
|
<rule-component rule-type="add_column_rownum"
|
|
:display-rule-type="displayRuleType"
|
|
:builder="this">
|
|
<label>
|
|
{{ l("Starting from") }}
|
|
<input type="number" v-model="addColumnRownumStart" min="0" />
|
|
</label>
|
|
</rule-component>
|
|
<rule-component rule-type="add_column_metadata"
|
|
:display-rule-type="displayRuleType"
|
|
:builder="this">
|
|
<label>
|
|
{{ l("For") }}
|
|
<select v-model="addColumnMetadataValue">
|
|
<option v-for="(col, index) in metadataOptions" :value="index">{{ col }}</option>
|
|
</select>
|
|
</label>
|
|
</rule-component>
|
|
<rule-component rule-type="add_column_regex"
|
|
:display-rule-type="displayRuleType"
|
|
:builder="this">
|
|
<column-selector :target.sync="addColumnRegexTarget" :col-headers="activeRuleColHeaders" />
|
|
<input type="radio" v-model="addColumnRegexType" value="global">Create column matching expression.<br />
|
|
<input type="radio" v-model="addColumnRegexType" value="groups">Create columns matching expression groups.<br />
|
|
<input type="radio" v-model="addColumnRegexType" value="replacement">Create column from expression replacement.<br />
|
|
<regular-expression-input :target.sync="addColumnRegexExpression" />
|
|
<label v-if="addColumnRegexType=='groups'">
|
|
{{ l("Number of Groups") }}
|
|
<input type="number" v-model="addColumnRegexGroupCount" min="1" />
|
|
</label>
|
|
<label v-if="addColumnRegexType=='replacement'">
|
|
{{ l("Replacement Expression") }}
|
|
<input type="text" v-model="addColumnRegexReplacement" class="rule-replacement" />
|
|
</label>
|
|
</rule-component>
|
|
<rule-component rule-type="add_column_concatenate"
|
|
:display-rule-type="displayRuleType"
|
|
:builder="this">
|
|
<column-selector :target.sync="addColumnConcatenateTarget0" :col-headers="activeRuleColHeaders" />
|
|
<column-selector :target.sync="addColumnConcatenateTarget1" :col-headers="activeRuleColHeaders" />
|
|
</rule-component>
|
|
<rule-component rule-type="add_column_substr"
|
|
:display-rule-type="displayRuleType"
|
|
:builder="this">
|
|
<column-selector :target.sync="addColumnSubstrTarget" :col-headers="activeRuleColHeaders" />
|
|
<label>
|
|
<select v-model="addColumnSubstrType">
|
|
<option value="keep_prefix">Keep only prefix specified.</option>
|
|
<option value="drop_prefix">Strip off prefix specified.</option>
|
|
<option value="keep_suffix">Keep only suffix specified.</option>
|
|
<option value="drop_suffix">Strip off suffix specified.</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
{{ l("Prefix or suffix length") }}
|
|
<input type="number" v-model="addColumnSubstrLength" min="0" />
|
|
</label>
|
|
</rule-component>
|
|
<rule-component rule-type="add_column_value"
|
|
:display-rule-type="displayRuleType"
|
|
:builder="this">
|
|
<label>
|
|
{{ l("Value") }}
|
|
<input type="text" v-model="addColumnValue" />
|
|
</label>
|
|
</rule-component>
|
|
<rule-component rule-type="remove_columns"
|
|
:display-rule-type="displayRuleType"
|
|
:builder="this">
|
|
<column-selector :target.sync="removeColumnTargets" :col-headers="activeRuleColHeaders" :multiple="true" />
|
|
</rule-component>
|
|
<rule-component rule-type="split_columns"
|
|
:display-rule-type="displayRuleType"
|
|
:builder="this">
|
|
<column-selector :target.sync="splitColumnsTargets0" label="Odd Row Column(s)" :col-headers="activeRuleColHeaders" :multiple="true" />
|
|
<column-selector :target.sync="splitColumnsTargets1" label="Even Row Column(s)" :col-headers="activeRuleColHeaders" :multiple="true" />
|
|
</rule-component>
|
|
<rule-component rule-type="swap_columns"
|
|
:display-rule-type="displayRuleType"
|
|
:builder="this">
|
|
<column-selector :target.sync="swapColumnsTarget0" label="Swap Column" :col-headers="activeRuleColHeaders" />
|
|
<column-selector :target.sync="swapColumnsTarget1" label="With Column" :col-headers="activeRuleColHeaders" />
|
|
</rule-component>
|
|
<rule-component rule-type="add_filter_regex"
|
|
:display-rule-type="displayRuleType"
|
|
:builder="this">
|
|
<column-selector :target.sync="addFilterRegexTarget" :col-headers="activeRuleColHeaders" />
|
|
<regular-expression-input :target.sync="addFilterRegexExpression" />
|
|
<label v-b-tooltip.hover :title="titleInvertFilterRegex">
|
|
<input type="checkbox" v-model="addFilterRegexInvert" />
|
|
{{ l("Invert filter.") }}
|
|
</label>
|
|
</rule-component>
|
|
<rule-component rule-type="add_filter_matches"
|
|
:display-rule-type="displayRuleType"
|
|
:builder="this">
|
|
<column-selector :target.sync="addFilterMatchesTarget" :col-headers="activeRuleColHeaders" />
|
|
<input type="text" v-model="addFilterMatchesValue" />
|
|
<label v-b-tooltip.hover :title="titleInvertFilterMatches">
|
|
<input type="checkbox" v-model="addFilterMatchesInvert" />
|
|
{{ l("Invert filter.") }}
|
|
</label>
|
|
</rule-component>
|
|
<rule-component rule-type="add_filter_compare"
|
|
:display-rule-type="displayRuleType"
|
|
:builder="this">
|
|
<column-selector :target.sync="addFilterCompareTarget" :col-headers="activeRuleColHeaders" />
|
|
<label>
|
|
Filter out rows
|
|
<select v-model="addFilterCompareType">
|
|
<option value="less_than">{{ l("less than") }} </option>
|
|
<option value="less_than_equal">{{ l("less than or equal to") }}</option>
|
|
<option value="greater_than">{{ l("greater than") }}</option>
|
|
<option value="greater_than_equal">{{ l("greater than or equal to") }}</option>
|
|
</select>
|
|
</label>
|
|
<input type="text" v-model="addFilterCompareValue" />
|
|
</rule-component>
|
|
<rule-component rule-type="add_filter_count"
|
|
:display-rule-type="displayRuleType"
|
|
:builder="this">
|
|
<label>
|
|
Filter which rows?
|
|
<select v-model="addFilterCountWhich">
|
|
<option value="first">first</option>
|
|
<option value="last">last</option>
|
|
</select>
|
|
</label>
|
|
<label>
|
|
Filter how many rows?
|
|
<input type="number" v-model="addFilterCountN" />
|
|
</label>
|
|
<label v-b-tooltip.hover :title="titleInvertFilterMatches">
|
|
<input type="checkbox" v-model="addFilterCountInvert" />
|
|
{{ l("Invert filter.") }}
|
|
</label>
|
|
</rule-component>
|
|
<rule-component rule-type="add_filter_empty"
|
|
:display-rule-type="displayRuleType"
|
|
:builder="this">
|
|
<column-selector :target.sync="addFilterEmptyTarget" :col-headers="activeRuleColHeaders" />
|
|
<label v-b-tooltip.hover :title="titleInvertFilterEmpty">
|
|
<input type="checkbox" v-model="addFilterEmptyInvert" />
|
|
{{ l("Invert filter.") }}
|
|
</label>
|
|
</rule-component>
|
|
<div v-if="displayRuleType == 'mapping'">
|
|
<div class="map"
|
|
v-for="(map, index) in mapping"
|
|
v-bind:index="index"
|
|
v-bind:key="map.type">
|
|
<column-selector
|
|
:class="'rule-map-' + map.type.replace(/_/g, '-')"
|
|
:label="mappingTargets()[map.type].label"
|
|
:help="mappingTargets()[map.type].help"
|
|
:target.sync="map.columns"
|
|
:ordered-edit.sync="map.editing"
|
|
:col-headers="colHeaders"
|
|
:multiple="mappingTargets()[map.type].multiple"
|
|
:ordered="true"
|
|
:value-as-list="true">
|
|
<span v-b-tooltip.hover :title="titleRemoveMapping" class="fa fa-times" @click="removeMapping(index)"></span>
|
|
</column-selector>
|
|
</div>
|
|
<div class="buttons rule-edit-buttons d-flex justify-content-end">
|
|
<button v-b-tooltip.hover :title="titleAddColumnDefinition" type="button" class="dropdown-toggle btn btn-primary mr-1" data-toggle="dropdown" v-if="unmappedTargets.length > 0">
|
|
<span class="fa fa-plus rule-add-mapping"></span> {{ "Add Definition" }}<span class="caret"></span>
|
|
</button>
|
|
<div class="dropdown-menu" role="menu">
|
|
<a v-for="target in unmappedTargets"
|
|
v-bind:index="target"
|
|
v-bind:key="target"
|
|
class="dropdown-item"
|
|
href="javascript:void(0)"
|
|
:class="'rule-add-mapping-' + target.replace(/_/g, '-')"
|
|
@click="addIdentifier(target)">
|
|
{{ mappingTargets()[target].label }}
|
|
</a>
|
|
</div>
|
|
<b-button v-b-tooltip.hover.bottom :title="titleApplyColumnDefinitions" class="rule-mapping-ok" v-if="!hasActiveMappingEdit" @click="displayRuleType = null">{{ l("Apply") }}</b-button>
|
|
</div>
|
|
</div>
|
|
<div class="rule-summary" v-if="displayRuleType == null">
|
|
<span class="title">
|
|
{{ l("Rules") }}
|
|
<span v-b-tooltip.hover class="fa fa-wrench rule-builder-view-source" :title="titleViewSource" @click="viewSource"></span>
|
|
</span>
|
|
<div v-if="jaggedData" class="rule-warning">
|
|
Rows contain differing numbers of columns, there was likely a problem parsing your data.
|
|
</div>
|
|
<ol class="rules">
|
|
<!-- Example at the end of https://vuejs.org/v2/guide/list.html -->
|
|
<rule-display
|
|
v-for="(rule, index) in rules"
|
|
v-bind:rule="rule"
|
|
v-bind:index="index"
|
|
v-bind:key="index"
|
|
@edit="editRule(rule, index)"
|
|
@remove="removeRule(index)"
|
|
:col-headers="colHeadersPerRule[index]" />
|
|
<identifier-display v-for="(map, index) in mapping"
|
|
v-bind="map"
|
|
v-bind:index="index"
|
|
v-bind:key="map.type"
|
|
@remove="removeMapping(index)"
|
|
@edit="displayRuleType = 'mapping'"
|
|
v-on:mouseover.native="map.columns.forEach((col) => highlightColumn(col))"
|
|
v-on:mouseout.native="map.columns.forEach((col) => unhighlightColumn(col))"
|
|
:col-headers="colHeaders" />
|
|
<div v-if="mapping.length == 0">
|
|
One or more column definitions must be specified. These are required to specify how to build collections and datasets from rows and columns of the table. <a href="javascript:void(0)" @click="displayRuleType = 'mapping'">Click here</a> to manage column definitions.
|
|
</div>
|
|
</ol>
|
|
<div class="rules-buttons">
|
|
<div class="btn-group dropup">
|
|
<button type="button" v-b-tooltip.hover.bottom :title="titleRulesMenu" class="rule-menu-rules-button primary-button dropdown-toggle" data-toggle="dropdown">
|
|
<span class="fa fa-plus"></span> {{ l("Rules") }}<span class="caret"></span>
|
|
</button>
|
|
<div class="dropdown-menu" role="menu">
|
|
<rule-target-component :builder="this" rule-type="sort" />
|
|
<rule-target-component :builder="this" rule-type="remove_columns" />
|
|
<rule-target-component :builder="this" rule-type="split_columns" />
|
|
<rule-target-component :builder="this" rule-type="swap_columns" />
|
|
<a href="javascript:void(0)" class="dropdown-item rule-link rule-link-mapping" @click="displayRuleType = 'mapping'">Add / Modify Column Definitions</a>
|
|
</div>
|
|
</div>
|
|
<div class="btn-group dropup">
|
|
<button type="button" v-b-tooltip.hover.bottom :title="titleFilterMenu" class="rule-menu-filter-button primary-button dropdown-toggle" data-toggle="dropdown">
|
|
<span class="fa fa-plus"></span> {{ l("Filter") }}<span class="caret"></span>
|
|
</button>
|
|
<div class="dropdown-menu" role="menu">
|
|
<rule-target-component :builder="this" rule-type="add_filter_regex" />
|
|
<rule-target-component :builder="this" rule-type="add_filter_matches" />
|
|
<rule-target-component :builder="this" rule-type="add_filter_compare" />
|
|
<rule-target-component :builder="this" rule-type="add_filter_empty" />
|
|
<rule-target-component :builder="this" rule-type="add_filter_count" />
|
|
</div>
|
|
</div>
|
|
<div class="btn-group dropup">
|
|
<button type="button" v-b-tooltip.hover.bottom :title="titleColumMenu" class="rule-menu-column-button primary-button dropdown-toggle" data-toggle="dropdown">
|
|
<span class="fa fa-plus"></span> {{ l("Column") }}<span class="caret"></span>
|
|
</button>
|
|
<div class="dropdown-menu" role="menu">
|
|
<rule-target-component :builder="this" rule-type="add_column_basename" />
|
|
<rule-target-component :builder="this" rule-type="add_column_metadata" v-if="elementsType == 'collection_contents'"/>
|
|
<rule-target-component :builder="this" rule-type="add_column_regex" />
|
|
<rule-target-component :builder="this" rule-type="add_column_concatenate" />
|
|
<rule-target-component :builder="this" rule-type="add_column_rownum" />
|
|
<rule-target-component :builder="this" rule-type="add_column_value" />
|
|
<rule-target-component :builder="this" rule-type="add_column_substr" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<!-- flex-column column -->
|
|
<!-- style="width: 70%;" -->
|
|
<div class="table-column" v-bind:class="orientation" style="width: 100%;" v-if="initialElements !== null">
|
|
<hot-table id="hot-table"
|
|
ref="hotTable"
|
|
:data="hotData['data']"
|
|
:colHeaders="colHeadersDisplay"
|
|
:readOnly="true"
|
|
stretchH="all">
|
|
</hot-table>
|
|
</div>
|
|
</div>
|
|
</rule-modal-middle>
|
|
<rule-modal-footer v-if="ruleView == 'source'">
|
|
<b-button v-b-tooltip.hover :title="titleSourceCancel" @click="cancelSourceEdit" class="rule-btn-cancel">
|
|
{{ l("Cancel") }}
|
|
</b-button>
|
|
<b-button v-b-tooltip.hover :title="titleSourceReset" class="creator-reset-btn rule-btn-reset">
|
|
{{ l("Reset") }}
|
|
</b-button>
|
|
<b-button v-b-tooltip.hover :title="titleSourceApply" @click="attemptRulePreview" class="rule-btn-okay">
|
|
{{ l("Apply")}}
|
|
</b-button>
|
|
</rule-modal-footer>
|
|
<rule-modal-footer v-else-if="ruleView == 'normal'">
|
|
<div class="rule-footer-inputs" slot="inputs">
|
|
<label v-if="elementsType == 'datasets'">
|
|
{{ l("Hide original elements") }}:
|
|
</label>
|
|
<input type="checkbox" v-model="hideSourceItems" v-if="elementsType == 'datasets'"/>
|
|
<div class="rule-footer-extension-group" v-if="extension && showFileTypeSelector">
|
|
<label>
|
|
{{ l("Type") }}:
|
|
</label>
|
|
<select2 name="extension" class="extension-select" v-model="extension" >
|
|
<option v-for="(col, index) in extensions" :value="col['id']"">{{ col["text"] }}</option>
|
|
</select2>
|
|
</div>
|
|
<div class="rule-footer-genome-group" v-if="genome && showGenomeSelector">
|
|
<label>
|
|
{{ l("Genome") }}:
|
|
</label>
|
|
<select2 class="genome-select" v-model="genome">
|
|
<option v-for="(col, index) in genomes" :value="col['id']"">{{ col["text"] }}</option>
|
|
</select2>
|
|
</div>
|
|
<div class="rule-footer-name-group" v-if="showCollectionNameInput">
|
|
<b-input class="collection-name"
|
|
:placeholder="namePlaceholder" :title="namePlaceholder" v-b-tooltip.hover v-model="collectionName" />
|
|
<label>
|
|
{{ l("Name") }}:
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<b-button v-b-tooltip.hover :help="titleCancel" @click="cancel" class="creator-cancel-btn rule-btn-cancel" tabindex="-1">
|
|
{{ l("Cancel") }}
|
|
</b-button>
|
|
<b-button v-b-tooltip.hover @click="resetRulesAndState" :title="titleReset" class="creator-reset-btn rule-btn-reset">
|
|
{{ l("Reset") }}
|
|
</b-button>
|
|
<b-button v-b-tooltip.hover @click="createCollection" :title="titleFinish" class="create-collection rule-btn-okay" variant="primary" :disabled="!validInput">
|
|
{{ finishButtonTitle }}
|
|
</b-button>
|
|
</rule-modal-footer>
|
|
</state-div>
|
|
<state-div v-else-if="state == 'wait'">
|
|
<rule-modal-header>
|
|
{{ l("Galaxy is waiting for collection creation, this dialog will close when this is complete.") }}
|
|
</rule-modal-header>
|
|
<rule-modal-footer>
|
|
<b-button @click="cancel" class="creator-cancel-btn" tabindex="-1">
|
|
{{ l("Close") }}
|
|
</b-button>
|
|
</rule-modal-footer>
|
|
</state-div>
|
|
<state-div v-else-if="state == 'error'">
|
|
<!-- TODO: steal styling from paired collection builder warning... -->
|
|
<rule-modal-header>
|
|
A problem was encountered.
|
|
</rule-modal-header>
|
|
<rule-modal-middle>
|
|
<p class="errormessagelarge">{{ errorMessage }}</p>
|
|
</rule-modal-middle>
|
|
<rule-modal-footer>
|
|
<b-button v-b-tooltip.hover :title="titleCancel" @click="cancel" class="creator-cancel-btn" tabindex="-1">
|
|
{{ l("Close") }}
|
|
</b-button>
|
|
<b-button v-b-tooltip.hover :title="titleErrorOkay" @click="state = 'build'" tabindex="-1">
|
|
{{ l("Okay") }}
|
|
</b-button>
|
|
</rule-modal-footer>
|
|
</state-div>
|
|
</template>
|
|
<script>
|
|
import AjaxQueue from "utils/ajax-queue";
|
|
import axios from "axios";
|
|
import _l from "utils/localization";
|
|
import HotTable from "@handsontable/vue";
|
|
import Popover from "mvc/ui/ui-popover";
|
|
import UploadUtils from "mvc/upload/upload-utils";
|
|
import JobStatesModel from "mvc/history/job-states-model";
|
|
import RuleDefs from "mvc/rules/rule-definitions";
|
|
import Vue from "vue";
|
|
import BootstrapVue from "bootstrap-vue";
|
|
|
|
Vue.use(BootstrapVue);
|
|
|
|
const RULES = RuleDefs.RULES;
|
|
const MAPPING_TARGETS = RuleDefs.MAPPING_TARGETS;
|
|
|
|
// Local components...
|
|
|
|
// Based on https://vuejs.org/v2/examples/select2.html but adapted to handle list values
|
|
// with "multiple: true" set.
|
|
const Select2 = {
|
|
props: ["options", "value", "placeholder"],
|
|
template: `<select>
|
|
<slot></slot>
|
|
</select>`,
|
|
mounted: function() {
|
|
var vm = this;
|
|
$(this.$el)
|
|
// init select2
|
|
.select2({ data: this.options, placeholder: this.placeholder, allowClear: this.placeholder })
|
|
.val(this.value)
|
|
.trigger("change")
|
|
// emit event on change.
|
|
.on("change", function(event) {
|
|
vm.$emit("input", event.val);
|
|
});
|
|
},
|
|
watch: {
|
|
value: function(value) {
|
|
// update value
|
|
$(this.$el).val(value);
|
|
},
|
|
options: function(options) {
|
|
// update options
|
|
$(this.$el)
|
|
.empty()
|
|
.select2({ data: options });
|
|
}
|
|
},
|
|
destroyed: function() {
|
|
$(this.$el)
|
|
.off()
|
|
.select2("destroy");
|
|
}
|
|
};
|
|
|
|
const ColumnSelector = {
|
|
template: `
|
|
<div class="rule-column-selector" v-if="!multiple || !ordered">
|
|
<label class="d-flex justify-content-end align-items-center">
|
|
<span class="mr-auto" v-b-tooltip.hover :title="help">{{ label }}</span>
|
|
<div class="mr-1" v-b-tooltip.hover :title="title"><select2 :value="target" @input="handleInput" :multiple="multiple">
|
|
<option v-for="(col, index) in colHeaders" :value="index">{{ col }}</option>
|
|
</select2></div>
|
|
<slot></slot>
|
|
</label>
|
|
</div>
|
|
<div class="rule-column-selector" v-else>
|
|
<span>{{ label }}</span>
|
|
<slot></slot>
|
|
<ol>
|
|
<li v-for="(targetEl, index) in target"
|
|
v-bind:index="index"
|
|
v-bind:key="targetEl"
|
|
class="rule-column-selector-target">
|
|
{{ colHeaders[targetEl] }}
|
|
<span class="fa fa-times rule-column-selector-target-remove" @click="handleRemove(index)"></span>
|
|
<span class="fa fa-arrow-up rule-column-selector-up" v-if="index !== 0" @click="moveUp(index)"></span>
|
|
<span class="fa fa-arrow-down rule-column-selector-down" v-if="index < target.length - 1" @click="moveUp(index + 1)"></span>
|
|
</li>
|
|
<li v-if="this.target.length < this.colHeaders.length">
|
|
<span class="rule-column-selector-target-add" v-if="!orderedEdit">
|
|
<i @click="$emit('update:orderedEdit', true)">... {{ l("Assign Another Column") }}</i>
|
|
</span>
|
|
<span class="rule-column-selector-target-select" v-else>
|
|
<select2 @input="handleAdd" placeholder="Select a column">
|
|
<option /><!-- empty option selection for placeholder -->
|
|
<option v-for="(col, index) in remainingHeaders" :value="index">{{ col }}</option>
|
|
</select2>
|
|
</span>
|
|
</li>
|
|
</ol>
|
|
</div>
|
|
`,
|
|
data: function() {
|
|
return {
|
|
l: _l
|
|
};
|
|
},
|
|
props: {
|
|
target: {
|
|
required: true
|
|
},
|
|
label: {
|
|
required: false,
|
|
type: String,
|
|
default: _l("From Column")
|
|
},
|
|
help: {
|
|
required: false
|
|
},
|
|
colHeaders: {
|
|
type: Array,
|
|
required: true
|
|
},
|
|
multiple: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
ordered: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
valueAsList: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
},
|
|
orderedEdit: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: false
|
|
}
|
|
},
|
|
computed: {
|
|
remainingHeaders() {
|
|
const colHeaders = this.colHeaders;
|
|
if (!this.multiple) {
|
|
return colHeaders;
|
|
}
|
|
const remaining = {};
|
|
for (let key in colHeaders) {
|
|
if (this.target.indexOf(parseInt(key)) === -1) {
|
|
remaining[key] = colHeaders[key];
|
|
}
|
|
}
|
|
return remaining;
|
|
},
|
|
title() {
|
|
return _l("Select a column");
|
|
}
|
|
},
|
|
methods: {
|
|
handleInput(value) {
|
|
if (this.multiple) {
|
|
// https://stackoverflow.com/questions/262427/why-does-parseint-yield-nan-with-arraymap
|
|
let val = value.map(idx => parseInt(idx));
|
|
this.$emit("update:target", val);
|
|
} else {
|
|
let val = parseInt(value);
|
|
if (this.valueAsList) {
|
|
val = [val];
|
|
}
|
|
this.$emit("update:target", val);
|
|
}
|
|
},
|
|
handleAdd(value) {
|
|
this.target.push(parseInt(value));
|
|
this.$emit("update:orderedEdit", false);
|
|
},
|
|
handleRemove(index) {
|
|
this.target.splice(index, 1);
|
|
},
|
|
moveUp(value) {
|
|
const swapVal = this.target[value - 1];
|
|
Vue.set(this.target, value - 1, this.target[value]);
|
|
Vue.set(this.target, value, swapVal);
|
|
}
|
|
},
|
|
components: {
|
|
Select2
|
|
}
|
|
};
|
|
|
|
const RegularExpressionInput = {
|
|
template: `
|
|
<div>
|
|
<label for="regular_expression" v-b-tooltip.hover :title="title">{{ label }}</label>
|
|
<span v-b-popover.html="popoverContent" :title="popoverTitle" class="fa fa-question"></span>
|
|
<input v-b-tooltip.hover.left :title="title" name="regular_expression" class="rule-regular-expression" type="text" :value="target" @input="$emit('update:target', $event.target.value)" />
|
|
</div>
|
|
`,
|
|
props: {
|
|
target: {
|
|
required: true
|
|
}
|
|
},
|
|
computed: {
|
|
label() {
|
|
return _l("Regular Expression");
|
|
},
|
|
title() {
|
|
return _l("Enter a regular expression.");
|
|
},
|
|
popoverTitle() {
|
|
return _l("Regular Expressions");
|
|
},
|
|
popoverContent() {
|
|
return _l(
|
|
`Regular expressions are patterns used to match character combinations in strings. This input accepts Python-style regular expressions, find more information about these in <a href="https://pythonforbiologists.com/regular-expressions/">this Python for Biologists tutorial</a>.`
|
|
);
|
|
}
|
|
}
|
|
};
|
|
|
|
const RuleDisplay = {
|
|
template: `
|
|
<li class="rule">
|
|
<span class="rule-display">
|
|
<span class="mr-1">{{ title }}</span>
|
|
<span v-b-tooltip.hover :title="editTitle" class="fa fa-edit mr-1" @click="edit"></span>
|
|
<span v-b-tooltip.hover :title="removeTitle" class="fa fa-times map" @click="remove"></span>
|
|
</span>
|
|
<span class="rule-warning" v-if="rule.warn">
|
|
{{ rule.warn }}
|
|
</span>
|
|
<span class="rule-error" v-if="rule.error">
|
|
<span class="alert-message">{{ rule.error }}</span>
|
|
</span>
|
|
</li>
|
|
`,
|
|
props: {
|
|
rule: {
|
|
required: true,
|
|
type: Object
|
|
},
|
|
colHeaders: {
|
|
type: Array,
|
|
required: true
|
|
}
|
|
},
|
|
computed: {
|
|
title() {
|
|
const ruleType = this.rule.type;
|
|
return RULES[ruleType].display(this.rule, this.colHeaders);
|
|
},
|
|
editTitle() {
|
|
return _l("Edit this rule.");
|
|
},
|
|
removeTitle() {
|
|
return _l("Remove this rule.");
|
|
}
|
|
},
|
|
methods: {
|
|
edit() {
|
|
this.$emit("edit");
|
|
},
|
|
remove() {
|
|
this.$emit("remove");
|
|
}
|
|
}
|
|
};
|
|
|
|
const IdentifierDisplay = {
|
|
template: `
|
|
<li class="rule">
|
|
<span v-b-tooltip.hover :title="help">Set {{ columnsLabel }} as {{ typeDisplay }}</span>
|
|
<span v-b-tooltip.hover :title="titleEdit" class="fa fa-edit" @click="edit"></span>
|
|
<span v-b-tooltip.hover :title="titleRemove" class="fa fa-times" @click="remove"></span>
|
|
</li>
|
|
`,
|
|
props: {
|
|
type: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
columns: {
|
|
required: true
|
|
},
|
|
colHeaders: {
|
|
type: Array,
|
|
required: true
|
|
}
|
|
},
|
|
methods: {
|
|
remove() {
|
|
this.$emit("remove");
|
|
},
|
|
edit() {
|
|
this.$emit("edit");
|
|
}
|
|
},
|
|
computed: {
|
|
typeDisplay() {
|
|
return MAPPING_TARGETS[this.type].label;
|
|
},
|
|
help() {
|
|
return MAPPING_TARGETS[this.type].help || "";
|
|
},
|
|
titleEdit() {
|
|
return _l("Edit column definition");
|
|
},
|
|
titleRemove() {
|
|
return _l("Remove this column definition");
|
|
},
|
|
columnsLabel() {
|
|
return RuleDefs.columnDisplay(this.columns, this.colHeaders);
|
|
}
|
|
}
|
|
};
|
|
|
|
const RuleTargetComponent = {
|
|
template: `<a class="rule-link dropdown-item" href="javascript:void(0)" :class="linkClassName" @click="builder.addNewRule(ruleType)">{{title}}</a>`,
|
|
props: {
|
|
ruleType: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
builder: {
|
|
required: true
|
|
}
|
|
},
|
|
computed: {
|
|
linkClassName() {
|
|
return "rule-link-" + this.ruleType.replace(/_/g, "-");
|
|
},
|
|
title() {
|
|
return RULES[this.ruleType].title;
|
|
}
|
|
}
|
|
};
|
|
|
|
const RuleComponent = {
|
|
template: `
|
|
<div v-if="ruleType == displayRuleType" class="rule-editor" :class="typeToClass">
|
|
<slot></slot>
|
|
<div class="buttons rule-edit-buttons d-flex justify-content-end">
|
|
<button type="button" class="btn rule-editor-cancel mr-1" @click="cancel">{{ cancelLabel }}</button>
|
|
<button type="button" class="btn btn-primary rule-editor-ok" @click="okay">{{ applyLabel }}</button>
|
|
</div>
|
|
</div>`,
|
|
data: function() {
|
|
return {
|
|
applyLabel: _l("Apply"),
|
|
cancelLabel: _l("Cancel")
|
|
};
|
|
},
|
|
props: {
|
|
ruleType: {
|
|
type: String,
|
|
required: true
|
|
},
|
|
displayRuleType: {
|
|
required: true
|
|
},
|
|
builder: {
|
|
required: true
|
|
}
|
|
},
|
|
methods: {
|
|
cancel() {
|
|
this.builder.displayRuleType = null;
|
|
},
|
|
okay() {
|
|
this.builder.handleRuleSave(this.ruleType);
|
|
this.cancel();
|
|
}
|
|
},
|
|
computed: {
|
|
typeToClass() {
|
|
return "rule-edit-" + this.ruleType.replace(/_/g, "-");
|
|
}
|
|
}
|
|
};
|
|
|
|
const StateDiv = {
|
|
template: `
|
|
<div class="rule-collection-creator collection-creator flex-row-container">
|
|
<slot></slot>
|
|
</div>`
|
|
};
|
|
|
|
const RuleModalHeader = {
|
|
template: `<div class="header flex-row no-flex"><slot></slot></div>`
|
|
};
|
|
|
|
const RuleModalMiddle = {
|
|
template: `<div class="middle flex-row flex-row-container"><slot></slot></div>`
|
|
};
|
|
|
|
const RuleModalFooter = {
|
|
template: `
|
|
<div class="rule-footer footer flex-row no-flex">
|
|
<slot name="inputs"></slot>
|
|
<div class="actions clear vertically-spaced">
|
|
<div class="main-options float-right">
|
|
<slot></slot>
|
|
</div>
|
|
</div>
|
|
</div>`
|
|
};
|
|
|
|
export default {
|
|
data: function() {
|
|
let orientation = "vertical";
|
|
let mapping, rules;
|
|
if (this.initialRules) {
|
|
mapping = this.initialRules.mapping.slice();
|
|
rules = this.initialRules.rules.slice();
|
|
} else {
|
|
if (this.elementsType == "ftp") {
|
|
mapping = [{ type: "ftp_path", columns: [0] }];
|
|
} else if (this.elementsType == "datasets") {
|
|
mapping = [{ type: "list_identifiers", columns: [1] }];
|
|
} else {
|
|
mapping = [];
|
|
}
|
|
rules = [];
|
|
if (this.elementsType == "collection_contents") {
|
|
if (this.initialElements !== null) {
|
|
const collectionType = this.initialElements.collection_type;
|
|
const collectionTypeRanks = collectionType.split(":");
|
|
for (let index in collectionTypeRanks) {
|
|
rules.push({
|
|
type: "add_column_metadata",
|
|
value: "identifier" + index
|
|
});
|
|
}
|
|
} else {
|
|
orientation = "horizontal";
|
|
// just assume a list is given by default.
|
|
rules.push({
|
|
type: "add_column_metadata",
|
|
value: "identifier0"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
rules: rules,
|
|
colHeadersPerRule: [],
|
|
mapping: mapping,
|
|
state: "build", // 'build', 'error', 'wait',
|
|
ruleView: "normal", // 'normal' or 'source'
|
|
ruleSource: "",
|
|
ruleSourceJson: null,
|
|
ruleSourceError: null,
|
|
errorMessage: "",
|
|
jaggedData: false,
|
|
waitingJobState: "new",
|
|
titleReset: _l("Undo all reordering and discards"),
|
|
titleNumericSort: _l(
|
|
"By default columns will be sorted lexiographically, check this option if the columns are numeric values and should be sorted as numbers"
|
|
),
|
|
titleInvertFilterRegex: _l("Remove rows not matching the specified regular expression at specified column"),
|
|
titleInvertFilterEmpty: _l("Remove rows that have non-empty values at specified column"),
|
|
titleInvertFilterMatches: _l("Remove rows not matching supplied value"),
|
|
titleViewSource: _l(
|
|
"Advanced Option: View and or edit the JSON representation of the rules to apply to this tabular data"
|
|
),
|
|
titleSourceCancel: _l("Stop editing rules and dismiss changes"),
|
|
titleSourceReset: _l("Reset text area to current set of rules"),
|
|
titleSourceApply: _l("Apply changes to rule source and return to rule preview"),
|
|
titleRulesMenu: _l("General rules to apply"),
|
|
titleFilterMenu: _l("Rules that filter rows from the data"),
|
|
titleColumMenu: _l("Rules that generate new columns"),
|
|
titleRemoveMapping: _l("Remove column definition assignment"),
|
|
titleApplyColumnDefinitions: _l("Apply these column definitions and return to rules preview"),
|
|
titleAddColumnDefinition: _l("Assign a new piece of metadata as being derived from a column of the table"),
|
|
titleErrorOkay: _l("Dismiss this error and return to the rule builder to try again with new rules"),
|
|
namePlaceholder: _l("Enter a name for your new collection"),
|
|
activeRuleIndex: null,
|
|
addColumnRegexTarget: 0,
|
|
addColumnBasenameTarget: 0,
|
|
addColumnRegexExpression: "",
|
|
addColumnRegexReplacement: null,
|
|
addColumnRegexGroupCount: null,
|
|
addColumnRegexType: "global",
|
|
addColumnMetadataValue: 0,
|
|
addColumnConcatenateTarget0: 0,
|
|
addColumnConcatenateTarget1: 0,
|
|
addColumnRownumStart: 1,
|
|
addColumnSubstrTarget: 0,
|
|
addColumnSubstrType: "keep_prefix",
|
|
addColumnSubstrLength: 1,
|
|
addColumnValue: "",
|
|
removeColumnTargets: [],
|
|
addFilterRegexTarget: 0,
|
|
addFilterRegexExpression: "",
|
|
addFilterRegexInvert: false,
|
|
addFilterMatchesTarget: 0,
|
|
addFilterMatchesValue: "",
|
|
addFilterMatchesInvert: false,
|
|
addFilterEmptyTarget: 0,
|
|
addFilterEmptyInvert: false,
|
|
addFilterCompareTarget: 0,
|
|
addFilterCompareValue: 0,
|
|
addFilterCompareType: "less_than",
|
|
addFilterCountN: 1,
|
|
addFilterCountInvert: false,
|
|
addFilterCountWhich: "first",
|
|
addSortingTarget: 0,
|
|
addSortingNumeric: false,
|
|
splitColumnsTargets0: [],
|
|
splitColumnsTargets1: [],
|
|
swapColumnsTarget0: 0,
|
|
swapColumnsTarget1: 0,
|
|
collectionName: "",
|
|
displayRuleType: null,
|
|
extensions: [],
|
|
extension: null,
|
|
genomes: [],
|
|
genome: null,
|
|
hideSourceItems: this.defaultHideSourceItems,
|
|
orientation: orientation
|
|
};
|
|
},
|
|
props: {
|
|
initialElements: {
|
|
required: true
|
|
},
|
|
importType: {
|
|
type: String,
|
|
required: false,
|
|
default: "collections"
|
|
},
|
|
elementsType: {
|
|
type: String,
|
|
required: false,
|
|
default: "datasets"
|
|
},
|
|
// required if elementsType is "datasets" - hook into Backbone code for creating
|
|
// collections from HDAs, etc...
|
|
creationFn: {
|
|
required: false,
|
|
type: Function
|
|
},
|
|
// required if elementsType is "collection_contents" - hook into tool form to update
|
|
// rule parameter
|
|
saveRulesFn: {
|
|
required: false,
|
|
type: Function
|
|
},
|
|
initialRules: {
|
|
required: false,
|
|
type: Object
|
|
},
|
|
defaultHideSourceItems: {
|
|
type: Boolean,
|
|
required: false,
|
|
default: true
|
|
},
|
|
// Callbacks sent in by modal code.
|
|
oncancel: {
|
|
required: true,
|
|
type: Function
|
|
},
|
|
oncreate: {
|
|
required: true,
|
|
type: Function
|
|
},
|
|
ftpUploadSite: {
|
|
type: String,
|
|
required: false,
|
|
default: null
|
|
}
|
|
},
|
|
computed: {
|
|
exisistingDatasets() {
|
|
const elementsType = this.elementsType;
|
|
return (
|
|
elementsType === "datasets" ||
|
|
elementsType === "collection_contents" ||
|
|
elementsType === "library_datasets"
|
|
);
|
|
},
|
|
showFileTypeSelector() {
|
|
return !this.exisistingDatasets && !this.mappingAsDict.file_type;
|
|
},
|
|
showGenomeSelector() {
|
|
return !this.exisistingDatasets && !this.mappingAsDict.dbkey;
|
|
},
|
|
showCollectionNameInput() {
|
|
return (
|
|
this.importType == "collections" &&
|
|
this.elementsType != "collection_contents" &&
|
|
!this.mappingAsDict.collection_name
|
|
);
|
|
},
|
|
titleFinish() {
|
|
if (this.elementsType == "datasets" || this.elementsType == "library_datasets") {
|
|
return _l("Create new collection from specified rules and datasets");
|
|
} else if (this.elementsType == "collection_contents") {
|
|
return _l("Save rules and return to tool form");
|
|
} else {
|
|
return _l("Upload collection using specified rules");
|
|
}
|
|
},
|
|
titleCancel() {
|
|
if (this.importType == "datasets") {
|
|
return _l("Close this modal and do not upload any datasets");
|
|
} else {
|
|
return _l("Close this modal and do not create any collections");
|
|
}
|
|
},
|
|
finishButtonTitle() {
|
|
if (this.elementsType == "datasets" || this.elementsType == "library_datasets") {
|
|
return _l("Create");
|
|
} else if (this.elementsType == "collection_contents") {
|
|
return _l("Save");
|
|
} else {
|
|
return _l("Upload");
|
|
}
|
|
},
|
|
hasActiveMappingEdit() {
|
|
const has = _.any(_.values(this.mapping), mapping => mapping.editing);
|
|
return has;
|
|
},
|
|
activeRule() {
|
|
return this.activeRuleIndex !== null && this.rules[this.activeRuleIndex];
|
|
},
|
|
activeRuleColHeaders() {
|
|
const rulesHeaders = this.activeRuleIndex !== null && this.colHeadersPerRule[this.activeRuleIndex];
|
|
return rulesHeaders || this.colHeaders;
|
|
},
|
|
horizontal() {
|
|
return this.orientation == "horizontal";
|
|
},
|
|
vertical() {
|
|
return this.orientation == "vertical";
|
|
},
|
|
mappedTargets() {
|
|
const targets = [];
|
|
for (let mapping of this.mapping) {
|
|
targets.push(mapping.type);
|
|
}
|
|
return targets;
|
|
},
|
|
unmappedTargets() {
|
|
const targets = [];
|
|
const mappedTargets = this.mappedTargets;
|
|
for (let target in MAPPING_TARGETS) {
|
|
const targetModes = MAPPING_TARGETS[target].modes;
|
|
|
|
if (targetModes && targetModes.indexOf(this.elementsType) < 0) {
|
|
continue;
|
|
}
|
|
|
|
const targetDefinition = MAPPING_TARGETS[target];
|
|
const targetImportType = targetDefinition.importType;
|
|
if (targetImportType && this.importType != targetImportType) {
|
|
continue;
|
|
}
|
|
if (!this.ftpUploadSite && targetDefinition.requiresFtp) {
|
|
continue;
|
|
}
|
|
if (mappedTargets.indexOf(target) < 0) {
|
|
targets.push(target);
|
|
}
|
|
}
|
|
return targets;
|
|
},
|
|
hotData() {
|
|
let data, sources, columns;
|
|
if (this.elementsType == "datasets") {
|
|
data = this.initialElements.map(el => [el["hid"], el["name"]]);
|
|
sources = this.initialElements.slice();
|
|
columns = ["new", "new"];
|
|
} else if (this.elementsType == "library_datasets") {
|
|
data = this.initialElements.map(el => [el["name"]]);
|
|
sources = this.initialElements.slice();
|
|
columns = ["new"];
|
|
} else if (this.elementsType == "collection_contents") {
|
|
const collection = this.initialElements;
|
|
if (collection) {
|
|
const obj = this.populateElementsFromCollectionDescription(
|
|
collection.elements,
|
|
collection.collection_type
|
|
);
|
|
data = obj.data;
|
|
sources = obj.sources;
|
|
columns = [];
|
|
} else {
|
|
data = [];
|
|
sources = [];
|
|
columns = [];
|
|
}
|
|
} else {
|
|
data = this.initialElements.slice();
|
|
sources = data.map(el => null);
|
|
columns = [];
|
|
if (this.initialElements) {
|
|
for (var columnIndex in this.initialElements[0]) {
|
|
columns.push("new");
|
|
}
|
|
}
|
|
}
|
|
|
|
this.colHeadersPerRule = [];
|
|
return RuleDefs.applyRules(data, sources, columns, this.rules, this.colHeadersPerRule);
|
|
},
|
|
colHeaders() {
|
|
const data = this.hotData["data"];
|
|
const columns = this.hotData["columns"];
|
|
return RuleDefs.colHeadersFor(data, columns);
|
|
},
|
|
colHeadersDisplay() {
|
|
const formattedHeaders = [];
|
|
for (let colIndex in this.colHeaders) {
|
|
const colHeader = this.colHeaders[colIndex];
|
|
formattedHeaders[colIndex] = `<b>${_.escape(colHeader)}</b>`;
|
|
const mappingDisplay = [];
|
|
for (let mapping of this.mapping) {
|
|
if (mapping.columns.indexOf(parseInt(colIndex)) !== -1) {
|
|
const mappingDef = MAPPING_TARGETS[mapping.type];
|
|
mappingDisplay.push(`<i>${_.escape(mappingDef.columnHeader || mappingDef.label)}</i>`);
|
|
}
|
|
}
|
|
if (mappingDisplay.length == 1) {
|
|
formattedHeaders[colIndex] += ` (${mappingDisplay[0]})`;
|
|
} else if (mappingDisplay.length > 1) {
|
|
formattedHeaders[colIndex] += ` (${[
|
|
mappingDisplay.slice(0, -1).join(", "),
|
|
mappingDisplay.slice(-1)[0]
|
|
].join(" & ")})`;
|
|
}
|
|
}
|
|
return formattedHeaders;
|
|
},
|
|
mappingAsDict() {
|
|
const asDict = {};
|
|
for (let mapping of this.mapping) {
|
|
asDict[mapping.type] = mapping;
|
|
}
|
|
return asDict;
|
|
},
|
|
metadataOptions() {
|
|
const metadataOptions = {};
|
|
if (this.elementsType == "collection_contents") {
|
|
let collectionType;
|
|
if (this.initialElements) {
|
|
collectionType = this.initialElements.collection_type;
|
|
} else {
|
|
// give a bunch of different options if not constrained with given input
|
|
collectionType = "list:list:list:paired";
|
|
}
|
|
const collectionTypeRanks = collectionType.split(":");
|
|
for (let index in collectionTypeRanks) {
|
|
const collectionTypeRank = collectionTypeRanks[index];
|
|
if (collectionTypeRank == "list") {
|
|
// TODO: drop the numeral at the end if only flat list
|
|
metadataOptions["identifier" + index] = _l("List Identifier ") + (parseInt(index) + 1);
|
|
} else {
|
|
metadataOptions["identifier" + index] = _l("Paired Identifier");
|
|
}
|
|
}
|
|
}
|
|
return metadataOptions;
|
|
},
|
|
collectionType() {
|
|
let identifierColumns = [];
|
|
if (this.mappingAsDict.list_identifiers) {
|
|
identifierColumns = this.mappingAsDict.list_identifiers.columns;
|
|
}
|
|
let collectionType = identifierColumns.map(col => "list").join(":");
|
|
if (this.mappingAsDict.paired_identifier) {
|
|
if (collectionType) {
|
|
collectionType += ":paired";
|
|
} else {
|
|
collectionType = "paired";
|
|
}
|
|
}
|
|
return collectionType;
|
|
},
|
|
validInput() {
|
|
const identifierColumns = this.identifierColumns();
|
|
const mappingAsDict = this.mappingAsDict;
|
|
const buildingCollection = identifierColumns.length > 0 && this.elementsType != "collection_contents";
|
|
|
|
let valid = true;
|
|
if (buildingCollection && !mappingAsDict.collection_name) {
|
|
valid = this.collectionName.length > 0;
|
|
}
|
|
|
|
if (mappingAsDict.ftp_path && mappingAsDict.url) {
|
|
// Can only specify one of these.
|
|
valid = false;
|
|
}
|
|
|
|
for (var rule of this.rules) {
|
|
if (rule.error) {
|
|
valid = false;
|
|
}
|
|
}
|
|
|
|
// raw tabular variant can build stand-alone datasets without identifier
|
|
// columns for the collection builder for existing datasets cannot do this.
|
|
const fromDatasets = this.elementsType == "datasets" || this.elementsType == "library_datasets";
|
|
if (fromDatasets && identifierColumns.length == 0) {
|
|
valid = false;
|
|
}
|
|
return valid;
|
|
}
|
|
},
|
|
methods: {
|
|
l(str) {
|
|
// _l conflicts private methods of Vue internals, expose as l instead
|
|
return _l(str);
|
|
},
|
|
cancel() {
|
|
this.oncancel();
|
|
},
|
|
mappingTargets() {
|
|
return MAPPING_TARGETS;
|
|
},
|
|
resetRules() {
|
|
this.rules.splice(0, this.rules.length);
|
|
this.mapping.splice(0, this.mapping.length);
|
|
},
|
|
resetRulesAndState() {
|
|
this.resetRules();
|
|
this.state = "build";
|
|
this.errorMessage = "";
|
|
this.collectionName = "";
|
|
},
|
|
addNewRule(ruleType) {
|
|
RULES[ruleType].init(this);
|
|
this.displayRuleType = ruleType;
|
|
this.activeRuleIndex = null;
|
|
},
|
|
handleRuleSave(ruleType) {
|
|
const rule = this.activeRule;
|
|
if (rule) {
|
|
RULES[ruleType].save(this, rule);
|
|
} else {
|
|
const rule = { type: ruleType };
|
|
RULES[ruleType].save(this, rule);
|
|
this.rules.push(rule);
|
|
}
|
|
},
|
|
viewSource() {
|
|
this.resetSource();
|
|
this.ruleView = "source";
|
|
},
|
|
resetSource() {
|
|
const replacer = function(key, value) {
|
|
if (key == "error" || key == "warn") {
|
|
return undefined;
|
|
}
|
|
return value;
|
|
};
|
|
const asJson = {
|
|
rules: this.rules,
|
|
mapping: this.mapping
|
|
};
|
|
if (!this.exisistingDatasets) {
|
|
if (this.extension !== UploadUtils.DEFAULT_EXTENSION) {
|
|
asJson.extension = this.extension;
|
|
}
|
|
if (this.genome !== UploadUtils.DEFAULT_GENOME) {
|
|
asJson.genome = this.genome;
|
|
}
|
|
}
|
|
this.ruleSourceJson = asJson;
|
|
this.ruleSource = JSON.stringify(asJson, replacer, " ");
|
|
this.ruleSourceError = null;
|
|
},
|
|
attemptRulePreview() {
|
|
// Leave source mode if rules are valid and view.
|
|
this.ruleSourceError = null;
|
|
let asJson;
|
|
try {
|
|
asJson = JSON.parse(this.ruleSource);
|
|
} catch (error) {
|
|
this.ruleSourceError = "Problem parsing your rules.";
|
|
return;
|
|
}
|
|
this.updateFromSource(asJson);
|
|
this.ruleView = "normal";
|
|
},
|
|
cancelSourceEdit() {
|
|
this.ruleSourceError = null;
|
|
this.ruleView = "normal";
|
|
},
|
|
updateFromSource(asJson) {
|
|
this.resetRules();
|
|
if (asJson.extension) {
|
|
this.extension = asJson.extension;
|
|
}
|
|
if (asJson.genome) {
|
|
this.genome = asJson.genome;
|
|
}
|
|
if (asJson.rules) {
|
|
this.rules = asJson.rules;
|
|
}
|
|
if (asJson.mapping) {
|
|
this.mapping = asJson.mapping;
|
|
}
|
|
},
|
|
addIdentifier(identifier) {
|
|
const multiple = this.mappingTargets()[identifier].multiple;
|
|
// If multiple selection, pop open a new column selector in edit mode.
|
|
const initialColumns = multiple ? [] : [0];
|
|
this.mapping.push({ type: identifier, columns: initialColumns, editing: multiple });
|
|
},
|
|
editRule(rule, index) {
|
|
const ruleType = rule.type;
|
|
this.activeRuleIndex = index;
|
|
RULES[ruleType].init(this, rule);
|
|
this.displayRuleType = ruleType;
|
|
},
|
|
removeRule(index) {
|
|
this.rules.splice(index, 1);
|
|
},
|
|
removeMapping(index) {
|
|
this.mapping.splice(index, 1);
|
|
},
|
|
refreshAndWait(response) {
|
|
if (Galaxy && Galaxy.currHistoryPanel) {
|
|
Galaxy.currHistoryPanel.refreshContents();
|
|
}
|
|
this.waitOnJob(response);
|
|
},
|
|
waitOnJob(response) {
|
|
const jobId = response.data.jobs[0].id;
|
|
const handleJobShow = jobResponse => {
|
|
const state = jobResponse.data.state;
|
|
this.waitingJobState = state;
|
|
if (JobStatesModel.NON_TERMINAL_STATES.indexOf(state) !== -1) {
|
|
setTimeout(doJobCheck, 1000);
|
|
} else if (JobStatesModel.ERROR_STATES.indexOf(state) !== -1) {
|
|
this.state = "error";
|
|
this.errorMessage =
|
|
"Unknown error encountered while running your upload job, this could be a server issue or a problem with the upload definition.";
|
|
} else {
|
|
const history =
|
|
parent.Galaxy && parent.Galaxy.currHistoryPanel && parent.Galaxy.currHistoryPanel.model;
|
|
history.refresh();
|
|
this.oncreate();
|
|
}
|
|
};
|
|
const doJobCheck = () => {
|
|
axios
|
|
.get(`${Galaxy.root}api/jobs/${jobId}`)
|
|
.then(handleJobShow)
|
|
.catch(this.renderFetchError);
|
|
};
|
|
setTimeout(doJobCheck, 1000);
|
|
},
|
|
renderFetchError(error) {
|
|
this.state = "error";
|
|
if (error.response) {
|
|
console.log(error.response);
|
|
this.errorMessage = error.response.data.err_msg;
|
|
} else {
|
|
console.log(error);
|
|
this.errorMessage = "Unknown error encountered: " + error;
|
|
}
|
|
},
|
|
swapOrientation() {
|
|
this.orientation = this.orientation == "horizontal" ? "vertical" : "horizontal";
|
|
const hotTable = this.$refs.hotTable.table;
|
|
if (this.orientation == "horizontal") {
|
|
this.$nextTick(function() {
|
|
const fullWidth = $(".rule-builder-body").width();
|
|
hotTable.updateSettings({
|
|
width: fullWidth
|
|
});
|
|
});
|
|
} else {
|
|
this.$nextTick(function() {
|
|
const fullWidth = $(".rule-builder-body").width();
|
|
hotTable.updateSettings({
|
|
width: fullWidth - 270
|
|
});
|
|
});
|
|
}
|
|
},
|
|
createCollection() {
|
|
this.state = "wait";
|
|
const name = this.collectionName;
|
|
const collectionType = this.collectionType;
|
|
if (this.elementsType == "datasets" || this.elementsType == "library_datasets") {
|
|
const elements = this.creationElementsFromDatasets();
|
|
if (this.state !== "error") {
|
|
new AjaxQueue.AjaxQueue(
|
|
_.map(elements, (elements, name) => {
|
|
return () => {
|
|
const response = this.creationFn(elements, collectionType, name, this.hideSourceItems);
|
|
return response;
|
|
};
|
|
})
|
|
)
|
|
.done(this.oncreate)
|
|
.fail(this.renderFetchError);
|
|
}
|
|
} else if (this.elementsType == "collection_contents") {
|
|
this.resetSource();
|
|
if (this.state !== "error") {
|
|
this.saveRulesFn(this.ruleSourceJson);
|
|
this.oncreate();
|
|
}
|
|
} else {
|
|
const historyId = Galaxy.currHistoryPanel.model.id;
|
|
let elements, targets;
|
|
if (collectionType) {
|
|
targets = [];
|
|
const elementsByCollectionName = this.creationElementsForFetch();
|
|
for (let collectionName in elementsByCollectionName) {
|
|
const target = {
|
|
destination: { type: "hdca" },
|
|
elements: elementsByCollectionName[collectionName],
|
|
collection_type: collectionType,
|
|
name: collectionName
|
|
};
|
|
targets.push(target);
|
|
}
|
|
} else {
|
|
elements = this.creationDatasetsForFetch();
|
|
targets = [
|
|
{
|
|
destination: { type: "hdas" },
|
|
elements: elements,
|
|
name: name
|
|
}
|
|
];
|
|
}
|
|
|
|
if (this.state !== "error") {
|
|
axios
|
|
.post(`${Galaxy.root}api/tools/fetch`, {
|
|
history_id: historyId,
|
|
targets: targets
|
|
})
|
|
.then(this.refreshAndWait)
|
|
.catch(this.renderFetchError);
|
|
}
|
|
}
|
|
},
|
|
identifierColumns() {
|
|
const mappingAsDict = this.mappingAsDict;
|
|
let identifierColumns = [];
|
|
if (mappingAsDict.list_identifiers) {
|
|
identifierColumns = mappingAsDict.list_identifiers.columns.slice();
|
|
}
|
|
if (this.mappingAsDict.paired_identifier) {
|
|
identifierColumns.push(this.mappingAsDict.paired_identifier.columns[0]);
|
|
}
|
|
return identifierColumns;
|
|
},
|
|
buildRequestElements(createDatasetDescription, createSubcollectionDescription, subElementProp) {
|
|
const data = this.hotData["data"];
|
|
const identifierColumns = this.identifierColumns();
|
|
if (identifierColumns.length < 1) {
|
|
console.log("Error but this shouldn't have happened, create button should have been disabled.");
|
|
return;
|
|
}
|
|
|
|
const numIdentifierColumns = identifierColumns.length;
|
|
const collectionType = this.collectionType;
|
|
const elementsByName = {};
|
|
|
|
let dataByCollection = {};
|
|
const collectionNameMap = this.mappingAsDict.collection_name;
|
|
if (collectionNameMap) {
|
|
const collectionNameTarget = collectionNameMap.columns[0];
|
|
for (let dataIndex in data) {
|
|
const row = data[dataIndex];
|
|
const name = row[collectionNameTarget];
|
|
if (!dataByCollection[name]) {
|
|
dataByCollection[name] = {};
|
|
}
|
|
dataByCollection[name][dataIndex] = row;
|
|
}
|
|
} else {
|
|
// use global collection name from the form.
|
|
dataByCollection[this.collectionName] = data;
|
|
}
|
|
|
|
for (let collectionName in dataByCollection) {
|
|
const elements = [];
|
|
const identifiers = [];
|
|
|
|
for (let dataIndex in dataByCollection[collectionName]) {
|
|
const rowData = data[dataIndex];
|
|
|
|
// For each row, find place in depth for this element.
|
|
let collectionTypeAtDepth = collectionType;
|
|
let elementsAtDepth = elements;
|
|
let identifiersAtDepth = identifiers;
|
|
|
|
for (
|
|
let identifierColumnIndex = 0;
|
|
identifierColumnIndex < numIdentifierColumns;
|
|
identifierColumnIndex++
|
|
) {
|
|
// typeof indicates identifier is a string, but the raw string value coming from this data
|
|
// structure sometimes does not work as expected with indexOf below, I don't understand why
|
|
// but as a result this cast here seems needed.
|
|
let identifier = String(rowData[identifierColumns[identifierColumnIndex]]);
|
|
if (identifierColumnIndex + 1 == numIdentifierColumns) {
|
|
// At correct final position in nested structure for this dataset.
|
|
if (collectionTypeAtDepth === "paired") {
|
|
if (["f", "1", "r1", "forward"].indexOf(identifier.toLowerCase()) > -1) {
|
|
identifier = "forward";
|
|
} else if (["r", "2", "r2", "reverse"].indexOf(identifier.toLowerCase()) > -1) {
|
|
identifier = "reverse";
|
|
} else {
|
|
this.state = "error";
|
|
this.errorMessage =
|
|
"Unknown indicator of paired status encountered - only values of F, R, 1, 2, R1, R2, forward, or reverse are allowed.";
|
|
return;
|
|
}
|
|
}
|
|
const element = createDatasetDescription(dataIndex, identifier);
|
|
elementsAtDepth.push(element);
|
|
if (identifiersAtDepth.indexOf(identifier) >= 0) {
|
|
this.state = "error";
|
|
this.errorMessage =
|
|
"Duplicate identifiers detected, collection identifiers must be unique.";
|
|
return;
|
|
}
|
|
identifiersAtDepth.push(identifier);
|
|
} else {
|
|
// Create nesting for this element.
|
|
collectionTypeAtDepth = collectionTypeAtDepth
|
|
.split(":")
|
|
.slice(1)
|
|
.join(":");
|
|
let found = false;
|
|
for (let element of elementsAtDepth) {
|
|
if (element["name"] == identifier) {
|
|
elementsAtDepth = element[subElementProp];
|
|
identifiersAtDepth = identifiersAtDepth[identifier];
|
|
found = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!found) {
|
|
const subcollection = createSubcollectionDescription(identifier);
|
|
elementsAtDepth.push(subcollection);
|
|
identifiersAtDepth[identifier] = [];
|
|
const childCollectionElements = [];
|
|
subcollection[subElementProp] = childCollectionElements;
|
|
subcollection.collection_type = collectionTypeAtDepth;
|
|
elementsAtDepth = childCollectionElements;
|
|
identifiersAtDepth = identifiersAtDepth[identifier];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
elementsByName[collectionName] = elements;
|
|
}
|
|
|
|
return elementsByName;
|
|
},
|
|
creationElementsFromDatasets() {
|
|
const sources = this.hotData["sources"];
|
|
const data = this.hotData["data"];
|
|
|
|
const elementsByCollectionName = this.buildRequestElements(
|
|
(dataIndex, identifier) => {
|
|
const source = sources[dataIndex];
|
|
const src = this.elementsType == "datasets" ? "hda" : "ldda";
|
|
return { id: source["id"], name: identifier, src: src };
|
|
},
|
|
identifier => {
|
|
return { name: identifier, src: "new_collection" };
|
|
},
|
|
"element_identifiers"
|
|
);
|
|
return elementsByCollectionName;
|
|
},
|
|
creationElementsForFetch() {
|
|
// fetch elements for HDCA
|
|
const data = this.hotData["data"];
|
|
const mappingAsDict = this.mappingAsDict;
|
|
|
|
const elementsByCollectionName = this.buildRequestElements(
|
|
(dataIndex, identifier) => {
|
|
const res = this._datasetFor(dataIndex, data, mappingAsDict);
|
|
res["name"] = identifier;
|
|
return res;
|
|
},
|
|
identifier => {
|
|
return { name: identifier };
|
|
},
|
|
"elements"
|
|
);
|
|
|
|
return elementsByCollectionName;
|
|
},
|
|
creationDatasetsForFetch() {
|
|
// fetch elements for HDAs if not collection information specified.
|
|
const data = this.hotData["data"];
|
|
const mappingAsDict = this.mappingAsDict;
|
|
|
|
const datasets = [];
|
|
|
|
for (let dataIndex in data) {
|
|
const rowData = data[dataIndex];
|
|
const res = this._datasetFor(dataIndex, data, mappingAsDict);
|
|
datasets.push(res);
|
|
}
|
|
|
|
return datasets;
|
|
},
|
|
populateElementsFromCollectionDescription(elements, collectionType, parentIdentifiers_) {
|
|
const parentIdentifiers = parentIdentifiers_ ? parentIdentifiers_ : [];
|
|
let data = [];
|
|
let sources = [];
|
|
for (let element of elements) {
|
|
const elementObject = element.object;
|
|
const identifiers = parentIdentifiers.concat([element.element_identifier]);
|
|
const collectionTypeLevelSepIndex = collectionType.indexOf(":");
|
|
if (collectionTypeLevelSepIndex === -1) {
|
|
// Flat collection at this depth.
|
|
// sources are the elements
|
|
// TOOD: right thing is probably this: data.push([]);
|
|
data.push([]);
|
|
sources.push({ identifiers: identifiers, dataset: elementObject });
|
|
} else {
|
|
const restCollectionType = collectionType.slice(collectionTypeLevelSepIndex + 1);
|
|
let elementObj = this.populateElementsFromCollectionDescription(
|
|
elementObject.elements,
|
|
restCollectionType,
|
|
identifiers
|
|
);
|
|
const elementData = elementObj.data;
|
|
const elementSources = elementObj.sources;
|
|
data = data.concat(elementData);
|
|
sources = sources.concat(elementSources);
|
|
}
|
|
}
|
|
return { data, sources };
|
|
},
|
|
highlightColumn(n) {
|
|
const headerSelection = $(`.htCore > thead > tr > th:nth-child(${n + 1})`);
|
|
headerSelection.addClass("ht__highlight");
|
|
const bodySelection = $(`.htCore > tbody > tr > td:nth-child(${n + 1})`);
|
|
bodySelection.addClass("rule-highlight");
|
|
},
|
|
unhighlightColumn(n) {
|
|
const headerSelection = $(`.htCore > thead > tr > th:nth-child(${n + 1})`);
|
|
headerSelection.removeClass("ht__highlight");
|
|
const bodySelection = $(`.htCore > tbody > tr > td:nth-child(${n + 1})`);
|
|
bodySelection.removeClass("rule-highlight");
|
|
},
|
|
_datasetFor(dataIndex, data, mappingAsDict) {
|
|
const res = {};
|
|
if (mappingAsDict.url) {
|
|
const urlColumn = mappingAsDict.url.columns[0];
|
|
let url = data[dataIndex][urlColumn];
|
|
if (url.indexOf("://") == -1) {
|
|
url = "http://" + url;
|
|
}
|
|
res["url"] = url;
|
|
res["src"] = "url";
|
|
} else {
|
|
const ftpPathColumn = mappingAsDict.ftp_path.columns[0];
|
|
const ftpPath = data[dataIndex][ftpPathColumn];
|
|
res["ftp_path"] = ftpPath;
|
|
res["src"] = "ftp_import";
|
|
}
|
|
if (mappingAsDict.dbkey) {
|
|
const dbkeyColumn = mappingAsDict.dbkey.columns[0];
|
|
const dbkey = data[dataIndex][dbkeyColumn];
|
|
res["dbkey"] = dbkey;
|
|
} else if (this.genome) {
|
|
res["dbkey"] = this.genome;
|
|
}
|
|
if (mappingAsDict.file_type) {
|
|
const fileTypeColumn = mappingAsDict.file_type.columns[0];
|
|
const fileType = data[dataIndex][fileTypeColumn];
|
|
res["ext"] = file_type;
|
|
} else if (this.extension) {
|
|
res["ext"] = this.extension;
|
|
}
|
|
if (mappingAsDict.name) {
|
|
const nameColumn = mappingAsDict.name.columns[0];
|
|
const name = data[dataIndex][nameColumn];
|
|
res["name"] = name;
|
|
}
|
|
if (mappingAsDict.info) {
|
|
const infoColumn = mappingAsDict.info.columns[0];
|
|
const info = data[dataIndex][infoColumn];
|
|
res["info"] = info;
|
|
}
|
|
return res;
|
|
}
|
|
},
|
|
created() {
|
|
if (this.elementsType !== "collection_contents") {
|
|
let columnCount = null;
|
|
if (this.elementsType == "datasets") {
|
|
for (let element of this.initialElements) {
|
|
if (element.history_content_type == "dataset_collection") {
|
|
this.errorMessage =
|
|
"This component can only be used with datasets, you have specified one or more collections.";
|
|
this.state = "error";
|
|
}
|
|
}
|
|
} else {
|
|
for (let row of this.initialElements) {
|
|
if (columnCount == null) {
|
|
columnCount = row.length;
|
|
} else {
|
|
if (columnCount != row.length) {
|
|
this.jaggedData = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
UploadUtils.getUploadDatatypes(
|
|
extensions => {
|
|
this.extensions = extensions;
|
|
this.extension = UploadUtils.DEFAULT_EXTENSION;
|
|
},
|
|
false,
|
|
UploadUtils.AUTO_EXTENSION
|
|
);
|
|
UploadUtils.getUploadGenomes(genomes => {
|
|
this.genomes = genomes;
|
|
this.genome = UploadUtils.DEFAULT_GENOME;
|
|
}, UploadUtils.DEFAULT_GENOME);
|
|
}
|
|
},
|
|
watch: {
|
|
addColumnRegexType: function(val) {
|
|
if (val == "groups") {
|
|
this.addColumnRegexGroupCount = 1;
|
|
}
|
|
if (val == "replacement") {
|
|
this.addColumnRegexReplacement = "\0";
|
|
}
|
|
}
|
|
},
|
|
components: {
|
|
HotTable,
|
|
RuleComponent,
|
|
RuleTargetComponent,
|
|
RuleDisplay,
|
|
IdentifierDisplay,
|
|
ColumnSelector,
|
|
RegularExpressionInput,
|
|
StateDiv,
|
|
RuleModalHeader,
|
|
RuleModalMiddle,
|
|
RuleModalFooter,
|
|
Select2
|
|
}
|
|
};
|
|
</script>
|
|
|
|
<style src="../../../node_modules/handsontable/dist/handsontable.full.css">
|
|
</style>
|
|
<style>
|
|
.table-column {
|
|
width: 100%;
|
|
/* overflow: scroll; */
|
|
}
|
|
.select2-container {
|
|
min-width: 60px;
|
|
}
|
|
.vertical #hot-table {
|
|
width: 100%;
|
|
overflow: scroll;
|
|
height: 400px;
|
|
}
|
|
.horizontal #hot-table {
|
|
width: 100%;
|
|
overflow: scroll;
|
|
height: 250px;
|
|
}
|
|
.rule-builder-body {
|
|
height: 400px;
|
|
}
|
|
.rule-column.vertical {
|
|
height: 400px;
|
|
}
|
|
.rule-column.horizontal {
|
|
height: 150px;
|
|
}
|
|
.rules-container-full {
|
|
width: 100%;
|
|
height: 400px;
|
|
}
|
|
.rules-container {
|
|
border: 1px dashed #ccc;
|
|
padding: 5px;
|
|
}
|
|
.rules-container-vertical {
|
|
width: 280px;
|
|
height: 400px;
|
|
}
|
|
.rules-container-horizontal {
|
|
width: 100%;
|
|
height: 150px;
|
|
}
|
|
.rules-container .title {
|
|
font-weight: bold;
|
|
}
|
|
.rule-summary {
|
|
height: 100%;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
.rule-edit-buttons {
|
|
margin: 5px;
|
|
}
|
|
.rules {
|
|
flex-grow: 1;
|
|
overflow-y: scroll;
|
|
}
|
|
.rule-source {
|
|
height: 400px;
|
|
}
|
|
.rules li {
|
|
list-style-type: circle;
|
|
list-style-position: inside;
|
|
padding: 5px;
|
|
padding-top: 0px;
|
|
padding-bottom: 0px;
|
|
}
|
|
.rule-column-selector li {
|
|
list-style-type: circle;
|
|
list-style-position: inside;
|
|
padding: 5px;
|
|
padding-top: 0px;
|
|
padding-bottom: 0px;
|
|
}
|
|
.rules .rule-error {
|
|
display: block;
|
|
margin-left: 10px;
|
|
font-style: italic;
|
|
color: red;
|
|
}
|
|
.rule-warning {
|
|
display: block;
|
|
margin-left: 10px;
|
|
font-style: italic;
|
|
color: #e28809;
|
|
}
|
|
.rule-summary .title {
|
|
font-size: 1.1em;
|
|
}
|
|
.rule-highlight {
|
|
font-style: italic;
|
|
font-weight: bold;
|
|
}
|
|
.rules-buttons {
|
|
}
|
|
.rule-footer-inputs label {
|
|
padding-left: 20px;
|
|
align-self: baseline;
|
|
}
|
|
.rule-footer-inputs .select2-container {
|
|
align-self: baseline;
|
|
}
|
|
.rule-footer-inputs {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
flex-wrap: wrap;
|
|
align-items: baseline;
|
|
}
|
|
.rule-footer-inputs input {
|
|
align-self: baseline;
|
|
}
|
|
.extension-select {
|
|
flex: 1;
|
|
max-width: 120px;
|
|
min-width: 60px;
|
|
}
|
|
.genome-select {
|
|
flex: 1;
|
|
max-width: 300px;
|
|
min-width: 120px;
|
|
}
|
|
.collection-name {
|
|
flex: 1;
|
|
min-width: 120px;
|
|
max-width: 500px;
|
|
}
|
|
.rule-footer-genome-group {
|
|
flex: 2;
|
|
display: flex;
|
|
}
|
|
.rule-footer-extension-group {
|
|
flex: 1;
|
|
display: flex;
|
|
}
|
|
.rule-footer-name-group {
|
|
flex: 3;
|
|
display: flex;
|
|
flex-direction: row-reverse;
|
|
}
|
|
.fa-edit,
|
|
.fa-times,
|
|
.fa-wrench {
|
|
cursor: pointer;
|
|
}
|
|
</style>
|