mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
beta history, editing ui fixes
This commit is contained in:
+2
-1
@@ -29,7 +29,7 @@
|
||||
"axios": "^0.21.1",
|
||||
"backbone": "1.4.0",
|
||||
"bootstrap": "4.5.0",
|
||||
"bootstrap-vue": "^2.15.0",
|
||||
"bootstrap-vue": "^2.21.2",
|
||||
"citation-js": "^0.5.0-alpha.5",
|
||||
"d3": "3",
|
||||
"date-fns": "^2.21.1",
|
||||
@@ -63,6 +63,7 @@
|
||||
"pouchdb-erase": "^1.0.2",
|
||||
"pouchdb-find": "^7.2.2",
|
||||
"pouchdb-upsert": "^2.2.0",
|
||||
"pretty-bytes": "^5.6.0",
|
||||
"proper-skip-list": "^4.0.2",
|
||||
"pyre-to-regexp": "^0.0.5",
|
||||
"regression": "^2.0.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<b-input-group v-if="params">
|
||||
<DebouncedInput v-model.trim="filterText" v-slot="{ value, input }">
|
||||
<b-form-input size="sm" :value="value" @input="input" :placeholder="'Search Filter' | localize" />
|
||||
<b-form-input size="sm" :value="value" @input="input" :placeholder="'Content Filter' | localize" />
|
||||
</DebouncedInput>
|
||||
|
||||
<b-input-group-append>
|
||||
|
||||
@@ -8,7 +8,7 @@ export default {
|
||||
const { history_content_type } = this.item;
|
||||
switch (history_content_type) {
|
||||
case "dataset":
|
||||
return "Dataset";
|
||||
return "Subdataset";
|
||||
case "dataset_collection":
|
||||
return "Subcollection";
|
||||
default:
|
||||
|
||||
@@ -11,18 +11,25 @@
|
||||
import Placeholder from "./Placeholder";
|
||||
import Dataset from "./Dataset";
|
||||
import DatasetCollection from "./DatasetCollection";
|
||||
// import SubDataset from "./SubDataset";
|
||||
import Subdataset from "./Subdataset";
|
||||
import Subcollection from "./Subcollection";
|
||||
import "./styles.scss";
|
||||
|
||||
export default {
|
||||
template: `
|
||||
<component :is="contentItemComponent"
|
||||
class="content-item p-1"
|
||||
:data-ci-type="contentItemComponent"
|
||||
class="content-item"
|
||||
:class="{ loading }"
|
||||
:tabindex="index"
|
||||
:writable="writable"
|
||||
v-on="$listeners"
|
||||
v-bind="bindProps"
|
||||
@mouseover.native.self.stop="setFocus(index)"
|
||||
v-bind="$attrs"
|
||||
:item="item"
|
||||
:index="index"
|
||||
:row-key="rowKey"
|
||||
:writable="writable"
|
||||
@mouseover.native.stop="setFocus(index)"
|
||||
@keydown.native.arrow-up.self.stop="setFocus(index - 1)"
|
||||
@keydown.native.arrow-down.self.stop="setFocus(index + 1)"
|
||||
/>
|
||||
@@ -32,13 +39,15 @@ export default {
|
||||
Placeholder,
|
||||
Dataset,
|
||||
DatasetCollection,
|
||||
// SubDataset,
|
||||
Subdataset,
|
||||
Subcollection,
|
||||
},
|
||||
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
index: { type: Number, required: true },
|
||||
index: { type: Number, required: false, default: null },
|
||||
rowKey: { type: [Number, String], required: false, default: "" },
|
||||
writable: { type: Boolean, required: false, default: true },
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
@@ -50,8 +59,8 @@ export default {
|
||||
if (this.suppressFocus) {
|
||||
return;
|
||||
}
|
||||
const ul = this.$el.closest(".scroller");
|
||||
const el = ul.querySelector(`[tabindex="${index}"]`);
|
||||
const scrollContainer = this.$el.closest(".scroller");
|
||||
const el = scrollContainer.querySelector(`[tabindex="${index}"]`);
|
||||
if (el) {
|
||||
el.focus();
|
||||
}
|
||||
@@ -66,9 +75,6 @@ export default {
|
||||
// override me
|
||||
return "Placeholder";
|
||||
},
|
||||
bindProps() {
|
||||
return { ...this.$props, ...this.$attrs };
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
:dataset="dataset"
|
||||
:row-key="rowKey"
|
||||
@update:dataset="onUpdate"
|
||||
@delete="onDelete"
|
||||
@undelete="onUndelete"
|
||||
@@ -26,6 +27,7 @@ export default {
|
||||
},
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
rowKey: { type: [Number, String], required: false, default: "" },
|
||||
},
|
||||
computed: {
|
||||
dataset() {
|
||||
|
||||
@@ -1,159 +1,170 @@
|
||||
<template>
|
||||
<PriorityMenu :starting-height="27">
|
||||
<PriorityMenuItem
|
||||
v-if="notIn(STATES.NOT_VIEWABLE, STATES.DISCARDED)"
|
||||
key="view-dataset"
|
||||
:title="displayButtonTitle"
|
||||
:disabled="dataset.purged || isIn(STATES.UPLOAD, STATES.NEW)"
|
||||
@click.stop="viewData"
|
||||
icon="fas fa-eye"
|
||||
/>
|
||||
|
||||
<PriorityMenuItem
|
||||
v-if="notIn(STATES.DISCARDED)"
|
||||
key="edit-dataset"
|
||||
:title="editButtonTitle"
|
||||
:disabled="dataset.deleted || isIn(STATES.UPLOAD, STATES.NEW)"
|
||||
@click.stop="
|
||||
backboneRoute('datasets/edit', {
|
||||
dataset_id: dataset.id,
|
||||
})
|
||||
"
|
||||
icon="fa fa-pencil"
|
||||
/>
|
||||
|
||||
<PriorityMenuItem
|
||||
v-if="dataset.accessible"
|
||||
key="delete-dataset"
|
||||
:title="deleteButtonTitle"
|
||||
:disabled="dataset.purged"
|
||||
@click.stop="onDeleteClick"
|
||||
:icon="dataset.deleted ? 'fas fa-trash-restore' : 'fas fa-trash'"
|
||||
/>
|
||||
|
||||
<PriorityMenuItem
|
||||
v-if="expanded"
|
||||
key="edit-tags"
|
||||
title="Edit Dataset Tags"
|
||||
:pressed="showTags"
|
||||
@click.stop="$emit('update:showTags', !showTags)"
|
||||
icon="fas fa-tags"
|
||||
/>
|
||||
|
||||
<PriorityMenuItem
|
||||
v-if="isIn(STATES.ERROR)"
|
||||
key="show-error"
|
||||
title="Error"
|
||||
@click.stop="
|
||||
backboneRoute('datasets/error', {
|
||||
dataset_id: dataset.id,
|
||||
})
|
||||
"
|
||||
icon="fa fa-bug"
|
||||
/>
|
||||
|
||||
<PriorityMenuItem
|
||||
v-if="!(showDownloads && dataset.hasMetaData)"
|
||||
key="download"
|
||||
title="Download"
|
||||
:href="prependPath(dataset.getUrl('download'))"
|
||||
target="_blank"
|
||||
download
|
||||
icon="fas fa-file-download"
|
||||
/>
|
||||
|
||||
<PriorityMenuItem
|
||||
v-if="showDownloads && dataset.hasMetaData"
|
||||
key="download-metadata"
|
||||
title="Download"
|
||||
:href="prependPath(dataset.getUrl('download'))"
|
||||
target="_blank"
|
||||
download
|
||||
icon="fas fa-file-download"
|
||||
/>
|
||||
|
||||
<PriorityMenuItem
|
||||
v-if="!dataset.purged && dataset.getUrl('download')"
|
||||
key="copy-link"
|
||||
title="Copy Link"
|
||||
icon="fa fa-chain"
|
||||
@click.stop="$emit('copy-link')"
|
||||
/>
|
||||
|
||||
<div v-if="showDownloads && dataset.hasMetaData">
|
||||
<PriorityMenuItem
|
||||
v-for="mf in dataset.meta_files"
|
||||
:key="'download-' + mf.download_url"
|
||||
:title="'Download ' + mf.file_type"
|
||||
:href="prependPath(dataset.getUrl('meta_download') + mf.file_type)"
|
||||
target="_blank"
|
||||
download
|
||||
icon="fas fa-file-download"
|
||||
<div>
|
||||
<b-button-group>
|
||||
<IconButton
|
||||
v-if="notIn(STATES.NOT_VIEWABLE, STATES.DISCARDED)"
|
||||
icon="eye"
|
||||
:title="displayButtonTitle"
|
||||
:disabled="dataset.purged || isIn(STATES.UPLOAD, STATES.NEW)"
|
||||
@click.stop="viewData"
|
||||
variant="link"
|
||||
class="px-1"
|
||||
/>
|
||||
<IconButton
|
||||
v-if="notIn(STATES.DISCARDED)"
|
||||
icon="pen"
|
||||
:title="editButtonTitle"
|
||||
:disabled="dataset.deleted || isIn(STATES.UPLOAD, STATES.NEW)"
|
||||
@click.stop="backboneRoute('datasets/edit', { dataset_id: dataset.id })"
|
||||
variant="link"
|
||||
class="px-1"
|
||||
/>
|
||||
<IconButton
|
||||
v-if="dataset.accessible"
|
||||
:icon="dataset.deleted ? 'trash-restore' : 'trash'"
|
||||
:title="deleteButtonTitle"
|
||||
:disabled="dataset.purged"
|
||||
v-b-modal="bsId('delete-modal')"
|
||||
variant="link"
|
||||
class="px-1"
|
||||
@click.stop
|
||||
/>
|
||||
</div>
|
||||
|
||||
<PriorityMenuItem
|
||||
v-if="dataset.rerunnable && dataset.creating_job && notIn(STATES.UPLOAD, STATES.NOT_VIEWABLE)"
|
||||
key="run-job"
|
||||
title="Run job again"
|
||||
:href="prependPath(dataset.getUrl('rerun'))"
|
||||
@click.stop="
|
||||
backboneRoute('/', {
|
||||
job_id: dataset.creating_job,
|
||||
})
|
||||
"
|
||||
icon="fa fa-redo"
|
||||
/>
|
||||
<b-dropdown
|
||||
size="sm"
|
||||
variant="link"
|
||||
:text="'Dataset Operations' | l"
|
||||
toggle-class="p-1 pl-2"
|
||||
class="flex-grow-0"
|
||||
v-if="expanded"
|
||||
boundary="window"
|
||||
>
|
||||
<template v-slot:button-content>
|
||||
<Icon icon="ellipsis-v" variant="link" />
|
||||
<span class="sr-only">Dataset Operations</span>
|
||||
</template>
|
||||
|
||||
<PriorityMenuItem
|
||||
v-if="showViz && hasViz && isIn(STATES.OK, STATES.FAILED_METADATA)"
|
||||
key="visualize"
|
||||
title="Visualize Data"
|
||||
@click.stop.prevent="visualize"
|
||||
icon="fa-bar-chart"
|
||||
/>
|
||||
<b-dropdown-item
|
||||
v-if="isIn(STATES.ERROR)"
|
||||
key="show-error"
|
||||
title="Error"
|
||||
@click.stop="
|
||||
backboneRoute('datasets/error', {
|
||||
dataset_id: dataset.id,
|
||||
})
|
||||
"
|
||||
>
|
||||
<Icon icon="exclamation-triangle" class="mr-1" />
|
||||
<span v-localize>Show Error</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<PriorityMenuItem
|
||||
v-if="currentUser && currentUser.id && dataset.creating_job"
|
||||
key="tool-tip"
|
||||
title="Tool Help"
|
||||
@click.stop="showToolHelp(dataset.creating_job)"
|
||||
icon="fa fa-question"
|
||||
/>
|
||||
<b-dropdown-item
|
||||
v-if="!dataset.purged && dataset.getUrl('download')"
|
||||
title="Copy Link"
|
||||
@click.stop="$emit('copy-link')"
|
||||
>
|
||||
<Icon icon="link" class="mr-1" />
|
||||
<span v-localize>Copy Link</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<PriorityMenuItem
|
||||
v-if="notIn(STATES.NOT_VIEWABLE)"
|
||||
key="dataset-details"
|
||||
title="View Details"
|
||||
@click.stop.prevent="
|
||||
iframeAdd({
|
||||
path: dataset.getUrl('show_params'),
|
||||
title: 'Dataset details',
|
||||
})
|
||||
"
|
||||
icon="fa fa-info-circle"
|
||||
/>
|
||||
</PriorityMenu>
|
||||
<b-dropdown-item
|
||||
v-if="!(showDownloads && dataset.hasMetaData)"
|
||||
title="Download"
|
||||
:href="prependPath(dataset.getUrl('download'))"
|
||||
target="_blank"
|
||||
download
|
||||
>
|
||||
<Icon icon="download" class="mr-1" />
|
||||
<span v-localize>Download</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-group v-if="showDownloads && dataset.hasMetaData">
|
||||
<b-dropdown-item
|
||||
v-for="mf in dataset.meta_files"
|
||||
:key="'download-' + mf.download_url"
|
||||
:title="'Download ' + mf.file_type"
|
||||
:href="prependPath(dataset.getUrl('meta_download') + mf.file_type)"
|
||||
target="_blank"
|
||||
download
|
||||
>
|
||||
<Icon icon="download" class="mr-1" />
|
||||
<span v-localize>{{ "Download " + mf.file_type }}</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown-group>
|
||||
|
||||
<b-dropdown-item
|
||||
v-if="dataset.rerunnable && dataset.creating_job && notIn(STATES.UPLOAD, STATES.NOT_VIEWABLE)"
|
||||
title="Run job again"
|
||||
:href="prependPath(dataset.getUrl('rerun'))"
|
||||
@click.stop="
|
||||
backboneRoute('/', {
|
||||
job_id: dataset.creating_job,
|
||||
})
|
||||
"
|
||||
>
|
||||
<Icon icon="play" class="mr-1" />
|
||||
<span v-localize>Run job again</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
v-if="showViz && hasViz && isIn(STATES.OK, STATES.FAILED_METADATA)"
|
||||
title="Visualize Data"
|
||||
@click.stop.prevent="visualize"
|
||||
>
|
||||
<Icon icon="chart-area" class="mr-1" />
|
||||
<span v-localize>Visualize Data</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
v-if="currentUser && currentUser.id && dataset.creating_job"
|
||||
title="Tool Help"
|
||||
@click.stop="showToolHelp(dataset.creating_job)"
|
||||
>
|
||||
<Icon icon="question" class="mr-1" />
|
||||
<span v-localize>Tool Help</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
v-if="notIn(STATES.NOT_VIEWABLE)"
|
||||
key="dataset-details"
|
||||
title="View Dataset Details"
|
||||
@click.stop.prevent="
|
||||
iframeAdd({
|
||||
path: dataset.getUrl('show_params'),
|
||||
title: 'View Dataset Details',
|
||||
})
|
||||
"
|
||||
>
|
||||
<Icon icon="info-circle" class="mr-1" />
|
||||
<span v-localize>View Dataset Details</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</b-button-group>
|
||||
|
||||
<!-- modals -->
|
||||
<b-modal :id="bsId('delete-modal')" title="Delete Dataset?" title-tag="h2" @ok="onDeleteClick">
|
||||
<p v-localize>Really delete this dataset?</p>
|
||||
</b-modal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from "vuex";
|
||||
import { Dataset, STATES } from "../../model";
|
||||
import { PriorityMenu, PriorityMenuItem } from "components/PriorityMenu";
|
||||
import { Dataset } from "../../model";
|
||||
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
|
||||
import IconButton from "components/IconButton";
|
||||
|
||||
export default {
|
||||
mixins: [legacyNavigationMixin],
|
||||
inject: ["STATES"],
|
||||
|
||||
components: {
|
||||
PriorityMenu,
|
||||
PriorityMenuItem,
|
||||
IconButton,
|
||||
},
|
||||
|
||||
props: {
|
||||
dataset: { type: Dataset, required: true },
|
||||
expanded: { type: Boolean, required: true },
|
||||
showTags: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
|
||||
data() {
|
||||
@@ -163,11 +174,6 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
// make available to template
|
||||
this.STATES = STATES;
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters("user", ["currentUser"]),
|
||||
...mapGetters("config", ["config"]),
|
||||
@@ -180,10 +186,10 @@ export default {
|
||||
if (this.dataset.purged) {
|
||||
return "Cannot display datasets removed from disk";
|
||||
}
|
||||
if (this.dataset.state == STATES.UPLOAD) {
|
||||
if (this.dataset.state == this.STATES.UPLOAD) {
|
||||
return "This dataset must finish uploading before it can be viewed";
|
||||
}
|
||||
if (this.dataset.state == STATES.NEW) {
|
||||
if (this.dataset.state == this.STATES.NEW) {
|
||||
return "This dataset is not yet viewable";
|
||||
}
|
||||
return "View data";
|
||||
@@ -196,7 +202,7 @@ export default {
|
||||
if (this.dataset.purged) {
|
||||
return "Cannot edit attributes of datasets removed from disk";
|
||||
}
|
||||
const unreadyStates = new Set([STATES.UPLOAD, STATES.NEW]);
|
||||
const unreadyStates = new Set([this.STATES.UPLOAD, this.STATES.NEW]);
|
||||
if (unreadyStates.has(this.dataset.state)) {
|
||||
return "This dataset is not yet editable";
|
||||
}
|
||||
@@ -218,7 +224,7 @@ export default {
|
||||
if (!this.dataset.hasData) {
|
||||
return false;
|
||||
}
|
||||
const okStates = new Set([STATES.OK, STATES.FAILED_METADATA, STATES.ERROR]);
|
||||
const okStates = new Set([this.STATES.OK, this.STATES.FAILED_METADATA, this.STATES.ERROR]);
|
||||
return okStates.has(this.dataset.state);
|
||||
},
|
||||
|
||||
@@ -278,6 +284,11 @@ export default {
|
||||
const eventName = this.dataset.deleted ? "undelete" : "delete";
|
||||
this.$emit(eventName);
|
||||
},
|
||||
|
||||
// bootstrap demands ids instead of simply using a ref so I guess we're doing this
|
||||
bsId(suffix) {
|
||||
return `dataset-${this.dataset.id}-${suffix}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -8,7 +8,7 @@ either through the props, and make updates through the events -->
|
||||
<template>
|
||||
<div
|
||||
class="dataset history-content"
|
||||
:id="typedId"
|
||||
:data-content-type-id="typeId"
|
||||
:class="{ expanded, collapsed, selected }"
|
||||
:data-state="dataset.state"
|
||||
@keydown.arrow-left.self.stop="$emit('update:expanded', false)"
|
||||
@@ -16,73 +16,101 @@ either through the props, and make updates through the events -->
|
||||
@keydown.space.self.stop.prevent="$emit('update:selected', !selected)"
|
||||
>
|
||||
<!-- name, state buttons, menus -->
|
||||
<nav class="content-top-menu" @click.stop="$emit('update:expanded', !expanded)">
|
||||
<div class="content-status-indicators" @click.stop>
|
||||
<b-check
|
||||
class="selector"
|
||||
v-if="showSelection"
|
||||
:checked="selected"
|
||||
@change="$emit('update:selected', $event)"
|
||||
<nav class="p-1 d-flex align-items-center cursor-pointer" @click.stop="$emit('update:expanded', !expanded)">
|
||||
<div class="d-flex align-items-center flex-grow-1 overflow-hidden">
|
||||
<div class="pl-1" v-if="showSelectionBox">
|
||||
<b-check class="selector" :checked="selected" @change="$emit('update:selected', $event)"></b-check>
|
||||
</div>
|
||||
|
||||
<StatusIcon
|
||||
v-if="!collapsed"
|
||||
class="status-icon px-1"
|
||||
:state="dataset.state"
|
||||
@click.stop="onStatusClick"
|
||||
/>
|
||||
<StatusIcon v-if="!ok" class="status-icon px-1" :state="dataset.state" @click.stop="onStatusClick" />
|
||||
<StateBtn
|
||||
v-if="!dataset.visible"
|
||||
|
||||
<IconButton
|
||||
v-if="!collapsed && !dataset.visible"
|
||||
class="px-1"
|
||||
state="hidden"
|
||||
title="Unhide"
|
||||
icon="fa fa-eye-slash"
|
||||
icon="eye-slash"
|
||||
@click.stop="$emit('unhide')"
|
||||
/>
|
||||
<StateBtn
|
||||
v-if="dataset.isDeleted && !dataset.purged"
|
||||
|
||||
<IconButton
|
||||
v-if="!collapsed && dataset.isDeleted && !dataset.purged"
|
||||
class="px-1"
|
||||
state="deleted"
|
||||
title="Undelete"
|
||||
icon="fas fa-trash-restore"
|
||||
icon="trash-restore"
|
||||
@click.stop="$emit('undelete')"
|
||||
/>
|
||||
|
||||
<div class="content-title p-1 overflow-hidden">
|
||||
<h5 class="text-truncate" v-if="collapsed">
|
||||
<!-- <span class="hid">{{ dataset.hid }}</span> -->
|
||||
<span class="name">{{ dataset.title }}</span>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-title">
|
||||
<h5 class="text-truncate">
|
||||
<span class="hid">{{ dataset.hid }}</span>
|
||||
<span v-if="collapsed || !dataset.canEditName" class="name">{{ dataset.title }}</span>
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="content-item-menu">
|
||||
<div class="d-flex">
|
||||
<slot name="menu">
|
||||
<DatasetMenu :dataset="dataset" :expanded="expanded" v-on="$listeners" :show-tags.sync="showTags" />
|
||||
<DatasetMenu :dataset="dataset" :expanded="expanded" v-on="$listeners" />
|
||||
</slot>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!--- read-only tags with name: prefix -->
|
||||
<div v-if="collapsed && dataset.nameTags.length" class="nametags p-1">
|
||||
<!--- read-only tags with name: prefix
|
||||
<div v-if="collapsed && dataset.nameTags.length" class="nametags p-2">
|
||||
<Nametag v-for="tag in dataset.nameTags" :key="tag" :tag="tag" />
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<!-- expanded view with editors -->
|
||||
<header v-if="expanded" class="p-2 details">
|
||||
<ClickToEdit
|
||||
v-if="dataset.canEditName"
|
||||
tag-name="h4"
|
||||
:value="dataset.name"
|
||||
@input="$emit('update:dataset', { name: $event })"
|
||||
:display-label="dataset.title"
|
||||
:tooltip-title="'Edit dataset name...' | localize"
|
||||
tooltip-placement="left"
|
||||
/>
|
||||
<div v-if="expanded" class="p-3 details">
|
||||
<div class="d-flex">
|
||||
<div class="flex-grow-1">
|
||||
<h4>{{ name || "(Dataset Name)" }}</h4>
|
||||
|
||||
<Annotation
|
||||
class="mt-1"
|
||||
:value="dataset.annotation"
|
||||
@input="$emit('update:dataset', { annotation: $event })"
|
||||
/>
|
||||
<template v-if="!editing">
|
||||
<p v-if="annotation">{{ annotation }}</p>
|
||||
<div v-if="dataset.nameTags.length" class="nametags mt-2">
|
||||
<Nametag v-for="tag in dataset.nameTags" :key="tag" :tag="tag" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<ContentTags v-if="showTags" class="mt-2" :content="dataset" />
|
||||
<div v-else-if="dataset.nameTags.length" class="nametags mt-2">
|
||||
<Nametag v-for="tag in dataset.nameTags" :key="tag" :tag="tag" />
|
||||
<div v-else class="my-3">
|
||||
<b-textarea
|
||||
class="mb-3"
|
||||
v-model="name"
|
||||
placeholder="Dataset Name"
|
||||
trim
|
||||
max-rows="4"
|
||||
></b-textarea>
|
||||
|
||||
<b-textarea
|
||||
class="mb-3"
|
||||
v-model="annotation"
|
||||
placeholder="Annotation (optional)"
|
||||
trim
|
||||
max-rows="4"
|
||||
></b-textarea>
|
||||
|
||||
<ContentTags class="mt-2" :content="dataset" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<EditorMenu
|
||||
class="ml-3 flex-grow-0 d-flex flex-column"
|
||||
v-if="writable"
|
||||
model-name="Collection"
|
||||
:editing.sync="editing"
|
||||
:writable="writable"
|
||||
:valid="valid"
|
||||
:dirty="dirty"
|
||||
@save="save"
|
||||
@revert="revert"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="details">
|
||||
@@ -102,41 +130,44 @@ either through the props, and make updates through the events -->
|
||||
</div>
|
||||
<pre v-if="dataset.peek" class="dataset-peek p-1" v-html="dataset.peek"></pre>
|
||||
</div>
|
||||
</header>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Dataset, STATES } from "../../model";
|
||||
import ClickToEdit from "components/ClickToEdit";
|
||||
import Annotation from "components/Annotation";
|
||||
import { Nametag } from "components/Nametags";
|
||||
import { StatusIcon, StateBtn } from "../../StatusIcon";
|
||||
import StatusIcon from "../../StatusIcon";
|
||||
import DatasetMenu from "./DatasetMenu";
|
||||
import DatasetSummary from "./Summary";
|
||||
import ContentTags from "../../ContentTags";
|
||||
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
|
||||
import IconButton from "components/IconButton";
|
||||
import EditorMenu from "../../EditorMenu";
|
||||
|
||||
export default {
|
||||
mixins: [legacyNavigationMixin],
|
||||
components: {
|
||||
ClickToEdit,
|
||||
Annotation,
|
||||
StatusIcon,
|
||||
StateBtn,
|
||||
DatasetMenu,
|
||||
DatasetSummary,
|
||||
ContentTags,
|
||||
Nametag,
|
||||
IconButton,
|
||||
EditorMenu,
|
||||
},
|
||||
props: {
|
||||
dataset: { type: Dataset, required: true },
|
||||
expanded: { type: Boolean, required: true },
|
||||
selected: { type: Boolean, required: false, default: false },
|
||||
showSelection: { type: Boolean, required: false, default: false },
|
||||
writable: { type: Boolean, required: false, default: true },
|
||||
selectable: { type: Boolean, required: false, default: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showTags: false,
|
||||
editing: false,
|
||||
tempContent: this.dataset.clone(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
@@ -146,11 +177,33 @@ export default {
|
||||
typeId() {
|
||||
return this.dataset.type_id;
|
||||
},
|
||||
ok() {
|
||||
return this.dataset.state == "ok";
|
||||
isEditing() {
|
||||
return this.writable && this.editing;
|
||||
},
|
||||
typedId() {
|
||||
return `dataset-${this.dataset.id}`;
|
||||
valid() {
|
||||
return true;
|
||||
},
|
||||
dirty() {
|
||||
return !this.tempContent.equals(this.dataset);
|
||||
},
|
||||
name: {
|
||||
get() {
|
||||
return this.tempContent?.name || "";
|
||||
},
|
||||
set(newVal) {
|
||||
this.patchTempContent({ name: newVal });
|
||||
},
|
||||
},
|
||||
annotation: {
|
||||
get() {
|
||||
return this.tempContent?.annotation || "";
|
||||
},
|
||||
set(newVal) {
|
||||
this.patchTempContent({ annotation: newVal });
|
||||
},
|
||||
},
|
||||
showSelectionBox() {
|
||||
return this.selectable && this.showSelection;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -163,6 +216,23 @@ export default {
|
||||
console.log("unhandled status icon click", this.dataset.state);
|
||||
}
|
||||
},
|
||||
patchTempContent(props = {}) {
|
||||
this.tempContent = this.tempContent.patch(props);
|
||||
},
|
||||
save() {
|
||||
if (this.valid) {
|
||||
this.$emit("update:dataset", this.tempContent);
|
||||
this.editing = false;
|
||||
}
|
||||
},
|
||||
reset() {
|
||||
this.tempContent = this.dataset.clone();
|
||||
},
|
||||
revert() {
|
||||
if (this.dirty) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -1,18 +1,20 @@
|
||||
<template>
|
||||
<b-dropdown no-caret right variant="link" size="sm" boundary="window" toggle-class="p-1">
|
||||
<b-dropdown no-caret right variant="link" size="sm" boundary="window" toggle-class="p-1 pl-3">
|
||||
<template v-slot:button-content>
|
||||
<i class="fas fa-trash" />
|
||||
<Icon icon="trash" variant="link" />
|
||||
<span class="sr-only">Delete Collection</span>
|
||||
</template>
|
||||
|
||||
<b-dropdown-item @click.stop="$emit('delete')"> Delete Collection Only </b-dropdown-item>
|
||||
<b-dropdown-item @click.stop="$emit('delete')">
|
||||
<span v-localize>Delete Collection Only </span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item @click.stop="$emit('delete', { recursive: true })">
|
||||
Delete Contained Datasets
|
||||
<span v-localize>Delete Contained Datasets</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item @click.stop="$emit('delete', { recursive: true, purge: true })">
|
||||
Purge Contained Datasets
|
||||
<span v-localize>Purge Contained Datasets</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</template>
|
||||
|
||||
@@ -9,16 +9,17 @@
|
||||
:data-state="dsc.state"
|
||||
@keydown.arrow-right.self.stop="$emit('viewCollection')"
|
||||
@keydown.space.self.stop.prevent="$emit('update:selected', !selected)"
|
||||
@click.stop="$emit('viewCollection')"
|
||||
>
|
||||
<nav class="content-top-menu">
|
||||
<div class="content-status-indicators mr-1" @click.stop>
|
||||
<b-check
|
||||
class="selector"
|
||||
v-if="showSelection"
|
||||
:checked="selected"
|
||||
@change="$emit('update:selected', $event)"
|
||||
/>
|
||||
<nav class="p-1 d-flex align-items-center cursor-pointer">
|
||||
<div class="d-flex align-items-center flex-grow-1 overflow-hidden">
|
||||
<div class="pl-1" v-if="showSelection">
|
||||
<b-check
|
||||
class="selector"
|
||||
:checked="selected"
|
||||
@click.stop
|
||||
@change="$emit('update:selected', $event)"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<StatusIcon
|
||||
v-if="dsc.state != 'ok'"
|
||||
@@ -27,44 +28,45 @@
|
||||
@click.stop="onStatusClick"
|
||||
/>
|
||||
|
||||
<StateBtn
|
||||
<IconButton
|
||||
v-if="!dsc.visible"
|
||||
class="px-1"
|
||||
state="hidden"
|
||||
title="Unhide"
|
||||
icon="fa fa-eye-slash"
|
||||
icon="eye-slash"
|
||||
@click.stop="$emit('unhide')"
|
||||
/>
|
||||
|
||||
<StateBtn
|
||||
<IconButton
|
||||
v-if="dsc.deleted"
|
||||
class="px-1"
|
||||
state="deleted"
|
||||
title="Undelete"
|
||||
icon="fas fa-trash-restore"
|
||||
icon="trash-restore"
|
||||
@click.stop="$emit('undelete')"
|
||||
/>
|
||||
|
||||
<StateBtn
|
||||
<IconButton
|
||||
class="px-1"
|
||||
state="ok"
|
||||
title="Collection"
|
||||
icon="fas fa-folder"
|
||||
icon="folder"
|
||||
@click.stop="$emit('viewCollection')"
|
||||
variant="link"
|
||||
/>
|
||||
|
||||
<div class="content-title flex-grow-1 overflow-hidden" @click.stop="$emit('viewCollection')">
|
||||
<h5 class="text-truncate">
|
||||
<!-- <span class="hid">{{ dsc.hid }}</span> -->
|
||||
<span class="name">{{ dsc.name }}</span>
|
||||
<span class="description">
|
||||
({{ dsc.collectionType | localize }} {{ dsc.collectionCountDescription | localize }})
|
||||
</span>
|
||||
</h5>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-title mr-1">
|
||||
<h5 class="text-truncate">
|
||||
<span class="hid">{{ dsc.hid }}</span>
|
||||
<span class="name">{{ dsc.name }}</span>
|
||||
<span class="description">
|
||||
({{ dsc.collectionType | localize }} {{ dsc.collectionCountDescription | localize }})
|
||||
</span>
|
||||
</h5>
|
||||
</div>
|
||||
|
||||
<div class="content-item-menu">
|
||||
<div class="d-flex" v-if="writable">
|
||||
<slot name="menu">
|
||||
<DscMenu v-if="!dsc.deleted" v-on="$listeners" />
|
||||
</slot>
|
||||
@@ -72,7 +74,7 @@
|
||||
</nav>
|
||||
|
||||
<!--- read-only tags with name: prefix -->
|
||||
<div v-if="dsc.nameTags.length" class="nametags p-1">
|
||||
<div v-if="dsc.nameTags.length" class="nametags p-2">
|
||||
<Nametag v-for="tag in dsc.nameTags" :key="tag" :tag="tag" />
|
||||
</div>
|
||||
|
||||
@@ -82,23 +84,25 @@
|
||||
|
||||
<script>
|
||||
import { DatasetCollection } from "../../model/DatasetCollection";
|
||||
import { StatusIcon, StateBtn } from "../../StatusIcon";
|
||||
import StatusIcon from "../../StatusIcon";
|
||||
import JobStateProgress from "./JobStateProgress";
|
||||
import DscMenu from "./DscMenu";
|
||||
import { Nametag } from "components/Nametags";
|
||||
import IconButton from "components/IconButton";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StatusIcon,
|
||||
StateBtn,
|
||||
JobStateProgress,
|
||||
DscMenu,
|
||||
Nametag,
|
||||
IconButton,
|
||||
},
|
||||
props: {
|
||||
dsc: { type: DatasetCollection, required: true },
|
||||
selected: { type: Boolean, required: false, default: false },
|
||||
showSelection: { type: Boolean, required: false, default: false },
|
||||
writable: { type: Boolean, required: false, default: true },
|
||||
},
|
||||
methods: {
|
||||
onStatusClick() {
|
||||
|
||||
@@ -28,7 +28,6 @@ import { STATES } from "../model";
|
||||
export default {
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
index: { type: Number, required: true },
|
||||
},
|
||||
computed: {
|
||||
dsc() {
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
<!-- a read-only dataset inside a collection. The collection query does not return enough
|
||||
information to properly render the dataset, so what we do is look it up on expand, checking the
|
||||
cache first to see if we already have the data -->
|
||||
|
||||
<template>
|
||||
<DatasetUI
|
||||
v-if="dataset"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
:dataset="dataset"
|
||||
:writable="false"
|
||||
:selectable="false"
|
||||
:row-key="rowKey"
|
||||
@update:expanded="onExpand"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Dataset } from "../model";
|
||||
import { getContentByTypeId, cacheContent } from "../caching";
|
||||
import { getContentDetails } from "../model/queries";
|
||||
import DatasetUI from "./Dataset/DatasetUI";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DatasetUI,
|
||||
},
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
localItem: { ...this.item },
|
||||
loading: false,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dataset() {
|
||||
return new Dataset(this.localItem);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onExpand(val) {
|
||||
if (val) {
|
||||
this.loadDetails();
|
||||
}
|
||||
this.$emit("update:expand", val);
|
||||
},
|
||||
async loadDetails() {
|
||||
const { type_id, history_id } = this.item;
|
||||
const existingDataset = await getContentByTypeId(history_id, type_id);
|
||||
if (existingDataset) {
|
||||
this.localItem = existingDataset;
|
||||
} else {
|
||||
const loadedDataset = await getContentDetails(this.item);
|
||||
await cacheContent(loadedDataset);
|
||||
this.localItem = loadedDataset;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,9 +1,6 @@
|
||||
/**
|
||||
* Scrolling components for history contents and collection contents.
|
||||
*/
|
||||
|
||||
import "./styles.scss";
|
||||
|
||||
export { default as ContentItem } from "./ContentItem";
|
||||
export { default as CollectionContentItem } from "./CollectionContentItem";
|
||||
export { default as HistoryContentItem } from "./HistoryContentItem";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
.content-item {
|
||||
outline: none;
|
||||
border-bottom: 1px solid white;
|
||||
border-bottom: 2px solid white;
|
||||
&:focus-within {
|
||||
box-shadow: inset 0 0 1em 0.25em rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
@@ -11,36 +11,9 @@
|
||||
|
||||
// Nav across the top of the content item
|
||||
.content-item .content-top-menu {
|
||||
/* a fixed-height is important for proper
|
||||
functioning of the PriorityMenuItem dropdown since
|
||||
it relies on flexbox and overflow */
|
||||
min-height: 2em;
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
// flex parent
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
|
||||
// error, clock, etc.
|
||||
.content-status-indicators {
|
||||
// d-flex mr-1 align-items-center
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
&:blank {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
// name & labelling
|
||||
.content-title {
|
||||
|
||||
// flex child
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
|
||||
.hid {
|
||||
color: adjust-color($text-color, $alpha: -0.6);
|
||||
font-size: 80%;
|
||||
@@ -56,24 +29,4 @@
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
// priority menu on right side
|
||||
.content-item-menu {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Setting a max-width here limits the number of icons that
|
||||
// will show. The priority menu is css awayre
|
||||
|
||||
.content-item.collapsed .content-item-menu {
|
||||
max-width: 90px;
|
||||
}
|
||||
|
||||
// .content-menu button:hover,
|
||||
// .content-menu .show > button.dropdown-toggle {
|
||||
// background-color: transparent;
|
||||
// border-color: transparent;
|
||||
// }
|
||||
}
|
||||
@@ -1,136 +1,168 @@
|
||||
<template>
|
||||
<section>
|
||||
<header class="d-flex align-items-center justify-content-between">
|
||||
<h6 class="text-nowrap mr-3">
|
||||
<span v-localize>Matches</span>
|
||||
<a href="#" @click="showFilter = !showFilter">
|
||||
<span>{{ totalMatches }}</span>
|
||||
</a>
|
||||
</h6>
|
||||
|
||||
<PriorityMenu style="max-width: 50%" :starting-height="27">
|
||||
<PriorityMenuItem
|
||||
key="filter"
|
||||
<nav class="content-operations-menu d-flex justify-content-between bg-secondary">
|
||||
<b-button-group>
|
||||
<IconButton
|
||||
:title="hasSelection ? 'Select All Contents' : 'Unselect All Contents'"
|
||||
icon="check-square"
|
||||
:disabled="!totalMatches"
|
||||
:pressed="hasSelection"
|
||||
@click="toggleSelectAll"
|
||||
/>
|
||||
<IconButton
|
||||
title="Filter History Content"
|
||||
icon="fa fa-filter"
|
||||
@click="showFilter = !showFilter"
|
||||
icon="filter"
|
||||
:disabled="!totalMatches"
|
||||
:pressed="showFilter"
|
||||
@click="showFilter = !showFilter"
|
||||
/>
|
||||
|
||||
<PriorityMenuItem
|
||||
key="selection"
|
||||
title="Operations on multiple datasets"
|
||||
class="show-history-content-selectors-btn"
|
||||
icon="fas fa-check-square"
|
||||
@click="$emit('update:show-selection', !showSelection)"
|
||||
:pressed="showSelection"
|
||||
<IconButton
|
||||
title="Collapse Expanded Items"
|
||||
icon="compress"
|
||||
:disabled="!expandedCount"
|
||||
@click="$emit('collapseAllContent')"
|
||||
/>
|
||||
</b-button-group>
|
||||
|
||||
<PriorityMenuItem
|
||||
key="copy-datasets"
|
||||
class="copy-datasets-menu-item"
|
||||
title="Copy Datasets"
|
||||
icon="fas fa-copy"
|
||||
@click="iframeRedirect('/dataset/copy_datasets')"
|
||||
/>
|
||||
|
||||
<PriorityMenuItem
|
||||
key="collapse-expanded"
|
||||
title="Collapse Expanded Datasets"
|
||||
icon="fas fa-compress-alt"
|
||||
@click="emit('collapseAllContent')"
|
||||
/>
|
||||
|
||||
<PriorityMenuItem
|
||||
key="show-hidden-datasets"
|
||||
title="Unhide All Hidden Datasets"
|
||||
icon="fas fa-eye"
|
||||
@click="$bvModal.show('show-hidden-content')"
|
||||
/>
|
||||
|
||||
<PriorityMenuItem
|
||||
key="delete-hidden"
|
||||
title="Delete Hidden Datasets"
|
||||
icon="fas fa-trash"
|
||||
@click="$bvModal.show('delete-hidden-content')"
|
||||
/>
|
||||
|
||||
<PriorityMenuItem
|
||||
key="purge-hidden"
|
||||
title="Purge Hidden Datasets"
|
||||
icon="fas fa-burn"
|
||||
@click="$bvModal.show('purge-deleted-content')"
|
||||
/>
|
||||
</PriorityMenu>
|
||||
</header>
|
||||
|
||||
<transition name="shutterfade">
|
||||
<content-filters v-if="showContentFilters" class="content-filters mt-1" :params.sync="localParams" />
|
||||
</transition>
|
||||
|
||||
<transition name="shutterfade">
|
||||
<b-button-toolbar
|
||||
v-if="showSelection"
|
||||
class="content-selection justify-content-between mt-1 list-action-menu"
|
||||
>
|
||||
<b-button-group>
|
||||
<b-button size="sm" @click="$emit('selectAllContent')">
|
||||
{{ "Select All" | localize }}
|
||||
</b-button>
|
||||
<b-button size="sm" @click="$emit('resetSelection')">
|
||||
{{ "Unselect All" | localize }}
|
||||
</b-button>
|
||||
</b-button-group>
|
||||
|
||||
<b-button-group>
|
||||
<b-dropdown
|
||||
class="ml-auto history-contents-list-action-menu-btn"
|
||||
class="history-contents-list-action-menu-btn"
|
||||
size="sm"
|
||||
text="With Selected"
|
||||
text="Selection"
|
||||
:disabled="!hasSelection"
|
||||
>
|
||||
<b-dropdown-item @click="hideSelected">
|
||||
{{ "Hide Datasets" | localize }}
|
||||
<b-dropdown-text id="history-op-selected-content">
|
||||
<span v-localize v-if="hasSelection">With {{ numSelected }} selected items...</span>
|
||||
<span v-localize v-else>You don't have any selected content.</span>
|
||||
</b-dropdown-text>
|
||||
|
||||
<b-dropdown-item aria-describedby="history-op-selected-content" v-b-modal:hide-selected-content>
|
||||
<span v-localize>Hide</span>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item @click="unhideSelected">
|
||||
{{ "Unhide Datasets" | localize }}
|
||||
|
||||
<b-dropdown-item aria-describedby="history-op-selected-content" v-b-modal:show-selected-content>
|
||||
<span v-localize>Unhide</span>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item @click="deleteSelected">
|
||||
{{ "Delete Datasets" | localize }}
|
||||
|
||||
<b-dropdown-item aria-describedby="history-op-selected-content" v-b-modal:delete-selected-content>
|
||||
<span v-localize>Delete</span>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item @click="undeleteSelected">
|
||||
{{ "Undelete Datasets" | localize }}
|
||||
|
||||
<b-dropdown-item aria-describedby="history-op-selected-content" v-b-modal:restore-selected-content>
|
||||
<span v-localize>Undelete</span>
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item @click="purgeSelected">
|
||||
{{ "Permanently Delete Datasets" | localize }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item @click="buildDatasetList">
|
||||
{{ "Build Dataset List" | localize }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item @click="buildDatasetPair">
|
||||
{{ "Build Dataset Pair" | localize }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item @click="buildListOfPairs">
|
||||
{{ "Build List of Dataset Pairs" | localize }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item @click="buildCollectionFromRules">
|
||||
{{ "Build Collection from Rules" | localize }}
|
||||
|
||||
<b-dropdown-item
|
||||
aria-describedby="history-op-selected-content"
|
||||
v-b-modal:purge-selected-content
|
||||
:disabled="!hasSelection"
|
||||
>
|
||||
<span v-localize>Permanently Delete</span>
|
||||
<span v-if="numSelected">{{ numSelected }} items</span>
|
||||
<span v-else>(Disabled)</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</b-button-toolbar>
|
||||
|
||||
<b-dropdown
|
||||
class="history-contents-list-action-menu-btn"
|
||||
size="sm"
|
||||
text="History"
|
||||
:disabled="!totalMatches"
|
||||
>
|
||||
<b-dropdown-text id="history-op-all-content">
|
||||
<span v-localize>With entire history...</span>
|
||||
</b-dropdown-text>
|
||||
|
||||
<b-dropdown-item v-b-modal:show-all-hidden-content aria-describedby="history-op-all-content">
|
||||
<span v-localize>Unhide All Hidden Content</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item v-b-modal:delete-all-hidden-content aria-describedby="history-op-all-content">
|
||||
<span v-localize>Delete All Hidden Content</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item v-b-modal:purge-all-deleted-content aria-describedby="history-op-all-content">
|
||||
<span v-localize>Purge All Hidden Content</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
|
||||
<b-dropdown
|
||||
class="history-contents-list-action-menu-btn"
|
||||
size="sm"
|
||||
text="Create New Content"
|
||||
:disabled="!totalMatches"
|
||||
>
|
||||
<template v-slot:button-content>
|
||||
<Icon icon="plus" />
|
||||
<span class="sr-only" v-localize>Create New Content</span>
|
||||
</template>
|
||||
|
||||
<b-dropdown-text id="history-op-new-content">
|
||||
<span>Create new content</span>
|
||||
</b-dropdown-text>
|
||||
|
||||
<b-dropdown-item
|
||||
aria-describedby="history-op-new-content"
|
||||
@click="iframeRedirect('/dataset/copy_datasets')"
|
||||
>
|
||||
<span v-localize>Copy Datasets</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item aria-describedby="history-op-new-content" @click="buildDatasetList">
|
||||
<span v-localize>Build Dataset List</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item aria-describedby="history-op-new-content" @click="buildDatasetPair">
|
||||
<span v-localize>Build Dataset Pair</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item aria-describedby="history-op-new-content" @click="buildListOfPairs">
|
||||
<span v-localize>Build List of Dataset Pairs</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item aria-describedby="history-op-new-content" @click="buildCollectionFromRules">
|
||||
<span v-localize>Build Collection from Rules</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
</b-button-group>
|
||||
</nav>
|
||||
|
||||
<transition name="shutterfade">
|
||||
<content-filters v-if="showContentFilters" class="content-filters p-2" :params.sync="localParams" />
|
||||
</transition>
|
||||
|
||||
<!-- #region Menus and popovers -->
|
||||
|
||||
<b-modal id="show-hidden-content" title="Show Hidden Datasets" title-tag="h2" @ok="unhideAll">
|
||||
<p>{{ "Really unhide all hidden datasets?" | localize }}</p>
|
||||
<b-modal id="hide-selected-content" title="Hide Selected Content?" title-tag="h2" @ok="hideSelected">
|
||||
<p v-localize>Really hide {{ numSelected }} content items?</p>
|
||||
</b-modal>
|
||||
|
||||
<b-modal id="delete-hidden-content" title="Delete Hidden Datasets" title-tag="h2" @ok="deleteAllHidden">
|
||||
<p>{{ "Really delete all hidden datasets?" | localize }}</p>
|
||||
<b-modal id="show-selected-content" title="Show Selected Content?" title-tag="h2" @ok="unhideSelected">
|
||||
<p v-localize>Really show {{ numSelected }} content items?</p>
|
||||
</b-modal>
|
||||
|
||||
<b-modal id="purge-deleted-content" title="Purge Deleted Datasets" title-tag="h2" @ok="purgeAllDeleted">
|
||||
<p>{{ "Really delete all deleted datasets permanently? This cannot be undone." | localize }}</p>
|
||||
<b-modal id="delete-selected-content" title="Delete Selected Content?" title-tag="h2" @ok="deleteSelected">
|
||||
<p v-localize>Really delete {{ numSelected }} content items?</p>
|
||||
</b-modal>
|
||||
|
||||
<b-modal id="restore-selected-content" title="Restore Selected Content?" title-tag="h2" @ok="undeleteSelected">
|
||||
<p v-localize>Really restore {{ numSelected }} content items?</p>
|
||||
</b-modal>
|
||||
|
||||
<b-modal id="purge-selected-content" title="Purge Selected Content?" title-tag="h2" @ok="purgeSelected">
|
||||
<p v-localize>Permanently delete {{ numSelected }} content items? This cannot be undone.</p>
|
||||
</b-modal>
|
||||
|
||||
<b-modal id="show-all-hidden-content" title="Show Hidden Datasets" title-tag="h2" @ok="unhideAll">
|
||||
<p v-localize>Really unhide all hidden datasets?</p>
|
||||
</b-modal>
|
||||
|
||||
<b-modal id="delete-all-hidden-content" title="Delete Hidden Datasets" title-tag="h2" @ok="deleteAllHidden">
|
||||
<p v-localize>Really delete all hidden datasets?</p>
|
||||
</b-modal>
|
||||
|
||||
<b-modal id="purge-all-deleted-content" title="Purge Deleted Datasets" title-tag="h2" @ok="purgeAllDeleted">
|
||||
<p v-localize>"Really delete all deleted datasets permanently? This cannot be undone.</p>
|
||||
</b-modal>
|
||||
|
||||
<!-- #endregion -->
|
||||
@@ -152,18 +184,16 @@ import {
|
||||
} from "./model";
|
||||
import { createDatasetCollection } from "./model/queries";
|
||||
import { cacheContent } from "./caching";
|
||||
|
||||
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
|
||||
import ContentFilters from "./ContentFilters";
|
||||
import { PriorityMenu, PriorityMenuItem } from "components/PriorityMenu";
|
||||
import { buildCollectionModal } from "./adapters/buildCollectionModal";
|
||||
import ContentFilters from "./ContentFilters";
|
||||
import IconButton from "components/IconButton";
|
||||
|
||||
export default {
|
||||
mixins: [legacyNavigationMixin],
|
||||
components: {
|
||||
ContentFilters,
|
||||
PriorityMenu,
|
||||
PriorityMenuItem,
|
||||
IconButton,
|
||||
},
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
@@ -171,6 +201,8 @@ export default {
|
||||
contentSelection: { type: Map, required: true },
|
||||
showSelection: { type: Boolean, required: true },
|
||||
totalMatches: { type: Number, required: true },
|
||||
expandedCount: { type: Number, required: false, default: 0 },
|
||||
debug: { type: Boolean, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -178,13 +210,11 @@ export default {
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
// pop it open if anything is selected
|
||||
showContentFilters() {
|
||||
const { showHidden, showDeleted, filterText } = this.params;
|
||||
return this.showFilter || showHidden || showDeleted || filterText;
|
||||
},
|
||||
|
||||
// pass-through
|
||||
localParams: {
|
||||
get() {
|
||||
return this.params;
|
||||
@@ -194,8 +224,12 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
numSelected() {
|
||||
return this.contentSelection.size || 0;
|
||||
},
|
||||
|
||||
hasSelection() {
|
||||
return this.contentSelection.size > 0;
|
||||
return this.numSelected > 0;
|
||||
},
|
||||
|
||||
countHidden() {
|
||||
@@ -203,22 +237,29 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
toggleSelectAll() {
|
||||
if (this.hasSelection) {
|
||||
this.$emit("resetSelection");
|
||||
this.$emit("update:show-selection", false);
|
||||
} else {
|
||||
this.$emit("selectAllContent");
|
||||
this.$emit("update:show-selection", true);
|
||||
}
|
||||
},
|
||||
|
||||
// #region history-wide bulk updates, does server query first to determine "selection"
|
||||
|
||||
async unhideAll(evt) {
|
||||
await unhideAllHiddenContent(this.history);
|
||||
this.$emit("manualReload");
|
||||
evt.vueTarget.hide();
|
||||
},
|
||||
async deleteAllHidden(evt) {
|
||||
await deleteAllHiddenContent(this.history);
|
||||
this.$emit("manualReload");
|
||||
evt.vueTarget.hide();
|
||||
},
|
||||
async purgeAllDeleted(evt) {
|
||||
await purgeAllDeletedContent(this.history);
|
||||
this.$emit("manualReload");
|
||||
evt.vueTarget.hide();
|
||||
},
|
||||
|
||||
// #endregion
|
||||
@@ -243,7 +284,10 @@ export default {
|
||||
async runOnSelection(fn) {
|
||||
const items = Array.from(this.contentSelection.values());
|
||||
const type_ids = items.map((o) => o.type_id);
|
||||
await fn(this.history, type_ids);
|
||||
const results = await fn(this.history, type_ids);
|
||||
if (this.debug) {
|
||||
console.log("operation results", results);
|
||||
}
|
||||
this.$emit("resetSelection");
|
||||
this.$emit("manualReload");
|
||||
},
|
||||
@@ -282,3 +326,10 @@ export default {
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// remove borders around buttons in menu
|
||||
.content-operations-menu .btn-group .btn {
|
||||
border-color: transparent !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<b-modal ref="copyModal" v-model="showModal" :title="title" title-tag="h2">
|
||||
<b-modal v-bind="$attrs" v-on="$listeners" :title="title" title-tag="h2">
|
||||
<transition name="fade">
|
||||
<b-alert :show="currentUser.isAnonymous" variant="warning" v-localize>
|
||||
As an anonymous user, unless you login or register, you will lose your current history after copying
|
||||
@@ -51,7 +51,6 @@ import { mapActions, mapGetters } from "vuex";
|
||||
export default {
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
value: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -81,16 +80,6 @@ export default {
|
||||
formValid() {
|
||||
return this.newNameValid;
|
||||
},
|
||||
showModal: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(newVal, oldVal) {
|
||||
if (newVal !== oldVal) {
|
||||
this.$emit("input", newVal);
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions("betaHistory", ["copyHistory"]),
|
||||
|
||||
@@ -1,45 +1,108 @@
|
||||
<!-- Edits a collection, if that collection is editable. -->
|
||||
|
||||
<template>
|
||||
<section class="history-details controls">
|
||||
<ClickToEdit
|
||||
:value="dsc.name"
|
||||
v-if="writable"
|
||||
@input="(name) => $emit('update:dsc', { name })"
|
||||
tag-name="h3"
|
||||
class="history-title mt-3"
|
||||
:tooltip-title="'Rename collection...' | localize"
|
||||
/>
|
||||
<h3 v-else class="history-title mt-3">{{ dsc.name }}</h3>
|
||||
<section class="history-details d-flex">
|
||||
<div class="flex-grow-1">
|
||||
<h3 class="history-title">{{ dscName || "(Collection Name)" }}</h3>
|
||||
<p class="mt-1">
|
||||
<i class="fas fa-folder"></i>
|
||||
a {{ dsc.collectionType | localize }}
|
||||
{{ dsc.collectionCount | localize }}
|
||||
</p>
|
||||
|
||||
<p class="mt-1">
|
||||
<i class="fas fa-folder"></i>
|
||||
a {{ dsc.collectionType | localize }}
|
||||
{{ dsc.collectionCount | localize }}
|
||||
</p>
|
||||
<div v-if="isEditing" class="mt-3">
|
||||
<b-textarea v-model="dscName" placeholder="Collection Name" trim max-rows="4"></b-textarea>
|
||||
<ContentTags :content="dsc" class="mt-3" />
|
||||
</div>
|
||||
|
||||
<transition name="shutterfade">
|
||||
<ContentTags v-if="writable" :content="dsc" />
|
||||
<div v-else-if="dsc.tags && dsc.tags.length" class="nametags p-1">
|
||||
<div v-else-if="dsc.tags && dsc.tags.length">
|
||||
<Nametag v-for="tag in dsc.tags" :key="tag" :tag="tag" />
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
|
||||
<EditorMenu
|
||||
class="ml-3 flex-grow-0 d-flex flex-column"
|
||||
v-if="writable"
|
||||
model-name="Collection"
|
||||
:editing.sync="editing"
|
||||
:writable="writable"
|
||||
:valid="valid"
|
||||
:dirty="dirty"
|
||||
@save="save"
|
||||
@revert="revert"
|
||||
/>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DatasetCollection } from "../model";
|
||||
import ClickToEdit from "components/ClickToEdit";
|
||||
import { Nametag } from "components/Nametags";
|
||||
import ContentTags from "../ContentTags";
|
||||
import EditorMenu from "../EditorMenu";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ClickToEdit,
|
||||
ContentTags,
|
||||
Nametag,
|
||||
EditorMenu,
|
||||
},
|
||||
props: {
|
||||
dsc: { type: DatasetCollection, required: true },
|
||||
writable: { type: Boolean, required: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
editing: false,
|
||||
tempContent: this.dsc.clone(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
dscId() {
|
||||
return this.dsc.id;
|
||||
},
|
||||
isEditing() {
|
||||
return this.writable && this.editing;
|
||||
},
|
||||
valid() {
|
||||
return this.tempContent.name.length > 0;
|
||||
},
|
||||
dirty() {
|
||||
return !this.tempContent.equals(this.dsc);
|
||||
},
|
||||
dscName: {
|
||||
get() {
|
||||
return this.tempContent.name;
|
||||
},
|
||||
set(newName) {
|
||||
this.patchTempDsc({ name: newName });
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
dscId(newId, oldId) {
|
||||
if (newId != oldId) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reset() {
|
||||
this.tempContent = this.dsc.clone();
|
||||
},
|
||||
patchTempDsc(props = {}) {
|
||||
this.tempContent = this.tempContent.patch(props);
|
||||
},
|
||||
save() {
|
||||
if (this.valid) {
|
||||
this.$emit("update:dsc", this.tempContent);
|
||||
this.editing = false;
|
||||
}
|
||||
},
|
||||
revert() {
|
||||
if (this.dirty) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -9,10 +9,19 @@
|
||||
v-slot="{ isExpanded, setExpanded }"
|
||||
>
|
||||
<Layout class="dataset-collection-panel">
|
||||
<template v-slot:nav>
|
||||
<template v-slot:globalNav>
|
||||
<TopNav :history="history" :selected-collections="selectedCollections" v-on="$listeners" />
|
||||
</template>
|
||||
|
||||
<template v-slot:localNav>
|
||||
<IconButton
|
||||
icon="download"
|
||||
title="Download Collection"
|
||||
:href="downloadCollectionUrl"
|
||||
download
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template v-slot:details>
|
||||
<Details :dsc="dsc" :writable="writable" @update:dsc="updateDsc(dsc, $event)" />
|
||||
</template>
|
||||
@@ -23,13 +32,16 @@
|
||||
:class="{ loadingBackground: loading }"
|
||||
v-bind="payload"
|
||||
@scroll="setScrollPos"
|
||||
:debug="false"
|
||||
>
|
||||
<template v-slot="{ item, index }">
|
||||
<template v-slot="{ item, index, rowKey }">
|
||||
<CollectionContentItem
|
||||
:item="item"
|
||||
:index="index"
|
||||
:row-key="rowKey"
|
||||
:expanded="isExpanded(item)"
|
||||
:writable="writable"
|
||||
:writable="false"
|
||||
:selectable="false"
|
||||
@update:expanded="setExpanded(item, $event)"
|
||||
@viewCollection="$emit('viewCollection', item)"
|
||||
/>
|
||||
@@ -55,6 +67,7 @@ import Scroller from "../Scroller";
|
||||
import { CollectionContentItem } from "../ContentItem";
|
||||
|
||||
import { reportPayload } from "../providers/ContentProvider/helpers";
|
||||
import IconButton from "components/IconButton";
|
||||
|
||||
export default {
|
||||
filters: {
|
||||
@@ -69,6 +82,7 @@ export default {
|
||||
Scroller,
|
||||
CollectionContentItem,
|
||||
ExpandedItems,
|
||||
IconButton,
|
||||
},
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
@@ -86,14 +100,22 @@ export default {
|
||||
writable() {
|
||||
return this.isRoot;
|
||||
},
|
||||
rootCollection() {
|
||||
return this.selectedCollections[0];
|
||||
},
|
||||
downloadCollectionUrl() {
|
||||
let url = "";
|
||||
if (this.rootCollection) {
|
||||
url = `${this.rootCollection.url}/download`;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
// change the data of the root collection, anything past the root
|
||||
// collection is part of the dataset collection, which i believe is supposed to
|
||||
// be immutable, so only edit name, tags, blah of top-level selected collection,
|
||||
|
||||
async updateDsc(collection, fields) {
|
||||
// console.log("updateDsc", this.writable, collection, fields);
|
||||
if (this.writable) {
|
||||
const ajaxResult = await updateContentFields(collection, fields);
|
||||
await cacheContent({ ...collection, ...ajaxResult });
|
||||
|
||||
@@ -1,46 +1,34 @@
|
||||
<template>
|
||||
<div class="navigation">
|
||||
<b-dropdown v-if="breadCrumbOptions.length" class="mr-auto" size="sm" text="Go to..." boundary="viewport">
|
||||
<b-dropdown-item @click="close"> History: {{ history.name }} </b-dropdown-item>
|
||||
<b-dropdown-item v-for="option in breadCrumbOptions" :key="option.key" @click="reselect(option.value)">
|
||||
{{ option.text }}
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
<b-dropdown v-if="breadCrumbOptions.length" size="sm" text="Return to..." boundary="viewport" no-caret>
|
||||
<template v-slot:button-content>
|
||||
<Icon icon="arrow-up" class="mr-1" />
|
||||
<b class="text-nowrap" v-localize>Return to...</b>
|
||||
</template>
|
||||
|
||||
<b-button v-else size="sm" class="mr-auto back" @click="close"> Back to: {{ history.name }} </b-button>
|
||||
<b-dropdown-item @click="close">
|
||||
<span>History: {{ history.name }}</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<PriorityMenu :starting-height="27">
|
||||
<PriorityMenuItem
|
||||
key="download-collection"
|
||||
title="Download Collection"
|
||||
icon="fas fa-file-download"
|
||||
tag="a"
|
||||
download
|
||||
:href="downloadCollectionUrl"
|
||||
/>
|
||||
<PriorityMenuItem key="back-up" title="Back One" icon="fas fa-level-up-alt" @click="back" />
|
||||
</PriorityMenu>
|
||||
</div>
|
||||
<b-dropdown-item v-for="option in breadCrumbOptions" :key="option.key" @click="reselect(option.value)">
|
||||
<span>{{ option.text }}</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
|
||||
<b-button v-else size="sm" title="`Return to: ${history.name}`" @click="close">
|
||||
<Icon icon="arrow-up" class="mr-1" />
|
||||
<b class="text-nowrap">Return to: {{ history.name }}</b>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { History } from "../model";
|
||||
import { PriorityMenuItem, PriorityMenu } from "components/PriorityMenu";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
PriorityMenuItem,
|
||||
PriorityMenu,
|
||||
},
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
selectedCollections: { type: Array, required: true, validate: (val) => val.length > 0 },
|
||||
},
|
||||
computed: {
|
||||
rootCollection() {
|
||||
return this.selectedCollections[0];
|
||||
},
|
||||
|
||||
// List for the dropdown. Should be the history and every parent
|
||||
// collection excepting the current one.
|
||||
breadCrumbOptions() {
|
||||
@@ -54,14 +42,6 @@ export default {
|
||||
});
|
||||
return options;
|
||||
},
|
||||
|
||||
downloadCollectionUrl() {
|
||||
let url = "";
|
||||
if (this.rootCollection) {
|
||||
url = `${this.rootCollection.url}/download`;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reselect(newList) {
|
||||
|
||||
@@ -2,20 +2,17 @@
|
||||
<CurrentUser class="d-flex flex-column" v-slot="{ user }">
|
||||
<UserHistories v-if="user" :user="user" v-slot="{ currentHistory, histories, handlers }">
|
||||
<HistoryPanel v-if="currentHistory" :history="currentHistory" v-on="handlers" id="current-history-panel">
|
||||
<template v-slot:nav>
|
||||
<div>
|
||||
<HistorySelector
|
||||
:histories="histories"
|
||||
:current-history="currentHistory"
|
||||
:show-modal="showModal"
|
||||
@update:currentHistory="handlers.setCurrentHistory"
|
||||
@hideModal="showModalHandler"
|
||||
/>
|
||||
<HistoriesMenu v-on="handlers" @selectHistoryModal="showModalHandler" />
|
||||
</div>
|
||||
<template v-slot:globalNav>
|
||||
<HistoriesMenu
|
||||
v-on="handlers"
|
||||
:histories="histories"
|
||||
:current-history="currentHistory"
|
||||
title="Current History"
|
||||
/>
|
||||
</template>
|
||||
</HistoryPanel>
|
||||
|
||||
<!-- still loading -->
|
||||
<div v-else class="flex-grow-1 loadingBackground h-100">
|
||||
<span class="sr-only" v-localize>Loading History...</span>
|
||||
</div>
|
||||
@@ -27,7 +24,6 @@
|
||||
import CurrentUser from "components/providers/CurrentUser";
|
||||
import UserHistories from "./providers/UserHistories";
|
||||
import HistoryPanel from "./HistoryPanel";
|
||||
import HistorySelector from "./HistorySelector";
|
||||
import HistoriesMenu from "./HistoriesMenu";
|
||||
|
||||
export default {
|
||||
@@ -35,18 +31,12 @@ export default {
|
||||
CurrentUser,
|
||||
UserHistories,
|
||||
HistoryPanel,
|
||||
HistorySelector,
|
||||
HistoriesMenu,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showModal: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
showModalHandler(event) {
|
||||
this.showModal = !this.showModal;
|
||||
},
|
||||
},
|
||||
// data() {
|
||||
// return {
|
||||
// showModal: false,
|
||||
// };
|
||||
// },
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
<template>
|
||||
<nav class="edit-controls">
|
||||
<IconButton
|
||||
:title="label('Edit') | l"
|
||||
class="edit-button mb-1"
|
||||
:pressed="editing"
|
||||
icon="pen"
|
||||
@click="$emit('update:editing', !editing)"
|
||||
/>
|
||||
|
||||
<template v-if="editing">
|
||||
<!-- save -->
|
||||
<IconButton
|
||||
:title="label('Save') | l"
|
||||
class="save-button mb-1"
|
||||
:class="{ disabled: !valid }"
|
||||
icon="save"
|
||||
@click="$emit('save')"
|
||||
/>
|
||||
|
||||
<!-- revert -->
|
||||
<IconButton
|
||||
:title="label('Revert') | l"
|
||||
class="revert-changes-button mb-1"
|
||||
:class="{ disabled: !dirty }"
|
||||
icon="undo"
|
||||
@click="$emit('revert')"
|
||||
/>
|
||||
|
||||
<!-- form status -->
|
||||
<IconButton
|
||||
:title="statusLabel | l"
|
||||
class="status-button mb-1"
|
||||
:class="{ valid: valid, disabled: !valid }"
|
||||
:icon="valid ? 'check' : 'exclamation-triangle'"
|
||||
variant="link"
|
||||
/>
|
||||
</template>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import IconButton from "components/IconButton";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
IconButton,
|
||||
},
|
||||
props: {
|
||||
modelName: { type: String, required: true },
|
||||
editing: { type: Boolean, required: true },
|
||||
dirty: { type: Boolean, required: false, default: false },
|
||||
valid: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
computed: {
|
||||
statusLabel() {
|
||||
const status = this.valid ? "Valid" : "Invalid";
|
||||
return `${status} ${this.modelName}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
label(verb) {
|
||||
return `${verb} ${this.modelName}`;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.edit-controls .disabled {
|
||||
opacity: 0.25;
|
||||
}
|
||||
</style>
|
||||
@@ -1,66 +1,83 @@
|
||||
<!-- menu allowing user to change the current history, make a new one, basically anything that's
|
||||
"above" editing the current history -->
|
||||
|
||||
<template>
|
||||
<PriorityMenu :starting-height="27">
|
||||
<PriorityMenuItem
|
||||
key="select-new-history"
|
||||
title="Select History"
|
||||
icon="fa fa-folder-open"
|
||||
@click="$emit('selectHistoryModal')"
|
||||
<div>
|
||||
<b-dropdown
|
||||
size="sm"
|
||||
variant="link"
|
||||
:text="title | l"
|
||||
toggle-class="text-decoration-none"
|
||||
no-caret
|
||||
class="histories-operation-menu"
|
||||
data-description="histories operation menu"
|
||||
>
|
||||
<template v-slot:button-content>
|
||||
<Icon class="mr-1" icon="folder" />
|
||||
<span id="click-me-fool" class="text-nowrap">{{ title | l }}</span>
|
||||
</template>
|
||||
|
||||
<b-dropdown-text>
|
||||
<span>You have {{ histories.length }} histories.</span>
|
||||
</b-dropdown-text>
|
||||
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
|
||||
<b-dropdown-item v-b-modal.history-selector-modal>
|
||||
<Icon class="mr-1" icon="exchange-alt" />
|
||||
<span v-localize>Change the Current History</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item @click="$emit('createNewHistory')">
|
||||
<Icon class="mr-1" icon="plus" />
|
||||
<span v-localize>Create a New History</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item @click="backboneRoute('/histories/list')">
|
||||
<Icon class="mr-1" icon="list" />
|
||||
<span v-localize>View Saved Histories</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item @click="redirect('/history/view_multiple')">
|
||||
<Icon class="mr-1" icon="columns" />
|
||||
<span v-localize>Show Histories Side-by-Side</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
|
||||
<b-dropdown-item @click="switchToLegacyHistoryPanel">
|
||||
<Icon class="mr-1" icon="arrow-up" />
|
||||
<span v-localize>Return to legacy panel</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
|
||||
<!-- modals -->
|
||||
<HistorySelectorModal
|
||||
id="history-selector-modal"
|
||||
:histories="histories"
|
||||
:current-history="currentHistory"
|
||||
@selectHistory="$emit('setCurrentHistory', $event)"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="create-new-history"
|
||||
title="Create New History"
|
||||
class="history-new-button"
|
||||
icon="fa fa-plus"
|
||||
@click="$emit('createNewHistory')"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="view-all-histories"
|
||||
title="View All Histories"
|
||||
icon="fa fa-columns"
|
||||
class="history-view-multi-button"
|
||||
@click="redirect('/history/view_multiple')"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="saved-histories"
|
||||
title="Saved Histories"
|
||||
icon="fas fa-save"
|
||||
@click="backboneRoute('/histories/list')"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="clear-history-cache"
|
||||
title="Refresh History"
|
||||
icon="fa fa-refresh"
|
||||
class="history-refresh-button"
|
||||
@click.stop="clearCache"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="use-legacy-history"
|
||||
title="Return to legacy history panel"
|
||||
icon="fas fa-exchange-alt"
|
||||
@click="switchToLegacyHistoryPanel"
|
||||
/>
|
||||
</PriorityMenu>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { PriorityMenuItem, PriorityMenu } from "components/PriorityMenu";
|
||||
import { wipeDatabase, clearHistoryDateStore } from "./caching";
|
||||
import { History } from "./model";
|
||||
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
|
||||
import { switchToLegacyHistoryPanel } from "./adapters/betaToggle";
|
||||
import { reloadPage } from "utils/redirect";
|
||||
import HistorySelectorModal from "./HistorySelectorModal";
|
||||
|
||||
export default {
|
||||
mixins: [legacyNavigationMixin],
|
||||
components: {
|
||||
PriorityMenu,
|
||||
PriorityMenuItem,
|
||||
HistorySelectorModal,
|
||||
},
|
||||
props: {
|
||||
histories: { type: Array, required: true },
|
||||
currentHistory: { type: History, required: true },
|
||||
title: { type: String, required: false, default: "Histories" },
|
||||
},
|
||||
methods: {
|
||||
async clearCache() {
|
||||
await wipeDatabase();
|
||||
await clearHistoryDateStore();
|
||||
reloadPage();
|
||||
},
|
||||
switchToLegacyHistoryPanel,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
<ExpandedItems
|
||||
:scope-key="history.id"
|
||||
:get-item-key="(item) => item.type_id"
|
||||
v-slot="{ isExpanded, setExpanded }"
|
||||
v-slot="{ expandedCount, isExpanded, setExpanded, collapseAll }"
|
||||
>
|
||||
<SelectedItems
|
||||
:scope-key="history.id"
|
||||
@@ -26,16 +26,20 @@
|
||||
}"
|
||||
>
|
||||
<Layout>
|
||||
<template v-slot:nav>
|
||||
<slot name="nav"></slot>
|
||||
<template v-slot:globalNav>
|
||||
<slot name="globalNav" :history="history"></slot>
|
||||
</template>
|
||||
|
||||
<template v-slot:localNav>
|
||||
<HistoryMenu :history="history" v-on="$listeners" />
|
||||
</template>
|
||||
|
||||
<template v-slot:details>
|
||||
<HistoryDetails class="history-details" :history="history" v-on="$listeners" />
|
||||
<HistoryDetails :history="history" v-on="$listeners" />
|
||||
</template>
|
||||
|
||||
<template v-slot:messages>
|
||||
<HistoryMessages class="history-messages m-2" :history="history" />
|
||||
<HistoryMessages class="m-2" :history="history" />
|
||||
</template>
|
||||
|
||||
<template v-slot:listcontrols>
|
||||
@@ -51,6 +55,8 @@
|
||||
@resetSelection="resetSelection"
|
||||
@selectAllContent="selectItems(payload.contents)"
|
||||
@manualReload="manualReload"
|
||||
:expanded-count="expandedCount"
|
||||
@collapseAllContent="collapseAll"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -65,10 +71,11 @@
|
||||
:debug="false"
|
||||
@scroll="setScrollPos"
|
||||
>
|
||||
<template v-slot="{ item, index }">
|
||||
<template v-slot="{ item, index, rowKey }">
|
||||
<HistoryContentItem
|
||||
:item="item"
|
||||
:index="index"
|
||||
:row-key="rowKey"
|
||||
:show-selection="showSelection"
|
||||
:expanded="isExpanded(item)"
|
||||
@update:expanded="setExpanded(item, $event)"
|
||||
@@ -101,6 +108,7 @@ import ToolHelpModal from "./ToolHelpModal";
|
||||
import Scroller from "./Scroller";
|
||||
import { HistoryContentItem } from "./ContentItem";
|
||||
import { reportPayload } from "./providers/ContentProvider/helpers";
|
||||
import HistoryMenu from "./HistoryMenu";
|
||||
|
||||
export default {
|
||||
filters: {
|
||||
@@ -118,6 +126,7 @@ export default {
|
||||
HistoryContentItem,
|
||||
ExpandedItems,
|
||||
SelectedItems,
|
||||
HistoryMenu,
|
||||
},
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
|
||||
@@ -1,215 +1,145 @@
|
||||
<template>
|
||||
<header>
|
||||
<div class="d-flex justify-content-between align-items-center actions">
|
||||
<h6 class="text-nowrap text-truncate mr-auto">
|
||||
<span class="size history-size">{{ history.niceSize | localize }}</span>
|
||||
</h6>
|
||||
<!--
|
||||
History object editor. Change name, edit tags, description.
|
||||
Emit new History model object when done via .sync syntax
|
||||
-->
|
||||
|
||||
<PriorityMenu style="max-width: 50%" :starting-height="28">
|
||||
<PriorityMenuItem
|
||||
key="edit-history-tags"
|
||||
title="Edit History Tags"
|
||||
class="history-tag-btn"
|
||||
icon="fas fa-tags"
|
||||
:pressed="showTags"
|
||||
@click="showTags = !showTags"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="edit-history-annotation"
|
||||
title="Edit Annotation"
|
||||
class="history-annotate-btn"
|
||||
icon="fas fa-comment"
|
||||
:pressed="showAnnotation"
|
||||
@click="showAnnotation = !showAnnotation"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="copy-history"
|
||||
title="Copy History"
|
||||
icon="fas fa-copy"
|
||||
@click="showCopyModal = !showCopyModal"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="share"
|
||||
title="Share or Publish"
|
||||
icon="fas fa-share-alt"
|
||||
@click="backboneRoute('/histories/sharing', { id: history.id })"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="delete-this-history"
|
||||
title="Delete this History"
|
||||
icon="fas fa-trash"
|
||||
@click="showDeleteModal = !showDeleteModal"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="purge-this-history"
|
||||
title="Purge this History"
|
||||
icon="fas fa-burn"
|
||||
@click="showPurgeModal = !showPurgeModal"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="structure"
|
||||
title="Show Structure"
|
||||
icon="fas fa-code-branch"
|
||||
@click="backboneRoute('/histories/show_structure')"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="extract-workflow"
|
||||
title="Extract Workflow"
|
||||
icon="fas fa-file-export"
|
||||
@click="iframeRedirect('/workflow/build_from_current_history')"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="set-permissions"
|
||||
title="Set Permissions"
|
||||
icon="fas fa-user-lock"
|
||||
@click="backboneRoute('/histories/permissions', { id: history.id })"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="make-private"
|
||||
title="Make Private"
|
||||
icon="fas fa-lock"
|
||||
@click="showPrivacyModal = !showPrivacyModal"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="resume-paused-jobs"
|
||||
title="Resume Paused Jobs"
|
||||
icon="fas fa-play"
|
||||
@click="iframeRedirect('/history/resume_paused_jobs?current=True')"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="export-citations"
|
||||
title="Export Tool Citations"
|
||||
icon="fas fa-stream"
|
||||
@click="backboneRoute('/histories/citations', { id: history.id })"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="export-history-to-file"
|
||||
title="Export History to File"
|
||||
icon="fas fa-file-archive"
|
||||
@click="backboneRoute(exportLink)"
|
||||
/>
|
||||
</PriorityMenu>
|
||||
<template>
|
||||
<section class="history-details d-flex">
|
||||
<div class="flex-grow-1">
|
||||
<h3>{{ historyName || "(History Name)" }}</h3>
|
||||
<h5 class="history-size">{{ history.size | niceFileSize }}</h5>
|
||||
|
||||
<!-- display title, annotation, tags -->
|
||||
<div v-if="!editing">
|
||||
<p v-if="annotation" class="mt-1">{{ annotation }}</p>
|
||||
<HistoryTags class="mt-2" :history="history" :disabled="true" />
|
||||
</div>
|
||||
|
||||
<!-- edit form, change title, annotation, or tags -->
|
||||
<div v-else class="mt-3">
|
||||
<b-textarea
|
||||
class="mb-2"
|
||||
v-model="historyName"
|
||||
placeholder="History Name"
|
||||
trim
|
||||
max-rows="4"
|
||||
></b-textarea>
|
||||
|
||||
<b-textarea
|
||||
class="mb-2"
|
||||
v-model="annotation"
|
||||
placeholder="Annotation (optional)"
|
||||
trim
|
||||
max-rows="4"
|
||||
></b-textarea>
|
||||
|
||||
<HistoryTags class="mt-3" :history="history" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- click to edit the name of the history -->
|
||||
<ClickToEdit
|
||||
class="history-title mt-2"
|
||||
tag-name="h3"
|
||||
v-model="historyName"
|
||||
:title="'Rename history...' | localize"
|
||||
v-b-tooltip.hover="{ boundary: 'viewport', placement: 'left' }"
|
||||
<EditorMenu
|
||||
class="ml-3 flex-grow-0 d-flex flex-column"
|
||||
v-if="writable"
|
||||
model-name="History"
|
||||
:editing.sync="editing"
|
||||
:writable="writable"
|
||||
:valid="valid"
|
||||
:dirty="dirty"
|
||||
@save="save"
|
||||
@revert="revert"
|
||||
/>
|
||||
|
||||
<transition name="shutterfade">
|
||||
<Annotation v-if="showAnnotation" class="history-annotation mt-2" v-model="annotation" />
|
||||
</transition>
|
||||
|
||||
<transition name="shutterfade">
|
||||
<HistoryTags v-if="showTags" class="mt-2" :history="history" />
|
||||
</transition>
|
||||
|
||||
<!-- #region modals -->
|
||||
|
||||
<CopyHistoryModal v-model="showCopyModal" :history="history" />
|
||||
|
||||
<b-modal v-model="showPrivacyModal" title="Make History Private" title-tag="h2" @ok="makePrivate">
|
||||
<p v-localize>
|
||||
This will make all the data in this history private (excluding library datasets), and will set
|
||||
permissions such that all new data is created as private. Any datasets within that are currently shared
|
||||
will need to be re-shared or published. Are you sure you want to do this?
|
||||
</p>
|
||||
</b-modal>
|
||||
|
||||
<b-modal v-model="showDeleteModal" title="Delete History?" title-tag="h2" @ok="deleteHistory">
|
||||
<p v-localize>Really delete the current history?</p>
|
||||
</b-modal>
|
||||
|
||||
<b-modal v-model="showPurgeModal" title="Permanently Delete History?" title-tag="h2" @ok="purgeHistory">
|
||||
<p v-localize>Really delete the current history permanently? This cannot be undone.</p>
|
||||
</b-modal>
|
||||
|
||||
<!-- #endregion -->
|
||||
</header>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import prettyBytes from "pretty-bytes";
|
||||
import { History } from "./model";
|
||||
import { PriorityMenuItem, PriorityMenu } from "components/PriorityMenu";
|
||||
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
|
||||
import ClickToEdit from "components/ClickToEdit";
|
||||
import Annotation from "components/Annotation";
|
||||
import HistoryTags from "./HistoryTags";
|
||||
import CopyHistoryModal from "./CopyModal";
|
||||
import EditorMenu from "./EditorMenu";
|
||||
|
||||
export default {
|
||||
mixins: [legacyNavigationMixin],
|
||||
components: {
|
||||
PriorityMenu,
|
||||
PriorityMenuItem,
|
||||
HistoryTags,
|
||||
Annotation,
|
||||
ClickToEdit,
|
||||
CopyHistoryModal,
|
||||
EditorMenu,
|
||||
},
|
||||
filters: {
|
||||
niceFileSize(rawSize = 0) {
|
||||
return prettyBytes(rawSize);
|
||||
},
|
||||
},
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
writable: { type: Boolean, required: false, default: true },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
showTags: false,
|
||||
showAnnotation: false,
|
||||
showCopyModal: false,
|
||||
showPrivacyModal: false,
|
||||
showDeleteModal: false,
|
||||
showPurgeModal: false,
|
||||
editing: false,
|
||||
tempHistory: this.history.clone(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
historyId() {
|
||||
return this.history.id;
|
||||
},
|
||||
isEditing() {
|
||||
return this.writable && this.editing;
|
||||
},
|
||||
valid() {
|
||||
return this.tempHistory.name.length > 0;
|
||||
},
|
||||
dirty() {
|
||||
return !this.tempHistory.equals(this.history);
|
||||
},
|
||||
annotation: {
|
||||
get() {
|
||||
return this.history.annotation || "";
|
||||
return this.tempHistory.annotation;
|
||||
},
|
||||
set(annotation) {
|
||||
if (annotation.length && annotation !== this.history.annotation) {
|
||||
this.updateFields({ annotation });
|
||||
}
|
||||
this.patchTempHistory({ annotation });
|
||||
},
|
||||
},
|
||||
|
||||
historyName: {
|
||||
get() {
|
||||
return this.history?.name || "";
|
||||
return this.tempHistory.name;
|
||||
},
|
||||
set(name) {
|
||||
if (name.length && name !== this.history.name) {
|
||||
this.updateFields({ name });
|
||||
}
|
||||
this.patchTempHistory({ name });
|
||||
},
|
||||
},
|
||||
exportLink() {
|
||||
return `histories/${this.history.id}/export`;
|
||||
tags: {
|
||||
get() {
|
||||
return this.tempHistory.tags || [];
|
||||
},
|
||||
set(newTags) {
|
||||
this.patchTempHistory({ tags: newTags });
|
||||
},
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
historyId(newId, oldId) {
|
||||
if (newId != oldId) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async updateFields(fields = {}) {
|
||||
this.$emit("updateHistory", { id: this.history.id, ...fields });
|
||||
reset() {
|
||||
this.tempHistory = this.history.clone();
|
||||
},
|
||||
async deleteHistory(evt) {
|
||||
this.$emit("deleteHistory", this.history);
|
||||
evt.vueTarget.hide();
|
||||
patchTempHistory(props = {}) {
|
||||
this.tempHistory = this.tempHistory.patch(props);
|
||||
},
|
||||
async purgeHistory(evt) {
|
||||
this.$emit("purgeHistory", this.history);
|
||||
evt.vueTarget.hide();
|
||||
save() {
|
||||
if (this.valid) {
|
||||
this.$emit("update:history", this.tempHistory);
|
||||
this.editing = false;
|
||||
}
|
||||
},
|
||||
async makePrivate(evt) {
|
||||
this.$emit("secureHistory", this.history);
|
||||
evt.vueTarget.hide();
|
||||
revert() {
|
||||
if (this.dirty) {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "scss/transitions.scss";
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-dropdown size="sm" variant="link" text="Histories" toggle-class="text-decoration-none">
|
||||
<template v-slot:button-content>
|
||||
<Icon icon="wrench" />
|
||||
<span class="sr-only">Operations on the current history</span>
|
||||
</template>
|
||||
|
||||
<b-dropdown-item v-b-modal:copy-history-modal>
|
||||
<Icon icon="copy" class="mr-1" />
|
||||
<span v-localize>Copy this History</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item v-b-modal:delete-history-modal>
|
||||
<Icon icon="trash" class="mr-1" />
|
||||
<span v-localize>Delete this History</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item v-b-modal:purge-history-modal>
|
||||
<Icon icon="burn" class="mr-1" />
|
||||
<span v-localize>Purge this History</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
|
||||
<b-dropdown-item @click="iframeRedirect('/history/resume_paused_jobs?current=True')">
|
||||
<Icon icon="play" class="mr-1" />
|
||||
<span v-localize>Resume Paused Jobs</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item @click="iframeRedirect('/workflow/build_from_current_history')">
|
||||
<Icon icon="file-export" class="mr-1" />
|
||||
<span v-localize>Extract Workflow</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item @click="backboneRoute('/histories/show_structure')">
|
||||
<Icon icon="code-branch" class="mr-1" />
|
||||
<span v-localize>Show Structure</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
|
||||
<b-dropdown-item @click="backboneRoute('/histories/sharing', { id: history.id })">
|
||||
<Icon icon="share-alt" class="mr-1" />
|
||||
<span v-localize>Share or Publish</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item @click="backboneRoute('/histories/permissions', { id: history.id })">
|
||||
<Icon icon="user-lock" class="mr-1" />
|
||||
<span v-localize>Set Permissions</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item v-b-modal:history-privacy-modal>
|
||||
<Icon icon="lock" class="mr-1" />
|
||||
<span v-localize>Make Private</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-divider></b-dropdown-divider>
|
||||
|
||||
<b-dropdown-item @click="backboneRoute('/histories/citations', { id: history.id })">
|
||||
<Icon icon="stream" class="mr-1" />
|
||||
<span v-localize>Export Tool Citations</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item @click="backboneRoute(history.exportLink)">
|
||||
<Icon icon="file-archive" class="mr-1" />
|
||||
<span v-localize>Export History to File</span>
|
||||
</b-dropdown-item>
|
||||
</b-dropdown>
|
||||
|
||||
<!-- #region modals -->
|
||||
|
||||
<CopyHistoryModal id="copy-history-modal" :history="history" />
|
||||
|
||||
<b-modal
|
||||
id="history-privacy-modal"
|
||||
title="Make History Private"
|
||||
title-tag="h2"
|
||||
@ok="$emit('secureHistory', history)"
|
||||
>
|
||||
<p v-localize>
|
||||
This will make all the data in this history private (excluding library datasets), and will set
|
||||
permissions such that all new data is created as private. Any datasets within that are currently shared
|
||||
will need to be re-shared or published. Are you sure you want to do this?
|
||||
</p>
|
||||
</b-modal>
|
||||
|
||||
<b-modal id="delete-history-modal" title="Delete History?" title-tag="h2" @ok="$emit('deleteHistory', history)">
|
||||
<p v-localize>Really delete the current history?</p>
|
||||
</b-modal>
|
||||
|
||||
<b-modal
|
||||
id="purge-history-modal"
|
||||
title="Permanently Delete History?"
|
||||
title-tag="h2"
|
||||
@ok="$emit('purgeHistory', history)"
|
||||
>
|
||||
<p v-localize>Really delete the current history permanently? This cannot be undone.</p>
|
||||
</b-modal>
|
||||
|
||||
<!-- #endregion -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { History } from "./model";
|
||||
import CopyHistoryModal from "./CopyModal";
|
||||
import { legacyNavigationMixin } from "components/plugins/legacyNavigation";
|
||||
|
||||
export default {
|
||||
mixins: [legacyNavigationMixin],
|
||||
components: {
|
||||
CopyHistoryModal,
|
||||
},
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,106 @@
|
||||
<template>
|
||||
<b-modal ref="modal" v-on="$listeners" v-bind="$attrs" :title="'Switch to History' | l">
|
||||
<b-form-group :description="'Filter histories' | l">
|
||||
<b-input v-model="filter" :placeholder="'Search Filter' | l" />
|
||||
</b-form-group>
|
||||
|
||||
<b-table
|
||||
ref="history-list"
|
||||
striped
|
||||
hover
|
||||
sticky-header="50vh"
|
||||
v-model="currentRows"
|
||||
primary-key="id"
|
||||
:fields="fields"
|
||||
:filter="filter"
|
||||
:items="histories"
|
||||
:per-page="perPage"
|
||||
:current-page="currentPage"
|
||||
:selectable="true"
|
||||
select-mode="single"
|
||||
selected-variant="success"
|
||||
@row-selected="switchToHistory"
|
||||
>
|
||||
<template v-slot:cell(tags)="row">
|
||||
<stateless-tags :value="row.item.tags" :disabled="true" />
|
||||
</template>
|
||||
<template v-slot:cell(update_time)="data">
|
||||
<UtcDate :date="data.value" mode="elapsed" />
|
||||
</template>
|
||||
</b-table>
|
||||
|
||||
<template v-slot:modal-footer>
|
||||
<b-pagination v-model="currentPage" :total-rows="filteredRowCount" :per-page="perPage"></b-pagination>
|
||||
</template>
|
||||
</b-modal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { StatelessTags } from "components/Tags";
|
||||
import UtcDate from "components/UtcDate";
|
||||
import { debounce } from "underscore";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StatelessTags,
|
||||
UtcDate,
|
||||
},
|
||||
props: {
|
||||
currentHistory: { type: Object, required: true },
|
||||
histories: { type: Array, default: () => [] },
|
||||
perPage: { type: Number, required: false, default: 50 },
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
filter: null,
|
||||
currentPage: 1,
|
||||
currentRows: [],
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
filteredRowCount() {
|
||||
return this.currentRows.length;
|
||||
},
|
||||
selectedIndex() {
|
||||
return this.currentRows.findIndex((h) => h.id == this.currentHistory.id);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.fields = [
|
||||
{ key: "name", sortable: true },
|
||||
{ key: "tags", sortable: true },
|
||||
{ key: "update_time", label: "Updated", sortable: true },
|
||||
];
|
||||
this.debounceSelectCurrentRow = debounce(this.selectCurrentRow, 100);
|
||||
},
|
||||
watch: {
|
||||
currentRows() {
|
||||
this.selectCurrentRow();
|
||||
},
|
||||
filteredRowCount(newVal, oldVal) {
|
||||
if (newVal != oldVal) {
|
||||
this.currentPage = 1;
|
||||
}
|
||||
},
|
||||
selectedIndex(idx, oldIdx) {
|
||||
if (idx != oldIdx) {
|
||||
this.debounceSelectCurrentRow();
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
switchToHistory(selected) {
|
||||
if (selected.length == 1) {
|
||||
this.$emit("selectHistory", selected[0]);
|
||||
}
|
||||
},
|
||||
selectCurrentRow() {
|
||||
const idx = this.selectedIndex;
|
||||
const list = this.$refs["history-list"];
|
||||
if (list && idx > -1) {
|
||||
list.selectRow(idx);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<Tags :tags="history.tags" :store-key="storeKey" :tag-service="historyTagService" />
|
||||
<Tags :tags="history.tags" :store-key="storeKey" :tag-service="historyTagService" :disabled="disabled" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
@@ -48,6 +48,7 @@ export default {
|
||||
components: { Tags },
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
disabled: { type: Boolean, required: false, default: false },
|
||||
},
|
||||
computed: {
|
||||
storeKey() {
|
||||
|
||||
@@ -4,14 +4,17 @@ amateur-hour baby-tool like Bootstrap instead of taking the afternoon
|
||||
to learn how CSS is supposed to work. -->
|
||||
|
||||
<template>
|
||||
<section class="history d-flex flex-column">
|
||||
<header class="p-2">
|
||||
<!-- optional slot for injecting global navigation -->
|
||||
<nav v-if="$slots.nav" class="top-nav pb-2">
|
||||
<slot name="nav"></slot>
|
||||
<section class="history beta d-flex flex-column">
|
||||
<header>
|
||||
<nav class="d-flex justify-content-between bg-secondary">
|
||||
<slot name="globalNav">Global Nav Default</slot>
|
||||
<slot name="localNav">Local nav Default</slot>
|
||||
</nav>
|
||||
|
||||
<!-- details about the thing you're looking at -->
|
||||
<slot name="details"></slot>
|
||||
<div class="px-3 py-4 details">
|
||||
<slot name="details"></slot>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- warnings, messages, dirty limericks -->
|
||||
@@ -20,12 +23,12 @@ to learn how CSS is supposed to work. -->
|
||||
</section>
|
||||
|
||||
<!-- controls atop the list -->
|
||||
<section v-if="$slots.listcontrols" class="p-2">
|
||||
<section v-if="$slots.listcontrols" class="list-controls">
|
||||
<slot name="listcontrols"></slot>
|
||||
</section>
|
||||
|
||||
<!-- scrolling list -->
|
||||
<section class="scroller flex-grow-1">
|
||||
<section class="position-relative flex-grow-1 scroller">
|
||||
<slot name="listing"></slot>
|
||||
</section>
|
||||
|
||||
@@ -37,13 +40,8 @@ to learn how CSS is supposed to work. -->
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
@import "scss/mixins.scss";
|
||||
@import "scss/transitions.scss";
|
||||
@import "scss/loadingBackground.scss";
|
||||
|
||||
/* TODO: css reset? */
|
||||
|
||||
.history {
|
||||
.history.beta {
|
||||
/* TODO: css reset? */
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
@@ -52,31 +50,14 @@ to learn how CSS is supposed to work. -->
|
||||
h6 {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
container css for first child of top-nav,
|
||||
flex horizontally
|
||||
.list-controls {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
Vue can't put in multi-root slot contents so we have to do some kung-fu to get
|
||||
the layout right on the top-nav. There will have to be an extra div container
|
||||
around the contents of the nav slot, so we'll style that right here so the user
|
||||
doesn't need to do it manually every time. */
|
||||
|
||||
.history > header > .top-nav > * {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.history .messages:empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* make sure scroller actually scrolls */
|
||||
|
||||
.history .scroller {
|
||||
position: relative;
|
||||
.list-controls:empty,
|
||||
.messages:empty {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -3,9 +3,14 @@
|
||||
<!-- mousewheel moves up and down linearly, allows for fine-tuning of view by moving just a
|
||||
few rows at a time -->
|
||||
<div ref="listing" class="listing">
|
||||
<ul>
|
||||
<li v-for="(item, index) in itemWindow" :key="getItemKey(item, index)">
|
||||
<slot :item="item" :index="index" :row-key="getItemKey(item, index)">
|
||||
<ul class="list-unstyled m-0">
|
||||
<li
|
||||
v-for="(item, index) in itemWindow"
|
||||
:key="getItemKey(item)"
|
||||
:data-row-index="index"
|
||||
:data-row-key="getItemKey(item)"
|
||||
>
|
||||
<slot :item="item" :index="index" :row-key="getItemKey(item)">
|
||||
<pre>{{ item }}</pre>
|
||||
</slot>
|
||||
</li>
|
||||
@@ -15,6 +20,7 @@
|
||||
<!-- big blank scroll region uses scroll bar to drag view to entirely new region, calculated
|
||||
by visual height of the scroller knob --->
|
||||
<div
|
||||
v-if="overlayActive"
|
||||
v-show="showScroller"
|
||||
ref="scrollSliderContainer"
|
||||
class="scrollSliderContainer"
|
||||
@@ -23,6 +29,12 @@
|
||||
>
|
||||
<div ref="scrollSlider"></div>
|
||||
</div>
|
||||
|
||||
<!-- debugging overlay -->
|
||||
<div v-if="debug" class="debugOverlay">
|
||||
<pre>{{ $attrs }}</pre>
|
||||
<pre>{{ debugProps }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -75,10 +87,26 @@ export default {
|
||||
|
||||
// width of scroll bar container
|
||||
sbWidth: 30,
|
||||
|
||||
// whether to activate the 2nd scrollbar which does a fast-seek on large histories
|
||||
overlayActive: true,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
debugProps() {
|
||||
return {
|
||||
manualStartIndex: this.manualStartIndex,
|
||||
keyField: this.keyField,
|
||||
contentsLength: this.contents.length,
|
||||
startKeyIndex: this.startKeyIndex,
|
||||
totalMatches: this.totalMatches,
|
||||
topRows: this.topRows,
|
||||
bottomRows: this.bottomRows,
|
||||
pageSize: this.pageSize,
|
||||
};
|
||||
},
|
||||
|
||||
// The index to start rendering content at, can be manually adjusted with
|
||||
// the mouse wheel, and must be updated when new contents come in
|
||||
itemStartIndex() {
|
||||
@@ -144,7 +172,7 @@ export default {
|
||||
// non-reactive data
|
||||
this.suppressEvents = false;
|
||||
this.suppressionTimeout = null;
|
||||
this.debouncedUpdateCursor = debounce(this.updateCursor, 100);
|
||||
// this.debouncedUpdateCursor = debounce(this.updateCursor, 100);
|
||||
this.debouncedAdjustScrollTop = debounce(this.adjustScrollTop, 100);
|
||||
},
|
||||
|
||||
@@ -193,7 +221,8 @@ export default {
|
||||
// value which we send out on scrollPos
|
||||
|
||||
onScroll() {
|
||||
this.debouncedUpdateCursor();
|
||||
// this.debouncedUpdateCursor();
|
||||
this.updateCursor();
|
||||
},
|
||||
|
||||
// determines cursor from scrollTop, need to wait until after render to calculate
|
||||
@@ -290,22 +319,15 @@ export default {
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "scss/mixins.scss";
|
||||
|
||||
.scrollContainer,
|
||||
.listing,
|
||||
.scrollSliderContainer {
|
||||
@include absfill();
|
||||
}
|
||||
|
||||
.scrollContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
|
||||
.listing,
|
||||
.scrollSliderContainer {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.listing {
|
||||
overflow-y: scroll;
|
||||
z-index: 0;
|
||||
@@ -326,14 +348,14 @@ export default {
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
ul,
|
||||
li {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
.debugOverlay {
|
||||
background: rgba(255, 255, 255, 0.75);
|
||||
z-index: 2;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,82 +1,55 @@
|
||||
<template>
|
||||
<StateBtn
|
||||
v-if="inState(STATES.RUNNING)"
|
||||
:state="state"
|
||||
icon="upload-symbol upload-icon fa fa-spinner fa-spin"
|
||||
<IconButton
|
||||
v-if="stateIcon"
|
||||
v-b-tooltip
|
||||
:title="theTitle"
|
||||
:icon="stateIcon"
|
||||
:variant="variant"
|
||||
v-on="$listeners"
|
||||
v-bind="$attrs"
|
||||
/>
|
||||
|
||||
<StateBtn v-else-if="inState(STATES.QUEUED, STATES.NEW)" :state="state" icon="fas fa-clock" v-on="$listeners" />
|
||||
|
||||
<StateBtn v-else-if="inState(STATES.PAUSED)" :state="state" icon="fas fa-pause" v-on="$listeners" />
|
||||
|
||||
<StateBtn v-else-if="inState(STATES.ERROR)" :state="state" icon="fas fa-exclamation-triangle" v-on="$listeners" />
|
||||
|
||||
<StateBtn v-else-if="inState(STATES.MIXED)" :state="state" icon="fas fa-exclamation-circle" v-on="$listeners" />
|
||||
|
||||
<span v-else class="sr-only">
|
||||
{{ state | localize }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import capitalize from "underscore.string/capitalize";
|
||||
import IconButton from "components/IconButton";
|
||||
|
||||
export const StateBtn = {
|
||||
template: `
|
||||
<b-button class="state-button"
|
||||
v-b-tooltip
|
||||
:title="theTitle | localize"
|
||||
size="sm"
|
||||
variant="link"
|
||||
v-on="$listeners"
|
||||
v-bind="$attrs">
|
||||
<i :class="icon" />
|
||||
<span class="sr-only">{{ theTitle | localize }}</span>
|
||||
</b-button>
|
||||
`,
|
||||
export default {
|
||||
inject: ["STATES"],
|
||||
components: {
|
||||
IconButton,
|
||||
},
|
||||
props: {
|
||||
state: { type: String, required: true },
|
||||
icon: { type: String, required: true },
|
||||
title: { type: String, required: false, default: "" },
|
||||
variant: { type: String, required: false, default: "link" },
|
||||
},
|
||||
inject: ["STATES"],
|
||||
computed: {
|
||||
stateIcon() {
|
||||
if (this.inState(this.STATES.QUEUED, this.STATES.NEW)) {
|
||||
return "clock";
|
||||
}
|
||||
if (this.inState(this.STATES.PAUSED)) {
|
||||
return "pause";
|
||||
}
|
||||
if (this.inState(this.STATES.ERROR)) {
|
||||
return "exclamation-triangle";
|
||||
}
|
||||
if (this.inState(this.STATES.MIXED)) {
|
||||
return "exclamation-circle";
|
||||
}
|
||||
if (this.inState(this.STATES.OK)) {
|
||||
return "check";
|
||||
}
|
||||
return null;
|
||||
},
|
||||
theTitle() {
|
||||
return this.title || capitalize(this.state);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const StatusIcon = {
|
||||
components: {
|
||||
StateBtn,
|
||||
},
|
||||
props: {
|
||||
state: { type: String, required: true },
|
||||
},
|
||||
inject: ["STATES"],
|
||||
methods: {
|
||||
inState(...states) {
|
||||
return states.includes(this.state);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default StatusIcon;
|
||||
</script>
|
||||
|
||||
<style lang="css">
|
||||
/* Override ham-fisted base css rules */
|
||||
|
||||
button.state-button,
|
||||
button.state-button.disabled,
|
||||
button.state-button:disabled,
|
||||
button.state-button:focus,
|
||||
button.state-button:hover,
|
||||
button.state-button:not(:disabled):active {
|
||||
background-color: transparent !important;
|
||||
border-style: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -10,13 +10,14 @@ import { bulkCacheDscContent } from "./db";
|
||||
import { SearchParams } from "../model/SearchParams";
|
||||
import { prependPath } from "./workerConfig";
|
||||
import { summarizeCacheOperation, dateStore } from "./loadHistoryContents";
|
||||
import { show } from "utils/observable";
|
||||
|
||||
/**
|
||||
* Load collection content (drill down)
|
||||
* Params: contents_url + search params + element_index
|
||||
*/
|
||||
export const loadDscContent = (cfg = {}) => {
|
||||
// const { onceEvery = 10 * 1000 } = cfg;
|
||||
const { debug = false } = cfg;
|
||||
|
||||
return publish((inputs$) => {
|
||||
const url$ = inputs$.pipe(nth(0));
|
||||
@@ -24,6 +25,7 @@ export const loadDscContent = (cfg = {}) => {
|
||||
return inputs$.pipe(
|
||||
map(buildDscContentUrl),
|
||||
map(prependPath),
|
||||
show(debug, (url) => console.log("Sending collection request:", url)),
|
||||
requestWithUpdateTime({ dateStore }),
|
||||
pluck("response"),
|
||||
withLatestFrom(url$),
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
import { pipe } from "rxjs";
|
||||
import { map } from "rxjs/operators";
|
||||
|
||||
/**
|
||||
* Bust incoming numeric source values into blocks of designated size.
|
||||
*
|
||||
* @param {*} chunkSize
|
||||
* @param {*} ceil
|
||||
*/
|
||||
// prettier-ignore
|
||||
export const chunk = (chunkSize, ceil = false) => pipe(
|
||||
map((chunkMe) => {
|
||||
const rawVal = chunkMe / chunkSize;
|
||||
return chunkSize * (ceil ? Math.ceil(rawVal) : Math.floor(rawVal));
|
||||
})
|
||||
);
|
||||
|
||||
/**
|
||||
* Change one parameter to be an multiple of indicated block size. Used to
|
||||
* regulate the URLs we send to the server so that some will be cached. Pos is
|
||||
* the position in the combined inputs array, and the chunkSize is the size of
|
||||
* the block to break the value into.
|
||||
*
|
||||
* Ex: Source Inputs [ x, y, z, 750 ],
|
||||
* chunkParam(3, 200)
|
||||
* Results in [ x, y, z, 600]
|
||||
*
|
||||
* @param {integer} pos Input array parameter number to chunk
|
||||
* @param {integer} chunkSize Size of chunks
|
||||
* @param {Boolean} ceil Math.ceil or Math.floor chunk value
|
||||
*/
|
||||
// prettier-ignore
|
||||
export const chunkParam = (pos, chunkSize, ceil = false) => pipe(
|
||||
map((inputs) => {
|
||||
const chunkMe = inputs[pos];
|
||||
const rawVal = 1.0 * chunkMe / chunkSize;
|
||||
const chunkedVal = chunkSize * (ceil ? Math.ceil(rawVal) : Math.floor(rawVal));
|
||||
const newInputs = inputs.slice();
|
||||
newInputs[pos] = chunkedVal;
|
||||
return newInputs;
|
||||
})
|
||||
);
|
||||
@@ -5,6 +5,7 @@
|
||||
*/
|
||||
|
||||
import { Content } from "./Content";
|
||||
import { scrubModelProps } from "utils/safeAssign";
|
||||
|
||||
export class Dataset extends Content {
|
||||
getUrl(urlType) {
|
||||
@@ -40,4 +41,28 @@ export class Dataset extends Content {
|
||||
const { display_apps = [], display_types = [] } = this;
|
||||
return [...display_apps, ...display_types];
|
||||
}
|
||||
|
||||
clone() {
|
||||
const newProps = cleanDatasetProps(this);
|
||||
return new Dataset(newProps);
|
||||
}
|
||||
|
||||
patch(newProps) {
|
||||
const cleanProps = cleanDatasetProps({ ...this, ...newProps });
|
||||
return new Dataset(cleanProps);
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
return Dataset.equals(this, other);
|
||||
}
|
||||
}
|
||||
|
||||
Dataset.equals = function (a, b) {
|
||||
return JSON.stringify(a) == JSON.stringify(b);
|
||||
};
|
||||
|
||||
const scrubber = scrubModelProps(Dataset);
|
||||
export const cleanDatasetProps = (props = {}) => {
|
||||
const cleanProps = JSON.parse(JSON.stringify(props));
|
||||
return scrubber(cleanProps);
|
||||
};
|
||||
|
||||
@@ -7,11 +7,11 @@
|
||||
|
||||
import { Content } from "./Content";
|
||||
import { JobStateSummary } from "./JobStateSummary";
|
||||
import { scrubModelProps } from "utils/safeAssign";
|
||||
|
||||
export class DatasetCollection extends Content {
|
||||
loadProps(raw = {}) {
|
||||
if (!raw.contents_url) {
|
||||
// console.log("ouch", raw);
|
||||
throw new Error("missing contents_url", raw);
|
||||
}
|
||||
super.loadProps(raw);
|
||||
@@ -64,8 +64,28 @@ export class DatasetCollection extends Content {
|
||||
get jobSummary() {
|
||||
return new JobStateSummary(this);
|
||||
}
|
||||
|
||||
clone() {
|
||||
const newProps = cleanDscProps(this);
|
||||
return new DatasetCollection(newProps);
|
||||
}
|
||||
|
||||
patch(newProps) {
|
||||
const cleanProps = cleanDscProps({ ...this, ...newProps });
|
||||
return new DatasetCollection(cleanProps);
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
return DatasetCollection.equals(this, other);
|
||||
}
|
||||
}
|
||||
|
||||
DatasetCollection.equals = function (a, b) {
|
||||
return JSON.stringify(a) == JSON.stringify(b);
|
||||
};
|
||||
|
||||
const scrubber = scrubModelProps(DatasetCollection);
|
||||
export const cleanDscProps = (props = {}) => {
|
||||
const cleanProps = JSON.parse(JSON.stringify(props));
|
||||
return scrubber(cleanProps);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { dateMixin, ModelBase } from "./ModelBase";
|
||||
import { scrubModelProps } from "utils/safeAssign";
|
||||
|
||||
export class History extends dateMixin(ModelBase) {
|
||||
// not deleted
|
||||
@@ -36,8 +37,32 @@ export class History extends dateMixin(ModelBase) {
|
||||
}
|
||||
return status.join(", ");
|
||||
}
|
||||
|
||||
get exportLink() {
|
||||
return `histories/${this.id}/export`;
|
||||
}
|
||||
|
||||
clone() {
|
||||
const newProps = cleanHistoryProps(this);
|
||||
return new History(newProps);
|
||||
}
|
||||
|
||||
patch(newProps) {
|
||||
const cleanProps = cleanHistoryProps({ ...this, ...newProps });
|
||||
return new History(cleanProps);
|
||||
}
|
||||
|
||||
equals(other) {
|
||||
return History.equals(this, other);
|
||||
}
|
||||
}
|
||||
|
||||
History.equals = function (a, b) {
|
||||
return JSON.stringify(a) == JSON.stringify(b);
|
||||
};
|
||||
|
||||
const scrubber = scrubModelProps(History);
|
||||
export const cleanHistoryProps = (props = {}) => {
|
||||
const cleanProps = JSON.parse(JSON.stringify(props));
|
||||
return scrubber(cleanProps);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { History, cleanHistoryProps } from "./History";
|
||||
|
||||
describe("History model", () => {
|
||||
const initialProps = {
|
||||
id: "abc123",
|
||||
name: "Foo",
|
||||
tags: ["a", "b", "c"],
|
||||
annotation: "I am a test history",
|
||||
};
|
||||
|
||||
const h = new History(initialProps);
|
||||
|
||||
describe("clone", () => {
|
||||
const c = h.clone();
|
||||
|
||||
it("must be able to clone itself for use in editors", () => {
|
||||
// should be == but not ===
|
||||
expect(h).not.toBe(c);
|
||||
expect(c).toBeInstanceOf(History);
|
||||
expect(History.equals(h, c)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should have the same props before and after cloning", () => {
|
||||
expect(h.id).toEqual(c.id);
|
||||
expect(h.name).toEqual(c.name);
|
||||
expect(h.tags).toEqual(c.tags);
|
||||
expect(h.annotation).toEqual(c.annotation);
|
||||
});
|
||||
});
|
||||
|
||||
describe("cleanHistoryProps", () => {
|
||||
it("should ignore computed prop names during cloning", () => {
|
||||
const sloppyProps = {
|
||||
...initialProps,
|
||||
// these are all computed values in the history class which are defined by getters,
|
||||
// if we feed thsm into the constructor they should be ignored
|
||||
statusDescription: "123",
|
||||
totalItems: Number.POSITIVE_INFINITY,
|
||||
hidItems: "four",
|
||||
active: "The Fonz",
|
||||
};
|
||||
|
||||
const cleanProps = cleanHistoryProps(sloppyProps);
|
||||
expect(cleanProps).toEqual(initialProps);
|
||||
expect(cleanProps).not.toBe(initialProps);
|
||||
|
||||
expect(cleanProps).not.toHaveProperty("statusDescription");
|
||||
expect(cleanProps).not.toHaveProperty("totalItems");
|
||||
expect(cleanProps).not.toHaveProperty("hidItems");
|
||||
expect(cleanProps).not.toHaveProperty("active");
|
||||
|
||||
const h = new History(cleanProps);
|
||||
expect(h.statusDescription).not.toEqual(sloppyProps.statusDescription);
|
||||
});
|
||||
});
|
||||
|
||||
describe("patch", () => {
|
||||
const patchVals = {
|
||||
name: "i was patched",
|
||||
annotation: "patched annotation",
|
||||
tags: ["foo", "bar", "blech"],
|
||||
};
|
||||
|
||||
const clone = h.patch(patchVals);
|
||||
|
||||
it("should return a new instance of History", () => {
|
||||
expect(h).not.toBe(clone);
|
||||
expect(clone).toBeInstanceOf(History);
|
||||
expect(History.equals(h, clone)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("should patch in passed properties", () => {
|
||||
expect(clone.name).toEqual(patchVals.name);
|
||||
expect(clone.annotation).toEqual(patchVals.annotation);
|
||||
expect(clone.tags).toEqual(patchVals.tags);
|
||||
});
|
||||
});
|
||||
|
||||
describe("equivalence comparison", () => {
|
||||
it("should recognize different props", () => {
|
||||
const hDifferent = new History({ id: "def345" });
|
||||
expect(History.equals(h, hDifferent)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("equivalence should work fine with cloned histories", () => {
|
||||
const clone = h.clone();
|
||||
// different object
|
||||
expect(h).not.toBe(clone);
|
||||
// but equivalent because props the same
|
||||
expect(History.equals(h, clone)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("should work with patch", () => {
|
||||
const clone = h.patch({ id: "floobar" });
|
||||
// different object
|
||||
expect(h).not.toBe(clone);
|
||||
// not equivalent because different val
|
||||
expect(History.equals(h, clone)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,9 @@ import { bulkContentUpdate, getAllContentByFilter } from "./queries";
|
||||
export const updateSelectedContent = (updates) => async (history, type_ids) => {
|
||||
// single ajax call with wierd syntax because.... reasons
|
||||
const changes = await bulkContentUpdate(history, type_ids, updates);
|
||||
await bulkCacheContent(changes);
|
||||
const cacheResult = await bulkCacheContent(changes, true);
|
||||
console.log("cacheResult", changes, cacheResult);
|
||||
return cacheResult;
|
||||
};
|
||||
|
||||
export const hideSelectedContent = updateSelectedContent({
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
import axios from "axios";
|
||||
import moment from "moment";
|
||||
import { prependPath } from "utils/redirect";
|
||||
import { History } from "./History";
|
||||
|
||||
// #region setup & utils
|
||||
|
||||
@@ -87,9 +88,10 @@ const stdHistoryParams = {
|
||||
* Return list of available histories
|
||||
*/
|
||||
export async function getHistoryList() {
|
||||
const params = { view: "summary" };
|
||||
const params = { view: "summary", keys: "size" };
|
||||
const response = await api.get("/histories", { params });
|
||||
return doResponse(response);
|
||||
const rawList = doResponse(response);
|
||||
return rawList.map((props) => new History(props));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,7 +103,8 @@ export async function getHistoryById(id, since) {
|
||||
const sinceParam = since !== undefined ? moment.utc(since).toISOString() : null;
|
||||
const url = sinceParam ? `${path}?q=update_time-gt&qv=${sinceParam}` : path;
|
||||
const response = await api.get(url, { params: stdHistoryParams });
|
||||
return doResponse(response);
|
||||
const props = doResponse(response);
|
||||
return new History(props);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,7 +124,8 @@ export async function createNewHistory(props = {}) {
|
||||
if (!id) {
|
||||
throw new Error("failed to create and select new history");
|
||||
}
|
||||
return doResponse(createResponse);
|
||||
const newHistoryProps = doResponse(createResponse);
|
||||
return new History(newHistoryProps);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,7 +143,8 @@ export async function cloneHistory(history, name, copyAll) {
|
||||
current: true,
|
||||
};
|
||||
const response = await api.post(url, payload, { params: stdHistoryParams });
|
||||
return doResponse(response);
|
||||
const clonedProps = doResponse(response);
|
||||
return new History(clonedProps);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -171,7 +176,8 @@ export async function undeleteHistoryById(id) {
|
||||
export async function updateHistoryFields(id, payload) {
|
||||
const url = `/histories/${id}`;
|
||||
const response = await api.put(url, payload, { params: stdHistoryParams });
|
||||
return doResponse(response);
|
||||
const props = doResponse(response);
|
||||
return new History(props);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -202,7 +208,8 @@ export async function getCurrentHistoryFromServer() {
|
||||
const response = await api.get(url, {
|
||||
baseURL: prependPath("/"), // old api doesn't use api path
|
||||
});
|
||||
return doResponse(response);
|
||||
const props = doResponse(response);
|
||||
return new History(props);
|
||||
}
|
||||
|
||||
export async function setCurrentHistoryOnServer(history_id) {
|
||||
|
||||
+5
-1
@@ -1,10 +1,14 @@
|
||||
import { DatasetCollection } from "../../model";
|
||||
import { SearchParams, DatasetCollection } from "../../model";
|
||||
import { ContentProvider, processContentStreams } from "../ContentProvider";
|
||||
import { collectionPayload } from "./collectionPayload";
|
||||
|
||||
export default {
|
||||
mixins: [ContentProvider],
|
||||
|
||||
props: {
|
||||
params: { type: SearchParams, default: () => new SearchParams({ showHidden: false }) },
|
||||
},
|
||||
|
||||
computed: {
|
||||
dsc() {
|
||||
if (this.parent instanceof DatasetCollection) {
|
||||
|
||||
+5
-2
@@ -1,5 +1,5 @@
|
||||
import { of, Observable, Subject, partition, merge } from "rxjs";
|
||||
import { tap, map, pluck, switchMap, publish, distinctUntilChanged, share } from "rxjs/operators";
|
||||
import { tap, map, pluck, switchMap, publish, distinctUntilChanged, share, throttleTime } from "rxjs/operators";
|
||||
import { chunk } from "utils/observable";
|
||||
import { SearchParams } from "../../model/SearchParams";
|
||||
import { loadDscContent } from "../../caching";
|
||||
@@ -13,6 +13,8 @@ export const collectionPayload = (cfg = {}) => {
|
||||
pageSize = SearchParams.pageSize,
|
||||
debug = false,
|
||||
loadingEvents$ = new Subject(),
|
||||
debouncePeriod = 100,
|
||||
chunkSize = pageSize
|
||||
} = cfg;
|
||||
|
||||
const { totalElements: totalMatches, contents_url } = dsc;
|
||||
@@ -34,7 +36,7 @@ export const collectionPayload = (cfg = {}) => {
|
||||
|
||||
// chunk the input to the server request so we don't request for every single mouse move
|
||||
const chunked$ = elementIndex$.pipe(
|
||||
chunk(pageSize, false),
|
||||
chunk(chunkSize),
|
||||
distinctUntilChanged(),
|
||||
);
|
||||
|
||||
@@ -48,6 +50,7 @@ export const collectionPayload = (cfg = {}) => {
|
||||
);
|
||||
|
||||
const serverLoad$ = pagination$.pipe(
|
||||
throttleTime(debouncePeriod, undefined, { trailing: true }),
|
||||
switchMap(pagination => of([contents_url, filters, pagination]).pipe(
|
||||
tap(() => loadingEvents$.next(true)),
|
||||
loadDscContent({ debug }),
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
timeoutWith,
|
||||
ignoreElements,
|
||||
} from "rxjs/operators";
|
||||
import { chunk, show, debounceBurst, shareButDie } from "utils/observable";
|
||||
import { show, debounceBurst, shareButDie } from "utils/observable";
|
||||
import { SearchParams } from "../../model/SearchParams";
|
||||
import { processContentUpdate, newUpdateMap, buildContentResult, getKeyForUpdateMap } from "./aggregation";
|
||||
import { SEEK } from "../../caching/enums";
|
||||
@@ -41,7 +41,7 @@ export const aggregateCacheUpdates = (monitor, cfg = {}) => (src$) => {
|
||||
// interval in which we create new monitors
|
||||
// if we chunk the inputs then we won't make as many
|
||||
// might be easier on memory
|
||||
inputChunk = Math.floor(pageSize / 2),
|
||||
// inputChunk = Math.floor(pageSize / 2),
|
||||
|
||||
// reduces duplicate updates to the skiplist
|
||||
// which can be expensive
|
||||
@@ -59,7 +59,7 @@ export const aggregateCacheUpdates = (monitor, cfg = {}) => (src$) => {
|
||||
// avoids creating a new monitor for every single key emission
|
||||
// chunk to ceiling if descending, floor if ascending
|
||||
const monitorInputKey$ = targetKey$.pipe(
|
||||
chunk(inputChunk, keyDirection == SEEK.DESC),
|
||||
// chunk(inputChunk, keyDirection == SEEK.DESC, "monitorInputKey"),
|
||||
distinctUntilChanged(),
|
||||
show(debug, (key) => console.log("monitorInputKey", key)),
|
||||
);
|
||||
@@ -76,10 +76,10 @@ export const aggregateCacheUpdates = (monitor, cfg = {}) => (src$) => {
|
||||
// when we're just going to change it again shortly
|
||||
const groupedUpdates$ = cacheUpdates$.pipe(
|
||||
groupBy(
|
||||
update => update.key,
|
||||
update => update,
|
||||
update => update.key,
|
||||
update => update,
|
||||
updateByKey$ => updateByKey$.pipe(
|
||||
timeoutWith(updateGroupLifetime, EMPTY),
|
||||
timeoutWith(updateGroupLifetime, EMPTY),
|
||||
ignoreElements()
|
||||
)
|
||||
),
|
||||
@@ -104,7 +104,7 @@ export const aggregateCacheUpdates = (monitor, cfg = {}) => (src$) => {
|
||||
withLatestFrom(targetKey$),
|
||||
);
|
||||
|
||||
// query skiplist with scroll position (transformed into a key from the skiplist)
|
||||
// query skiplist with scroll position (transformed into a key from the skiplist)
|
||||
// to produce list of nearby content
|
||||
const contentList$ = contentListInputs$.pipe(
|
||||
map(buildContentResult({ pageSize, keyDirection, getKey })),
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
/**
|
||||
* General plumbing for the 2 types of content providers. This funcction emits 3 observables:
|
||||
* General plumbing for the 2 types of content providers. This function returns 3 observables:
|
||||
* loading, scrolling, and payload. Payload's the most important and the operator that genrates the
|
||||
* payload from the inputs is passed in as a parameter to this factory.
|
||||
*/
|
||||
import { debounceTime, distinctUntilChanged, share, startWith, switchMap } from "rxjs/operators";
|
||||
import { distinctUntilChanged, share, startWith, switchMap } from "rxjs/operators";
|
||||
import { activity, whenAny, show } from "utils/observable";
|
||||
import { propMatch } from "./helpers";
|
||||
import { SearchParams, ScrollPos } from "../../model";
|
||||
@@ -23,7 +23,7 @@ import { Subject } from "rxjs";
|
||||
*/
|
||||
// prettier-ignore
|
||||
export function processContentStreams(payloadOperator, sources = {}, settings = {}) {
|
||||
const { debouncePeriod, debug = false } = settings;
|
||||
const { debouncePeriod, debug = true } = settings;
|
||||
|
||||
// clean incoming source streams
|
||||
const parent$ = sources.parent$.pipe(
|
||||
@@ -35,7 +35,7 @@ export function processContentStreams(payloadOperator, sources = {}, settings =
|
||||
show(debug, (params) => console.log('processContentStreams: params changed', params)),
|
||||
);
|
||||
const pos$ = sources.scrollPos$.pipe(
|
||||
debounceTime(debouncePeriod),
|
||||
// debounceTime(debouncePeriod),
|
||||
show(debug, (pos) => console.log('processContentStreams: pos changed', pos)),
|
||||
distinctUntilChanged(ScrollPos.equals),
|
||||
);
|
||||
@@ -48,7 +48,7 @@ export function processContentStreams(payloadOperator, sources = {}, settings =
|
||||
activity(debouncePeriod),
|
||||
startWith(true)
|
||||
);
|
||||
|
||||
|
||||
const resetPos$ = new Subject();
|
||||
|
||||
// The actual loader, when history or params change we poll against the api and
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { saveSet, loadSet } from "./setCache";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
scopeKey: { type: String, required: true },
|
||||
@@ -8,6 +10,11 @@ export default {
|
||||
items: new Set(),
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
expandedCount() {
|
||||
return this.items.size;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isExpanded(item) {
|
||||
const key = this.getItemKey(item);
|
||||
@@ -29,11 +36,23 @@ export default {
|
||||
this.reset();
|
||||
}
|
||||
},
|
||||
items(newSet) {
|
||||
saveSet(this.key, newSet);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.key = "expanded-history-items";
|
||||
const cachedSet = loadSet(this.key);
|
||||
if (cachedSet) {
|
||||
this.items = cachedSet;
|
||||
}
|
||||
},
|
||||
render() {
|
||||
return this.$scopedSlots.default({
|
||||
isExpanded: this.isExpanded,
|
||||
setExpanded: this.setExpanded,
|
||||
collapseAll: this.reset,
|
||||
expandedCount: this.expandedCount,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -34,10 +34,10 @@ import { defaultPayload } from "../ContentProvider";
|
||||
*/
|
||||
// prettier-ignore
|
||||
export const contentPayload = (cfg = {}) => {
|
||||
const {
|
||||
parent: history,
|
||||
const {
|
||||
parent: history,
|
||||
filters = new SearchParams(),
|
||||
pageSize = SearchParams.pageSize,
|
||||
pageSize = SearchParams.pageSize,
|
||||
debouncePeriod = 250,
|
||||
loadingTimeout = 2000,
|
||||
disablePoll = false,
|
||||
@@ -45,6 +45,7 @@ export const contentPayload = (cfg = {}) => {
|
||||
// optional feedback indicators
|
||||
loadingEvents$ = new Subject(),
|
||||
resetPos$ = new Subject(),
|
||||
chunkSize = pageSize
|
||||
} = cfg;
|
||||
|
||||
// These running totals are shared between instances of content payload because a lot of the
|
||||
@@ -78,14 +79,15 @@ export const contentPayload = (cfg = {}) => {
|
||||
filter(hid => !isNaN(hid)),
|
||||
share(),
|
||||
);
|
||||
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region server loading
|
||||
|
||||
const serverHid$ = hid$.pipe(
|
||||
chunk(pageSize, true),
|
||||
chunk({ chunkSize, debug, label: "serverHid" }),
|
||||
distinctUntilChanged(),
|
||||
debounceTime(debouncePeriod),
|
||||
show(debug, hid => console.log("serverHid", hid)),
|
||||
share(),
|
||||
);
|
||||
@@ -109,7 +111,7 @@ export const contentPayload = (cfg = {}) => {
|
||||
mergeAll(),
|
||||
debounceTime(100),
|
||||
);
|
||||
|
||||
|
||||
const cacheMonitor$ = cacheHid$.pipe(
|
||||
watchHistoryContents({ history, filters, pageSize, debouncePeriod, debug }),
|
||||
show(debug, (result) => console.log("cacheMonitor.contents (hid)", result.contents.map(o => o.hid))),
|
||||
@@ -133,7 +135,7 @@ export const contentPayload = (cfg = {}) => {
|
||||
// #endregion
|
||||
|
||||
// #region Reposition View to accomodate recent updates
|
||||
|
||||
|
||||
// When new updates to the history come in, we should shift the scrollPos so that we can see
|
||||
// them. Without some kind of explicit move, the system will think they're just random
|
||||
// updates that are happening outside the region of interest, and there would be no
|
||||
@@ -151,7 +153,7 @@ export const contentPayload = (cfg = {}) => {
|
||||
const fudge = 2;
|
||||
const scrollNearLastTop = Math.abs(hid - lastResponse.maxContentHid) < fudge;
|
||||
const scrollerAtTop = scrollerInDefault || scrollNearLastTop;
|
||||
|
||||
|
||||
// console.group("should i reposition?");
|
||||
// console.log("updatesAtTop", updatesAtTop);
|
||||
// console.log("scrollerAtTop", scrollerAtTop);
|
||||
@@ -190,7 +192,7 @@ export const contentPayload = (cfg = {}) => {
|
||||
const sub = payload$.subscribe(obs);
|
||||
|
||||
// Tie stat feedback subscriptions to main payload sub
|
||||
// Make sure not to combineLatest these subject accumulators or we'll
|
||||
// Make sure not to combineLatest these subject accumulators or we'll
|
||||
// end up with an infinite loop
|
||||
sub.add(newCursorToHid$.subscribe(cursorToHid$));
|
||||
sub.add(newHidToTopRows$.subscribe(hidToTopRows$));
|
||||
|
||||
@@ -43,6 +43,10 @@ export default {
|
||||
"loadUserHistories",
|
||||
"secureHistory",
|
||||
]),
|
||||
|
||||
updateCurrentHistory(updates) {
|
||||
this.updateHistory({ ...updates, id: this.currentHistory.id });
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
// when user changes reload histories
|
||||
@@ -81,7 +85,11 @@ export default {
|
||||
// save new history params should be an object with an id property and any additional
|
||||
// properties that are to be updated on the server. A full history object is not required
|
||||
updateHistory: this.updateHistory,
|
||||
updateCurrentHistory: () => this.updateHistory(this.currentHistory),
|
||||
updateCurrentHistory: this.updateCurrentHistory,
|
||||
|
||||
// also conform to .sync event name format
|
||||
"update:history": this.updateHistory,
|
||||
"update:currentHistory": this.updateCurrentHistory,
|
||||
|
||||
// delete history then clear currentHistoryId
|
||||
deleteHistory: (history) => this.deleteHistory({ history }),
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Caches expanded/selected Sets in sessionStorage
|
||||
// Putting this in separate file for ease of mocking
|
||||
|
||||
export function saveSet(key, setObject) {
|
||||
try {
|
||||
const setToArray = Array.from(setObject);
|
||||
const json = JSON.stringify(setToArray);
|
||||
sessionStorage.setItem(key, json);
|
||||
} catch (err) {
|
||||
console.log("error saving stuff to cache", err);
|
||||
sessionStorage.removeItem(key);
|
||||
}
|
||||
}
|
||||
|
||||
export function loadSet(key) {
|
||||
try {
|
||||
const arrJson = sessionStorage.getItem(key) || [];
|
||||
const cachedArray = JSON.parse(arrJson);
|
||||
return Array.isArray(cachedArray) ? new Set(cachedArray) : new Set();
|
||||
} catch (err) {
|
||||
console.log("erorr loading cached list", err);
|
||||
sessionStorage.removeItem(key);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<template>
|
||||
<b-button
|
||||
class="galaxy-icon-button"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
:title="title"
|
||||
:size="size"
|
||||
:pressed="pressed"
|
||||
:disabled="disabled"
|
||||
:variant="localVariant"
|
||||
>
|
||||
<Icon :icon="icon" />
|
||||
<span class="sr-only">{{ title | l }}</span>
|
||||
</b-button>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
icon: { type: [String, Array, Object], required: true },
|
||||
title: { type: String, required: true },
|
||||
size: { type: String, required: false, default: "sm" },
|
||||
pressed: { type: Boolean, required: false, default: false },
|
||||
disabled: { type: Boolean, required: false, default: false },
|
||||
// overrides variant set by pressed/disabled
|
||||
variant: { type: String, required: false, default: null },
|
||||
},
|
||||
computed: {
|
||||
localVariant() {
|
||||
if (this.variant) {
|
||||
// override with prop
|
||||
return this.variant;
|
||||
}
|
||||
if (this.disabled) {
|
||||
return "secondary";
|
||||
}
|
||||
return this.pressed ? "info" : "secondary";
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
// removes irritating bootstrap outline
|
||||
.galaxy-icon-button.btn {
|
||||
box-shadow: none !important;
|
||||
&.disabled > svg {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -59,7 +59,10 @@
|
||||
<font-awesome-icon v-if="row.item.shared" v-b-tooltip.hover title="Shared" icon="share-alt" />
|
||||
</template>
|
||||
<template v-slot:cell(show_in_tool_panel)="row">
|
||||
<b-form-checkbox v-model="row.item.show_in_tool_panel" @change="bookmarkWorkflow(row.item)" />
|
||||
<b-form-checkbox
|
||||
:checked="row.item.show_in_tool_panel"
|
||||
@change="(checked) => bookmarkWorkflow(row.item, checked)"
|
||||
/>
|
||||
</template>
|
||||
<template v-slot:cell(update_time)="data">
|
||||
<UtcDate :date="data.value" mode="elapsed" />
|
||||
@@ -192,14 +195,13 @@ export default {
|
||||
executeWorkflow: function (workflow) {
|
||||
window.location = `${this.root}workflows/run?id=${workflow.id}`;
|
||||
},
|
||||
bookmarkWorkflow: function (workflow) {
|
||||
bookmarkWorkflow: function (workflow, checked) {
|
||||
// This reloads the whole page, so that the workflow appears in the tool panel.
|
||||
// Ideally we would notify only the tool panel of a change
|
||||
const id = workflow.id;
|
||||
const show_in_tool_panel = workflow.show_in_tool_panel;
|
||||
const tags = workflow.tags;
|
||||
const data = {
|
||||
show_in_tool_panel: !show_in_tool_panel,
|
||||
show_in_tool_panel: checked,
|
||||
tags: tags,
|
||||
};
|
||||
this.services
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import {
|
||||
faArrowUp,
|
||||
faBolt,
|
||||
faBurn,
|
||||
faChartArea,
|
||||
faCheckSquare,
|
||||
faClock,
|
||||
faCodeBranch,
|
||||
faColumns,
|
||||
faCompress,
|
||||
faCopy,
|
||||
faDownload,
|
||||
faEllipsisH,
|
||||
faEllipsisV,
|
||||
faExchangeAlt,
|
||||
faEye,
|
||||
faFileArchive,
|
||||
faFileExport,
|
||||
faFilter,
|
||||
faFolder,
|
||||
faInfoCircle,
|
||||
faList,
|
||||
faLink,
|
||||
faLock,
|
||||
faPause,
|
||||
faPen,
|
||||
faPlay,
|
||||
faPlus,
|
||||
faQuestion,
|
||||
faShareAlt,
|
||||
faStream,
|
||||
faTags,
|
||||
faTrash,
|
||||
faTrashRestore,
|
||||
faExclamationTriangle,
|
||||
faUndo,
|
||||
faUserLock,
|
||||
faWrench,
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(
|
||||
faArrowUp,
|
||||
faBolt,
|
||||
faBurn,
|
||||
faChartArea,
|
||||
faCheckSquare,
|
||||
faClock,
|
||||
faCodeBranch,
|
||||
faColumns,
|
||||
faCompress,
|
||||
faCopy,
|
||||
faDownload,
|
||||
faEllipsisH,
|
||||
faEllipsisV,
|
||||
faExchangeAlt,
|
||||
faEye,
|
||||
faFileArchive,
|
||||
faFileExport,
|
||||
faFilter,
|
||||
faFolder,
|
||||
faInfoCircle,
|
||||
faLink,
|
||||
faList,
|
||||
faLock,
|
||||
faPause,
|
||||
faPen,
|
||||
faPlay,
|
||||
faPlus,
|
||||
faQuestion,
|
||||
faShareAlt,
|
||||
faStream,
|
||||
faTags,
|
||||
faTrash,
|
||||
faTrashRestore,
|
||||
faExclamationTriangle,
|
||||
faUndo,
|
||||
faUserLock,
|
||||
faWrench
|
||||
);
|
||||
|
||||
export const iconMixin = {
|
||||
components: {
|
||||
Icon: FontAwesomeIcon,
|
||||
},
|
||||
};
|
||||
|
||||
export const iconPlugin = {
|
||||
install(Vue) {
|
||||
Vue.mixin(iconMixin);
|
||||
},
|
||||
};
|
||||
@@ -1,6 +1,7 @@
|
||||
export { eventHubMixin, eventHubPlugin } from "./eventHub";
|
||||
export { vueRxShortcutPlugin, vueRxShortcuts } from "./vueRxShortcuts";
|
||||
export { localizationPlugin } from "./localization";
|
||||
export { iconPlugin, iconMixin } from "./icons";
|
||||
|
||||
// I'm explicitly ommitting this because of its ugly dependencies, I want us to have to import the
|
||||
// legacy nav features manually (when required) rather than it coming in every time somebody accesses
|
||||
|
||||
@@ -6,7 +6,7 @@ import Vue from "vue";
|
||||
import Vuex from "vuex";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import store from "../store";
|
||||
import { eventHubPlugin, localizationPlugin, vueRxShortcutPlugin } from "components/plugins";
|
||||
import { eventHubPlugin, localizationPlugin, vueRxShortcutPlugin, iconPlugin } from "components/plugins";
|
||||
|
||||
Vue.use(Vuex);
|
||||
|
||||
@@ -24,6 +24,9 @@ Vue.use(localizationPlugin);
|
||||
// rxjs utilities
|
||||
Vue.use(vueRxShortcutPlugin);
|
||||
|
||||
// font-awesome svg icon registration/loading
|
||||
Vue.use(iconPlugin);
|
||||
|
||||
export const mountVueComponent = (ComponentDefinition) => {
|
||||
const component = Vue.extend(ComponentDefinition);
|
||||
return (propsData, el) => new component({ store, propsData, el });
|
||||
|
||||
@@ -3,14 +3,24 @@ import { map } from "rxjs/operators";
|
||||
/**
|
||||
* Bust incoming numeric source values into blocks of designated size.
|
||||
*
|
||||
* @param {*} chunkSize
|
||||
* @param {Number|Object} cfg object or numeric chunksize
|
||||
* @param {*} ceil
|
||||
*/
|
||||
// prettier-ignore
|
||||
export const chunk = (chunkSize, ceil = false) => {
|
||||
export const chunk = (cfg) => {
|
||||
const settings = Number.isInteger(cfg) ? { chunkSize: Math.round(cfg) } : cfg;
|
||||
const { chunkSize = 0, ceil = false, debug = false, label } = settings;
|
||||
if (chunkSize == 0) {
|
||||
throw new Error("Please provide a chunk size");
|
||||
}
|
||||
|
||||
return map((chunkMe) => {
|
||||
const rawVal = chunkMe / chunkSize;
|
||||
return chunkSize * (ceil ? Math.ceil(rawVal) : Math.floor(rawVal));
|
||||
const rawVal = 1.0 * chunkMe / chunkSize;
|
||||
const result = chunkSize * (ceil ? Math.ceil(rawVal) : Math.floor(rawVal));
|
||||
if (debug) {
|
||||
console.log(`chunk: ${label}`, chunkMe, result, settings);
|
||||
}
|
||||
return result;
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
@@ -9,3 +9,22 @@ export function safeAssign(target, source = {}) {
|
||||
.filter((prop) => Object.prototype.hasOwnProperty.call(target, prop))
|
||||
.forEach((prop) => (target[prop] = source[prop]));
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a reusable utility scrubber function that removes reserved initialization props from raw
|
||||
* which are already defined by Klass. This lets us just stick one model instance into.
|
||||
*
|
||||
* Ex: const scrubber = scrubModelProps(History); *
|
||||
* const newHistory = new History(scrubber(oldHistory));
|
||||
*/
|
||||
|
||||
export function scrubModelProps(Klass) {
|
||||
const restrictedKeys = new Set(Object.getOwnPropertyNames(Klass.prototype));
|
||||
|
||||
return (raw = {}) => {
|
||||
const pairs = Object.entries(raw || {});
|
||||
return pairs.reduce((clean, [propName, val]) => {
|
||||
return restrictedKeys.has(propName) ? clean : { ...clean, [propName]: val };
|
||||
}, {});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { createLocalVue, shallowMount } from "@vue/test-utils";
|
||||
import { localizationPlugin } from "components/plugins/localization";
|
||||
import { vueRxShortcutPlugin } from "components/plugins/vueRxShortcuts";
|
||||
import { eventHubPlugin } from "components/plugins/eventHub";
|
||||
import { iconPlugin } from "components/plugins/icons";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import Vuex from "vuex";
|
||||
|
||||
@@ -95,6 +96,7 @@ export function getLocalVue() {
|
||||
localVue.use(localizationPlugin);
|
||||
localVue.use(vueRxShortcutPlugin);
|
||||
localVue.use(eventHubPlugin);
|
||||
localVue.use(iconPlugin);
|
||||
localVue.directive("b-tooltip", mockedDirective);
|
||||
localVue.directive("b-popover", mockedDirective);
|
||||
return localVue;
|
||||
@@ -137,7 +139,7 @@ export const untilNthEmission = (src$, n = 1, safetyTimeout = 2000) => {
|
||||
let result;
|
||||
// prettier-ignore
|
||||
src$.pipe(
|
||||
take(n),
|
||||
take(n),
|
||||
takeUntil(timer(safetyTimeout))
|
||||
).subscribe({
|
||||
next: (val) => (result = val),
|
||||
|
||||
+37
-19
@@ -1719,14 +1719,14 @@
|
||||
"@nodelib/fs.scandir" "2.1.3"
|
||||
fastq "^1.6.0"
|
||||
|
||||
"@nuxt/opencollective@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@nuxt/opencollective/-/opencollective-0.3.0.tgz#11d8944dcf2d526e31660bb69570be03f8fb72b7"
|
||||
integrity sha512-Vf09BxCdj1iT2IRqVwX5snaY2WCTkvM0O4cWWSO1ThCFuc4if0Q/nNwAgCxRU0FeYHJ7DdyMUNSdswCLKlVqeg==
|
||||
"@nuxt/opencollective@^0.3.2":
|
||||
version "0.3.2"
|
||||
resolved "https://registry.yarnpkg.com/@nuxt/opencollective/-/opencollective-0.3.2.tgz#83cb70cdb2bac5fad6f8c93529e7b11187d49c02"
|
||||
integrity sha512-XG7rUdXG9fcafu9KTDIYjJSkRO38EwjlKYIb5TQ/0WDbiTUTtUtgncMscKOYzfsY86kGs05pAuMOR+3Fi0aN3A==
|
||||
dependencies:
|
||||
chalk "^2.4.2"
|
||||
consola "^2.10.1"
|
||||
node-fetch "^2.6.0"
|
||||
chalk "^4.1.0"
|
||||
consola "^2.15.0"
|
||||
node-fetch "^2.6.1"
|
||||
|
||||
"@sentry/browser@^5.20.1":
|
||||
version "5.20.1"
|
||||
@@ -3323,18 +3323,18 @@ boolbase@^1.0.0, boolbase@~1.0.0:
|
||||
resolved "https://registry.yarnpkg.com/boolbase/-/boolbase-1.0.0.tgz#68dff5fbe60c51eb37725ea9e3ed310dcc1e776e"
|
||||
integrity sha1-aN/1++YMUes3cl6p4+0xDcwed24=
|
||||
|
||||
bootstrap-vue@^2.15.0:
|
||||
version "2.15.0"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap-vue/-/bootstrap-vue-2.15.0.tgz#0dfc12c054496c0f10efed510da1def41697cf3c"
|
||||
integrity sha512-ncxWkDG0mKFVot314wWKJELi+ESO7k6ngV//qvJFs9iVzlFI8Hx3rBVbpcPW2vrJ+0vitH8N2SOwn4fdQ3frMQ==
|
||||
bootstrap-vue@^2.21.2:
|
||||
version "2.21.2"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap-vue/-/bootstrap-vue-2.21.2.tgz#ec38f66c3a2205becccddb6158a991d96509ed0b"
|
||||
integrity sha512-0Exe+4MZysqhZNXIKf4TzkvXaupxh9EHsoCRez0o5Dc0J7rlafayOEwql63qXv74CgZO8E4U8ugRNJko1vMvNw==
|
||||
dependencies:
|
||||
"@nuxt/opencollective" "^0.3.0"
|
||||
bootstrap ">=4.5.0 <5.0.0"
|
||||
"@nuxt/opencollective" "^0.3.2"
|
||||
bootstrap ">=4.5.3 <5.0.0"
|
||||
popper.js "^1.16.1"
|
||||
portal-vue "^2.1.7"
|
||||
vue-functional-data-merge "^3.1.0"
|
||||
|
||||
bootstrap@4.5.0, "bootstrap@>=4.5.0 <5.0.0":
|
||||
bootstrap@4.5.0:
|
||||
version "4.5.0"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.5.0.tgz#97d9dbcb5a8972f8722c9962483543b907d9b9ec"
|
||||
integrity sha512-Z93QoXvodoVslA+PWNdk23Hze4RBYIkpb5h8I2HY2Tu2h7A0LpAgLcyrhrSUyo2/Oxm2l1fRZPs1e5hnxnliXA==
|
||||
@@ -3344,6 +3344,11 @@ bootstrap@>=4.0.0-beta:
|
||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.0.2.tgz#aff23d5e0e03c31255ad437530ee6556e78e728e"
|
||||
integrity sha512-1Ge963tyEQWJJ+8qtXFU6wgmAVj9gweEjibUdbmcCEYsn38tVwRk8107rk2vzt6cfQcRr3SlZ8aQBqaD8aqf+Q==
|
||||
|
||||
"bootstrap@>=4.5.3 <5.0.0":
|
||||
version "4.6.0"
|
||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.0.tgz#97b9f29ac98f98dfa43bf7468262d84392552fd7"
|
||||
integrity sha512-Io55IuQY3kydzHtbGvQya3H+KorS/M9rSNyfCGCg9WZ4pyT/lCxIlpJgG1GXW/PswzC84Tr2fBYi+7+jFVQQBw==
|
||||
|
||||
brace-expansion@^1.1.7:
|
||||
version "1.1.11"
|
||||
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd"
|
||||
@@ -3826,6 +3831,14 @@ chalk@^4.0.0, chalk@~4.1.0:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
chalk@^4.1.0:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01"
|
||||
integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
|
||||
dependencies:
|
||||
ansi-styles "^4.1.0"
|
||||
supports-color "^7.1.0"
|
||||
|
||||
char-regex@^1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
|
||||
@@ -4390,10 +4403,10 @@ connect@^3.7.0:
|
||||
parseurl "~1.3.3"
|
||||
utils-merge "1.0.1"
|
||||
|
||||
consola@^2.10.1:
|
||||
version "2.10.1"
|
||||
resolved "https://registry.yarnpkg.com/consola/-/consola-2.10.1.tgz#4693edba714677c878d520e4c7e4f69306b4b927"
|
||||
integrity sha512-4sxpH6SGFYLADfUip4vuY65f/gEogrzJoniVhNUYkJHtng0l8ZjnDCqxxrSVRHOHwKxsy8Vm5ONZh1wOR3/l/w==
|
||||
consola@^2.15.0:
|
||||
version "2.15.3"
|
||||
resolved "https://registry.yarnpkg.com/consola/-/consola-2.15.3.tgz#2e11f98d6a4be71ff72e0bdf07bd23e12cb61550"
|
||||
integrity sha512-9vAdYbHj6x2fLKC4+oPH0kFzY/orMZyG2Aj+kNylHxKGJ/Ed4dpNyAQYwJOdqO4zdM7XpVHmyejQDcQHrnuXbw==
|
||||
|
||||
console-browserify@^1.1.0:
|
||||
version "1.1.0"
|
||||
@@ -10086,7 +10099,7 @@ node-fetch@^1.0.1:
|
||||
encoding "^0.1.11"
|
||||
is-stream "^1.0.1"
|
||||
|
||||
node-fetch@^2.2.0:
|
||||
node-fetch@^2.2.0, node-fetch@^2.6.1:
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||
@@ -11691,6 +11704,11 @@ prettier@^2.3.0:
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-2.3.0.tgz#b6a5bf1284026ae640f17f7ff5658a7567fc0d18"
|
||||
integrity sha512-kXtO4s0Lz/DW/IJ9QdWhAf7/NmPWQXkFr/r/WkR3vyI+0v8amTDxiaQSLzs8NBlytfLWX/7uQUMIW677yLKl4w==
|
||||
|
||||
pretty-bytes@^5.6.0:
|
||||
version "5.6.0"
|
||||
resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb"
|
||||
integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==
|
||||
|
||||
pretty-format@^25.2.1, pretty-format@^25.5.0:
|
||||
version "25.5.0"
|
||||
resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-25.5.0.tgz#7873c1d774f682c34b8d48b6743a2bf2ac55791a"
|
||||
|
||||
Reference in New Issue
Block a user