Skip to content

Commit

Permalink
Page object permissions - WIP
Browse files Browse the repository at this point in the history
  • Loading branch information
jmchilton committed Dec 21, 2023
1 parent 941a40f commit e2040b0
Show file tree
Hide file tree
Showing 8 changed files with 348 additions and 6 deletions.
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();
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);
}
}
209 changes: 209 additions & 0 deletions client/src/components/PageEditor/ObjectPermissions.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<script setup lang="ts">
import axios from "axios";
import Vue, { computed, Ref, ref, watch } from "vue";
import { enableLink, sharing } from "@/api/histories";
import { referencedObjects } from "@/components/Markdown/parse";
import { useHistoryStore } from "@/stores/historyStore";
import _l from "@/utils/localization";
import { withPrefix } from "@/utils/redirect";
import SharingIndicator from "./SharingIndicator.vue";
import LoadingSpan from "@/components/LoadingSpan.vue";
const { getHistoryNameById, loadHistoryById } = useHistoryStore();
interface ObjectPermissionsProps {
markdownContent: string;
}
const props = defineProps<ObjectPermissionsProps>();
const referencedJobIds = ref<string[]>([]);
const referencedHistoryDatasetIds = ref<string[]>([]);
const referencedHistoryDatasetCollectionIds = ref<string[]>([]);
const referencedWorkflowIds = ref<string[]>([]);
const referencedInvocationIds = ref<string[]>([]);
// We mostly defer to history permissions for all these objects. Track them...
const jobsToHistories: Ref<{ [key: string]: string }> = ref({});
const invocationsToHistories: Ref<{ [key: string]: string }> = ref({});
const historyDatasetCollectionsToHistories: Ref<{ [key: string]: string }> = ref({});
const historyAccessible: Ref<{ [key: string]: Boolean | null }> = ref({});
watch(referencedJobIds, async () => {
referencedJobIds.value.forEach((jobId) => {
if (jobId in jobsToHistories.value) {
return;
}
axios.get(withPrefix(`/api/jobs/${jobId}`)).then((response) => {
const historyId = response.data.history_id;
Vue.set(jobsToHistories.value, jobId, historyId);
});
});
});
watch(referencedInvocationIds, async () => {
referencedInvocationIds.value.forEach((invocationId) => {
if (invocationId in invocationsToHistories.value) {
return;
}
axios.get(withPrefix(`/api/invocations/${invocationId}`)).then((response) => {
const historyId = response.data.history_id;
console.log(`${invocationId} => ${historyId}`);
Vue.set(invocationsToHistories.value, invocationId, historyId);
});
});
});
watch(referencedInvocationIds, async () => {
referencedInvocationIds.value.forEach((invocationId) => {
if (invocationId in invocationsToHistories.value) {
return;
}
axios.get(withPrefix(`/api/invocations/${invocationId}`)).then((response) => {
const historyId = response.data.history_id;
console.log(`${invocationId} => ${historyId}`);
Vue.set(invocationsToHistories.value, invocationId, historyId);
});
});
});
watch(referencedHistoryDatasetCollectionIds, async () => {
referencedHistoryDatasetCollectionIds.value.forEach((historyDatasetCollectionId) => {
if (historyDatasetCollectionId in historyDatasetCollectionsToHistories.value) {
return;
}
axios.get(withPrefix(`/api/invocations/${historyDatasetCollectionId}`)).then((response) => {
const historyId = response.data.history_id;
console.log(`${historyDatasetCollectionId} => ${historyId}`);
Vue.set(historyDatasetCollectionsToHistories.value, historyDatasetCollectionId, historyId);
});
});
});
const historyIds = computed<string[]>(() => {
// be sure to reference all refs required for full computation
const jobIds = referencedJobIds.value;
const jobMapping = jobsToHistories.value;
const invocationIds = referencedInvocationIds.value;
const invocationMapping = invocationsToHistories.value;
const collectionIds = 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;
});
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 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 };
const tableFields = [SHARING_FIELD, NAME_FIELD, TYPE_FIELD];
watch(
props,
() => {
const objects = referencedObjects(props.markdownContent);
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());
},
{ 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;
console.log(`setting ${historyId} to ${accessible}`);
Vue.set(historyAccessible.value, historyId, accessible);
});
}
}
});
const tableItems = computed<ItemInterface[]>(() => {
return histories.value;
});
function makeAccessible(item: ItemInterface) {
if (item.type == "history") {
enableLink({ history_id: item.id }).then((response) => {
Vue.set(historyAccessible.value, item.id, true);
});
}
}
</script>

<template>
<div>
<p>
Permissions for {{ markdownContent }}.... {{ jobsToHistories }} jobs are {{ referencedJobIds }} invocations
are {{ referencedInvocationIds }} histories are {{ histories }}
</p>

<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">
{{ row.item.type }}
</template>
</b-table>
</div>
</template>
15 changes: 15 additions & 0 deletions client/src/components/PageEditor/ObjectPermissionsModal.vue
Original file line number Diff line number Diff line change
@@ -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>
22 changes: 20 additions & 2 deletions client/src/components/PageEditor/PageEditorMarkdown.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
<template>
<MarkdownEditor :title="title" :markdown-text="markdownText" :markdown-config="contentData" @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
Expand All @@ -25,7 +39,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";
Expand All @@ -34,14 +48,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: {
Expand All @@ -68,6 +85,7 @@ export default {
data: function () {
return {
markdownText: this.content,
showPermissions: false,
};
},
methods: {
Expand Down
43 changes: 43 additions & 0 deletions client/src/components/PageEditor/SharingIndicator.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<script setup lang="ts">
import { ref, watch } from "vue";
import LoadingSpan from "@/components/LoadingSpan.vue";
interface SharingIndicatorProps {
accessible: Boolean | 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>
<BFormCheckbox
v-else
switch
:checked="accessible"
class="make-accessible"
:disabled="accessible"
@change="
(event) => {
makeAccessible(event);
}
">
</BFormCheckbox>
</div>
</template>
Loading

0 comments on commit e2040b0

Please sign in to comment.