Skip to content

Commit 00d4ddc

Browse files
Add sharing interface for invocations
This uses the `GenericWizard` to enforce the user to make their history and workflow both accessible, and then shows them the link they can copy. Fixes galaxyproject#19756
1 parent 804b1f6 commit 00d4ddc

5 files changed

Lines changed: 249 additions & 2 deletions

File tree

client/src/components/Common/Wizard/GenericWizard.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ const steps = computed<[string, WizardStep][]>(() => {
261261
display: flex;
262262
flex-direction: column;
263263
align-items: center;
264+
width: 100%;
264265
}
265266
}
266267

client/src/components/CopyToClipboard.vue

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
<template>
2-
<FontAwesomeIcon class="cursor-pointer" :title="title" :icon="['far', 'copy']" @click="copy(text, message)" />
2+
<component :is="component" @click="copy(text, message)">
3+
<FontAwesomeIcon :class="{ 'cursor-pointer': component !== 'button' }" :title="title" :icon="['far', 'copy']" />
4+
</component>
35
</template>
46

57
<script>
@@ -28,6 +30,11 @@ export default {
2830
default: "copy to clipboard",
2931
required: false,
3032
},
33+
component: {
34+
type: String,
35+
default: "span",
36+
required: false,
37+
},
3138
},
3239
methods: {
3340
copy(text, message) {

client/src/components/Sharing/SharingPage.vue

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const props = defineProps<{
2525
id: string;
2626
pluralName: string;
2727
modelClass: string;
28+
noHeading?: boolean;
2829
}>();
2930
3031
const errors = ref<string[]>([]);
@@ -219,11 +220,13 @@ async function setUsername() {
219220
}
220221
221222
const embedable = computed(() => item.value.importable && props.modelClass.toLocaleLowerCase() === "workflow");
223+
224+
defineExpose({ item });
222225
</script>
223226

224227
<template>
225228
<div class="sharing-page">
226-
<Heading h1 size="lg" separator>
229+
<Heading v-if="!props.noHeading" h1 size="lg" separator>
227230
<span>
228231
Share or Publish {{ modelClass }} <span v-if="ready">"{{ item.title }}"</span>
229232
</span>
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
<script setup lang="ts">
2+
import { faLink, faShareAlt } from "@fortawesome/free-solid-svg-icons";
3+
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
4+
import {
5+
BAlert,
6+
BButton,
7+
BFormInput,
8+
BInputGroup,
9+
BInputGroupAppend,
10+
BInputGroupPrepend,
11+
BInputGroupText,
12+
BModal,
13+
} from "bootstrap-vue";
14+
import { computed, ref, watch } from "vue";
15+
16+
import { getFullAppUrl } from "@/app/utils";
17+
import { useWorkflowInstance } from "@/composables/useWorkflowInstance";
18+
import { useHistoryStore } from "@/stores/historyStore";
19+
20+
import { useWizard } from "../Common/Wizard/useWizard";
21+
22+
import GenericWizard from "../Common/Wizard/GenericWizard.vue";
23+
import CopyToClipboard from "../CopyToClipboard.vue";
24+
import LoadingSpan from "../LoadingSpan.vue";
25+
import SharingPage from "../Sharing/SharingPage.vue";
26+
27+
const props = defineProps<{
28+
invocationId: string;
29+
workflowId: string;
30+
historyId: string;
31+
}>();
32+
33+
const modalToggle = ref(false);
34+
35+
// Workflow and History refs
36+
const { workflow, loading: workflowLoading, error: workflowError } = useWorkflowInstance(props.workflowId);
37+
const historyStore = useHistoryStore();
38+
const history = computed(() => historyStore.getHistoryById(props.historyId, true));
39+
40+
/** The link to the invocation. */
41+
const invocationLink = computed(() => getFullAppUrl(`workflows/invocations/${props.invocationId}`));
42+
43+
// Component references
44+
const historySharing = ref<InstanceType<typeof SharingPage> | null>(null);
45+
const workflowSharing = ref<InstanceType<typeof SharingPage> | null>(null);
46+
47+
// Computed properties to check if the workflow and history are accessible
48+
const workflowIsAccessible = computed(() => {
49+
const { item } = workflowSharing.value || {};
50+
return Boolean(item?.importable) || Boolean(item?.users_shared_with?.length);
51+
});
52+
const historyIsAccessible = computed(() => {
53+
const { item } = historySharing.value || {};
54+
return Boolean(item?.importable) || Boolean(item?.users_shared_with?.length);
55+
});
56+
57+
/** The workflow and history sharing wizard steps. */
58+
const wizard = useWizard({
59+
"share-workflow": {
60+
label: "Share workflow",
61+
instructions: "",
62+
isValid: () => workflowIsAccessible.value,
63+
isSkippable: () => false,
64+
},
65+
"share-history": {
66+
label: "Share history",
67+
instructions: "",
68+
isValid: () => historyIsAccessible.value,
69+
isSkippable: () => false,
70+
},
71+
"get-link": {
72+
label: "Share the invocation",
73+
instructions: "Here is a link to the invocation.",
74+
isValid: () => true,
75+
isSkippable: () => false,
76+
},
77+
});
78+
79+
/** Information to show in the instructional message. */
80+
const currentItemInfo = computed(() => {
81+
if (wizard.isCurrent("share-workflow")) {
82+
return {
83+
type: "workflow",
84+
name: workflow.value?.name || "...",
85+
accessibleStatus: workflowIsAccessible.value,
86+
};
87+
} else if (wizard.isCurrent("share-history")) {
88+
return {
89+
type: "history",
90+
name: history.value?.name || "...",
91+
accessibleStatus: historyIsAccessible.value,
92+
};
93+
} else {
94+
return null;
95+
}
96+
});
97+
98+
// This next block is used to navigate to the "get-link" step of the wizard
99+
// when the modal is opened and the workflow and history are already accessible.
100+
const navigatedToLinkOnce = ref(false);
101+
watch(
102+
() => modalToggle.value,
103+
(opened) => {
104+
if (opened && !navigatedToLinkOnce.value && !wizard.isCurrent("get-link")) {
105+
// A timeout is used to ensure that the sharing components are loaded
106+
setTimeout(() => {
107+
if (workflowIsAccessible.value && historyIsAccessible.value) {
108+
wizard.goTo("get-link");
109+
}
110+
navigatedToLinkOnce.value = true;
111+
}, 1000);
112+
}
113+
},
114+
{ immediate: true }
115+
);
116+
</script>
117+
118+
<template>
119+
<div>
120+
<BButton
121+
v-b-tooltip.noninteractive.hover
122+
title="Share Invocation"
123+
size="sm"
124+
class="text-decoration-none"
125+
variant="link"
126+
:disabled="!workflow || !history"
127+
@click="modalToggle = true">
128+
<FontAwesomeIcon :icon="faShareAlt" fixed-width />
129+
</BButton>
130+
131+
<BModal
132+
v-model="modalToggle"
133+
title="Share Workflow Invocation"
134+
title-sr-only
135+
hide-header
136+
size="xl"
137+
scrollable
138+
dialog-class="invocation-share-dialog"
139+
ok-only
140+
ok-title="Exit"
141+
ok-variant="secondary">
142+
<BAlert v-if="workflowError" variant="danger" show>
143+
{{ workflowError }}
144+
</BAlert>
145+
146+
<LoadingSpan v-else-if="workflowLoading" message="Loading details for invocation" />
147+
148+
<GenericWizard
149+
v-else-if="!!workflow && !!history"
150+
title="Share the associated workflow and history first"
151+
:use="wizard"
152+
submit-button-label="Next"
153+
:is-busy="workflowLoading">
154+
<div v-if="currentItemInfo?.accessibleStatus" class="donemessagelarge">
155+
The {{ currentItemInfo.type }}
156+
<strong> "{{ currentItemInfo.name }}" </strong>
157+
is either made accessible via link or shared with at least one user.
158+
<strong>Click next to continue.</strong>
159+
</div>
160+
<BAlert v-else-if="currentItemInfo?.accessibleStatus === false" variant="info" show>
161+
The {{ currentItemInfo.type }}
162+
<strong> "{{ currentItemInfo.name }}" </strong>
163+
is not accessible via link or shared with any user. Select your sharing preferences below to make it
164+
accessible.
165+
</BAlert>
166+
167+
<div v-show="wizard.isCurrent('share-workflow')" class="w-100">
168+
<SharingPage
169+
:id="workflow.id"
170+
ref="workflowSharing"
171+
:key="`workflow-${workflow.id}`"
172+
class="sharing-view"
173+
plural-name="Workflows"
174+
model-class="Workflow"
175+
no-heading />
176+
</div>
177+
178+
<div v-show="wizard.isCurrent('share-history')" class="w-100">
179+
<SharingPage
180+
:id="history.id"
181+
ref="historySharing"
182+
:key="`history-${history.id}`"
183+
class="sharing-view"
184+
plural-name="Histories"
185+
model-class="History"
186+
no-heading />
187+
</div>
188+
189+
<div v-if="wizard.isCurrent('get-link')" class="w-100">
190+
<BInputGroup>
191+
<BInputGroupPrepend>
192+
<BInputGroupText>
193+
<FontAwesomeIcon :icon="faLink" />
194+
</BInputGroupText>
195+
</BInputGroupPrepend>
196+
197+
<BFormInput type="text" :value="invocationLink" disabled />
198+
199+
<BInputGroupAppend>
200+
<CopyToClipboard
201+
message="Link to invocation copied to clipboard"
202+
:text="invocationLink"
203+
title="Copy key"
204+
component="button" />
205+
</BInputGroupAppend>
206+
</BInputGroup>
207+
208+
<p class="mt-2">
209+
<strong>Note:</strong>
210+
If you share this link with someone, and they are still unable to view the invocation, please go
211+
back and ensure that the workflow and history are either made accessible to anyone via link, or
212+
shared with the same user.
213+
</p>
214+
</div>
215+
</GenericWizard>
216+
</BModal>
217+
</div>
218+
</template>
219+
220+
<style lang="scss">
221+
.invocation-share-dialog {
222+
width: 900px;
223+
}
224+
</style>
225+
226+
<style lang="scss" scoped>
227+
.sharing-view {
228+
container-type: unset;
229+
overflow-y: unset;
230+
}
231+
</style>

client/src/components/WorkflowInvocationState/WorkflowInvocationState.vue

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import WorkflowInvocationExportOptions from "./WorkflowInvocationExportOptions.v
2828
import WorkflowInvocationInputOutputTabs from "./WorkflowInvocationInputOutputTabs.vue";
2929
import WorkflowInvocationMetrics from "./WorkflowInvocationMetrics.vue";
3030
import WorkflowInvocationOverview from "./WorkflowInvocationOverview.vue";
31+
import WorkflowInvocationShare from "./WorkflowInvocationShare.vue";
3132
import LoadingSpan from "@/components/LoadingSpan.vue";
3233
3334
interface Props {
@@ -263,6 +264,10 @@ async function onCancel() {
263264
<FontAwesomeIcon :icon="faSquare" fixed-width />
264265
Cancel
265266
</BButton>
267+
<WorkflowInvocationShare
268+
:invocation-id="invocation.id"
269+
:workflow-id="invocation.workflow_id"
270+
:history-id="invocation.history_id" />
266271
</template>
267272
</WorkflowNavigationTitle>
268273
<WorkflowAnnotation

0 commit comments

Comments
 (0)