feat: make dynamic parameters respect owner in form (#18013)

Closes https://github.com/coder/coder/issues/18012

---------

Co-authored-by: Jaayden Halko <jaayden.halko@gmail.com>
This commit is contained in:
Steven Masley
2025-05-27 15:43:00 -05:00
committed by GitHub
parent 5b9c40481f
commit b4531c4218
13 changed files with 264 additions and 189 deletions

74
coderd/apidoc/docs.go generated
View File

@@ -5880,6 +5880,43 @@ const docTemplate = `{
}
}
},
"/templateversions/{templateversion}/dynamic-parameters": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"tags": [
"Templates"
],
"summary": "Open dynamic parameters WebSocket by template version",
"operationId": "open-dynamic-parameters-websocket-by-template-version",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Template version ID",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"format": "uuid",
"description": "Template version ID",
"name": "templateversion",
"in": "path",
"required": true
}
],
"responses": {
"101": {
"description": "Switching Protocols"
}
}
}
},
"/templateversions/{templateversion}/external-auth": {
"get": {
"security": [
@@ -7735,43 +7772,6 @@ const docTemplate = `{
}
}
},
"/users/{user}/templateversions/{templateversion}/parameters": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"tags": [
"Templates"
],
"summary": "Open dynamic parameters WebSocket by template version",
"operationId": "open-dynamic-parameters-websocket-by-template-version",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Template version ID",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"format": "uuid",
"description": "Template version ID",
"name": "templateversion",
"in": "path",
"required": true
}
],
"responses": {
"101": {
"description": "Switching Protocols"
}
}
}
},
"/users/{user}/webpush/subscription": {
"post": {
"security": [

View File

@@ -5197,6 +5197,41 @@
}
}
},
"/templateversions/{templateversion}/dynamic-parameters": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"tags": ["Templates"],
"summary": "Open dynamic parameters WebSocket by template version",
"operationId": "open-dynamic-parameters-websocket-by-template-version",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Template version ID",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"format": "uuid",
"description": "Template version ID",
"name": "templateversion",
"in": "path",
"required": true
}
],
"responses": {
"101": {
"description": "Switching Protocols"
}
}
}
},
"/templateversions/{templateversion}/external-auth": {
"get": {
"security": [
@@ -6834,41 +6869,6 @@
}
}
},
"/users/{user}/templateversions/{templateversion}/parameters": {
"get": {
"security": [
{
"CoderSessionToken": []
}
],
"tags": ["Templates"],
"summary": "Open dynamic parameters WebSocket by template version",
"operationId": "open-dynamic-parameters-websocket-by-template-version",
"parameters": [
{
"type": "string",
"format": "uuid",
"description": "Template version ID",
"name": "user",
"in": "path",
"required": true
},
{
"type": "string",
"format": "uuid",
"description": "Template version ID",
"name": "templateversion",
"in": "path",
"required": true
}
],
"responses": {
"101": {
"description": "Switching Protocols"
}
}
}
},
"/users/{user}/webpush/subscription": {
"post": {
"security": [

View File

@@ -1122,6 +1122,7 @@ func New(options *Options) *API {
})
})
})
r.Route("/templateversions/{templateversion}", func(r chi.Router) {
r.Use(
apiKeyMiddleware,
@@ -1150,6 +1151,13 @@ func New(options *Options) *API {
r.Get("/{jobID}/matched-provisioners", api.templateVersionDryRunMatchedProvisioners)
r.Patch("/{jobID}/cancel", api.patchTemplateVersionDryRunCancel)
})
r.Group(func(r chi.Router) {
r.Use(
httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentDynamicParameters),
)
r.Get("/dynamic-parameters", api.templateVersionDynamicParameters)
})
})
r.Route("/users", func(r chi.Router) {
r.Get("/first", api.firstUser)
@@ -1210,19 +1218,6 @@ func New(options *Options) *API {
r.Group(func(r chi.Router) {
r.Use(httpmw.ExtractUserParam(options.Database))
// Similarly to creating a workspace, evaluating parameters for a
// new workspace should also match the authz story of
// postWorkspacesByOrganization
// TODO: Do not require site wide read user permission. Make this work
// with org member permissions.
r.Route("/templateversions/{templateversion}", func(r chi.Router) {
r.Use(
httpmw.ExtractTemplateVersionParam(options.Database),
httpmw.RequireExperiment(api.Experiments, codersdk.ExperimentDynamicParameters),
)
r.Get("/parameters", api.templateVersionDynamicParameters)
})
r.Post("/convert-login", api.postConvertLoginType)
r.Delete("/", api.deleteUser)
r.Get("/", api.userByName)

View File

@@ -36,7 +36,7 @@ import (
// @Param user path string true "Template version ID" format(uuid)
// @Param templateversion path string true "Template version ID" format(uuid)
// @Success 101
// @Router /users/{user}/templateversions/{templateversion}/parameters [get]
// @Router /templateversions/{templateversion}/dynamic-parameters [get]
func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http.Request) {
ctx := r.Context()
templateVersion := httpmw.TemplateVersionParam(r)
@@ -77,12 +77,12 @@ func (api *API) templateVersionDynamicParameters(rw http.ResponseWriter, r *http
}
}
type previewFunction func(ctx context.Context, values map[string]string) (*preview.Output, hcl.Diagnostics)
type previewFunction func(ctx context.Context, ownerID uuid.UUID, values map[string]string) (*preview.Output, hcl.Diagnostics)
func (api *API) handleDynamicParameters(rw http.ResponseWriter, r *http.Request, tf database.TemplateVersionTerraformValue, templateVersion database.TemplateVersion) {
var (
ctx = r.Context()
user = httpmw.UserParam(r)
ctx = r.Context()
apikey = httpmw.APIKey(r)
)
// nolint:gocritic // We need to fetch the templates files for the Terraform
@@ -130,7 +130,7 @@ func (api *API) handleDynamicParameters(rw http.ResponseWriter, r *http.Request,
templateFS = files.NewOverlayFS(templateFS, []files.Overlay{{Path: ".terraform/modules", FS: moduleFilesFS}})
}
owner, err := getWorkspaceOwnerData(ctx, api.Database, user, templateVersion.OrganizationID)
owner, err := getWorkspaceOwnerData(ctx, api.Database, apikey.UserID, templateVersion.OrganizationID)
if err != nil {
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
Message: "Internal error fetching workspace owner.",
@@ -145,10 +145,46 @@ func (api *API) handleDynamicParameters(rw http.ResponseWriter, r *http.Request,
Owner: owner,
}
api.handleParameterWebsocket(rw, r, func(ctx context.Context, values map[string]string) (*preview.Output, hcl.Diagnostics) {
// failedOwners keeps track of which owners failed to fetch from the database.
// This prevents db spam on repeated requests for the same failed owner.
failedOwners := make(map[uuid.UUID]error)
failedOwnerDiag := hcl.Diagnostics{
{
Severity: hcl.DiagError,
Summary: "Failed to fetch workspace owner",
Detail: "Please check your permissions or the user may not exist.",
Extra: previewtypes.DiagnosticExtra{
Code: "owner_not_found",
},
},
}
api.handleParameterWebsocket(rw, r, apikey.UserID, func(ctx context.Context, ownerID uuid.UUID, values map[string]string) (*preview.Output, hcl.Diagnostics) {
if ownerID == uuid.Nil {
// Default to the authenticated user
// Nice for testing
ownerID = apikey.UserID
}
if _, ok := failedOwners[ownerID]; ok {
// If it has failed once, assume it will fail always.
// Re-open the websocket to try again.
return nil, failedOwnerDiag
}
// Update the input values with the new values.
// The rest of the input is unchanged.
input.ParameterValues = values
// Update the owner if there is a change
if input.Owner.ID != ownerID.String() {
owner, err = getWorkspaceOwnerData(ctx, api.Database, ownerID, templateVersion.OrganizationID)
if err != nil {
failedOwners[ownerID] = err
return nil, failedOwnerDiag
}
input.Owner = owner
}
return preview.Preview(ctx, input, templateFS)
})
}
@@ -239,7 +275,7 @@ func (api *API) handleStaticParameters(rw http.ResponseWriter, r *http.Request,
params = append(params, param)
}
api.handleParameterWebsocket(rw, r, func(_ context.Context, values map[string]string) (*preview.Output, hcl.Diagnostics) {
api.handleParameterWebsocket(rw, r, uuid.Nil, func(_ context.Context, _ uuid.UUID, values map[string]string) (*preview.Output, hcl.Diagnostics) {
for i := range params {
param := &params[i]
paramValue, ok := values[param.Name]
@@ -264,7 +300,7 @@ func (api *API) handleStaticParameters(rw http.ResponseWriter, r *http.Request,
})
}
func (api *API) handleParameterWebsocket(rw http.ResponseWriter, r *http.Request, render previewFunction) {
func (api *API) handleParameterWebsocket(rw http.ResponseWriter, r *http.Request, ownerID uuid.UUID, render previewFunction) {
ctx, cancel := context.WithTimeout(r.Context(), 30*time.Minute)
defer cancel()
@@ -284,7 +320,7 @@ func (api *API) handleParameterWebsocket(rw http.ResponseWriter, r *http.Request
)
// Send an initial form state, computed without any user input.
result, diagnostics := render(ctx, map[string]string{})
result, diagnostics := render(ctx, ownerID, map[string]string{})
response := codersdk.DynamicParametersResponse{
ID: -1, // Always start with -1.
Diagnostics: db2sdk.HCLDiagnostics(diagnostics),
@@ -312,7 +348,7 @@ func (api *API) handleParameterWebsocket(rw http.ResponseWriter, r *http.Request
return
}
result, diagnostics := render(ctx, update.Inputs)
result, diagnostics := render(ctx, update.OwnerID, update.Inputs)
response := codersdk.DynamicParametersResponse{
ID: update.ID,
Diagnostics: db2sdk.HCLDiagnostics(diagnostics),
@@ -332,17 +368,24 @@ func (api *API) handleParameterWebsocket(rw http.ResponseWriter, r *http.Request
func getWorkspaceOwnerData(
ctx context.Context,
db database.Store,
user database.User,
ownerID uuid.UUID,
organizationID uuid.UUID,
) (previewtypes.WorkspaceOwner, error) {
var g errgroup.Group
// TODO: @emyrk we should only need read access on the org member, not the
// site wide user object. Figure out a better way to handle this.
user, err := db.GetUserByID(ctx, ownerID)
if err != nil {
return previewtypes.WorkspaceOwner{}, xerrors.Errorf("fetch user: %w", err)
}
var ownerRoles []previewtypes.WorkspaceOwnerRBACRole
g.Go(func() error {
// nolint:gocritic // This is kind of the wrong query to use here, but it
// matches how the provisioner currently works. We should figure out
// something that needs less escalation but has the correct behavior.
row, err := db.GetAuthorizationUserRoles(dbauthz.AsSystemRestricted(ctx), user.ID)
row, err := db.GetAuthorizationUserRoles(dbauthz.AsSystemRestricted(ctx), ownerID)
if err != nil {
return err
}
@@ -372,7 +415,7 @@ func getWorkspaceOwnerData(
// The correct public key has to be sent. This will not be leaked
// unless the template leaks it.
// nolint:gocritic
key, err := db.GetGitSSHKey(dbauthz.AsSystemRestricted(ctx), user.ID)
key, err := db.GetGitSSHKey(dbauthz.AsSystemRestricted(ctx), ownerID)
if err != nil {
return err
}
@@ -388,7 +431,7 @@ func getWorkspaceOwnerData(
// nolint:gocritic
groups, err := db.GetGroups(dbauthz.AsSystemRestricted(ctx), database.GetGroupsParams{
OrganizationID: organizationID,
HasMemberID: user.ID,
HasMemberID: ownerID,
})
if err != nil {
return err
@@ -400,7 +443,7 @@ func getWorkspaceOwnerData(
return nil
})
err := g.Wait()
err = g.Wait()
if err != nil {
return previewtypes.WorkspaceOwner{}, err
}

View File

@@ -32,7 +32,7 @@ func TestDynamicParametersOwnerSSHPublicKey(t *testing.T) {
cfg.Experiments = []string{string(codersdk.ExperimentDynamicParameters)}
ownerClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, DeploymentValues: cfg})
owner := coderdtest.CreateFirstUser(t, ownerClient)
templateAdmin, templateAdminUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
templateAdmin, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/public_key/main.tf")
require.NoError(t, err)
@@ -57,7 +57,7 @@ func TestDynamicParametersOwnerSSHPublicKey(t *testing.T) {
_ = coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID)
ctx := testutil.Context(t, testutil.WaitShort)
stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, templateAdminUser.ID, version.ID)
stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, version.ID)
require.NoError(t, err)
defer stream.Close(websocket.StatusGoingAway)
@@ -210,6 +210,47 @@ func TestDynamicParametersWithTerraformValues(t *testing.T) {
// test to make it obvious what this test is doing.
require.Zero(t, setup.api.FileCache.Count())
})
t.Run("BadOwner", func(t *testing.T) {
t.Parallel()
dynamicParametersTerraformSource, err := os.ReadFile("testdata/parameters/modules/main.tf")
require.NoError(t, err)
modulesArchive, err := terraform.GetModulesArchive(os.DirFS("testdata/parameters/modules"))
require.NoError(t, err)
setup := setupDynamicParamsTest(t, setupDynamicParamsTestParams{
provisionerDaemonVersion: provProto.CurrentVersion.String(),
mainTF: dynamicParametersTerraformSource,
modulesArchive: modulesArchive,
plan: nil,
static: nil,
})
ctx := testutil.Context(t, testutil.WaitShort)
stream := setup.stream
previews := stream.Chan()
// Should see the output of the module represented
preview := testutil.RequireReceive(ctx, t, previews)
require.Equal(t, -1, preview.ID)
require.Empty(t, preview.Diagnostics)
err = stream.Send(codersdk.DynamicParametersRequest{
ID: 1,
Inputs: map[string]string{
"jetbrains_ide": "GO",
},
OwnerID: uuid.New(),
})
require.NoError(t, err)
preview = testutil.RequireReceive(ctx, t, previews)
require.Equal(t, 1, preview.ID)
require.Len(t, preview.Diagnostics, 1)
require.Equal(t, preview.Diagnostics[0].Extra.Code, "owner_not_found")
})
}
type setupDynamicParamsTestParams struct {
@@ -242,7 +283,7 @@ func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dyn
})
owner := coderdtest.CreateFirstUser(t, ownerClient)
templateAdmin, templateAdminUser := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
templateAdmin, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
files := echo.WithExtraFiles(map[string][]byte{
"main.tf": args.mainTF,
@@ -262,7 +303,7 @@ func setupDynamicParamsTest(t *testing.T, args setupDynamicParamsTestParams) dyn
_ = coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID)
ctx := testutil.Context(t, testutil.WaitShort)
stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, templateAdminUser.ID, version.ID)
stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, version.ID)
if args.expectWebsocketError {
require.Errorf(t, err, "expected error forming websocket")
} else {

View File

@@ -114,6 +114,8 @@ type DynamicParametersRequest struct {
// ID so that the client can match it to the request.
ID int `json:"id"`
Inputs map[string]string `json:"inputs"`
// OwnerID if uuid.Nil, it defaults to `codersdk.Me`
OwnerID uuid.UUID `json:"owner_id,omitempty" format:"uuid"`
}
type DynamicParametersResponse struct {
@@ -123,8 +125,8 @@ type DynamicParametersResponse struct {
// TODO: Workspace tags
}
func (c *Client) TemplateVersionDynamicParameters(ctx context.Context, userID, version uuid.UUID) (*wsjson.Stream[DynamicParametersResponse, DynamicParametersRequest], error) {
conn, err := c.Dial(ctx, fmt.Sprintf("/api/v2/users/%s/templateversions/%s/parameters", userID, version), nil)
func (c *Client) TemplateVersionDynamicParameters(ctx context.Context, version uuid.UUID) (*wsjson.Stream[DynamicParametersResponse, DynamicParametersRequest], error) {
conn, err := c.Dial(ctx, fmt.Sprintf("/api/v2/templateversions/%s/dynamic-parameters", version), nil)
if err != nil {
return nil, err
}

View File

@@ -2577,6 +2577,33 @@ Status Code **200**
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Open dynamic parameters WebSocket by template version
### Code samples
```shell
# Example request using curl
curl -X GET http://coder-server:8080/api/v2/templateversions/{templateversion}/dynamic-parameters \
-H 'Coder-Session-Token: API_KEY'
```
`GET /templateversions/{templateversion}/dynamic-parameters`
### Parameters
| Name | In | Type | Required | Description |
|-------------------|------|--------------|----------|---------------------|
| `user` | path | string(uuid) | true | Template version ID |
| `templateversion` | path | string(uuid) | true | Template version ID |
### Responses
| Status | Meaning | Description | Schema |
|--------|--------------------------------------------------------------------------|---------------------|--------|
| 101 | [Switching Protocols](https://tools.ietf.org/html/rfc7231#section-6.2.2) | Switching Protocols | |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Get external auth by template version
### Code samples
@@ -3344,30 +3371,3 @@ Status Code **200**
| `type` | `bool` |
To perform this operation, you must be authenticated. [Learn more](authentication.md).
## Open dynamic parameters WebSocket by template version
### Code samples
```shell
# Example request using curl
curl -X GET http://coder-server:8080/api/v2/users/{user}/templateversions/{templateversion}/parameters \
-H 'Coder-Session-Token: API_KEY'
```
`GET /users/{user}/templateversions/{templateversion}/parameters`
### Parameters
| Name | In | Type | Required | Description |
|-------------------|------|--------------|----------|---------------------|
| `user` | path | string(uuid) | true | Template version ID |
| `templateversion` | path | string(uuid) | true | Template version ID |
### Responses
| Status | Meaning | Description | Schema |
|--------|--------------------------------------------------------------------------|---------------------|--------|
| 101 | [Switching Protocols](https://tools.ietf.org/html/rfc7231#section-6.2.2) | Switching Protocols | |
To perform this operation, you must be authenticated. [Learn more](authentication.md).

View File

@@ -59,7 +59,7 @@ func TestDynamicParametersOwnerGroups(t *testing.T) {
_ = coderdtest.CreateTemplate(t, templateAdmin, owner.OrganizationID, version.ID)
ctx := testutil.Context(t, testutil.WaitShort)
stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, templateAdminUser.ID, version.ID)
stream, err := templateAdmin.TemplateVersionDynamicParameters(ctx, version.ID)
require.NoError(t, err)
defer stream.Close(websocket.StatusGoingAway)

View File

@@ -1000,7 +1000,6 @@ class ApiMethods {
};
templateVersionDynamicParameters = (
userId: string,
versionId: string,
{
onMessage,
@@ -1013,7 +1012,7 @@ class ApiMethods {
},
): WebSocket => {
const socket = createWebSocket(
`/api/v2/users/${userId}/templateversions/${versionId}/parameters`,
`/api/v2/templateversions/${versionId}/dynamic-parameters`,
);
socket.addEventListener("message", (event) =>

View File

@@ -774,6 +774,7 @@ export const DisplayApps: DisplayApp[] = [
export interface DynamicParametersRequest {
readonly id: number;
readonly inputs: Record<string, string>;
readonly owner_id?: string;
}
// From codersdk/parameters.go

View File

@@ -88,16 +88,19 @@ const CreateWorkspacePageExperimental: FC = () => {
const autofillParameters = getAutofillParameters(searchParams);
const sendMessage = useCallback((formValues: Record<string, string>) => {
const request: DynamicParametersRequest = {
id: wsResponseId.current + 1,
inputs: formValues,
};
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify(request));
wsResponseId.current = wsResponseId.current + 1;
}
}, []);
const sendMessage = useEffectEvent(
(formValues: Record<string, string>, ownerId?: string) => {
const request: DynamicParametersRequest = {
id: wsResponseId.current + 1,
owner_id: ownerId ?? owner.id,
inputs: formValues,
};
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify(request));
wsResponseId.current = wsResponseId.current + 1;
}
},
);
// On page load, sends all initial parameter values to the websocket
// (including defaults and autofilled from the url)
@@ -145,35 +148,31 @@ const CreateWorkspacePageExperimental: FC = () => {
useEffect(() => {
if (!realizedVersionId) return;
const socket = API.templateVersionDynamicParameters(
owner.id,
realizedVersionId,
{
onMessage,
onError: (error) => {
if (ws.current === socket) {
setWsError(error);
}
},
onClose: () => {
if (ws.current === socket) {
setWsError(
new DetailedError(
"Websocket connection for dynamic parameters unexpectedly closed.",
"Refresh the page to reset the form.",
),
);
}
},
const socket = API.templateVersionDynamicParameters(realizedVersionId, {
onMessage,
onError: (error) => {
if (ws.current === socket) {
setWsError(error);
}
},
);
onClose: () => {
if (ws.current === socket) {
setWsError(
new DetailedError(
"Websocket connection for dynamic parameters unexpectedly closed.",
"Refresh the page to reset the form.",
),
);
}
},
});
ws.current = socket;
return () => {
socket.close();
};
}, [owner.id, realizedVersionId, onMessage]);
}, [realizedVersionId, onMessage]);
const organizationId = templateQuery.data?.organization_id;

View File

@@ -79,7 +79,7 @@ interface CreateWorkspacePageViewExperimentalProps {
owner: TypesGen.User,
) => void;
resetMutation: () => void;
sendMessage: (message: Record<string, string>) => void;
sendMessage: (message: Record<string, string>, ownerId?: string) => void;
startPollingExternalAuth: () => void;
owner: TypesGen.User;
setOwner: (user: TypesGen.User) => void;
@@ -280,9 +280,10 @@ export const CreateWorkspacePageViewExperimental: FC<
form.values.rich_parameter_values,
]);
// send the last user modified parameter and all touched parameters to the websocket
// include any modified parameters and all touched parameters to the websocket request
const sendDynamicParamsRequest = (
parameters: Array<{ parameter: PreviewParameter; value: string }>,
ownerId?: string,
) => {
const formInputs: Record<string, string> = {};
const formParameters = form.values.rich_parameter_values ?? [];
@@ -303,7 +304,12 @@ export const CreateWorkspacePageViewExperimental: FC<
}
}
sendMessage(formInputs);
sendMessage(formInputs, ownerId);
};
const handleOwnerChange = (user: TypesGen.User) => {
setOwner(user);
sendDynamicParamsRequest([], user.id);
};
const handleChange = async (
@@ -486,7 +492,7 @@ export const CreateWorkspacePageViewExperimental: FC<
<UserAutocomplete
value={owner}
onChange={(user) => {
setOwner(user ?? defaultOwner);
handleOwnerChange(user ?? defaultOwner);
}}
size="medium"
/>

View File

@@ -21,14 +21,7 @@ import {
import { useEffectEvent } from "hooks/hookPolyfills";
import { CircleHelp, Undo2 } from "lucide-react";
import type { FC } from "react";
import {
useCallback,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useContext, useEffect, useMemo, useRef, useState } from "react";
import { Helmet } from "react-helmet-async";
import { useMutation, useQuery } from "react-query";
import { useNavigate } from "react-router-dom";
@@ -53,16 +46,17 @@ const WorkspaceParametersPageExperimental: FC = () => {
const ws = useRef<WebSocket | null>(null);
const [wsError, setWsError] = useState<Error | null>(null);
const sendMessage = useCallback((formValues: Record<string, string>) => {
const sendMessage = useEffectEvent((formValues: Record<string, string>) => {
const request: DynamicParametersRequest = {
id: wsResponseId.current + 1,
owner_id: workspace.owner_id,
inputs: formValues,
};
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify(request));
wsResponseId.current = wsResponseId.current + 1;
}
}, []);
});
const onMessage = useEffectEvent((response: DynamicParametersResponse) => {
if (latestResponse && latestResponse?.id >= response.id) {
@@ -76,7 +70,6 @@ const WorkspaceParametersPageExperimental: FC = () => {
if (!workspace.latest_build.template_version_id) return;
const socket = API.templateVersionDynamicParameters(
workspace.owner_id,
workspace.latest_build.template_version_id,
{
onMessage,
@@ -103,11 +96,7 @@ const WorkspaceParametersPageExperimental: FC = () => {
return () => {
socket.close();
};
}, [
workspace.owner_id,
workspace.latest_build.template_version_id,
onMessage,
]);
}, [workspace.latest_build.template_version_id, onMessage]);
const updateParameters = useMutation({
mutationFn: (buildParameters: WorkspaceBuildParameter[]) =>