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.
This commit is contained in:
@@ -14,7 +14,7 @@ A file browser for your workspace.
|
|||||||
```tf
|
```tf
|
||||||
module "filebrowser" {
|
module "filebrowser" {
|
||||||
source = "registry.coder.com/modules/filebrowser/coder"
|
source = "registry.coder.com/modules/filebrowser/coder"
|
||||||
version = "1.0.22"
|
version = "1.0.23"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -28,7 +28,7 @@ module "filebrowser" {
|
|||||||
```tf
|
```tf
|
||||||
module "filebrowser" {
|
module "filebrowser" {
|
||||||
source = "registry.coder.com/modules/filebrowser/coder"
|
source = "registry.coder.com/modules/filebrowser/coder"
|
||||||
version = "1.0.22"
|
version = "1.0.23"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
folder = "/home/coder/project"
|
folder = "/home/coder/project"
|
||||||
}
|
}
|
||||||
@@ -39,17 +39,29 @@ module "filebrowser" {
|
|||||||
```tf
|
```tf
|
||||||
module "filebrowser" {
|
module "filebrowser" {
|
||||||
source = "registry.coder.com/modules/filebrowser/coder"
|
source = "registry.coder.com/modules/filebrowser/coder"
|
||||||
version = "1.0.22"
|
version = "1.0.23"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
database_path = ".config/filebrowser.db"
|
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
|
```tf
|
||||||
module "filebrowser" {
|
module "filebrowser" {
|
||||||
source = "registry.coder.com/modules/filebrowser/coder"
|
source = "registry.coder.com/modules/filebrowser/coder"
|
||||||
|
version = "1.0.23"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
agent_name = "main"
|
agent_name = "main"
|
||||||
subdomain = false
|
subdomain = false
|
||||||
|
|||||||
@@ -20,13 +20,8 @@ data "coder_workspace_owner" "me" {}
|
|||||||
|
|
||||||
variable "agent_name" {
|
variable "agent_name" {
|
||||||
type = string
|
type = string
|
||||||
description = "The name of the main deployment. (Used to build the subpath for coder_app.)"
|
description = "The name of the coder_agent resource. (Only required if subdomain is false and the template uses multiple agents.)"
|
||||||
default = ""
|
default = null
|
||||||
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."
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "database_path" {
|
variable "database_path" {
|
||||||
@@ -73,6 +68,12 @@ variable "order" {
|
|||||||
default = null
|
default = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "slug" {
|
||||||
|
type = string
|
||||||
|
description = "The slug of the coder_app resource."
|
||||||
|
default = "filebrowser"
|
||||||
|
}
|
||||||
|
|
||||||
variable "subdomain" {
|
variable "subdomain" {
|
||||||
type = bool
|
type = bool
|
||||||
description = <<-EOT
|
description = <<-EOT
|
||||||
@@ -85,7 +86,7 @@ variable "subdomain" {
|
|||||||
resource "coder_script" "filebrowser" {
|
resource "coder_script" "filebrowser" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
display_name = "File Browser"
|
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", {
|
script = templatefile("${path.module}/run.sh", {
|
||||||
LOG_PATH : var.log_path,
|
LOG_PATH : var.log_path,
|
||||||
PORT : var.port,
|
PORT : var.port,
|
||||||
@@ -93,18 +94,30 @@ resource "coder_script" "filebrowser" {
|
|||||||
LOG_PATH : var.log_path,
|
LOG_PATH : var.log_path,
|
||||||
DB_PATH : var.database_path,
|
DB_PATH : var.database_path,
|
||||||
SUBDOMAIN : var.subdomain,
|
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
|
run_on_start = true
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "coder_app" "filebrowser" {
|
resource "coder_app" "filebrowser" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
slug = "filebrowser"
|
slug = var.slug
|
||||||
display_name = "File Browser"
|
display_name = "File Browser"
|
||||||
url = "http://localhost:${var.port}"
|
url = local.url
|
||||||
icon = "https://raw.githubusercontent.com/filebrowser/logo/master/icon_raw.svg"
|
icon = "/icon/filebrowser.svg"
|
||||||
subdomain = var.subdomain
|
subdomain = var.subdomain
|
||||||
share = var.share
|
share = var.share
|
||||||
order = var.order
|
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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ This module adds a JetBrains Gateway Button to open any workspace with a single
|
|||||||
```tf
|
```tf
|
||||||
module "jetbrains_gateway" {
|
module "jetbrains_gateway" {
|
||||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||||
version = "1.0.21"
|
version = "1.0.23"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
agent_name = "example"
|
agent_name = "example"
|
||||||
folder = "/home/coder/example"
|
folder = "/home/coder/example"
|
||||||
@@ -32,7 +32,7 @@ module "jetbrains_gateway" {
|
|||||||
```tf
|
```tf
|
||||||
module "jetbrains_gateway" {
|
module "jetbrains_gateway" {
|
||||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||||
version = "1.0.21"
|
version = "1.0.23"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
agent_name = "example"
|
agent_name = "example"
|
||||||
folder = "/home/coder/example"
|
folder = "/home/coder/example"
|
||||||
@@ -46,7 +46,7 @@ module "jetbrains_gateway" {
|
|||||||
```tf
|
```tf
|
||||||
module "jetbrains_gateway" {
|
module "jetbrains_gateway" {
|
||||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||||
version = "1.0.21"
|
version = "1.0.23"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
agent_name = "example"
|
agent_name = "example"
|
||||||
folder = "/home/coder/example"
|
folder = "/home/coder/example"
|
||||||
@@ -61,7 +61,7 @@ module "jetbrains_gateway" {
|
|||||||
```tf
|
```tf
|
||||||
module "jetbrains_gateway" {
|
module "jetbrains_gateway" {
|
||||||
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
source = "registry.coder.com/modules/jetbrains-gateway/coder"
|
||||||
version = "1.0.21"
|
version = "1.0.23"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
agent_name = "example"
|
agent_name = "example"
|
||||||
folder = "/home/coder/example"
|
folder = "/home/coder/example"
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ variable "agent_id" {
|
|||||||
|
|
||||||
variable "slug" {
|
variable "slug" {
|
||||||
type = string
|
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"
|
default = "gateway"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "agent_name" {
|
variable "agent_name" {
|
||||||
type = string
|
type = string
|
||||||
description = "Agent name."
|
description = "The name of the coder_agent resource."
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "folder" {
|
variable "folder" {
|
||||||
@@ -258,26 +258,18 @@ resource "coder_app" "gateway" {
|
|||||||
icon = local.icon
|
icon = local.icon
|
||||||
external = true
|
external = true
|
||||||
order = var.order
|
order = var.order
|
||||||
url = join("", [
|
url = format(
|
||||||
"jetbrains-gateway://connect#type=coder&workspace=",
|
"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,
|
data.coder_workspace.me.name,
|
||||||
"&owner=",
|
|
||||||
data.coder_workspace_owner.me.name,
|
data.coder_workspace_owner.me.name,
|
||||||
"&agent=",
|
|
||||||
var.agent_name,
|
var.agent_name,
|
||||||
"&folder=",
|
|
||||||
var.folder,
|
var.folder,
|
||||||
"&url=",
|
|
||||||
data.coder_workspace.me.access_url,
|
data.coder_workspace.me.access_url,
|
||||||
"&token=",
|
|
||||||
"$SESSION_TOKEN",
|
"$SESSION_TOKEN",
|
||||||
"&ide_product_code=",
|
|
||||||
data.coder_parameter.jetbrains_ide.value,
|
data.coder_parameter.jetbrains_ide.value,
|
||||||
"&ide_build_number=",
|
|
||||||
local.build_number,
|
local.build_number,
|
||||||
"&ide_download_link=",
|
local.download_link
|
||||||
local.download_link,
|
)
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
output "identifier" {
|
output "identifier" {
|
||||||
|
|||||||
@@ -11,12 +11,36 @@ tags: [jupyter, helper, ide, web]
|
|||||||
|
|
||||||
A module that adds Jupyter Notebook in your Coder template.
|
A module that adds Jupyter Notebook in your Coder template.
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "jupyter-notebook" {
|
module "jupyter-notebook" {
|
||||||
source = "registry.coder.com/modules/jupyter-notebook/coder"
|
source = "registry.coder.com/modules/jupyter-notebook/coder"
|
||||||
version = "1.0.19"
|
version = "1.0.23"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -42,9 +42,30 @@ variable "order" {
|
|||||||
default = null
|
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" {
|
resource "coder_script" "jupyter-notebook" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
display_name = "jupyter-notebook"
|
display_name = "Jupyter Notebook"
|
||||||
icon = "/icon/jupyter.svg"
|
icon = "/icon/jupyter.svg"
|
||||||
script = templatefile("${path.module}/run.sh", {
|
script = templatefile("${path.module}/run.sh", {
|
||||||
LOG_PATH : var.log_path,
|
LOG_PATH : var.log_path,
|
||||||
@@ -55,11 +76,22 @@ resource "coder_script" "jupyter-notebook" {
|
|||||||
|
|
||||||
resource "coder_app" "jupyter-notebook" {
|
resource "coder_app" "jupyter-notebook" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
slug = "jupyter-notebook"
|
slug = var.slug
|
||||||
display_name = "Jupyter Notebook"
|
display_name = "Jupyter Notebook"
|
||||||
url = "http://localhost:${var.port}"
|
url = local.url
|
||||||
icon = "/icon/jupyter.svg"
|
icon = "/icon/jupyter.svg"
|
||||||
subdomain = true
|
subdomain = true
|
||||||
share = var.share
|
share = var.share
|
||||||
order = var.order
|
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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,12 +11,36 @@ tags: [jupyter, helper, ide, web]
|
|||||||
|
|
||||||
A module that adds JupyterLab in your Coder template.
|
A module that adds JupyterLab in your Coder template.
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
```tf
|
```tf
|
||||||
module "jupyterlab" {
|
module "jupyterlab" {
|
||||||
source = "registry.coder.com/modules/jupyterlab/coder"
|
source = "registry.coder.com/modules/jupyterlab/coder"
|
||||||
version = "1.0.22"
|
version = "1.0.23"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## 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"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -41,7 +41,10 @@ variable "share" {
|
|||||||
|
|
||||||
variable "subdomain" {
|
variable "subdomain" {
|
||||||
type = bool
|
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
|
default = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +54,18 @@ variable "order" {
|
|||||||
default = null
|
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" {
|
resource "coder_script" "jupyterlab" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
display_name = "jupyterlab"
|
display_name = "jupyterlab"
|
||||||
@@ -58,18 +73,24 @@ resource "coder_script" "jupyterlab" {
|
|||||||
script = templatefile("${path.module}/run.sh", {
|
script = templatefile("${path.module}/run.sh", {
|
||||||
LOG_PATH : var.log_path,
|
LOG_PATH : var.log_path,
|
||||||
PORT : var.port
|
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
|
run_on_start = true
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "coder_app" "jupyterlab" {
|
resource "coder_app" "jupyterlab" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
slug = "jupyterlab" # sync with the usage in URL
|
slug = var.slug
|
||||||
display_name = "JupyterLab"
|
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"
|
icon = "/icon/jupyter.svg"
|
||||||
subdomain = var.subdomain
|
subdomain = var.subdomain
|
||||||
share = var.share
|
share = var.share
|
||||||
order = var.order
|
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"
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ Uses the [Coder Remote VS Code Extension](https://github.com/coder/vscode-coder)
|
|||||||
```tf
|
```tf
|
||||||
module "vscode" {
|
module "vscode" {
|
||||||
source = "registry.coder.com/modules/vscode-desktop/coder"
|
source = "registry.coder.com/modules/vscode-desktop/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.23"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@@ -28,7 +28,7 @@ module "vscode" {
|
|||||||
```tf
|
```tf
|
||||||
module "vscode" {
|
module "vscode" {
|
||||||
source = "registry.coder.com/modules/vscode-desktop/coder"
|
source = "registry.coder.com/modules/vscode-desktop/coder"
|
||||||
version = "1.0.15"
|
version = "1.0.23"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
folder = "/home/coder/project"
|
folder = "/home/coder/project"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,12 @@ variable "folder" {
|
|||||||
default = ""
|
default = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
variable "slug" {
|
||||||
|
type = string
|
||||||
|
description = "The slug of the coder_app resource."
|
||||||
|
default = "vscode-desktop"
|
||||||
|
}
|
||||||
|
|
||||||
variable "open_recent" {
|
variable "open_recent" {
|
||||||
type = bool
|
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."
|
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
|
agent_id = var.agent_id
|
||||||
external = true
|
external = true
|
||||||
icon = "/icon/code.svg"
|
icon = "/icon/code.svg"
|
||||||
slug = "vscode"
|
slug = var.slug
|
||||||
display_name = "VS Code Desktop"
|
display_name = "VS Code Desktop"
|
||||||
order = var.order
|
order = var.order
|
||||||
url = join("", [
|
url = format(
|
||||||
"vscode://coder.coder-remote/open",
|
"vscode://coder.coder-remote/open?owner=%s&workspace=%s%s%s&url=%s&token=$SESSION_TOKEN",
|
||||||
"?owner=",
|
|
||||||
data.coder_workspace_owner.me.name,
|
data.coder_workspace_owner.me.name,
|
||||||
"&workspace=",
|
|
||||||
data.coder_workspace.me.name,
|
data.coder_workspace.me.name,
|
||||||
var.folder != "" ? join("", ["&folder=", var.folder]) : "",
|
var.folder != "" ? format("&folder=%s", var.folder) : "",
|
||||||
var.open_recent ? "&openRecent" : "",
|
var.open_recent ? "&openRecent" : "",
|
||||||
"&url=",
|
data.coder_workspace.me.access_url
|
||||||
data.coder_workspace.me.access_url,
|
)
|
||||||
"&token=$SESSION_TOKEN",
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
output "vscode_url" {
|
output "vscode_url" {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ Automatically install [Visual Studio Code Server](https://code.visualstudio.com/
|
|||||||
```tf
|
```tf
|
||||||
module "vscode-web" {
|
module "vscode-web" {
|
||||||
source = "registry.coder.com/modules/vscode-web/coder"
|
source = "registry.coder.com/modules/vscode-web/coder"
|
||||||
version = "1.0.22"
|
version = "1.0.23"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
accept_license = true
|
accept_license = true
|
||||||
}
|
}
|
||||||
@@ -29,7 +29,7 @@ module "vscode-web" {
|
|||||||
```tf
|
```tf
|
||||||
module "vscode-web" {
|
module "vscode-web" {
|
||||||
source = "registry.coder.com/modules/vscode-web/coder"
|
source = "registry.coder.com/modules/vscode-web/coder"
|
||||||
version = "1.0.22"
|
version = "1.0.23"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
install_prefix = "/home/coder/.vscode-web"
|
install_prefix = "/home/coder/.vscode-web"
|
||||||
folder = "/home/coder"
|
folder = "/home/coder"
|
||||||
@@ -42,7 +42,7 @@ module "vscode-web" {
|
|||||||
```tf
|
```tf
|
||||||
module "vscode-web" {
|
module "vscode-web" {
|
||||||
source = "registry.coder.com/modules/vscode-web/coder"
|
source = "registry.coder.com/modules/vscode-web/coder"
|
||||||
version = "1.0.22"
|
version = "1.0.23"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
|
extensions = ["github.copilot", "ms-python.python", "ms-toolsai.jupyter"]
|
||||||
accept_license = true
|
accept_license = true
|
||||||
@@ -56,7 +56,7 @@ Configure VS Code's [settings.json](https://code.visualstudio.com/docs/getstarte
|
|||||||
```tf
|
```tf
|
||||||
module "vscode-web" {
|
module "vscode-web" {
|
||||||
source = "registry.coder.com/modules/vscode-web/coder"
|
source = "registry.coder.com/modules/vscode-web/coder"
|
||||||
version = "1.0.22"
|
version = "1.0.23"
|
||||||
agent_id = coder_agent.example.id
|
agent_id = coder_agent.example.id
|
||||||
extensions = ["dracula-theme.theme-dracula"]
|
extensions = ["dracula-theme.theme-dracula"]
|
||||||
settings = {
|
settings = {
|
||||||
@@ -65,3 +65,26 @@ module "vscode-web" {
|
|||||||
accept_license = true
|
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
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|||||||
@@ -130,6 +130,12 @@ variable "subdomain" {
|
|||||||
default = true
|
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_owner" "me" {}
|
||||||
data "coder_workspace" "me" {}
|
data "coder_workspace" "me" {}
|
||||||
|
|
||||||
@@ -185,7 +191,7 @@ resource "coder_app" "vscode-web" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
locals {
|
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}"
|
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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
# AWS example. See below for examples of using this module with other providers
|
||||||
module "windows_rdp" {
|
module "windows_rdp" {
|
||||||
source = "registry.coder.com/modules/windows-rdp/coder"
|
source = "registry.coder.com/modules/windows-rdp/coder"
|
||||||
version = "1.0.18"
|
version = "1.0.23"
|
||||||
count = data.coder_workspace.me.start_count
|
count = data.coder_workspace.me.start_count
|
||||||
agent_id = resource.coder_agent.main.id
|
agent_id = resource.coder_agent.main.id
|
||||||
resource_id = resource.aws_instance.dev.id
|
resource_id = resource.aws_instance.dev.id
|
||||||
@@ -33,7 +33,7 @@ module "windows_rdp" {
|
|||||||
```tf
|
```tf
|
||||||
module "windows_rdp" {
|
module "windows_rdp" {
|
||||||
source = "registry.coder.com/modules/windows-rdp/coder"
|
source = "registry.coder.com/modules/windows-rdp/coder"
|
||||||
version = "1.0.18"
|
version = "1.0.23"
|
||||||
count = data.coder_workspace.me.start_count
|
count = data.coder_workspace.me.start_count
|
||||||
agent_id = resource.coder_agent.main.id
|
agent_id = resource.coder_agent.main.id
|
||||||
resource_id = resource.aws_instance.dev.id
|
resource_id = resource.aws_instance.dev.id
|
||||||
@@ -45,7 +45,7 @@ module "windows_rdp" {
|
|||||||
```tf
|
```tf
|
||||||
module "windows_rdp" {
|
module "windows_rdp" {
|
||||||
source = "registry.coder.com/modules/windows-rdp/coder"
|
source = "registry.coder.com/modules/windows-rdp/coder"
|
||||||
version = "1.0.18"
|
version = "1.0.23"
|
||||||
count = data.coder_workspace.me.start_count
|
count = data.coder_workspace.me.start_count
|
||||||
agent_id = resource.coder_agent.main.id
|
agent_id = resource.coder_agent.main.id
|
||||||
resource_id = resource.google_compute_instance.dev[0].id
|
resource_id = resource.google_compute_instance.dev[0].id
|
||||||
|
|||||||
@@ -53,23 +53,23 @@ const SCREEN_POLL_INTERVAL_MS = 500;
|
|||||||
* @satisfies {FormFieldEntries}
|
* @satisfies {FormFieldEntries}
|
||||||
*/
|
*/
|
||||||
const formFieldEntries = {
|
const formFieldEntries = {
|
||||||
/** @readonly */
|
/** @readonly */
|
||||||
username: {
|
username: {
|
||||||
/** @readonly */
|
/** @readonly */
|
||||||
querySelector: "web-client-username-control input",
|
querySelector: "web-client-username-control input",
|
||||||
|
|
||||||
/** @readonly */
|
/** @readonly */
|
||||||
value: "${CODER_USERNAME}",
|
value: "${CODER_USERNAME}",
|
||||||
},
|
},
|
||||||
|
|
||||||
/** @readonly */
|
/** @readonly */
|
||||||
password: {
|
password: {
|
||||||
/** @readonly */
|
/** @readonly */
|
||||||
querySelector: "web-client-password-control input",
|
querySelector: "web-client-password-control input",
|
||||||
|
|
||||||
/** @readonly */
|
/** @readonly */
|
||||||
value: "${CODER_PASSWORD}",
|
value: "${CODER_PASSWORD}",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -93,32 +93,32 @@ const formFieldEntries = {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
function setInputValue(inputField, inputText) {
|
function setInputValue(inputField, inputText) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
// Adding timeout for input event, even though we'll be dispatching it
|
// Adding timeout for input event, even though we'll be dispatching it
|
||||||
// immediately, just in the off chance that something in the Angular app
|
// immediately, just in the off chance that something in the Angular app
|
||||||
// intercepts it or stops it from propagating properly
|
// intercepts it or stops it from propagating properly
|
||||||
const timeoutId = window.setTimeout(() => {
|
const timeoutId = window.setTimeout(() => {
|
||||||
reject(new Error("Input event did not get processed correctly in time."));
|
reject(new Error("Input event did not get processed correctly in time."));
|
||||||
}, 3_000);
|
}, 3_000);
|
||||||
|
|
||||||
const handleSuccessfulDispatch = () => {
|
const handleSuccessfulDispatch = () => {
|
||||||
window.clearTimeout(timeoutId);
|
window.clearTimeout(timeoutId);
|
||||||
inputField.removeEventListener("input", handleSuccessfulDispatch);
|
inputField.removeEventListener("input", handleSuccessfulDispatch);
|
||||||
resolve();
|
resolve();
|
||||||
};
|
};
|
||||||
|
|
||||||
inputField.addEventListener("input", handleSuccessfulDispatch);
|
inputField.addEventListener("input", handleSuccessfulDispatch);
|
||||||
|
|
||||||
// Code assumes that Angular will have an event handler in place to handle
|
// Code assumes that Angular will have an event handler in place to handle
|
||||||
// the new event
|
// the new event
|
||||||
const inputEvent = new Event("input", {
|
const inputEvent = new Event("input", {
|
||||||
bubbles: true,
|
bubbles: true,
|
||||||
cancelable: true,
|
cancelable: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
inputField.value = inputText;
|
inputField.value = inputText;
|
||||||
inputField.dispatchEvent(inputEvent);
|
inputField.dispatchEvent(inputEvent);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -137,91 +137,91 @@ function setInputValue(inputField, inputText) {
|
|||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async function autoSubmitForm(myForm) {
|
async function autoSubmitForm(myForm) {
|
||||||
const setProtocolValue = () => {
|
const setProtocolValue = () => {
|
||||||
/** @type {HTMLDivElement | null} */
|
/** @type {HTMLDivElement | null} */
|
||||||
const protocolDropdownTrigger = myForm.querySelector('div[role="button"]');
|
const protocolDropdownTrigger = myForm.querySelector('div[role="button"]');
|
||||||
if (protocolDropdownTrigger === null) {
|
if (protocolDropdownTrigger === null) {
|
||||||
throw new Error("No clickable trigger for setting protocol value");
|
throw new Error("No clickable trigger for setting protocol value");
|
||||||
}
|
}
|
||||||
|
|
||||||
protocolDropdownTrigger.click();
|
protocolDropdownTrigger.click();
|
||||||
|
|
||||||
// Can't use form as container for querying the list of dropdown options,
|
// 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
|
// 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
|
// 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?
|
// they're part of the form. Avoids CSS stacking context issues, maybe?
|
||||||
/** @type {HTMLLIElement | null} */
|
/** @type {HTMLLIElement | null} */
|
||||||
const protocolOption = document.querySelector(
|
const protocolOption = document.querySelector(
|
||||||
'p-dropdownitem[ng-reflect-label="' + PROTOCOL + '"] li',
|
`p-dropdownitem[ng-reflect-label="${PROTOCOL}"] li`,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (protocolOption === null) {
|
if (protocolOption === null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Unable to find protocol option on screen that matches desired protocol",
|
"Unable to find protocol option on screen that matches desired protocol",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protocolOption.click();
|
protocolOption.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
const setHostname = () => {
|
const setHostname = () => {
|
||||||
/** @type {HTMLInputElement | null} */
|
/** @type {HTMLInputElement | null} */
|
||||||
const hostnameInput = myForm.querySelector("p-autocomplete#hostname input");
|
const hostnameInput = myForm.querySelector("p-autocomplete#hostname input");
|
||||||
|
|
||||||
if (hostnameInput === null) {
|
if (hostnameInput === null) {
|
||||||
throw new Error("Unable to find field for adding hostname");
|
throw new Error("Unable to find field for adding hostname");
|
||||||
}
|
}
|
||||||
|
|
||||||
return setInputValue(hostnameInput, HOSTNAME);
|
return setInputValue(hostnameInput, HOSTNAME);
|
||||||
};
|
};
|
||||||
|
|
||||||
const setCoderFormFieldValues = async () => {
|
const setCoderFormFieldValues = async () => {
|
||||||
// The RDP form will not appear on screen unless the dropdown is set to use
|
// The RDP form will not appear on screen unless the dropdown is set to use
|
||||||
// the RDP protocol
|
// the RDP protocol
|
||||||
const rdpSubsection = myForm.querySelector("rdp-form");
|
const rdpSubsection = myForm.querySelector("rdp-form");
|
||||||
if (rdpSubsection === null) {
|
if (rdpSubsection === null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Unable to find RDP subsection. Is the value of the protocol set to RDP?",
|
"Unable to find RDP subsection. Is the value of the protocol set to RDP?",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const { value, querySelector } of Object.values(formFieldEntries)) {
|
for (const { value, querySelector } of Object.values(formFieldEntries)) {
|
||||||
/** @type {HTMLInputElement | null} */
|
/** @type {HTMLInputElement | null} */
|
||||||
const input = document.querySelector(querySelector);
|
const input = document.querySelector(querySelector);
|
||||||
|
|
||||||
if (input === null) {
|
if (input === null) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
'Unable to element that matches query "' + querySelector + '"',
|
`Unable to element that matches query "${querySelector}"`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await setInputValue(input, value);
|
await setInputValue(input, value);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const triggerSubmission = () => {
|
const triggerSubmission = () => {
|
||||||
/** @type {HTMLButtonElement | null} */
|
/** @type {HTMLButtonElement | null} */
|
||||||
const submitButton = myForm.querySelector(
|
const submitButton = myForm.querySelector(
|
||||||
'p-button[ng-reflect-type="submit"] button',
|
'p-button[ng-reflect-type="submit"] button',
|
||||||
);
|
);
|
||||||
|
|
||||||
if (submitButton === null) {
|
if (submitButton === null) {
|
||||||
throw new Error("Unable to find submission button");
|
throw new Error("Unable to find submission button");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (submitButton.disabled) {
|
if (submitButton.disabled) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Unable to submit form because submit button is disabled. Are all fields filled out correctly?",
|
"Unable to submit form because submit button is disabled. Are all fields filled out correctly?",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
submitButton.click();
|
submitButton.click();
|
||||||
};
|
};
|
||||||
|
|
||||||
setProtocolValue();
|
setProtocolValue();
|
||||||
await setHostname();
|
await setHostname();
|
||||||
await setCoderFormFieldValues();
|
await setCoderFormFieldValues();
|
||||||
triggerSubmission();
|
triggerSubmission();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -231,58 +231,58 @@ async function autoSubmitForm(myForm) {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function setupFormDetection() {
|
function setupFormDetection() {
|
||||||
/** @type {HTMLFormElement | null} */
|
/** @type {HTMLFormElement | null} */
|
||||||
let formValueFromLastMutation = null;
|
let formValueFromLastMutation = null;
|
||||||
|
|
||||||
/** @returns {void} */
|
/** @returns {void} */
|
||||||
const onDynamicTabMutation = () => {
|
const onDynamicTabMutation = () => {
|
||||||
/** @type {HTMLFormElement | null} */
|
/** @type {HTMLFormElement | null} */
|
||||||
const latestForm = document.querySelector("web-client-form > form");
|
const latestForm = document.querySelector("web-client-form > form");
|
||||||
|
|
||||||
// Only try to auto-fill if we went from having no form on screen to
|
// 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
|
// 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
|
// form if the user is trying to customize values, and this essentially
|
||||||
// makes the script values function as default values
|
// makes the script values function as default values
|
||||||
const mounted = formValueFromLastMutation === null && latestForm !== null;
|
const mounted = formValueFromLastMutation === null && latestForm !== null;
|
||||||
if (mounted) {
|
if (mounted) {
|
||||||
autoSubmitForm(latestForm);
|
autoSubmitForm(latestForm);
|
||||||
}
|
}
|
||||||
|
|
||||||
formValueFromLastMutation = latestForm;
|
formValueFromLastMutation = latestForm;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** @type {number | undefined} */
|
/** @type {number | undefined} */
|
||||||
let pollingId = undefined;
|
let pollingId = undefined;
|
||||||
|
|
||||||
/** @returns {void} */
|
/** @returns {void} */
|
||||||
const checkScreenForDynamicTab = () => {
|
const checkScreenForDynamicTab = () => {
|
||||||
const dynamicTab = document.querySelector("web-client-dynamic-tab");
|
const dynamicTab = document.querySelector("web-client-dynamic-tab");
|
||||||
|
|
||||||
// Keep polling until the main content container is on screen
|
// Keep polling until the main content container is on screen
|
||||||
if (dynamicTab === null) {
|
if (dynamicTab === null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.clearInterval(pollingId);
|
window.clearInterval(pollingId);
|
||||||
|
|
||||||
// Call the mutation callback manually, to ensure it runs at least once
|
// Call the mutation callback manually, to ensure it runs at least once
|
||||||
onDynamicTabMutation();
|
onDynamicTabMutation();
|
||||||
|
|
||||||
// Having the mutation observer is kind of an extra safety net that isn't
|
// 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
|
// 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
|
// tab is being rendered through Canvas, which won't trigger any mutations
|
||||||
// that the observer can detect
|
// that the observer can detect
|
||||||
const dynamicTabObserver = new MutationObserver(onDynamicTabMutation);
|
const dynamicTabObserver = new MutationObserver(onDynamicTabMutation);
|
||||||
dynamicTabObserver.observe(dynamicTab, {
|
dynamicTabObserver.observe(dynamicTab, {
|
||||||
subtree: true,
|
subtree: true,
|
||||||
childList: true,
|
childList: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
pollingId = window.setInterval(
|
pollingId = window.setInterval(
|
||||||
checkScreenForDynamicTab,
|
checkScreenForDynamicTab,
|
||||||
SCREEN_POLL_INTERVAL_MS,
|
SCREEN_POLL_INTERVAL_MS,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -292,34 +292,34 @@ function setupFormDetection() {
|
|||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
function setupAlwaysOnStyles() {
|
function setupAlwaysOnStyles() {
|
||||||
const styleId = "coder-patch--styles-always-on";
|
const styleId = "coder-patch--styles-always-on";
|
||||||
const existingContainer = document.querySelector("#" + styleId);
|
const existingContainer = document.querySelector(`#${styleId}`);
|
||||||
if (existingContainer) {
|
if (existingContainer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const styleContainer = document.createElement("style");
|
const styleContainer = document.createElement("style");
|
||||||
styleContainer.id = styleId;
|
styleContainer.id = styleId;
|
||||||
styleContainer.innerHTML = `
|
styleContainer.innerHTML = `
|
||||||
/* app-menu corresponds to the sidebar of the default view. */
|
/* app-menu corresponds to the sidebar of the default view. */
|
||||||
app-menu {
|
app-menu {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.head.appendChild(styleContainer);
|
document.head.appendChild(styleContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hideFormForInitialSubmission() {
|
function hideFormForInitialSubmission() {
|
||||||
const styleId = "coder-patch--styles-initial-submission";
|
const styleId = "coder-patch--styles-initial-submission";
|
||||||
const cssOpacityVariableName = "--coder-opacity-multiplier";
|
const cssOpacityVariableName = "--coder-opacity-multiplier";
|
||||||
|
|
||||||
/** @type {HTMLStyleElement | null} */
|
/** @type {HTMLStyleElement | null} */
|
||||||
let styleContainer = document.querySelector("#" + styleId);
|
let styleContainer = document.querySelector(`#${styleId}`);
|
||||||
if (!styleContainer) {
|
if (!styleContainer) {
|
||||||
styleContainer = document.createElement("style");
|
styleContainer = document.createElement("style");
|
||||||
styleContainer.id = styleId;
|
styleContainer.id = styleId;
|
||||||
styleContainer.innerHTML = `
|
styleContainer.innerHTML = `
|
||||||
/*
|
/*
|
||||||
Have to use opacity instead of visibility, because the element still
|
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.
|
needs to be interactive via the script so that it can be auto-filled.
|
||||||
@@ -350,50 +350,50 @@ function hideFormForInitialSubmission() {
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
document.head.appendChild(styleContainer);
|
document.head.appendChild(styleContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
// The root node being undefined should be physically impossible (if it's
|
// 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
|
// 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
|
// here so that the rest of the function doesn't need to do type checks over
|
||||||
// and over.
|
// and over.
|
||||||
const rootNode = document.querySelector(":root");
|
const rootNode = document.querySelector(":root");
|
||||||
if (!(rootNode instanceof HTMLHtmlElement)) {
|
if (!(rootNode instanceof HTMLHtmlElement)) {
|
||||||
// Remove the container entirely because if the browser is busted, who knows
|
// Remove the container entirely because if the browser is busted, who knows
|
||||||
// if the CSS variables can be applied correctly. Better to have something
|
// 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
|
// be a bit more ugly/painful to use, than have it be impossible to use
|
||||||
styleContainer.remove();
|
styleContainer.remove();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// It's safe to make the form visible preemptively because Devolutions
|
// It's safe to make the form visible preemptively because Devolutions
|
||||||
// outputs the Windows view through an HTML canvas that it overlays on top
|
// 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,
|
// of the rest of the app. Even if the form isn't hidden at the style level,
|
||||||
// it will still be covered up.
|
// it will still be covered up.
|
||||||
const restoreOpacity = () => {
|
const restoreOpacity = () => {
|
||||||
rootNode.style.setProperty(cssOpacityVariableName, "1");
|
rootNode.style.setProperty(cssOpacityVariableName, "1");
|
||||||
};
|
};
|
||||||
|
|
||||||
// If this file gets more complicated, it might make sense to set up the
|
// 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,
|
// 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.
|
// 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.
|
// Not a big deal if these don't get cleaned up.
|
||||||
|
|
||||||
// Have the form automatically reappear no matter what, so that if something
|
// Have the form automatically reappear no matter what, so that if something
|
||||||
// does break, the user isn't left out to dry
|
// does break, the user isn't left out to dry
|
||||||
window.setTimeout(restoreOpacity, 5_000);
|
window.setTimeout(restoreOpacity, 5_000);
|
||||||
|
|
||||||
/** @type {HTMLFormElement | null} */
|
/** @type {HTMLFormElement | null} */
|
||||||
const form = document.querySelector("web-client-form > form");
|
const form = document.querySelector("web-client-form > form");
|
||||||
form?.addEventListener(
|
form?.addEventListener(
|
||||||
"submit",
|
"submit",
|
||||||
() => {
|
() => {
|
||||||
// Not restoring opacity right away just to give the HTML canvas a little
|
// 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
|
// bit of time to get spun up and cover up the main form
|
||||||
window.setTimeout(restoreOpacity, 1_000);
|
window.setTimeout(restoreOpacity, 1_000);
|
||||||
},
|
},
|
||||||
{ once: true },
|
{ once: true },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Always safe to call these immediately because even if the Angular app isn't
|
// Always safe to call these immediately because even if the Angular app isn't
|
||||||
@@ -403,7 +403,7 @@ setupAlwaysOnStyles();
|
|||||||
hideFormForInitialSubmission();
|
hideFormForInitialSubmission();
|
||||||
|
|
||||||
if (document.readyState === "loading") {
|
if (document.readyState === "loading") {
|
||||||
document.addEventListener("DOMContentLoaded", setupFormDetection);
|
document.addEventListener("DOMContentLoaded", setupFormDetection);
|
||||||
} else {
|
} else {
|
||||||
setupFormDetection();
|
setupFormDetection();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,6 +39,18 @@ variable "admin_password" {
|
|||||||
sensitive = true
|
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" {
|
resource "coder_script" "windows-rdp" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
display_name = "windows-rdp"
|
display_name = "windows-rdp"
|
||||||
@@ -47,6 +59,7 @@ resource "coder_script" "windows-rdp" {
|
|||||||
script = templatefile("${path.module}/powershell-installation-script.tftpl", {
|
script = templatefile("${path.module}/powershell-installation-script.tftpl", {
|
||||||
admin_username = var.admin_username
|
admin_username = var.admin_username
|
||||||
admin_password = var.admin_password
|
admin_password = var.admin_password
|
||||||
|
port = var.port
|
||||||
|
|
||||||
# Wanted to have this be in the powershell template file, but Terraform
|
# Wanted to have this be in the powershell template file, but Terraform
|
||||||
# doesn't allow recursive calls to the templatefile function. Have to feed
|
# 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" {
|
resource "coder_app" "windows-rdp" {
|
||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
share = var.share
|
share = var.share
|
||||||
slug = "web-rdp"
|
slug = var.slug
|
||||||
display_name = "Web RDP"
|
display_name = "Web RDP"
|
||||||
url = "http://localhost:7171"
|
url = "http://localhost:${var.port}"
|
||||||
icon = "/icon/desktop.svg"
|
icon = "/icon/desktop.svg"
|
||||||
subdomain = true
|
subdomain = true
|
||||||
|
|
||||||
healthcheck {
|
healthcheck {
|
||||||
url = "http://localhost:7171"
|
url = "http://localhost:${var.port}"
|
||||||
interval = 5
|
interval = 5
|
||||||
threshold = 15
|
threshold = 15
|
||||||
}
|
}
|
||||||
@@ -80,7 +93,7 @@ resource "coder_app" "rdp-docs" {
|
|||||||
agent_id = var.agent_id
|
agent_id = var.agent_id
|
||||||
display_name = "Local RDP"
|
display_name = "Local RDP"
|
||||||
slug = "rdp-docs"
|
slug = "rdp-docs"
|
||||||
icon = "https://raw.githubusercontent.com/matifali/logos/main/windows.svg"
|
icon = "/icon/windows.svg"
|
||||||
url = "https://coder.com/docs/ides/remote-desktops#rdp-desktop"
|
url = "https://coder.com/docs/user-guides/workspace-access/remote-desktops#rdp-desktop"
|
||||||
external = true
|
external = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ Install-DGatewayPackage
|
|||||||
|
|
||||||
# Configure Devolutions Gateway
|
# Configure Devolutions Gateway
|
||||||
$Hostname = "localhost"
|
$Hostname = "localhost"
|
||||||
$HttpListener = New-DGatewayListener 'http://*:7171' 'http://*:7171'
|
$HttpListener = New-DGatewayListener "http://*:${port}" "http://*:${port}"
|
||||||
$WebApp = New-DGatewayWebAppConfig -Enabled $true -Authentication None
|
$WebApp = New-DGatewayWebAppConfig -Enabled $true -Authentication None
|
||||||
$ConfigParams = @{
|
$ConfigParams = @{
|
||||||
Hostname = $Hostname
|
Hostname = $Hostname
|
||||||
|
|||||||
Reference in New Issue
Block a user