Skip to content

Commit

Permalink
Page object permissions summary...
Browse files Browse the repository at this point in the history
  • Loading branch information
jmchilton committed Jan 12, 2024
1 parent 13357e3 commit 88ab222
Show file tree
Hide file tree
Showing 14 changed files with 689 additions and 8 deletions.
13 changes: 11 additions & 2 deletions client/src/api/datasetCollections.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
Expand Down
3 changes: 3 additions & 0 deletions client/src/api/histories.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,6 @@ export const historiesFetcher = fetcher.path("/api/histories").method("get").cre
export const archivedHistoriesFetcher = fetcher.path("/api/histories/archived").method("get").create();
export const undeleteHistory = fetcher.path("/api/histories/deleted/{history_id}/undelete").method("post").create();
export const purgeHistory = fetcher.path("/api/histories/{history_id}").method("delete").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 changes: 2 additions & 0 deletions client/src/api/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
4 changes: 4 additions & 0 deletions client/src/api/workflows.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { fetcher } from "@/api/schema";

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();
45 changes: 45 additions & 0 deletions client/src/components/Markdown/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,48 @@ export function getArgs(content: string) {
content: content,
};
}

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, toSet: Set<string>): void {
if (value) {
toSet.add(value);
}
}
305 changes: 305 additions & 0 deletions client/src/components/PageEditor/ObjectPermissions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,305 @@
<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 permissionInputs = response.data.permission_inputs;
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>
Loading

0 comments on commit 88ab222

Please sign in to comment.