Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added New Bug Report Type #19508

Draft
wants to merge 5 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions client/src/api/schema/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2934,6 +2934,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/tools/{tool_id}/error": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/** Submits a bug report via the API. */
post: operations["report_error_api_tools__tool_id__error_post"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/jobs/{job_id}/inputs": {
parameters: {
query?: never;
Expand Down Expand Up @@ -12765,6 +12782,14 @@ export interface components {
*/
parameters: components["schemas"]["JobParameter"][];
};
/** ToolErrorSummary */
ToolErrorSummary: {
/**
* Error messages
* @description The error messages for the specified job.
*/
messages: string[][];
};
/** JobErrorSummary */
JobErrorSummary: {
/**
Expand Down Expand Up @@ -15585,6 +15610,30 @@ export interface components {
*/
message?: string | null;
};
/** ReportToolErrorPayload */
ReportToolErrorPayload: {
/**
* Tool ID
* @description Tool ID related to the error.
* @example "myTool"
*/
/**
* Reportable Data
* @description The tool data related to the error.
* @example "{tool_id: 'myTool', params: {param1: 'value1', param2: 'value2'}}"
*/
reportable_data: object;
/**
* Email
* @description Email address for communication with the user. Only required for anonymous users.
*/
email?: string | null;
/**
* Message
* @description The optional message sent with the error report.
*/
message?: string | null;
};
/**
* RequestDataType
* @description Particular pieces of information that can be requested for a dataset.
Expand Down Expand Up @@ -28097,6 +28146,54 @@ export interface operations {
};
};
};
report_error_api_tools__tool_id__error_post: {
parameters: {
query?: never;
header?: {
/** @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. */
"run-as"?: string | null;
};
path: {
/** @description The ID of the tool */
tool_id: string;
};
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["ReportToolErrorPayload"];
};
};
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ToolErrorSummary"];
};
};
/** @description Request Error */
"4XX": {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MessageExceptionModel"];
};
};
/** @description Server Error */
"5XX": {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["MessageExceptionModel"];
};
};
};
};
report_error_api_jobs__job_id__error_post: {
parameters: {
query?: never;
Expand Down
100 changes: 100 additions & 0 deletions client/src/components/Collections/common/UserReportingError.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
<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, BCollapse, BLink } from "bootstrap-vue";
import { computed, ref } from "vue";

import { dispatchReport, type ReportType } 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 {
reportType: ReportType;
reportableData: object;
reportingEmail: string;
}

const props = defineProps<Props>();
const { renderMarkdown } = useMarkdown({ openLinksInNewPage: true });
const message = ref("");
const errorMessage = ref("");
const resultMessages = ref<string[][]>([]);
const isExpanded = ref(<boolean>false);
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
)
);
const expandedIcon = computed(() => (isExpanded.value ? "-" : "+"));

async function handleSubmit(reportType: ReportType, data?: any, email?: string | null) {
if (!data || !email) {
return;
}

const { messages, error } = await dispatchReport(reportType, 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])" />
</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" />
<BLink
:aria-expanded="isExpanded ? 'true' : 'false'"
aria-controls="collapse-previous"
@click="isExpanded = !isExpanded">
({{ expandedIcon }}) Error transcript:
</BLink>
<BCollapse id="collapse-previous" v-model="isExpanded">
<pre class="rounded code">{{ reportableData }}</pre>
</BCollapse>
<br />
<BButton
id="data-error-submit"
v-b-tooltip.hover
:title="fieldMessages.join('\n')"
variant="primary"
class="mt-3"
@click="handleSubmit(props.reportType, props.reportableData, props.reportingEmail)">
<FontAwesomeIcon :icon="faBug" class="mr-1" />
Report
</BButton>
</div>
</div>
</template>
74 changes: 74 additions & 0 deletions client/src/components/Collections/common/reporting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { GalaxyApi } from "@/api";
import { type HDADetailed } from "@/api";
import { errorMessageAsString } from "@/utils/simple-error";

export type ReportType = "dataset" | "tool";

export async function dispatchReport(
reportType: ReportType,
reportableData: HDADetailed | any, // TODO Better option for Tools than "any" ?
message: string,
email: string
) {
if (reportType === "dataset") {
return await submitReportDataset(reportableData, message, email);
} else {
return await submitReportTool(reportableData, message, email);
}
}

export async function submitReportDataset(
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) };
}
}

export async function submitReportTool(
reportableData: any,
message: string,
email: string
): Promise<{ messages: string[][]; error?: string }> {
try {
const { data, error } = await GalaxyApi().POST("/api/tools/{tool_id}/error", {
params: {
path: {
tool_id: reportableData.tool_id,
},
},
body: {
reportable_data: reportableData,
message,
email,
},
});

if (error) {
return { messages: [], error: errorMessageAsString(error) };
}
// return { messages: data.messages };
return { messages: [["Success!", "success"]] };
} catch (err) {
console.log("api error (err)", 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
Loading