mirror of
				https://github.com/zulip/zulip.git
				synced 2025-11-03 21:43:21 +00:00 
			
		
		
		
	integrations_dev_panel: Convert module to TypeScript.
This commit is contained in:
		@@ -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;
 | 
			
		||||
    });
 | 
			
		||||
		Reference in New Issue
	
	Block a user