From 0b2bc1de9e18082def77915817393888ae87548e Mon Sep 17 00:00:00 2001 From: Muhammad Atif Ali Date: Tue, 22 Oct 2024 10:43:37 +0500 Subject: [PATCH] Refactor multiple modules for improved flexibility - Transition modules to use slug and agent_name variables for custom configurations. - Update Terraform resources to dynamically generate URLs and paths. - Enhance form handling logic in Devolutions patch script. --- filebrowser/README.md | 20 +- filebrowser/main.tf | 37 +- jetbrains-gateway/README.md | 8 +- jetbrains-gateway/main.tf | 20 +- jupyter-notebook/README.md | 30 +- jupyter-notebook/main.tf | 38 +- jupyterlab/README.md | 30 +- jupyterlab/main.tf | 29 +- vscode-desktop/README.md | 4 +- vscode-desktop/main.tf | 22 +- vscode-web/README.md | 31 +- vscode-web/main.tf | 10 +- windows-rdp/README.md | 6 +- windows-rdp/devolutions-patch.js | 490 +++++++++--------- windows-rdp/main.tf | 23 +- .../powershell-installation-script.tftpl | 2 +- 16 files changed, 481 insertions(+), 319 deletions(-) diff --git a/filebrowser/README.md b/filebrowser/README.md index 1b53ffa..8b97d31 100644 --- a/filebrowser/README.md +++ b/filebrowser/README.md @@ -14,7 +14,7 @@ A file browser for your workspace. ```tf module "filebrowser" { source = "registry.coder.com/modules/filebrowser/coder" - version = "1.0.22" + version = "1.0.23" agent_id = coder_agent.example.id } ``` @@ -28,7 +28,7 @@ module "filebrowser" { ```tf module "filebrowser" { source = "registry.coder.com/modules/filebrowser/coder" - version = "1.0.22" + version = "1.0.23" agent_id = coder_agent.example.id folder = "/home/coder/project" } @@ -39,17 +39,29 @@ module "filebrowser" { ```tf module "filebrowser" { source = "registry.coder.com/modules/filebrowser/coder" - version = "1.0.22" + version = "1.0.23" agent_id = coder_agent.example.id database_path = ".config/filebrowser.db" } ``` -### Serve from the same domain (no subdomain) +### Serve on a subpath (no wildcard subdomain) + +```tf +module "filebrowser" { + source = "registry.coder.com/modules/filebrowser/coder" + version = "1.0.23" + agent_id = coder_agent.example.id + subdomain = false +} +``` + +### Serve on a subpath with a specific agent name (multiple agents) ```tf module "filebrowser" { source = "registry.coder.com/modules/filebrowser/coder" + version = "1.0.23" agent_id = coder_agent.example.id agent_name = "main" subdomain = false diff --git a/filebrowser/main.tf b/filebrowser/main.tf index 4fd7459..bbaee6b 100644 --- a/filebrowser/main.tf +++ b/filebrowser/main.tf @@ -20,13 +20,8 @@ data "coder_workspace_owner" "me" {} variable "agent_name" { type = string - description = "The name of the main deployment. (Used to build the subpath for coder_app.)" - default = "" - validation { - # If subdomain is false, then agent_name must be set. - condition = var.subdomain || var.agent_name != "" - error_message = "The agent_name must be set." - } + description = "The name of the coder_agent resource. (Only required if subdomain is false and the template uses multiple agents.)" + default = null } variable "database_path" { @@ -73,6 +68,12 @@ variable "order" { default = null } +variable "slug" { + type = string + description = "The slug of the coder_app resource." + default = "filebrowser" +} + variable "subdomain" { type = bool description = <<-EOT @@ -85,7 +86,7 @@ variable "subdomain" { resource "coder_script" "filebrowser" { agent_id = var.agent_id display_name = "File Browser" - icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg" + icon = "/icon/filebrowser.svg" script = templatefile("${path.module}/run.sh", { LOG_PATH : var.log_path, PORT : var.port, @@ -93,18 +94,30 @@ resource "coder_script" "filebrowser" { LOG_PATH : var.log_path, DB_PATH : var.database_path, SUBDOMAIN : var.subdomain, - SERVER_BASE_PATH : var.subdomain ? "" : format("/@%s/%s.%s/apps/filebrowser", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.agent_name), + SERVER_BASE_PATH : local.server_base_path }) run_on_start = true } resource "coder_app" "filebrowser" { agent_id = var.agent_id - slug = "filebrowser" + slug = var.slug display_name = "File Browser" - url = "http://localhost:${var.port}" - icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg" + url = local.url + icon = "/icon/filebrowser.svg" subdomain = var.subdomain share = var.share order = var.order + + healthcheck { + url = local.healthcheck_url + interval = 5 + threshold = 6 + } +} + +locals { + server_base_path = var.subdomain ? "" : format(var.agent_name != null ? "/@%s/%s.%s/apps/%s" : "/@%s/%s/apps/%s", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.agent_name, var.slug) + url = "http://localhost:${var.port}${local.server_base_path}" + healthcheck_url = "http://localhost:${var.port}${local.server_base_path}/health" } diff --git a/jetbrains-gateway/README.md b/jetbrains-gateway/README.md index 0745fa7..43caf8e 100644 --- a/jetbrains-gateway/README.md +++ b/jetbrains-gateway/README.md @@ -14,7 +14,7 @@ This module adds a JetBrains Gateway Button to open any workspace with a single ```tf module "jetbrains_gateway" { source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.21" + version = "1.0.23" agent_id = coder_agent.example.id agent_name = "example" folder = "/home/coder/example" @@ -32,7 +32,7 @@ module "jetbrains_gateway" { ```tf module "jetbrains_gateway" { source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.21" + version = "1.0.23" agent_id = coder_agent.example.id agent_name = "example" folder = "/home/coder/example" @@ -46,7 +46,7 @@ module "jetbrains_gateway" { ```tf module "jetbrains_gateway" { source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.21" + version = "1.0.23" agent_id = coder_agent.example.id agent_name = "example" folder = "/home/coder/example" @@ -61,7 +61,7 @@ module "jetbrains_gateway" { ```tf module "jetbrains_gateway" { source = "registry.coder.com/modules/jetbrains-gateway/coder" - version = "1.0.21" + version = "1.0.23" agent_id = coder_agent.example.id agent_name = "example" folder = "/home/coder/example" diff --git a/jetbrains-gateway/main.tf b/jetbrains-gateway/main.tf index 2bc00d3..f693902 100644 --- a/jetbrains-gateway/main.tf +++ b/jetbrains-gateway/main.tf @@ -20,13 +20,13 @@ variable "agent_id" { variable "slug" { type = string - description = "The slug for the coder_app. Allows resuing the module with the same template." + description = "The slug for the coder_app resource." default = "gateway" } variable "agent_name" { type = string - description = "Agent name." + description = "The name of the coder_agent resource." } variable "folder" { @@ -258,26 +258,18 @@ resource "coder_app" "gateway" { icon = local.icon external = true order = var.order - url = join("", [ - "jetbrains-gateway://connect#type=coder&workspace=", + url = format( + "jetbrains-gateway://connect#type=coder&workspace=%s&owner=%s&agent=%s&folder=%s&url=%s&token=%s&ide_product_code=%s&ide_build_number=%s&ide_download_link=%s", data.coder_workspace.me.name, - "&owner=", data.coder_workspace_owner.me.name, - "&agent=", var.agent_name, - "&folder=", var.folder, - "&url=", data.coder_workspace.me.access_url, - "&token=", "$SESSION_TOKEN", - "&ide_product_code=", data.coder_parameter.jetbrains_ide.value, - "&ide_build_number=", local.build_number, - "&ide_download_link=", - local.download_link, - ]) + local.download_link + ) } output "identifier" { diff --git a/jupyter-notebook/README.md b/jupyter-notebook/README.md index 83d36cb..d9ef513 100644 --- a/jupyter-notebook/README.md +++ b/jupyter-notebook/README.md @@ -11,12 +11,36 @@ tags: [jupyter, helper, ide, web] A module that adds Jupyter Notebook in your Coder template. -![Jupyter Notebook](../.images/jupyter-notebook.png) - ```tf module "jupyter-notebook" { source = "registry.coder.com/modules/jupyter-notebook/coder" - version = "1.0.19" + version = "1.0.23" agent_id = coder_agent.example.id } ``` + +![Jupyter Notebook](../.images/jupyter-notebook.png) + +## Examples + +### Serve on a subpath (no wildcard subdomain) + +```tf +module "jupyter-notebook" { + source = "registry.coder.com/modules/jupyter-notebook/coder" + version = "1.0.23" + agent_id = coder_agent.example.id + subdomain = false +} +``` + +### Serve on a subpath with a specific agent name (multiple agents) + +```tf +module "jupyter-notebook" { + source = "registry.coder.com/modules/jupyter-notebook/coder" + version = "1.0.23" + agent_id = coder_agent.example.id + agent_name = "main" +} +``` diff --git a/jupyter-notebook/main.tf b/jupyter-notebook/main.tf index a588ef1..e846fad 100644 --- a/jupyter-notebook/main.tf +++ b/jupyter-notebook/main.tf @@ -42,9 +42,30 @@ variable "order" { default = null } +variable "agent_name" { + type = string + description = "The name of the coder_agent resource. (Only required if subdomain is false and the template uses multiple agents.)" + default = null +} + +variable "slug" { + type = string + description = "The slug of the coder_app resource." + default = "jupyter-notebook" +} + +variable "subdomain" { + type = bool + description = <<-EOT + Determines whether the app will be accessed via it's own subdomain or whether it will be accessed via a path on Coder. + If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible. + EOT + default = true +} + resource "coder_script" "jupyter-notebook" { agent_id = var.agent_id - display_name = "jupyter-notebook" + display_name = "Jupyter Notebook" icon = "/icon/jupyter.svg" script = templatefile("${path.module}/run.sh", { LOG_PATH : var.log_path, @@ -55,11 +76,22 @@ resource "coder_script" "jupyter-notebook" { resource "coder_app" "jupyter-notebook" { agent_id = var.agent_id - slug = "jupyter-notebook" + slug = var.slug display_name = "Jupyter Notebook" - url = "http://localhost:${var.port}" + url = local.url icon = "/icon/jupyter.svg" subdomain = true share = var.share order = var.order + + healthcheck { + url = local.healthcheck_url + interval = 5 + threshold = 6 + } +} +locals { + server_base_path = var.subdomain ? "" : format(var.agent_name != null ? "/@%s/%s.%s/apps/%s" : "/@%s/%s/apps/%s", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.agent_name, var.slug) + url = "http://localhost:${var.port}${local.server_base_path}" + healthcheck_url = "http://localhost:${var.port}${local.server_base_path}/api" } diff --git a/jupyterlab/README.md b/jupyterlab/README.md index 52d5a50..ecf4d3e 100644 --- a/jupyterlab/README.md +++ b/jupyterlab/README.md @@ -11,12 +11,36 @@ tags: [jupyter, helper, ide, web] A module that adds JupyterLab in your Coder template. -![JupyterLab](../.images/jupyterlab.png) - ```tf module "jupyterlab" { source = "registry.coder.com/modules/jupyterlab/coder" - version = "1.0.22" + version = "1.0.23" agent_id = coder_agent.example.id } ``` + +![JupyterLab](../.images/jupyterlab.png) + +## Examples + +### Serve on a subpath (no wildcard subdomain) + +```tf +module "jupyterlab" { + source = "registry.coder.com/modules/jupyterlab/coder" + version = "1.0.23" + agent_id = coder_agent.example.id + subdomain = false +} +``` + +### Serve on a subpath with a specific agent name (multiple agents) + +```tf +module "jupyterlab" { + source = "registry.coder.com/modules/jupyterlab/coder" + version = "1.0.23" + agent_id = coder_agent.example.id + agent_name = "main" +} +``` diff --git a/jupyterlab/main.tf b/jupyterlab/main.tf index d66edb1..7a9943c 100644 --- a/jupyterlab/main.tf +++ b/jupyterlab/main.tf @@ -41,7 +41,10 @@ variable "share" { variable "subdomain" { type = bool - description = "Determines whether JupyterLab will be accessed via its own subdomain or whether it will be accessed via a path on Coder." + description = <<-EOT + Determines whether the app will be accessed via it's own subdomain or whether it will be accessed via a path on Coder. + If wildcards have not been setup by the administrator then apps with "subdomain" set to true will not be accessible. + EOT default = true } @@ -51,6 +54,18 @@ variable "order" { default = null } +variable "agent_name" { + type = string + description = "The name of the coder_agent resource. (Only required if subdomain is false and the template uses multiple agents.)" + default = null +} + +variable "slug" { + type = string + description = "The slug of the coder_app resource." + default = "jupyterlab" +} + resource "coder_script" "jupyterlab" { agent_id = var.agent_id display_name = "jupyterlab" @@ -58,18 +73,24 @@ resource "coder_script" "jupyterlab" { script = templatefile("${path.module}/run.sh", { LOG_PATH : var.log_path, PORT : var.port - BASE_URL : var.subdomain ? "" : "/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}/apps/jupyterlab" + BASE_URL : local.server_base_path }) run_on_start = true } resource "coder_app" "jupyterlab" { agent_id = var.agent_id - slug = "jupyterlab" # sync with the usage in URL + slug = var.slug display_name = "JupyterLab" - url = var.subdomain ? "http://localhost:${var.port}" : "http://localhost:${var.port}/@${data.coder_workspace_owner.me.name}/${data.coder_workspace.me.name}/apps/jupyterlab" + url = local.url icon = "/icon/jupyter.svg" subdomain = var.subdomain share = var.share order = var.order } + +locals { + server_base_path = var.subdomain ? "" : format(var.agent_name != null ? "/@%s/%s.%s/apps/%s" : "/@%s/%s/apps/%s", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.agent_name, var.slug) + url = "http://localhost:${var.port}${local.server_base_path}" + healthcheck_url = "http://localhost:${var.port}${local.server_base_path}/api" +} \ No newline at end of file diff --git a/vscode-desktop/README.md b/vscode-desktop/README.md index bc8920d..e54d6b9 100644 --- a/vscode-desktop/README.md +++ b/vscode-desktop/README.md @@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder) ```tf module "vscode" { source = "registry.coder.com/modules/vscode-desktop/coder" - version = "1.0.15" + version = "1.0.23" agent_id = coder_agent.example.id } ``` @@ -28,7 +28,7 @@ module "vscode" { ```tf module "vscode" { source = "registry.coder.com/modules/vscode-desktop/coder" - version = "1.0.15" + version = "1.0.23" agent_id = coder_agent.example.id folder = "/home/coder/project" } diff --git a/vscode-desktop/main.tf b/vscode-desktop/main.tf index 16d070b..2575645 100644 --- a/vscode-desktop/main.tf +++ b/vscode-desktop/main.tf @@ -20,6 +20,12 @@ variable "folder" { default = "" } +variable "slug" { + type = string + description = "The slug of the coder_app resource." + default = "vscode-desktop" +} + variable "open_recent" { type = bool description = "Open the most recent workspace or folder. Falls back to the folder if there is no recent workspace or folder to open." @@ -39,21 +45,17 @@ resource "coder_app" "vscode" { agent_id = var.agent_id external = true icon = "/icon/code.svg" - slug = "vscode" + slug = var.slug display_name = "VS Code Desktop" order = var.order - url = join("", [ - "vscode://coder.coder-remote/open", - "?owner=", + url = format( + "vscode://coder.coder-remote/open?owner=%s&workspace=%s%s%s&url=%s&token=$SESSION_TOKEN", data.coder_workspace_owner.me.name, - "&workspace=", data.coder_workspace.me.name, - var.folder != "" ? join("", ["&folder=", var.folder]) : "", + var.folder != "" ? format("&folder=%s", var.folder) : "", var.open_recent ? "&openRecent" : "", - "&url=", - data.coder_workspace.me.access_url, - "&token=$SESSION_TOKEN", - ]) + data.coder_workspace.me.access_url + ) } output "vscode_url" { diff --git a/vscode-web/README.md b/vscode-web/README.md index 8b5fa68..18bb35c 100644 --- a/vscode-web/README.md +++ b/vscode-web/README.md @@ -14,7 +14,7 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/ ```tf module "vscode-web" { source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.22" + version = "1.0.23" agent_id = coder_agent.example.id accept_license = true } @@ -29,7 +29,7 @@ module "vscode-web" { ```tf module "vscode-web" { source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.22" + version = "1.0.23" agent_id = coder_agent.example.id install_prefix = "/home/coder/.vscode-web" folder = "/home/coder" @@ -42,7 +42,7 @@ module "vscode-web" { ```tf module "vscode-web" { source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.22" + version = "1.0.23" agent_id = coder_agent.example.id extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"] accept_license = true @@ -56,7 +56,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte ```tf module "vscode-web" { source = "registry.coder.com/modules/vscode-web/coder" - version = "1.0.22" + version = "1.0.23" agent_id = coder_agent.example.id extensions = ["dracula-theme.theme-dracula"] settings = { @@ -65,3 +65,26 @@ module "vscode-web" { accept_license = true } ``` + +### Serve on a subpath (no wildcard subdomain) + +```tf +module "vscode-web" { + source = "registry.coder.com/modules/vscode-web/coder" + version = "1.0.23" + agent_id = coder_agent.example.id + subdomain = false +} +``` + +### Serve on a subpath with a specific agent name (multiple agents) + +```tf +module "vscode-web" { + source = "registry.coder.com/modules/vscode-web/coder" + version = "1.0.23" + agent_id = coder_agent.example.id + agent_name = "main" + subdomain = false +} +``` diff --git a/vscode-web/main.tf b/vscode-web/main.tf index 207450e..16c7cc8 100644 --- a/vscode-web/main.tf +++ b/vscode-web/main.tf @@ -130,6 +130,12 @@ variable "subdomain" { default = true } +variable "agent_name" { + type = string + description = "The name of the coder_agent resource. (Only required if subdomain is false and the template uses multiple agents.)" + default = null +} + data "coder_workspace_owner" "me" {} data "coder_workspace" "me" {} @@ -185,7 +191,7 @@ resource "coder_app" "vscode-web" { } locals { - server_base_path = var.subdomain ? "" : format("/@%s/%s/apps/%s/", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.slug) + server_base_path = var.subdomain ? "" : format(var.agent_name != null ? "/@%s/%s.%s/apps/%s" : "/@%s/%s/apps/%s", data.coder_workspace_owner.me.name, data.coder_workspace.me.name, var.agent_name, var.slug) url = var.folder == "" ? "http://localhost:${var.port}${local.server_base_path}" : "http://localhost:${var.port}${local.server_base_path}?folder=${var.folder}" - healthcheck_url = var.subdomain ? "http://localhost:${var.port}/healthz" : "http://localhost:${var.port}${local.server_base_path}/healthz" + healthcheck_url = "http://localhost:${var.port}${local.server_base_path}/healthz" } diff --git a/windows-rdp/README.md b/windows-rdp/README.md index c4d35fd..d07954a 100644 --- a/windows-rdp/README.md +++ b/windows-rdp/README.md @@ -15,7 +15,7 @@ Enable Remote Desktop + a web based client on Windows workspaces, powered by [de # AWS example. See below for examples of using this module with other providers module "windows_rdp" { source = "registry.coder.com/modules/windows-rdp/coder" - version = "1.0.18" + version = "1.0.23" count = data.coder_workspace.me.start_count agent_id = resource.coder_agent.main.id resource_id = resource.aws_instance.dev.id @@ -33,7 +33,7 @@ module "windows_rdp" { ```tf module "windows_rdp" { source = "registry.coder.com/modules/windows-rdp/coder" - version = "1.0.18" + version = "1.0.23" count = data.coder_workspace.me.start_count agent_id = resource.coder_agent.main.id resource_id = resource.aws_instance.dev.id @@ -45,7 +45,7 @@ module "windows_rdp" { ```tf module "windows_rdp" { source = "registry.coder.com/modules/windows-rdp/coder" - version = "1.0.18" + version = "1.0.23" count = data.coder_workspace.me.start_count agent_id = resource.coder_agent.main.id resource_id = resource.google_compute_instance.dev[0].id diff --git a/windows-rdp/devolutions-patch.js b/windows-rdp/devolutions-patch.js index 020a40f..43577b1 100644 --- a/windows-rdp/devolutions-patch.js +++ b/windows-rdp/devolutions-patch.js @@ -53,23 +53,23 @@ const SCREEN_POLL_INTERVAL_MS = 500; * @satisfies {FormFieldEntries} */ const formFieldEntries = { - /** @readonly */ - username: { - /** @readonly */ - querySelector: "web-client-username-control input", - - /** @readonly */ - value: "${CODER_USERNAME}", - }, - - /** @readonly */ - password: { - /** @readonly */ - querySelector: "web-client-password-control input", - - /** @readonly */ - value: "${CODER_PASSWORD}", - }, + /** @readonly */ + username: { + /** @readonly */ + querySelector: "web-client-username-control input", + + /** @readonly */ + value: "${CODER_USERNAME}", + }, + + /** @readonly */ + password: { + /** @readonly */ + querySelector: "web-client-password-control input", + + /** @readonly */ + value: "${CODER_PASSWORD}", + }, }; /** @@ -93,32 +93,32 @@ const formFieldEntries = { * @returns {Promise} */ function setInputValue(inputField, inputText) { - return new Promise((resolve, reject) => { - // Adding timeout for input event, even though we'll be dispatching it - // immediately, just in the off chance that something in the Angular app - // intercepts it or stops it from propagating properly - const timeoutId = window.setTimeout(() => { - reject(new Error("Input event did not get processed correctly in time.")); - }, 3_000); - - const handleSuccessfulDispatch = () => { - window.clearTimeout(timeoutId); - inputField.removeEventListener("input", handleSuccessfulDispatch); - resolve(); - }; - - inputField.addEventListener("input", handleSuccessfulDispatch); - - // Code assumes that Angular will have an event handler in place to handle - // the new event - const inputEvent = new Event("input", { - bubbles: true, - cancelable: true, - }); - - inputField.value = inputText; - inputField.dispatchEvent(inputEvent); - }); + return new Promise((resolve, reject) => { + // Adding timeout for input event, even though we'll be dispatching it + // immediately, just in the off chance that something in the Angular app + // intercepts it or stops it from propagating properly + const timeoutId = window.setTimeout(() => { + reject(new Error("Input event did not get processed correctly in time.")); + }, 3_000); + + const handleSuccessfulDispatch = () => { + window.clearTimeout(timeoutId); + inputField.removeEventListener("input", handleSuccessfulDispatch); + resolve(); + }; + + inputField.addEventListener("input", handleSuccessfulDispatch); + + // Code assumes that Angular will have an event handler in place to handle + // the new event + const inputEvent = new Event("input", { + bubbles: true, + cancelable: true, + }); + + inputField.value = inputText; + inputField.dispatchEvent(inputEvent); + }); } /** @@ -137,91 +137,91 @@ function setInputValue(inputField, inputText) { * @returns {Promise} */ async function autoSubmitForm(myForm) { - const setProtocolValue = () => { - /** @type {HTMLDivElement | null} */ - const protocolDropdownTrigger = myForm.querySelector('div[role="button"]'); - if (protocolDropdownTrigger === null) { - throw new Error("No clickable trigger for setting protocol value"); - } - - protocolDropdownTrigger.click(); - - // Can't use form as container for querying the list of dropdown options, - // because the elements don't actually exist inside the form. They're placed - // in the top level of the HTML doc, and repositioned to make it look like - // they're part of the form. Avoids CSS stacking context issues, maybe? - /** @type {HTMLLIElement | null} */ - const protocolOption = document.querySelector( - 'p-dropdownitem[ng-reflect-label="' + PROTOCOL + '"] li', - ); - - if (protocolOption === null) { - throw new Error( - "Unable to find protocol option on screen that matches desired protocol", - ); - } - - protocolOption.click(); - }; - - const setHostname = () => { - /** @type {HTMLInputElement | null} */ - const hostnameInput = myForm.querySelector("p-autocomplete#hostname input"); - - if (hostnameInput === null) { - throw new Error("Unable to find field for adding hostname"); - } - - return setInputValue(hostnameInput, HOSTNAME); - }; - - const setCoderFormFieldValues = async () => { - // The RDP form will not appear on screen unless the dropdown is set to use - // the RDP protocol - const rdpSubsection = myForm.querySelector("rdp-form"); - if (rdpSubsection === null) { - throw new Error( - "Unable to find RDP subsection. Is the value of the protocol set to RDP?", - ); - } - - for (const { value, querySelector } of Object.values(formFieldEntries)) { - /** @type {HTMLInputElement | null} */ - const input = document.querySelector(querySelector); - - if (input === null) { - throw new Error( - 'Unable to element that matches query "' + querySelector + '"', - ); - } - - await setInputValue(input, value); - } - }; - - const triggerSubmission = () => { - /** @type {HTMLButtonElement | null} */ - const submitButton = myForm.querySelector( - 'p-button[ng-reflect-type="submit"] button', - ); - - if (submitButton === null) { - throw new Error("Unable to find submission button"); - } - - if (submitButton.disabled) { - throw new Error( - "Unable to submit form because submit button is disabled. Are all fields filled out correctly?", - ); - } - - submitButton.click(); - }; - - setProtocolValue(); - await setHostname(); - await setCoderFormFieldValues(); - triggerSubmission(); + const setProtocolValue = () => { + /** @type {HTMLDivElement | null} */ + const protocolDropdownTrigger = myForm.querySelector('div[role="button"]'); + if (protocolDropdownTrigger === null) { + throw new Error("No clickable trigger for setting protocol value"); + } + + protocolDropdownTrigger.click(); + + // Can't use form as container for querying the list of dropdown options, + // because the elements don't actually exist inside the form. They're placed + // in the top level of the HTML doc, and repositioned to make it look like + // they're part of the form. Avoids CSS stacking context issues, maybe? + /** @type {HTMLLIElement | null} */ + const protocolOption = document.querySelector( + `p-dropdownitem[ng-reflect-label="${PROTOCOL}"] li`, + ); + + if (protocolOption === null) { + throw new Error( + "Unable to find protocol option on screen that matches desired protocol", + ); + } + + protocolOption.click(); + }; + + const setHostname = () => { + /** @type {HTMLInputElement | null} */ + const hostnameInput = myForm.querySelector("p-autocomplete#hostname input"); + + if (hostnameInput === null) { + throw new Error("Unable to find field for adding hostname"); + } + + return setInputValue(hostnameInput, HOSTNAME); + }; + + const setCoderFormFieldValues = async () => { + // The RDP form will not appear on screen unless the dropdown is set to use + // the RDP protocol + const rdpSubsection = myForm.querySelector("rdp-form"); + if (rdpSubsection === null) { + throw new Error( + "Unable to find RDP subsection. Is the value of the protocol set to RDP?", + ); + } + + for (const { value, querySelector } of Object.values(formFieldEntries)) { + /** @type {HTMLInputElement | null} */ + const input = document.querySelector(querySelector); + + if (input === null) { + throw new Error( + `Unable to element that matches query "${querySelector}"`, + ); + } + + await setInputValue(input, value); + } + }; + + const triggerSubmission = () => { + /** @type {HTMLButtonElement | null} */ + const submitButton = myForm.querySelector( + 'p-button[ng-reflect-type="submit"] button', + ); + + if (submitButton === null) { + throw new Error("Unable to find submission button"); + } + + if (submitButton.disabled) { + throw new Error( + "Unable to submit form because submit button is disabled. Are all fields filled out correctly?", + ); + } + + submitButton.click(); + }; + + setProtocolValue(); + await setHostname(); + await setCoderFormFieldValues(); + triggerSubmission(); } /** @@ -231,58 +231,58 @@ async function autoSubmitForm(myForm) { * @returns {void} */ function setupFormDetection() { - /** @type {HTMLFormElement | null} */ - let formValueFromLastMutation = null; - - /** @returns {void} */ - const onDynamicTabMutation = () => { - /** @type {HTMLFormElement | null} */ - const latestForm = document.querySelector("web-client-form > form"); - - // Only try to auto-fill if we went from having no form on screen to - // having a form on screen. That way, we don't accidentally override the - // form if the user is trying to customize values, and this essentially - // makes the script values function as default values - const mounted = formValueFromLastMutation === null && latestForm !== null; - if (mounted) { - autoSubmitForm(latestForm); - } - - formValueFromLastMutation = latestForm; - }; - - /** @type {number | undefined} */ - let pollingId = undefined; - - /** @returns {void} */ - const checkScreenForDynamicTab = () => { - const dynamicTab = document.querySelector("web-client-dynamic-tab"); - - // Keep polling until the main content container is on screen - if (dynamicTab === null) { - return; - } - - window.clearInterval(pollingId); - - // Call the mutation callback manually, to ensure it runs at least once - onDynamicTabMutation(); - - // Having the mutation observer is kind of an extra safety net that isn't - // really expected to run that often. Most of the content in the dynamic - // tab is being rendered through Canvas, which won't trigger any mutations - // that the observer can detect - const dynamicTabObserver = new MutationObserver(onDynamicTabMutation); - dynamicTabObserver.observe(dynamicTab, { - subtree: true, - childList: true, - }); - }; - - pollingId = window.setInterval( - checkScreenForDynamicTab, - SCREEN_POLL_INTERVAL_MS, - ); + /** @type {HTMLFormElement | null} */ + let formValueFromLastMutation = null; + + /** @returns {void} */ + const onDynamicTabMutation = () => { + /** @type {HTMLFormElement | null} */ + const latestForm = document.querySelector("web-client-form > form"); + + // Only try to auto-fill if we went from having no form on screen to + // having a form on screen. That way, we don't accidentally override the + // form if the user is trying to customize values, and this essentially + // makes the script values function as default values + const mounted = formValueFromLastMutation === null && latestForm !== null; + if (mounted) { + autoSubmitForm(latestForm); + } + + formValueFromLastMutation = latestForm; + }; + + /** @type {number | undefined} */ + let pollingId = undefined; + + /** @returns {void} */ + const checkScreenForDynamicTab = () => { + const dynamicTab = document.querySelector("web-client-dynamic-tab"); + + // Keep polling until the main content container is on screen + if (dynamicTab === null) { + return; + } + + window.clearInterval(pollingId); + + // Call the mutation callback manually, to ensure it runs at least once + onDynamicTabMutation(); + + // Having the mutation observer is kind of an extra safety net that isn't + // really expected to run that often. Most of the content in the dynamic + // tab is being rendered through Canvas, which won't trigger any mutations + // that the observer can detect + const dynamicTabObserver = new MutationObserver(onDynamicTabMutation); + dynamicTabObserver.observe(dynamicTab, { + subtree: true, + childList: true, + }); + }; + + pollingId = window.setInterval( + checkScreenForDynamicTab, + SCREEN_POLL_INTERVAL_MS, + ); } /** @@ -292,34 +292,34 @@ function setupFormDetection() { * @returns {void} */ function setupAlwaysOnStyles() { - const styleId = "coder-patch--styles-always-on"; - const existingContainer = document.querySelector("#" + styleId); - if (existingContainer) { - return; - } - - const styleContainer = document.createElement("style"); - styleContainer.id = styleId; - styleContainer.innerHTML = ` + const styleId = "coder-patch--styles-always-on"; + const existingContainer = document.querySelector(`#${styleId}`); + if (existingContainer) { + return; + } + + const styleContainer = document.createElement("style"); + styleContainer.id = styleId; + styleContainer.innerHTML = ` /* app-menu corresponds to the sidebar of the default view. */ app-menu { display: none !important; } `; - document.head.appendChild(styleContainer); + document.head.appendChild(styleContainer); } function hideFormForInitialSubmission() { - const styleId = "coder-patch--styles-initial-submission"; - const cssOpacityVariableName = "--coder-opacity-multiplier"; - - /** @type {HTMLStyleElement | null} */ - let styleContainer = document.querySelector("#" + styleId); - if (!styleContainer) { - styleContainer = document.createElement("style"); - styleContainer.id = styleId; - styleContainer.innerHTML = ` + const styleId = "coder-patch--styles-initial-submission"; + const cssOpacityVariableName = "--coder-opacity-multiplier"; + + /** @type {HTMLStyleElement | null} */ + let styleContainer = document.querySelector(`#${styleId}`); + if (!styleContainer) { + styleContainer = document.createElement("style"); + styleContainer.id = styleId; + styleContainer.innerHTML = ` /* Have to use opacity instead of visibility, because the element still needs to be interactive via the script so that it can be auto-filled. @@ -350,50 +350,50 @@ function hideFormForInitialSubmission() { } `; - document.head.appendChild(styleContainer); - } - - // The root node being undefined should be physically impossible (if it's - // undefined, the browser itself is busted), but we need to do a type check - // here so that the rest of the function doesn't need to do type checks over - // and over. - const rootNode = document.querySelector(":root"); - if (!(rootNode instanceof HTMLHtmlElement)) { - // Remove the container entirely because if the browser is busted, who knows - // if the CSS variables can be applied correctly. Better to have something - // be a bit more ugly/painful to use, than have it be impossible to use - styleContainer.remove(); - return; - } - - // It's safe to make the form visible preemptively because Devolutions - // outputs the Windows view through an HTML canvas that it overlays on top - // of the rest of the app. Even if the form isn't hidden at the style level, - // it will still be covered up. - const restoreOpacity = () => { - rootNode.style.setProperty(cssOpacityVariableName, "1"); - }; - - // If this file gets more complicated, it might make sense to set up the - // timeout and event listener so that if one triggers, it cancels the other, - // but having restoreOpacity run more than once is a no-op for right now. - // Not a big deal if these don't get cleaned up. - - // Have the form automatically reappear no matter what, so that if something - // does break, the user isn't left out to dry - window.setTimeout(restoreOpacity, 5_000); - - /** @type {HTMLFormElement | null} */ - const form = document.querySelector("web-client-form > form"); - form?.addEventListener( - "submit", - () => { - // Not restoring opacity right away just to give the HTML canvas a little - // bit of time to get spun up and cover up the main form - window.setTimeout(restoreOpacity, 1_000); - }, - { once: true }, - ); + document.head.appendChild(styleContainer); + } + + // The root node being undefined should be physically impossible (if it's + // undefined, the browser itself is busted), but we need to do a type check + // here so that the rest of the function doesn't need to do type checks over + // and over. + const rootNode = document.querySelector(":root"); + if (!(rootNode instanceof HTMLHtmlElement)) { + // Remove the container entirely because if the browser is busted, who knows + // if the CSS variables can be applied correctly. Better to have something + // be a bit more ugly/painful to use, than have it be impossible to use + styleContainer.remove(); + return; + } + + // It's safe to make the form visible preemptively because Devolutions + // outputs the Windows view through an HTML canvas that it overlays on top + // of the rest of the app. Even if the form isn't hidden at the style level, + // it will still be covered up. + const restoreOpacity = () => { + rootNode.style.setProperty(cssOpacityVariableName, "1"); + }; + + // If this file gets more complicated, it might make sense to set up the + // timeout and event listener so that if one triggers, it cancels the other, + // but having restoreOpacity run more than once is a no-op for right now. + // Not a big deal if these don't get cleaned up. + + // Have the form automatically reappear no matter what, so that if something + // does break, the user isn't left out to dry + window.setTimeout(restoreOpacity, 5_000); + + /** @type {HTMLFormElement | null} */ + const form = document.querySelector("web-client-form > form"); + form?.addEventListener( + "submit", + () => { + // Not restoring opacity right away just to give the HTML canvas a little + // bit of time to get spun up and cover up the main form + window.setTimeout(restoreOpacity, 1_000); + }, + { once: true }, + ); } // Always safe to call these immediately because even if the Angular app isn't @@ -403,7 +403,7 @@ setupAlwaysOnStyles(); hideFormForInitialSubmission(); if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", setupFormDetection); + document.addEventListener("DOMContentLoaded", setupFormDetection); } else { - setupFormDetection(); + setupFormDetection(); } diff --git a/windows-rdp/main.tf b/windows-rdp/main.tf index 10ece09..a53204c 100644 --- a/windows-rdp/main.tf +++ b/windows-rdp/main.tf @@ -39,6 +39,18 @@ variable "admin_password" { sensitive = true } +variable "port" { + type = number + description = "The port to run the Devolutions Gateway on." + default = 7171 +} + +variable "slug" { + type = string + description = "The slug of the coder_app resource." + default = "web-rdp" +} + resource "coder_script" "windows-rdp" { agent_id = var.agent_id display_name = "windows-rdp" @@ -47,6 +59,7 @@ resource "coder_script" "windows-rdp" { script = templatefile("${path.module}/powershell-installation-script.tftpl", { admin_username = var.admin_username admin_password = var.admin_password + port = var.port # Wanted to have this be in the powershell template file, but Terraform # doesn't allow recursive calls to the templatefile function. Have to feed @@ -63,14 +76,14 @@ resource "coder_script" "windows-rdp" { resource "coder_app" "windows-rdp" { agent_id = var.agent_id share = var.share - slug = "web-rdp" + slug = var.slug display_name = "Web RDP" - url = "http://localhost:7171" + url = "http://localhost:${var.port}" icon = "/icon/desktop.svg" subdomain = true healthcheck { - url = "http://localhost:7171" + url = "http://localhost:${var.port}" interval = 5 threshold = 15 } @@ -80,7 +93,7 @@ resource "coder_app" "rdp-docs" { agent_id = var.agent_id display_name = "Local RDP" slug = "rdp-docs" - icon = "https://raw.githubusercontent.com/matifali/logos/main/windows.svg" - url = "https://coder.com/docs/ides/remote-desktops#rdp-desktop" + icon = "/icon/windows.svg" + url = "https://coder.com/docs/user-guides/workspace-access/remote-desktops#rdp-desktop" external = true } diff --git a/windows-rdp/powershell-installation-script.tftpl b/windows-rdp/powershell-installation-script.tftpl index 1b7ab48..dd2d58f 100644 --- a/windows-rdp/powershell-installation-script.tftpl +++ b/windows-rdp/powershell-installation-script.tftpl @@ -47,7 +47,7 @@ Install-DGatewayPackage # Configure Devolutions Gateway $Hostname = "localhost" -$HttpListener = New-DGatewayListener 'http://*:7171' 'http://*:7171' +$HttpListener = New-DGatewayListener "http://*:${port}" "http://*:${port}" $WebApp = New-DGatewayWebAppConfig -Enabled $true -Authentication None $ConfigParams = @{ Hostname = $Hostname