Skip to content

Commit

Permalink
Refactored Dataset Error Page email web form
Browse files Browse the repository at this point in the history
Make component reusable, changes made:
* Componentized email web form into Vue.js
* Decoupled the View (html email) from the Controller (API methods)
* Abstracted the View elements for reuse with other objects (eg.Tools)
* Utilized existing dependency (Python Jinja2) for repo consistency

Reported by @hexylena in issue
#17560 .
  • Loading branch information
hujambo-dunia committed Jan 16, 2025
1 parent 9fc0f05 commit 922a3fa
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 190 deletions.
87 changes: 87 additions & 0 deletions client/src/components/Collections/common/UserReportingError.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faBug } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BAlert, BButton } from "bootstrap-vue";
import { computed, ref } from "vue";
import { submitReport } from "@/components/Collections/common/reporting";
import { useMarkdown } from "@/composables/markdown";
import localize from "@/utils/localization";
import FormElement from "@/components/Form/FormElement.vue";
library.add(faBug);
interface Props {
reportableData: object;
reportingEmail: string;
}
const props = defineProps<Props>();
const { renderMarkdown } = useMarkdown({ openLinksInNewPage: true });
const message = ref("");
const errorMessage = ref("");
const resultMessages = ref<string[][]>([]);
const showForm = computed(() => {
const noResult = !resultMessages.value.length;
const hasError = resultMessages.value.some((msg) => msg[1] === "danger");
return noResult || hasError;
});
const FIELD_MESSAGE = {
loginRequired: localize("You must be logged in to send emails."),
dataRequired: localize("You must provide a valid object to send emails."),
};
const fieldMessages = computed(() =>
[!props.reportableData && FIELD_MESSAGE.dataRequired, !props.reportingEmail && FIELD_MESSAGE.loginRequired].filter(
Boolean
)
);
async function handleSubmit(data?: any, email?: string | null) {
if (!data || !email) {
return;
}
const { messages, error } = await submitReport(data, message.value, email);
if (error) {
errorMessage.value = error;
} else {
resultMessages.value = messages;
}
}
</script>

<template>
<div>
<BAlert v-for="(resultMessage, index) in resultMessages" :key="index" :variant="resultMessage[1]" show>
<span v-html="renderMarkdown(resultMessage[0])" />

Check warning on line 59 in client/src/components/Collections/common/UserReportingError.vue

View workflow job for this annotation

GitHub Actions / client-unit-test (18)

'v-html' directive can lead to XSS attack
</BAlert>

<div v-if="showForm" id="data-error-form">
<div>
<span class="mr-2 font-weight-bold">{{ localize("Your email address") }}</span>
<span v-if="props.reportingEmail">{{ props.reportingEmail }}</span>
<span v-else>{{ FIELD_MESSAGE.loginRequired }}</span>
</div>
<div>
<span class="mr-2 font-weight-bold">{{
localize("Please provide detailed information on the activities leading to this issue:")
}}</span>
<span v-if="!props.reportableData">{{ FIELD_MESSAGE.dataRequired }}</span>
</div>
<FormElement v-if="props.reportableData" id="object-error-message" v-model="message" :area="true" />
<BButton
id="data-error-submit"
v-b-tooltip.hover
:title="fieldMessages.join('\n')"
variant="primary"
class="mt-3"
@click="handleSubmit(props.reportableData, props.reportingEmail)">
<FontAwesomeIcon :icon="faBug" class="mr-1" />
Report
</BButton>
</div>
</div>
</template>
34 changes: 34 additions & 0 deletions client/src/components/Collections/common/reporting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { GalaxyApi } from "@/api";
import { type HDADetailed } from "@/api";
import { errorMessageAsString } from "@/utils/simple-error";

export interface ReportableObject {
id: string;
creating_job: string;
}

export async function submitReport(
reportableData: HDADetailed,
message: string,
email: string
): Promise<{ messages: string[][]; error?: string }> {
try {
const { data, error } = await GalaxyApi().POST("/api/jobs/{job_id}/error", {
params: {
path: { job_id: reportableData.creating_job },
},
body: {
dataset_id: reportableData.id,
message,
email,
},
});

if (error) {
return { messages: [], error: errorMessageAsString(error) };
}
return { messages: data.messages };
} catch (err) {
return { messages: [], error: errorMessageAsString(err) };
}
}
6 changes: 3 additions & 3 deletions client/src/components/DatasetInformation/DatasetError.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ describe("DatasetError", () => {
});

it("hides form fields and button on success", async () => {
const wrapper = await montDatasetError();
const wrapper = await montDatasetError(false, false, "test_email");

server.use(
http.post("/api/jobs/{job_id}/error", ({ response }) => {
Expand All @@ -112,10 +112,10 @@ describe("DatasetError", () => {
})
);

const FormAndSubmitButton = "#dataset-error-form";
const FormAndSubmitButton = "#data-error-form";
expect(wrapper.find(FormAndSubmitButton).exists()).toBe(true);

const submitButton = "#dataset-error-submit";
const submitButton = "#data-error-submit";
expect(wrapper.find(submitButton).exists()).toBe(true);

await wrapper.find(submitButton).trigger("click");
Expand Down
68 changes: 3 additions & 65 deletions client/src/components/DatasetInformation/DatasetError.vue
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
<script setup lang="ts">
import { library } from "@fortawesome/fontawesome-svg-core";
import { faBug } from "@fortawesome/free-solid-svg-icons";
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
import { BAlert, BButton, BCard } from "bootstrap-vue";
import { BAlert, BCard } from "bootstrap-vue";
import { storeToRefs } from "pinia";
import { computed, onMounted, ref } from "vue";
import { GalaxyApi, type HDADetailed } from "@/api";
import { fetchDatasetDetails } from "@/api/datasets";
import { type JobDetails, type JobInputSummary } from "@/api/jobs";
import { useConfig } from "@/composables/config";
import { useMarkdown } from "@/composables/markdown";
import { useUserStore } from "@/stores/userStore";
import localize from "@/utils/localization";
import { errorMessageAsString } from "@/utils/simple-error";
import UserReportingError from "../Collections/common/UserReportingError.vue";
import DatasetErrorDetails from "@/components/DatasetInformation/DatasetErrorDetails.vue";
import FormElement from "@/components/Form/FormElement.vue";
import GalaxyWizard from "@/components/GalaxyWizard.vue";
library.add(faBug);
Expand All @@ -26,29 +23,18 @@ interface Props {
}
const props = defineProps<Props>();
const userStore = useUserStore();
const { currentUser } = storeToRefs(userStore);
const { renderMarkdown } = useMarkdown({ openLinksInNewPage: true });
const { config, isConfigLoaded } = useConfig();
const message = ref("");
const jobLoading = ref(true);
const errorMessage = ref("");
const datasetLoading = ref(false);
const jobDetails = ref<JobDetails>();
const jobProblems = ref<JobInputSummary>();
const resultMessages = ref<string[][]>([]);
const dataset = ref<HDADetailed>();
const showForm = computed(() => {
const noResult = !resultMessages.value.length;
const hasError = resultMessages.value.some((msg) => msg[1] === "danger");
return noResult || hasError;
});
const showWizard = computed(() => isConfigLoaded && config.value?.llm_api_configured);
async function getDatasetDetails() {
Expand Down Expand Up @@ -97,31 +83,6 @@ async function getJobProblems(jobId: string) {
jobProblems.value = data;
}
async function submit(dataset?: HDADetailed, userEmailJob?: string | null) {
if (!dataset) {
errorMessage.value = "No dataset found.";
return;
}
const { data, error } = await GalaxyApi().POST("/api/jobs/{job_id}/error", {
params: {
path: { job_id: dataset.creating_job },
},
body: {
dataset_id: dataset.id,
message: message.value,
email: userEmailJob,
},
});
if (error) {
errorMessage.value = errorMessageAsString(error);
return;
}
resultMessages.value = data.messages;
}
function onMissingJobId() {
errorMessage.value = "No job ID found for this dataset.";
}
Expand Down Expand Up @@ -210,30 +171,7 @@ onMounted(async () => {
</p>

<h4 class="mb-3 h-md">Issue Report</h4>
<BAlert v-for="(resultMessage, index) in resultMessages" :key="index" :variant="resultMessage[1]" show>
<span v-html="renderMarkdown(resultMessage[0])" />
</BAlert>

<div v-if="showForm" id="dataset-error-form">
<span class="mr-2 font-weight-bold">{{ localize("Your email address") }}</span>
<span v-if="currentUser?.email">{{ currentUser.email }}</span>
<span v-else>{{ localize("You must be logged in to receive emails") }}</span>

<FormElement
id="dataset-error-message"
v-model="message"
:area="true"
title="Please provide detailed information on the activities leading to this issue:" />

<BButton
id="dataset-error-submit"
variant="primary"
class="mt-3"
@click="submit(dataset, jobDetails?.user_email)">
<FontAwesomeIcon :icon="faBug" class="mr-1" />
Report
</BButton>
</div>
<UserReportingError :reportable-data="dataset" :reporting-email="currentUser?.email" />
</div>
</div>
</template>
87 changes: 87 additions & 0 deletions lib/galaxy/config/templates/mail/error-report-dataset.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<html>
<body>
<h1>Galaxy Tool Error Report</h1>
<span class="sub">
<i>from </i>
<span style="font-family: monospace;">
<a href="{{ host }}">{{ host }}</a>
</span>
</span>
<h3>Error Localization</h3>
<table style="margin:1em">
<tbody>
<tr>
<td>Dataset</td>
<td>
<a href="{{ hda_show_params_link }}">{{ dataset_id }} ({{ dataset_id_encoded }})</a>
</td>
</tr>
<tr style="background-color: #f2f2f2">
<td>History</td>
<td>
<a href="{{ history_view_link }}">{{ history_id }} ({{ history_id_encoded }})</a>
</td>
</tr>
<tr>
<td>Failed Job</td>
<td>{{ hid }}: {{ history_item_name }} ({{ hda_id_encoded }})</td>
</tr>
</tbody>
</table>
<h3>User Provided Information</h3>
The user
<span style="font-family: monospace;">{{ email_str }}</span> provided the following information:
<pre style="white-space: pre-wrap;background: #eeeeee;border:1px solid black;padding:1em;">
{{ message }}
</pre>
<h3>Detailed Job Information</h3>
Job environment and execution information is available at the job
<a href="{{ hda_show_params_link }}">info page</a>.
<table style="margin:1em">
<tbody>
<tr>
<td>Job ID</td>
<td>{{ job_id }} ({{ job_id_encoded }})</td>
</tr>
<tr style="background-color: #f2f2f2">
<td>Tool ID</td>
<td>{{ job_tool_id }}</td>
</tr>
<tr>
<td>Tool Version</td>
<td>{{ tool_version }}</td>
</tr>
<tr style="background-color: #f2f2f2">
<td>Job PID or DRM id</td>
<td>{{ job_runner_external_id }}</td>
</tr>
<tr>
<td>Job Tool Version</td>
<td>{{ job_tool_version }}</td>
</tr>
</tbody>
</table>
<h3>Job Execution and Failure Information</h3>
<h4>Command Line</h4>
<pre style="white-space: pre-wrap;background: #eeeeee;border:1px solid black;padding:1em;">
{{ job_command_line }}
</pre>
<h4>stderr</h4>
<pre style="white-space: pre-wrap;background: #eeeeee;border:1px solid black;padding:1em;">
{{ job_stderr }}
</pre>
<h4>stdout</h4>
<pre style="white-space: pre-wrap;background: #eeeeee;border:1px solid black;padding:1em;">
{{ job_stdout }}
</pre>
<h4>Job Information</h4>
<pre style="white-space: pre-wrap;background: #eeeeee;border:1px solid black;padding:1em;">
{{ job_info }}
</pre>
<h4>Job Traceback</h4>
<pre style="white-space: pre-wrap;background: #eeeeee;border:1px solid black;padding:1em;">
{{ job_traceback }}
</pre>
This is an automated message. Do not reply to this address.
</body>
</html>
44 changes: 44 additions & 0 deletions lib/galaxy/config/templates/mail/error-report-dataset.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
GALAXY TOOL ERROR REPORT
------------------------

This error report was sent from the Galaxy instance hosted on the server
"{{ host }}"
-----------------------------------------------------------------------------
This is in reference to dataset id {{ dataset_id }} ({{ dataset_id_encoded }}) from history id {{ history_id }} ({{ history_id_encoded }})
-----------------------------------------------------------------------------
You should be able to view the history containing the related history item ({{ hda_id_encoded }})

{{ hid }}: {{ history_item_name }}

by logging in as a Galaxy admin user to the Galaxy instance referenced above
and pointing your browser to the following link.

{{ history_view_link }}
-----------------------------------------------------------------------------
The user {{ email_str }} provided the following information:

{{ message }}
-----------------------------------------------------------------------------
info url: {{ hda_show_params_link }}
job id: {{ job_id }} ({{ job_id_encoded }})
tool id: {{ job_tool_id }}
tool version: {{ tool_version }}
job pid or drm id: {{ job_runner_external_id }}
job tool version: {{ job_tool_version }}
-----------------------------------------------------------------------------
job command line:
{{ job_command_line }}
-----------------------------------------------------------------------------
job stderr:
{{ job_stderr }}
-----------------------------------------------------------------------------
job stdout:
{{ job_stdout }}
-----------------------------------------------------------------------------
job info:
{{ job_info }}
-----------------------------------------------------------------------------
job traceback:
{{ job_traceback }}
-----------------------------------------------------------------------------
(This is an automated message).
Loading

0 comments on commit 922a3fa

Please sign in to comment.