diff --git a/cli/cliui/table.go b/cli/cliui/table.go
index b7ed7a2dff..b4b00e8759 100644
--- a/cli/cliui/table.go
+++ b/cli/cliui/table.go
@@ -188,32 +188,39 @@ func DisplayTable(out any, sort string, filterColumns []string) (string, error)
// returned. If the table tag is malformed, an error is returned.
//
// The returned name is transformed from "snake_case" to "normal text".
-func parseTableStructTag(field reflect.StructField) (name string, defaultSort, recursive bool, err error) {
+func parseTableStructTag(field reflect.StructField) (name string, defaultSort, recursive bool, skipParentName bool, err error) {
tags, err := structtag.Parse(string(field.Tag))
if err != nil {
- return "", false, false, xerrors.Errorf("parse struct field tag %q: %w", string(field.Tag), err)
+ return "", false, false, false, xerrors.Errorf("parse struct field tag %q: %w", string(field.Tag), err)
}
tag, err := tags.Get("table")
if err != nil || tag.Name == "-" {
// tags.Get only returns an error if the tag is not found.
- return "", false, false, nil
+ return "", false, false, false, nil
}
defaultSortOpt := false
recursiveOpt := false
+ skipParentNameOpt := false
for _, opt := range tag.Options {
switch opt {
case "default_sort":
defaultSortOpt = true
case "recursive":
recursiveOpt = true
+ case "recursive_inline":
+ // recursive_inline is a helper to make recursive tables look nicer.
+ // It skips prefixing the parent name to the child name. If you do this,
+ // make sure the child name is unique across all nested structs in the parent.
+ recursiveOpt = true
+ skipParentNameOpt = true
default:
- return "", false, false, xerrors.Errorf("unknown option %q in struct field tag", opt)
+ return "", false, false, false, xerrors.Errorf("unknown option %q in struct field tag", opt)
}
}
- return strings.ReplaceAll(tag.Name, "_", " "), defaultSortOpt, recursiveOpt, nil
+ return strings.ReplaceAll(tag.Name, "_", " "), defaultSortOpt, recursiveOpt, skipParentNameOpt, nil
}
func isStructOrStructPointer(t reflect.Type) bool {
@@ -235,7 +242,7 @@ func typeToTableHeaders(t reflect.Type) ([]string, string, error) {
defaultSortName := ""
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
- name, defaultSort, recursive, err := parseTableStructTag(field)
+ name, defaultSort, recursive, skip, err := parseTableStructTag(field)
if err != nil {
return nil, "", xerrors.Errorf("parse struct tags for field %q in type %q: %w", field.Name, t.String(), err)
}
@@ -260,7 +267,11 @@ func typeToTableHeaders(t reflect.Type) ([]string, string, error) {
return nil, "", xerrors.Errorf("get child field header names for field %q in type %q: %w", field.Name, fieldType.String(), err)
}
for _, childName := range childNames {
- headers = append(headers, fmt.Sprintf("%s %s", name, childName))
+ fullName := fmt.Sprintf("%s %s", name, childName)
+ if skip {
+ fullName = childName
+ }
+ headers = append(headers, fullName)
}
continue
}
@@ -296,7 +307,7 @@ func valueToTableMap(val reflect.Value) (map[string]any, error) {
for i := 0; i < val.NumField(); i++ {
field := val.Type().Field(i)
fieldVal := val.Field(i)
- name, _, recursive, err := parseTableStructTag(field)
+ name, _, recursive, skip, err := parseTableStructTag(field)
if err != nil {
return nil, xerrors.Errorf("parse struct tags for field %q in type %T: %w", field.Name, val, err)
}
@@ -318,7 +329,11 @@ func valueToTableMap(val reflect.Value) (map[string]any, error) {
return nil, xerrors.Errorf("get child field values for field %q in type %q: %w", field.Name, fieldType.String(), err)
}
for childName, childValue := range childMap {
- row[fmt.Sprintf("%s %s", name, childName)] = childValue
+ fullName := fmt.Sprintf("%s %s", name, childName)
+ if skip {
+ fullName = childName
+ }
+ row[fullName] = childValue
}
continue
}
diff --git a/cli/cliui/table_test.go b/cli/cliui/table_test.go
index 249e3f00c3..aca6f7bc82 100644
--- a/cli/cliui/table_test.go
+++ b/cli/cliui/table_test.go
@@ -49,6 +49,11 @@ type tableTest3 struct {
Sub tableTest2 `table:"inner,recursive,default_sort"`
}
+type tableTest4 struct {
+ Inline tableTest2 `table:"ignored,recursive_inline"`
+ SortField string `table:"sort_field,default_sort"`
+}
+
func Test_DisplayTable(t *testing.T) {
t.Parallel()
@@ -188,6 +193,31 @@ foo foo1 foo3 2022-08-02T15:49:10Z
compareTables(t, expected, out)
})
+ t.Run("Inline", func(t *testing.T) {
+ t.Parallel()
+
+ expected := `
+NAME AGE
+Alice 25
+ `
+
+ inlineIn := []tableTest4{
+ {
+ Inline: tableTest2{
+ Name: stringWrapper{
+ str: "Alice",
+ },
+ Age: 25,
+ NotIncluded: "IgnoreMe",
+ },
+ },
+ }
+ out, err := cliui.DisplayTable(inlineIn, "", []string{"name", "age"})
+ log.Println("rendered table:\n" + out)
+ require.NoError(t, err)
+ compareTables(t, expected, out)
+ })
+
// This test ensures that safeties against invalid use of `table` tags
// causes errors (even without data).
t.Run("Errors", func(t *testing.T) {
diff --git a/coderd/apidoc/docs.go b/coderd/apidoc/docs.go
index f415a0ee50..54c2f68cbc 100644
--- a/coderd/apidoc/docs.go
+++ b/coderd/apidoc/docs.go
@@ -1711,7 +1711,7 @@ const docTemplate = `{
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.RegionsResponse"
+ "$ref": "#/definitions/codersdk.RegionsResponse-codersdk_Region"
}
}
}
@@ -5109,7 +5109,7 @@ const docTemplate = `{
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/codersdk.WorkspaceProxy"
+ "$ref": "#/definitions/codersdk.RegionsResponse-codersdk_WorkspaceProxy"
}
}
}
@@ -8648,7 +8648,7 @@ const docTemplate = `{
}
}
},
- "codersdk.RegionsResponse": {
+ "codersdk.RegionsResponse-codersdk_Region": {
"type": "object",
"properties": {
"regions": {
@@ -8659,6 +8659,17 @@ const docTemplate = `{
}
}
},
+ "codersdk.RegionsResponse-codersdk_WorkspaceProxy": {
+ "type": "object",
+ "properties": {
+ "regions": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.WorkspaceProxy"
+ }
+ }
+ }
+ },
"codersdk.Replica": {
"type": "object",
"properties": {
@@ -10144,7 +10155,10 @@ const docTemplate = `{
"display_name": {
"type": "string"
},
- "icon": {
+ "healthy": {
+ "type": "boolean"
+ },
+ "icon_url": {
"type": "string"
},
"id": {
@@ -10154,6 +10168,10 @@ const docTemplate = `{
"name": {
"type": "string"
},
+ "path_app_url": {
+ "description": "PathAppURL is the URL to the base path for path apps. Optional\nunless wildcard_hostname is set.\nE.g. https://us.example.com",
+ "type": "string"
+ },
"status": {
"description": "Status is the latest status check of the proxy. This will be empty for deleted\nproxies. This value can be used to determine if a workspace proxy is healthy\nand ready to use.",
"allOf": [
@@ -10166,12 +10184,8 @@ const docTemplate = `{
"type": "string",
"format": "date-time"
},
- "url": {
- "description": "Full url including scheme of the proxy api url: https://us.example.com",
- "type": "string"
- },
"wildcard_hostname": {
- "description": "WildcardHostname with the wildcard for subdomain based app hosting: *.us.example.com",
+ "description": "WildcardHostname is the wildcard hostname for subdomain apps.\nE.g. *.us.example.com\nE.g. *--suffix.au.example.com\nOptional. Does not need to be on the same domain as PathAppURL.",
"type": "string"
}
}
diff --git a/coderd/apidoc/swagger.json b/coderd/apidoc/swagger.json
index e5e8d051cd..30d2b5bb31 100644
--- a/coderd/apidoc/swagger.json
+++ b/coderd/apidoc/swagger.json
@@ -1493,7 +1493,7 @@
"200": {
"description": "OK",
"schema": {
- "$ref": "#/definitions/codersdk.RegionsResponse"
+ "$ref": "#/definitions/codersdk.RegionsResponse-codersdk_Region"
}
}
}
@@ -4505,7 +4505,7 @@
"schema": {
"type": "array",
"items": {
- "$ref": "#/definitions/codersdk.WorkspaceProxy"
+ "$ref": "#/definitions/codersdk.RegionsResponse-codersdk_WorkspaceProxy"
}
}
}
@@ -7776,7 +7776,7 @@
}
}
},
- "codersdk.RegionsResponse": {
+ "codersdk.RegionsResponse-codersdk_Region": {
"type": "object",
"properties": {
"regions": {
@@ -7787,6 +7787,17 @@
}
}
},
+ "codersdk.RegionsResponse-codersdk_WorkspaceProxy": {
+ "type": "object",
+ "properties": {
+ "regions": {
+ "type": "array",
+ "items": {
+ "$ref": "#/definitions/codersdk.WorkspaceProxy"
+ }
+ }
+ }
+ },
"codersdk.Replica": {
"type": "object",
"properties": {
@@ -9182,7 +9193,10 @@
"display_name": {
"type": "string"
},
- "icon": {
+ "healthy": {
+ "type": "boolean"
+ },
+ "icon_url": {
"type": "string"
},
"id": {
@@ -9192,6 +9206,10 @@
"name": {
"type": "string"
},
+ "path_app_url": {
+ "description": "PathAppURL is the URL to the base path for path apps. Optional\nunless wildcard_hostname is set.\nE.g. https://us.example.com",
+ "type": "string"
+ },
"status": {
"description": "Status is the latest status check of the proxy. This will be empty for deleted\nproxies. This value can be used to determine if a workspace proxy is healthy\nand ready to use.",
"allOf": [
@@ -9204,12 +9222,8 @@
"type": "string",
"format": "date-time"
},
- "url": {
- "description": "Full url including scheme of the proxy api url: https://us.example.com",
- "type": "string"
- },
"wildcard_hostname": {
- "description": "WildcardHostname with the wildcard for subdomain based app hosting: *.us.example.com",
+ "description": "WildcardHostname is the wildcard hostname for subdomain apps.\nE.g. *.us.example.com\nE.g. *--suffix.au.example.com\nOptional. Does not need to be on the same domain as PathAppURL.",
"type": "string"
}
}
diff --git a/coderd/workspaceproxies.go b/coderd/workspaceproxies.go
index e7db84898b..f861233507 100644
--- a/coderd/workspaceproxies.go
+++ b/coderd/workspaceproxies.go
@@ -72,7 +72,7 @@ func (api *API) PrimaryWorkspaceProxy(ctx context.Context) (database.WorkspacePr
// @Security CoderSessionToken
// @Produce json
// @Tags WorkspaceProxies
-// @Success 200 {object} codersdk.RegionsResponse
+// @Success 200 {object} codersdk.RegionsResponse[codersdk.Region]
// @Router /regions [get]
func (api *API) regions(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
@@ -87,7 +87,7 @@ func (api *API) regions(rw http.ResponseWriter, r *http.Request) {
return
}
- httpapi.Write(ctx, rw, http.StatusOK, codersdk.RegionsResponse{
+ httpapi.Write(ctx, rw, http.StatusOK, codersdk.RegionsResponse[codersdk.Region]{
Regions: []codersdk.Region{region},
})
}
diff --git a/codersdk/workspaceproxy.go b/codersdk/workspaceproxy.go
index 1c10e9981b..2ce15b043e 100644
--- a/codersdk/workspaceproxy.go
+++ b/codersdk/workspaceproxy.go
@@ -46,22 +46,17 @@ type ProxyHealthReport struct {
}
type WorkspaceProxy struct {
- ID uuid.UUID `json:"id" format:"uuid" table:"id"`
- Name string `json:"name" table:"name,default_sort"`
- DisplayName string `json:"display_name" table:"display_name"`
- Icon string `json:"icon" table:"icon"`
- // Full url including scheme of the proxy api url: https://us.example.com
- URL string `json:"url" table:"url"`
- // WildcardHostname with the wildcard for subdomain based app hosting: *.us.example.com
- WildcardHostname string `json:"wildcard_hostname" table:"wildcard_hostname"`
- CreatedAt time.Time `json:"created_at" format:"date-time" table:"created_at"`
- UpdatedAt time.Time `json:"updated_at" format:"date-time" table:"updated_at"`
- Deleted bool `json:"deleted" table:"deleted"`
+ // Extends Region with extra information
+ Region `table:"region,recursive_inline"`
// Status is the latest status check of the proxy. This will be empty for deleted
// proxies. This value can be used to determine if a workspace proxy is healthy
// and ready to use.
Status WorkspaceProxyStatus `json:"status,omitempty" table:"proxy,recursive"`
+
+ CreatedAt time.Time `json:"created_at" format:"date-time" table:"created_at,default_sort"`
+ UpdatedAt time.Time `json:"updated_at" format:"date-time" table:"updated_at"`
+ Deleted bool `json:"deleted" table:"deleted"`
}
type CreateWorkspaceProxyRequest struct {
@@ -93,21 +88,21 @@ func (c *Client) CreateWorkspaceProxy(ctx context.Context, req CreateWorkspacePr
return resp, json.NewDecoder(res.Body).Decode(&resp)
}
-func (c *Client) WorkspaceProxies(ctx context.Context) ([]WorkspaceProxy, error) {
+func (c *Client) WorkspaceProxies(ctx context.Context) (RegionsResponse[WorkspaceProxy], error) {
res, err := c.Request(ctx, http.MethodGet,
"/api/v2/workspaceproxies",
nil,
)
if err != nil {
- return nil, xerrors.Errorf("make request: %w", err)
+ return RegionsResponse[WorkspaceProxy]{}, xerrors.Errorf("make request: %w", err)
}
defer res.Body.Close()
if res.StatusCode != http.StatusOK {
- return nil, ReadBodyAsError(res)
+ return RegionsResponse[WorkspaceProxy]{}, ReadBodyAsError(res)
}
- var proxies []WorkspaceProxy
+ var proxies RegionsResponse[WorkspaceProxy]
return proxies, json.NewDecoder(res.Body).Decode(&proxies)
}
@@ -179,27 +174,31 @@ func (c *Client) WorkspaceProxyByID(ctx context.Context, id uuid.UUID) (Workspac
return c.WorkspaceProxyByName(ctx, id.String())
}
-type RegionsResponse struct {
- Regions []Region `json:"regions"`
+type RegionTypes interface {
+ Region | WorkspaceProxy
+}
+
+type RegionsResponse[R RegionTypes] struct {
+ Regions []R `json:"regions"`
}
type Region struct {
- ID uuid.UUID `json:"id" format:"uuid"`
- Name string `json:"name"`
- DisplayName string `json:"display_name"`
- IconURL string `json:"icon_url"`
- Healthy bool `json:"healthy"`
+ ID uuid.UUID `json:"id" format:"uuid" table:"id"`
+ Name string `json:"name" table:"name,default_sort"`
+ DisplayName string `json:"display_name" table:"display_name"`
+ IconURL string `json:"icon_url" table:"icon_url"`
+ Healthy bool `json:"healthy" table:"healthy"`
// PathAppURL is the URL to the base path for path apps. Optional
// unless wildcard_hostname is set.
// E.g. https://us.example.com
- PathAppURL string `json:"path_app_url"`
+ PathAppURL string `json:"path_app_url" table:"url"`
// WildcardHostname is the wildcard hostname for subdomain apps.
// E.g. *.us.example.com
// E.g. *--suffix.au.example.com
// Optional. Does not need to be on the same domain as PathAppURL.
- WildcardHostname string `json:"wildcard_hostname"`
+ WildcardHostname string `json:"wildcard_hostname" table:"wildcard_hostname"`
}
func (c *Client) Regions(ctx context.Context) ([]Region, error) {
@@ -216,6 +215,6 @@ func (c *Client) Regions(ctx context.Context) ([]Region, error) {
return nil, ReadBodyAsError(res)
}
- var regions RegionsResponse
+ var regions RegionsResponse[Region]
return regions.Regions, json.NewDecoder(res.Body).Decode(®ions)
}
diff --git a/docs/api/enterprise.md b/docs/api/enterprise.md
index c5ecaea1c2..844cef85f9 100644
--- a/docs/api/enterprise.md
+++ b/docs/api/enterprise.md
@@ -1180,55 +1180,62 @@ curl -X GET http://coder-server:8080/api/v2/workspaceproxies \
```json
[
{
- "created_at": "2019-08-24T14:15:22Z",
- "deleted": true,
- "display_name": "string",
- "icon": "string",
- "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
- "name": "string",
- "status": {
- "checked_at": "2019-08-24T14:15:22Z",
- "report": {
- "errors": ["string"],
- "warnings": ["string"]
- },
- "status": "ok"
- },
- "updated_at": "2019-08-24T14:15:22Z",
- "url": "string",
- "wildcard_hostname": "string"
+ "regions": [
+ {
+ "created_at": "2019-08-24T14:15:22Z",
+ "deleted": true,
+ "display_name": "string",
+ "healthy": true,
+ "icon_url": "string",
+ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+ "name": "string",
+ "path_app_url": "string",
+ "status": {
+ "checked_at": "2019-08-24T14:15:22Z",
+ "report": {
+ "errors": ["string"],
+ "warnings": ["string"]
+ },
+ "status": "ok"
+ },
+ "updated_at": "2019-08-24T14:15:22Z",
+ "wildcard_hostname": "string"
+ }
+ ]
}
]
```
### Responses
-| Status | Meaning | Description | Schema |
-| ------ | ------------------------------------------------------- | ----------- | --------------------------------------------------------------------- |
-| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.WorkspaceProxy](schemas.md#codersdkworkspaceproxy) |
+| Status | Meaning | Description | Schema |
+| ------ | ------------------------------------------------------- | ----------- | ----------------------------------------------------------------------------------------------------------------------- |
+| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.RegionsResponse-codersdk_WorkspaceProxy](schemas.md#codersdkregionsresponse-codersdk_workspaceproxy) |
Response Schema
Status Code **200**
-| Name | Type | Required | Restrictions | Description |
-| --------------------- | ------------------------------------------------------------------------ | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `[array item]` | array | false | | |
-| `» created_at` | string(date-time) | false | | |
-| `» deleted` | boolean | false | | |
-| `» display_name` | string | false | | |
-| `» icon` | string | false | | |
-| `» id` | string(uuid) | false | | |
-| `» name` | string | false | | |
-| `» status` | [codersdk.WorkspaceProxyStatus](schemas.md#codersdkworkspaceproxystatus) | false | | Status is the latest status check of the proxy. This will be empty for deleted proxies. This value can be used to determine if a workspace proxy is healthy and ready to use. |
-| `»» checked_at` | string(date-time) | false | | |
-| `»» report` | [codersdk.ProxyHealthReport](schemas.md#codersdkproxyhealthreport) | false | | Report provides more information about the health of the workspace proxy. |
-| `»»» errors` | array | false | | Errors are problems that prevent the workspace proxy from being healthy |
-| `»»» warnings` | array | false | | Warnings do not prevent the workspace proxy from being healthy, but should be addressed. |
-| `»» status` | [codersdk.ProxyHealthStatus](schemas.md#codersdkproxyhealthstatus) | false | | |
-| `» updated_at` | string(date-time) | false | | |
-| `» url` | string | false | | Full URL including scheme of the proxy api url: https://us.example.com |
-| `» wildcard_hostname` | string | false | | Wildcard hostname with the wildcard for subdomain based app hosting: \*.us.example.com |
+| Name | Type | Required | Restrictions | Description |
+| ---------------------- | ------------------------------------------------------------------------ | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `[array item]` | array | false | | |
+| `» regions` | array | false | | |
+| `»» created_at` | string(date-time) | false | | |
+| `»» deleted` | boolean | false | | |
+| `»» display_name` | string | false | | |
+| `»» healthy` | boolean | false | | |
+| `»» icon_url` | string | false | | |
+| `»» id` | string(uuid) | false | | |
+| `»» name` | string | false | | |
+| `»» path_app_url` | string | false | | »path app URL is the URL to the base path for path apps. Optional unless wildcard_hostname is set. E.g. https://us.example.com |
+| `»» status` | [codersdk.WorkspaceProxyStatus](schemas.md#codersdkworkspaceproxystatus) | false | | Status is the latest status check of the proxy. This will be empty for deleted proxies. This value can be used to determine if a workspace proxy is healthy and ready to use. |
+| `»»» checked_at` | string(date-time) | false | | |
+| `»»» report` | [codersdk.ProxyHealthReport](schemas.md#codersdkproxyhealthreport) | false | | Report provides more information about the health of the workspace proxy. |
+| `»»»» errors` | array | false | | Errors are problems that prevent the workspace proxy from being healthy |
+| `»»»» warnings` | array | false | | Warnings do not prevent the workspace proxy from being healthy, but should be addressed. |
+| `»»» status` | [codersdk.ProxyHealthStatus](schemas.md#codersdkproxyhealthstatus) | false | | |
+| `»» updated_at` | string(date-time) | false | | |
+| `»» wildcard_hostname` | string | false | | »wildcard hostname is the wildcard hostname for subdomain apps. E.g. _.us.example.com E.g. _--suffix.au.example.com Optional. Does not need to be on the same domain as PathAppURL. |
#### Enumerated Values
@@ -1280,9 +1287,11 @@ curl -X POST http://coder-server:8080/api/v2/workspaceproxies \
"created_at": "2019-08-24T14:15:22Z",
"deleted": true,
"display_name": "string",
- "icon": "string",
+ "healthy": true,
+ "icon_url": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"name": "string",
+ "path_app_url": "string",
"status": {
"checked_at": "2019-08-24T14:15:22Z",
"report": {
@@ -1292,7 +1301,6 @@ curl -X POST http://coder-server:8080/api/v2/workspaceproxies \
"status": "ok"
},
"updated_at": "2019-08-24T14:15:22Z",
- "url": "string",
"wildcard_hostname": "string"
}
```
@@ -1333,9 +1341,11 @@ curl -X GET http://coder-server:8080/api/v2/workspaceproxies/{workspaceproxy} \
"created_at": "2019-08-24T14:15:22Z",
"deleted": true,
"display_name": "string",
- "icon": "string",
+ "healthy": true,
+ "icon_url": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"name": "string",
+ "path_app_url": "string",
"status": {
"checked_at": "2019-08-24T14:15:22Z",
"report": {
@@ -1345,7 +1355,6 @@ curl -X GET http://coder-server:8080/api/v2/workspaceproxies/{workspaceproxy} \
"status": "ok"
},
"updated_at": "2019-08-24T14:15:22Z",
- "url": "string",
"wildcard_hostname": "string"
}
```
@@ -1444,9 +1453,11 @@ curl -X PATCH http://coder-server:8080/api/v2/workspaceproxies/{workspaceproxy}
"created_at": "2019-08-24T14:15:22Z",
"deleted": true,
"display_name": "string",
- "icon": "string",
+ "healthy": true,
+ "icon_url": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"name": "string",
+ "path_app_url": "string",
"status": {
"checked_at": "2019-08-24T14:15:22Z",
"report": {
@@ -1456,7 +1467,6 @@ curl -X PATCH http://coder-server:8080/api/v2/workspaceproxies/{workspaceproxy}
"status": "ok"
},
"updated_at": "2019-08-24T14:15:22Z",
- "url": "string",
"wildcard_hostname": "string"
}
```
diff --git a/docs/api/schemas.md b/docs/api/schemas.md
index 53c76eaaed..12a988ee11 100644
--- a/docs/api/schemas.md
+++ b/docs/api/schemas.md
@@ -3606,7 +3606,7 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| `path_app_url` | string | false | | Path app URL is the URL to the base path for path apps. Optional unless wildcard_hostname is set. E.g. https://us.example.com |
| `wildcard_hostname` | string | false | | Wildcard hostname is the wildcard hostname for subdomain apps. E.g. _.us.example.com E.g. _--suffix.au.example.com Optional. Does not need to be on the same domain as PathAppURL. |
-## codersdk.RegionsResponse
+## codersdk.RegionsResponse-codersdk_Region
```json
{
@@ -3630,6 +3630,41 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
| --------- | ------------------------------------------- | -------- | ------------ | ----------- |
| `regions` | array of [codersdk.Region](#codersdkregion) | false | | |
+## codersdk.RegionsResponse-codersdk_WorkspaceProxy
+
+```json
+{
+ "regions": [
+ {
+ "created_at": "2019-08-24T14:15:22Z",
+ "deleted": true,
+ "display_name": "string",
+ "healthy": true,
+ "icon_url": "string",
+ "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
+ "name": "string",
+ "path_app_url": "string",
+ "status": {
+ "checked_at": "2019-08-24T14:15:22Z",
+ "report": {
+ "errors": ["string"],
+ "warnings": ["string"]
+ },
+ "status": "ok"
+ },
+ "updated_at": "2019-08-24T14:15:22Z",
+ "wildcard_hostname": "string"
+ }
+ ]
+}
+```
+
+### Properties
+
+| Name | Type | Required | Restrictions | Description |
+| --------- | ----------------------------------------------------------- | -------- | ------------ | ----------- |
+| `regions` | array of [codersdk.WorkspaceProxy](#codersdkworkspaceproxy) | false | | |
+
## codersdk.Replica
```json
@@ -5448,9 +5483,11 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"created_at": "2019-08-24T14:15:22Z",
"deleted": true,
"display_name": "string",
- "icon": "string",
+ "healthy": true,
+ "icon_url": "string",
"id": "497f6eca-6276-4993-bfeb-53cbbbba6f08",
"name": "string",
+ "path_app_url": "string",
"status": {
"checked_at": "2019-08-24T14:15:22Z",
"report": {
@@ -5460,25 +5497,25 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
"status": "ok"
},
"updated_at": "2019-08-24T14:15:22Z",
- "url": "string",
"wildcard_hostname": "string"
}
```
### Properties
-| Name | Type | Required | Restrictions | Description |
-| ------------------- | -------------------------------------------------------------- | -------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `created_at` | string | false | | |
-| `deleted` | boolean | false | | |
-| `display_name` | string | false | | |
-| `icon` | string | false | | |
-| `id` | string | false | | |
-| `name` | string | false | | |
-| `status` | [codersdk.WorkspaceProxyStatus](#codersdkworkspaceproxystatus) | false | | Status is the latest status check of the proxy. This will be empty for deleted proxies. This value can be used to determine if a workspace proxy is healthy and ready to use. |
-| `updated_at` | string | false | | |
-| `url` | string | false | | Full URL including scheme of the proxy api url: https://us.example.com |
-| `wildcard_hostname` | string | false | | Wildcard hostname with the wildcard for subdomain based app hosting: \*.us.example.com |
+| Name | Type | Required | Restrictions | Description |
+| ------------------- | -------------------------------------------------------------- | -------- | ------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `created_at` | string | false | | |
+| `deleted` | boolean | false | | |
+| `display_name` | string | false | | |
+| `healthy` | boolean | false | | |
+| `icon_url` | string | false | | |
+| `id` | string | false | | |
+| `name` | string | false | | |
+| `path_app_url` | string | false | | Path app URL is the URL to the base path for path apps. Optional unless wildcard_hostname is set. E.g. https://us.example.com |
+| `status` | [codersdk.WorkspaceProxyStatus](#codersdkworkspaceproxystatus) | false | | Status is the latest status check of the proxy. This will be empty for deleted proxies. This value can be used to determine if a workspace proxy is healthy and ready to use. |
+| `updated_at` | string | false | | |
+| `wildcard_hostname` | string | false | | Wildcard hostname is the wildcard hostname for subdomain apps. E.g. _.us.example.com E.g. _--suffix.au.example.com Optional. Does not need to be on the same domain as PathAppURL. |
## codersdk.WorkspaceProxyStatus
diff --git a/docs/api/workspaceproxies.md b/docs/api/workspaceproxies.md
index 5cd961c2a5..2113d53d16 100644
--- a/docs/api/workspaceproxies.md
+++ b/docs/api/workspaceproxies.md
@@ -35,8 +35,8 @@ curl -X GET http://coder-server:8080/api/v2/regions \
### Responses
-| Status | Meaning | Description | Schema |
-| ------ | ------------------------------------------------------- | ----------- | -------------------------------------------------------------- |
-| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.RegionsResponse](schemas.md#codersdkregionsresponse) |
+| Status | Meaning | Description | Schema |
+| ------ | ------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------- |
+| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | [codersdk.RegionsResponse-codersdk_Region](schemas.md#codersdkregionsresponse-codersdk_region) |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
diff --git a/enterprise/cli/workspaceproxy.go b/enterprise/cli/workspaceproxy.go
index f53eb242af..15e2727b5a 100644
--- a/enterprise/cli/workspaceproxy.go
+++ b/enterprise/cli/workspaceproxy.go
@@ -63,7 +63,7 @@ func (r *RootCmd) regenerateProxyToken() *clibase.Cmd {
ID: proxy.ID,
Name: proxy.Name,
DisplayName: proxy.DisplayName,
- Icon: proxy.Icon,
+ Icon: proxy.IconURL,
RegenerateToken: true,
})
if err != nil {
@@ -138,7 +138,7 @@ func (r *RootCmd) patchProxy() *clibase.Cmd {
displayName = proxy.DisplayName
}
if proxyIcon == "" {
- proxyIcon = proxy.Icon
+ proxyIcon = proxy.IconURL
}
updated, err := client.PatchWorkspaceProxy(ctx, codersdk.PatchWorkspaceProxy{
@@ -322,7 +322,7 @@ func (r *RootCmd) listProxies() *clibase.Cmd {
sep := ""
for i, proxy := range resp {
_, _ = str.WriteString(sep)
- _, _ = str.WriteString(fmt.Sprintf("%d: %s %s %s", i, proxy.Name, proxy.URL, proxy.Status.Status))
+ _, _ = str.WriteString(fmt.Sprintf("%d: %s %s %s", i, proxy.Name, proxy.PathAppURL, proxy.Status.Status))
for _, errMsg := range proxy.Status.Report.Errors {
_, _ = str.WriteString(color.RedString("\n\tErr: %s", errMsg))
}
@@ -351,7 +351,7 @@ func (r *RootCmd) listProxies() *clibase.Cmd {
return xerrors.Errorf("list workspace proxies: %w", err)
}
- output, err := formatter.Format(ctx, proxies)
+ output, err := formatter.Format(ctx, proxies.Regions)
if err != nil {
return err
}
diff --git a/enterprise/cli/workspaceproxy_test.go b/enterprise/cli/workspaceproxy_test.go
index 357c609125..11ef569322 100644
--- a/enterprise/cli/workspaceproxy_test.go
+++ b/enterprise/cli/workspaceproxy_test.go
@@ -83,9 +83,9 @@ func Test_ProxyCRUD(t *testing.T) {
proxies, err := client.WorkspaceProxies(ctx)
require.NoError(t, err, "failed to get workspace proxies")
// Include primary
- require.Len(t, proxies, 2, "expected 1 proxy")
+ require.Len(t, proxies.Regions, 2, "expected 1 proxy")
found := false
- for _, proxy := range proxies {
+ for _, proxy := range proxies.Regions {
if proxy.Name == expectedName {
found = true
}
@@ -137,6 +137,6 @@ func Test_ProxyCRUD(t *testing.T) {
proxies, err := client.WorkspaceProxies(ctx)
require.NoError(t, err, "failed to get workspace proxies")
- require.Len(t, proxies, 1, "expected only primary proxy")
+ require.Len(t, proxies.Regions, 1, "expected only primary proxy")
})
}
diff --git a/enterprise/coderd/coderd.go b/enterprise/coderd/coderd.go
index 1df66af44a..1a4a6d9e7d 100644
--- a/enterprise/coderd/coderd.go
+++ b/enterprise/coderd/coderd.go
@@ -18,6 +18,7 @@ import (
"cdr.dev/slog"
"github.com/coder/coder/coderd"
agplaudit "github.com/coder/coder/coderd/audit"
+ "github.com/coder/coder/coderd/database/dbauthz"
"github.com/coder/coder/coderd/httpapi"
"github.com/coder/coder/coderd/httpmw"
"github.com/coder/coder/coderd/rbac"
@@ -67,7 +68,15 @@ func New(ctx context.Context, options *Options) (_ *API, err error) {
api.AGPL.Options.SetUserGroups = api.setUserGroups
api.AGPL.SiteHandler.AppearanceFetcher = api.fetchAppearanceConfig
- api.AGPL.SiteHandler.RegionsFetcher = api.fetchRegions
+ api.AGPL.SiteHandler.RegionsFetcher = func(ctx context.Context) (any, error) {
+ // If the user can read the workspace proxy resource, return that.
+ // If not, always default to the regions.
+ actor, ok := dbauthz.ActorFromContext(ctx)
+ if ok && api.Authorizer.Authorize(ctx, actor, rbac.ActionRead, rbac.ResourceWorkspaceProxy) == nil {
+ return api.fetchWorkspaceProxies(ctx)
+ }
+ return api.fetchRegions(ctx)
+ }
oauthConfigs := &httpmw.OAuth2Configs{
Github: options.GithubOAuth2Config,
diff --git a/enterprise/coderd/proxyhealth/proxyhealth.go b/enterprise/coderd/proxyhealth/proxyhealth.go
index be7368e9c6..d95a44d5a0 100644
--- a/enterprise/coderd/proxyhealth/proxyhealth.go
+++ b/enterprise/coderd/proxyhealth/proxyhealth.go
@@ -171,6 +171,11 @@ func (p *ProxyHealth) ForceUpdate(ctx context.Context) error {
// HealthStatus returns the current health status of all proxies stored in the
// cache.
func (p *ProxyHealth) HealthStatus() map[uuid.UUID]ProxyStatus {
+ if p == nil {
+ // This can happen because workspace proxies are still an experiment.
+ // For the /regions endpoint, this will be nil in those cases.
+ return map[uuid.UUID]ProxyStatus{}
+ }
ptr := p.cache.Load()
if ptr == nil {
return map[uuid.UUID]ProxyStatus{}
diff --git a/enterprise/coderd/proxyhealth/proxyhealth_test.go b/enterprise/coderd/proxyhealth/proxyhealth_test.go
index 5fb9614385..10d8d69dba 100644
--- a/enterprise/coderd/proxyhealth/proxyhealth_test.go
+++ b/enterprise/coderd/proxyhealth/proxyhealth_test.go
@@ -36,6 +36,14 @@ func insertProxy(t *testing.T, db database.Store, url string) database.Workspace
return proxy
}
+// Test the nil guard for experiment off cases.
+func TestProxyHealth_Nil(t *testing.T) {
+ t.Parallel()
+ var ph *proxyhealth.ProxyHealth
+
+ require.NotNil(t, ph.HealthStatus())
+}
+
func TestProxyHealth_Unregistered(t *testing.T) {
t.Parallel()
db := dbfake.New()
diff --git a/enterprise/coderd/workspaceproxy.go b/enterprise/coderd/workspaceproxy.go
index 296c6482be..8de4a95f02 100644
--- a/enterprise/coderd/workspaceproxy.go
+++ b/enterprise/coderd/workspaceproxy.go
@@ -48,47 +48,27 @@ func (api *API) regions(rw http.ResponseWriter, r *http.Request) {
httpapi.Write(r.Context(), rw, http.StatusOK, regions)
}
-func (api *API) fetchRegions(ctx context.Context) (codersdk.RegionsResponse, error) {
+func (api *API) fetchRegions(ctx context.Context) (codersdk.RegionsResponse[codersdk.Region], error) {
//nolint:gocritic // this intentionally requests resources that users
// cannot usually access in order to give them a full list of available
- // regions.
+ // regions. Regions are just a data subset of proxies.
ctx = dbauthz.AsSystemRestricted(ctx)
-
- primaryRegion, err := api.AGPL.PrimaryRegion(ctx)
+ proxies, err := api.fetchWorkspaceProxies(ctx)
if err != nil {
- return codersdk.RegionsResponse{}, err
- }
- regions := []codersdk.Region{primaryRegion}
-
- proxies, err := api.Database.GetWorkspaceProxies(ctx)
- if err != nil {
- return codersdk.RegionsResponse{}, err
+ return codersdk.RegionsResponse[codersdk.Region]{}, err
}
- // Only add additional regions if the proxy health is enabled.
- // If it is nil, it is because the moons feature flag is not on.
- // By default, we still want to return the primary region.
- if api.ProxyHealth != nil {
- proxyHealth := api.ProxyHealth.HealthStatus()
- for _, proxy := range proxies {
- if proxy.Deleted {
- continue
- }
-
- health := proxyHealth[proxy.ID]
- regions = append(regions, codersdk.Region{
- ID: proxy.ID,
- Name: proxy.Name,
- DisplayName: proxy.DisplayName,
- IconURL: proxy.Icon,
- Healthy: health.Status == proxyhealth.Healthy,
- PathAppURL: proxy.Url,
- WildcardHostname: proxy.WildcardHostname,
- })
+ regions := make([]codersdk.Region, 0, len(proxies.Regions))
+ for i := range proxies.Regions {
+ // Ignore deleted proxies.
+ if proxies.Regions[i].Deleted {
+ continue
}
+ // Append the inner region data.
+ regions = append(regions, proxies.Regions[i].Region)
}
- return codersdk.RegionsResponse{
+ return codersdk.RegionsResponse[codersdk.Region]{
Regions: regions,
}, nil
}
@@ -415,27 +395,41 @@ func validateProxyURL(u string) error {
// @Security CoderSessionToken
// @Produce json
// @Tags Enterprise
-// @Success 200 {array} codersdk.WorkspaceProxy
+// @Success 200 {array} codersdk.RegionsResponse[codersdk.WorkspaceProxy]
// @Router /workspaceproxies [get]
func (api *API) workspaceProxies(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
-
- proxies, err := api.Database.GetWorkspaceProxies(ctx)
- if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
+ proxies, err := api.fetchWorkspaceProxies(r.Context())
+ if err != nil {
+ if dbauthz.IsNotAuthorizedError(err) {
+ httpapi.Write(ctx, rw, http.StatusForbidden, codersdk.Response{
+ Message: "You are not authorized to use this endpoint.",
+ })
+ return
+ }
httpapi.InternalServerError(rw, err)
return
}
+ httpapi.Write(ctx, rw, http.StatusOK, proxies)
+}
+
+func (api *API) fetchWorkspaceProxies(ctx context.Context) (codersdk.RegionsResponse[codersdk.WorkspaceProxy], error) {
+ proxies, err := api.Database.GetWorkspaceProxies(ctx)
+ if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
+ return codersdk.RegionsResponse[codersdk.WorkspaceProxy]{}, err
+ }
// Add the primary as well
primaryProxy, err := api.AGPL.PrimaryWorkspaceProxy(ctx)
if err != nil && !xerrors.Is(err, sql.ErrNoRows) {
- httpapi.InternalServerError(rw, err)
- return
+ return codersdk.RegionsResponse[codersdk.WorkspaceProxy]{}, err
}
proxies = append([]database.WorkspaceProxy{primaryProxy}, proxies...)
statues := api.ProxyHealth.HealthStatus()
- httpapi.Write(ctx, rw, http.StatusOK, convertProxies(proxies, statues))
+ return codersdk.RegionsResponse[codersdk.WorkspaceProxy]{
+ Regions: convertProxies(proxies, statues),
+ }, nil
}
// @Summary Issue signed workspace app token
@@ -710,6 +704,18 @@ func convertProxies(p []database.WorkspaceProxy, statuses map[uuid.UUID]proxyhea
return resp
}
+func convertRegion(proxy database.WorkspaceProxy, status proxyhealth.ProxyStatus) codersdk.Region {
+ return codersdk.Region{
+ ID: proxy.ID,
+ Name: proxy.Name,
+ DisplayName: proxy.DisplayName,
+ IconURL: proxy.Icon,
+ Healthy: status.Status == proxyhealth.Healthy,
+ PathAppURL: proxy.Url,
+ WildcardHostname: proxy.WildcardHostname,
+ }
+}
+
func convertProxy(p database.WorkspaceProxy, status proxyhealth.ProxyStatus) codersdk.WorkspaceProxy {
if p.IsPrimary() {
// Primary is always healthy since the primary serves the api that this
@@ -727,15 +733,10 @@ func convertProxy(p database.WorkspaceProxy, status proxyhealth.ProxyStatus) cod
status.Status = proxyhealth.Unknown
}
return codersdk.WorkspaceProxy{
- ID: p.ID,
- Name: p.Name,
- DisplayName: p.DisplayName,
- Icon: p.Icon,
- URL: p.Url,
- WildcardHostname: p.WildcardHostname,
- CreatedAt: p.CreatedAt,
- UpdatedAt: p.UpdatedAt,
- Deleted: p.Deleted,
+ Region: convertRegion(p, status),
+ CreatedAt: p.CreatedAt,
+ UpdatedAt: p.UpdatedAt,
+ Deleted: p.Deleted,
Status: codersdk.WorkspaceProxyStatus{
Status: codersdk.ProxyHealthStatus(status.Status),
Report: status.Report,
diff --git a/enterprise/coderd/workspaceproxy_test.go b/enterprise/coderd/workspaceproxy_test.go
index f96b1dd2bf..9793269216 100644
--- a/enterprise/coderd/workspaceproxy_test.go
+++ b/enterprise/coderd/workspaceproxy_test.go
@@ -288,7 +288,7 @@ func TestWorkspaceProxyCRUD(t *testing.T) {
require.NoError(t, err)
require.Equal(t, expName, found.Name, "name")
require.Equal(t, expDisplayName, found.DisplayName, "display name")
- require.Equal(t, expIcon, found.Icon, "icon")
+ require.Equal(t, expIcon, found.IconURL, "icon")
})
t.Run("Delete", func(t *testing.T) {
@@ -323,7 +323,7 @@ func TestWorkspaceProxyCRUD(t *testing.T) {
proxies, err := client.WorkspaceProxies(ctx)
require.NoError(t, err)
// Default proxy is always there
- require.Len(t, proxies, 1)
+ require.Len(t, proxies.Regions, 1)
})
}
diff --git a/scripts/apitypings/main.go b/scripts/apitypings/main.go
index e39d15e171..3919d24a00 100644
--- a/scripts/apitypings/main.go
+++ b/scripts/apitypings/main.go
@@ -584,7 +584,7 @@ type TypescriptType struct {
// Example: 'C = comparable'.
GenericTypes map[string]string
// GenericValue is the value using the Generic name, rather than the constraint.
- // This is only usedful if you can use the generic syntax. Things like maps
+ // This is only useful if you can use the generic syntax. Things like maps
// don't currently support this, and will use the ValueType instead.
// Example:
// Given the Golang
@@ -667,9 +667,14 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
}
aboveTypeLine = aboveTypeLine + valueType.AboveTypeLine
+ mergeGens := keyType.GenericTypes
+ for k, v := range valueType.GenericTypes {
+ mergeGens[k] = v
+ }
return TypescriptType{
ValueType: fmt.Sprintf("Record<%s, %s>", keyType.ValueType, valueType.ValueType),
AboveTypeLine: aboveTypeLine,
+ GenericTypes: mergeGens,
}, nil
case *types.Slice, *types.Array:
// Slice/Arrays are pretty much the same.
@@ -691,7 +696,16 @@ func (g *Generator) typescriptType(ty types.Type) (TypescriptType, error) {
if err != nil {
return TypescriptType{}, xerrors.Errorf("array: %w", err)
}
- return TypescriptType{ValueType: underlying.ValueType + "[]", AboveTypeLine: underlying.AboveTypeLine}, nil
+ genValue := ""
+ if underlying.GenericValue != "" {
+ genValue = underlying.GenericValue + "[]"
+ }
+ return TypescriptType{
+ ValueType: underlying.ValueType + "[]",
+ GenericValue: genValue,
+ AboveTypeLine: underlying.AboveTypeLine,
+ GenericTypes: underlying.GenericTypes,
+ }, nil
}
case *types.Named:
n := ty
diff --git a/scripts/apitypings/testdata/genericmap/genericmap.go b/scripts/apitypings/testdata/genericmap/genericmap.go
new file mode 100644
index 0000000000..ab9163b587
--- /dev/null
+++ b/scripts/apitypings/testdata/genericmap/genericmap.go
@@ -0,0 +1,28 @@
+package genericmap
+
+type Foo struct {
+ Bar string `json:"bar"`
+}
+
+type Buzz struct {
+ Foo `json:"foo"`
+ Bazz string `json:"bazz"`
+}
+
+type Custom interface {
+ Foo | Buzz
+}
+
+type FooBuzz[R Custom] struct {
+ Something []R `json:"something"`
+}
+
+// Not yet supported
+//type FooBuzzMap[R Custom] struct {
+// Something map[string]R `json:"something"`
+//}
+
+// Not yet supported
+//type FooBuzzAnonymousUnion[R Foo | Buzz] struct {
+// Something []R `json:"something"`
+//}
diff --git a/scripts/apitypings/testdata/genericmap/genericmap.ts b/scripts/apitypings/testdata/genericmap/genericmap.ts
new file mode 100644
index 0000000000..5489d03970
--- /dev/null
+++ b/scripts/apitypings/testdata/genericmap/genericmap.ts
@@ -0,0 +1,20 @@
+// Code generated by 'make site/src/api/typesGenerated.ts'. DO NOT EDIT.
+
+// From codersdk/genericmap.go
+export interface Buzz {
+ readonly foo: Foo
+ readonly bazz: string
+}
+
+// From codersdk/genericmap.go
+export interface Foo {
+ readonly bar: string
+}
+
+// From codersdk/genericmap.go
+export interface FooBuzz {
+ readonly something: R[]
+}
+
+// From codersdk/genericmap.go
+export type Custom = Foo | Buzz
diff --git a/site/jest.setup.ts b/site/jest.setup.ts
index 1ee61c0a6e..9eef56164b 100644
--- a/site/jest.setup.ts
+++ b/site/jest.setup.ts
@@ -7,7 +7,7 @@ import { TextEncoder, TextDecoder } from "util"
import { Blob } from "buffer"
import jestFetchMock from "jest-fetch-mock"
import { ProxyLatencyReport } from "contexts/useProxyLatency"
-import { RegionsResponse } from "api/typesGenerated"
+import { Region } from "api/typesGenerated"
import { useMemo } from "react"
jestFetchMock.enableMocks()
@@ -16,14 +16,14 @@ jestFetchMock.enableMocks()
// This would fail unit testing, or at least make it very slow with
// actual network requests. So just globally mock this hook.
jest.mock("contexts/useProxyLatency", () => ({
- useProxyLatency: (proxies?: RegionsResponse) => {
+ useProxyLatency: (proxies?: Region[]) => {
// Must use `useMemo` here to avoid infinite loop.
// Mocking the hook with a hook.
const proxyLatencies = useMemo(() => {
if (!proxies) {
return {} as Record
}
- return proxies.regions.reduce((acc, proxy) => {
+ return proxies.reduce((acc, proxy) => {
acc[proxy.id] = {
accurate: true,
// Return a constant latency of 8ms.
diff --git a/site/site.go b/site/site.go
index 2ada3c4908..a358d458e5 100644
--- a/site/site.go
+++ b/site/site.go
@@ -146,7 +146,9 @@ type Handler struct {
buildInfoJSON string
AppearanceFetcher func(ctx context.Context) (codersdk.AppearanceConfig, error)
- RegionsFetcher func(ctx context.Context) (codersdk.RegionsResponse, error)
+ // RegionsFetcher will attempt to fetch the more detailed WorkspaceProxy data, but will fall back to the
+ // regions if the user does not have the correct permissions.
+ RegionsFetcher func(ctx context.Context) (any, error)
Entitlements atomic.Pointer[codersdk.Entitlements]
Experiments atomic.Pointer[codersdk.Experiments]
diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx
index b9af04fb47..0b31a88737 100644
--- a/site/src/AppRouter.tsx
+++ b/site/src/AppRouter.tsx
@@ -276,6 +276,10 @@ export const AppRouter: FC = () => {
} />
} />
} />
+ }
+ />
}>
@@ -286,10 +290,6 @@ export const AppRouter: FC = () => {
} />
} />
- }
- />
diff --git a/site/src/api/api.ts b/site/src/api/api.ts
index 945abd5599..d31ee22168 100644
--- a/site/src/api/api.ts
+++ b/site/src/api/api.ts
@@ -966,13 +966,23 @@ export const getFile = async (fileId: string): Promise => {
return response.data
}
-export const getWorkspaceProxies =
- async (): Promise => {
- const response = await axios.get(
- `/api/v2/regions`,
- )
- return response.data
- }
+export const getWorkspaceProxyRegions = async (): Promise<
+ TypesGen.RegionsResponse
+> => {
+ const response = await axios.get>(
+ `/api/v2/regions`,
+ )
+ return response.data
+}
+
+export const getWorkspaceProxies = async (): Promise<
+ TypesGen.RegionsResponse
+> => {
+ const response = await axios.get<
+ TypesGen.RegionsResponse
+ >(`/api/v2/workspaceproxies`)
+ return response.data
+}
export const getAppearance = async (): Promise => {
try {
diff --git a/site/src/api/typesGenerated.ts b/site/src/api/typesGenerated.ts
index d159e89ef7..050ef91d88 100644
--- a/site/src/api/typesGenerated.ts
+++ b/site/src/api/typesGenerated.ts
@@ -747,8 +747,8 @@ export interface Region {
}
// From codersdk/workspaceproxy.go
-export interface RegionsResponse {
- readonly regions: Region[]
+export interface RegionsResponse {
+ readonly regions: R[]
}
// From codersdk/replicas.go
@@ -1307,17 +1307,11 @@ export interface WorkspaceOptions {
}
// From codersdk/workspaceproxy.go
-export interface WorkspaceProxy {
- readonly id: string
- readonly name: string
- readonly display_name: string
- readonly icon: string
- readonly url: string
- readonly wildcard_hostname: string
+export interface WorkspaceProxy extends Region {
+ readonly status?: WorkspaceProxyStatus
readonly created_at: string
readonly updated_at: string
readonly deleted: boolean
- readonly status?: WorkspaceProxyStatus
}
// From codersdk/deployment.go
@@ -1732,3 +1726,6 @@ export const WorkspaceTransitions: WorkspaceTransition[] = [
"start",
"stop",
]
+
+// From codersdk/workspaceproxy.go
+export type RegionTypes = Region | WorkspaceProxy
diff --git a/site/src/components/DeploySettingsLayout/Badges.tsx b/site/src/components/DeploySettingsLayout/Badges.tsx
index 8a3173f1f4..14d2c0c60d 100644
--- a/site/src/components/DeploySettingsLayout/Badges.tsx
+++ b/site/src/components/DeploySettingsLayout/Badges.tsx
@@ -40,6 +40,24 @@ export const NotHealthyBadge: FC = () => {
)
}
+export const NotRegisteredBadge: FC = () => {
+ const styles = useStyles()
+ return (
+
+ Not Registered
+
+ )
+}
+
+export const NotReachableBadge: FC = () => {
+ const styles = useStyles()
+ return (
+
+ Not Reachable
+
+ )
+}
+
export const DisabledBadge: FC = () => {
const styles = useStyles()
return (
@@ -88,6 +106,7 @@ const useStyles = makeStyles((theme) => ({
display: "flex",
alignItems: "center",
width: "fit-content",
+ whiteSpace: "nowrap",
},
enterpriseBadge: {
@@ -115,6 +134,11 @@ const useStyles = makeStyles((theme) => ({
backgroundColor: theme.palette.error.dark,
},
+ warnBadge: {
+ border: `1px solid ${theme.palette.warning.light}`,
+ backgroundColor: theme.palette.warning.dark,
+ },
+
disabledBadge: {
border: `1px solid ${theme.palette.divider}`,
backgroundColor: theme.palette.background.paper,
diff --git a/site/src/components/DeploySettingsLayout/Sidebar.tsx b/site/src/components/DeploySettingsLayout/Sidebar.tsx
index e7db4e2b5f..788877558e 100644
--- a/site/src/components/DeploySettingsLayout/Sidebar.tsx
+++ b/site/src/components/DeploySettingsLayout/Sidebar.tsx
@@ -4,12 +4,14 @@ import LaunchOutlined from "@mui/icons-material/LaunchOutlined"
import ApprovalIcon from "@mui/icons-material/VerifiedUserOutlined"
import LockRounded from "@mui/icons-material/LockOutlined"
import Globe from "@mui/icons-material/PublicOutlined"
+import HubOutlinedIcon from "@mui/icons-material/HubOutlined"
import VpnKeyOutlined from "@mui/icons-material/VpnKeyOutlined"
import { GitIcon } from "components/Icons/GitIcon"
import { Stack } from "components/Stack/Stack"
import { ElementType, PropsWithChildren, ReactNode, FC } from "react"
import { NavLink } from "react-router-dom"
import { combineClasses } from "utils/combineClasses"
+import { useDashboard } from "components/Dashboard/DashboardProvider"
const SidebarNavItem: FC<
PropsWithChildren<{ href: string; icon: ReactNode }>
@@ -40,6 +42,7 @@ const SidebarNavItemIcon: FC<{ icon: ElementType }> = ({ icon: Icon }) => {
export const Sidebar: React.FC = () => {
const styles = useStyles()
+ const dashboard = useDashboard()
return (