mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
Merge pull request #17225 from jmchilton/page_permissions
Implement a page object accessibility dialog
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { CollectionEntry, DCESummary, HDCADetailed, isHDCA } from "@/api";
|
||||
import { CollectionEntry, DCESummary, HDCADetailed, HDCASummary, isHDCA } from "@/api";
|
||||
import { fetcher } from "@/api/schema";
|
||||
|
||||
const DEFAULT_LIMIT = 50;
|
||||
@@ -11,7 +11,16 @@ const getCollectionDetails = fetcher.path("/api/dataset_collections/{id}").metho
|
||||
*/
|
||||
export async function fetchCollectionDetails(params: { id: string }): Promise<HDCADetailed> {
|
||||
const { data } = await getCollectionDetails({ id: params.id });
|
||||
return data;
|
||||
return data as HDCADetailed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches the details of a collection.
|
||||
* @param params.id The ID of the collection (HDCA) to fetch.
|
||||
*/
|
||||
export async function fetchCollectionSummary(params: { id: string }): Promise<HDCASummary> {
|
||||
const { data } = await getCollectionDetails({ id: params.id, view: "collection" });
|
||||
return data as HDCASummary;
|
||||
}
|
||||
|
||||
const getCollectionContents = fetcher
|
||||
|
||||
@@ -12,3 +12,5 @@ export const updateHistoryItemsInBulk = fetcher
|
||||
.path("/api/histories/{history_id}/contents/bulk")
|
||||
.method("put")
|
||||
.create();
|
||||
export const sharing = fetcher.path("/api/histories/{history_id}/sharing").method("get").create();
|
||||
export const enableLink = fetcher.path("/api/histories/{history_id}/enable_link_access").method("put").create();
|
||||
|
||||
@@ -2,6 +2,8 @@ import { components, fetcher } from "@/api/schema";
|
||||
|
||||
export type JobDestinationParams = components["schemas"]["JobDestinationParams"];
|
||||
|
||||
export const getJobDetails = fetcher.path("/api/jobs/{job_id}").method("get").create();
|
||||
|
||||
export const jobLockStatus = fetcher.path("/api/job_lock").method("get").create();
|
||||
export const jobLockUpdate = fetcher.path("/api/job_lock").method("put").create();
|
||||
|
||||
|
||||
@@ -13341,8 +13341,10 @@ export interface operations {
|
||||
/** Returns detailed information about the given collection. */
|
||||
parameters: {
|
||||
/** @description The type of collection instance. Either `history` (default) or `library`. */
|
||||
/** @description The view of collection instance to return. */
|
||||
query?: {
|
||||
instance_type?: "history" | "library";
|
||||
view?: string;
|
||||
};
|
||||
/** @description The user ID that will be used to effectively make this API call. Only admins and designated users can make API calls on behalf of other users. */
|
||||
header?: {
|
||||
@@ -13357,7 +13359,7 @@ export interface operations {
|
||||
/** @description Successful Response */
|
||||
200: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["HDCADetailed"];
|
||||
"application/json": components["schemas"]["HDCADetailed"] | components["schemas"]["HDCASummary"];
|
||||
};
|
||||
};
|
||||
/** @description Validation Error */
|
||||
|
||||
@@ -3,3 +3,6 @@ import { fetcher } from "@/api/schema";
|
||||
export const workflowsFetcher = fetcher.path("/api/workflows").method("get").create();
|
||||
|
||||
export const invocationCountsFetcher = fetcher.path("/api/workflows/{workflow_id}/counts").method("get").create();
|
||||
|
||||
export const sharing = fetcher.path("/api/workflows/{workflow_id}/sharing").method("get").create();
|
||||
export const enableLink = fetcher.path("/api/workflows/{workflow_id}/enable_link_access").method("put").create();
|
||||
|
||||
@@ -168,3 +168,48 @@ function namedArgumentRegex(argument: string): RegExp {
|
||||
function escapeRegExpReplacement(value: string): string {
|
||||
return value.replace(/\$/g, "$$$$");
|
||||
}
|
||||
|
||||
class ReferencedObjects {
|
||||
jobs: Set<string> = new Set();
|
||||
historyDatasets: Set<string> = new Set();
|
||||
historyDatasetCollections: Set<string> = new Set();
|
||||
workflows: Set<string> = new Set();
|
||||
invocations: Set<string> = new Set();
|
||||
}
|
||||
|
||||
export function referencedObjects(markdown: string) {
|
||||
const { sections } = splitMarkdown(markdown);
|
||||
const objects = new ReferencedObjects();
|
||||
for (const section of sections) {
|
||||
if (!("args" in section)) {
|
||||
continue;
|
||||
}
|
||||
const args = section.args;
|
||||
if (!args) {
|
||||
continue;
|
||||
}
|
||||
if ("job_id" in args) {
|
||||
addToSetIfHasValue(args.job_id, objects.jobs);
|
||||
}
|
||||
if ("history_dataset_id" in args) {
|
||||
addToSetIfHasValue(args.history_dataset_id, objects.historyDatasets);
|
||||
}
|
||||
if ("history_dataset_collection_id" in args) {
|
||||
addToSetIfHasValue(args.history_dataset_collection_id, objects.historyDatasetCollections);
|
||||
}
|
||||
if ("invocation_id" in args) {
|
||||
addToSetIfHasValue(args.invocation_id, objects.invocations);
|
||||
}
|
||||
if ("workflow_id" in args) {
|
||||
addToSetIfHasValue(args.workflow_id, objects.workflows);
|
||||
}
|
||||
// TODO: implicit collect job ids
|
||||
}
|
||||
return objects;
|
||||
}
|
||||
|
||||
function addToSetIfHasValue(value: string | undefined, toSet: Set<string>): void {
|
||||
if (value) {
|
||||
toSet.add(value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,311 @@
|
||||
<script setup lang="ts">
|
||||
import axios from "axios";
|
||||
import Vue, { computed, Ref, ref, watch } from "vue";
|
||||
|
||||
import { fetchCollectionSummary } from "@/api/datasetCollections";
|
||||
import { enableLink, sharing } from "@/api/histories";
|
||||
import { fetchInvocationDetails } from "@/api/invocations";
|
||||
import { getJobDetails } from "@/api/jobs";
|
||||
import { enableLink as enableLinkWorkflow, sharing as sharingWorkflow } from "@/api/workflows";
|
||||
import { useToast } from "@/composables/toast";
|
||||
import { useDatasetStore } from "@/stores/datasetStore";
|
||||
import { useHistoryStore } from "@/stores/historyStore";
|
||||
import { useWorkflowStore } from "@/stores/workflowStore";
|
||||
import _l from "@/utils/localization";
|
||||
import { withPrefix } from "@/utils/redirect";
|
||||
import { errorMessageAsString } from "@/utils/simple-error";
|
||||
|
||||
import {
|
||||
initializeObjectReferences,
|
||||
initializeObjectToHistoryRefs,
|
||||
updateReferences,
|
||||
} from "./object-permission-composables";
|
||||
|
||||
import PermissionObjectType from "./PermissionObjectType.vue";
|
||||
import SharingIndicator from "./SharingIndicator.vue";
|
||||
import LoadingSpan from "@/components/LoadingSpan.vue";
|
||||
|
||||
const { getHistoryNameById, loadHistoryById } = useHistoryStore();
|
||||
const { getStoredWorkflowNameByInstanceId, fetchWorkflowForInstanceId } = useWorkflowStore();
|
||||
const { getDataset, fetchDataset } = useDatasetStore();
|
||||
|
||||
const toast = useToast();
|
||||
|
||||
interface ObjectPermissionsProps {
|
||||
markdownContent: string;
|
||||
}
|
||||
|
||||
const props = defineProps<ObjectPermissionsProps>();
|
||||
|
||||
const referencedObjects = initializeObjectReferences();
|
||||
const {
|
||||
referencedJobIds,
|
||||
referencedHistoryDatasetIds,
|
||||
referencedHistoryDatasetCollectionIds,
|
||||
referencedWorkflowIds,
|
||||
referencedInvocationIds,
|
||||
} = referencedObjects;
|
||||
|
||||
// We mostly defer to history permissions for various objects. Track them and merge
|
||||
// into histories IDs.
|
||||
const { jobsToHistories, invocationsToHistories, historyDatasetCollectionsToHistories, historyIds } =
|
||||
initializeObjectToHistoryRefs(referencedObjects);
|
||||
|
||||
type ErrorString = string;
|
||||
type AccessibleState = Boolean | null | ErrorString;
|
||||
type AccessibleMapRef = Ref<{ [key: string]: AccessibleState }>;
|
||||
const historyAccessible: AccessibleMapRef = ref({});
|
||||
const workflowAccessible: AccessibleMapRef = ref({});
|
||||
const historyDatasetAccessible: AccessibleMapRef = ref({});
|
||||
|
||||
function catchErrorToToast(title: string, prolog: string) {
|
||||
function handleError(e: Error) {
|
||||
toast.error(`${prolog} Reason: ${errorMessageAsString(e)}.`, title);
|
||||
}
|
||||
return handleError;
|
||||
}
|
||||
|
||||
watch(referencedJobIds, async () => {
|
||||
referencedJobIds.value.forEach((jobId) => {
|
||||
if (jobId in jobsToHistories.value) {
|
||||
return;
|
||||
}
|
||||
const handleError = catchErrorToToast(
|
||||
"Failed to job information",
|
||||
"Some referenced objects may not be listed."
|
||||
);
|
||||
getJobDetails({ job_id: jobId })
|
||||
.then(({ data }) => {
|
||||
if ("history_id" in data) {
|
||||
const historyId = data.history_id;
|
||||
Vue.set(jobsToHistories.value, jobId, historyId);
|
||||
}
|
||||
})
|
||||
.catch(handleError);
|
||||
});
|
||||
});
|
||||
|
||||
watch(referencedInvocationIds, async () => {
|
||||
referencedInvocationIds.value.forEach((invocationId) => {
|
||||
if (invocationId in invocationsToHistories.value) {
|
||||
return;
|
||||
}
|
||||
const handleError = catchErrorToToast(
|
||||
"Failed to fetch workflow information",
|
||||
"Some referenced objects may not be listed."
|
||||
);
|
||||
fetchInvocationDetails({ id: invocationId })
|
||||
.then(({ data }) => {
|
||||
if ("history_id" in data) {
|
||||
const historyId = data.history_id;
|
||||
Vue.set(invocationsToHistories.value, invocationId, historyId);
|
||||
}
|
||||
})
|
||||
.catch(handleError);
|
||||
});
|
||||
});
|
||||
|
||||
watch(referencedHistoryDatasetCollectionIds, async () => {
|
||||
referencedHistoryDatasetCollectionIds.value.forEach((historyDatasetCollectionId) => {
|
||||
if (historyDatasetCollectionId in historyDatasetCollectionsToHistories.value) {
|
||||
return;
|
||||
}
|
||||
const handleError = catchErrorToToast(
|
||||
"Failed to fetch collection information",
|
||||
"Some referenced objects may not be listed."
|
||||
);
|
||||
fetchCollectionSummary({ id: historyDatasetCollectionId })
|
||||
.then((data) => {
|
||||
const historyId = data.history_id;
|
||||
Vue.set(historyDatasetCollectionsToHistories.value, historyDatasetCollectionId, historyId);
|
||||
})
|
||||
.catch(handleError);
|
||||
});
|
||||
});
|
||||
|
||||
interface ItemInterface {
|
||||
id: string;
|
||||
accessible: Boolean | null;
|
||||
name: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const histories = computed<ItemInterface[]>(() => {
|
||||
return historyIds.value.map((historyId: string) => {
|
||||
return {
|
||||
id: historyId,
|
||||
type: "history",
|
||||
name: getHistoryNameById(historyId),
|
||||
accessible: historyAccessible.value[historyId],
|
||||
} as ItemInterface;
|
||||
});
|
||||
});
|
||||
|
||||
const workflows = computed<ItemInterface[]>(() => {
|
||||
return referencedWorkflowIds.value.map((workflowId: string) => {
|
||||
return {
|
||||
id: workflowId,
|
||||
type: "workflow",
|
||||
name: getStoredWorkflowNameByInstanceId(workflowId),
|
||||
accessible: workflowAccessible.value[workflowId],
|
||||
} as ItemInterface;
|
||||
});
|
||||
});
|
||||
|
||||
const datasets = computed<ItemInterface[]>(() => {
|
||||
return referencedHistoryDatasetIds.value.map((historyDatasetId: string) => {
|
||||
return {
|
||||
id: historyDatasetId,
|
||||
type: "historyDataset",
|
||||
name: getDataset(historyDatasetId)?.name || "Fetching dataset name...",
|
||||
accessible: historyDatasetAccessible.value[historyDatasetId],
|
||||
} as ItemInterface;
|
||||
});
|
||||
});
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const SHARING_FIELD = { key: "accessible", label: _l("Accessible"), sortable: false, thStyle: { width: "10%" } };
|
||||
const NAME_FIELD = { key: "name", label: _l("Name"), sortable: true };
|
||||
const TYPE_FIELD = { key: "type", label: _l("Type"), sortable: true, thStyle: { width: "10%" } };
|
||||
|
||||
const tableFields = [SHARING_FIELD, TYPE_FIELD, NAME_FIELD];
|
||||
|
||||
watch(
|
||||
props,
|
||||
() => {
|
||||
updateReferences(referencedObjects, props.markdownContent);
|
||||
initWorkflowData();
|
||||
initHistoryDatasetData();
|
||||
},
|
||||
{ immediate: true }
|
||||
);
|
||||
|
||||
watch(historyIds, () => {
|
||||
for (const historyId of historyIds.value) {
|
||||
loadHistoryById(historyId);
|
||||
if (historyId && !(historyId in historyAccessible.value)) {
|
||||
Vue.set(historyAccessible.value, historyId, null);
|
||||
sharing({ history_id: historyId })
|
||||
.then((response) => {
|
||||
const accessible = response.data.importable;
|
||||
Vue.set(historyAccessible.value, historyId, accessible);
|
||||
})
|
||||
.catch((e) => {
|
||||
const errorMessage = errorMessageAsString(e);
|
||||
const title = "Failed to fetch history metadata.";
|
||||
toast.error(errorMessage, title);
|
||||
Vue.set(historyAccessible.value, historyId, `${title} Reason: ${errorMessage}.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function initWorkflowData() {
|
||||
for (const workflowId of referencedWorkflowIds.value) {
|
||||
fetchWorkflowForInstanceId(workflowId);
|
||||
if (workflowId && !(workflowId in workflowAccessible.value)) {
|
||||
Vue.set(workflowAccessible.value, workflowId, null);
|
||||
sharingWorkflow({ workflow_id: workflowId })
|
||||
.then((response) => {
|
||||
const accessible = response.data.importable;
|
||||
Vue.set(workflowAccessible.value, workflowId, accessible);
|
||||
})
|
||||
.catch((e) => {
|
||||
const errorMessage = errorMessageAsString(e);
|
||||
const title = "Failed to fetch workflow metadata.";
|
||||
toast.error(errorMessage, title);
|
||||
Vue.set(workflowAccessible.value, workflowId, `${title} Reason: ${errorMessage}.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initHistoryDatasetData() {
|
||||
for (const historyDatasetId of referencedHistoryDatasetIds.value) {
|
||||
fetchDataset({ id: historyDatasetId });
|
||||
if (historyDatasetId && !(historyDatasetId in historyDatasetAccessible.value)) {
|
||||
axios
|
||||
.get(withPrefix(`/dataset/get_edit?dataset_id=${historyDatasetId}`))
|
||||
.then((response) => {
|
||||
const permissionDisable = response.data.permission_disable;
|
||||
const permissionInputs = response.data.permission_inputs;
|
||||
if (permissionDisable) {
|
||||
const errorStr = `Cannot modify permissions of this dataset. Reason: ${permissionInputs[0].label}`;
|
||||
Vue.set(historyDatasetAccessible.value, historyDatasetId, errorStr);
|
||||
return;
|
||||
}
|
||||
const accessPermissionInput = permissionInputs[1];
|
||||
if (accessPermissionInput.name != "DATASET_ACCESS") {
|
||||
throw Error("Galaxy Bug");
|
||||
}
|
||||
const accessible = (accessPermissionInput.value || []).length == 0;
|
||||
Vue.set(historyDatasetAccessible.value, historyDatasetId, accessible);
|
||||
})
|
||||
.catch((e) => {
|
||||
const errorMessage = errorMessageAsString(e);
|
||||
const title = "Failed to fetch dataset metadata.";
|
||||
toast.error(errorMessage, title);
|
||||
Vue.set(historyDatasetAccessible.value, historyDatasetId, `${title} Reason: ${errorMessage}.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const tableItems = computed<ItemInterface[]>(() => {
|
||||
return [...histories.value, ...workflows.value, ...datasets.value];
|
||||
});
|
||||
|
||||
function makeAccessible(item: ItemInterface) {
|
||||
let promise;
|
||||
let accessibleMap: AccessibleMapRef;
|
||||
if (item.type == "history") {
|
||||
promise = enableLink({ history_id: item.id });
|
||||
accessibleMap = historyAccessible;
|
||||
} else if (item.type == "workflow") {
|
||||
promise = enableLinkWorkflow({ workflow_id: item.id });
|
||||
accessibleMap = workflowAccessible;
|
||||
} else if (item.type == "historyDataset") {
|
||||
const data = {
|
||||
dataset_id: item.id,
|
||||
action: "remove_restrictions",
|
||||
};
|
||||
promise = axios.put(withPrefix(`/api/datasets/${item.id}/permissions`), data);
|
||||
accessibleMap = historyDatasetAccessible;
|
||||
}
|
||||
if (!promise) {
|
||||
console.log("Serious client programming error - unknown object type encountered.");
|
||||
return;
|
||||
}
|
||||
promise
|
||||
.then(() => Vue.set(accessibleMap.value, item.id, true))
|
||||
.catch((e) => {
|
||||
const errorMessage = errorMessageAsString(e);
|
||||
const title = "Failed update object accessibility.";
|
||||
toast.error(errorMessage, title);
|
||||
Vue.set(accessibleMap.value, item.id, `${title} Reason: ${errorMessage}.`);
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<b-table :items="tableItems" :fields="tableFields">
|
||||
<template v-slot:empty>
|
||||
<LoadingSpan v-if="loading" message="Loading objects" />
|
||||
<b-alert v-else variant="info" show>
|
||||
<div>No objects found in referenced Galaxy markdown content.</div>
|
||||
</b-alert>
|
||||
</template>
|
||||
<template v-slot:cell(name)="row">
|
||||
{{ row.item.name }}
|
||||
</template>
|
||||
<template v-slot:cell(accessible)="row">
|
||||
<SharingIndicator :accessible="row.item.accessible" @makeAccessible="makeAccessible(row.item)" />
|
||||
</template>
|
||||
<template v-slot:cell(type)="row">
|
||||
<PermissionObjectType :type="row.item.type" />
|
||||
</template>
|
||||
</b-table>
|
||||
</div>
|
||||
</template>
|
||||
@@ -0,0 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import ObjectPermissions from "./ObjectPermissions.vue";
|
||||
|
||||
interface ObjectPermissionsProps {
|
||||
markdownContent: string;
|
||||
}
|
||||
|
||||
defineProps<ObjectPermissionsProps>();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<b-modal v-bind="$attrs" title="Page Object Permissions" title-tag="h2" ok-only v-on="$listeners">
|
||||
<ObjectPermissions :markdown-content="markdownContent" />
|
||||
</b-modal>
|
||||
</template>
|
||||
@@ -6,6 +6,20 @@
|
||||
mode="page"
|
||||
@onUpdate="onUpdate">
|
||||
<template v-slot:buttons>
|
||||
<ObjectPermissionsModal
|
||||
id="object-permissions-modal"
|
||||
v-model="showPermissions"
|
||||
:markdown-content="markdownText" />
|
||||
<b-button
|
||||
id="permissions-button"
|
||||
v-b-tooltip.hover.bottom
|
||||
v-b-modal:object-permissions-modal
|
||||
title="Permissions"
|
||||
variant="link"
|
||||
role="button"
|
||||
@click="showPermissions = true">
|
||||
<FontAwesomeIcon icon="users" />
|
||||
</b-button>
|
||||
<b-button
|
||||
id="save-button"
|
||||
v-b-tooltip.hover.bottom
|
||||
@@ -30,7 +44,7 @@
|
||||
|
||||
<script>
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faEye, faSave } from "@fortawesome/free-solid-svg-icons";
|
||||
import { faEye, faSave, faUsers } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import BootstrapVue from "bootstrap-vue";
|
||||
import MarkdownEditor from "components/Markdown/MarkdownEditor";
|
||||
@@ -39,14 +53,17 @@ import Vue from "vue";
|
||||
|
||||
import { save } from "./util";
|
||||
|
||||
import ObjectPermissionsModal from "./ObjectPermissionsModal.vue";
|
||||
|
||||
Vue.use(BootstrapVue);
|
||||
|
||||
library.add(faEye, faSave);
|
||||
library.add(faEye, faSave, faUsers);
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MarkdownEditor,
|
||||
FontAwesomeIcon,
|
||||
ObjectPermissionsModal,
|
||||
},
|
||||
props: {
|
||||
pageId: {
|
||||
@@ -73,6 +90,7 @@ export default {
|
||||
data: function () {
|
||||
return {
|
||||
markdownText: this.content,
|
||||
showPermissions: false,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from "vue";
|
||||
|
||||
interface PermissionObjectTypeProps {
|
||||
type: "history" | "historyDataset" | "workflow";
|
||||
}
|
||||
|
||||
const props = defineProps<PermissionObjectTypeProps>();
|
||||
|
||||
const typeTitle = computed(() => {
|
||||
if (props.type == "history") {
|
||||
return "History";
|
||||
} else if (props.type == "historyDataset") {
|
||||
return "Dataset";
|
||||
} else {
|
||||
return "Workflow";
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<span class="object-type">
|
||||
{{ typeTitle }}
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.object-type {
|
||||
text-decoration: underline;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,70 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
|
||||
import LoadingSpan from "@/components/LoadingSpan.vue";
|
||||
|
||||
interface SharingIndicatorProps {
|
||||
accessible: Boolean | String | null;
|
||||
}
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: "makeAccessible"): void;
|
||||
}>();
|
||||
|
||||
const props = defineProps<SharingIndicatorProps>();
|
||||
|
||||
const makingAccessible = ref(false);
|
||||
|
||||
function makeAccessible() {
|
||||
makingAccessible.value = true;
|
||||
emit("makeAccessible");
|
||||
}
|
||||
|
||||
watch(props, () => {
|
||||
makingAccessible.value = false;
|
||||
});
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
<LoadingSpan v-if="accessible == null || makingAccessible" spinner-only> </LoadingSpan>
|
||||
<div v-else-if="typeof accessible == 'string'">
|
||||
<Icon
|
||||
:title="accessible"
|
||||
icon="exclamation-triangle"
|
||||
class="make-page-object-accessible-sharing-error-icon" />
|
||||
</div>
|
||||
<BFormCheckbox
|
||||
v-else
|
||||
switch
|
||||
:checked="accessible"
|
||||
class="make-page-object-accessible"
|
||||
:disabled="accessible"
|
||||
@change="
|
||||
(event) => {
|
||||
makeAccessible(event);
|
||||
}
|
||||
">
|
||||
</BFormCheckbox>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
/* scoped doesn't seem to work to prefixing with a class name */
|
||||
@import "theme/blue.scss";
|
||||
|
||||
.make-page-object-accessible-sharing-error-icon {
|
||||
color: $brand-danger;
|
||||
}
|
||||
|
||||
.make-page-object-accessible .custom-control-label::after {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.make-page-object-accessible .custom-control-label::before {
|
||||
background-color: lighten($brand-warning, 15%) !important;
|
||||
}
|
||||
|
||||
.make-page-object-accessible .custom-control-input[disabled] ~ .custom-control-label::before {
|
||||
background-color: lighten($brand-success, 15%) !important;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,81 @@
|
||||
import { set } from "vue";
|
||||
|
||||
import {
|
||||
initializeObjectReferences,
|
||||
initializeObjectToHistoryRefs,
|
||||
updateReferences,
|
||||
} from "./object-permission-composables";
|
||||
|
||||
describe("object-permission-composables", () => {
|
||||
it("should initialize and update refs in a markdown document", () => {
|
||||
const refs = initializeObjectReferences();
|
||||
expect(refs.referencedJobIds.value.length).toBe(0);
|
||||
|
||||
updateReferences(refs, "some content\n```galaxy\njob_metrics(job_id=THISFAKEID)\n```\nfoo bar\n");
|
||||
expect(refs.referencedJobIds.value.length).toBe(1);
|
||||
expect(refs.referencedJobIds.value[0]).toBe("THISFAKEID");
|
||||
});
|
||||
|
||||
describe("initializeObjectToHistoryRefs", () => {
|
||||
function init() {
|
||||
const refs = initializeObjectReferences();
|
||||
const historyMaps = initializeObjectToHistoryRefs(refs);
|
||||
return { refs, historyMaps };
|
||||
}
|
||||
|
||||
describe("historyIds computed", () => {
|
||||
it("should merge in referenced job histories once they've been cached", async () => {
|
||||
const { refs, historyMaps } = init();
|
||||
refs.referencedJobIds.value = ["THISFAKEID"];
|
||||
expect(historyMaps.historyIds.value.length).toBe(0);
|
||||
set(historyMaps.jobsToHistories.value, "THISFAKEID", "THATFAKEID");
|
||||
expect(historyMaps.historyIds.value.length).toBe(1);
|
||||
expect(historyMaps.historyIds.value[0]).toBe("THATFAKEID");
|
||||
});
|
||||
|
||||
it("should merge in referenced invocation histories once they've been cached", async () => {
|
||||
const { refs, historyMaps } = init();
|
||||
refs.referencedInvocationIds.value = ["THISFAKEID"];
|
||||
expect(historyMaps.historyIds.value.length).toBe(0);
|
||||
set(historyMaps.invocationsToHistories.value, "THISFAKEID", "THATFAKEID");
|
||||
expect(historyMaps.historyIds.value.length).toBe(1);
|
||||
expect(historyMaps.historyIds.value[0]).toBe("THATFAKEID");
|
||||
});
|
||||
|
||||
it("should merge in referenced collections once they've been cached", async () => {
|
||||
const { refs, historyMaps } = init();
|
||||
refs.referencedHistoryDatasetCollectionIds.value = ["THISFAKEID"];
|
||||
expect(historyMaps.historyIds.value.length).toBe(0);
|
||||
set(historyMaps.historyDatasetCollectionsToHistories.value, "THISFAKEID", "THATFAKEID");
|
||||
expect(historyMaps.historyIds.value.length).toBe(1);
|
||||
expect(historyMaps.historyIds.value[0]).toBe("THATFAKEID");
|
||||
});
|
||||
|
||||
it("should merge in referenced objects across sources once they've been cached", async () => {
|
||||
const { refs, historyMaps } = init();
|
||||
refs.referencedJobIds.value = ["THISFAKEJOBID"];
|
||||
refs.referencedInvocationIds.value = ["THISFAKEINVOCATIONID"];
|
||||
refs.referencedHistoryDatasetCollectionIds.value = ["THISFAKECOLLECTIONID"];
|
||||
set(historyMaps.jobsToHistories.value, "THISFAKEJOBID", "HISTORYID1");
|
||||
set(historyMaps.invocationsToHistories.value, "THISFAKEINVOCATIONID", "HISTORYID2");
|
||||
set(historyMaps.historyDatasetCollectionsToHistories.value, "THISFAKECOLLECTIONID", "HISTORYID3");
|
||||
expect(historyMaps.historyIds.value.length).toBe(3);
|
||||
expect(historyMaps.historyIds.value).toContain("HISTORYID1");
|
||||
expect(historyMaps.historyIds.value).toContain("HISTORYID2");
|
||||
expect(historyMaps.historyIds.value).toContain("HISTORYID3");
|
||||
});
|
||||
|
||||
it("should merge in referenced objects across sources once they've been cached and de-duplicate", async () => {
|
||||
const { refs, historyMaps } = init();
|
||||
refs.referencedJobIds.value = ["THISFAKEJOBID"];
|
||||
refs.referencedInvocationIds.value = ["THISFAKEINVOCATIONID"];
|
||||
refs.referencedHistoryDatasetCollectionIds.value = ["THISFAKECOLLECTIONID"];
|
||||
set(historyMaps.jobsToHistories.value, "THISFAKEJOBID", "THATFAKEID");
|
||||
set(historyMaps.invocationsToHistories.value, "THISFAKEINVOCATIONID", "THATFAKEID");
|
||||
set(historyMaps.historyDatasetCollectionsToHistories.value, "THISFAKECOLLECTIONID", "THATFAKEID");
|
||||
expect(historyMaps.historyIds.value.length).toBe(1);
|
||||
expect(historyMaps.historyIds.value[0]).toBe("THATFAKEID");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
import { computed, Ref, ref } from "vue";
|
||||
|
||||
import { referencedObjects } from "@/components/Markdown/parse";
|
||||
|
||||
interface ObjectVueRefs {
|
||||
referencedJobIds: Ref<string[]>;
|
||||
referencedHistoryDatasetIds: Ref<string[]>;
|
||||
referencedHistoryDatasetCollectionIds: Ref<string[]>;
|
||||
referencedWorkflowIds: Ref<string[]>;
|
||||
referencedInvocationIds: Ref<string[]>;
|
||||
}
|
||||
|
||||
export function initializeObjectReferences(): ObjectVueRefs {
|
||||
const referencedJobIds = ref<string[]>([]);
|
||||
const referencedHistoryDatasetIds = ref<string[]>([]);
|
||||
const referencedHistoryDatasetCollectionIds = ref<string[]>([]);
|
||||
const referencedWorkflowIds = ref<string[]>([]);
|
||||
const referencedInvocationIds = ref<string[]>([]);
|
||||
return {
|
||||
referencedJobIds,
|
||||
referencedHistoryDatasetIds,
|
||||
referencedHistoryDatasetCollectionIds,
|
||||
referencedWorkflowIds,
|
||||
referencedInvocationIds,
|
||||
};
|
||||
}
|
||||
|
||||
export function updateReferences(objectsRefs: ObjectVueRefs, markdown: string) {
|
||||
const {
|
||||
referencedJobIds,
|
||||
referencedHistoryDatasetIds,
|
||||
referencedHistoryDatasetCollectionIds,
|
||||
referencedWorkflowIds,
|
||||
referencedInvocationIds,
|
||||
} = objectsRefs;
|
||||
|
||||
const objects = referencedObjects(markdown);
|
||||
|
||||
referencedJobIds.value = Array.from(objects.jobs.values());
|
||||
referencedHistoryDatasetIds.value = Array.from(objects.historyDatasets.values());
|
||||
referencedHistoryDatasetCollectionIds.value = Array.from(objects.historyDatasetCollections.values());
|
||||
referencedWorkflowIds.value = Array.from(objects.workflows.values());
|
||||
referencedInvocationIds.value = Array.from(objects.invocations.values());
|
||||
}
|
||||
|
||||
export function initializeObjectToHistoryRefs(referenceObjects: ObjectVueRefs) {
|
||||
const jobsToHistories: Ref<{ [key: string]: string }> = ref({});
|
||||
const invocationsToHistories: Ref<{ [key: string]: string }> = ref({});
|
||||
const historyDatasetCollectionsToHistories: Ref<{ [key: string]: string }> = ref({});
|
||||
|
||||
const historyIds = computed<string[]>(() => {
|
||||
// be sure to reference all refs required for full computation
|
||||
const jobIds = referenceObjects.referencedJobIds.value;
|
||||
const jobMapping = jobsToHistories.value;
|
||||
|
||||
const invocationIds = referenceObjects.referencedInvocationIds.value;
|
||||
const invocationMapping = invocationsToHistories.value;
|
||||
|
||||
const collectionIds = referenceObjects.referencedHistoryDatasetCollectionIds.value;
|
||||
const collectionMapping = historyDatasetCollectionsToHistories.value;
|
||||
|
||||
const theHistories = new Set();
|
||||
|
||||
for (const jobId of jobIds) {
|
||||
if (jobId in jobMapping) {
|
||||
theHistories.add(jobMapping[jobId]);
|
||||
}
|
||||
}
|
||||
for (const invocationId of invocationIds) {
|
||||
if (invocationId in invocationMapping) {
|
||||
theHistories.add(invocationMapping[invocationId]);
|
||||
}
|
||||
}
|
||||
for (const historyDatasetCollectionId of collectionIds) {
|
||||
if (historyDatasetCollectionId in collectionMapping) {
|
||||
theHistories.add(collectionMapping[historyDatasetCollectionId]);
|
||||
}
|
||||
}
|
||||
const historyIds = Array.from(theHistories.values()) as string[];
|
||||
return historyIds;
|
||||
});
|
||||
|
||||
return {
|
||||
jobsToHistories,
|
||||
invocationsToHistories,
|
||||
historyDatasetCollectionsToHistories,
|
||||
historyIds,
|
||||
};
|
||||
}
|
||||
@@ -11,6 +11,7 @@ from typing_extensions import Annotated
|
||||
from galaxy.managers.context import ProvidesHistoryContext
|
||||
from galaxy.schema.fields import DecodedDatabaseIdField
|
||||
from galaxy.schema.schema import (
|
||||
AnyHDCA,
|
||||
CreateNewCollectionPayload,
|
||||
DatasetCollectionInstanceType,
|
||||
DCESummary,
|
||||
@@ -43,6 +44,11 @@ InstanceTypeQueryParam: DatasetCollectionInstanceType = Query(
|
||||
description="The type of collection instance. Either `history` (default) or `library`.",
|
||||
)
|
||||
|
||||
ViewTypeQueryParam: str = Query(
|
||||
default="element",
|
||||
description="The view of collection instance to return.",
|
||||
)
|
||||
|
||||
|
||||
@router.cbv
|
||||
class FastAPIDatasetCollections:
|
||||
@@ -104,8 +110,9 @@ class FastAPIDatasetCollections:
|
||||
id: HistoryHDCAIDPathParam,
|
||||
trans: ProvidesHistoryContext = DependsOnTrans,
|
||||
instance_type: DatasetCollectionInstanceType = InstanceTypeQueryParam,
|
||||
) -> HDCADetailed:
|
||||
return self.service.show(trans, id, instance_type)
|
||||
view: str = ViewTypeQueryParam,
|
||||
) -> AnyHDCA:
|
||||
return self.service.show(trans, id, instance_type, view=view)
|
||||
|
||||
@router.get(
|
||||
"/api/dataset_collections/{hdca_id}/contents/{parent_id}",
|
||||
|
||||
@@ -32,6 +32,7 @@ from galaxy.schema.fields import (
|
||||
ModelClassField,
|
||||
)
|
||||
from galaxy.schema.schema import (
|
||||
AnyHDCA,
|
||||
CreateNewCollectionPayload,
|
||||
DatasetCollectionInstanceType,
|
||||
DCESummary,
|
||||
@@ -184,7 +185,8 @@ class DatasetCollectionsService(ServiceBase, UsesLibraryMixinItems):
|
||||
trans: ProvidesHistoryContext,
|
||||
id: DecodedDatabaseIdField,
|
||||
instance_type: DatasetCollectionInstanceType = "history",
|
||||
) -> HDCADetailed:
|
||||
view: str = "element",
|
||||
) -> AnyHDCA:
|
||||
"""
|
||||
Returns information about a particular dataset collection.
|
||||
"""
|
||||
@@ -203,7 +205,7 @@ class DatasetCollectionsService(ServiceBase, UsesLibraryMixinItems):
|
||||
security=trans.security,
|
||||
url_builder=trans.url_builder,
|
||||
parent=parent,
|
||||
view="element",
|
||||
view=view,
|
||||
)
|
||||
return rval
|
||||
|
||||
|
||||
Reference in New Issue
Block a user