mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge branch 'dev' of https://github.com/neoformit/galaxy into dev-isee
This commit is contained in:
@@ -0,0 +1,31 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**Galaxy Version and/or server at which you observed the bug**
|
||||
Galaxy Version: (check <galaxy_url>/api/version if you don't know)
|
||||
Commit: (run `git rev-parse HEAD` if you run this Galaxy server)
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
4. See error
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Screenshots**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
@@ -5,8 +5,6 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
env:
|
||||
GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8'
|
||||
GALAXY_TEST_SELENIUM_REMOTE: '1'
|
||||
GALAXY_TEST_SELENIUM_REMOTE_PORT: "4444"
|
||||
GALAXY_SKIP_CLIENT_BUILD: '0'
|
||||
GALAXY_TEST_SELENIUM_RETRIES: 1
|
||||
YARN_INSTALL_OPTS: --frozen-lockfile
|
||||
@@ -26,10 +24,6 @@ jobs:
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
selenium:
|
||||
image: selenium/standalone-chrome:3.141.59
|
||||
ports:
|
||||
- 4444:4444
|
||||
steps:
|
||||
- name: Prune unused docker image, volumes and containers
|
||||
run: docker system prune -a -f
|
||||
@@ -48,6 +42,7 @@ jobs:
|
||||
- uses: mvdbeek/gha-yarn-cache@master
|
||||
with:
|
||||
yarn-lock-file: 'galaxy root/client/yarn.lock'
|
||||
- uses: nanasess/setup-chromedriver@master
|
||||
- name: Run tests
|
||||
run: './run_tests.sh -integration test/integration_selenium'
|
||||
working-directory: 'galaxy root'
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
name: Selenium tests (beta history panel)
|
||||
on: [push, pull_request]
|
||||
env:
|
||||
GALAXY_TEST_DBURI: 'postgresql://postgres:postgres@localhost:5432/galaxy?client_encoding=utf8'
|
||||
GALAXY_TEST_SKIP_FLAKEY_TESTS_ON_ERROR: 1
|
||||
GALAXY_TEST_SELENIUM_RETRIES: 1
|
||||
GALAXY_TEST_SELENIUM_BETA_HISTORY: 1
|
||||
jobs:
|
||||
test:
|
||||
name: Test
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
python-version: ['3.7']
|
||||
chunk: [0, 1, 2]
|
||||
continue-on-error: true
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:13
|
||||
env:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
with:
|
||||
path: 'galaxy root'
|
||||
- uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Cache pip dir
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: pip-cache-${{ matrix.python-version }}-${{ hashFiles('galaxy root/requirements.txt') }}
|
||||
- uses: nanasess/setup-chromedriver@master
|
||||
- name: Run tests
|
||||
run: ./run_tests.sh -selenium lib/galaxy_test/selenium -- --num-shards=3 --shard-id=${{ matrix.chunk }} || true
|
||||
working-directory: 'galaxy root'
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Selenium beta history panel test results (${{ matrix.python-version }}, ${{ matrix.chunk }})
|
||||
path: 'galaxy root/database/test_errors'
|
||||
- uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: Selenium beta history panel test results (${{ matrix.python-version }})
|
||||
path: 'galaxy root/run_selenium_tests.html'
|
||||
|
||||
@@ -83,6 +83,10 @@ environment variable:
|
||||
|
||||
GALAXY_URL="http://localhost:8000" make client-dev-server
|
||||
|
||||
Sometimes you want to run your local UI against a remote Galaxy server. This is also possible, if you enable `CHANGE_ORIGIN` flag
|
||||
|
||||
CHANGE_ORIGIN=true GALAXY_URL="https://usegalaxy.org/" make client-dev-server
|
||||
|
||||
## Changing Styles/CSS
|
||||
|
||||
Galaxy uses Sass for its styling, which is a superset of CSS that compiles down
|
||||
|
||||
+5
-2
@@ -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",
|
||||
@@ -55,6 +55,7 @@
|
||||
"markdown-it": "^11.0.0",
|
||||
"markdown-it-regexp": "^0.4.0",
|
||||
"moment": "2.26.0",
|
||||
"object-hash": "^2.2.0",
|
||||
"popper.js": "^1.16.1",
|
||||
"pouchdb": "^7.2.2",
|
||||
"pouchdb-adapter-memory": "^7.2.2",
|
||||
@@ -62,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",
|
||||
@@ -85,7 +87,8 @@
|
||||
"vueisotope": "^3.1.2",
|
||||
"vuex": "^3.4.0",
|
||||
"vuex-cache": "^3.2.0",
|
||||
"vuex-persist": "^3.1.3"
|
||||
"vuex-persist": "^3.1.3",
|
||||
"vuex-persistedstate": "^4.0.0"
|
||||
},
|
||||
"scripts": {
|
||||
"watch": "gulp && yarn run save-build-hash && yarn run webpack-watch",
|
||||
|
||||
@@ -32,6 +32,7 @@ export { default as LegacyGridView } from "legacy/grid/grid-view";
|
||||
export { create_chart, create_histogram } from "reports/run_stats";
|
||||
export { default as ToolshedGroups } from "toolshed/toolshed.groups";
|
||||
export { default as IES } from "galaxy.interactive_environments";
|
||||
export { openGlobalUploadModal } from "components/Upload";
|
||||
|
||||
export { Toast } from "ui/toast"; // TODO: remove when external consumers are updated/gone (IES right now)
|
||||
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<template>
|
||||
<b-button v-if="wait" variant="info" disabled>
|
||||
<font-awesome-icon icon="spinner" class="mr-2" spin />Please wait...
|
||||
</b-button>
|
||||
<b-button v-else variant="primary" v-b-tooltip.hover.bottom :title="tooltip" @click="$emit('onClick')">
|
||||
<font-awesome-icon icon="check" class="mr-2" />{{ title }}
|
||||
</b-button>
|
||||
</template>
|
||||
<script>
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faCheck, faSpinner } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(faCheck);
|
||||
library.add(faSpinner);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FontAwesomeIcon,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
wait: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
tooltip: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<div ref="webhook" id="webhook-view" />
|
||||
</template>
|
||||
<script>
|
||||
import Webhooks from "mvc/webhooks";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
toolId: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
const el = this.$refs["webhook"];
|
||||
new Webhooks.WebhookView({
|
||||
el,
|
||||
type: this.type,
|
||||
toolId: this.toolId,
|
||||
});
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -66,7 +66,7 @@ export default {
|
||||
const description = storageInfo.description;
|
||||
this.storageInfo = storageInfo;
|
||||
if (description) {
|
||||
this.descriptionRendered = MarkdownIt().render(storageInfo.description);
|
||||
this.descriptionRendered = MarkdownIt({ html: true }).render(storageInfo.description);
|
||||
} else {
|
||||
this.descriptionRendered = null;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
import Vue from "vue";
|
||||
import axios from "axios";
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import DatasetAttributes from "./DatasetAttributes";
|
||||
import MockProvider from "../providers/MockProvider";
|
||||
import MockAdapter from "axios-mock-adapter";
|
||||
import flushPromises from "flush-promises";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
function buildWrapper(conversion_disable = false) {
|
||||
return mount(DatasetAttributes, {
|
||||
propsData: {
|
||||
datasetId: "dataset_id",
|
||||
},
|
||||
localVue,
|
||||
stubs: {
|
||||
DatasetAttributesProvider: MockProvider({
|
||||
result: {
|
||||
attribute_inputs: [{ name: "attribute_text", type: "text" }],
|
||||
conversion_inputs: [{ name: "conversion_text", type: "text" }],
|
||||
conversion_disable: conversion_disable,
|
||||
datatype_inputs: [{ name: "datatype_text", type: "text" }],
|
||||
permission_inputs: [{ name: "permission_text", type: "text" }],
|
||||
},
|
||||
}),
|
||||
FontAwesomeIcon: false,
|
||||
FormElement: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("DatasetAttributes", () => {
|
||||
it("check rendering", async () => {
|
||||
const axiosMock = new MockAdapter(axios);
|
||||
axiosMock.onPut(`/dataset/set_edit`).reply(200, { message: "success", status: "success" });
|
||||
const wrapper = buildWrapper();
|
||||
await Vue.nextTick();
|
||||
expect(wrapper.findAll("button").length).toBe(6);
|
||||
expect(wrapper.findAll("[tour_id=attribute_text]").length).toBe(1);
|
||||
expect(wrapper.findAll("[tour_id=conversion_text]").length).toBe(1);
|
||||
expect(wrapper.findAll("[tour_id=datatype_text]").length).toBe(1);
|
||||
expect(wrapper.findAll("[tour_id=permission_text]").length).toBe(1);
|
||||
expect(wrapper.findAll(".tab-pane").length).toBe(4);
|
||||
const $button = wrapper.find("#dataset-attributes-default-save");
|
||||
await $button.trigger("click");
|
||||
await flushPromises();
|
||||
expect(wrapper.find("[role=alert]").text()).toBe("success");
|
||||
});
|
||||
|
||||
it("check rendering without conversion option", async () => {
|
||||
const wrapper = buildWrapper(true);
|
||||
await Vue.nextTick();
|
||||
expect(wrapper.findAll("button").length).toBe(5);
|
||||
expect(wrapper.findAll("[tour_id=attribute_text]").length).toBe(1);
|
||||
expect(wrapper.findAll("[tour_id=conversion_text]").length).toBe(0);
|
||||
expect(wrapper.findAll("[tour_id=datatype_text]").length).toBe(1);
|
||||
expect(wrapper.findAll("[tour_id=permission_text]").length).toBe(1);
|
||||
expect(wrapper.findAll(".tab-pane").length).toBe(3);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,134 @@
|
||||
<template>
|
||||
<div>
|
||||
<h4>Edit Dataset Attributes</h4>
|
||||
<b-alert v-if="messageText" :variant="messageVariant" show>
|
||||
{{ messageText | l }}
|
||||
</b-alert>
|
||||
<DatasetAttributesProvider :id="datasetId" :use-cache="false" v-slot="{ result, loading }" @error="onError">
|
||||
<div v-if="!loading" class="mt-3">
|
||||
<b-tabs>
|
||||
<b-tab v-if="!result['attribute_disable']">
|
||||
<template v-slot:title>
|
||||
<font-awesome-icon icon="bars" class="mr-1" />{{ "Attributes" | l }}
|
||||
</template>
|
||||
<FormDisplay :inputs="result['attribute_inputs']" @onChange="onAttribute" />
|
||||
<div class="mt-2">
|
||||
<b-button
|
||||
id="dataset-attributes-default-save"
|
||||
@click="submit('attribute', 'attributes')"
|
||||
variant="primary"
|
||||
class="mr-1"
|
||||
>
|
||||
<font-awesome-icon icon="save" class="mr-1" />{{ "Save" | l }}
|
||||
</b-button>
|
||||
<b-button @click="submit('attribute', 'autodetect')">
|
||||
<font-awesome-icon icon="redo" class="mr-1" />{{ "Auto-detect" | l }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-tab>
|
||||
<b-tab v-if="!result['conversion_disable']">
|
||||
<template v-slot:title>
|
||||
<font-awesome-icon icon="cog" class="mr-1" />{{ "Convert" | l }}
|
||||
</template>
|
||||
<FormDisplay :inputs="result['conversion_inputs']" @onChange="onConversion" />
|
||||
<div class="mt-2">
|
||||
<b-button @click="submit('conversion', 'conversion')" variant="primary">
|
||||
<font-awesome-icon icon="exchange-alt" class="mr-1" />{{ "Create Dataset" | l }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-tab>
|
||||
<b-tab v-if="!result['datatype_disable']">
|
||||
<template v-slot:title>
|
||||
<font-awesome-icon icon="database" class="mr-1" />{{ "Datatypes" | l }}
|
||||
</template>
|
||||
<FormDisplay :inputs="result['datatype_inputs']" @onChange="onDatatype" />
|
||||
<div class="mt-2">
|
||||
<b-button @click="submit('datatype', 'datatype')" variant="primary" class="mr-1">
|
||||
<font-awesome-icon icon="save" class="mr-1" />{{ "Save" | l }}
|
||||
</b-button>
|
||||
<b-button @click="submit('datatype', 'datatype_detect')">
|
||||
<font-awesome-icon icon="redo" class="mr-1" />{{ "Auto-detect" | l }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-tab>
|
||||
<b-tab v-if="!result['permission_disable']">
|
||||
<template v-slot:title>
|
||||
<font-awesome-icon icon="user" class="mr-1" />{{ "Permissions" | l }}
|
||||
</template>
|
||||
<FormDisplay :inputs="result['permission_inputs']" @onChange="onPermission" />
|
||||
<div class="mt-2">
|
||||
<b-button @click="submit('permission', 'permission')" variant="primary">
|
||||
<font-awesome-icon icon="save" class="mr-1" />{{ "Save" | l }}
|
||||
</b-button>
|
||||
</div>
|
||||
</b-tab>
|
||||
</b-tabs>
|
||||
</div>
|
||||
</DatasetAttributesProvider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getGalaxyInstance } from "app";
|
||||
import FormDisplay from "components/Form/FormDisplay";
|
||||
import { DatasetAttributesProvider } from "components/providers/DatasetProvider";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faBars, faCog, faDatabase, faExchangeAlt, faRedo, faSave, faUser } from "@fortawesome/free-solid-svg-icons";
|
||||
import { setAttributes } from "./services";
|
||||
|
||||
library.add(faBars, faCog, faDatabase, faExchangeAlt, faRedo, faSave, faUser);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DatasetAttributesProvider,
|
||||
FontAwesomeIcon,
|
||||
FormDisplay,
|
||||
},
|
||||
props: {
|
||||
datasetId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
messageText: null,
|
||||
messageVariant: null,
|
||||
formData: {},
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onAttribute(data) {
|
||||
this.formData["attribute"] = data;
|
||||
},
|
||||
onConversion(data) {
|
||||
this.formData["conversion"] = data;
|
||||
},
|
||||
onDatatype(data) {
|
||||
this.formData["datatype"] = data;
|
||||
},
|
||||
onPermission(data) {
|
||||
this.formData["permission"] = data;
|
||||
},
|
||||
onError(messageText) {
|
||||
this.messageText = messageText;
|
||||
this.messageVariant = "danger";
|
||||
},
|
||||
submit(key, operation) {
|
||||
setAttributes(this.datasetId, this.formData[key], operation).then((response) => {
|
||||
this.messageText = response.message;
|
||||
this.messageVariant = response.status;
|
||||
this._reloadHistory();
|
||||
}, this.onError);
|
||||
},
|
||||
/** reload Galaxy's history after updating dataset's attributes */
|
||||
_reloadHistory: function () {
|
||||
const Galaxy = getGalaxyInstance();
|
||||
if (Galaxy) {
|
||||
Galaxy.currHistoryPanel.loadCurrentHistory();
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,64 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import DatasetError from "./DatasetError";
|
||||
import MockProvider from "../providers/MockProvider";
|
||||
|
||||
jest.mock("components/WorkflowInvocationState/providers", () => {
|
||||
return {}; // stubbed below
|
||||
});
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
function buildWrapper(has_duplicate_inputs = true, has_empty_inputs = true, user_email = "") {
|
||||
return mount(DatasetError, {
|
||||
propsData: {
|
||||
datasetId: "dataset_id",
|
||||
},
|
||||
localVue,
|
||||
stubs: {
|
||||
JobDetailsProvider: MockProvider({
|
||||
result: {
|
||||
tool_id: "tool_id",
|
||||
tool_stderr: "tool_stderr",
|
||||
job_stderr: "job_stderr",
|
||||
job_messages: [{ desc: "message_1" }, { desc: "message_2" }],
|
||||
user_email: user_email,
|
||||
},
|
||||
}),
|
||||
JobProblemProvider: MockProvider({
|
||||
result: { has_duplicate_inputs: has_duplicate_inputs, has_empty_inputs: has_empty_inputs },
|
||||
}),
|
||||
DatasetProvider: MockProvider({
|
||||
resultLabel: "item",
|
||||
result: { id: "dataset_id", creating_job: "creating_job" },
|
||||
}),
|
||||
FontAwesomeIcon: false,
|
||||
FormElement: false,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe("DatasetError", () => {
|
||||
it("check props with common problems", async () => {
|
||||
const wrapper = buildWrapper();
|
||||
expect(wrapper.find("#dataset-error-tool-id").text()).toBe("tool_id");
|
||||
expect(wrapper.find("#dataset-error-tool-stderr").text()).toBe("tool_stderr");
|
||||
expect(wrapper.find("#dataset-error-job-stderr").text()).toBe("job_stderr");
|
||||
const messages = wrapper.findAll("#dataset-error-job-messages .code");
|
||||
expect(messages.at(0).text()).toBe("message_1");
|
||||
expect(messages.at(1).text()).toBe("message_2");
|
||||
expect(wrapper.find("#dataset-error-has-empty-inputs")).toBeDefined();
|
||||
expect(wrapper.find("#dataset-error-has-duplicate-inputs")).toBeDefined();
|
||||
expect(wrapper.findAll("#dataset-error-email").length).toBe(1);
|
||||
});
|
||||
|
||||
it("check props without common problems", async () => {
|
||||
const wrapper = buildWrapper(false, false, "user_email");
|
||||
expect(wrapper.find("#dataset-error-tool-id").text()).toBe("tool_id");
|
||||
expect(wrapper.find("#dataset-error-tool-stderr").text()).toBe("tool_stderr");
|
||||
expect(wrapper.find("#dataset-error-job-stderr").text()).toBe("job_stderr");
|
||||
expect(wrapper.findAll("#dataset-error-has-empty-inputs").length).toBe(0);
|
||||
expect(wrapper.findAll("#dataset-error-has-duplicate-inputs").length).toBe(0);
|
||||
expect(wrapper.findAll("#dataset-error-email").length).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,140 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-alert v-if="errorMessage" variant="danger" show>
|
||||
<h4 class="alert-heading">Failed to access Dataset details.</h4>
|
||||
{{ errorMessage }}
|
||||
</b-alert>
|
||||
<DatasetProvider :id="datasetId" v-slot="{ item: dataset, loading: datasetLoading }">
|
||||
<JobDetailsProvider
|
||||
v-if="!datasetLoading"
|
||||
:jobid="dataset.creating_job"
|
||||
v-slot="{ result: jobDetails, loading }"
|
||||
@error="onError"
|
||||
>
|
||||
<div v-if="!loading">
|
||||
<div class="page-container edit-attr">
|
||||
<div class="response-message"></div>
|
||||
</div>
|
||||
<h2>Dataset Error Report</h2>
|
||||
<p>
|
||||
An error occurred while running the tool
|
||||
<b id="dataset-error-tool-id" class="text-break">{{ jobDetails.tool_id }}</b
|
||||
>.
|
||||
</p>
|
||||
<DatasetErrorDetails
|
||||
:tool-stderr="jobDetails.tool_stderr"
|
||||
:job-stderr="jobDetails.job_stderr"
|
||||
:job-messages="jobDetails.job_messages"
|
||||
/>
|
||||
<JobProblemProvider :jobid="dataset.creating_job" v-slot="{ result: jobProblems }" @error="onError">
|
||||
<div v-if="jobProblems && (jobProblems.has_duplicate_inputs || jobProblems.has_empty_inputs)">
|
||||
<h3 class="common_problems mt-3">Detected Common Potential Problems</h3>
|
||||
<p v-if="jobProblems.has_empty_inputs" id="dataset-error-has-empty-inputs">
|
||||
The tool was executed with one or more empty input datasets. This frequently results in
|
||||
tool errors due to problematic input choices.
|
||||
</p>
|
||||
<p v-if="jobProblems.has_duplicate_inputs" id="dataset-error-has-duplicate-inputs">
|
||||
The tool was executed with one or more duplicate input datasets. This frequently results
|
||||
in tool errors due to problematic input choices.
|
||||
</p>
|
||||
</div>
|
||||
</JobProblemProvider>
|
||||
<h3 class="mt-3">Troubleshooting</h3>
|
||||
<p>
|
||||
There are a number of helpful resources to self diagnose and correct problems.
|
||||
<br />
|
||||
Start here:
|
||||
<b>
|
||||
<a href="https://galaxyproject.org/support/tool-error/" target="_blank">
|
||||
My job ended with an error. What can I do?
|
||||
</a>
|
||||
</b>
|
||||
</p>
|
||||
<h3 class="mb-3">Issue Report</h3>
|
||||
<b-alert
|
||||
v-for="(resultMessage, index) in resultMessages"
|
||||
:key="index"
|
||||
:variant="resultMessage[1]"
|
||||
show
|
||||
>{{ resultMessage[0] }}</b-alert
|
||||
>
|
||||
<FormElement
|
||||
v-if="!jobDetails.user_email"
|
||||
id="dataset-error-email"
|
||||
v-model="email"
|
||||
title="Please provide your email:"
|
||||
/>
|
||||
<FormElement
|
||||
id="dataset-error-message"
|
||||
v-model="message"
|
||||
:area="true"
|
||||
title="Please provide detailed information on the activities leading to this issue:"
|
||||
/>
|
||||
<b-button
|
||||
id="dataset-error-submit"
|
||||
variant="primary"
|
||||
@click="submit(dataset, jobDetails.user_email)"
|
||||
class="mt-3"
|
||||
>
|
||||
<font-awesome-icon icon="bug" class="mr-1" />Report
|
||||
</b-button>
|
||||
</div>
|
||||
</JobDetailsProvider>
|
||||
</DatasetProvider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import DatasetErrorDetails from "./DatasetErrorDetails";
|
||||
import FormElement from "components/Form/FormElement";
|
||||
import { DatasetProvider } from "components/WorkflowInvocationState/providers";
|
||||
import { JobDetailsProvider, JobProblemProvider } from "components/providers/JobProvider";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faBug } from "@fortawesome/free-solid-svg-icons";
|
||||
import { sendErrorReport } from "./services";
|
||||
|
||||
library.add(faBug);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DatasetProvider,
|
||||
DatasetErrorDetails,
|
||||
FontAwesomeIcon,
|
||||
FormElement,
|
||||
JobDetailsProvider,
|
||||
JobProblemProvider,
|
||||
},
|
||||
props: {
|
||||
datasetId: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
message: null,
|
||||
email: null,
|
||||
errorMessage: null,
|
||||
resultMessages: [],
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onError(err) {
|
||||
this.errorMessage = err;
|
||||
},
|
||||
submit(dataset, userEmail) {
|
||||
const email = this.email || userEmail;
|
||||
const message = this.message;
|
||||
sendErrorReport(dataset, message, email).then(
|
||||
(resultMessages) => {
|
||||
this.resultMessages = resultMessages;
|
||||
},
|
||||
(errorMessage) => {
|
||||
this.errorMessage = errorMessage;
|
||||
}
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<template>
|
||||
<div v-if="hasDetails">
|
||||
<h3>Details</h3>
|
||||
<div v-if="hasMessages" id="dataset-error-job-messages">
|
||||
<p>Execution resulted in the following messages:</p>
|
||||
<div v-for="(jobMessage, index) in jobMessages" :key="index">
|
||||
<pre class="rounded code">{{ jobMessage["desc"] }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="toolStderr">
|
||||
<p>Tool generated the following standard error:</p>
|
||||
<pre id="dataset-error-tool-stderr" class="rounded code">{{ toolStderr }}</pre>
|
||||
</div>
|
||||
<div v-if="jobStderr">
|
||||
<p>Galaxy job runner generated the following standard error:</p>
|
||||
<pre id="dataset-error-job-stderr" class="rounded code">{{ jobStderr }}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
toolStderr: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
jobStderr: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
jobMessages: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
hasDetails() {
|
||||
return this.toolStderr || this.jobStderr || this.hasMessages;
|
||||
},
|
||||
hasMessages() {
|
||||
return this.jobMessages && this.jobMessages.length > 0;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,33 @@
|
||||
import axios from "axios";
|
||||
import { rethrowSimple } from "utils/simple-error";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
|
||||
export async function sendErrorReport(dataset, message, email) {
|
||||
const payload = {
|
||||
dataset_id: dataset.id,
|
||||
message,
|
||||
email,
|
||||
};
|
||||
const url = `${getAppRoot()}api/jobs/${dataset.creating_job}/error`;
|
||||
try {
|
||||
const { data } = await axios.post(url, payload);
|
||||
return data.messages;
|
||||
} catch (e) {
|
||||
rethrowSimple(e);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setAttributes(datasetId, settings, operation) {
|
||||
const payload = {
|
||||
dataset_id: datasetId,
|
||||
operation: operation,
|
||||
...settings,
|
||||
};
|
||||
const url = `${getAppRoot()}dataset/set_edit`;
|
||||
try {
|
||||
const { data } = await axios.put(url, payload);
|
||||
return data;
|
||||
} catch (e) {
|
||||
rethrowSimple(e);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import FormBoolean from "./FormBoolean";
|
||||
import Vue from "vue";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
describe("FormBoolean", () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(FormBoolean, {
|
||||
propsData: {
|
||||
value: false,
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
});
|
||||
|
||||
it("check initial value and value change", async () => {
|
||||
const input = wrapper.find("input");
|
||||
expect(wrapper.vm.currentValue).toBe(false);
|
||||
wrapper.setProps({ value: true });
|
||||
await Vue.nextTick();
|
||||
expect(wrapper.vm.currentValue).toBe(true);
|
||||
input.trigger("click");
|
||||
expect(input.element.checked).toBe(false);
|
||||
input.trigger("click");
|
||||
expect(input.element.checked).toBe(true);
|
||||
expect(wrapper.emitted().input[0][0]).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
<template>
|
||||
<b-form-checkbox v-model="currentValue" size="lg" switch />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
value: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
currentValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit("input", val);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
+2
-2
@@ -24,7 +24,7 @@ describe("FormInput", () => {
|
||||
expect(input.element.value).toBe("initial_value");
|
||||
await input.setValue("new_value");
|
||||
expect(input.element.value).toBe("new_value");
|
||||
expect(wrapper.emitted().onChange[0][0]).toBe("new_value");
|
||||
expect(wrapper.emitted().input[0][0]).toBe("new_value");
|
||||
});
|
||||
|
||||
it("check switching to text area", async () => {
|
||||
@@ -33,6 +33,6 @@ describe("FormInput", () => {
|
||||
expect(input.element.value).toBe("initial_value");
|
||||
await input.setValue("new_value");
|
||||
expect(input.element.value).toBe("new_value");
|
||||
expect(wrapper.emitted().onChange[0][0]).toBe("new_value");
|
||||
expect(wrapper.emitted().input[0][0]).toBe("new_value");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
<template>
|
||||
<textarea v-if="area" :id="id" class="ui-textarea" v-model="currentValue" />
|
||||
<input v-else :id="id" class="ui-input" v-model="currentValue" />
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
area: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
currentValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit("input", val);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,68 +0,0 @@
|
||||
<template>
|
||||
<div ref="form" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Form from "mvc/form/form-view";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
inputs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
textEnable: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
textDisable: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
initialErrors: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
id() {
|
||||
this.onRender();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.onRender();
|
||||
},
|
||||
methods: {
|
||||
onChange() {
|
||||
this.$emit("onChange", this.form.data.create());
|
||||
},
|
||||
onRender() {
|
||||
this.$nextTick(() => {
|
||||
const el = this.$refs["form"];
|
||||
this.form = new Form({
|
||||
el,
|
||||
inputs: this.inputs,
|
||||
initial_errors: this.initialErrors,
|
||||
text_enable: this.textEnable,
|
||||
text_disable: this.textDisable,
|
||||
}).on("change", this.onChange);
|
||||
this.onChange();
|
||||
});
|
||||
},
|
||||
parseUpdate(data) {
|
||||
this.$nextTick(() => {
|
||||
this.form.update(data, false);
|
||||
});
|
||||
},
|
||||
parseErrors(data) {
|
||||
this.$nextTick(() => {
|
||||
this.form.errors(data);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -0,0 +1,29 @@
|
||||
import { mount } from "@vue/test-utils";
|
||||
import { getLocalVue } from "jest/helpers";
|
||||
import FormCard from "./FormCard";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
|
||||
describe("FormCard", () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = mount(FormCard, {
|
||||
propsData: {
|
||||
title: "title",
|
||||
description: "description",
|
||||
icon: "icon-class",
|
||||
},
|
||||
localVue,
|
||||
});
|
||||
});
|
||||
|
||||
it("check props", async () => {
|
||||
const title = wrapper.find(".portlet-title-text > b");
|
||||
expect(title.text()).toBe("title");
|
||||
const description = wrapper.find(".portlet-title-text > span");
|
||||
expect(description.text()).toBe("description");
|
||||
const icon = wrapper.find(".portlet-title-icon");
|
||||
expect(icon.classes()).toContain("icon-class");
|
||||
});
|
||||
});
|
||||
@@ -3,22 +3,44 @@
|
||||
<div class="portlet-header">
|
||||
<div class="portlet-operations">
|
||||
<slot name="operations" />
|
||||
<b-button
|
||||
v-if="collapsible"
|
||||
role="button"
|
||||
title="Collapse/Expand"
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="float-right"
|
||||
v-b-tooltip.hover.bottom
|
||||
@click="onCollapse"
|
||||
>
|
||||
<font-awesome-icon v-if="collapsed" icon="eye-slash" class="fa-fw" />
|
||||
<font-awesome-icon v-else icon="eye" class="fa-fw" />
|
||||
</b-button>
|
||||
</div>
|
||||
<div class="portlet-title">
|
||||
<a class="portlet-title" @click="onCollapse" :href="href">
|
||||
<i :class="['portlet-title-icon fa mr-1', icon]" style="display: inline"></i>
|
||||
<span class="portlet-title-text">
|
||||
<b itemprop="name">{{ title }}</b> <span itemprop="description">{{ description }}</span>
|
||||
{{ version }}
|
||||
</span>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
<div class="portlet-content">
|
||||
<div v-show="!collapsed" class="portlet-content">
|
||||
<slot name="body" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faEye, faEyeSlash } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
library.add(faEye);
|
||||
library.add(faEyeSlash);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FontAwesomeIcon,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
type: String,
|
||||
@@ -26,15 +48,34 @@ export default {
|
||||
},
|
||||
description: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
version: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: "fa-wrench",
|
||||
default: "",
|
||||
},
|
||||
collapsible: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
collapsed: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {};
|
||||
},
|
||||
computed: {
|
||||
href() {
|
||||
return this.collapsible ? "#" : null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onCollapse() {
|
||||
if (this.collapsible) {
|
||||
this.$emit("update:collapsed", !this.collapsed);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,190 @@
|
||||
<template>
|
||||
<div ref="form" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import _ from "underscore";
|
||||
import Form from "mvc/form/form-view";
|
||||
import { visitInputs } from "components/Form/utilities";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
inputs: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
errors: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
sustainRepeats: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
sustainConditionals: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
textEnable: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
textDisable: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
validationScrollTo: {
|
||||
type: Array,
|
||||
default: null,
|
||||
},
|
||||
replaceParams: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
formData: {},
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
id() {
|
||||
this.onRender();
|
||||
},
|
||||
validationScrollTo() {
|
||||
this.onHighlight(this.validationScrollTo);
|
||||
},
|
||||
validation() {
|
||||
this.onHighlight(this.validation, true);
|
||||
this.$emit("onValidation", this.validation);
|
||||
},
|
||||
inputs() {
|
||||
this.$nextTick(() => {
|
||||
this.form.update(this.inputs);
|
||||
});
|
||||
},
|
||||
errors() {
|
||||
this.$nextTick(() => {
|
||||
this.form.errors(this.errors);
|
||||
});
|
||||
},
|
||||
replaceParams() {
|
||||
this.onReplaceParams();
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.onRender();
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.form.off();
|
||||
},
|
||||
computed: {
|
||||
validation() {
|
||||
let batch_n = -1;
|
||||
let batch_src = null;
|
||||
for (const job_input_id in this.formData) {
|
||||
const input_value = this.formData[job_input_id];
|
||||
const input_id = this.form.data.match(job_input_id);
|
||||
const input_field = this.form.field_list[input_id];
|
||||
const input_def = this.form.input_list[input_id];
|
||||
if (!input_id || !input_def || !input_field || input_def.step_linked) {
|
||||
continue;
|
||||
}
|
||||
if (
|
||||
input_value &&
|
||||
Array.isArray(input_value.values) &&
|
||||
input_value.values.length == 0 &&
|
||||
!input_def.optional
|
||||
) {
|
||||
return [job_input_id, "Please provide data for this input."];
|
||||
}
|
||||
if (input_value == null && !input_def.optional && input_def.type != "hidden") {
|
||||
return [job_input_id, "Please provide a value for this option."];
|
||||
}
|
||||
if (input_def.wp_linked && input_def.text_value == input_value) {
|
||||
return [job_input_id, "Please provide a value for this workflow parameter."];
|
||||
}
|
||||
if (input_field.validate) {
|
||||
const message = input_field.validate();
|
||||
if (message) {
|
||||
return [job_input_id, message];
|
||||
}
|
||||
}
|
||||
if (input_value && input_value.batch) {
|
||||
const n = input_value.values.length;
|
||||
const src = n > 0 && input_value.values[0] && input_value.values[0].src;
|
||||
if (src) {
|
||||
if (batch_src === null) {
|
||||
batch_src = src;
|
||||
} else if (batch_src !== src) {
|
||||
return [
|
||||
job_input_id,
|
||||
"Please select either dataset or dataset list fields for all batch mode fields.",
|
||||
];
|
||||
}
|
||||
}
|
||||
if (batch_n === -1) {
|
||||
batch_n = n;
|
||||
} else if (batch_n !== n) {
|
||||
return [
|
||||
job_input_id,
|
||||
`Please make sure that you select the same number of inputs for all batch mode fields. This field contains <b>${n}</b> selection(s) while a previous field contains <b>${batch_n}</b>.`,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onReplaceParams() {
|
||||
if (this.replaceParams) {
|
||||
this.params = {};
|
||||
visitInputs(this.inputs, (input, name) => {
|
||||
this.params[name] = input;
|
||||
});
|
||||
_.each(this.params, (input, name) => {
|
||||
const newValue = this.replaceParams[name];
|
||||
if (newValue) {
|
||||
const field = this.form.field_list[this.form.data.match(name)];
|
||||
field.value(newValue);
|
||||
}
|
||||
});
|
||||
this.form.trigger("change");
|
||||
}
|
||||
},
|
||||
onChange(refreshRequest) {
|
||||
this.formData = this.form.data.create();
|
||||
this.$emit("onChange", this.formData, refreshRequest);
|
||||
},
|
||||
onRender() {
|
||||
this.$nextTick(() => {
|
||||
const el = this.$refs["form"];
|
||||
this.form = new Form({
|
||||
el,
|
||||
inputs: this.inputs,
|
||||
text_enable: this.textEnable,
|
||||
text_disable: this.textDisable,
|
||||
sustain_repeats: this.sustainRepeats,
|
||||
sustain_conditionals: this.sustainConditionals,
|
||||
onchange: (refreshRequest) => {
|
||||
this.onChange(refreshRequest);
|
||||
},
|
||||
});
|
||||
this.onChange();
|
||||
});
|
||||
},
|
||||
onHighlight(validation, silent = false) {
|
||||
this.form.trigger("reset");
|
||||
if (validation && validation.length == 2) {
|
||||
const input_id = this.form.data.match(validation[0]);
|
||||
this.form.highlight(input_id, validation[1], silent);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -22,7 +22,7 @@ describe("FormElement", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("check help text", async () => {
|
||||
it("check props", async () => {
|
||||
const help = wrapper.find(".ui-form-info");
|
||||
expect(help.text()).toBe("help_text");
|
||||
const error = wrapper.find(".ui-form-error-text");
|
||||
|
||||
@@ -6,16 +6,19 @@
|
||||
</div>
|
||||
<div class="ui-form-title">{{ title }}</div>
|
||||
<div class="ui-form-field">
|
||||
<FormInput :id="id" :value="value" :area="area" @onChange="onChange" />
|
||||
<FormInput v-if="type == 'text'" :id="id" :area="area" v-model="currentValue" />
|
||||
<FormBoolean v-else-if="type == 'boolean'" :id="id" v-model="currentValue" />
|
||||
<span class="ui-form-info form-text text-muted mt-2">{{ help }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import FormInput from "./FormInput";
|
||||
import FormBoolean from "./Elements/FormBoolean";
|
||||
import FormInput from "./Elements/FormInput";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
FormBoolean,
|
||||
FormInput,
|
||||
},
|
||||
props: {
|
||||
@@ -24,16 +27,16 @@ export default {
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
type: [String, Boolean],
|
||||
default: null,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: null,
|
||||
},
|
||||
help: {
|
||||
type: String,
|
||||
default: "",
|
||||
default: null,
|
||||
},
|
||||
area: {
|
||||
type: Boolean,
|
||||
@@ -43,8 +46,20 @@ export default {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
type: {
|
||||
type: String,
|
||||
default: "text",
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
currentValue: {
|
||||
get() {
|
||||
return this.value;
|
||||
},
|
||||
set(val) {
|
||||
this.$emit("input", val);
|
||||
},
|
||||
},
|
||||
hasError() {
|
||||
return !!this.error;
|
||||
},
|
||||
@@ -56,10 +71,5 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onChange(value) {
|
||||
this.$emit("onChange", value);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
<template>
|
||||
<UrlDataProvider :url="url" v-slot="{ result: config, loading }" @error="onError">
|
||||
<div :id="id" v-if="!loading">
|
||||
<b-alert v-if="config.message" :variant="configMessageVariant(config)" show>
|
||||
{{ config.message }}
|
||||
</b-alert>
|
||||
<b-alert v-if="messageText" :variant="messageVariant" show>
|
||||
{{ messageText }}
|
||||
</b-alert>
|
||||
<FormCard :title="configTitle(config)" :icon="configIcon(config)">
|
||||
<template v-slot:body>
|
||||
<FormDisplay :inputs="config.inputs" :replace-params="replaceParams" @onChange="onChange" />
|
||||
</template>
|
||||
</FormCard>
|
||||
<div class="mt-3">
|
||||
<b-button id="submit" variant="primary" class="mr-1" @click="onSubmit()">
|
||||
<span :class="submitIconClass" />{{ submitTitle | l }}
|
||||
</b-button>
|
||||
<b-button v-if="cancelRedirect" @click="onCancel()">
|
||||
<span class="mr-1 fa fa-times" />{{ "Cancel" | l }}
|
||||
</b-button>
|
||||
</div>
|
||||
</div>
|
||||
</UrlDataProvider>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { submitData } from "./services";
|
||||
import { UrlDataProvider } from "components/providers/UrlDataProvider";
|
||||
import { visitInputs } from "components/Form/utilities";
|
||||
import FormCard from "components/Form/FormCard";
|
||||
import FormDisplay from "components/Form/FormDisplay";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
submitIcon: {
|
||||
type: String,
|
||||
default: "fa-save",
|
||||
},
|
||||
submitTitle: {
|
||||
type: String,
|
||||
default: "Save",
|
||||
},
|
||||
cancelRedirect: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
url: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
redirect: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
FormCard,
|
||||
FormDisplay,
|
||||
UrlDataProvider,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
messageText: null,
|
||||
messageVariant: null,
|
||||
formData: {},
|
||||
replaceParams: null,
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
submitIconClass() {
|
||||
return `mr-1 fa ${this.submitIcon}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
configTitle(options) {
|
||||
return this.title || options.title;
|
||||
},
|
||||
configIcon(options) {
|
||||
return this.icon || options.icon;
|
||||
},
|
||||
configMessageVariant(options) {
|
||||
return options.status || "warning";
|
||||
},
|
||||
onChange(formData) {
|
||||
this.formData = formData;
|
||||
},
|
||||
onCancel() {
|
||||
window.location = `${getAppRoot()}${this.cancelRedirect}`;
|
||||
},
|
||||
onSubmit() {
|
||||
submitData(this.url, this.formData).then((response) => {
|
||||
let params = {};
|
||||
if (response.id) {
|
||||
params.id = response.id;
|
||||
} else {
|
||||
params = {
|
||||
message: response.message,
|
||||
status: "success",
|
||||
persistent: false,
|
||||
};
|
||||
}
|
||||
if (this.redirect) {
|
||||
const urlParams = new URLSearchParams(params);
|
||||
window.location = `${getAppRoot()}${this.redirect}?${urlParams.toString()}`;
|
||||
} else {
|
||||
const replaceParams = {};
|
||||
visitInputs(response.inputs, (input, name) => {
|
||||
replaceParams[name] = input.value;
|
||||
});
|
||||
this.replaceParams = replaceParams;
|
||||
this.showMessage(response.message);
|
||||
}
|
||||
}, this.onError);
|
||||
},
|
||||
onError(error) {
|
||||
this.showMessage(error || `Failed to load resource ${this.url}.`, "danger");
|
||||
},
|
||||
showMessage(message, variant = "success") {
|
||||
this.messageText = message;
|
||||
this.messageVariant = variant;
|
||||
document.querySelector(".center-panel").scrollTop = 0;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,37 +0,0 @@
|
||||
<template>
|
||||
<textarea v-if="area" class="ui-input" v-model="currentValue" @input="onChange" @change="onChange" />
|
||||
<input v-else class="ui-input" v-model="currentValue" @input="onChange" @change="onChange" />
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
id: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: "",
|
||||
},
|
||||
area: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(newValue) {
|
||||
this.currentValue = newValue;
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentValue: this.value,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
onChange() {
|
||||
this.$emit("onChange", this.currentValue);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<b-alert :variant="variant" :show="showAlert">
|
||||
<b-alert class="mt-2" :variant="variant" :show="showAlert">
|
||||
{{ message | l }}
|
||||
</b-alert>
|
||||
</template>
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import axios from "axios";
|
||||
import { rethrowSimple } from "utils/simple-error";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
|
||||
export async function submitData(url, payload) {
|
||||
try {
|
||||
const { data } = await axios.put(`${getAppRoot()}${url}`, payload);
|
||||
return data;
|
||||
} catch (e) {
|
||||
rethrowSimple(e);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,64 @@
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import { copy } from "utils/clipboard";
|
||||
import _ from "underscore";
|
||||
|
||||
export function copyLink(toolId, message) {
|
||||
copy(`${window.location.origin + getAppRoot()}root?tool_id=${toolId}`, message);
|
||||
}
|
||||
/** Visits tool inputs
|
||||
* @param{dict} inputs - Nested dictionary of input elements
|
||||
* @param{dict} callback - Called with the mapped dictionary object and corresponding model node
|
||||
*/
|
||||
export var visitInputs = (inputs, callback, prefix, context) => {
|
||||
context = Object.assign({}, context);
|
||||
_.each(inputs, (input) => {
|
||||
if (input && input.type && input.name) {
|
||||
context[input.name] = input;
|
||||
}
|
||||
});
|
||||
for (var key in inputs) {
|
||||
var node = inputs[key];
|
||||
node.name = node.name || key;
|
||||
var name = prefix ? `${prefix}|${node.name}` : node.name;
|
||||
switch (node.type) {
|
||||
case "repeat":
|
||||
_.each(node.cache, (cache, j) => {
|
||||
visitInputs(cache, callback, `${name}_${j}`, context);
|
||||
});
|
||||
break;
|
||||
case "conditional":
|
||||
if (node.test_param) {
|
||||
callback(node.test_param, `${name}|${node.test_param.name}`, context);
|
||||
var selectedCase = matchCase(node, node.test_param.value);
|
||||
if (selectedCase != -1) {
|
||||
visitInputs(node.cases[selectedCase].inputs, callback, name, context);
|
||||
} else {
|
||||
console.debug(`Form.utilities::visitInputs() - Invalid case for ${name}.`);
|
||||
}
|
||||
} else {
|
||||
console.debug(`Form.utilities::visitInputs() - Conditional test parameter missing for ${name}.`);
|
||||
}
|
||||
break;
|
||||
case "section":
|
||||
visitInputs(node.inputs, callback, name, context);
|
||||
break;
|
||||
default:
|
||||
callback(node, name, context);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export function downloadTool(toolId) {
|
||||
window.location.href = `${getAppRoot()}api/tools/${toolId}/download`;
|
||||
}
|
||||
|
||||
export function openLink(url) {
|
||||
window.open(url);
|
||||
}
|
||||
/** Match conditional values to selected cases
|
||||
* @param{dict} input - Definition of conditional input parameter
|
||||
* @param{dict} value - Current value
|
||||
*/
|
||||
export var matchCase = (input, value) => {
|
||||
if (input.test_param.type == "boolean") {
|
||||
if (value == "true") {
|
||||
value = input.test_param.truevalue || "true";
|
||||
} else {
|
||||
value = input.test_param.falsevalue || "false";
|
||||
}
|
||||
}
|
||||
for (var i in input.cases) {
|
||||
if (input.cases[i].value == value) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
<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="'Search Filter' | localize"
|
||||
data-description="filter text input"
|
||||
/>
|
||||
</DebouncedInput>
|
||||
|
||||
<b-input-group-append>
|
||||
@@ -10,6 +16,7 @@
|
||||
:pressed="showDeleted"
|
||||
:variant="showDeleted ? 'info' : 'secondary'"
|
||||
@click="showDeleted = !showDeleted"
|
||||
data-description="show deleted filter toggle"
|
||||
>
|
||||
{{ "Deleted" | localize }}
|
||||
</b-button>
|
||||
@@ -18,6 +25,7 @@
|
||||
:pressed="showHidden"
|
||||
:variant="showHidden ? 'info' : 'secondary'"
|
||||
@click="showHidden = !showHidden"
|
||||
data-description="show hidden filter toggle"
|
||||
>
|
||||
{{ "Hidden" | localize }}
|
||||
</b-button>
|
||||
|
||||
@@ -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,24 @@
|
||||
import Placeholder from "./Placeholder";
|
||||
import Dataset from "./Dataset";
|
||||
import DatasetCollection from "./DatasetCollection";
|
||||
// import SubDataset from "./SubDataset";
|
||||
import Subdataset from "./Subdataset";
|
||||
import Subcollection from "./Subcollection";
|
||||
|
||||
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 +38,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: () => ({
|
||||
@@ -66,9 +74,6 @@ export default {
|
||||
// override me
|
||||
return "Placeholder";
|
||||
},
|
||||
bindProps() {
|
||||
return { ...this.$props, ...this.$attrs };
|
||||
},
|
||||
},
|
||||
|
||||
created() {
|
||||
|
||||
@@ -1,159 +1,171 @@
|
||||
<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="writable && 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="writable && 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 },
|
||||
writable: { type: Boolean, default: true },
|
||||
},
|
||||
|
||||
data() {
|
||||
@@ -163,11 +175,6 @@ export default {
|
||||
};
|
||||
},
|
||||
|
||||
created() {
|
||||
// make available to template
|
||||
this.STATES = STATES;
|
||||
},
|
||||
|
||||
computed: {
|
||||
...mapGetters("user", ["currentUser"]),
|
||||
...mapGetters("config", ["config"]),
|
||||
@@ -180,10 +187,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 +203,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 +225,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 +285,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>
|
||||
|
||||
@@ -7,7 +7,8 @@ either through the props, and make updates through the events -->
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="dataset"
|
||||
class="dataset history-content"
|
||||
:id="typeId"
|
||||
:class="{ expanded, collapsed, selected }"
|
||||
:data-state="dataset.state"
|
||||
@keydown.arrow-left.self.stop="$emit('update:expanded', false)"
|
||||
@@ -15,72 +16,105 @@ 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 v-if="showSelection" :checked="selected" @change="$emit('update:selected', $event)" />
|
||||
<StatusIcon v-if="!ok" class="status-icon px-1" :state="dataset.state" @click.stop="onStatusClick" />
|
||||
<StateBtn
|
||||
v-if="!dataset.visible"
|
||||
<nav class="content-top-menu p-1 d-flex cursor-pointer" @click.stop="$emit('update:expanded', !expanded)">
|
||||
<div class="d-flex 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"
|
||||
/>
|
||||
|
||||
<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 title p-1 overflow-hidden">
|
||||
<h5 class="text-truncate" v-if="collapsed">
|
||||
<span class="hid" data-description="dataset hid">{{ dataset.hid }}</span>
|
||||
<span class="name" data-description="dataset 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" :writable="writable" v-on="$listeners" />
|
||||
</slot>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!--- read-only tags with name: prefix -->
|
||||
<div v-if="collapsed && dataset.nameTags.length" class="nametags p-1">
|
||||
<div v-if="collapsed && dataset.nameTags.length" class="nametags px-2 pb-2">
|
||||
<Nametag v-for="tag in dataset.nameTags" :key="tag" :tag="tag" />
|
||||
</div>
|
||||
|
||||
<!-- expanded view with editors -->
|
||||
<header v-if="expanded" class="p-2">
|
||||
<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 data-description="dataset name">{{ 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>
|
||||
|
||||
<StatelessTags class="mt-2" v-model="tags" />
|
||||
</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">
|
||||
<DatasetSummary :dataset="dataset" />
|
||||
<DatasetSummary :dataset="dataset" class="summary" />
|
||||
|
||||
<div class="display-applications" v-if="dataset.displayLinks.length">
|
||||
<div class="display-application" v-for="app in dataset.displayLinks" :key="app.label">
|
||||
@@ -96,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";
|
||||
import { StatelessTags } from "components/Tags";
|
||||
|
||||
export default {
|
||||
mixins: [legacyNavigationMixin],
|
||||
components: {
|
||||
ClickToEdit,
|
||||
Annotation,
|
||||
StatusIcon,
|
||||
StateBtn,
|
||||
DatasetMenu,
|
||||
DatasetSummary,
|
||||
ContentTags,
|
||||
Nametag,
|
||||
IconButton,
|
||||
EditorMenu,
|
||||
StatelessTags,
|
||||
},
|
||||
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: {
|
||||
@@ -140,8 +177,43 @@ export default {
|
||||
typeId() {
|
||||
return this.dataset.type_id;
|
||||
},
|
||||
ok() {
|
||||
return this.dataset.state == "ok";
|
||||
isEditing() {
|
||||
return this.writable && this.editing;
|
||||
},
|
||||
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 });
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
get() {
|
||||
return this.tempContent?.tags || [];
|
||||
},
|
||||
set(newTags) {
|
||||
if (Array.isArray(newTags)) {
|
||||
this.patchTempContent({ tags: newTags });
|
||||
}
|
||||
},
|
||||
},
|
||||
showSelectionBox() {
|
||||
return this.selectable && this.showSelection;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
@@ -154,6 +226,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,5 +1,5 @@
|
||||
<template>
|
||||
<component :is="summaryComponent" :dataset="dataset" class="summary" />
|
||||
<component :is="summaryComponent" :dataset="dataset" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
v-if="dsc"
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
class="dataset-collection"
|
||||
class="dataset-collection history-content"
|
||||
:dsc="dsc"
|
||||
@update:dsc="onUpdate"
|
||||
@delete="onDelete"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -5,14 +5,21 @@
|
||||
<div
|
||||
class="dataset dataset-collection collapsed"
|
||||
:class="{ selected }"
|
||||
:id="typedId"
|
||||
: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 v-if="showSelection" :checked="selected" @change="$emit('update:selected', $event)" />
|
||||
<nav class="content-top-menu p-1 d-flex cursor-pointer">
|
||||
<div class="d-flex 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'"
|
||||
@@ -21,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 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>
|
||||
@@ -66,7 +74,7 @@
|
||||
</nav>
|
||||
|
||||
<!--- read-only tags with name: prefix -->
|
||||
<div v-if="dsc.nameTags.length" class="nametags p-1">
|
||||
<div v-if="collapsed && dsc.nameTags.length" class="nametags px-2 pb-2">
|
||||
<Nametag v-for="tag in dsc.nameTags" :key="tag" :tag="tag" />
|
||||
</div>
|
||||
|
||||
@@ -76,29 +84,39 @@
|
||||
|
||||
<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() {
|
||||
console.log("onStatusClick", ...arguments);
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
typedId() {
|
||||
return this.dsc.type_id;
|
||||
},
|
||||
collapsed() {
|
||||
return !this.expanded;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
|
||||
@@ -2,43 +2,27 @@
|
||||
inside of other collections -->
|
||||
|
||||
<template>
|
||||
<div
|
||||
v-bind="$attrs"
|
||||
v-on="$listeners"
|
||||
class="collapsed"
|
||||
:data-state="state"
|
||||
@keydown.arrow-right.self.stop.prevent="$emit('viewCollection')"
|
||||
@click.stop="$emit('viewCollection')"
|
||||
>
|
||||
<nav class="content-top-menu d-flex align-items-center justify-content-between p-1">
|
||||
<h5 class="flex-grow-1 overflow-hidden mr-auto text-nowrap text-truncate">
|
||||
<span class="name">{{ dsc.name }}</span>
|
||||
<span class="description">
|
||||
({{ dsc.collectionType | localize }} {{ dsc.collectionCountDescription | localize }})
|
||||
</span>
|
||||
</h5>
|
||||
</nav>
|
||||
</div>
|
||||
<DscUI v-if="dsc" v-bind="$attrs" v-on="$listeners" class="dataset-collection history-content" :dsc="dsc" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { DatasetCollection } from "../model/DatasetCollection";
|
||||
import { STATES } from "../model";
|
||||
import DscUI from "./DatasetCollection/DscUI";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
DscUI,
|
||||
},
|
||||
props: {
|
||||
item: { type: Object, required: true },
|
||||
index: { type: Number, required: true },
|
||||
},
|
||||
computed: {
|
||||
dsc() {
|
||||
return new DatasetCollection(this.item);
|
||||
},
|
||||
state() {
|
||||
// TODO: see if there are situations where this rule won't work
|
||||
// subcollection doesn't have as much information as the collection
|
||||
// did, but it's probably ok
|
||||
return this.dsc.state || STATES.OK;
|
||||
// TODO: I'm sure this is not the right way to set the state, but the collection api
|
||||
// doesn't return any kind of state value at because the api is inconsistent
|
||||
// /api/dataset_collections/d071e794759ab192/contents/e144222fed44799a
|
||||
return new DatasetCollection({ ...this.item, populated_state: STATES.OK });
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
<!-- 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"
|
||||
@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>
|
||||
@@ -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,126 +1,187 @@
|
||||
<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"
|
||||
data-description="content filter toggle"
|
||||
/>
|
||||
|
||||
<PriorityMenuItem
|
||||
key="selection"
|
||||
title="Operations on multiple datasets"
|
||||
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"
|
||||
title="Copy Datasets"
|
||||
icon="fas fa-copy"
|
||||
@click="iframeRedirect('/dataset/copy_datasets')"
|
||||
/>
|
||||
<b-button-group>
|
||||
<b-dropdown
|
||||
class="history-contents-list-action-menu-btn"
|
||||
size="sm"
|
||||
text="Selection"
|
||||
:disabled="!hasSelection"
|
||||
>
|
||||
<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>
|
||||
|
||||
<PriorityMenuItem
|
||||
key="collapse-expanded"
|
||||
title="Collapse Expanded Datasets"
|
||||
icon="fas fa-compress-alt"
|
||||
@click="emit('collapseAllContent')"
|
||||
/>
|
||||
<b-dropdown-item aria-describedby="history-op-selected-content" v-b-modal:hide-selected-content>
|
||||
<span v-localize>Hide</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<PriorityMenuItem
|
||||
key="show-hidden-datasets"
|
||||
title="Unhide All Hidden Datasets"
|
||||
icon="fas fa-eye"
|
||||
@click="$bvModal.show('show-hidden-content')"
|
||||
/>
|
||||
<b-dropdown-item aria-describedby="history-op-selected-content" v-b-modal:show-selected-content>
|
||||
<span v-localize>Unhide</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<PriorityMenuItem
|
||||
key="delete-hidden"
|
||||
title="Delete Hidden Datasets"
|
||||
icon="fas fa-trash"
|
||||
@click="$bvModal.show('delete-hidden-content')"
|
||||
/>
|
||||
<b-dropdown-item aria-describedby="history-op-selected-content" v-b-modal:delete-selected-content>
|
||||
<span v-localize>Delete</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<PriorityMenuItem
|
||||
key="purge-hidden"
|
||||
title="Purge Hidden Datasets"
|
||||
icon="fas fa-burn"
|
||||
@click="$bvModal.show('purge-deleted-content')"
|
||||
/>
|
||||
</PriorityMenu>
|
||||
</header>
|
||||
<b-dropdown-item aria-describedby="history-op-selected-content" v-b-modal:restore-selected-content>
|
||||
<span v-localize>Undelete</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<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">
|
||||
<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-dropdown class="ml-auto" size="sm" text="With Selected" :disabled="!hasSelection">
|
||||
<b-dropdown-item @click="hideSelected">
|
||||
{{ "Hide Datasets" | localize }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item @click="unhideSelected">
|
||||
{{ "Unhide Datasets" | localize }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item @click="deleteSelected">
|
||||
{{ "Delete Datasets" | localize }}
|
||||
</b-dropdown-item>
|
||||
<b-dropdown-item @click="undeleteSelected">
|
||||
{{ "Undelete Datasets" | localize }}
|
||||
</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"
|
||||
data-description="new content menu"
|
||||
>
|
||||
<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')"
|
||||
data-description="copy datasets"
|
||||
>
|
||||
<span v-localize>Copy Datasets</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
aria-describedby="history-op-new-content"
|
||||
@click="buildDatasetList"
|
||||
data-description="build list"
|
||||
>
|
||||
<span v-localize>Build Dataset List</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
aria-describedby="history-op-new-content"
|
||||
@click="buildDatasetPair"
|
||||
data-description="build pair"
|
||||
>
|
||||
<span v-localize>Build Dataset Pair</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
aria-describedby="history-op-new-content"
|
||||
@click="buildListOfPairs"
|
||||
data-description="build list of pairs"
|
||||
>
|
||||
<span v-localize>Build List of Dataset Pairs</span>
|
||||
</b-dropdown-item>
|
||||
|
||||
<b-dropdown-item
|
||||
aria-describedby="history-op-new-content"
|
||||
@click="buildCollectionFromRules"
|
||||
data-description="build collection from rules"
|
||||
>
|
||||
<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 -->
|
||||
@@ -142,18 +203,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 },
|
||||
@@ -161,6 +220,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 {
|
||||
@@ -168,13 +229,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;
|
||||
@@ -184,8 +243,12 @@ export default {
|
||||
},
|
||||
},
|
||||
|
||||
numSelected() {
|
||||
return this.contentSelection.size || 0;
|
||||
},
|
||||
|
||||
hasSelection() {
|
||||
return this.contentSelection.size > 0;
|
||||
return this.numSelected > 0;
|
||||
},
|
||||
|
||||
countHidden() {
|
||||
@@ -193,22 +256,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
|
||||
@@ -233,7 +303,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");
|
||||
},
|
||||
@@ -257,8 +330,19 @@ export default {
|
||||
async buildNewCollection(collectionTypeCode) {
|
||||
const modalResult = await buildCollectionModal(collectionTypeCode, this.history.id, this.contentSelection);
|
||||
const newCollection = await createDatasetCollection(this.history, modalResult);
|
||||
|
||||
// cache the collection
|
||||
await cacheContent(newCollection);
|
||||
this.$emit("manualReload");
|
||||
|
||||
// have to hide the source items if that was requested
|
||||
if (modalResult.hide_source_items) {
|
||||
this.contentSelection.forEach(async (dataset) => {
|
||||
await cacheContent({ ...dataset, visible: false }, true);
|
||||
});
|
||||
this.$emit("resetSelection");
|
||||
}
|
||||
|
||||
// this.$emit("manualReload");
|
||||
},
|
||||
|
||||
// #endregion
|
||||
@@ -272,3 +356,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,54 +0,0 @@
|
||||
<template>
|
||||
<StatelessTags :value="tags" @before-adding-tag="saveTag" @before-deleting-tag="deleteTag" />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// TODO: rewrite tags with provider component instead of dependency injection
|
||||
|
||||
import { Content } from "./model";
|
||||
import { updateContentFields } from "./model/queries";
|
||||
import { cacheContent } from "./caching";
|
||||
|
||||
import { StatelessTags } from "components/Tags";
|
||||
import { createTag } from "components/Tags/model";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
StatelessTags,
|
||||
},
|
||||
props: {
|
||||
content: { type: Content, required: true },
|
||||
},
|
||||
computed: {
|
||||
tags() {
|
||||
return this.content.tags;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async saveTag({ tag, addTag }) {
|
||||
const newTag = createTag(tag);
|
||||
if (!newTag.valid) {
|
||||
return;
|
||||
}
|
||||
const tagSet = new Set(this.tags);
|
||||
tagSet.add(newTag.text);
|
||||
const tags = Array.from(tagSet);
|
||||
const ajaxResult = await updateContentFields(this.content, { tags });
|
||||
await cacheContent(ajaxResult);
|
||||
addTag(newTag);
|
||||
},
|
||||
async deleteTag({ tag, deleteTag }) {
|
||||
const doomedTag = createTag(tag);
|
||||
if (!doomedTag.valid) {
|
||||
return;
|
||||
}
|
||||
const tagSet = new Set([...this.tags]);
|
||||
tagSet.delete(doomedTag.text);
|
||||
const tags = Array.from(tagSet);
|
||||
const ajaxResult = await updateContentFields(this.content, { tags });
|
||||
await cacheContent(ajaxResult);
|
||||
deleteTag(tag);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
@@ -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,130 @@
|
||||
<!-- Edits a collection, if that collection is editable. -->
|
||||
|
||||
<template>
|
||||
<section class="history-details">
|
||||
<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" data-description="edit details">
|
||||
<div class="flex-grow-1">
|
||||
<h3 class="history-title" data-description="collection name display">
|
||||
{{ 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" @keydown.esc="revertAndCancel" data-description="edit form">
|
||||
<b-textarea
|
||||
v-model="dscName"
|
||||
placeholder="Collection Name"
|
||||
trim
|
||||
max-rows="4"
|
||||
data-description="name input"
|
||||
></b-textarea>
|
||||
<StatelessTags v-model="tags" class="mt-3 tags" />
|
||||
</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";
|
||||
import { StatelessTags } from "components/Tags";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
ClickToEdit,
|
||||
ContentTags,
|
||||
Nametag,
|
||||
EditorMenu,
|
||||
StatelessTags,
|
||||
},
|
||||
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 });
|
||||
},
|
||||
},
|
||||
tags: {
|
||||
get() {
|
||||
return this.tempContent?.tags || [];
|
||||
},
|
||||
set(newTags) {
|
||||
if (Array.isArray(newTags)) {
|
||||
this.patchTempDsc({ tags: newTags });
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
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();
|
||||
}
|
||||
},
|
||||
revertAndCancel() {
|
||||
this.revert();
|
||||
this.editing = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -8,11 +8,20 @@
|
||||
:get-item-key="(item) => item.type_id"
|
||||
v-slot="{ isExpanded, setExpanded }"
|
||||
>
|
||||
<Layout>
|
||||
<template v-slot:nav>
|
||||
<Layout class="dataset-collection-panel">
|
||||
<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,15 +32,20 @@
|
||||
: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)"
|
||||
:data-index="index"
|
||||
:data-row-key="rowKey"
|
||||
/>
|
||||
</template>
|
||||
</Scroller>
|
||||
@@ -55,6 +69,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 +84,7 @@ export default {
|
||||
Scroller,
|
||||
CollectionContentItem,
|
||||
ExpandedItems,
|
||||
IconButton,
|
||||
},
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
@@ -86,14 +102,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,48 @@
|
||||
<template>
|
||||
<div>
|
||||
<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"
|
||||
data-description="collection breadcrumbs menu"
|
||||
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" @click="close"> Back to: {{ history.name }} </b-button>
|
||||
<b-dropdown-item @click="close" data-description="back to history">
|
||||
<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
|
||||
class="back"
|
||||
size="sm"
|
||||
title="`Return to: ${history.name}`"
|
||||
@click="close"
|
||||
data-description="back to history"
|
||||
>
|
||||
<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 +56,6 @@ export default {
|
||||
});
|
||||
return options;
|
||||
},
|
||||
|
||||
downloadCollectionUrl() {
|
||||
let url = "";
|
||||
if (this.rootCollection) {
|
||||
url = `${this.rootCollection.url}/download`;
|
||||
}
|
||||
return url;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reselect(newList) {
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
<template>
|
||||
<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">
|
||||
<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>
|
||||
<HistoryPanel v-if="currentHistory" :history="currentHistory" v-on="handlers" id="current-history-panel">
|
||||
<template v-slot:globalNav>
|
||||
<HistoriesMenu
|
||||
v-on="handlers"
|
||||
:histories="histories"
|
||||
:current-history="currentHistory"
|
||||
title="Current History"
|
||||
/>
|
||||
</template>
|
||||
</HistoryPanel>
|
||||
|
||||
@@ -27,7 +23,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 +30,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,77 @@
|
||||
<template>
|
||||
<nav class="edit-controls">
|
||||
<IconButton
|
||||
:title="label('Edit') | l"
|
||||
class="edit-button mb-1"
|
||||
:pressed="editing"
|
||||
icon="pen"
|
||||
@click="$emit('update:editing', !editing)"
|
||||
data-description="editor toggle"
|
||||
/>
|
||||
|
||||
<template v-if="editing">
|
||||
<!-- save -->
|
||||
<IconButton
|
||||
:title="label('Save') | l"
|
||||
class="save-button mb-1"
|
||||
:class="{ disabled: !valid }"
|
||||
icon="save"
|
||||
@click="$emit('save')"
|
||||
data-description="editor save button"
|
||||
/>
|
||||
|
||||
<!-- revert -->
|
||||
<IconButton
|
||||
:title="label('Revert') | l"
|
||||
class="revert-changes-button mb-1"
|
||||
:class="{ disabled: !dirty }"
|
||||
icon="undo"
|
||||
@click="$emit('revert')"
|
||||
data-description="editor revert button"
|
||||
/>
|
||||
|
||||
<!-- form status -->
|
||||
<IconButton
|
||||
:title="statusLabel | l"
|
||||
class="status-button mb-1"
|
||||
:class="{ valid: valid, disabled: !valid }"
|
||||
:icon="valid ? 'check' : 'exclamation-triangle'"
|
||||
variant="link"
|
||||
data-description="editor status"
|
||||
/>
|
||||
</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,63 +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 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 data-description="create new history" @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"
|
||||
icon="fa fa-plus"
|
||||
@click="$emit('createNewHistory')"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="view-all-histories"
|
||||
title="View All Histories"
|
||||
icon="fa fa-columns"
|
||||
@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-sync"
|
||||
@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,16 +71,20 @@
|
||||
: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)"
|
||||
:selected="isSelected(item)"
|
||||
@update:selected="setSelected(item, $event)"
|
||||
@viewCollection="$emit('viewCollection', item)"
|
||||
:data-hid="item.hid"
|
||||
:data-index="index"
|
||||
:data-row-key="rowKey"
|
||||
/>
|
||||
</template>
|
||||
</Scroller>
|
||||
@@ -101,6 +111,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 +129,7 @@ export default {
|
||||
HistoryContentItem,
|
||||
ExpandedItems,
|
||||
SelectedItems,
|
||||
HistoryMenu,
|
||||
},
|
||||
props: {
|
||||
history: { type: History, required: true },
|
||||
|
||||
@@ -1,210 +1,154 @@
|
||||
<template>
|
||||
<header>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<h6 class="text-nowrap text-truncate mr-auto">
|
||||
<span class="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"
|
||||
icon="fas fa-tags"
|
||||
:pressed="showTags"
|
||||
@click="showTags = !showTags"
|
||||
/>
|
||||
<PriorityMenuItem
|
||||
key="edit-history-annotation"
|
||||
title="Edit Annotation"
|
||||
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="iframeRedirect('/history/export_archive?preview=True')"
|
||||
/>
|
||||
</PriorityMenu>
|
||||
<template>
|
||||
<section class="history-details d-flex" data-description="edit details">
|
||||
<div class="flex-grow-1 overflow-hidden">
|
||||
<h3 data-description="history name display">{{ historyName || "(History Name)" }}</h3>
|
||||
<h5 class="history-size">
|
||||
<span v-if="history.size">{{ history.size | niceFileSize }}</span>
|
||||
<span v-else>(empty)</span>
|
||||
</h5>
|
||||
|
||||
<!-- display title, annotation, tags -->
|
||||
<div v-if="!editing">
|
||||
<p data-description="history annotation" v-if="annotation" class="mt-1">{{ annotation }}</p>
|
||||
<StatelessTags class="mt-2 tags" :value="tags" :disabled="true" />
|
||||
</div>
|
||||
|
||||
<!-- edit form, change title, annotation, or tags -->
|
||||
<div v-else class="mt-3" @keydown.esc="revertAndCancel" data-description="edit form">
|
||||
<b-textarea
|
||||
class="mb-2"
|
||||
v-model="historyName"
|
||||
placeholder="History Name"
|
||||
trim
|
||||
max-rows="4"
|
||||
data-description="name input"
|
||||
></b-textarea>
|
||||
|
||||
<b-textarea
|
||||
class="mb-2"
|
||||
v-model="annotation"
|
||||
placeholder="Annotation (optional)"
|
||||
trim
|
||||
max-rows="4"
|
||||
data-description="annotation input"
|
||||
></b-textarea>
|
||||
|
||||
<StatelessTags class="mt-3 tags" v-model="tags" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- click to edit the name of the history -->
|
||||
<ClickToEdit
|
||||
class="history-title mt-2"
|
||||
tag-name="h3"
|
||||
v-model="historyName"
|
||||
:tooltip-title="'Rename history...' | localize"
|
||||
tooltip-placement="top"
|
||||
<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";
|
||||
import { StatelessTags } from "components/Tags";
|
||||
|
||||
export default {
|
||||
mixins: [legacyNavigationMixin],
|
||||
components: {
|
||||
PriorityMenu,
|
||||
PriorityMenuItem,
|
||||
HistoryTags,
|
||||
Annotation,
|
||||
ClickToEdit,
|
||||
CopyHistoryModal,
|
||||
EditorMenu,
|
||||
StatelessTags,
|
||||
},
|
||||
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 });
|
||||
},
|
||||
},
|
||||
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();
|
||||
}
|
||||
},
|
||||
revertAndCancel() {
|
||||
this.revert();
|
||||
this.editing = false;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import "scss/transitions.scss";
|
||||
</style>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<b-alert show>
|
||||
<h4 class="mb-1">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
<i class="fa fa-info-circle empty-message"></i>
|
||||
<span>{{ message | l }}</span>
|
||||
</h4>
|
||||
<p>
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
<template>
|
||||
<div>
|
||||
<b-dropdown
|
||||
size="sm"
|
||||
variant="link"
|
||||
text="Histories"
|
||||
toggle-class="text-decoration-none"
|
||||
data-description="history menu"
|
||||
>
|
||||
<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)" data-description="export to file">
|
||||
<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, this.STATES.RUNNING)) {
|
||||
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>
|
||||
|
||||
@@ -15,7 +15,7 @@ import LIST_OF_PAIRS_COLLECTION_CREATOR from "../../Collections/PairedListCollec
|
||||
import RULE_BASED_COLLECTION_CREATOR from "../../Collections/RuleBasedCollectionCreatorModal";
|
||||
|
||||
// stand-in for buildCollection from history-view-edit.js
|
||||
export async function buildCollectionModal(collectionType, history_id, selectedContent, hideSourceItems = false) {
|
||||
export async function buildCollectionModal(collectionType, history_id, selectedContent, hideSourceItems = true) {
|
||||
// select legacy function
|
||||
let createFunc;
|
||||
if (collectionType == "list") {
|
||||
|
||||
@@ -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$),
|
||||
|
||||
@@ -22,7 +22,7 @@ export const clearHistoryDateStore = async () => {
|
||||
*/
|
||||
// prettier-ignore
|
||||
export const loadHistoryContents = (cfg = {}) => (rawInputs$) => {
|
||||
const {
|
||||
const {
|
||||
noInitial = false,
|
||||
windowSize = SearchParams.pageSize
|
||||
} = cfg;
|
||||
@@ -39,7 +39,7 @@ export const loadHistoryContents = (cfg = {}) => (rawInputs$) => {
|
||||
return `${baseUrl}?${params.historyContentQueryString}`;
|
||||
}),
|
||||
map(prependPath),
|
||||
requestWithUpdateTime({ dateStore, noInitial, responseQualifier }),
|
||||
requestWithUpdateTime({ dateStore, noInitial, responseQualifier, dateFieldName: "since" }),
|
||||
);
|
||||
|
||||
const validResponses$ = ajaxResponse$.pipe(
|
||||
@@ -81,7 +81,7 @@ export const loadHistoryContents = (cfg = {}) => (rawInputs$) => {
|
||||
|
||||
const matches = areDefined(matchesUp, matchesDown) ? matchesUp + matchesDown : undefined;
|
||||
const totalMatches = areDefined(totalMatchesUp, totalMatchesDown) ? totalMatchesUp + totalMatchesDown : undefined;
|
||||
|
||||
|
||||
return {
|
||||
summary,
|
||||
matches,
|
||||
|
||||
@@ -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;
|
||||
})
|
||||
);
|
||||
@@ -25,7 +25,7 @@ export const requestWithUpdateTime = (config = {}) => {
|
||||
requestTime = moment.utc(),
|
||||
// indicates we don't want initial results
|
||||
noInitial = false,
|
||||
responseQualifier = () => true
|
||||
responseQualifier = (response) => response.status == 200
|
||||
} = config;
|
||||
|
||||
// mark and flag this update-time, append to next request with same base
|
||||
|
||||
@@ -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,29 +7,23 @@
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// number of contained contents
|
||||
// Need to do some manual handling, because once again, the API is
|
||||
// inconsistent, element_count not returned in the case of a pair. It
|
||||
// should be two, but we need to add that value in.
|
||||
get totalElements() {
|
||||
if ("element_count" in this) {
|
||||
return this.element_count;
|
||||
}
|
||||
|
||||
if (this.collection_type == "paired") {
|
||||
return 2;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -58,14 +52,34 @@ export class DatasetCollection extends Content {
|
||||
|
||||
// amalgam state value
|
||||
get state() {
|
||||
return this.jobSummary.state || this.populated_state;
|
||||
return this.jobSummary.state;
|
||||
}
|
||||
|
||||
get jobSummary() {
|
||||
return new JobStateSummary(this);
|
||||
return Object.freeze(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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,20 +1,13 @@
|
||||
/**
|
||||
* Read-only class takes raw data and applies some rules
|
||||
* to determine an amalgam state for a collection.
|
||||
*
|
||||
* Logic stolen from job-state-model.js and hdca-li.js
|
||||
*/
|
||||
import { STATES } from "./states";
|
||||
|
||||
// logic largely stolen from the butchered, degenerate mess at:
|
||||
// import nonsense from "mvc/history/job-states-model.js"
|
||||
|
||||
// Job-state-summary lists
|
||||
|
||||
const NON_TERMINAL_STATES = [STATES.NEW, STATES.WAITING, STATES.QUEUED, STATES.RUNNING];
|
||||
|
||||
const ERROR_STATES = [
|
||||
STATES.ERROR,
|
||||
STATES.DELETED, // does this exist?
|
||||
];
|
||||
const ERROR_STATES = [STATES.ERROR, STATES.DELETED];
|
||||
|
||||
export class JobStateSummary extends Map {
|
||||
constructor(dsc = {}) {
|
||||
@@ -23,7 +16,6 @@ export class JobStateSummary extends Map {
|
||||
this.populated_state = populated_state;
|
||||
}
|
||||
|
||||
// state element has value > 0
|
||||
hasJobs(key) {
|
||||
return this.has(key) ? this.get(key) > 0 : false;
|
||||
}
|
||||
@@ -33,48 +25,23 @@ export class JobStateSummary extends Map {
|
||||
return states.some((s) => this.hasJobs(s));
|
||||
}
|
||||
|
||||
// amalgam state, basically just for display purposes
|
||||
get state() {
|
||||
// FROM mvc/history/hdca-li.js
|
||||
//TODO: model currently has no state
|
||||
// var state;
|
||||
// var jobStatesSummary = this.model.jobStatesSummary;
|
||||
// if (jobStatesSummary) {
|
||||
// if (jobStatesSummary.new()) {
|
||||
// state = "loading";
|
||||
// } else if (jobStatesSummary.errored()) {
|
||||
// state = "error";
|
||||
// } else if (jobStatesSummary.terminal()) {
|
||||
// state = "ok";
|
||||
// } else if (jobStatesSummary.running()) {
|
||||
// state = "running";
|
||||
// } else {
|
||||
// state = "queued";
|
||||
// }
|
||||
// } else if (this.model.get("job_source_id")) {
|
||||
// // Initial rendering - polling will fill in more details in a bit.
|
||||
// state = "loading";
|
||||
// } else {
|
||||
// state = this.model.get("populated_state") ? STATES.OK : STATES.RUNNING;
|
||||
// }
|
||||
|
||||
if (this.jobCount) {
|
||||
if (this.isErrored) {
|
||||
return STATES.ERROR;
|
||||
}
|
||||
if (this.isNew) {
|
||||
return STATES.NEW;
|
||||
}
|
||||
if (this.isRunning) {
|
||||
return STATES.RUNNING;
|
||||
}
|
||||
if (this.isNew) {
|
||||
return STATES.LOADING;
|
||||
}
|
||||
if (this.isTerminal) {
|
||||
return STATES.OK;
|
||||
}
|
||||
return STATES.QUEUED;
|
||||
}
|
||||
|
||||
return null;
|
||||
return this.populated_state;
|
||||
}
|
||||
|
||||
// Flags
|
||||
@@ -84,11 +51,7 @@ export class JobStateSummary extends Map {
|
||||
}
|
||||
|
||||
get isErrored() {
|
||||
return this.anyHasJobs(...ERROR_STATES);
|
||||
}
|
||||
|
||||
get isRunning() {
|
||||
return this.hasJobs(STATES.RUNNING);
|
||||
return this.populated_state == STATES.ERROR || this.anyHasJobs(...ERROR_STATES);
|
||||
}
|
||||
|
||||
get isTerminal() {
|
||||
@@ -98,6 +61,10 @@ export class JobStateSummary extends Map {
|
||||
return !this.anyHasJobs(...NON_TERMINAL_STATES);
|
||||
}
|
||||
|
||||
get isRunning() {
|
||||
return this.hasJobs(STATES.RUNNING);
|
||||
}
|
||||
|
||||
// Counts
|
||||
|
||||
get jobCount() {
|
||||
@@ -109,6 +76,6 @@ export class JobStateSummary extends Map {
|
||||
}
|
||||
|
||||
get runningCount() {
|
||||
return this.get(STATES.ERRUNNINGROR);
|
||||
return this.get(STATES.RUNNING);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export class ScrollPos {
|
||||
constructor(props = {}) {
|
||||
const { cursor = 0.0, key = null } = props;
|
||||
this.cursor = cursor;
|
||||
const { cursor = null, key = null } = props;
|
||||
this.cursor = key === null && cursor === null ? 0 : cursor;
|
||||
this.key = key;
|
||||
}
|
||||
}
|
||||
@@ -12,5 +12,5 @@ ScrollPos.create = function (props) {
|
||||
};
|
||||
|
||||
ScrollPos.equals = function (a, b) {
|
||||
return a.cursor == b.cursor && a.key == b.key;
|
||||
return a.cursor === b.cursor && a.key === b.key;
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
@@ -352,7 +359,7 @@ export async function createDatasetCollection(history, inputs = {}) {
|
||||
copy_elements: true,
|
||||
name: "list",
|
||||
element_identifiers: [],
|
||||
hide_source_items: "True",
|
||||
hide_source_items: true,
|
||||
type: "dataset_collection",
|
||||
};
|
||||
|
||||
|
||||
@@ -33,6 +33,8 @@ export const STATES = {
|
||||
// found in job-state summary model?
|
||||
// this an actual state value or something derived from deleted prop?
|
||||
DELETED: "deleted",
|
||||
|
||||
LOADING: "loading",
|
||||
};
|
||||
|
||||
export const NOT_READY_STATES = [STATES.UPLOAD, STATES.QUEUED, STATES.RUNNING, STATES.SETTING_METADATA, STATES.NEW];
|
||||
|
||||
+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 }),
|
||||
|
||||
@@ -83,7 +83,7 @@ export const ContentProvider = {
|
||||
|
||||
methods: {
|
||||
resetScrollPos() {
|
||||
this.setScrollPos(ScrollPos.create());
|
||||
this.setScrollPos();
|
||||
},
|
||||
|
||||
initStreams() {
|
||||
@@ -96,8 +96,8 @@ export const ContentProvider = {
|
||||
* cursor (0-1 value representing how far down it is)
|
||||
* @param {object} Object consisting of key and cursor props
|
||||
*/
|
||||
setScrollPos({ cursor = 0.0, key = null } = {}) {
|
||||
if (isValidNumber(cursor)) {
|
||||
setScrollPos({ cursor = null, key = null } = {}) {
|
||||
if (isValidNumber(cursor) || key !== null) {
|
||||
this.scrollPos = ScrollPos.create({ cursor, key });
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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";
|
||||
@@ -38,11 +38,6 @@ export const aggregateCacheUpdates = (monitor, cfg = {}) => (src$) => {
|
||||
keyField = "hid",
|
||||
keyDirection = SEEK.DESC,
|
||||
|
||||
// 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),
|
||||
|
||||
// reduces duplicate updates to the skiplist
|
||||
// which can be expensive
|
||||
updateGroupLifetime = 2 * debouncePeriod,
|
||||
@@ -59,7 +54,6 @@ 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),
|
||||
distinctUntilChanged(),
|
||||
show(debug, (key) => console.log("monitorInputKey", key)),
|
||||
);
|
||||
@@ -76,10 +70,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 +98,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";
|
||||
@@ -35,7 +35,6 @@ export function processContentStreams(payloadOperator, sources = {}, settings =
|
||||
show(debug, (params) => console.log('processContentStreams: params changed', params)),
|
||||
);
|
||||
const pos$ = sources.scrollPos$.pipe(
|
||||
debounceTime(debouncePeriod),
|
||||
show(debug, (pos) => console.log('processContentStreams: pos changed', pos)),
|
||||
distinctUntilChanged(ScrollPos.equals),
|
||||
);
|
||||
@@ -48,7 +47,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,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
+1
-1
@@ -148,7 +148,7 @@ describe("HistoryContentProvider", () => {
|
||||
|
||||
test("should show results when we pick a HID by scroller height", async () => {
|
||||
// set scroller to non-existent hid
|
||||
wrapper.vm.setScrollPos({ cursor: 0.52323, key: null });
|
||||
wrapper.vm.setScrollPos({ cursor: 0.52323 });
|
||||
const scrollPayload = await payloadChange({ vm: wrapper.vm });
|
||||
const { contents, startKey, topRows, bottomRows, totalMatches } = scrollPayload;
|
||||
const hids = payloadHids(scrollPayload);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { concat, race, timer, merge, partition, Observable, BehaviorSubject, Subject } from "rxjs";
|
||||
import { combineLatest, concat, race, timer, merge, partition, Observable, BehaviorSubject, Subject } from "rxjs";
|
||||
import {
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
@@ -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
|
||||
@@ -64,12 +65,15 @@ export const contentPayload = (cfg = {}) => {
|
||||
pluck('key')
|
||||
);
|
||||
|
||||
// only have cursor height, need to make a guess
|
||||
const estimatedHid$ = noKey$.pipe(
|
||||
const cursor$ = noKey$.pipe(
|
||||
pluck('cursor'),
|
||||
withLatestFrom(cursorToHid$),
|
||||
);
|
||||
|
||||
// only have cursor height, need to make a guess
|
||||
const estimatedHid$ = combineLatest(cursor$, cursorToHid$).pipe(
|
||||
map((inputs) => estimateHid(...inputs)),
|
||||
map(hid => Math.round(hid)),
|
||||
distinctUntilChanged(),
|
||||
show(debug, hid => console.log("estimatedHid", hid, history)),
|
||||
);
|
||||
|
||||
@@ -78,14 +82,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 +114,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 +138,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
|
||||
@@ -147,18 +152,13 @@ export const contentPayload = (cfg = {}) => {
|
||||
map(([[lastResponse, response], pos, hid]) => {
|
||||
const updatesAtTop = response.maxHid > lastResponse.maxHid;
|
||||
|
||||
const scrollerInDefault = (pos.cursor == 0 && pos.key == null);
|
||||
const scrollerExactlyAtTop = pos.cursor === 0 || pos.key === lastResponse.maxHid;
|
||||
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);
|
||||
// console.groupEnd();
|
||||
const scrollNearLastTop = pos.key !== null ? Math.abs(pos.key - lastResponse.maxContentHid) < fudge : false;
|
||||
const scrollerAtTop = scrollerExactlyAtTop || scrollNearLastTop;
|
||||
|
||||
if (updatesAtTop && scrollerAtTop) {
|
||||
return new ScrollPos({ cursor: 0, key: response.maxHid })
|
||||
return ScrollPos.create({ cursor: 0, key: response.maxContentHid });
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
@@ -190,7 +190,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
|
||||
@@ -65,7 +69,7 @@ export default {
|
||||
|
||||
// currently selected history object, should be a full object not just a summary
|
||||
currentHistory: this.currentHistoryModel,
|
||||
currentHistoryId: this.currentHistoryModel?.id || null,
|
||||
currentHistoryId: this.currentHistoryId,
|
||||
|
||||
handlers: {
|
||||
// Updates the history in the store without a trip to the server, in the event that a
|
||||
@@ -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,22 @@
|
||||
export function saveSet(key, setObject) {
|
||||
try {
|
||||
const setToArray = Array.from(setObject);
|
||||
const json = JSON.stringify(setToArray);
|
||||
sessionStorage.setItem(key, json);
|
||||
} catch (err) {
|
||||
console.warn("Error saving set to cache", key, 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.warn("Error loading cached list", key, err);
|
||||
sessionStorage.removeItem(key);
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -220,10 +220,10 @@
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="row.item.can_manage && !row.item.deleted"
|
||||
@click="navigateToPermission(row.item)"
|
||||
size="sm"
|
||||
class="lib-btn permission_lib_btn"
|
||||
:title="`Permissions of ${row.item.name}`"
|
||||
:to="{ path: `${navigateToPermission(row.item)}` }"
|
||||
>
|
||||
<font-awesome-icon icon="users" />
|
||||
Manage
|
||||
@@ -241,17 +241,26 @@
|
||||
</div>
|
||||
</template>
|
||||
</b-table>
|
||||
<!-- hide pagination if the table is loading-->
|
||||
<b-container>
|
||||
<b-row class="justify-content-md-center">
|
||||
<b-row align-v="center" class="justify-content-md-center">
|
||||
<b-col md="auto">
|
||||
<div v-if="isBusy">
|
||||
<b-spinner small type="grow"></b-spinner>
|
||||
<b-spinner small type="grow"></b-spinner>
|
||||
<b-spinner small type="grow"></b-spinner>
|
||||
</div>
|
||||
<b-pagination
|
||||
v-model="currentPage"
|
||||
v-else
|
||||
:value="currentPage"
|
||||
:total-rows="total_rows"
|
||||
:per-page="perPage"
|
||||
@input="changePage"
|
||||
aria-controls="folder_list_body"
|
||||
>
|
||||
</b-pagination>
|
||||
</b-col>
|
||||
|
||||
<b-col cols="1.5">
|
||||
<table>
|
||||
<tr>
|
||||
@@ -304,7 +313,6 @@ function initialFolderState() {
|
||||
include_deleted: false,
|
||||
search_text: "",
|
||||
isAllSelectedMode: false,
|
||||
currentPage: 1,
|
||||
};
|
||||
}
|
||||
export default {
|
||||
@@ -313,6 +321,11 @@ export default {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
page: {
|
||||
type: Number,
|
||||
default: 1,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
FolderTopBar,
|
||||
@@ -323,6 +336,7 @@ export default {
|
||||
return {
|
||||
...initialFolderState(),
|
||||
...{
|
||||
currentPage: null,
|
||||
current_folder_id: null,
|
||||
error: null,
|
||||
isBusy: false,
|
||||
@@ -338,11 +352,12 @@ export default {
|
||||
},
|
||||
created() {
|
||||
this.services = new Services({ root: this.root });
|
||||
this.initFolder(this.folder_id);
|
||||
this.getFolder(this.folder_id, this.page);
|
||||
},
|
||||
methods: {
|
||||
initFolder(folder_id) {
|
||||
getFolder(folder_id, page) {
|
||||
this.current_folder_id = folder_id;
|
||||
this.currentPage = page;
|
||||
this.resetData();
|
||||
this.fetchFolderContents(this.include_deleted);
|
||||
},
|
||||
@@ -486,9 +501,9 @@ export default {
|
||||
},
|
||||
navigateToPermission(element) {
|
||||
if (element.type === "file") {
|
||||
this.$router.push({ path: `${this.folder_id}/dataset/${element.id}/permissions` });
|
||||
return `/folders/${this.folder_id}/dataset/${element.id}/permissions`;
|
||||
} else if (element.type === "folder") {
|
||||
this.$router.push({ path: `${element.id}/permissions` });
|
||||
return `/folders/${element.id}/permissions`;
|
||||
}
|
||||
},
|
||||
getMessage(element) {
|
||||
@@ -581,6 +596,9 @@ export default {
|
||||
);
|
||||
}
|
||||
},
|
||||
changePage(page) {
|
||||
this.$router.push({ name: `LibraryFolder`, params: { folder_id: this.folder_id, page: page } });
|
||||
},
|
||||
|
||||
/*
|
||||
Former Backbone code, adopted to work with Vue
|
||||
@@ -624,11 +642,6 @@ export default {
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentPage: {
|
||||
handler: function (value) {
|
||||
this.fetchFolderContents(this.include_deleted);
|
||||
},
|
||||
},
|
||||
perPage: {
|
||||
handler: function (value) {
|
||||
this.fetchFolderContents(this.include_deleted);
|
||||
@@ -636,7 +649,7 @@ export default {
|
||||
},
|
||||
},
|
||||
beforeRouteUpdate(to, from, next) {
|
||||
this.initFolder(to.params.folder_id);
|
||||
this.getFolder(to.params.folder_id, to.params.page);
|
||||
next();
|
||||
},
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<PermissionsHeader v-if="folder" :name="folder.name" :path="`/folders/${this.folder.parent_id}`" />
|
||||
<PermissionsHeader v-if="folder" :name="folder.name" />
|
||||
<b-container fluid>
|
||||
<div class="dataset_table">
|
||||
<h2 class="text-center">Folder permissions</h2>
|
||||
|
||||
@@ -24,11 +24,19 @@ export default new VueRouter({
|
||||
component: LibraryPermissions,
|
||||
props: true,
|
||||
},
|
||||
// redirect to the 1st page
|
||||
{ path: "/folders/:folder_id", redirect: "/folders/:folder_id/page/1" },
|
||||
{
|
||||
path: "/folders/:folder_id",
|
||||
path: "/folders/:folder_id/page/:page",
|
||||
name: "LibraryFolder",
|
||||
component: LibraryFolder,
|
||||
props: true,
|
||||
props(route) {
|
||||
const props = { ...route.params };
|
||||
if (props.page) {
|
||||
props.page = +props.page;
|
||||
}
|
||||
return props;
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/folders/:folder_id/permissions",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<PermissionsHeader v-if="library" :name="library.name" path="/" />
|
||||
<PermissionsHeader v-if="library" :name="library.name" />
|
||||
<h2 class="text-center">Library permissions</h2>
|
||||
<PermissionsInputField
|
||||
v-if="access_library_role_list"
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="name">
|
||||
<b-button variant="primary" title="back to parent folder" :to="{ path: `${this.path}` }">
|
||||
<font-awesome-icon icon="angle-double-left" />
|
||||
</b-button>
|
||||
<div>
|
||||
<div class="header text-center">{{ name }}</div>
|
||||
</div>
|
||||
@@ -15,8 +12,7 @@
|
||||
|
||||
<script>
|
||||
import { getGalaxyInstance } from "app";
|
||||
import LibraryPermissionsWarning from "components/Libraries/LibraryFolder/LibraryFolderPermissions/LibraryPermissionsWarning.vue";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import LibraryPermissionsWarning from "components/Libraries/LibraryFolder/LibraryFolderPermissions/LibraryPermissionsWarning";
|
||||
|
||||
export default {
|
||||
props: {
|
||||
@@ -24,14 +20,9 @@ export default {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
components: {
|
||||
LibraryPermissionsWarning,
|
||||
FontAwesomeIcon,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
+70
-31
@@ -1,45 +1,69 @@
|
||||
<template>
|
||||
<div class="w-100 p-2">
|
||||
<b-card body-class="p-0">
|
||||
<b-card-header>
|
||||
<span class="float-right">
|
||||
<b-button
|
||||
:href="downloadUrl"
|
||||
variant="link"
|
||||
size="sm"
|
||||
role="button"
|
||||
title="Download Collection"
|
||||
type="button"
|
||||
class="py-0 px-1"
|
||||
v-b-tooltip.hover
|
||||
>
|
||||
<span class="fa fa-download" />
|
||||
</b-button>
|
||||
</span>
|
||||
<span>
|
||||
<span>Dataset Collection:</span>
|
||||
<span class="font-weight-light">{{ collectionName }}</span>
|
||||
</span>
|
||||
</b-card-header>
|
||||
<b-card-body>
|
||||
<LoadingSpan v-if="loading" message="Loading Collection" />
|
||||
<div v-else class="content-height">
|
||||
<CollectionTree :node="itemContent" :skip-head="true" />
|
||||
</div>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</div>
|
||||
<b-card body-class="p-0">
|
||||
<b-card-header>
|
||||
<span class="float-right">
|
||||
<b-button
|
||||
:href="downloadUrl"
|
||||
variant="link"
|
||||
size="sm"
|
||||
role="button"
|
||||
title="Download Collection"
|
||||
type="button"
|
||||
class="py-0 px-1"
|
||||
v-b-tooltip.hover
|
||||
>
|
||||
<span class="fa fa-download" />
|
||||
</b-button>
|
||||
<CurrentUser v-slot="{ user }">
|
||||
<UserHistories v-if="user" :user="user" v-slot="{ currentHistoryId }">
|
||||
<b-button
|
||||
v-if="currentHistoryId"
|
||||
@click="onCopyCollection(currentHistoryId)"
|
||||
href="#"
|
||||
role="button"
|
||||
variant="link"
|
||||
title="Import Collection"
|
||||
type="button"
|
||||
class="py-0 px-1"
|
||||
v-b-tooltip.hover
|
||||
>
|
||||
<span class="fa fa-file-import" />
|
||||
</b-button>
|
||||
</UserHistories>
|
||||
</CurrentUser>
|
||||
</span>
|
||||
<span>
|
||||
<span>Dataset Collection:</span>
|
||||
<span class="font-weight-light">{{ collectionName }}</span>
|
||||
</span>
|
||||
</b-card-header>
|
||||
<b-card-body>
|
||||
<LoadingSpan v-if="loading" message="Loading Collection" />
|
||||
<div v-else class="content-height">
|
||||
<b-alert v-if="!!messageText" :variant="messageVariant" show>
|
||||
{{ messageText }}
|
||||
</b-alert>
|
||||
<CollectionTree :node="itemContent" :skip-head="true" />
|
||||
</div>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import axios from "axios";
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import CollectionTree from "./CollectionTree";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import CurrentUser from "components/providers/CurrentUser";
|
||||
import UserHistories from "components/History/providers/UserHistories";
|
||||
import { copyCollection } from "components/Markdown/services";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
CollectionTree,
|
||||
CurrentUser,
|
||||
LoadingSpan,
|
||||
UserHistories,
|
||||
},
|
||||
props: {
|
||||
args: {
|
||||
@@ -55,6 +79,8 @@ export default {
|
||||
return {
|
||||
itemContent: null,
|
||||
loading: true,
|
||||
messageText: null,
|
||||
messageVariant: null,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
@@ -78,6 +104,19 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onCopyCollection(currentHistoryId) {
|
||||
const hdcaId = this.args.history_dataset_collection_id;
|
||||
copyCollection(hdcaId, currentHistoryId).then(
|
||||
(response) => {
|
||||
this.messageVariant = "success";
|
||||
this.messageText = "Successfully copied to current history.";
|
||||
},
|
||||
(error) => {
|
||||
this.messageVariant = "danger";
|
||||
this.messageText = error;
|
||||
}
|
||||
);
|
||||
},
|
||||
async getContent() {
|
||||
try {
|
||||
const response = await axios.get(this.itemUrl);
|
||||
|
||||
@@ -1,58 +1,91 @@
|
||||
<template>
|
||||
<div class="w-100 p-2">
|
||||
<b-card body-class="p-0">
|
||||
<b-card-header v-if="!embedded">
|
||||
<span class="float-right">
|
||||
<b-button
|
||||
:href="downloadUrl"
|
||||
variant="link"
|
||||
size="sm"
|
||||
role="button"
|
||||
title="Download Dataset"
|
||||
type="button"
|
||||
class="py-0 px-1"
|
||||
v-b-tooltip.hover
|
||||
>
|
||||
<span class="fa fa-download" />
|
||||
</b-button>
|
||||
<b-button
|
||||
:href="importUrl"
|
||||
role="button"
|
||||
variant="link"
|
||||
title="Import Dataset"
|
||||
type="button"
|
||||
class="py-0 px-1"
|
||||
v-b-tooltip.hover
|
||||
>
|
||||
<span class="fa fa-file-import" />
|
||||
</b-button>
|
||||
</span>
|
||||
<span>
|
||||
<span>Dataset:</span>
|
||||
<span class="font-weight-light">{{ datasetName }}</span>
|
||||
</span>
|
||||
</b-card-header>
|
||||
<b-card-body>
|
||||
<b-card body-class="p-0">
|
||||
<b-card-header v-if="!embedded">
|
||||
<span class="float-right">
|
||||
<b-button
|
||||
:href="downloadUrl"
|
||||
variant="link"
|
||||
size="sm"
|
||||
role="button"
|
||||
title="Download Dataset"
|
||||
type="button"
|
||||
class="py-0 px-1"
|
||||
v-b-tooltip.hover
|
||||
>
|
||||
<span class="fa fa-download" />
|
||||
</b-button>
|
||||
<b-button
|
||||
:href="importUrl"
|
||||
role="button"
|
||||
variant="link"
|
||||
title="Import Dataset"
|
||||
type="button"
|
||||
class="py-0 px-1"
|
||||
v-b-tooltip.hover
|
||||
>
|
||||
<span class="fa fa-file-import" />
|
||||
</b-button>
|
||||
</span>
|
||||
<span>
|
||||
<span>Dataset:</span>
|
||||
<span class="font-weight-light">{{ datasetName }}</span>
|
||||
</span>
|
||||
</b-card-header>
|
||||
<b-card-body>
|
||||
<UrlDataProvider :url="itemUrl" v-slot="{ result: itemContent, loading, error }">
|
||||
<LoadingSpan v-if="loading" message="Loading Dataset" />
|
||||
<div v-else-if="error">{{ error }}</div>
|
||||
<div v-else class="embedded-dataset content-height">
|
||||
<pre v-if="itemContent.item_data">
|
||||
<code class="text-normalwrap">{{ itemContent.item_data }}</code>
|
||||
</pre>
|
||||
<div v-else>No content found.</div>
|
||||
<b-link v-if="itemContent.truncated" :href="itemContent.item_url"> Show More... </b-link>
|
||||
<UrlDataProvider :url="datatypesUrl" v-slot="{ result: datatypesModel, loading: datatypesLoading }">
|
||||
<LoadingSpan v-if="datatypesLoading" message="Loading Datatypes" />
|
||||
<div v-else>
|
||||
<b-embed
|
||||
v-if="isSubTypeOfAny(datasetType, ['pdf', 'html'], datatypesModel)"
|
||||
type="iframe"
|
||||
aspect="16by9"
|
||||
:src="displayUrl"
|
||||
/>
|
||||
<div v-else-if="itemContent.item_data">
|
||||
<div v-if="isSubTypeOfAny(datasetType, ['tabular'], datatypesModel)">
|
||||
<UrlDataProvider
|
||||
:url="metaUrl"
|
||||
v-slot="{ result: metaData, loading: metaLoading, error: metaError }"
|
||||
>
|
||||
<LoadingSpan v-if="metaLoading" message="Loading Metadata" />
|
||||
<div v-else-if="metaError">{{ metaError }}</div>
|
||||
<b-table
|
||||
v-else
|
||||
striped
|
||||
hover
|
||||
:fields="getFields(metaData)"
|
||||
:items="getItems(itemContent.item_data, metaData)"
|
||||
/>
|
||||
</UrlDataProvider>
|
||||
</div>
|
||||
<pre v-else>
|
||||
<code class="text-normalwrap">{{ itemContent.item_data }}</code>
|
||||
</pre>
|
||||
</div>
|
||||
<div v-else>No content found.</div>
|
||||
<b-link v-if="itemContent.truncated" :href="itemContent.item_url"> Show More... </b-link>
|
||||
</div>
|
||||
</UrlDataProvider>
|
||||
</div>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</div>
|
||||
</UrlDataProvider>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { getAppRoot } from "onload/loadConfig";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import axios from "axios";
|
||||
import { UrlDataProvider } from "components/providers/UrlDataProvider";
|
||||
import { DatatypesMapperModel } from "components/Datatypes/model";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
LoadingSpan,
|
||||
UrlDataProvider,
|
||||
},
|
||||
props: {
|
||||
args: {
|
||||
@@ -68,47 +101,81 @@ export default {
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
itemContent: null,
|
||||
loading: true,
|
||||
};
|
||||
},
|
||||
created() {
|
||||
this.getContent().then((data) => {
|
||||
this.itemContent = data;
|
||||
this.loading = false;
|
||||
});
|
||||
},
|
||||
computed: {
|
||||
datasetType() {
|
||||
const dataset = this.datasets[this.args.history_dataset_id];
|
||||
return dataset.ext;
|
||||
},
|
||||
datasetName() {
|
||||
const dataset = this.datasets[this.args.history_dataset_id];
|
||||
return dataset && dataset.name;
|
||||
},
|
||||
datatypesUrl() {
|
||||
return "api/datatypes/types_and_mapping";
|
||||
},
|
||||
downloadUrl() {
|
||||
return `${getAppRoot()}dataset/display?dataset_id=${this.args.history_dataset_id}`;
|
||||
},
|
||||
displayUrl() {
|
||||
return `${getAppRoot()}datasets/${this.args.history_dataset_id}/display`;
|
||||
},
|
||||
importUrl() {
|
||||
return `${getAppRoot()}dataset/imp?dataset_id=${this.args.history_dataset_id}`;
|
||||
},
|
||||
itemUrl() {
|
||||
return `${getAppRoot()}api/datasets/${this.args.history_dataset_id}/get_content_as_text`;
|
||||
return `api/datasets/${this.args.history_dataset_id}/get_content_as_text`;
|
||||
},
|
||||
metaUrl() {
|
||||
return `api/datasets/${this.args.history_dataset_id}`;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async getContent() {
|
||||
try {
|
||||
const response = await axios.get(this.itemUrl);
|
||||
return response.data;
|
||||
} catch (e) {
|
||||
return `Failed to retrieve content. ${e}`;
|
||||
isSubTypeOfAny(child, parents, datatypesModel) {
|
||||
const datatypesMapper = new DatatypesMapperModel(datatypesModel);
|
||||
return datatypesMapper.isSubTypeOfAny(child, parents);
|
||||
},
|
||||
getFields(metaData) {
|
||||
const fields = [];
|
||||
const columnNames = metaData.metadata_column_names || [];
|
||||
const columnCount = metaData.metadata_columns;
|
||||
for (let i = 0; i < columnCount; i++) {
|
||||
fields.push({
|
||||
key: `${i}`,
|
||||
label: columnNames[i] || i,
|
||||
sortable: true,
|
||||
});
|
||||
}
|
||||
return fields;
|
||||
},
|
||||
getItems(textData, metaData) {
|
||||
const tableData = [];
|
||||
const delimiter = metaData.metadata_delimiter || "\t";
|
||||
const comments = metaData.metadata_comment_lines || 0;
|
||||
const lines = textData.split("\n");
|
||||
lines.forEach((line, i) => {
|
||||
if (i >= comments) {
|
||||
const tabs = line.split(delimiter);
|
||||
const rowData = {};
|
||||
let hasData = false;
|
||||
tabs.forEach((cellData, j) => {
|
||||
const cellDataTrimmed = cellData.trim();
|
||||
if (cellDataTrimmed) {
|
||||
hasData = true;
|
||||
}
|
||||
rowData[j] = cellDataTrimmed;
|
||||
});
|
||||
if (hasData) {
|
||||
tableData.push(rowData);
|
||||
}
|
||||
}
|
||||
});
|
||||
return tableData;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped>
|
||||
.content-height {
|
||||
max-height: 15rem;
|
||||
max-height: 20rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<template>
|
||||
<div class="w-100 p-2">
|
||||
<b-card nobody>
|
||||
<JobMetrics class="job-metrics" :job-id="args.job_id" />
|
||||
</b-card>
|
||||
</div>
|
||||
<b-card nobody>
|
||||
<JobMetrics class="job-metrics" :job-id="args.job_id" />
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
<template>
|
||||
<div class="w-100 p-2">
|
||||
<b-card nobody>
|
||||
<JobParameters class="job-parameters" :job-id="args.job_id" :param="args.param" :include-title="false" />
|
||||
</b-card>
|
||||
</div>
|
||||
<b-card nobody>
|
||||
<JobParameters class="job-parameters" :job-id="args.job_id" :param="args.param" :include-title="false" />
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<template>
|
||||
<div class="w-100 p-2">
|
||||
<b-card nobody class="content-height">
|
||||
<div :class="name" :job_id="args.job_id">
|
||||
<pre><code class="text-normalwrap">{{ jobContent }}</code></pre>
|
||||
</div>
|
||||
</b-card>
|
||||
</div>
|
||||
<b-card nobody class="content-height">
|
||||
<div :class="name" :job_id="args.job_id">
|
||||
<pre><code class="text-normalwrap">{{ jobContent }}</code></pre>
|
||||
</div>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
<template>
|
||||
<div class="w-100 p-2">
|
||||
<b-card body-class="embed-responsive embed-responsive-4by3">
|
||||
<LoadingSpan v-if="loading" class="m-2" message="Loading Visualization" />
|
||||
<div v-else-if="error" class="m-2">{{ error }}</div>
|
||||
<iframe v-else class="embed-responsive-item" :src="visualizationUrl" />
|
||||
</b-card>
|
||||
</div>
|
||||
<b-card body-class="embed-responsive embed-responsive-4by3">
|
||||
<LoadingSpan v-if="loading" class="m-2" message="Loading Visualization" />
|
||||
<div v-else-if="error" class="m-2">{{ error }}</div>
|
||||
<iframe v-else class="embed-responsive-item" :src="visualizationUrl" />
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -1,48 +1,46 @@
|
||||
<template>
|
||||
<div class="w-100 p-2">
|
||||
<b-card body-class="p-0">
|
||||
<b-card-header v-if="!embedded">
|
||||
<span class="float-right">
|
||||
<b-button
|
||||
:href="downloadUrl"
|
||||
variant="link"
|
||||
size="sm"
|
||||
role="button"
|
||||
title="Download Workflow"
|
||||
type="button"
|
||||
class="py-0 px-1"
|
||||
v-b-tooltip.hover
|
||||
>
|
||||
<span class="fa fa-download" />
|
||||
</b-button>
|
||||
<b-button
|
||||
:href="importUrl"
|
||||
role="button"
|
||||
variant="link"
|
||||
title="Import Workflow"
|
||||
type="button"
|
||||
class="py-0 px-1"
|
||||
v-b-tooltip.hover
|
||||
>
|
||||
<span class="fa fa-file-import" />
|
||||
</b-button>
|
||||
</span>
|
||||
<span>
|
||||
<span>Workflow:</span>
|
||||
<span class="font-weight-light">{{ workflowName }}</span>
|
||||
</span>
|
||||
</b-card-header>
|
||||
<b-card-body>
|
||||
<LoadingSpan v-if="loading" message="Loading Workflow" />
|
||||
<div v-else class="content-height">
|
||||
<div v-for="step in itemContent.steps" :key="step.order_index" class="mb-2">
|
||||
<div>Step {{ step.order_index + 1 }}: {{ step.label }}</div>
|
||||
<WorkflowTree :input="step" :skip-head="true" />
|
||||
</div>
|
||||
<b-card body-class="p-0">
|
||||
<b-card-header v-if="!embedded">
|
||||
<span class="float-right">
|
||||
<b-button
|
||||
:href="downloadUrl"
|
||||
variant="link"
|
||||
size="sm"
|
||||
role="button"
|
||||
title="Download Workflow"
|
||||
type="button"
|
||||
class="py-0 px-1"
|
||||
v-b-tooltip.hover
|
||||
>
|
||||
<span class="fa fa-download" />
|
||||
</b-button>
|
||||
<b-button
|
||||
:href="importUrl"
|
||||
role="button"
|
||||
variant="link"
|
||||
title="Import Workflow"
|
||||
type="button"
|
||||
class="py-0 px-1"
|
||||
v-b-tooltip.hover
|
||||
>
|
||||
<span class="fa fa-file-import" />
|
||||
</b-button>
|
||||
</span>
|
||||
<span>
|
||||
<span>Workflow:</span>
|
||||
<span class="font-weight-light">{{ workflowName }}</span>
|
||||
</span>
|
||||
</b-card-header>
|
||||
<b-card-body>
|
||||
<LoadingSpan v-if="loading" message="Loading Workflow" />
|
||||
<div v-else class="content-height">
|
||||
<div v-for="step in itemContent.steps" :key="step.order_index" class="mb-2">
|
||||
<div>Step {{ step.order_index + 1 }}: {{ step.label }}</div>
|
||||
<WorkflowTree :input="step" :skip-head="true" />
|
||||
</div>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</div>
|
||||
</div>
|
||||
</b-card-body>
|
||||
</b-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
@@ -2,10 +2,18 @@
|
||||
<div class="markdown-wrapper">
|
||||
<LoadingSpan v-if="loading" />
|
||||
<div v-else>
|
||||
<a v-if="effectiveExportLink" :href="exportLink" class="markdown-export position-absolute p-3">
|
||||
<i class="fa fa-3x fa-download" />
|
||||
</a>
|
||||
<div>
|
||||
<b-button
|
||||
v-if="effectiveExportLink"
|
||||
class="float-right"
|
||||
title="Download PDF"
|
||||
variant="link"
|
||||
role="button"
|
||||
v-b-tooltip.hover.bottom
|
||||
@click="onDownload"
|
||||
>
|
||||
<font-awesome-icon icon="download" />
|
||||
</b-button>
|
||||
<b-button
|
||||
v-if="!readOnly"
|
||||
class="float-right"
|
||||
@@ -36,7 +44,7 @@
|
||||
</div>
|
||||
</b-alert>
|
||||
</div>
|
||||
<div v-for="(obj, index) in markdownObjects" :key="index">
|
||||
<div v-for="(obj, index) in markdownObjects" :key="index" class="markdown-components">
|
||||
<p v-if="obj.name == 'default'" v-html="obj.content" class="text-justify m-2" />
|
||||
<div v-else-if="obj.name == 'generate_galaxy_version'" class="galaxy-version">
|
||||
<pre><code>{{ getVersion }}</code></pre>
|
||||
@@ -97,7 +105,7 @@ import MarkdownIt from "markdown-it";
|
||||
import markdownItRegexp from "markdown-it-regexp";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faDownload, faEdit } from "@fortawesome/free-solid-svg-icons";
|
||||
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import HistoryDatasetAsImage from "./Elements/HistoryDatasetAsImage";
|
||||
@@ -128,7 +136,7 @@ md.use(mdNewline);
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
library.add(faEdit);
|
||||
library.add(faDownload, faEdit);
|
||||
|
||||
export default {
|
||||
store: store,
|
||||
@@ -279,6 +287,9 @@ export default {
|
||||
content: content,
|
||||
};
|
||||
},
|
||||
onDownload() {
|
||||
window.location.href = this.exportLink;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
@ok="onOk"
|
||||
@cancel="onCancel"
|
||||
>
|
||||
<Form :inputs="formInputs" @onChange="onChange" />
|
||||
<FormDisplay :inputs="formInputs" @onChange="onChange" />
|
||||
</b-modal>
|
||||
</span>
|
||||
</template>
|
||||
@@ -29,7 +29,7 @@ import Vue from "vue";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import MarkdownSelector from "./MarkdownSelector";
|
||||
import DataDialog from "components/DataDialog/DataDialog";
|
||||
import Form from "components/Form/Form";
|
||||
import FormDisplay from "components/Form/FormDisplay";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
@@ -37,7 +37,7 @@ export default {
|
||||
components: {
|
||||
MarkdownSelector,
|
||||
DataDialog,
|
||||
Form,
|
||||
FormDisplay,
|
||||
},
|
||||
props: {
|
||||
argumentName: {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user