mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-19 02:21:32 +08:00
Merge pull request #18234 from davelopez/24.0_drop_restriction_switch_immutable_histories
[24.1] Drop restriction to switch to immutable histories
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
import {
|
||||
type AnonymousUser,
|
||||
type AnyHistory,
|
||||
type HistorySummary,
|
||||
type HistorySummaryExtended,
|
||||
isRegisteredUser,
|
||||
type User,
|
||||
userOwnsHistory,
|
||||
} from ".";
|
||||
|
||||
const REGISTERED_USER_ID = "fake-user-id";
|
||||
const ANOTHER_USER_ID = "another-fake-user-id";
|
||||
const ANONYMOUS_USER_ID = null;
|
||||
|
||||
const REGISTERED_USER: User = {
|
||||
id: REGISTERED_USER_ID,
|
||||
email: "test@mail.test",
|
||||
tags_used: [],
|
||||
isAnonymous: false,
|
||||
total_disk_usage: 0,
|
||||
};
|
||||
|
||||
const ANONYMOUS_USER: AnonymousUser = {
|
||||
isAnonymous: true,
|
||||
};
|
||||
|
||||
const SESSIONLESS_USER = null;
|
||||
|
||||
function createFakeHistory<T>(historyId: string = "fake-id", user_id?: string | null): T {
|
||||
const history: AnyHistory = {
|
||||
id: historyId,
|
||||
name: "test",
|
||||
model_class: "History",
|
||||
deleted: false,
|
||||
archived: false,
|
||||
purged: false,
|
||||
published: false,
|
||||
annotation: null,
|
||||
update_time: "2021-09-01T00:00:00.000Z",
|
||||
tags: [],
|
||||
url: `/history/${historyId}`,
|
||||
contents_active: { active: 0, deleted: 0, hidden: 0 },
|
||||
count: 0,
|
||||
size: 0,
|
||||
};
|
||||
if (user_id !== undefined) {
|
||||
(history as HistorySummaryExtended).user_id = user_id;
|
||||
}
|
||||
return history as T;
|
||||
}
|
||||
|
||||
const HISTORY_OWNED_BY_REGISTERED_USER = createFakeHistory<HistorySummaryExtended>("1234", REGISTERED_USER_ID);
|
||||
const HISTORY_OWNED_BY_ANOTHER_USER = createFakeHistory<HistorySummaryExtended>("5678", ANOTHER_USER_ID);
|
||||
const HISTORY_OWNED_BY_ANONYMOUS_USER = createFakeHistory<HistorySummaryExtended>("1234", ANONYMOUS_USER_ID);
|
||||
const HISTORY_SUMMARY_WITHOUT_USER_ID = createFakeHistory<HistorySummary>("1234");
|
||||
|
||||
describe("API Types Helpers", () => {
|
||||
describe("isRegisteredUser", () => {
|
||||
it("should return true for a registered user", () => {
|
||||
expect(isRegisteredUser(REGISTERED_USER)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for an anonymous user", () => {
|
||||
expect(isRegisteredUser(ANONYMOUS_USER)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for sessionless users", () => {
|
||||
expect(isRegisteredUser(SESSIONLESS_USER)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("isAnonymousUser", () => {
|
||||
it("should return true for an anonymous user", () => {
|
||||
expect(isRegisteredUser(ANONYMOUS_USER)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for a registered user", () => {
|
||||
expect(isRegisteredUser(REGISTERED_USER)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for sessionless users", () => {
|
||||
expect(isRegisteredUser(SESSIONLESS_USER)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("userOwnsHistory", () => {
|
||||
it("should return true for a registered user owning the history", () => {
|
||||
expect(userOwnsHistory(REGISTERED_USER, HISTORY_OWNED_BY_REGISTERED_USER)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for a registered user not owning the history", () => {
|
||||
expect(userOwnsHistory(REGISTERED_USER, HISTORY_OWNED_BY_ANOTHER_USER)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return true for a registered user owning a history without user_id", () => {
|
||||
expect(userOwnsHistory(REGISTERED_USER, HISTORY_SUMMARY_WITHOUT_USER_ID)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return true for an anonymous user owning a history with null user_id", () => {
|
||||
expect(userOwnsHistory(ANONYMOUS_USER, HISTORY_OWNED_BY_ANONYMOUS_USER)).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for an anonymous user not owning a history", () => {
|
||||
expect(userOwnsHistory(ANONYMOUS_USER, HISTORY_OWNED_BY_REGISTERED_USER)).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for sessionless users", () => {
|
||||
expect(userOwnsHistory(SESSIONLESS_USER, HISTORY_OWNED_BY_REGISTERED_USER)).toBe(false);
|
||||
expect(userOwnsHistory(SESSIONLESS_USER, HISTORY_SUMMARY_WITHOUT_USER_ID)).toBe(false);
|
||||
expect(userOwnsHistory(SESSIONLESS_USER, HISTORY_OWNED_BY_ANONYMOUS_USER)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
+23
-5
@@ -26,7 +26,8 @@ export interface HistoryContentsStats {
|
||||
* Data returned by the API when requesting `?view=summary&keys=size,contents_active,user_id`.
|
||||
*/
|
||||
export interface HistorySummaryExtended extends HistorySummary, HistoryContentsStats {
|
||||
user_id: string;
|
||||
/** The ID of the user that owns the history. Null if the history is owned by an anonymous user. */
|
||||
user_id: string | null;
|
||||
}
|
||||
|
||||
type HistoryDetailedModel = components["schemas"]["HistoryDetailed"];
|
||||
@@ -212,6 +213,7 @@ export function isHistorySummaryExtended(history: AnyHistory): history is Histor
|
||||
|
||||
type QuotaUsageResponse = components["schemas"]["UserQuotaUsage"];
|
||||
|
||||
/** Represents a registered user.**/
|
||||
export interface User extends QuotaUsageResponse {
|
||||
id: string;
|
||||
email: string;
|
||||
@@ -230,15 +232,23 @@ export interface AnonymousUser {
|
||||
|
||||
export type GenericUser = User | AnonymousUser;
|
||||
|
||||
export function isRegisteredUser(user: User | AnonymousUser | null): user is User {
|
||||
return !user?.isAnonymous;
|
||||
/** Represents any user, including anonymous users or session-less (null) users.**/
|
||||
export type AnyUser = GenericUser | null;
|
||||
|
||||
export function isRegisteredUser(user: AnyUser): user is User {
|
||||
return user !== null && !user?.isAnonymous;
|
||||
}
|
||||
|
||||
export function userOwnsHistory(user: User | AnonymousUser | null, history: AnyHistory) {
|
||||
export function isAnonymousUser(user: AnyUser): user is AnonymousUser {
|
||||
return user !== null && user.isAnonymous;
|
||||
}
|
||||
|
||||
export function userOwnsHistory(user: AnyUser, history: AnyHistory) {
|
||||
return (
|
||||
// Assuming histories without user_id are owned by the current user
|
||||
(isRegisteredUser(user) && !hasOwner(history)) ||
|
||||
(isRegisteredUser(user) && hasOwner(history) && user.id === history.user_id)
|
||||
(isRegisteredUser(user) && hasOwner(history) && user.id === history.user_id) ||
|
||||
(isAnonymousUser(user) && hasAnonymousOwner(history))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -246,6 +256,14 @@ function hasOwner(history: AnyHistory): history is HistorySummaryExtended {
|
||||
return "user_id" in history && history.user_id !== null;
|
||||
}
|
||||
|
||||
function hasAnonymousOwner(history: AnyHistory): history is HistorySummaryExtended {
|
||||
return "user_id" in history && history.user_id === null;
|
||||
}
|
||||
|
||||
export function canMutateHistory(history: AnyHistory): boolean {
|
||||
return !history.purged && !history.archived;
|
||||
}
|
||||
|
||||
export type DatasetHash = components["schemas"]["DatasetHash"];
|
||||
|
||||
export type DatasetTransform = {
|
||||
|
||||
@@ -142,7 +142,6 @@ const fields: FieldArray = [
|
||||
{
|
||||
title: "Switch",
|
||||
icon: faExchangeAlt,
|
||||
condition: (data: HistoryEntry) => !data.deleted,
|
||||
handler: (data: HistoryEntry) => {
|
||||
const historyStore = useHistoryStore();
|
||||
historyStore.setCurrentHistory(String(data.id));
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faCopy, faEye, faUndo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faCopy, faExchangeAlt, faEye, faUndo } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { BBadge, BButton, BButtonGroup } from "bootstrap-vue";
|
||||
import { computed } from "vue";
|
||||
@@ -23,16 +23,21 @@ const canImportCopy = computed(() => props.history.export_record_data?.target_ur
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "onView", history: ArchivedHistorySummary): void;
|
||||
(e: "onSwitch", history: ArchivedHistorySummary): void;
|
||||
(e: "onRestore", history: ArchivedHistorySummary): void;
|
||||
(e: "onImportCopy", history: ArchivedHistorySummary): void;
|
||||
}>();
|
||||
|
||||
library.add(faUndo, faCopy, faEye);
|
||||
library.add(faExchangeAlt, faUndo, faCopy, faEye);
|
||||
|
||||
function onViewHistoryInCenterPanel() {
|
||||
emit("onView", props.history);
|
||||
}
|
||||
|
||||
function onSetAsCurrentHistory() {
|
||||
emit("onSwitch", props.history);
|
||||
}
|
||||
|
||||
async function onRestoreHistory() {
|
||||
emit("onRestore", props.history);
|
||||
}
|
||||
@@ -84,6 +89,15 @@ async function onImportCopy() {
|
||||
<FontAwesomeIcon :icon="faEye" size="lg" />
|
||||
View
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip
|
||||
:title="localize('Set as current history')"
|
||||
variant="link"
|
||||
class="p-0 px-1"
|
||||
@click.stop="onSetAsCurrentHistory">
|
||||
<FontAwesomeIcon :icon="faExchangeAlt" size="lg" />
|
||||
Switch
|
||||
</BButton>
|
||||
<BButton
|
||||
v-b-tooltip
|
||||
:title="localize('Unarchive this history and move it back to your active histories')"
|
||||
|
||||
@@ -66,6 +66,10 @@ function onViewHistoryInCenterPanel(history: ArchivedHistorySummary) {
|
||||
router.push(`/histories/view?id=${history.id}`);
|
||||
}
|
||||
|
||||
function onSetAsCurrentHistory(history: ArchivedHistorySummary) {
|
||||
historyStore.setCurrentHistory(history.id);
|
||||
}
|
||||
|
||||
async function onRestoreHistory(history: ArchivedHistorySummary) {
|
||||
const confirmTitle = localize(`Unarchive '${history.name}'?`);
|
||||
const confirmMessage =
|
||||
@@ -145,6 +149,7 @@ async function onImportCopy(history: ArchivedHistorySummary) {
|
||||
<ArchivedHistoryCard
|
||||
:history="history"
|
||||
@onView="onViewHistoryInCenterPanel"
|
||||
@onSwitch="onSetAsCurrentHistory"
|
||||
@onRestore="onRestoreHistory"
|
||||
@onImportCopy="onImportCopy" />
|
||||
</BListGroupItem>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import { computed, ref, watch } from "vue";
|
||||
|
||||
import type { CollectionEntry, DCESummary, HistorySummary, SubCollection } from "@/api";
|
||||
import { isCollectionElement, isHDCA } from "@/api";
|
||||
import { canMutateHistory, isCollectionElement, isHDCA } from "@/api";
|
||||
import ExpandedItems from "@/components/History/Content/ExpandedItems";
|
||||
import { updateContentFields } from "@/components/History/model/queries";
|
||||
import { useCollectionElementsStore } from "@/stores/collectionElementsStore";
|
||||
@@ -66,6 +66,7 @@ const rootCollection = computed(() => {
|
||||
}
|
||||
});
|
||||
const isRoot = computed(() => dsc.value == rootCollection.value);
|
||||
const canEdit = computed(() => isRoot.value && canMutateHistory(props.history));
|
||||
|
||||
function updateDsc(collection: any, fields: Object | undefined) {
|
||||
updateContentFields(collection, fields).then((response) => {
|
||||
@@ -124,8 +125,8 @@ watch(
|
||||
:history-name="history.name"
|
||||
:selected-collections="selectedCollections"
|
||||
v-on="$listeners" />
|
||||
<CollectionDetails :dsc="dsc" :writeable="isRoot" @update:dsc="updateDsc(dsc, $event)" />
|
||||
<CollectionOperations v-if="isRoot && showControls" :dsc="dsc" />
|
||||
<CollectionDetails :dsc="dsc" :writeable="canEdit" @update:dsc="updateDsc(dsc, $event)" />
|
||||
<CollectionOperations v-if="canEdit && showControls" :dsc="dsc" />
|
||||
</section>
|
||||
<section class="position-relative flex-grow-1 scroller">
|
||||
<div>
|
||||
|
||||
@@ -9,7 +9,7 @@ import prettyBytes from "pretty-bytes";
|
||||
import { computed, onMounted, ref, toRef } from "vue";
|
||||
import { useRouter } from "vue-router/composables";
|
||||
|
||||
import type { HistorySummaryExtended } from "@/api";
|
||||
import { type HistorySummaryExtended, userOwnsHistory } from "@/api";
|
||||
import { HistoryFilters } from "@/components/History/HistoryFilters.js";
|
||||
import { useConfig } from "@/composables/config";
|
||||
import { useHistoryContentStats } from "@/composables/historyContentStats";
|
||||
@@ -137,7 +137,7 @@ onMounted(() => {
|
||||
variant="link"
|
||||
size="sm"
|
||||
class="rounded-0 text-decoration-none history-storage-overview-button"
|
||||
:disabled="!showControls"
|
||||
:disabled="!userOwnsHistory(currentUser, props.history)"
|
||||
data-description="storage dashboard button"
|
||||
@click="onDashboard">
|
||||
<FontAwesomeIcon :icon="faDatabase" />
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faArchive, faBurn, faTrash } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { BAlert } from "bootstrap-vue";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import type { HistorySummary } from "@/api";
|
||||
import { type GenericUser, type HistorySummary, userOwnsHistory } from "@/api";
|
||||
import localize from "@/utils/localization";
|
||||
|
||||
library.add(faArchive, faBurn, faTrash);
|
||||
|
||||
interface Props {
|
||||
history: HistorySummary;
|
||||
currentUser: GenericUser | null;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
@@ -14,14 +20,28 @@ const props = defineProps<Props>();
|
||||
const userOverQuota = ref(false);
|
||||
|
||||
const hasMessages = computed(() => {
|
||||
return userOverQuota.value || props.history.deleted;
|
||||
return userOverQuota.value || props.history.deleted || props.history.archived;
|
||||
});
|
||||
|
||||
const currentUserOwnsHistory = computed(() => {
|
||||
return userOwnsHistory(props.currentUser, props.history);
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="hasMessages" class="mx-3 my-2">
|
||||
<BAlert :show="history.deleted" variant="warning">
|
||||
{{ localize("This history has been deleted") }}
|
||||
<div v-if="hasMessages" class="mx-3 mt-2" data-description="history messages">
|
||||
<BAlert v-if="history.purged" :show="history.purged" variant="warning">
|
||||
<FontAwesomeIcon :icon="faBurn" fixed-width />
|
||||
{{ localize("History has been purged") }}
|
||||
</BAlert>
|
||||
<BAlert v-else-if="history.deleted" :show="history.deleted" variant="warning">
|
||||
<FontAwesomeIcon :icon="faTrash" fixed-width />
|
||||
{{ localize("History has been deleted") }}
|
||||
</BAlert>
|
||||
|
||||
<BAlert :show="history.archived && currentUserOwnsHistory" variant="warning">
|
||||
<FontAwesomeIcon :icon="faArchive" fixed-width />
|
||||
{{ localize("History has been archived") }}
|
||||
</BAlert>
|
||||
|
||||
<BAlert :show="userOverQuota" variant="warning">
|
||||
|
||||
@@ -30,9 +30,9 @@ import {
|
||||
BSpinner,
|
||||
} from "bootstrap-vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
|
||||
import type { HistorySummary } from "@/api";
|
||||
import { canMutateHistory, type HistorySummary } from "@/api";
|
||||
import { iframeRedirect } from "@/components/plugins/legacyNavigation";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
@@ -80,6 +80,22 @@ const historyStore = useHistoryStore();
|
||||
const { isAnonymous } = storeToRefs(userStore);
|
||||
const { totalHistoryCount } = storeToRefs(historyStore);
|
||||
|
||||
const canEditHistory = computed(() => {
|
||||
return canMutateHistory(props.history);
|
||||
});
|
||||
|
||||
const historyState = computed(() => {
|
||||
if (props.history.purged) {
|
||||
return "purged";
|
||||
} else if (props.history.deleted) {
|
||||
return "deleted";
|
||||
} else if (props.history.archived) {
|
||||
return "archived";
|
||||
} else {
|
||||
return "active";
|
||||
}
|
||||
});
|
||||
|
||||
function onDelete() {
|
||||
if (purgeHistory.value) {
|
||||
historyStore.deleteHistory(props.history.id, true);
|
||||
@@ -160,7 +176,16 @@ function userTitle(title: string) {
|
||||
|
||||
<BDropdownDivider />
|
||||
|
||||
<BDropdownText v-if="!canEditHistory">
|
||||
This history has been <span class="font-weight-bold">{{ historyState }}</span
|
||||
>.
|
||||
<span v-localize>Some actions might not be available.</span>
|
||||
</BDropdownText>
|
||||
|
||||
<BDropdownDivider v-if="!canEditHistory" />
|
||||
|
||||
<BDropdownItem
|
||||
:disabled="!canEditHistory"
|
||||
:title="localize('Resume all Paused Jobs in this History')"
|
||||
@click="iframeRedirect('/history/resume_paused_jobs?current=True')">
|
||||
<FontAwesomeIcon fixed-width :icon="faPlay" class="mr-1" />
|
||||
@@ -177,7 +202,10 @@ function userTitle(title: string) {
|
||||
<span v-localize>Copy this History</span>
|
||||
</BDropdownItem>
|
||||
|
||||
<BDropdownItem v-b-modal:delete-history-modal :title="localize('Permanently Delete History')">
|
||||
<BDropdownItem
|
||||
v-b-modal:delete-history-modal
|
||||
:disabled="!canEditHistory"
|
||||
:title="localize('Permanently Delete History')">
|
||||
<FontAwesomeIcon fixed-width :icon="faTrash" class="mr-1" />
|
||||
<span v-localize>Delete this History</span>
|
||||
</BDropdownItem>
|
||||
@@ -191,6 +219,7 @@ function userTitle(title: string) {
|
||||
|
||||
<BDropdownItem
|
||||
data-description="export to file"
|
||||
:disabled="history.purged"
|
||||
:title="localize('Export and Download History as a File')"
|
||||
@click="$router.push(`/histories/${history.id}/export`)">
|
||||
<FontAwesomeIcon fixed-width :icon="faFileArchive" class="mr-1" />
|
||||
@@ -198,7 +227,7 @@ function userTitle(title: string) {
|
||||
</BDropdownItem>
|
||||
|
||||
<BDropdownItem
|
||||
:disabled="isAnonymous"
|
||||
:disabled="isAnonymous || history.archived || history.purged"
|
||||
data-description="archive history"
|
||||
:title="userTitle('Archive this History')"
|
||||
@click="$router.push(`/histories/${history.id}/archive`)">
|
||||
@@ -209,7 +238,7 @@ function userTitle(title: string) {
|
||||
<BDropdownItem
|
||||
:disabled="isAnonymous"
|
||||
:title="userTitle('Convert History to Workflow')"
|
||||
@click="iframeRedirect('/workflow/build_from_current_history')">
|
||||
@click="iframeRedirect(`/workflow/build_from_current_history?history_id=${history.id}`)">
|
||||
<FontAwesomeIcon fixed-width :icon="faFileExport" class="mr-1" />
|
||||
<span v-localize>Extract Workflow</span>
|
||||
</BDropdownItem>
|
||||
@@ -225,7 +254,7 @@ function userTitle(title: string) {
|
||||
<BDropdownDivider />
|
||||
|
||||
<BDropdownItem
|
||||
:disabled="isAnonymous"
|
||||
:disabled="isAnonymous || !canEditHistory"
|
||||
:title="userTitle('Share or Publish this History')"
|
||||
data-description="share or publish"
|
||||
@click="$router.push(`/histories/sharing?id=${history.id}`)">
|
||||
@@ -234,7 +263,7 @@ function userTitle(title: string) {
|
||||
</BDropdownItem>
|
||||
|
||||
<BDropdownItem
|
||||
:disabled="isAnonymous"
|
||||
:disabled="isAnonymous || !canEditHistory"
|
||||
:title="userTitle('Set who can View or Edit this History')"
|
||||
@click="$router.push(`/histories/permissions?id=${history.id}`)">
|
||||
<FontAwesomeIcon fixed-width :icon="faUserLock" class="mr-1" />
|
||||
@@ -243,7 +272,7 @@ function userTitle(title: string) {
|
||||
|
||||
<BDropdownItem
|
||||
v-b-modal:history-privacy-modal
|
||||
:disabled="isAnonymous"
|
||||
:disabled="isAnonymous || !canEditHistory"
|
||||
:title="userTitle('Make this History Private')">
|
||||
<FontAwesomeIcon fixed-width :icon="faLock" class="mr-1" />
|
||||
<span v-localize>Make Private</span>
|
||||
|
||||
@@ -3,7 +3,7 @@ import { BAlert } from "bootstrap-vue";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, onMounted, type Ref, ref, set as VueSet, unref, watch } from "vue";
|
||||
|
||||
import type { HistoryItemSummary, HistorySummaryExtended } from "@/api";
|
||||
import { type HistoryItemSummary, type HistorySummaryExtended, userOwnsHistory } from "@/api";
|
||||
import { copyDataset } from "@/api/datasets";
|
||||
import ExpandedItems from "@/components/History/Content/ExpandedItems";
|
||||
import SelectedItems from "@/components/History/Content/SelectedItems";
|
||||
@@ -15,6 +15,7 @@ import { startWatchingHistory } from "@/store/historyStore/model/watchHistory";
|
||||
import { useEventStore } from "@/stores/eventStore";
|
||||
import { useHistoryItemsStore } from "@/stores/historyItemsStore";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
import { type Alias, getOperatorForAlias } from "@/utils/filtering";
|
||||
import { setDrag } from "@/utils/setDrag";
|
||||
|
||||
@@ -49,7 +50,6 @@ interface Props {
|
||||
listOffset?: number;
|
||||
history: HistorySummaryExtended;
|
||||
filter?: string;
|
||||
canEditHistory?: boolean;
|
||||
filterable?: boolean;
|
||||
isMultiViewItem?: boolean;
|
||||
}
|
||||
@@ -59,7 +59,6 @@ type ContentItemRef = Record<string, Ref<InstanceType<typeof ContentItem> | null
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
listOffset: 0,
|
||||
filter: "",
|
||||
canEditHistory: true,
|
||||
filterable: false,
|
||||
isMultiViewItem: false,
|
||||
});
|
||||
@@ -90,6 +89,14 @@ const { lastCheckedTime, totalMatchesCount, isWatching } = storeToRefs(useHistor
|
||||
|
||||
const historyStore = useHistoryStore();
|
||||
const historyItemsStore = useHistoryItemsStore();
|
||||
const { currentUser } = storeToRefs(useUserStore());
|
||||
|
||||
const currentUserOwnsHistory = computed(() => {
|
||||
return userOwnsHistory(currentUser.value, props.history);
|
||||
});
|
||||
const canEditHistory = computed(() => {
|
||||
return currentUserOwnsHistory.value && !props.history.deleted && !props.history.archived;
|
||||
});
|
||||
|
||||
const historyUpdateTime = computed(() => {
|
||||
return props.history.update_time;
|
||||
@@ -535,13 +542,14 @@ function setItemDragstart(
|
||||
:summarized="isMultiViewItem"
|
||||
@update:history="historyStore.updateHistory($event)" />
|
||||
|
||||
<HistoryMessages :history="history" />
|
||||
<HistoryMessages :history="history" :current-user="currentUser" />
|
||||
|
||||
<HistoryCounter
|
||||
:history="history"
|
||||
:is-watching="isWatching"
|
||||
:last-checked="lastCheckedTime"
|
||||
:show-controls="canEditHistory"
|
||||
:owned-by-current-user="userOwnsHistory(currentUser, history)"
|
||||
:filter-text.sync="filterText"
|
||||
:hide-reload="isMultiViewItem"
|
||||
@reloadContents="reloadContents" />
|
||||
|
||||
@@ -22,6 +22,7 @@ function create_history(historyId, userId, purged = false, archived = false) {
|
||||
name: historyName,
|
||||
purged: purged,
|
||||
archived: archived,
|
||||
deleted: purged,
|
||||
count: 10,
|
||||
annotation: "This is a history",
|
||||
tags: ["tag_1", "tag_2"],
|
||||
@@ -174,17 +175,17 @@ describe("History center panel View", () => {
|
||||
const wrapper = await createWrapper(localVue, "user_1", history);
|
||||
expect(wrapper.vm.history).toEqual(history);
|
||||
|
||||
// history purged, not switchable and not importable
|
||||
// history purged, is switchable but not importable
|
||||
const switchButton = wrapper.find("[data-description='switch to history button']");
|
||||
const importButton = wrapper.find("[data-description='import history button']");
|
||||
expect(switchButton.attributes("disabled")).toBeTruthy();
|
||||
expect(switchButton.attributes("disabled")).toBeFalsy();
|
||||
expect(importButton.exists()).toBe(false);
|
||||
|
||||
// storage dashboard button should be disabled
|
||||
expect(storageDashboardButtonDisabled(wrapper)).toBeTruthy();
|
||||
// storage dashboard button can be accessed
|
||||
expect(storageDashboardButtonDisabled(wrapper)).toBeFalsy();
|
||||
|
||||
// instead we have an alert
|
||||
expect(wrapper.find("[data-description='history state info']").text()).toBe("This history has been purged.");
|
||||
expect(wrapper.find("[data-description='history messages']").text()).toBe("History has been purged");
|
||||
});
|
||||
|
||||
it("should not display archived message and should be importable when user is not owner and history is archived", async () => {
|
||||
@@ -202,7 +203,8 @@ describe("History center panel View", () => {
|
||||
expect(storageDashboardButtonDisabled(wrapper)).toBeTruthy();
|
||||
|
||||
expectCorrectLayout(wrapper);
|
||||
expect(wrapper.find("[data-description='history state info']").exists()).toBe(false);
|
||||
// There is no message about the history status
|
||||
expect(wrapper.find("[data-description='history messages']").text()).toBe("");
|
||||
});
|
||||
|
||||
it("should display archived message and should not be importable when user is owner and history is archived", async () => {
|
||||
@@ -213,13 +215,12 @@ describe("History center panel View", () => {
|
||||
const switchButton = wrapper.find("[data-description='switch to history button']");
|
||||
const importButton = wrapper.find("[data-description='import history button']");
|
||||
expect(switchButton.exists()).toBe(true);
|
||||
expect(switchButton.attributes("disabled")).toBeTruthy();
|
||||
expect(importButton.exists()).toBe(false);
|
||||
|
||||
// storage dashboard button should be disabled
|
||||
expect(storageDashboardButtonDisabled(wrapper)).toBeTruthy();
|
||||
// storage dashboard button can be accessed
|
||||
expect(storageDashboardButtonDisabled(wrapper)).toBeFalsy();
|
||||
|
||||
expectCorrectLayout(wrapper);
|
||||
expect(wrapper.find("[data-description='history state info']").text()).toBe("This history has been archived.");
|
||||
expect(wrapper.find("[data-description='history messages']").text()).toBe("History has been archived");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +1,5 @@
|
||||
<template>
|
||||
<div v-if="currentUser && history" class="d-flex flex-column h-100">
|
||||
<b-alert v-if="showHistoryStateInfo" variant="info" show data-description="history state info">
|
||||
{{ historyStateInfoMessage }}
|
||||
</b-alert>
|
||||
|
||||
<div class="flex-row flex-grow-0 pb-3">
|
||||
<b-button
|
||||
v-if="userOwnsHistory"
|
||||
@@ -35,12 +31,7 @@
|
||||
:selected-collections.sync="selectedCollections"
|
||||
:show-controls="false"
|
||||
@view-collection="onViewCollection" />
|
||||
<HistoryPanel
|
||||
v-else
|
||||
:history="history"
|
||||
:can-edit-history="canEditHistory"
|
||||
filterable
|
||||
@view-collection="onViewCollection" />
|
||||
<HistoryPanel v-else :history="history" filterable @view-collection="onViewCollection" />
|
||||
|
||||
<CopyModal id="copy-history-modal" :history="history" @ok="copyOkay" />
|
||||
</div>
|
||||
@@ -87,39 +78,14 @@ export default {
|
||||
return this.currentHistory?.id == this.history?.id;
|
||||
},
|
||||
isSetAsCurrentDisabled() {
|
||||
return this.isCurrentHistory || this.history.archived || this.history.purged;
|
||||
return this.isCurrentHistory;
|
||||
},
|
||||
setAsCurrentTitle() {
|
||||
if (this.isCurrentHistory) {
|
||||
return "This history is already your current history.";
|
||||
}
|
||||
if (this.history.archived) {
|
||||
return "This history has been archived and cannot be set as your current history. Unarchive it first.";
|
||||
}
|
||||
if (this.history.purged) {
|
||||
return "This history has been purged and cannot be set as your current history.";
|
||||
}
|
||||
return "Switch to this history";
|
||||
},
|
||||
canEditHistory() {
|
||||
return this.userOwnsHistory && !this.history.archived && !this.history.purged;
|
||||
},
|
||||
showHistoryArchived() {
|
||||
return this.history.archived && this.userOwnsHistory;
|
||||
},
|
||||
showHistoryStateInfo() {
|
||||
return this.showHistoryArchived || this.history.purged;
|
||||
},
|
||||
historyStateInfoMessage() {
|
||||
if (this.showHistoryArchived && this.history.purged) {
|
||||
return "This history has been archived and purged.";
|
||||
} else if (this.showHistoryArchived) {
|
||||
return "This history has been archived.";
|
||||
} else if (this.history.purged) {
|
||||
return "This history has been purged.";
|
||||
}
|
||||
return "";
|
||||
},
|
||||
canImportHistory() {
|
||||
return !this.userOwnsHistory && !this.history.purged;
|
||||
},
|
||||
|
||||
@@ -7,6 +7,8 @@ import { createPinia } from "pinia";
|
||||
import { useHistoryStore } from "stores/historyStore";
|
||||
import { getLocalVue } from "tests/jest/helpers";
|
||||
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
|
||||
import SelectorModal from "./SelectorModal";
|
||||
|
||||
const localVue = getLocalVue();
|
||||
@@ -35,6 +37,14 @@ const PROPS_FOR_MODAL_MULTIPLE_SELECT = {
|
||||
|
||||
const CURRENT_HISTORY_INDICATION_TEXT = "(Current)";
|
||||
|
||||
const CURRENT_USER = {
|
||||
email: "email",
|
||||
id: "user_id",
|
||||
tags_used: [],
|
||||
isAnonymous: false,
|
||||
total_disk_usage: 0,
|
||||
};
|
||||
|
||||
describe("History SelectorModal.vue", () => {
|
||||
let wrapper;
|
||||
let axiosMock;
|
||||
@@ -60,6 +70,10 @@ describe("History SelectorModal.vue", () => {
|
||||
icon: { template: "<div></div>" },
|
||||
},
|
||||
});
|
||||
|
||||
const userStore = useUserStore();
|
||||
userStore.setCurrentUser(CURRENT_USER);
|
||||
|
||||
historyStore = useHistoryStore();
|
||||
axiosMock = new MockAdapter(axios);
|
||||
getUpdatedAxiosMock();
|
||||
|
||||
@@ -3,6 +3,9 @@
|
||||
<b-alert :show="messageShow" :variant="messageVariant">
|
||||
{{ messageText }}
|
||||
</b-alert>
|
||||
<b-alert v-if="!showLoading && !canMutateHistory" show variant="warning">
|
||||
{{ immutableHistoryMessage }}
|
||||
</b-alert>
|
||||
<LoadingSpan v-if="showLoading" message="Loading Tool" />
|
||||
<div v-if="showEntryPoints">
|
||||
<ToolEntryPoints v-for="job in entryPoints" :key="job.id" :job-id="job.id" />
|
||||
@@ -87,6 +90,7 @@
|
||||
<ButtonSpinner
|
||||
id="execute"
|
||||
title="Run Tool"
|
||||
:disabled="!canMutateHistory"
|
||||
class="btn-sm"
|
||||
:wait="showExecuting"
|
||||
:tooltip="tooltip"
|
||||
@@ -96,6 +100,7 @@
|
||||
<ButtonSpinner
|
||||
title="Run Tool"
|
||||
class="mt-3 mb-3"
|
||||
:disabled="!canMutateHistory"
|
||||
:wait="showExecuting"
|
||||
:tooltip="tooltip"
|
||||
@onClick="onExecute(config, currentHistoryId)" />
|
||||
@@ -117,6 +122,7 @@ import { useHistoryItemsStore } from "stores/historyItemsStore";
|
||||
import { useJobStore } from "stores/jobStore";
|
||||
import { refreshContentsWrapper } from "utils/data";
|
||||
|
||||
import { canMutateHistory } from "@/api";
|
||||
import { useConfigStore } from "@/stores/configurationStore";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { useUserStore } from "@/stores/userStore";
|
||||
@@ -196,11 +202,13 @@ export default {
|
||||
{ label: "populate", value: "populate" },
|
||||
{ label: "bundle", value: "bundle" },
|
||||
],
|
||||
immutableHistoryMessage:
|
||||
"This history is immutable and you cannot run tools in it. Please switch to a different history.",
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
...mapState(useUserStore, ["currentUser"]),
|
||||
...mapState(useHistoryStore, ["currentHistoryId"]),
|
||||
...mapState(useHistoryStore, ["currentHistoryId", "currentHistory"]),
|
||||
...mapState(useHistoryItemsStore, ["lastUpdateTime"]),
|
||||
toolName() {
|
||||
return this.formConfig.name;
|
||||
@@ -212,6 +220,9 @@ export default {
|
||||
return id.endsWith(version) ? id : `${id}/${version}`;
|
||||
},
|
||||
tooltip() {
|
||||
if (!this.canMutateHistory) {
|
||||
return this.immutableHistoryMessage;
|
||||
}
|
||||
return `Run tool: ${this.formConfig.name} (${this.formConfig.version})`;
|
||||
},
|
||||
errorContentPretty() {
|
||||
@@ -234,6 +245,9 @@ export default {
|
||||
initialized() {
|
||||
return this.formData !== undefined;
|
||||
},
|
||||
canMutateHistory() {
|
||||
return this.currentHistory && canMutateHistory(this.currentHistory);
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
currentHistoryId() {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup>
|
||||
import { BTab, BTabs } from "bootstrap-vue";
|
||||
import { BAlert, BTab, BTabs } from "bootstrap-vue";
|
||||
import { getDatatypesMapper } from "components/Datatypes";
|
||||
import LoadingSpan from "components/LoadingSpan";
|
||||
import {
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
|
||||
import { canMutateHistory } from "@/api";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { useUploadStore } from "@/stores/uploadStore";
|
||||
import { uploadPayload } from "@/utils/upload-payload.js";
|
||||
|
||||
@@ -82,6 +84,8 @@ const regular = ref(null);
|
||||
|
||||
const { percentage, status } = storeToRefs(useUploadStore());
|
||||
|
||||
const { currentHistory } = storeToRefs(useHistoryStore());
|
||||
|
||||
const effectiveExtensions = computed(() => {
|
||||
if (props.formats === null || !datatypesMapperReady.value) {
|
||||
return listExtensions.value;
|
||||
@@ -106,6 +110,7 @@ const historyAvailable = computed(() => Boolean(props.currentHistoryId));
|
||||
const ready = computed(
|
||||
() => dbKeysSet.value && extensionsSet.value && historyAvailable.value && datatypesMapperReady.value
|
||||
);
|
||||
const canUploadToHistory = computed(() => currentHistory.value && canMutateHistory(currentHistory.value));
|
||||
const showCollection = computed(() => !props.formats && props.multiple);
|
||||
const showComposite = computed(() => !props.formats || hasCompositeExtension);
|
||||
const showRegular = computed(() => !props.formats || hasRegularExtension);
|
||||
@@ -160,7 +165,13 @@ defineExpose({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<BTabs v-if="ready">
|
||||
<BAlert v-if="!canUploadToHistory" variant="warning" show>
|
||||
<span v-localize>
|
||||
The current history is immutable and you cannot upload data to it. Please select a different history or
|
||||
create a new one.
|
||||
</span>
|
||||
</BAlert>
|
||||
<BTabs v-else-if="ready">
|
||||
<BTab v-if="showRegular" id="regular" title="Regular" button-id="tab-title-link-regular">
|
||||
<DefaultBox
|
||||
ref="regular"
|
||||
|
||||
@@ -196,6 +196,7 @@ async function onPermanentlyDeleteHistory(historyId: string) {
|
||||
item-type="history"
|
||||
:is-recoverable="isRecoverableDataPoint(data)"
|
||||
:is-archived="isArchivedDataPoint(data)"
|
||||
:can-edit="!isArchivedDataPoint(data)"
|
||||
@set-current-history="onSetCurrentHistory"
|
||||
@view-item="onViewHistory"
|
||||
@undelete-item="onUndeleteHistory"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { BButton, BForm, BFormGroup, BFormSelect, BInputGroup } from "bootstrap-vue";
|
||||
import { ref } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import { useRouter } from "vue-router/composables";
|
||||
|
||||
import { useSelectableObjectStores } from "@/composables/useObjectStores";
|
||||
@@ -27,7 +27,7 @@ import FilterObjectStoreLink from "@/components/Common/FilterObjectStoreLink.vue
|
||||
import LoadingSpan from "@/components/LoadingSpan.vue";
|
||||
|
||||
const router = useRouter();
|
||||
const { getHistoryNameById } = useHistoryStore();
|
||||
const { getHistoryNameById, getHistoryById } = useHistoryStore();
|
||||
|
||||
interface Props {
|
||||
historyId: string;
|
||||
@@ -53,6 +53,11 @@ const { selectableObjectStores, hasSelectableObjectStores } = useSelectableObjec
|
||||
|
||||
const objectStore = ref<string | null>(null);
|
||||
|
||||
const canEditHistory = computed(() => {
|
||||
const history = getHistoryById(props.historyId);
|
||||
return (history && !history.purged && !history.archived) ?? false;
|
||||
});
|
||||
|
||||
function onChangeObjectStore(value: string | null) {
|
||||
objectStore.value = value;
|
||||
reloadDataFromServer();
|
||||
@@ -209,6 +214,7 @@ function onUndelete(datasetId: string) {
|
||||
:data="data"
|
||||
item-type="dataset"
|
||||
:is-recoverable="isRecoverableDataPoint(data)"
|
||||
:can-edit="canEditHistory"
|
||||
@view-item="onViewDataset"
|
||||
@undelete-item="onUndelete"
|
||||
@permanently-delete-item="onPermDelete" />
|
||||
|
||||
@@ -25,12 +25,14 @@ interface SelectedItemActionsProps {
|
||||
isRecoverable: boolean;
|
||||
itemType: ItemTypes;
|
||||
isArchived?: boolean;
|
||||
canEdit?: boolean;
|
||||
}
|
||||
|
||||
const { currentHistoryId } = useHistoryStore();
|
||||
|
||||
const props = withDefaults(defineProps<SelectedItemActionsProps>(), {
|
||||
isArchived: false,
|
||||
canEdit: false,
|
||||
});
|
||||
|
||||
library.add(faArchive, faChartBar, faInfoCircle, faLocationArrow, faTrash, faUndo);
|
||||
@@ -38,9 +40,7 @@ library.add(faArchive, faChartBar, faInfoCircle, faLocationArrow, faTrash, faUnd
|
||||
const label = computed(() => props.data?.label ?? "No data");
|
||||
const prettySize = computed(() => bytesToString(props.data?.value ?? 0));
|
||||
const viewDetailsIcon = computed(() => (props.itemType === "history" ? "chart-bar" : "info-circle"));
|
||||
const canSetAsCurrent = computed(
|
||||
() => props.itemType === "history" && !props.isArchived && props.data.id !== currentHistoryId
|
||||
);
|
||||
const canSetAsCurrent = computed(() => props.itemType === "history" && props.data.id !== currentHistoryId);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "set-current-history", historyId: string): void;
|
||||
@@ -99,7 +99,7 @@ function onPermanentlyDeleteItem() {
|
||||
<FontAwesomeIcon :icon="viewDetailsIcon" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-if="isRecoverable"
|
||||
v-if="isRecoverable && canEdit"
|
||||
variant="outline-primary"
|
||||
size="sm"
|
||||
class="mx-2"
|
||||
@@ -108,6 +108,7 @@ function onPermanentlyDeleteItem() {
|
||||
<FontAwesomeIcon icon="undo" />
|
||||
</BButton>
|
||||
<BButton
|
||||
v-if="canEdit"
|
||||
variant="outline-danger"
|
||||
size="sm"
|
||||
class="mx-2"
|
||||
|
||||
@@ -5,6 +5,7 @@ import { computed, onMounted, ref, watch } from "vue";
|
||||
import { RouterLink } from "vue-router";
|
||||
import { useRouter } from "vue-router/composables";
|
||||
|
||||
import { canMutateHistory } from "@/api";
|
||||
import { copyWorkflow } from "@/components/Workflow/workflows.services";
|
||||
import { useHistoryItemsStore } from "@/stores/historyItemsStore";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
@@ -52,6 +53,13 @@ const editorLink = computed(() => `/workflows/edit?id=${props.workflowId}`);
|
||||
const historyStatusKey = computed(() => `${currentHistoryId.value}_${lastUpdateTime.value}`);
|
||||
const isOwner = computed(() => currentUser.value?.username === workflowModel.value.runData.owner);
|
||||
const lastUpdateTime = computed(() => historyItemsStore.lastUpdateTime);
|
||||
const canRunOnHistory = computed(() => {
|
||||
if (!currentHistoryId.value) {
|
||||
return false;
|
||||
}
|
||||
const history = historyStore.getHistoryById(currentHistoryId.value);
|
||||
return (history && canMutateHistory(history)) ?? false;
|
||||
});
|
||||
|
||||
function handleInvocations(incomingInvocations: any) {
|
||||
invocations.value = incomingInvocations;
|
||||
@@ -177,12 +185,14 @@ defineExpose({
|
||||
:model="workflowModel"
|
||||
:target-history="simpleFormTargetHistory"
|
||||
:use-job-cache="simpleFormUseJobCache"
|
||||
:can-mutate-current-history="canRunOnHistory"
|
||||
@submissionSuccess="handleInvocations"
|
||||
@submissionError="handleSubmissionError"
|
||||
@showAdvanced="showAdvanced" />
|
||||
<WorkflowRunForm
|
||||
v-else
|
||||
:model="workflowModel"
|
||||
:can-mutate-current-history="canRunOnHistory"
|
||||
@submissionSuccess="handleInvocations"
|
||||
@submissionError="handleSubmissionError" />
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
<template>
|
||||
<div v-if="currentUser && currentHistoryId" class="workflow-expanded-form">
|
||||
<BAlert v-if="!canRunOnHistory" variant="warning" show>
|
||||
<span v-localize>
|
||||
The workflow cannot run because the current history is immutable. Please select a different history or
|
||||
send the results to a new one.
|
||||
</span>
|
||||
</BAlert>
|
||||
<div class="h4 clearfix mb-3">
|
||||
<b>Workflow: {{ model.name }}</b>
|
||||
<ButtonSpinner
|
||||
id="run-workflow"
|
||||
class="float-right"
|
||||
title="Run Workflow"
|
||||
:disabled="!canRunOnHistory"
|
||||
:wait="showExecuting"
|
||||
@onClick="onExecute" />
|
||||
</div>
|
||||
@@ -53,6 +60,7 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { BAlert } from "bootstrap-vue";
|
||||
import ButtonSpinner from "components/Common/ButtonSpinner";
|
||||
import FormCard from "components/Form/FormCard";
|
||||
import FormDisplay from "components/Form/FormDisplay";
|
||||
@@ -70,6 +78,7 @@ import WorkflowRunInputStep from "./WorkflowRunInputStep";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
BAlert,
|
||||
ButtonSpinner,
|
||||
FormDisplay,
|
||||
FormCard,
|
||||
@@ -82,6 +91,10 @@ export default {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
canMutateCurrentHistory: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -140,6 +153,12 @@ export default {
|
||||
wpInputs() {
|
||||
return this.toArray(this.model.wpInputs);
|
||||
},
|
||||
shouldRunOnNewHistory() {
|
||||
return Boolean(this.historyData["new_history|name"]);
|
||||
},
|
||||
canRunOnHistory() {
|
||||
return this.shouldRunOnNewHistory || this.canMutateCurrentHistory;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
reuseAllowed(user) {
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<template>
|
||||
<div>
|
||||
<div v-if="isConfigLoaded" class="h4 clearfix mb-3">
|
||||
<BAlert v-if="!canRunOnHistory" variant="warning" show>
|
||||
<span v-localize>
|
||||
The workflow cannot run because the current history is immutable. Please select a different history
|
||||
or send the results to a new one using the run settings ⚙️
|
||||
</span>
|
||||
</BAlert>
|
||||
<b>Workflow: {{ model.name }}</b>
|
||||
<ButtonSpinner
|
||||
id="run-workflow"
|
||||
:wait="waitingForRequest"
|
||||
:disabled="hasValidationErrors"
|
||||
:disabled="hasValidationErrors || !canRunOnHistory"
|
||||
class="float-right"
|
||||
title="Run Workflow"
|
||||
@onClick="onExecute" />
|
||||
@@ -89,6 +95,10 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
canMutateCurrentHistory: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
setup() {
|
||||
const { config, isConfigLoaded } = useConfig(true);
|
||||
@@ -143,6 +153,9 @@ export default {
|
||||
hasValidationErrors() {
|
||||
return Boolean(Object.values(this.stepValidations).find((value) => value !== null && value !== undefined));
|
||||
},
|
||||
canRunOnHistory() {
|
||||
return this.canMutateCurrentHistory || this.sendToNewHistory;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onValidation(validation) {
|
||||
|
||||
@@ -322,7 +322,7 @@ class HistoryController(BaseUIController, SharableMixin, UsesAnnotations, UsesIt
|
||||
def set_as_current(self, trans, id, **kwargs):
|
||||
"""Change the current user's current history to one with `id`."""
|
||||
try:
|
||||
history = self.history_manager.get_mutable(self.decode_id(id), trans.user, current_history=trans.history)
|
||||
history = self.history_manager.get_owned(self.decode_id(id), trans.user, current_history=trans.history)
|
||||
trans.set_history(history)
|
||||
return self.history_data(trans, history)
|
||||
except exceptions.MessageException as msg_exc:
|
||||
|
||||
@@ -9,8 +9,10 @@ from galaxy import (
|
||||
util,
|
||||
web,
|
||||
)
|
||||
from galaxy.managers.histories import HistoryManager
|
||||
from galaxy.managers.sharable import SlugBuilder
|
||||
from galaxy.model.item_attrs import UsesItemRatings
|
||||
from galaxy.structured_app import StructuredApp
|
||||
from galaxy.tools.parameters.workflow_utils import workflow_building_modes
|
||||
from galaxy.util import FILENAME_VALID_CHARS
|
||||
from galaxy.web import url_for
|
||||
@@ -24,13 +26,18 @@ from galaxy.workflow.extract import (
|
||||
summarize,
|
||||
)
|
||||
from galaxy.workflow.modules import load_module_sections
|
||||
from ..api import depends
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixin, UsesItemRatings):
|
||||
history_manager: HistoryManager = depends(HistoryManager)
|
||||
slug_builder = SlugBuilder()
|
||||
|
||||
def __init__(self, app: StructuredApp):
|
||||
super().__init__(app)
|
||||
|
||||
@web.expose
|
||||
def display_by_username_and_slug(self, trans, username, slug, format="html", **kwargs):
|
||||
"""
|
||||
@@ -304,14 +311,18 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi
|
||||
workflow_name=None,
|
||||
dataset_names=None,
|
||||
dataset_collection_names=None,
|
||||
history_id=None,
|
||||
**kwargs,
|
||||
):
|
||||
user = trans.get_user()
|
||||
history = trans.get_history()
|
||||
history = trans.history
|
||||
if history_id:
|
||||
# Optionally target a different history than the current one.
|
||||
history = self.history_manager.get_owned(self.decode_id(history_id), trans.user, current_history=history)
|
||||
if not user:
|
||||
return trans.show_error_message("Must be logged in to create workflows")
|
||||
if (job_ids is None and dataset_ids is None) or workflow_name is None:
|
||||
jobs, warnings = summarize(trans)
|
||||
jobs, warnings = summarize(trans, history)
|
||||
# Render
|
||||
return trans.fill_template(
|
||||
"workflow/build_from_current_history.mako", jobs=jobs, warnings=warnings, history=history
|
||||
@@ -324,6 +335,7 @@ class WorkflowController(BaseUIController, SharableMixin, UsesStoredWorkflowMixi
|
||||
stored_workflow = extract_workflow(
|
||||
trans,
|
||||
user=user,
|
||||
history=history,
|
||||
job_ids=job_ids,
|
||||
dataset_ids=dataset_ids,
|
||||
dataset_collection_ids=dataset_collection_ids,
|
||||
|
||||
Reference in New Issue
Block a user