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