mirror of https://github.com/zulip/zulip.git
integrations_dev_panel: Convert module to TypeScript.
This commit is contained in:
parent
6019d8ae17
commit
a88445a6d2
|
@ -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;
|
||||
});
|
Loading…
Reference in New Issue