mirror of https://github.com/coder/coder.git
feat: add user-level parameter autofill (#11731)
This PR solves #10478 by auto-filling previously used template values in create and update workspace flows. I decided against explicit user values in settings for these reasons: * Autofill is far easier to implement * Users benefit from autofill _by default_ — we don't need to teach them new concepts * If we decide that autofill creates more harm than good, we can remove it without breaking compatibility
This commit is contained in:
parent
aeb4112513
commit
adbb025e74
|
@ -4142,6 +4142,50 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/users/{user}/autofill-parameters": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Users"
|
||||
],
|
||||
"summary": "Get autofill build parameters for user",
|
||||
"operationId": "get-autofill-build-parameters-for-user",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID, username, or me",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Template ID",
|
||||
"name": "template_id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.UserParameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{user}/convert-login": {
|
||||
"post": {
|
||||
"security": [
|
||||
|
@ -10493,6 +10537,7 @@ const docTemplate = `{
|
|||
"api_key",
|
||||
"user",
|
||||
"user_data",
|
||||
"user_workspace_build_parameters",
|
||||
"organization_member",
|
||||
"license",
|
||||
"deployment_config",
|
||||
|
@ -10518,6 +10563,7 @@ const docTemplate = `{
|
|||
"ResourceAPIKey",
|
||||
"ResourceUser",
|
||||
"ResourceUserData",
|
||||
"ResourceUserWorkspaceBuildParameters",
|
||||
"ResourceOrganizationMember",
|
||||
"ResourceLicense",
|
||||
"ResourceDeploymentValues",
|
||||
|
@ -11923,6 +11969,17 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UserParameter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UserQuietHoursScheduleConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -3638,6 +3638,46 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/users/{user}/autofill-parameters": {
|
||||
"get": {
|
||||
"security": [
|
||||
{
|
||||
"CoderSessionToken": []
|
||||
}
|
||||
],
|
||||
"produces": ["application/json"],
|
||||
"tags": ["Users"],
|
||||
"summary": "Get autofill build parameters for user",
|
||||
"operationId": "get-autofill-build-parameters-for-user",
|
||||
"parameters": [
|
||||
{
|
||||
"type": "string",
|
||||
"description": "User ID, username, or me",
|
||||
"name": "user",
|
||||
"in": "path",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"description": "Template ID",
|
||||
"name": "template_id",
|
||||
"in": "query",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/codersdk.UserParameter"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/users/{user}/convert-login": {
|
||||
"post": {
|
||||
"security": [
|
||||
|
@ -9439,6 +9479,7 @@
|
|||
"api_key",
|
||||
"user",
|
||||
"user_data",
|
||||
"user_workspace_build_parameters",
|
||||
"organization_member",
|
||||
"license",
|
||||
"deployment_config",
|
||||
|
@ -9464,6 +9505,7 @@
|
|||
"ResourceAPIKey",
|
||||
"ResourceUser",
|
||||
"ResourceUserData",
|
||||
"ResourceUserWorkspaceBuildParameters",
|
||||
"ResourceOrganizationMember",
|
||||
"ResourceLicense",
|
||||
"ResourceDeploymentValues",
|
||||
|
@ -10810,6 +10852,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UserParameter": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"codersdk.UserQuietHoursScheduleConfig": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
|
@ -821,6 +821,7 @@ func New(options *Options) *API {
|
|||
r.Post("/convert-login", api.postConvertLoginType)
|
||||
r.Delete("/", api.deleteUser)
|
||||
r.Get("/", api.userByName)
|
||||
r.Get("/autofill-parameters", api.userAutofillParameters)
|
||||
r.Get("/login-type", api.userLoginType)
|
||||
r.Put("/profile", api.putUserProfile)
|
||||
r.Route("/status", func(r chi.Router) {
|
||||
|
|
|
@ -1768,6 +1768,17 @@ func (q *querier) GetUserLinksByUserID(ctx context.Context, userID uuid.UUID) ([
|
|||
return q.db.GetUserLinksByUserID(ctx, userID)
|
||||
}
|
||||
|
||||
func (q *querier) GetUserWorkspaceBuildParameters(ctx context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
|
||||
u, err := q.db.GetUserByID(ctx, params.OwnerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := q.authorizeContext(ctx, rbac.ActionRead, u.UserWorkspaceBuildParametersObject()); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return q.db.GetUserWorkspaceBuildParameters(ctx, params)
|
||||
}
|
||||
|
||||
func (q *querier) GetUsers(ctx context.Context, arg database.GetUsersParams) ([]database.GetUsersRow, error) {
|
||||
// This does the filtering in SQL.
|
||||
prep, err := prepareSQLFilter(ctx, q.auth, rbac.ActionRead, rbac.ResourceUser.Type)
|
||||
|
|
|
@ -1052,6 +1052,17 @@ func (s *MethodTestSuite) TestUser() {
|
|||
UpdatedAt: u.UpdatedAt,
|
||||
}).Asserts(u.UserDataRBACObject(), rbac.ActionUpdate).Returns(u)
|
||||
}))
|
||||
s.Run("GetUserWorkspaceBuildParameters", s.Subtest(func(db database.Store, check *expects) {
|
||||
u := dbgen.User(s.T(), db, database.User{})
|
||||
check.Args(
|
||||
database.GetUserWorkspaceBuildParametersParams{
|
||||
OwnerID: u.ID,
|
||||
TemplateID: uuid.UUID{},
|
||||
},
|
||||
).Asserts(u.UserWorkspaceBuildParametersObject(), rbac.ActionRead).Returns(
|
||||
[]database.GetUserWorkspaceBuildParametersRow{},
|
||||
)
|
||||
}))
|
||||
s.Run("UpdateUserAppearanceSettings", s.Subtest(func(db database.Store, check *expects) {
|
||||
u := dbgen.User(s.T(), db, database.User{})
|
||||
check.Args(database.UpdateUserAppearanceSettingsParams{
|
||||
|
|
|
@ -3797,6 +3797,65 @@ func (q *FakeQuerier) GetUserLinksByUserID(_ context.Context, userID uuid.UUID)
|
|||
return uls, nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetUserWorkspaceBuildParameters(_ context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
|
||||
q.mutex.RLock()
|
||||
defer q.mutex.RUnlock()
|
||||
|
||||
userWorkspaceIDs := make(map[uuid.UUID]struct{})
|
||||
for _, ws := range q.workspaces {
|
||||
if ws.OwnerID != params.OwnerID {
|
||||
continue
|
||||
}
|
||||
if ws.TemplateID != params.TemplateID {
|
||||
continue
|
||||
}
|
||||
userWorkspaceIDs[ws.ID] = struct{}{}
|
||||
}
|
||||
|
||||
userWorkspaceBuilds := make(map[uuid.UUID]struct{})
|
||||
for _, wb := range q.workspaceBuilds {
|
||||
if _, ok := userWorkspaceIDs[wb.WorkspaceID]; !ok {
|
||||
continue
|
||||
}
|
||||
userWorkspaceBuilds[wb.ID] = struct{}{}
|
||||
}
|
||||
|
||||
templateVersions := make(map[uuid.UUID]struct{})
|
||||
for _, tv := range q.templateVersions {
|
||||
if tv.TemplateID.UUID != params.TemplateID {
|
||||
continue
|
||||
}
|
||||
templateVersions[tv.ID] = struct{}{}
|
||||
}
|
||||
|
||||
tvps := make(map[string]struct{})
|
||||
for _, tvp := range q.templateVersionParameters {
|
||||
if _, ok := templateVersions[tvp.TemplateVersionID]; !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if _, ok := tvps[tvp.Name]; !ok && !tvp.Ephemeral {
|
||||
tvps[tvp.Name] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
userWorkspaceBuildParameters := make(map[string]database.GetUserWorkspaceBuildParametersRow)
|
||||
for _, wbp := range q.workspaceBuildParameters {
|
||||
if _, ok := userWorkspaceBuilds[wbp.WorkspaceBuildID]; !ok {
|
||||
continue
|
||||
}
|
||||
if _, ok := tvps[wbp.Name]; !ok {
|
||||
continue
|
||||
}
|
||||
userWorkspaceBuildParameters[wbp.Name] = database.GetUserWorkspaceBuildParametersRow{
|
||||
Name: wbp.Name,
|
||||
Value: wbp.Value,
|
||||
}
|
||||
}
|
||||
|
||||
return maps.Values(userWorkspaceBuildParameters), nil
|
||||
}
|
||||
|
||||
func (q *FakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams) ([]database.GetUsersRow, error) {
|
||||
if err := validateDatabaseType(params); err != nil {
|
||||
return nil, err
|
||||
|
|
|
@ -1012,6 +1012,13 @@ func (m metricsStore) GetUserLinksByUserID(ctx context.Context, userID uuid.UUID
|
|||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) GetUserWorkspaceBuildParameters(ctx context.Context, ownerID database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetUserWorkspaceBuildParameters(ctx, ownerID)
|
||||
m.queryLatencies.WithLabelValues("GetUserWorkspaceBuildParameters").Observe(time.Since(start).Seconds())
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m metricsStore) GetUsers(ctx context.Context, arg database.GetUsersParams) ([]database.GetUsersRow, error) {
|
||||
start := time.Now()
|
||||
users, err := m.s.GetUsers(ctx, arg)
|
||||
|
|
|
@ -2104,6 +2104,21 @@ func (mr *MockStoreMockRecorder) GetUserLinksByUserID(arg0, arg1 any) *gomock.Ca
|
|||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserLinksByUserID", reflect.TypeOf((*MockStore)(nil).GetUserLinksByUserID), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetUserWorkspaceBuildParameters mocks base method.
|
||||
func (m *MockStore) GetUserWorkspaceBuildParameters(arg0 context.Context, arg1 database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetUserWorkspaceBuildParameters", arg0, arg1)
|
||||
ret0, _ := ret[0].([]database.GetUserWorkspaceBuildParametersRow)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetUserWorkspaceBuildParameters indicates an expected call of GetUserWorkspaceBuildParameters.
|
||||
func (mr *MockStoreMockRecorder) GetUserWorkspaceBuildParameters(arg0, arg1 any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetUserWorkspaceBuildParameters", reflect.TypeOf((*MockStore)(nil).GetUserWorkspaceBuildParameters), arg0, arg1)
|
||||
}
|
||||
|
||||
// GetUsers mocks base method.
|
||||
func (m *MockStore) GetUsers(arg0 context.Context, arg1 database.GetUsersParams) ([]database.GetUsersRow, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
|
|
@ -256,6 +256,10 @@ func (u User) UserDataRBACObject() rbac.Object {
|
|||
return rbac.ResourceUserData.WithID(u.ID).WithOwner(u.ID.String())
|
||||
}
|
||||
|
||||
func (u User) UserWorkspaceBuildParametersObject() rbac.Object {
|
||||
return rbac.ResourceUserWorkspaceBuildParameters.WithID(u.ID).WithOwner(u.ID.String())
|
||||
}
|
||||
|
||||
func (u GetUsersRow) RBACObject() rbac.Object {
|
||||
return rbac.ResourceUserObject(u.ID)
|
||||
}
|
||||
|
|
|
@ -211,6 +211,7 @@ type sqlcQuerier interface {
|
|||
GetUserLinkByLinkedID(ctx context.Context, linkedID string) (UserLink, error)
|
||||
GetUserLinkByUserIDLoginType(ctx context.Context, arg GetUserLinkByUserIDLoginTypeParams) (UserLink, error)
|
||||
GetUserLinksByUserID(ctx context.Context, userID uuid.UUID) ([]UserLink, error)
|
||||
GetUserWorkspaceBuildParameters(ctx context.Context, arg GetUserWorkspaceBuildParametersParams) ([]GetUserWorkspaceBuildParametersRow, error)
|
||||
// This will never return deleted users.
|
||||
GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error)
|
||||
// This shouldn't check for deleted, because it's frequently used
|
||||
|
|
|
@ -9939,6 +9939,62 @@ func (q *sqlQuerier) InsertWorkspaceAppStats(ctx context.Context, arg InsertWork
|
|||
return err
|
||||
}
|
||||
|
||||
const getUserWorkspaceBuildParameters = `-- name: GetUserWorkspaceBuildParameters :many
|
||||
SELECT DISTINCT ON (tvp.name)
|
||||
tvp.name,
|
||||
wbp.value
|
||||
FROM
|
||||
workspace_build_parameters wbp
|
||||
JOIN
|
||||
workspace_builds wb ON wb.id = wbp.workspace_build_id
|
||||
JOIN
|
||||
workspaces w ON w.id = wb.workspace_id
|
||||
JOIN
|
||||
template_version_parameters tvp ON tvp.template_version_id = wb.template_version_id
|
||||
WHERE
|
||||
w.owner_id = $1
|
||||
AND wb.transition = 'start'
|
||||
AND w.template_id = $2
|
||||
AND tvp.ephemeral = false
|
||||
AND tvp.name = wbp.name
|
||||
ORDER BY
|
||||
tvp.name, wb.created_at DESC
|
||||
LIMIT 100
|
||||
`
|
||||
|
||||
type GetUserWorkspaceBuildParametersParams struct {
|
||||
OwnerID uuid.UUID `db:"owner_id" json:"owner_id"`
|
||||
TemplateID uuid.UUID `db:"template_id" json:"template_id"`
|
||||
}
|
||||
|
||||
type GetUserWorkspaceBuildParametersRow struct {
|
||||
Name string `db:"name" json:"name"`
|
||||
Value string `db:"value" json:"value"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetUserWorkspaceBuildParameters(ctx context.Context, arg GetUserWorkspaceBuildParametersParams) ([]GetUserWorkspaceBuildParametersRow, error) {
|
||||
rows, err := q.db.QueryContext(ctx, getUserWorkspaceBuildParameters, arg.OwnerID, arg.TemplateID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []GetUserWorkspaceBuildParametersRow
|
||||
for rows.Next() {
|
||||
var i GetUserWorkspaceBuildParametersRow
|
||||
if err := rows.Scan(&i.Name, &i.Value); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
const getWorkspaceBuildParameters = `-- name: GetWorkspaceBuildParameters :many
|
||||
SELECT
|
||||
workspace_build_id, name, value
|
||||
|
|
|
@ -14,3 +14,27 @@ FROM
|
|||
workspace_build_parameters
|
||||
WHERE
|
||||
workspace_build_id = $1;
|
||||
|
||||
-- name: GetUserWorkspaceBuildParameters :many
|
||||
-- name: GetUserWorkspaceBuildParameters :many
|
||||
SELECT DISTINCT ON (tvp.name)
|
||||
tvp.name,
|
||||
wbp.value
|
||||
FROM
|
||||
workspace_build_parameters wbp
|
||||
JOIN
|
||||
workspace_builds wb ON wb.id = wbp.workspace_build_id
|
||||
JOIN
|
||||
workspaces w ON w.id = wb.workspace_id
|
||||
JOIN
|
||||
template_version_parameters tvp ON tvp.template_version_id = wb.template_version_id
|
||||
WHERE
|
||||
w.owner_id = $1
|
||||
AND wb.transition = 'start'
|
||||
AND w.template_id = $2
|
||||
AND tvp.ephemeral = false
|
||||
AND tvp.name = wbp.name
|
||||
ORDER BY
|
||||
tvp.name, wb.created_at DESC
|
||||
LIMIT 100;
|
||||
|
||||
|
|
|
@ -148,6 +148,12 @@ var (
|
|||
Type: "user_data",
|
||||
}
|
||||
|
||||
// ResourceUserWorkspaceBuildParameters is the user's workspace build
|
||||
// parameter history.
|
||||
ResourceUserWorkspaceBuildParameters = Object{
|
||||
Type: "user_workspace_build_parameters",
|
||||
}
|
||||
|
||||
// ResourceOrganizationMember is a user's membership in an organization.
|
||||
// Has ONLY an organization owner.
|
||||
// create/delete = Create/delete member from org.
|
||||
|
|
|
@ -25,6 +25,7 @@ func AllResources() []Object {
|
|||
ResourceTemplateInsights,
|
||||
ResourceUser,
|
||||
ResourceUserData,
|
||||
ResourceUserWorkspaceBuildParameters,
|
||||
ResourceWildcard,
|
||||
ResourceWorkspace,
|
||||
ResourceWorkspaceApplicationConnect,
|
||||
|
|
|
@ -154,7 +154,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
|||
Permissions(map[string][]Action{
|
||||
// Users cannot do create/update/delete on themselves, but they
|
||||
// can read their own details.
|
||||
ResourceUser.Type: {ActionRead},
|
||||
ResourceUser.Type: {ActionRead},
|
||||
ResourceUserWorkspaceBuildParameters.Type: {ActionRead},
|
||||
// Users can create provisioner daemons scoped to themselves.
|
||||
ResourceProvisionerDaemon.Type: {ActionCreate, ActionRead, ActionUpdate},
|
||||
})...,
|
||||
|
@ -209,9 +210,10 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
|||
Name: userAdmin,
|
||||
DisplayName: "User Admin",
|
||||
Site: Permissions(map[string][]Action{
|
||||
ResourceRoleAssignment.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||
ResourceUser.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||
ResourceUserData.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||
ResourceRoleAssignment.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||
ResourceUser.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||
ResourceUserData.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||
ResourceUserWorkspaceBuildParameters.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||
// Full perms to manage org members
|
||||
ResourceOrganizationMember.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||
ResourceGroup.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||
|
|
|
@ -569,6 +569,57 @@ func (api *API) userByName(rw http.ResponseWriter, r *http.Request) {
|
|||
httpapi.Write(ctx, rw, http.StatusOK, db2sdk.User(user, organizationIDs))
|
||||
}
|
||||
|
||||
// Returns recent build parameters for the signed-in user.
|
||||
//
|
||||
// @Summary Get autofill build parameters for user
|
||||
// @ID get-autofill-build-parameters-for-user
|
||||
// @Security CoderSessionToken
|
||||
// @Produce json
|
||||
// @Tags Users
|
||||
// @Param user path string true "User ID, username, or me"
|
||||
// @Param template_id query string true "Template ID"
|
||||
// @Success 200 {array} codersdk.UserParameter
|
||||
// @Router /users/{user}/autofill-parameters [get]
|
||||
func (api *API) userAutofillParameters(rw http.ResponseWriter, r *http.Request) {
|
||||
user := httpmw.UserParam(r)
|
||||
|
||||
p := httpapi.NewQueryParamParser().Required("template_id")
|
||||
templateID := p.UUID(r.URL.Query(), uuid.UUID{}, "template_id")
|
||||
p.ErrorExcessParams(r.URL.Query())
|
||||
if len(p.Errors) > 0 {
|
||||
httpapi.Write(r.Context(), rw, http.StatusBadRequest, codersdk.Response{
|
||||
Message: "Invalid query parameters.",
|
||||
Validations: p.Errors,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
params, err := api.Database.GetUserWorkspaceBuildParameters(
|
||||
r.Context(),
|
||||
database.GetUserWorkspaceBuildParametersParams{
|
||||
OwnerID: user.ID,
|
||||
TemplateID: templateID,
|
||||
},
|
||||
)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
httpapi.Write(r.Context(), rw, http.StatusInternalServerError, codersdk.Response{
|
||||
Message: "Internal error fetching user's parameters.",
|
||||
Detail: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
sdkParams := []codersdk.UserParameter{}
|
||||
for _, param := range params {
|
||||
sdkParams = append(sdkParams, codersdk.UserParameter{
|
||||
Name: param.Name,
|
||||
Value: param.Value,
|
||||
})
|
||||
}
|
||||
|
||||
httpapi.Write(r.Context(), rw, http.StatusOK, sdkParams)
|
||||
}
|
||||
|
||||
// Returns the user's login type. This only works if the api key for authorization
|
||||
// and the requested user match. Eg: 'me'
|
||||
//
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||||
"github.com/coder/coder/v2/coderd/database"
|
||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||||
"github.com/coder/coder/v2/coderd/database/dbfake"
|
||||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
"github.com/coder/coder/v2/coderd/rbac"
|
||||
|
@ -1721,6 +1722,129 @@ func TestSuspendedPagination(t *testing.T) {
|
|||
require.Equal(t, expected, page.Users, "expected page")
|
||||
}
|
||||
|
||||
func TestUserAutofillParameters(t *testing.T) {
|
||||
t.Parallel()
|
||||
t.Run("NotSelf", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client1, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{})
|
||||
|
||||
u1 := coderdtest.CreateFirstUser(t, client1)
|
||||
|
||||
client2, u2 := coderdtest.CreateAnotherUser(t, client1, u1.OrganizationID)
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
db := api.Database
|
||||
|
||||
version := dbfake.TemplateVersion(t, db).Seed(database.TemplateVersion{
|
||||
CreatedBy: u1.UserID,
|
||||
OrganizationID: u1.OrganizationID,
|
||||
}).Params(database.TemplateVersionParameter{
|
||||
Name: "param",
|
||||
Required: true,
|
||||
}).Do()
|
||||
|
||||
_, err := client2.UserAutofillParameters(
|
||||
ctx,
|
||||
u1.UserID.String(),
|
||||
version.Template.ID,
|
||||
)
|
||||
|
||||
var apiErr *codersdk.Error
|
||||
require.ErrorAs(t, err, &apiErr)
|
||||
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
||||
|
||||
// u1 should be able to read u2's parameters as u1 is site admin.
|
||||
_, err = client1.UserAutofillParameters(
|
||||
ctx,
|
||||
u2.ID.String(),
|
||||
version.Template.ID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
})
|
||||
|
||||
t.Run("FindsParameters", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
client1, _, api := coderdtest.NewWithAPI(t, &coderdtest.Options{})
|
||||
|
||||
u1 := coderdtest.CreateFirstUser(t, client1)
|
||||
|
||||
client2, u2 := coderdtest.CreateAnotherUser(t, client1, u1.OrganizationID)
|
||||
|
||||
db := api.Database
|
||||
|
||||
version := dbfake.TemplateVersion(t, db).Seed(database.TemplateVersion{
|
||||
CreatedBy: u1.UserID,
|
||||
OrganizationID: u1.OrganizationID,
|
||||
}).Params(database.TemplateVersionParameter{
|
||||
Name: "param",
|
||||
Required: true,
|
||||
},
|
||||
database.TemplateVersionParameter{
|
||||
Name: "param2",
|
||||
Ephemeral: true,
|
||||
},
|
||||
).Do()
|
||||
|
||||
dbfake.WorkspaceBuild(t, db, database.Workspace{
|
||||
OwnerID: u2.ID,
|
||||
TemplateID: version.Template.ID,
|
||||
OrganizationID: u1.OrganizationID,
|
||||
}).Params(
|
||||
database.WorkspaceBuildParameter{
|
||||
Name: "param",
|
||||
Value: "foo",
|
||||
},
|
||||
database.WorkspaceBuildParameter{
|
||||
Name: "param2",
|
||||
Value: "bar",
|
||||
},
|
||||
).Do()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
||||
defer cancel()
|
||||
|
||||
// Use client2 since client1 is site admin, so
|
||||
// we don't get good coverage on RBAC working.
|
||||
params, err := client2.UserAutofillParameters(
|
||||
ctx,
|
||||
u2.ID.String(),
|
||||
version.Template.ID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(params))
|
||||
|
||||
require.Equal(t, "param", params[0].Name)
|
||||
require.Equal(t, "foo", params[0].Value)
|
||||
|
||||
// Verify that latest parameter value is returned.
|
||||
dbfake.WorkspaceBuild(t, db, database.Workspace{
|
||||
OrganizationID: u1.OrganizationID,
|
||||
OwnerID: u2.ID,
|
||||
TemplateID: version.Template.ID,
|
||||
}).Params(
|
||||
database.WorkspaceBuildParameter{
|
||||
Name: "param",
|
||||
Value: "foo_new",
|
||||
},
|
||||
).Do()
|
||||
|
||||
params, err = client2.UserAutofillParameters(
|
||||
ctx,
|
||||
u2.ID.String(),
|
||||
version.Template.ID,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, 1, len(params))
|
||||
|
||||
require.Equal(t, "param", params[0].Name)
|
||||
require.Equal(t, "foo_new", params[0].Value)
|
||||
})
|
||||
}
|
||||
|
||||
// TestPaginatedUsers creates a list of users, then tries to paginate through
|
||||
// them using different page sizes.
|
||||
func TestPaginatedUsers(t *testing.T) {
|
||||
|
|
|
@ -3,29 +3,30 @@ package codersdk
|
|||
type RBACResource string
|
||||
|
||||
const (
|
||||
ResourceWorkspace RBACResource = "workspace"
|
||||
ResourceWorkspaceProxy RBACResource = "workspace_proxy"
|
||||
ResourceWorkspaceExecution RBACResource = "workspace_execution"
|
||||
ResourceWorkspaceApplicationConnect RBACResource = "application_connect"
|
||||
ResourceAuditLog RBACResource = "audit_log"
|
||||
ResourceTemplate RBACResource = "template"
|
||||
ResourceGroup RBACResource = "group"
|
||||
ResourceFile RBACResource = "file"
|
||||
ResourceProvisionerDaemon RBACResource = "provisioner_daemon"
|
||||
ResourceOrganization RBACResource = "organization"
|
||||
ResourceRoleAssignment RBACResource = "assign_role"
|
||||
ResourceOrgRoleAssignment RBACResource = "assign_org_role"
|
||||
ResourceAPIKey RBACResource = "api_key"
|
||||
ResourceUser RBACResource = "user"
|
||||
ResourceUserData RBACResource = "user_data"
|
||||
ResourceOrganizationMember RBACResource = "organization_member"
|
||||
ResourceLicense RBACResource = "license"
|
||||
ResourceDeploymentValues RBACResource = "deployment_config"
|
||||
ResourceDeploymentStats RBACResource = "deployment_stats"
|
||||
ResourceReplicas RBACResource = "replicas"
|
||||
ResourceDebugInfo RBACResource = "debug_info"
|
||||
ResourceSystem RBACResource = "system"
|
||||
ResourceTemplateInsights RBACResource = "template_insights"
|
||||
ResourceWorkspace RBACResource = "workspace"
|
||||
ResourceWorkspaceProxy RBACResource = "workspace_proxy"
|
||||
ResourceWorkspaceExecution RBACResource = "workspace_execution"
|
||||
ResourceWorkspaceApplicationConnect RBACResource = "application_connect"
|
||||
ResourceAuditLog RBACResource = "audit_log"
|
||||
ResourceTemplate RBACResource = "template"
|
||||
ResourceGroup RBACResource = "group"
|
||||
ResourceFile RBACResource = "file"
|
||||
ResourceProvisionerDaemon RBACResource = "provisioner_daemon"
|
||||
ResourceOrganization RBACResource = "organization"
|
||||
ResourceRoleAssignment RBACResource = "assign_role"
|
||||
ResourceOrgRoleAssignment RBACResource = "assign_org_role"
|
||||
ResourceAPIKey RBACResource = "api_key"
|
||||
ResourceUser RBACResource = "user"
|
||||
ResourceUserData RBACResource = "user_data"
|
||||
ResourceUserWorkspaceBuildParameters RBACResource = "user_workspace_build_parameters"
|
||||
ResourceOrganizationMember RBACResource = "organization_member"
|
||||
ResourceLicense RBACResource = "license"
|
||||
ResourceDeploymentValues RBACResource = "deployment_config"
|
||||
ResourceDeploymentStats RBACResource = "deployment_stats"
|
||||
ResourceReplicas RBACResource = "replicas"
|
||||
ResourceDebugInfo RBACResource = "debug_info"
|
||||
ResourceSystem RBACResource = "system"
|
||||
ResourceTemplateInsights RBACResource = "template_insights"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -52,6 +53,7 @@ var (
|
|||
ResourceAPIKey,
|
||||
ResourceUser,
|
||||
ResourceUserData,
|
||||
ResourceUserWorkspaceBuildParameters,
|
||||
ResourceOrganizationMember,
|
||||
ResourceLicense,
|
||||
ResourceDeploymentValues,
|
||||
|
|
|
@ -221,6 +221,27 @@ type OIDCAuthMethod struct {
|
|||
IconURL string `json:"iconUrl"`
|
||||
}
|
||||
|
||||
type UserParameter struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
// UserAutofillParameters returns all recently used parameters for the given user.
|
||||
func (c *Client) UserAutofillParameters(ctx context.Context, user string, templateID uuid.UUID) ([]UserParameter, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, fmt.Sprintf("/api/v2/users/%s/autofill-parameters?template_id=%s", user, templateID), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, ReadBodyAsError(res)
|
||||
}
|
||||
|
||||
var params []UserParameter
|
||||
return params, json.NewDecoder(res.Body).Decode(¶ms)
|
||||
}
|
||||
|
||||
// HasFirstUser returns whether the first user has been created.
|
||||
func (c *Client) HasFirstUser(ctx context.Context) (bool, error) {
|
||||
res, err := c.Request(ctx, http.MethodGet, "/api/v2/users/first", nil)
|
||||
|
|
|
@ -4213,31 +4213,32 @@ AuthorizationObject can represent a "set" of objects, such as: all workspaces in
|
|||
|
||||
#### Enumerated Values
|
||||
|
||||
| Value |
|
||||
| --------------------- |
|
||||
| `workspace` |
|
||||
| `workspace_proxy` |
|
||||
| `workspace_execution` |
|
||||
| `application_connect` |
|
||||
| `audit_log` |
|
||||
| `template` |
|
||||
| `group` |
|
||||
| `file` |
|
||||
| `provisioner_daemon` |
|
||||
| `organization` |
|
||||
| `assign_role` |
|
||||
| `assign_org_role` |
|
||||
| `api_key` |
|
||||
| `user` |
|
||||
| `user_data` |
|
||||
| `organization_member` |
|
||||
| `license` |
|
||||
| `deployment_config` |
|
||||
| `deployment_stats` |
|
||||
| `replicas` |
|
||||
| `debug_info` |
|
||||
| `system` |
|
||||
| `template_insights` |
|
||||
| Value |
|
||||
| --------------------------------- |
|
||||
| `workspace` |
|
||||
| `workspace_proxy` |
|
||||
| `workspace_execution` |
|
||||
| `application_connect` |
|
||||
| `audit_log` |
|
||||
| `template` |
|
||||
| `group` |
|
||||
| `file` |
|
||||
| `provisioner_daemon` |
|
||||
| `organization` |
|
||||
| `assign_role` |
|
||||
| `assign_org_role` |
|
||||
| `api_key` |
|
||||
| `user` |
|
||||
| `user_data` |
|
||||
| `user_workspace_build_parameters` |
|
||||
| `organization_member` |
|
||||
| `license` |
|
||||
| `deployment_config` |
|
||||
| `deployment_stats` |
|
||||
| `replicas` |
|
||||
| `debug_info` |
|
||||
| `system` |
|
||||
| `template_insights` |
|
||||
|
||||
## codersdk.RateLimitConfig
|
||||
|
||||
|
@ -5832,6 +5833,22 @@ If the schedule is empty, the user will be updated to use the default schedule.|
|
|||
| ------------ | ---------------------------------------- | -------- | ------------ | ----------- |
|
||||
| `login_type` | [codersdk.LoginType](#codersdklogintype) | false | | |
|
||||
|
||||
## codersdk.UserParameter
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "string",
|
||||
"value": "string"
|
||||
}
|
||||
```
|
||||
|
||||
### Properties
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| ------- | ------ | -------- | ------------ | ----------- |
|
||||
| `name` | string | false | | |
|
||||
| `value` | string | false | | |
|
||||
|
||||
## codersdk.UserQuietHoursScheduleConfig
|
||||
|
||||
```json
|
||||
|
|
|
@ -513,6 +513,57 @@ curl -X PUT http://coder-server:8080/api/v2/users/{user}/appearance \
|
|||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get autofill build parameters for user
|
||||
|
||||
### Code samples
|
||||
|
||||
```shell
|
||||
# Example request using curl
|
||||
curl -X GET http://coder-server:8080/api/v2/users/{user}/autofill-parameters?template_id=string \
|
||||
-H 'Accept: application/json' \
|
||||
-H 'Coder-Session-Token: API_KEY'
|
||||
```
|
||||
|
||||
`GET /users/{user}/autofill-parameters`
|
||||
|
||||
### Parameters
|
||||
|
||||
| Name | In | Type | Required | Description |
|
||||
| ------------- | ----- | ------ | -------- | ------------------------ |
|
||||
| `user` | path | string | true | User ID, username, or me |
|
||||
| `template_id` | query | string | true | Template ID |
|
||||
|
||||
### Example responses
|
||||
|
||||
> 200 Response
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "string",
|
||||
"value": "string"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### Responses
|
||||
|
||||
| Status | Meaning | Description | Schema |
|
||||
| ------ | ------------------------------------------------------- | ----------- | ------------------------------------------------------------------- |
|
||||
| 200 | [OK](https://tools.ietf.org/html/rfc7231#section-6.3.1) | OK | array of [codersdk.UserParameter](schemas.md#codersdkuserparameter) |
|
||||
|
||||
<h3 id="get-autofill-build-parameters-for-user-responseschema">Response Schema</h3>
|
||||
|
||||
Status Code **200**
|
||||
|
||||
| Name | Type | Required | Restrictions | Description |
|
||||
| -------------- | ------ | -------- | ------------ | ----------- |
|
||||
| `[array item]` | array | false | | |
|
||||
| `» name` | string | false | | |
|
||||
| `» value` | string | false | | |
|
||||
|
||||
To perform this operation, you must be authenticated. [Learn more](authentication.md).
|
||||
|
||||
## Get user Git SSH key
|
||||
|
||||
### Code samples
|
||||
|
|
|
@ -177,14 +177,14 @@
|
|||
"title": "Resource metadata",
|
||||
"description": "Show information in the workspace about template resources",
|
||||
"path": "./templates/resource-metadata.md"
|
||||
},
|
||||
{
|
||||
"title": "Parameters",
|
||||
"description": "Prompt the user for additional information about a workspace",
|
||||
"path": "./templates/parameters.md"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Parameters",
|
||||
"description": "Prompt the user for additional information about a workspace",
|
||||
"path": "./templates/parameters.md"
|
||||
},
|
||||
{
|
||||
"title": "Administering templates",
|
||||
"description": "Configuration settings for template admins",
|
||||
|
|
|
@ -281,3 +281,15 @@ variable "CLOUD_API_KEY" {
|
|||
}
|
||||
|
||||
```
|
||||
|
||||
## Create Autofill
|
||||
|
||||
When the template doesn't specify default values, Coder may still autofill
|
||||
parameters.
|
||||
|
||||
1. Coder will look for URL query parameters with form `param.<name>=<value>`.
|
||||
This feature enables platform teams to create pre-filled template creation
|
||||
links.
|
||||
2. Coder will populate recently used parameter key-value pairs for the user.
|
||||
This feature helps reduce repetition when filling common parameters such as
|
||||
`dotfiles_url` or `region`.
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
// Default port from the server
|
||||
export const defaultPort = 3000;
|
||||
export const prometheusPort = 2114;
|
||||
export const pprofPort = 6061;
|
||||
|
||||
// Use alternate ports in case we're running in a Coder Workspace.
|
||||
export const agentPProfPort = 6061;
|
||||
export const coderdPProfPort = 6062;
|
||||
|
||||
// Credentials for the first user
|
||||
export const username = "admin";
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
Resource,
|
||||
RichParameter,
|
||||
} from "./provisionerGenerated";
|
||||
import { prometheusPort, pprofPort } from "./constants";
|
||||
import { prometheusPort, agentPProfPort } from "./constants";
|
||||
import { port } from "./playwright.config";
|
||||
import * as ssh from "ssh2";
|
||||
import { Duplex } from "stream";
|
||||
|
@ -306,7 +306,7 @@ export const startAgentWithCommand = async (
|
|||
...process.env,
|
||||
CODER_AGENT_URL: "http://localhost:" + port,
|
||||
CODER_AGENT_TOKEN: token,
|
||||
CODER_AGENT_PPROF_ADDRESS: "127.0.0.1:" + pprofPort,
|
||||
CODER_AGENT_PPROF_ADDRESS: "127.0.0.1:" + agentPProfPort,
|
||||
CODER_AGENT_PROMETHEUS_ADDRESS: "127.0.0.1:" + prometheusPort,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -128,6 +128,14 @@ export const seventhParameter: RichParameter = {
|
|||
order: 1,
|
||||
};
|
||||
|
||||
// randParamName returns a new parameter with a random name.
|
||||
// It helps to avoid cross-test interference when user-auto-fill triggers on
|
||||
// the same parameter name.
|
||||
export const randParamName = (p: RichParameter): RichParameter => {
|
||||
const name = p.name + "_" + Math.random().toString(36).substring(7);
|
||||
return { ...p, name: name };
|
||||
};
|
||||
|
||||
// Build options
|
||||
|
||||
export const firstBuildOption: RichParameter = {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { defineConfig } from "@playwright/test";
|
||||
import path from "path";
|
||||
import { defaultPort, gitAuth } from "./constants";
|
||||
import { defaultPort, coderdPProfPort, gitAuth } from "./constants";
|
||||
|
||||
export const port = process.env.CODER_E2E_PORT
|
||||
? Number(process.env.CODER_E2E_PORT)
|
||||
|
@ -103,6 +103,7 @@ export default defineConfig({
|
|||
gitAuth.webPort,
|
||||
gitAuth.validatePath,
|
||||
),
|
||||
CODER_PPROF_ADDRESS: "127.0.0.1:" + coderdPProfPort,
|
||||
},
|
||||
reuseExistingServer: false,
|
||||
},
|
||||
|
|
|
@ -119,7 +119,7 @@ const filteredServerLogLines = (chunk: string): string[] =>
|
|||
|
||||
const exportDebugPprof = async (outputFile: string) => {
|
||||
const response = await axios.get(
|
||||
"http://127.0.0.1:6060/debug/pprof/goroutine?debug=1",
|
||||
"http://127.0.0.1:6062/debug/pprof/goroutine?debug=1",
|
||||
);
|
||||
if (response.status !== 200) {
|
||||
throw new Error(`Error: Received status code ${response.status}`);
|
||||
|
|
|
@ -14,6 +14,7 @@ import {
|
|||
thirdParameter,
|
||||
seventhParameter,
|
||||
sixthParameter,
|
||||
randParamName,
|
||||
} from "../parameters";
|
||||
import { RichParameter } from "../provisionerGenerated";
|
||||
import { beforeCoderTest } from "../hooks";
|
||||
|
@ -101,10 +102,16 @@ test("create workspace with default and required parameters", async ({
|
|||
});
|
||||
|
||||
test("create workspace and overwrite default parameters", async ({ page }) => {
|
||||
const richParameters: RichParameter[] = [secondParameter, fourthParameter];
|
||||
// We use randParamName to prevent the new values from corrupting user_history
|
||||
// and thus affecting other tests.
|
||||
const richParameters: RichParameter[] = [
|
||||
randParamName(secondParameter),
|
||||
randParamName(fourthParameter),
|
||||
];
|
||||
|
||||
const buildParameters = [
|
||||
{ name: secondParameter.name, value: "AAAAA" },
|
||||
{ name: fourthParameter.name, value: "false" },
|
||||
{ name: richParameters[0].name, value: "AAAAA" },
|
||||
{ name: richParameters[1].name, value: "false" },
|
||||
];
|
||||
const template = await createTemplate(
|
||||
page,
|
||||
|
|
|
@ -23,14 +23,14 @@ test("restart workspace with ephemeral parameters", async ({ page }) => {
|
|||
|
||||
// Verify that build options are default (not selected).
|
||||
await verifyParameters(page, workspaceName, richParameters, [
|
||||
{ name: firstBuildOption.name, value: firstBuildOption.defaultValue },
|
||||
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
||||
{ name: richParameters[0].name, value: firstBuildOption.defaultValue },
|
||||
{ name: richParameters[1].name, value: secondBuildOption.defaultValue },
|
||||
]);
|
||||
|
||||
// Now, restart the workspace with ephemeral parameters selected.
|
||||
const buildParameters = [
|
||||
{ name: firstBuildOption.name, value: "AAAAA" },
|
||||
{ name: secondBuildOption.name, value: "true" },
|
||||
{ name: richParameters[0].name, value: "AAAAA" },
|
||||
{ name: richParameters[1].name, value: "true" },
|
||||
];
|
||||
await buildWorkspaceWithParameters(
|
||||
page,
|
||||
|
@ -42,7 +42,7 @@ test("restart workspace with ephemeral parameters", async ({ page }) => {
|
|||
|
||||
// Verify that build options are default (not selected).
|
||||
await verifyParameters(page, workspaceName, richParameters, [
|
||||
{ name: firstBuildOption.name, value: firstBuildOption.defaultValue },
|
||||
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
||||
{ name: richParameters[0].name, value: firstBuildOption.defaultValue },
|
||||
{ name: richParameters[1].name, value: secondBuildOption.defaultValue },
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -21,8 +21,8 @@ test("start workspace with ephemeral parameters", async ({ page }) => {
|
|||
|
||||
// Verify that build options are default (not selected).
|
||||
await verifyParameters(page, workspaceName, richParameters, [
|
||||
{ name: firstBuildOption.name, value: firstBuildOption.defaultValue },
|
||||
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
||||
{ name: richParameters[0].name, value: firstBuildOption.defaultValue },
|
||||
{ name: richParameters[1].name, value: secondBuildOption.defaultValue },
|
||||
]);
|
||||
|
||||
// Stop the workspace
|
||||
|
@ -30,8 +30,8 @@ test("start workspace with ephemeral parameters", async ({ page }) => {
|
|||
|
||||
// Now, start the workspace with ephemeral parameters selected.
|
||||
const buildParameters = [
|
||||
{ name: firstBuildOption.name, value: "AAAAA" },
|
||||
{ name: secondBuildOption.name, value: "true" },
|
||||
{ name: richParameters[0].name, value: "AAAAA" },
|
||||
{ name: richParameters[1].name, value: "true" },
|
||||
];
|
||||
|
||||
await buildWorkspaceWithParameters(
|
||||
|
@ -43,7 +43,7 @@ test("start workspace with ephemeral parameters", async ({ page }) => {
|
|||
|
||||
// Verify that build options are default (not selected).
|
||||
await verifyParameters(page, workspaceName, richParameters, [
|
||||
{ name: firstBuildOption.name, value: firstBuildOption.defaultValue },
|
||||
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
||||
{ name: richParameters[0].name, value: firstBuildOption.defaultValue },
|
||||
{ name: richParameters[1].name, value: secondBuildOption.defaultValue },
|
||||
]);
|
||||
});
|
||||
|
|
|
@ -129,6 +129,13 @@ export const getAuthenticatedUser = async () => {
|
|||
return response.data;
|
||||
};
|
||||
|
||||
export const getUserParameters = async (templateID: string) => {
|
||||
const response = await axios.get<TypesGen.UserParameter[]>(
|
||||
"/api/v2/users/me/autofill-parameters?template_id=" + templateID,
|
||||
);
|
||||
return response.data;
|
||||
};
|
||||
|
||||
export const getAuthMethods = async (): Promise<TypesGen.AuthMethods> => {
|
||||
const response = await axios.get<TypesGen.AuthMethods>(
|
||||
"/api/v2/users/authmethods",
|
||||
|
|
|
@ -1435,6 +1435,12 @@ export interface UserLoginType {
|
|||
readonly login_type: LoginType;
|
||||
}
|
||||
|
||||
// From codersdk/users.go
|
||||
export interface UserParameter {
|
||||
readonly name: string;
|
||||
readonly value: string;
|
||||
}
|
||||
|
||||
// From codersdk/deployment.go
|
||||
export interface UserQuietHoursScheduleConfig {
|
||||
readonly default_schedule: string;
|
||||
|
@ -2011,6 +2017,7 @@ export type RBACResource =
|
|||
| "template_insights"
|
||||
| "user"
|
||||
| "user_data"
|
||||
| "user_workspace_build_parameters"
|
||||
| "workspace"
|
||||
| "workspace_execution"
|
||||
| "workspace_proxy";
|
||||
|
@ -2035,6 +2042,7 @@ export const RBACResources: RBACResource[] = [
|
|||
"template_insights",
|
||||
"user",
|
||||
"user_data",
|
||||
"user_workspace_build_parameters",
|
||||
"workspace",
|
||||
"workspace_execution",
|
||||
"workspace_proxy",
|
||||
|
|
|
@ -10,6 +10,7 @@ import { MemoizedMarkdown } from "components/Markdown/Markdown";
|
|||
import { Stack } from "components/Stack/Stack";
|
||||
import { MultiTextField } from "./MultiTextField";
|
||||
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
||||
import { AutofillSource } from "utils/richParameters";
|
||||
import { Pill } from "components/Pill/Pill";
|
||||
import ErrorOutline from "@mui/icons-material/ErrorOutline";
|
||||
|
||||
|
@ -167,6 +168,7 @@ export type RichParameterInputProps = Omit<
|
|||
"size" | "onChange"
|
||||
> & {
|
||||
parameter: TemplateVersionParameter;
|
||||
autofillSource?: AutofillSource;
|
||||
onChange: (value: string) => void;
|
||||
size?: Size;
|
||||
};
|
||||
|
@ -174,6 +176,7 @@ export type RichParameterInputProps = Omit<
|
|||
export const RichParameterInput: FC<RichParameterInputProps> = ({
|
||||
parameter,
|
||||
size = "medium",
|
||||
autofillSource,
|
||||
...fieldProps
|
||||
}) => {
|
||||
return (
|
||||
|
@ -186,6 +189,17 @@ export const RichParameterInput: FC<RichParameterInputProps> = ({
|
|||
<ParameterLabel parameter={parameter} />
|
||||
<div css={{ display: "flex", flexDirection: "column" }}>
|
||||
<RichParameterField {...fieldProps} size={size} parameter={parameter} />
|
||||
{autofillSource && autofillSource !== "active_build" && (
|
||||
<div css={{ marginTop: 4, fontSize: 12 }}>
|
||||
🪄 Autofilled:{" "}
|
||||
{
|
||||
{
|
||||
["url"]: "value supplied by URL.",
|
||||
["user_history"]: "recently used value.",
|
||||
}[autofillSource]
|
||||
}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Stack>
|
||||
);
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
import { TemplateVersionParameter } from "api/typesGenerated";
|
||||
import { FormSection, FormFields } from "components/Form/Form";
|
||||
import { FormFields, FormSection } from "components/Form/Form";
|
||||
import {
|
||||
RichParameterInput,
|
||||
RichParameterInputProps,
|
||||
} from "components/RichParameterInput/RichParameterInput";
|
||||
import { ComponentProps, FC } from "react";
|
||||
import { AutofillSource } from "utils/richParameters";
|
||||
|
||||
export type TemplateParametersSectionProps = {
|
||||
templateParameters: TemplateVersionParameter[];
|
||||
autofillSources?: Record<string, AutofillSource>;
|
||||
getInputProps: (
|
||||
parameter: TemplateVersionParameter,
|
||||
index: number,
|
||||
|
@ -17,6 +19,7 @@ export type TemplateParametersSectionProps = {
|
|||
export const TemplateParametersSection: FC<TemplateParametersSectionProps> = ({
|
||||
templateParameters,
|
||||
getInputProps,
|
||||
autofillSources,
|
||||
...formSectionProps
|
||||
}) => {
|
||||
const hasMutableParameters =
|
||||
|
@ -38,6 +41,9 @@ export const TemplateParametersSection: FC<TemplateParametersSectionProps> = ({
|
|||
{...getInputProps(parameter, index)}
|
||||
key={parameter.name}
|
||||
parameter={parameter}
|
||||
autofillSource={
|
||||
autofillSources && autofillSources[parameter.name]
|
||||
}
|
||||
/>
|
||||
),
|
||||
)}
|
||||
|
|
|
@ -176,7 +176,9 @@ describe("CreateWorkspacePage", () => {
|
|||
"me",
|
||||
expect.objectContaining({
|
||||
template_id: MockTemplate.id,
|
||||
rich_parameter_values: [{ name: param, value: paramValue }],
|
||||
rich_parameter_values: [
|
||||
expect.objectContaining({ name: param, value: paramValue }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
@ -201,7 +203,9 @@ describe("CreateWorkspacePage", () => {
|
|||
"me",
|
||||
expect.objectContaining({
|
||||
template_version_id: MockTemplate.active_version_id,
|
||||
rich_parameter_values: [{ name: param, value: paramValue }],
|
||||
rich_parameter_values: [
|
||||
expect.objectContaining({ name: param, value: paramValue }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,34 +1,36 @@
|
|||
import { type FC, useCallback, useState, useEffect, useMemo } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import {
|
||||
uniqueNamesGenerator,
|
||||
animals,
|
||||
colors,
|
||||
NumberDictionary,
|
||||
} from "unique-names-generator";
|
||||
import type {
|
||||
TemplateVersionParameter,
|
||||
Workspace,
|
||||
WorkspaceBuildParameter,
|
||||
} from "api/typesGenerated";
|
||||
import { useMe } from "contexts/auth/useMe";
|
||||
import { useOrganizationId } from "contexts/auth/useOrganizationId";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { getUserParameters } from "api/api";
|
||||
import { checkAuthorization } from "api/queries/authCheck";
|
||||
import {
|
||||
richParameters,
|
||||
templateByName,
|
||||
templateVersionExternalAuth,
|
||||
richParameters,
|
||||
} from "api/queries/templates";
|
||||
import { autoCreateWorkspace, createWorkspace } from "api/queries/workspaces";
|
||||
import { useEffectEvent } from "hooks/hookPolyfills";
|
||||
import { paramsUsedToCreateWorkspace } from "utils/workspace";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import {
|
||||
TemplateVersionParameter,
|
||||
UserParameter,
|
||||
Workspace,
|
||||
} from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { CreateWSPermissions, createWorkspaceChecks } from "./permissions";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import { useMe } from "contexts/auth/useMe";
|
||||
import { useOrganizationId } from "contexts/auth/useOrganizationId";
|
||||
import { useEffectEvent } from "hooks/hookPolyfills";
|
||||
import { useCallback, useEffect, useMemo, useState, type FC } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { useMutation, useQuery, useQueryClient } from "react-query";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
NumberDictionary,
|
||||
animals,
|
||||
colors,
|
||||
uniqueNamesGenerator,
|
||||
} from "unique-names-generator";
|
||||
import { pageTitle } from "utils/page";
|
||||
import { AutofillBuildParameter } from "utils/richParameters";
|
||||
import { paramsUsedToCreateWorkspace } from "utils/workspace";
|
||||
import { CreateWorkspacePageView } from "./CreateWorkspacePageView";
|
||||
import { CreateWSPermissions, createWorkspaceChecks } from "./permissions";
|
||||
|
||||
export const createWorkspaceModes = ["form", "auto", "duplicate"] as const;
|
||||
export type CreateWorkspaceMode = (typeof createWorkspaceModes)[number];
|
||||
|
@ -41,7 +43,6 @@ const CreateWorkspacePage: FC = () => {
|
|||
const me = useMe();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const defaultBuildParameters = getDefaultBuildParameters(searchParams);
|
||||
const mode = getWorkspaceMode(searchParams);
|
||||
const customVersionId = searchParams.get("version") ?? undefined;
|
||||
|
||||
|
@ -61,6 +62,15 @@ const CreateWorkspacePage: FC = () => {
|
|||
const createWorkspaceMutation = useMutation(createWorkspace(queryClient));
|
||||
|
||||
const templateQuery = useQuery(templateByName(organizationId, templateName));
|
||||
|
||||
const userParametersQuery = useQuery(
|
||||
["userParameters"],
|
||||
() => getUserParameters(templateQuery.data!.id),
|
||||
{
|
||||
enabled: templateQuery.isSuccess,
|
||||
},
|
||||
);
|
||||
|
||||
const permissionsQuery = useQuery(
|
||||
checkAuthorization({
|
||||
checks: createWorkspaceChecks(organizationId),
|
||||
|
@ -101,12 +111,17 @@ const CreateWorkspacePage: FC = () => {
|
|||
[navigate],
|
||||
);
|
||||
|
||||
const autofillParameters = getAutofillParameters(
|
||||
searchParams,
|
||||
userParametersQuery.data ? userParametersQuery.data : [],
|
||||
);
|
||||
|
||||
const automateWorkspaceCreation = useEffectEvent(async () => {
|
||||
try {
|
||||
const newWorkspace = await autoCreateWorkspaceMutation.mutateAsync({
|
||||
templateName,
|
||||
organizationId,
|
||||
defaultBuildParameters,
|
||||
defaultBuildParameters: autofillParameters,
|
||||
defaultName,
|
||||
versionId: realizedVersionId,
|
||||
});
|
||||
|
@ -139,7 +154,7 @@ const CreateWorkspacePage: FC = () => {
|
|||
mode={mode}
|
||||
defaultName={defaultName}
|
||||
defaultOwner={me}
|
||||
defaultBuildParameters={defaultBuildParameters}
|
||||
autofillParameters={autofillParameters}
|
||||
error={createWorkspaceMutation.error}
|
||||
resetMutation={createWorkspaceMutation.reset}
|
||||
template={templateQuery.data!}
|
||||
|
@ -223,17 +238,34 @@ const useExternalAuth = (versionId: string | undefined) => {
|
|||
};
|
||||
};
|
||||
|
||||
const getDefaultBuildParameters = (
|
||||
const getAutofillParameters = (
|
||||
urlSearchParams: URLSearchParams,
|
||||
): WorkspaceBuildParameter[] => {
|
||||
const buildValues: WorkspaceBuildParameter[] = [];
|
||||
Array.from(urlSearchParams.keys())
|
||||
userParameters: UserParameter[],
|
||||
): AutofillBuildParameter[] => {
|
||||
const userParamMap = userParameters.reduce((acc, param) => {
|
||||
acc.set(param.name, param);
|
||||
return acc;
|
||||
}, new Map<string, UserParameter>());
|
||||
|
||||
const buildValues: AutofillBuildParameter[] = Array.from(
|
||||
urlSearchParams.keys(),
|
||||
)
|
||||
.filter((key) => key.startsWith("param."))
|
||||
.forEach((key) => {
|
||||
.map((key) => {
|
||||
const name = key.replace("param.", "");
|
||||
const value = urlSearchParams.get(key) ?? "";
|
||||
buildValues.push({ name, value });
|
||||
// URL should take precedence over user parameters
|
||||
userParamMap.delete(name);
|
||||
return { name, value, source: "url" };
|
||||
});
|
||||
|
||||
userParamMap.forEach((param) => {
|
||||
buildValues.push({
|
||||
name: param.name,
|
||||
value: param.value,
|
||||
source: "user_history",
|
||||
});
|
||||
});
|
||||
return buildValues;
|
||||
};
|
||||
|
||||
|
|
|
@ -17,7 +17,7 @@ const meta: Meta<typeof CreateWorkspacePageView> = {
|
|||
args: {
|
||||
defaultName: "",
|
||||
defaultOwner: MockUser,
|
||||
defaultBuildParameters: [],
|
||||
autofillParameters: [],
|
||||
template: MockTemplate,
|
||||
parameters: [],
|
||||
externalAuth: [],
|
||||
|
@ -86,6 +86,13 @@ export const Parameters: Story = {
|
|||
ephemeral: false,
|
||||
},
|
||||
],
|
||||
autofillParameters: [
|
||||
{
|
||||
name: "first_parameter",
|
||||
value: "It works!",
|
||||
source: "user_history",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import TextField from "@mui/material/TextField";
|
|||
import type * as TypesGen from "api/typesGenerated";
|
||||
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
|
||||
import { FormikContextType, useFormik } from "formik";
|
||||
import { type FC, useEffect, useState } from "react";
|
||||
import { type FC, useEffect, useState, useMemo } from "react";
|
||||
import {
|
||||
getFormHelpers,
|
||||
nameValidator,
|
||||
|
@ -17,6 +17,8 @@ import {
|
|||
HorizontalForm,
|
||||
} from "components/Form/Form";
|
||||
import {
|
||||
AutofillBuildParameter,
|
||||
AutofillSource,
|
||||
getInitialRichParameterValues,
|
||||
useValidationSchemaForRichParameters,
|
||||
} from "utils/richParameters";
|
||||
|
@ -58,7 +60,7 @@ export interface CreateWorkspacePageViewProps {
|
|||
externalAuthPollingState: ExternalAuthPollingState;
|
||||
startPollingExternalAuth: () => void;
|
||||
parameters: TypesGen.TemplateVersionParameter[];
|
||||
defaultBuildParameters: TypesGen.WorkspaceBuildParameter[];
|
||||
autofillParameters: AutofillBuildParameter[];
|
||||
permissions: CreateWSPermissions;
|
||||
creatingWorkspace: boolean;
|
||||
onCancel: () => void;
|
||||
|
@ -80,7 +82,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
|||
externalAuthPollingState,
|
||||
startPollingExternalAuth,
|
||||
parameters,
|
||||
defaultBuildParameters,
|
||||
autofillParameters,
|
||||
permissions,
|
||||
creatingWorkspace,
|
||||
onSubmit,
|
||||
|
@ -98,7 +100,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
|||
template_id: template.id,
|
||||
rich_parameter_values: getInitialRichParameterValues(
|
||||
parameters,
|
||||
defaultBuildParameters,
|
||||
autofillParameters,
|
||||
),
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
|
@ -126,6 +128,16 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
|||
error,
|
||||
);
|
||||
|
||||
const autofillSources = useMemo(() => {
|
||||
return autofillParameters.reduce(
|
||||
(acc, param) => {
|
||||
acc[param.name] = param.source;
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, AutofillSource>,
|
||||
);
|
||||
}, [autofillParameters]);
|
||||
|
||||
return (
|
||||
<Margins size="medium">
|
||||
<PageHeader actions={<Button onClick={onCancel}>Cancel</Button>}>
|
||||
|
@ -244,6 +256,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
|||
value,
|
||||
});
|
||||
}}
|
||||
autofillSource={autofillSources[parameter.name]}
|
||||
key={parameter.name}
|
||||
parameter={parameter}
|
||||
disabled={isDisabled}
|
||||
|
|
|
@ -21,7 +21,10 @@ import {
|
|||
import { useFormik } from "formik";
|
||||
import { docs } from "utils/docs";
|
||||
import { getFormHelpers } from "utils/formUtils";
|
||||
import { getInitialRichParameterValues } from "utils/richParameters";
|
||||
import {
|
||||
AutofillBuildParameter,
|
||||
getInitialRichParameterValues,
|
||||
} from "utils/richParameters";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
|
@ -113,7 +116,12 @@ const BuildParametersPopoverContent: FC<BuildParametersPopoverContentProps> = ({
|
|||
popover.setIsOpen(false);
|
||||
}}
|
||||
ephemeralParameters={ephemeralParameters}
|
||||
buildParameters={buildParameters}
|
||||
buildParameters={buildParameters.map(
|
||||
(p): AutofillBuildParameter => ({
|
||||
...p,
|
||||
source: "active_build",
|
||||
}),
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
|
@ -147,7 +155,7 @@ const BuildParametersPopoverContent: FC<BuildParametersPopoverContentProps> = ({
|
|||
|
||||
interface FormProps {
|
||||
ephemeralParameters: TemplateVersionParameter[];
|
||||
buildParameters: WorkspaceBuildParameter[];
|
||||
buildParameters: AutofillBuildParameter[];
|
||||
onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void;
|
||||
}
|
||||
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
} from "components/Form/Form";
|
||||
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput";
|
||||
import {
|
||||
AutofillBuildParameter,
|
||||
getInitialRichParameterValues,
|
||||
useValidationSchemaForRichParameters,
|
||||
} from "utils/richParameters";
|
||||
|
@ -27,7 +28,7 @@ export type WorkspaceParametersFormValues = {
|
|||
interface WorkspaceParameterFormProps {
|
||||
workspace: Workspace;
|
||||
templateVersionRichParameters: TemplateVersionParameter[];
|
||||
buildParameters: WorkspaceBuildParameter[];
|
||||
autofillParams: AutofillBuildParameter[];
|
||||
isSubmitting: boolean;
|
||||
canChangeVersions: boolean;
|
||||
error: unknown;
|
||||
|
@ -40,7 +41,7 @@ export const WorkspaceParametersForm: FC<WorkspaceParameterFormProps> = ({
|
|||
onCancel,
|
||||
onSubmit,
|
||||
templateVersionRichParameters,
|
||||
buildParameters,
|
||||
autofillParams,
|
||||
error,
|
||||
canChangeVersions,
|
||||
isSubmitting,
|
||||
|
@ -50,7 +51,7 @@ export const WorkspaceParametersForm: FC<WorkspaceParameterFormProps> = ({
|
|||
initialValues: {
|
||||
rich_parameter_values: getInitialRichParameterValues(
|
||||
templateVersionRichParameters,
|
||||
buildParameters,
|
||||
autofillParams,
|
||||
),
|
||||
},
|
||||
validationSchema: Yup.object({
|
||||
|
|
|
@ -24,6 +24,7 @@ import { EmptyState } from "components/EmptyState/EmptyState";
|
|||
import Button from "@mui/material/Button";
|
||||
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined";
|
||||
import { docs } from "utils/docs";
|
||||
import { AutofillBuildParameter } from "utils/richParameters";
|
||||
|
||||
const WorkspaceParametersPage: FC = () => {
|
||||
const workspace = useWorkspaceSettings();
|
||||
|
@ -126,7 +127,12 @@ export const WorkspaceParametersPageView: FC<
|
|||
<WorkspaceParametersForm
|
||||
workspace={workspace}
|
||||
canChangeVersions={canChangeVersions}
|
||||
buildParameters={data.buildParameters}
|
||||
autofillParams={data.buildParameters.map(
|
||||
(p): AutofillBuildParameter => ({
|
||||
...p,
|
||||
source: "active_build",
|
||||
}),
|
||||
)}
|
||||
templateVersionRichParameters={data.templateVersionRichParameters}
|
||||
error={submitError}
|
||||
isSubmitting={isSubmitting}
|
||||
|
|
|
@ -46,7 +46,7 @@ test("getInitialRichParameterValues return default value when default build para
|
|||
const cpuParameter = templateParameters[0];
|
||||
const [cpuParameterInitialValue] = getInitialRichParameterValues(
|
||||
templateParameters,
|
||||
[{ name: cpuParameter.name, value: "100" }],
|
||||
[{ name: cpuParameter.name, value: "100", source: "user_history" }],
|
||||
);
|
||||
|
||||
expect(cpuParameterInitialValue.value).toBe(cpuParameter.default_value);
|
||||
|
|
|
@ -4,25 +4,39 @@ import {
|
|||
} from "api/typesGenerated";
|
||||
import * as Yup from "yup";
|
||||
|
||||
export type AutofillSource = "user_history" | "url" | "active_build";
|
||||
|
||||
// AutofillBuildParameter is a build parameter destined to a form, alongside
|
||||
// its source so that the form can explain where the value comes from.
|
||||
export type AutofillBuildParameter = {
|
||||
source: AutofillSource;
|
||||
} & WorkspaceBuildParameter;
|
||||
|
||||
export const getInitialRichParameterValues = (
|
||||
templateParameters: TemplateVersionParameter[],
|
||||
buildParameters?: WorkspaceBuildParameter[],
|
||||
templateParams: TemplateVersionParameter[],
|
||||
autofillParams?: AutofillBuildParameter[],
|
||||
): WorkspaceBuildParameter[] => {
|
||||
return templateParameters.map((parameter) => {
|
||||
const existentBuildParameter = buildParameters?.find(
|
||||
(p) => p.name === parameter.name,
|
||||
);
|
||||
const shouldReturnTheDefaultValue =
|
||||
!existentBuildParameter ||
|
||||
!isValidValue(parameter, existentBuildParameter) ||
|
||||
parameter.ephemeral;
|
||||
if (shouldReturnTheDefaultValue) {
|
||||
return templateParams.map((parameter) => {
|
||||
// Short-circuit for ephemeral parameters, which are always reset to
|
||||
// the template-defined default.
|
||||
if (parameter.ephemeral) {
|
||||
return {
|
||||
name: parameter.name,
|
||||
value: parameter.default_value,
|
||||
};
|
||||
}
|
||||
return existentBuildParameter;
|
||||
|
||||
const autofillParam = autofillParams?.find(
|
||||
({ name }) => name === parameter.name,
|
||||
);
|
||||
|
||||
return {
|
||||
name: parameter.name,
|
||||
value:
|
||||
autofillParam && isValidValue(parameter, autofillParam)
|
||||
? autofillParam.value
|
||||
: parameter.default_value,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
|
|
Loading…
Reference in New Issue