integrations_dev_panel: Convert module to TypeScript.

This commit is contained in:
afeefuddin 2024-07-27 17:11:36 +05:30 committed by Tim Abbott
parent 6019d8ae17
commit a88445a6d2
1 changed files with 125 additions and 55 deletions

View File

@ -1,4 +1,6 @@
import $ from "jquery";
import assert from "minimalistic-assert";
import {z} from "zod";
import * as channel from "../channel";
// Main JavaScript file for the integrations development panel at
@ -7,36 +9,81 @@ import * as channel from "../channel";
// Data segment: We lazy load the requested fixtures from the backend
// as and when required and then cache them here.
const loaded_fixtures = new Map();
const fixture_schema = z.record(
z.string(),
z.object({
body: z.unknown(),
headers: z.record(z.string()),
}),
);
type Fixtures = z.infer<typeof fixture_schema>;
type HTMLSelectOneElement = HTMLSelectElement & {type: "select-one"};
type ClearHandlers = {
stream_name: string;
topic_name: string;
URL: string;
results_notice: string;
bot_name: () => void;
integration_name: () => void;
fixture_name: () => void;
fixture_body: () => void;
custom_http_headers: () => void;
results: () => void;
};
const integrations_api_response_schema = z.object({
msg: z.string(),
responses: z.array(
z.object({
status_code: z.number(),
message: z.string(),
fixture_name: z.optional(z.string()),
}),
),
result: z.string(),
});
type ServerResponse = z.infer<typeof integrations_api_response_schema>;
const loaded_fixtures = new Map<string, Fixtures>();
const url_base = "/api/v1/external/";
// A map defining how to clear the various UI elements.
const clear_handlers = {
const clear_handlers: ClearHandlers = {
stream_name: "#stream_name",
topic_name: "#topic_name",
URL: "#URL",
results_notice: "#results_notice",
bot_name() {
$("#bot_name").children()[0].selected = true;
const bot_option = $<HTMLSelectOneElement>("select:not([multiple])#bot_name").children()[0];
assert(bot_option instanceof HTMLOptionElement);
bot_option.selected = true;
},
integration_name() {
$("#integration_name").children()[0].selected = true;
const integration_option = $<HTMLSelectOneElement>(
"select:not([multiple])#integration_name",
).children()[0];
assert(integration_option instanceof HTMLOptionElement);
integration_option.selected = true;
},
fixture_name() {
$("#fixture_name").empty();
},
fixture_body() {
$("#fixture_body")[0].value = "";
$<HTMLTextAreaElement>("textarea#fixture_body")[0]!.value = "";
},
custom_http_headers() {
$("#custom_http_headers")[0].value = "{}";
$<HTMLTextAreaElement>("textarea#custom_http_headers")[0]!.value = "{}";
},
results() {
$("#idp-results")[0].value = "";
$<HTMLTextAreaElement>("textarea#idp-results")[0]!.value = "";
},
};
function clear_elements(elements) {
function clear_elements(elements: (keyof ClearHandlers)[]): void {
// Supports strings (a selector to clear) or calling a function
// (for more complex logic).
for (const element_name of elements) {
@ -56,24 +103,24 @@ const results_notice_level_to_color_map = {
success: "#085d44",
};
function set_results_notice(msg, level) {
function set_results_notice(msg: string, level: "warning" | "success"): void {
$("#results_notice").text(msg).css("color", results_notice_level_to_color_map[level]);
}
function get_api_key_from_selected_bot() {
return $("#bot_name").val();
function get_api_key_from_selected_bot(): string {
return $<HTMLSelectOneElement>("select:not([multiple])#bot_name").val()!;
}
function get_selected_integration_name() {
return $("#integration_name").val();
function get_selected_integration_name(): string {
return $<HTMLSelectOneElement>("select:not([multiple])#integration_name").val()!;
}
function get_fixture_format(fixture_name) {
function get_fixture_format(fixture_name: string): string | undefined {
return fixture_name.split(".").at(-1);
}
function get_custom_http_headers() {
let custom_headers = $("#custom_http_headers").val();
function get_custom_http_headers(): string | undefined {
let custom_headers = $<HTMLTextAreaElement>("textarea#custom_http_headers").val()!;
if (custom_headers !== "") {
// JSON.parse("") would trigger an error, as empty strings do not qualify as JSON.
try {
@ -87,7 +134,7 @@ function get_custom_http_headers() {
return custom_headers;
}
function set_results(response) {
function set_results(response: ServerResponse): void {
/* The backend returns the JSON responses for each of the
send_message actions included in our request (which is just 1 for
send, but usually is several for send all). We display these
@ -106,14 +153,15 @@ function set_results(response) {
}
data += "\nResponse: " + response.message + "\n\n";
}
$("#idp-results")[0].value = data;
$<HTMLTextAreaElement>("textarea#idp-results")[0]!.value = data;
}
function load_fixture_body(fixture_name) {
function load_fixture_body(fixture_name: string): void {
/* Given a fixture name, use the loaded_fixtures dictionary to set
* the fixture body field. */
const integration_name = get_selected_integration_name();
const fixture = loaded_fixtures.get(integration_name)[fixture_name];
const fixture = loaded_fixtures.get(integration_name)![fixture_name];
assert(fixture !== undefined);
let fixture_body = fixture.body;
const headers = fixture.headers;
if (fixture_body === undefined) {
@ -124,18 +172,27 @@ function load_fixture_body(fixture_name) {
// The 4 argument is pretty printer indentation.
fixture_body = JSON.stringify(fixture_body, null, 4);
}
$("#fixture_body")[0].value = fixture_body;
$("#custom_http_headers")[0].value = JSON.stringify(headers, null, 4);
assert(typeof fixture_body === "string");
$<HTMLTextAreaElement>("textarea#fixture_body")[0]!.value = fixture_body;
$<HTMLTextAreaElement>("textarea#custom_http_headers")[0]!.value = JSON.stringify(
headers,
null,
4,
);
return;
}
function load_fixture_options(integration_name) {
function load_fixture_options(integration_name: string): void {
/* Using the integration name and loaded_fixtures object to set
the fixture options for the fixture_names dropdown and also set
the fixture body to the first fixture by default. */
const fixtures_options_dropdown = $("#fixture_name")[0];
const fixtures_names = Object.keys(loaded_fixtures.get(integration_name)).sort();
const fixtures_options_dropdown = $<HTMLSelectOneElement>(
"select:not([multiple])#fixture_name",
)[0]!;
const fixtures = loaded_fixtures.get(integration_name);
assert(fixtures !== undefined);
const fixtures_names = Object.keys(fixtures).sort();
for (const fixture_name of fixtures_names) {
const new_dropdown_option = document.createElement("option");
@ -143,46 +200,48 @@ function load_fixture_options(integration_name) {
new_dropdown_option.textContent = fixture_name;
fixtures_options_dropdown.add(new_dropdown_option);
}
assert(fixtures_names[0] !== undefined);
load_fixture_body(fixtures_names[0]);
return;
}
function update_url() {
function update_url(): void {
/* Construct the URL that the webhook should be targeting, using
the bot's API key and the integration name. The stream and topic
are both optional, and for the sake of completeness, it should be
noted that the topic is irrelevant without specifying the
stream. */
const url_field = $("#URL")[0];
const url_field = $<HTMLInputElement>("input#URL")[0];
const integration_name = get_selected_integration_name();
const api_key = get_api_key_from_selected_bot();
assert(typeof api_key === "string");
if (integration_name === "" || api_key === "") {
clear_elements(["URL"]);
} else {
const params = new URLSearchParams({api_key});
const stream_name = $("#stream_name").val();
const stream_name = $<HTMLInputElement>("input#stream_name").val()!;
if (stream_name !== "") {
params.set("stream", stream_name);
const topic_name = $("#topic_name").val();
const topic_name = $<HTMLInputElement>("input#topic_name").val()!;
if (topic_name !== "") {
params.set("topic", topic_name);
}
}
const url = `${url_base}${integration_name}?${params}`;
url_field.value = url;
const url = `${url_base}${integration_name}?${params.toString()}`;
url_field!.value = url;
}
return;
}
// API callers: These methods handle communicating with the Python backend API.
function handle_unsuccessful_response(response) {
if (response.responseJSON?.msg) {
function handle_unsuccessful_response(response: JQuery.jqXHR): void {
const parsed = z.object({msg: z.string()}).safeParse(response.responseJSON);
if (parsed.data) {
const status_code = response.status;
set_results_notice(`Result: (${status_code}) ${response.responseJSON.msg}`, "warning");
set_results_notice(`Result: (${status_code}) ${parsed.data.msg}`, "warning");
} else {
// If the response is not a JSON response, then it is probably
// Django returning an HTML response containing a stack trace
@ -193,7 +252,7 @@ function handle_unsuccessful_response(response) {
return;
}
function get_fixtures(integration_name) {
function get_fixtures(integration_name: string): void {
/* Request fixtures from the backend for any integrations that we
don't already have fixtures cached in loaded_fixtures). */
if (integration_name === "") {
@ -215,9 +274,17 @@ function get_fixtures(integration_name) {
// We don't have the fixtures for this integration; fetch them
// from the backend. Relative URL pattern:
// /devtools/integrations/<integration_name>/fixtures
channel.get({
void channel.get({
url: "/devtools/integrations/" + integration_name + "/fixtures",
success(response) {
success(raw_response) {
const response = z
.object({
result: z.string(),
msg: z.string(),
fixtures: fixture_schema,
})
.parse(raw_response);
loaded_fixtures.set(integration_name, response.fixtures);
load_fixture_options(integration_name);
return;
@ -228,7 +295,7 @@ function get_fixtures(integration_name) {
return;
}
function send_webhook_fixture_message() {
function send_webhook_fixture_message(): void {
/* Make sure that the user is sending valid JSON in the fixture
body and that the URL is not empty. Then simply send the fixture
body to the target URL. */
@ -238,7 +305,7 @@ function send_webhook_fixture_message() {
// then the csrf token that we have stored in the hidden input
// element would have been expired, leading to an error message
// when the user tries to send the fixture body.
const csrftoken = $("#csrftoken").val();
const csrftoken = $<HTMLInputElement>("input#csrftoken").val()!;
const url = $("#URL").val();
if (url === "") {
@ -246,8 +313,8 @@ function send_webhook_fixture_message() {
return;
}
let body = $("#fixture_body").val();
const fixture_name = $("#fixture_name").val();
let body = $<HTMLTextAreaElement>("textarea#fixture_body").val()!;
const fixture_name = $<HTMLSelectOneElement>("select:not([multiple])#fixture_name").val();
let is_json = false;
if (fixture_name && get_fixture_format(fixture_name) === "json") {
try {
@ -262,17 +329,18 @@ function send_webhook_fixture_message() {
const custom_headers = get_custom_http_headers();
channel.post({
void channel.post({
url: "/devtools/integrations/check_send_webhook_fixture_message",
data: {url, body, custom_headers, is_json},
beforeSend(xhr) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
},
success(response) {
success(raw_response) {
// If the previous fixture body was sent successfully,
// then we should change the success message up a bit to
// let the user easily know that this fixture body was
// also sent successfully.
const response = integrations_api_response_schema.parse(raw_response);
set_results(response);
if ($("#results_notice").text() === "Success!") {
set_results_notice("Success!!!", "success");
@ -287,7 +355,7 @@ function send_webhook_fixture_message() {
return;
}
function send_all_fixture_messages() {
function send_all_fixture_messages(): void {
/* Send all fixture messages for a given integration. */
const url = $("#URL").val();
const integration = get_selected_integration_name();
@ -296,14 +364,15 @@ function send_all_fixture_messages() {
return;
}
const csrftoken = $("#csrftoken").val();
channel.post({
const csrftoken = $<HTMLInputElement>("input#csrftoken").val()!;
void channel.post({
url: "/devtools/integrations/send_all_webhook_fixture_messages",
data: {url, integration_name: integration},
beforeSend(xhr) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
},
success(response) {
success(raw_response) {
const response = integrations_api_response_schema.parse(raw_response);
set_results(response);
},
error: handle_unsuccessful_response,
@ -327,25 +396,26 @@ $(() => {
"results",
]);
$("#stream_name")[0].value = "Denmark";
$("#topic_name")[0].value = "Integrations testing";
$<HTMLInputElement>("input#stream_name")[0]!.value = "Denmark";
$<HTMLInputElement>("input#topic_name")[0]!.value = "Integrations testing";
const potential_default_bot = $("#bot_name")[0][1];
const potential_default_bot = $<HTMLSelectOneElement>("select:not([multiple])#bot_name")[0]![1];
assert(potential_default_bot instanceof HTMLOptionElement);
if (potential_default_bot !== undefined) {
potential_default_bot.selected = true;
}
$("#integration_name").on("change", function () {
$<HTMLSelectOneElement>("select:not([multiple])#integration_name").on("change", function () {
clear_elements(["custom_http_headers", "fixture_body", "fixture_name", "results_notice"]);
const integration_name = $(this.selectedOptions).val();
const integration_name = $(this.selectedOptions).val()!;
get_fixtures(integration_name);
update_url();
return;
});
$("#fixture_name").on("change", function () {
$<HTMLSelectOneElement>("select:not([multiple])#fixture_name").on("change", function () {
clear_elements(["fixture_body", "results_notice"]);
const fixture_name = $(this.selectedOptions).val();
const fixture_name = $(this.selectedOptions).val()!;
load_fixture_body(fixture_name);
return;
});