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": {
|
"/users/{user}/convert-login": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
|
@ -10493,6 +10537,7 @@ const docTemplate = `{
|
||||||
"api_key",
|
"api_key",
|
||||||
"user",
|
"user",
|
||||||
"user_data",
|
"user_data",
|
||||||
|
"user_workspace_build_parameters",
|
||||||
"organization_member",
|
"organization_member",
|
||||||
"license",
|
"license",
|
||||||
"deployment_config",
|
"deployment_config",
|
||||||
|
@ -10518,6 +10563,7 @@ const docTemplate = `{
|
||||||
"ResourceAPIKey",
|
"ResourceAPIKey",
|
||||||
"ResourceUser",
|
"ResourceUser",
|
||||||
"ResourceUserData",
|
"ResourceUserData",
|
||||||
|
"ResourceUserWorkspaceBuildParameters",
|
||||||
"ResourceOrganizationMember",
|
"ResourceOrganizationMember",
|
||||||
"ResourceLicense",
|
"ResourceLicense",
|
||||||
"ResourceDeploymentValues",
|
"ResourceDeploymentValues",
|
||||||
|
@ -11923,6 +11969,17 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.UserParameter": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.UserQuietHoursScheduleConfig": {
|
"codersdk.UserQuietHoursScheduleConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"/users/{user}/convert-login": {
|
||||||
"post": {
|
"post": {
|
||||||
"security": [
|
"security": [
|
||||||
|
@ -9439,6 +9479,7 @@
|
||||||
"api_key",
|
"api_key",
|
||||||
"user",
|
"user",
|
||||||
"user_data",
|
"user_data",
|
||||||
|
"user_workspace_build_parameters",
|
||||||
"organization_member",
|
"organization_member",
|
||||||
"license",
|
"license",
|
||||||
"deployment_config",
|
"deployment_config",
|
||||||
|
@ -9464,6 +9505,7 @@
|
||||||
"ResourceAPIKey",
|
"ResourceAPIKey",
|
||||||
"ResourceUser",
|
"ResourceUser",
|
||||||
"ResourceUserData",
|
"ResourceUserData",
|
||||||
|
"ResourceUserWorkspaceBuildParameters",
|
||||||
"ResourceOrganizationMember",
|
"ResourceOrganizationMember",
|
||||||
"ResourceLicense",
|
"ResourceLicense",
|
||||||
"ResourceDeploymentValues",
|
"ResourceDeploymentValues",
|
||||||
|
@ -10810,6 +10852,17 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"codersdk.UserParameter": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"codersdk.UserQuietHoursScheduleConfig": {
|
"codersdk.UserQuietHoursScheduleConfig": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
|
|
@ -821,6 +821,7 @@ func New(options *Options) *API {
|
||||||
r.Post("/convert-login", api.postConvertLoginType)
|
r.Post("/convert-login", api.postConvertLoginType)
|
||||||
r.Delete("/", api.deleteUser)
|
r.Delete("/", api.deleteUser)
|
||||||
r.Get("/", api.userByName)
|
r.Get("/", api.userByName)
|
||||||
|
r.Get("/autofill-parameters", api.userAutofillParameters)
|
||||||
r.Get("/login-type", api.userLoginType)
|
r.Get("/login-type", api.userLoginType)
|
||||||
r.Put("/profile", api.putUserProfile)
|
r.Put("/profile", api.putUserProfile)
|
||||||
r.Route("/status", func(r chi.Router) {
|
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)
|
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) {
|
func (q *querier) GetUsers(ctx context.Context, arg database.GetUsersParams) ([]database.GetUsersRow, error) {
|
||||||
// This does the filtering in SQL.
|
// This does the filtering in SQL.
|
||||||
prep, err := prepareSQLFilter(ctx, q.auth, rbac.ActionRead, rbac.ResourceUser.Type)
|
prep, err := prepareSQLFilter(ctx, q.auth, rbac.ActionRead, rbac.ResourceUser.Type)
|
||||||
|
|
|
@ -1052,6 +1052,17 @@ func (s *MethodTestSuite) TestUser() {
|
||||||
UpdatedAt: u.UpdatedAt,
|
UpdatedAt: u.UpdatedAt,
|
||||||
}).Asserts(u.UserDataRBACObject(), rbac.ActionUpdate).Returns(u)
|
}).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) {
|
s.Run("UpdateUserAppearanceSettings", s.Subtest(func(db database.Store, check *expects) {
|
||||||
u := dbgen.User(s.T(), db, database.User{})
|
u := dbgen.User(s.T(), db, database.User{})
|
||||||
check.Args(database.UpdateUserAppearanceSettingsParams{
|
check.Args(database.UpdateUserAppearanceSettingsParams{
|
||||||
|
|
|
@ -3797,6 +3797,65 @@ func (q *FakeQuerier) GetUserLinksByUserID(_ context.Context, userID uuid.UUID)
|
||||||
return uls, nil
|
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) {
|
func (q *FakeQuerier) GetUsers(_ context.Context, params database.GetUsersParams) ([]database.GetUsersRow, error) {
|
||||||
if err := validateDatabaseType(params); err != nil {
|
if err := validateDatabaseType(params); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|
|
@ -1012,6 +1012,13 @@ func (m metricsStore) GetUserLinksByUserID(ctx context.Context, userID uuid.UUID
|
||||||
return r0, r1
|
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) {
|
func (m metricsStore) GetUsers(ctx context.Context, arg database.GetUsersParams) ([]database.GetUsersRow, error) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
users, err := m.s.GetUsers(ctx, arg)
|
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)
|
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.
|
// GetUsers mocks base method.
|
||||||
func (m *MockStore) GetUsers(arg0 context.Context, arg1 database.GetUsersParams) ([]database.GetUsersRow, error) {
|
func (m *MockStore) GetUsers(arg0 context.Context, arg1 database.GetUsersParams) ([]database.GetUsersRow, error) {
|
||||||
m.ctrl.T.Helper()
|
m.ctrl.T.Helper()
|
||||||
|
|
|
@ -256,6 +256,10 @@ func (u User) UserDataRBACObject() rbac.Object {
|
||||||
return rbac.ResourceUserData.WithID(u.ID).WithOwner(u.ID.String())
|
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 {
|
func (u GetUsersRow) RBACObject() rbac.Object {
|
||||||
return rbac.ResourceUserObject(u.ID)
|
return rbac.ResourceUserObject(u.ID)
|
||||||
}
|
}
|
||||||
|
|
|
@ -211,6 +211,7 @@ type sqlcQuerier interface {
|
||||||
GetUserLinkByLinkedID(ctx context.Context, linkedID string) (UserLink, error)
|
GetUserLinkByLinkedID(ctx context.Context, linkedID string) (UserLink, error)
|
||||||
GetUserLinkByUserIDLoginType(ctx context.Context, arg GetUserLinkByUserIDLoginTypeParams) (UserLink, error)
|
GetUserLinkByUserIDLoginType(ctx context.Context, arg GetUserLinkByUserIDLoginTypeParams) (UserLink, error)
|
||||||
GetUserLinksByUserID(ctx context.Context, userID uuid.UUID) ([]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.
|
// This will never return deleted users.
|
||||||
GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error)
|
GetUsers(ctx context.Context, arg GetUsersParams) ([]GetUsersRow, error)
|
||||||
// This shouldn't check for deleted, because it's frequently used
|
// 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
|
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
|
const getWorkspaceBuildParameters = `-- name: GetWorkspaceBuildParameters :many
|
||||||
SELECT
|
SELECT
|
||||||
workspace_build_id, name, value
|
workspace_build_id, name, value
|
||||||
|
|
|
@ -14,3 +14,27 @@ FROM
|
||||||
workspace_build_parameters
|
workspace_build_parameters
|
||||||
WHERE
|
WHERE
|
||||||
workspace_build_id = $1;
|
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",
|
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.
|
// ResourceOrganizationMember is a user's membership in an organization.
|
||||||
// Has ONLY an organization owner.
|
// Has ONLY an organization owner.
|
||||||
// create/delete = Create/delete member from org.
|
// create/delete = Create/delete member from org.
|
||||||
|
|
|
@ -25,6 +25,7 @@ func AllResources() []Object {
|
||||||
ResourceTemplateInsights,
|
ResourceTemplateInsights,
|
||||||
ResourceUser,
|
ResourceUser,
|
||||||
ResourceUserData,
|
ResourceUserData,
|
||||||
|
ResourceUserWorkspaceBuildParameters,
|
||||||
ResourceWildcard,
|
ResourceWildcard,
|
||||||
ResourceWorkspace,
|
ResourceWorkspace,
|
||||||
ResourceWorkspaceApplicationConnect,
|
ResourceWorkspaceApplicationConnect,
|
||||||
|
|
|
@ -154,7 +154,8 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||||
Permissions(map[string][]Action{
|
Permissions(map[string][]Action{
|
||||||
// Users cannot do create/update/delete on themselves, but they
|
// Users cannot do create/update/delete on themselves, but they
|
||||||
// can read their own details.
|
// can read their own details.
|
||||||
ResourceUser.Type: {ActionRead},
|
ResourceUser.Type: {ActionRead},
|
||||||
|
ResourceUserWorkspaceBuildParameters.Type: {ActionRead},
|
||||||
// Users can create provisioner daemons scoped to themselves.
|
// Users can create provisioner daemons scoped to themselves.
|
||||||
ResourceProvisionerDaemon.Type: {ActionCreate, ActionRead, ActionUpdate},
|
ResourceProvisionerDaemon.Type: {ActionCreate, ActionRead, ActionUpdate},
|
||||||
})...,
|
})...,
|
||||||
|
@ -209,9 +210,10 @@ func ReloadBuiltinRoles(opts *RoleOptions) {
|
||||||
Name: userAdmin,
|
Name: userAdmin,
|
||||||
DisplayName: "User Admin",
|
DisplayName: "User Admin",
|
||||||
Site: Permissions(map[string][]Action{
|
Site: Permissions(map[string][]Action{
|
||||||
ResourceRoleAssignment.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
ResourceRoleAssignment.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||||
ResourceUser.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
ResourceUser.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||||
ResourceUserData.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
ResourceUserData.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||||
|
ResourceUserWorkspaceBuildParameters.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||||
// Full perms to manage org members
|
// Full perms to manage org members
|
||||||
ResourceOrganizationMember.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
ResourceOrganizationMember.Type: {ActionCreate, ActionRead, ActionUpdate, ActionDelete},
|
||||||
ResourceGroup.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))
|
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
|
// Returns the user's login type. This only works if the api key for authorization
|
||||||
// and the requested user match. Eg: 'me'
|
// 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/coderdtest"
|
||||||
"github.com/coder/coder/v2/coderd/database"
|
"github.com/coder/coder/v2/coderd/database"
|
||||||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
"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/dbgen"
|
||||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||||
"github.com/coder/coder/v2/coderd/rbac"
|
"github.com/coder/coder/v2/coderd/rbac"
|
||||||
|
@ -1721,6 +1722,129 @@ func TestSuspendedPagination(t *testing.T) {
|
||||||
require.Equal(t, expected, page.Users, "expected page")
|
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
|
// TestPaginatedUsers creates a list of users, then tries to paginate through
|
||||||
// them using different page sizes.
|
// them using different page sizes.
|
||||||
func TestPaginatedUsers(t *testing.T) {
|
func TestPaginatedUsers(t *testing.T) {
|
||||||
|
|
|
@ -3,29 +3,30 @@ package codersdk
|
||||||
type RBACResource string
|
type RBACResource string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ResourceWorkspace RBACResource = "workspace"
|
ResourceWorkspace RBACResource = "workspace"
|
||||||
ResourceWorkspaceProxy RBACResource = "workspace_proxy"
|
ResourceWorkspaceProxy RBACResource = "workspace_proxy"
|
||||||
ResourceWorkspaceExecution RBACResource = "workspace_execution"
|
ResourceWorkspaceExecution RBACResource = "workspace_execution"
|
||||||
ResourceWorkspaceApplicationConnect RBACResource = "application_connect"
|
ResourceWorkspaceApplicationConnect RBACResource = "application_connect"
|
||||||
ResourceAuditLog RBACResource = "audit_log"
|
ResourceAuditLog RBACResource = "audit_log"
|
||||||
ResourceTemplate RBACResource = "template"
|
ResourceTemplate RBACResource = "template"
|
||||||
ResourceGroup RBACResource = "group"
|
ResourceGroup RBACResource = "group"
|
||||||
ResourceFile RBACResource = "file"
|
ResourceFile RBACResource = "file"
|
||||||
ResourceProvisionerDaemon RBACResource = "provisioner_daemon"
|
ResourceProvisionerDaemon RBACResource = "provisioner_daemon"
|
||||||
ResourceOrganization RBACResource = "organization"
|
ResourceOrganization RBACResource = "organization"
|
||||||
ResourceRoleAssignment RBACResource = "assign_role"
|
ResourceRoleAssignment RBACResource = "assign_role"
|
||||||
ResourceOrgRoleAssignment RBACResource = "assign_org_role"
|
ResourceOrgRoleAssignment RBACResource = "assign_org_role"
|
||||||
ResourceAPIKey RBACResource = "api_key"
|
ResourceAPIKey RBACResource = "api_key"
|
||||||
ResourceUser RBACResource = "user"
|
ResourceUser RBACResource = "user"
|
||||||
ResourceUserData RBACResource = "user_data"
|
ResourceUserData RBACResource = "user_data"
|
||||||
ResourceOrganizationMember RBACResource = "organization_member"
|
ResourceUserWorkspaceBuildParameters RBACResource = "user_workspace_build_parameters"
|
||||||
ResourceLicense RBACResource = "license"
|
ResourceOrganizationMember RBACResource = "organization_member"
|
||||||
ResourceDeploymentValues RBACResource = "deployment_config"
|
ResourceLicense RBACResource = "license"
|
||||||
ResourceDeploymentStats RBACResource = "deployment_stats"
|
ResourceDeploymentValues RBACResource = "deployment_config"
|
||||||
ResourceReplicas RBACResource = "replicas"
|
ResourceDeploymentStats RBACResource = "deployment_stats"
|
||||||
ResourceDebugInfo RBACResource = "debug_info"
|
ResourceReplicas RBACResource = "replicas"
|
||||||
ResourceSystem RBACResource = "system"
|
ResourceDebugInfo RBACResource = "debug_info"
|
||||||
ResourceTemplateInsights RBACResource = "template_insights"
|
ResourceSystem RBACResource = "system"
|
||||||
|
ResourceTemplateInsights RBACResource = "template_insights"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -52,6 +53,7 @@ var (
|
||||||
ResourceAPIKey,
|
ResourceAPIKey,
|
||||||
ResourceUser,
|
ResourceUser,
|
||||||
ResourceUserData,
|
ResourceUserData,
|
||||||
|
ResourceUserWorkspaceBuildParameters,
|
||||||
ResourceOrganizationMember,
|
ResourceOrganizationMember,
|
||||||
ResourceLicense,
|
ResourceLicense,
|
||||||
ResourceDeploymentValues,
|
ResourceDeploymentValues,
|
||||||
|
|
|
@ -221,6 +221,27 @@ type OIDCAuthMethod struct {
|
||||||
IconURL string `json:"iconUrl"`
|
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.
|
// HasFirstUser returns whether the first user has been created.
|
||||||
func (c *Client) HasFirstUser(ctx context.Context) (bool, error) {
|
func (c *Client) HasFirstUser(ctx context.Context) (bool, error) {
|
||||||
res, err := c.Request(ctx, http.MethodGet, "/api/v2/users/first", nil)
|
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
|
#### Enumerated Values
|
||||||
|
|
||||||
| Value |
|
| Value |
|
||||||
| --------------------- |
|
| --------------------------------- |
|
||||||
| `workspace` |
|
| `workspace` |
|
||||||
| `workspace_proxy` |
|
| `workspace_proxy` |
|
||||||
| `workspace_execution` |
|
| `workspace_execution` |
|
||||||
| `application_connect` |
|
| `application_connect` |
|
||||||
| `audit_log` |
|
| `audit_log` |
|
||||||
| `template` |
|
| `template` |
|
||||||
| `group` |
|
| `group` |
|
||||||
| `file` |
|
| `file` |
|
||||||
| `provisioner_daemon` |
|
| `provisioner_daemon` |
|
||||||
| `organization` |
|
| `organization` |
|
||||||
| `assign_role` |
|
| `assign_role` |
|
||||||
| `assign_org_role` |
|
| `assign_org_role` |
|
||||||
| `api_key` |
|
| `api_key` |
|
||||||
| `user` |
|
| `user` |
|
||||||
| `user_data` |
|
| `user_data` |
|
||||||
| `organization_member` |
|
| `user_workspace_build_parameters` |
|
||||||
| `license` |
|
| `organization_member` |
|
||||||
| `deployment_config` |
|
| `license` |
|
||||||
| `deployment_stats` |
|
| `deployment_config` |
|
||||||
| `replicas` |
|
| `deployment_stats` |
|
||||||
| `debug_info` |
|
| `replicas` |
|
||||||
| `system` |
|
| `debug_info` |
|
||||||
| `template_insights` |
|
| `system` |
|
||||||
|
| `template_insights` |
|
||||||
|
|
||||||
## codersdk.RateLimitConfig
|
## 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 | | |
|
| `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
|
## codersdk.UserQuietHoursScheduleConfig
|
||||||
|
|
||||||
```json
|
```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).
|
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
|
## Get user Git SSH key
|
||||||
|
|
||||||
### Code samples
|
### Code samples
|
||||||
|
|
|
@ -177,14 +177,14 @@
|
||||||
"title": "Resource metadata",
|
"title": "Resource metadata",
|
||||||
"description": "Show information in the workspace about template resources",
|
"description": "Show information in the workspace about template resources",
|
||||||
"path": "./templates/resource-metadata.md"
|
"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",
|
"title": "Administering templates",
|
||||||
"description": "Configuration settings for template admins",
|
"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
|
// Default port from the server
|
||||||
export const defaultPort = 3000;
|
export const defaultPort = 3000;
|
||||||
export const prometheusPort = 2114;
|
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
|
// Credentials for the first user
|
||||||
export const username = "admin";
|
export const username = "admin";
|
||||||
|
|
|
@ -15,7 +15,7 @@ import {
|
||||||
Resource,
|
Resource,
|
||||||
RichParameter,
|
RichParameter,
|
||||||
} from "./provisionerGenerated";
|
} from "./provisionerGenerated";
|
||||||
import { prometheusPort, pprofPort } from "./constants";
|
import { prometheusPort, agentPProfPort } from "./constants";
|
||||||
import { port } from "./playwright.config";
|
import { port } from "./playwright.config";
|
||||||
import * as ssh from "ssh2";
|
import * as ssh from "ssh2";
|
||||||
import { Duplex } from "stream";
|
import { Duplex } from "stream";
|
||||||
|
@ -306,7 +306,7 @@ export const startAgentWithCommand = async (
|
||||||
...process.env,
|
...process.env,
|
||||||
CODER_AGENT_URL: "http://localhost:" + port,
|
CODER_AGENT_URL: "http://localhost:" + port,
|
||||||
CODER_AGENT_TOKEN: token,
|
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,
|
CODER_AGENT_PROMETHEUS_ADDRESS: "127.0.0.1:" + prometheusPort,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -128,6 +128,14 @@ export const seventhParameter: RichParameter = {
|
||||||
order: 1,
|
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
|
// Build options
|
||||||
|
|
||||||
export const firstBuildOption: RichParameter = {
|
export const firstBuildOption: RichParameter = {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { defineConfig } from "@playwright/test";
|
import { defineConfig } from "@playwright/test";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import { defaultPort, gitAuth } from "./constants";
|
import { defaultPort, coderdPProfPort, gitAuth } from "./constants";
|
||||||
|
|
||||||
export const port = process.env.CODER_E2E_PORT
|
export const port = process.env.CODER_E2E_PORT
|
||||||
? Number(process.env.CODER_E2E_PORT)
|
? Number(process.env.CODER_E2E_PORT)
|
||||||
|
@ -103,6 +103,7 @@ export default defineConfig({
|
||||||
gitAuth.webPort,
|
gitAuth.webPort,
|
||||||
gitAuth.validatePath,
|
gitAuth.validatePath,
|
||||||
),
|
),
|
||||||
|
CODER_PPROF_ADDRESS: "127.0.0.1:" + coderdPProfPort,
|
||||||
},
|
},
|
||||||
reuseExistingServer: false,
|
reuseExistingServer: false,
|
||||||
},
|
},
|
||||||
|
|
|
@ -119,7 +119,7 @@ const filteredServerLogLines = (chunk: string): string[] =>
|
||||||
|
|
||||||
const exportDebugPprof = async (outputFile: string) => {
|
const exportDebugPprof = async (outputFile: string) => {
|
||||||
const response = await axios.get(
|
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) {
|
if (response.status !== 200) {
|
||||||
throw new Error(`Error: Received status code ${response.status}`);
|
throw new Error(`Error: Received status code ${response.status}`);
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
thirdParameter,
|
thirdParameter,
|
||||||
seventhParameter,
|
seventhParameter,
|
||||||
sixthParameter,
|
sixthParameter,
|
||||||
|
randParamName,
|
||||||
} from "../parameters";
|
} from "../parameters";
|
||||||
import { RichParameter } from "../provisionerGenerated";
|
import { RichParameter } from "../provisionerGenerated";
|
||||||
import { beforeCoderTest } from "../hooks";
|
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 }) => {
|
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 = [
|
const buildParameters = [
|
||||||
{ name: secondParameter.name, value: "AAAAA" },
|
{ name: richParameters[0].name, value: "AAAAA" },
|
||||||
{ name: fourthParameter.name, value: "false" },
|
{ name: richParameters[1].name, value: "false" },
|
||||||
];
|
];
|
||||||
const template = await createTemplate(
|
const template = await createTemplate(
|
||||||
page,
|
page,
|
||||||
|
|
|
@ -23,14 +23,14 @@ test("restart workspace with ephemeral parameters", async ({ page }) => {
|
||||||
|
|
||||||
// Verify that build options are default (not selected).
|
// Verify that build options are default (not selected).
|
||||||
await verifyParameters(page, workspaceName, richParameters, [
|
await verifyParameters(page, workspaceName, richParameters, [
|
||||||
{ name: firstBuildOption.name, value: firstBuildOption.defaultValue },
|
{ name: richParameters[0].name, value: firstBuildOption.defaultValue },
|
||||||
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
{ name: richParameters[1].name, value: secondBuildOption.defaultValue },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Now, restart the workspace with ephemeral parameters selected.
|
// Now, restart the workspace with ephemeral parameters selected.
|
||||||
const buildParameters = [
|
const buildParameters = [
|
||||||
{ name: firstBuildOption.name, value: "AAAAA" },
|
{ name: richParameters[0].name, value: "AAAAA" },
|
||||||
{ name: secondBuildOption.name, value: "true" },
|
{ name: richParameters[1].name, value: "true" },
|
||||||
];
|
];
|
||||||
await buildWorkspaceWithParameters(
|
await buildWorkspaceWithParameters(
|
||||||
page,
|
page,
|
||||||
|
@ -42,7 +42,7 @@ test("restart workspace with ephemeral parameters", async ({ page }) => {
|
||||||
|
|
||||||
// Verify that build options are default (not selected).
|
// Verify that build options are default (not selected).
|
||||||
await verifyParameters(page, workspaceName, richParameters, [
|
await verifyParameters(page, workspaceName, richParameters, [
|
||||||
{ name: firstBuildOption.name, value: firstBuildOption.defaultValue },
|
{ name: richParameters[0].name, value: firstBuildOption.defaultValue },
|
||||||
{ name: secondBuildOption.name, value: secondBuildOption.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).
|
// Verify that build options are default (not selected).
|
||||||
await verifyParameters(page, workspaceName, richParameters, [
|
await verifyParameters(page, workspaceName, richParameters, [
|
||||||
{ name: firstBuildOption.name, value: firstBuildOption.defaultValue },
|
{ name: richParameters[0].name, value: firstBuildOption.defaultValue },
|
||||||
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
{ name: richParameters[1].name, value: secondBuildOption.defaultValue },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Stop the workspace
|
// Stop the workspace
|
||||||
|
@ -30,8 +30,8 @@ test("start workspace with ephemeral parameters", async ({ page }) => {
|
||||||
|
|
||||||
// Now, start the workspace with ephemeral parameters selected.
|
// Now, start the workspace with ephemeral parameters selected.
|
||||||
const buildParameters = [
|
const buildParameters = [
|
||||||
{ name: firstBuildOption.name, value: "AAAAA" },
|
{ name: richParameters[0].name, value: "AAAAA" },
|
||||||
{ name: secondBuildOption.name, value: "true" },
|
{ name: richParameters[1].name, value: "true" },
|
||||||
];
|
];
|
||||||
|
|
||||||
await buildWorkspaceWithParameters(
|
await buildWorkspaceWithParameters(
|
||||||
|
@ -43,7 +43,7 @@ test("start workspace with ephemeral parameters", async ({ page }) => {
|
||||||
|
|
||||||
// Verify that build options are default (not selected).
|
// Verify that build options are default (not selected).
|
||||||
await verifyParameters(page, workspaceName, richParameters, [
|
await verifyParameters(page, workspaceName, richParameters, [
|
||||||
{ name: firstBuildOption.name, value: firstBuildOption.defaultValue },
|
{ name: richParameters[0].name, value: firstBuildOption.defaultValue },
|
||||||
{ name: secondBuildOption.name, value: secondBuildOption.defaultValue },
|
{ name: richParameters[1].name, value: secondBuildOption.defaultValue },
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
|
@ -129,6 +129,13 @@ export const getAuthenticatedUser = async () => {
|
||||||
return response.data;
|
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> => {
|
export const getAuthMethods = async (): Promise<TypesGen.AuthMethods> => {
|
||||||
const response = await axios.get<TypesGen.AuthMethods>(
|
const response = await axios.get<TypesGen.AuthMethods>(
|
||||||
"/api/v2/users/authmethods",
|
"/api/v2/users/authmethods",
|
||||||
|
|
|
@ -1435,6 +1435,12 @@ export interface UserLoginType {
|
||||||
readonly login_type: LoginType;
|
readonly login_type: LoginType;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// From codersdk/users.go
|
||||||
|
export interface UserParameter {
|
||||||
|
readonly name: string;
|
||||||
|
readonly value: string;
|
||||||
|
}
|
||||||
|
|
||||||
// From codersdk/deployment.go
|
// From codersdk/deployment.go
|
||||||
export interface UserQuietHoursScheduleConfig {
|
export interface UserQuietHoursScheduleConfig {
|
||||||
readonly default_schedule: string;
|
readonly default_schedule: string;
|
||||||
|
@ -2011,6 +2017,7 @@ export type RBACResource =
|
||||||
| "template_insights"
|
| "template_insights"
|
||||||
| "user"
|
| "user"
|
||||||
| "user_data"
|
| "user_data"
|
||||||
|
| "user_workspace_build_parameters"
|
||||||
| "workspace"
|
| "workspace"
|
||||||
| "workspace_execution"
|
| "workspace_execution"
|
||||||
| "workspace_proxy";
|
| "workspace_proxy";
|
||||||
|
@ -2035,6 +2042,7 @@ export const RBACResources: RBACResource[] = [
|
||||||
"template_insights",
|
"template_insights",
|
||||||
"user",
|
"user",
|
||||||
"user_data",
|
"user_data",
|
||||||
|
"user_workspace_build_parameters",
|
||||||
"workspace",
|
"workspace",
|
||||||
"workspace_execution",
|
"workspace_execution",
|
||||||
"workspace_proxy",
|
"workspace_proxy",
|
||||||
|
|
|
@ -10,6 +10,7 @@ import { MemoizedMarkdown } from "components/Markdown/Markdown";
|
||||||
import { Stack } from "components/Stack/Stack";
|
import { Stack } from "components/Stack/Stack";
|
||||||
import { MultiTextField } from "./MultiTextField";
|
import { MultiTextField } from "./MultiTextField";
|
||||||
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
import { ExternalImage } from "components/ExternalImage/ExternalImage";
|
||||||
|
import { AutofillSource } from "utils/richParameters";
|
||||||
import { Pill } from "components/Pill/Pill";
|
import { Pill } from "components/Pill/Pill";
|
||||||
import ErrorOutline from "@mui/icons-material/ErrorOutline";
|
import ErrorOutline from "@mui/icons-material/ErrorOutline";
|
||||||
|
|
||||||
|
@ -167,6 +168,7 @@ export type RichParameterInputProps = Omit<
|
||||||
"size" | "onChange"
|
"size" | "onChange"
|
||||||
> & {
|
> & {
|
||||||
parameter: TemplateVersionParameter;
|
parameter: TemplateVersionParameter;
|
||||||
|
autofillSource?: AutofillSource;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
size?: Size;
|
size?: Size;
|
||||||
};
|
};
|
||||||
|
@ -174,6 +176,7 @@ export type RichParameterInputProps = Omit<
|
||||||
export const RichParameterInput: FC<RichParameterInputProps> = ({
|
export const RichParameterInput: FC<RichParameterInputProps> = ({
|
||||||
parameter,
|
parameter,
|
||||||
size = "medium",
|
size = "medium",
|
||||||
|
autofillSource,
|
||||||
...fieldProps
|
...fieldProps
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
|
@ -186,6 +189,17 @@ export const RichParameterInput: FC<RichParameterInputProps> = ({
|
||||||
<ParameterLabel parameter={parameter} />
|
<ParameterLabel parameter={parameter} />
|
||||||
<div css={{ display: "flex", flexDirection: "column" }}>
|
<div css={{ display: "flex", flexDirection: "column" }}>
|
||||||
<RichParameterField {...fieldProps} size={size} parameter={parameter} />
|
<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>
|
</div>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,13 +1,15 @@
|
||||||
import { TemplateVersionParameter } from "api/typesGenerated";
|
import { TemplateVersionParameter } from "api/typesGenerated";
|
||||||
import { FormSection, FormFields } from "components/Form/Form";
|
import { FormFields, FormSection } from "components/Form/Form";
|
||||||
import {
|
import {
|
||||||
RichParameterInput,
|
RichParameterInput,
|
||||||
RichParameterInputProps,
|
RichParameterInputProps,
|
||||||
} from "components/RichParameterInput/RichParameterInput";
|
} from "components/RichParameterInput/RichParameterInput";
|
||||||
import { ComponentProps, FC } from "react";
|
import { ComponentProps, FC } from "react";
|
||||||
|
import { AutofillSource } from "utils/richParameters";
|
||||||
|
|
||||||
export type TemplateParametersSectionProps = {
|
export type TemplateParametersSectionProps = {
|
||||||
templateParameters: TemplateVersionParameter[];
|
templateParameters: TemplateVersionParameter[];
|
||||||
|
autofillSources?: Record<string, AutofillSource>;
|
||||||
getInputProps: (
|
getInputProps: (
|
||||||
parameter: TemplateVersionParameter,
|
parameter: TemplateVersionParameter,
|
||||||
index: number,
|
index: number,
|
||||||
|
@ -17,6 +19,7 @@ export type TemplateParametersSectionProps = {
|
||||||
export const TemplateParametersSection: FC<TemplateParametersSectionProps> = ({
|
export const TemplateParametersSection: FC<TemplateParametersSectionProps> = ({
|
||||||
templateParameters,
|
templateParameters,
|
||||||
getInputProps,
|
getInputProps,
|
||||||
|
autofillSources,
|
||||||
...formSectionProps
|
...formSectionProps
|
||||||
}) => {
|
}) => {
|
||||||
const hasMutableParameters =
|
const hasMutableParameters =
|
||||||
|
@ -38,6 +41,9 @@ export const TemplateParametersSection: FC<TemplateParametersSectionProps> = ({
|
||||||
{...getInputProps(parameter, index)}
|
{...getInputProps(parameter, index)}
|
||||||
key={parameter.name}
|
key={parameter.name}
|
||||||
parameter={parameter}
|
parameter={parameter}
|
||||||
|
autofillSource={
|
||||||
|
autofillSources && autofillSources[parameter.name]
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
|
|
|
@ -176,7 +176,9 @@ describe("CreateWorkspacePage", () => {
|
||||||
"me",
|
"me",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
template_id: MockTemplate.id,
|
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",
|
"me",
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
template_version_id: MockTemplate.active_version_id,
|
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 { getUserParameters } from "api/api";
|
||||||
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 { checkAuthorization } from "api/queries/authCheck";
|
import { checkAuthorization } from "api/queries/authCheck";
|
||||||
import {
|
import {
|
||||||
|
richParameters,
|
||||||
templateByName,
|
templateByName,
|
||||||
templateVersionExternalAuth,
|
templateVersionExternalAuth,
|
||||||
richParameters,
|
|
||||||
} from "api/queries/templates";
|
} from "api/queries/templates";
|
||||||
import { autoCreateWorkspace, createWorkspace } from "api/queries/workspaces";
|
import { autoCreateWorkspace, createWorkspace } from "api/queries/workspaces";
|
||||||
import { useEffectEvent } from "hooks/hookPolyfills";
|
import {
|
||||||
import { paramsUsedToCreateWorkspace } from "utils/workspace";
|
TemplateVersionParameter,
|
||||||
import { Loader } from "components/Loader/Loader";
|
UserParameter,
|
||||||
|
Workspace,
|
||||||
|
} from "api/typesGenerated";
|
||||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
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 { CreateWorkspacePageView } from "./CreateWorkspacePageView";
|
||||||
|
import { CreateWSPermissions, createWorkspaceChecks } from "./permissions";
|
||||||
|
|
||||||
export const createWorkspaceModes = ["form", "auto", "duplicate"] as const;
|
export const createWorkspaceModes = ["form", "auto", "duplicate"] as const;
|
||||||
export type CreateWorkspaceMode = (typeof createWorkspaceModes)[number];
|
export type CreateWorkspaceMode = (typeof createWorkspaceModes)[number];
|
||||||
|
@ -41,7 +43,6 @@ const CreateWorkspacePage: FC = () => {
|
||||||
const me = useMe();
|
const me = useMe();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [searchParams, setSearchParams] = useSearchParams();
|
const [searchParams, setSearchParams] = useSearchParams();
|
||||||
const defaultBuildParameters = getDefaultBuildParameters(searchParams);
|
|
||||||
const mode = getWorkspaceMode(searchParams);
|
const mode = getWorkspaceMode(searchParams);
|
||||||
const customVersionId = searchParams.get("version") ?? undefined;
|
const customVersionId = searchParams.get("version") ?? undefined;
|
||||||
|
|
||||||
|
@ -61,6 +62,15 @@ const CreateWorkspacePage: FC = () => {
|
||||||
const createWorkspaceMutation = useMutation(createWorkspace(queryClient));
|
const createWorkspaceMutation = useMutation(createWorkspace(queryClient));
|
||||||
|
|
||||||
const templateQuery = useQuery(templateByName(organizationId, templateName));
|
const templateQuery = useQuery(templateByName(organizationId, templateName));
|
||||||
|
|
||||||
|
const userParametersQuery = useQuery(
|
||||||
|
["userParameters"],
|
||||||
|
() => getUserParameters(templateQuery.data!.id),
|
||||||
|
{
|
||||||
|
enabled: templateQuery.isSuccess,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const permissionsQuery = useQuery(
|
const permissionsQuery = useQuery(
|
||||||
checkAuthorization({
|
checkAuthorization({
|
||||||
checks: createWorkspaceChecks(organizationId),
|
checks: createWorkspaceChecks(organizationId),
|
||||||
|
@ -101,12 +111,17 @@ const CreateWorkspacePage: FC = () => {
|
||||||
[navigate],
|
[navigate],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const autofillParameters = getAutofillParameters(
|
||||||
|
searchParams,
|
||||||
|
userParametersQuery.data ? userParametersQuery.data : [],
|
||||||
|
);
|
||||||
|
|
||||||
const automateWorkspaceCreation = useEffectEvent(async () => {
|
const automateWorkspaceCreation = useEffectEvent(async () => {
|
||||||
try {
|
try {
|
||||||
const newWorkspace = await autoCreateWorkspaceMutation.mutateAsync({
|
const newWorkspace = await autoCreateWorkspaceMutation.mutateAsync({
|
||||||
templateName,
|
templateName,
|
||||||
organizationId,
|
organizationId,
|
||||||
defaultBuildParameters,
|
defaultBuildParameters: autofillParameters,
|
||||||
defaultName,
|
defaultName,
|
||||||
versionId: realizedVersionId,
|
versionId: realizedVersionId,
|
||||||
});
|
});
|
||||||
|
@ -139,7 +154,7 @@ const CreateWorkspacePage: FC = () => {
|
||||||
mode={mode}
|
mode={mode}
|
||||||
defaultName={defaultName}
|
defaultName={defaultName}
|
||||||
defaultOwner={me}
|
defaultOwner={me}
|
||||||
defaultBuildParameters={defaultBuildParameters}
|
autofillParameters={autofillParameters}
|
||||||
error={createWorkspaceMutation.error}
|
error={createWorkspaceMutation.error}
|
||||||
resetMutation={createWorkspaceMutation.reset}
|
resetMutation={createWorkspaceMutation.reset}
|
||||||
template={templateQuery.data!}
|
template={templateQuery.data!}
|
||||||
|
@ -223,17 +238,34 @@ const useExternalAuth = (versionId: string | undefined) => {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getDefaultBuildParameters = (
|
const getAutofillParameters = (
|
||||||
urlSearchParams: URLSearchParams,
|
urlSearchParams: URLSearchParams,
|
||||||
): WorkspaceBuildParameter[] => {
|
userParameters: UserParameter[],
|
||||||
const buildValues: WorkspaceBuildParameter[] = [];
|
): AutofillBuildParameter[] => {
|
||||||
Array.from(urlSearchParams.keys())
|
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."))
|
.filter((key) => key.startsWith("param."))
|
||||||
.forEach((key) => {
|
.map((key) => {
|
||||||
const name = key.replace("param.", "");
|
const name = key.replace("param.", "");
|
||||||
const value = urlSearchParams.get(key) ?? "";
|
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;
|
return buildValues;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ const meta: Meta<typeof CreateWorkspacePageView> = {
|
||||||
args: {
|
args: {
|
||||||
defaultName: "",
|
defaultName: "",
|
||||||
defaultOwner: MockUser,
|
defaultOwner: MockUser,
|
||||||
defaultBuildParameters: [],
|
autofillParameters: [],
|
||||||
template: MockTemplate,
|
template: MockTemplate,
|
||||||
parameters: [],
|
parameters: [],
|
||||||
externalAuth: [],
|
externalAuth: [],
|
||||||
|
@ -86,6 +86,13 @@ export const Parameters: Story = {
|
||||||
ephemeral: false,
|
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 type * as TypesGen from "api/typesGenerated";
|
||||||
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
|
import { UserAutocomplete } from "components/UserAutocomplete/UserAutocomplete";
|
||||||
import { FormikContextType, useFormik } from "formik";
|
import { FormikContextType, useFormik } from "formik";
|
||||||
import { type FC, useEffect, useState } from "react";
|
import { type FC, useEffect, useState, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
getFormHelpers,
|
getFormHelpers,
|
||||||
nameValidator,
|
nameValidator,
|
||||||
|
@ -17,6 +17,8 @@ import {
|
||||||
HorizontalForm,
|
HorizontalForm,
|
||||||
} from "components/Form/Form";
|
} from "components/Form/Form";
|
||||||
import {
|
import {
|
||||||
|
AutofillBuildParameter,
|
||||||
|
AutofillSource,
|
||||||
getInitialRichParameterValues,
|
getInitialRichParameterValues,
|
||||||
useValidationSchemaForRichParameters,
|
useValidationSchemaForRichParameters,
|
||||||
} from "utils/richParameters";
|
} from "utils/richParameters";
|
||||||
|
@ -58,7 +60,7 @@ export interface CreateWorkspacePageViewProps {
|
||||||
externalAuthPollingState: ExternalAuthPollingState;
|
externalAuthPollingState: ExternalAuthPollingState;
|
||||||
startPollingExternalAuth: () => void;
|
startPollingExternalAuth: () => void;
|
||||||
parameters: TypesGen.TemplateVersionParameter[];
|
parameters: TypesGen.TemplateVersionParameter[];
|
||||||
defaultBuildParameters: TypesGen.WorkspaceBuildParameter[];
|
autofillParameters: AutofillBuildParameter[];
|
||||||
permissions: CreateWSPermissions;
|
permissions: CreateWSPermissions;
|
||||||
creatingWorkspace: boolean;
|
creatingWorkspace: boolean;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
|
@ -80,7 +82,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||||
externalAuthPollingState,
|
externalAuthPollingState,
|
||||||
startPollingExternalAuth,
|
startPollingExternalAuth,
|
||||||
parameters,
|
parameters,
|
||||||
defaultBuildParameters,
|
autofillParameters,
|
||||||
permissions,
|
permissions,
|
||||||
creatingWorkspace,
|
creatingWorkspace,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
|
@ -98,7 +100,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||||
template_id: template.id,
|
template_id: template.id,
|
||||||
rich_parameter_values: getInitialRichParameterValues(
|
rich_parameter_values: getInitialRichParameterValues(
|
||||||
parameters,
|
parameters,
|
||||||
defaultBuildParameters,
|
autofillParameters,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object({
|
validationSchema: Yup.object({
|
||||||
|
@ -126,6 +128,16 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||||
error,
|
error,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const autofillSources = useMemo(() => {
|
||||||
|
return autofillParameters.reduce(
|
||||||
|
(acc, param) => {
|
||||||
|
acc[param.name] = param.source;
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<string, AutofillSource>,
|
||||||
|
);
|
||||||
|
}, [autofillParameters]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Margins size="medium">
|
<Margins size="medium">
|
||||||
<PageHeader actions={<Button onClick={onCancel}>Cancel</Button>}>
|
<PageHeader actions={<Button onClick={onCancel}>Cancel</Button>}>
|
||||||
|
@ -244,6 +256,7 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
|
||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
autofillSource={autofillSources[parameter.name]}
|
||||||
key={parameter.name}
|
key={parameter.name}
|
||||||
parameter={parameter}
|
parameter={parameter}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
|
|
|
@ -21,7 +21,10 @@ import {
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { docs } from "utils/docs";
|
import { docs } from "utils/docs";
|
||||||
import { getFormHelpers } from "utils/formUtils";
|
import { getFormHelpers } from "utils/formUtils";
|
||||||
import { getInitialRichParameterValues } from "utils/richParameters";
|
import {
|
||||||
|
AutofillBuildParameter,
|
||||||
|
getInitialRichParameterValues,
|
||||||
|
} from "utils/richParameters";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
|
@ -113,7 +116,12 @@ const BuildParametersPopoverContent: FC<BuildParametersPopoverContentProps> = ({
|
||||||
popover.setIsOpen(false);
|
popover.setIsOpen(false);
|
||||||
}}
|
}}
|
||||||
ephemeralParameters={ephemeralParameters}
|
ephemeralParameters={ephemeralParameters}
|
||||||
buildParameters={buildParameters}
|
buildParameters={buildParameters.map(
|
||||||
|
(p): AutofillBuildParameter => ({
|
||||||
|
...p,
|
||||||
|
source: "active_build",
|
||||||
|
}),
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
@ -147,7 +155,7 @@ const BuildParametersPopoverContent: FC<BuildParametersPopoverContentProps> = ({
|
||||||
|
|
||||||
interface FormProps {
|
interface FormProps {
|
||||||
ephemeralParameters: TemplateVersionParameter[];
|
ephemeralParameters: TemplateVersionParameter[];
|
||||||
buildParameters: WorkspaceBuildParameter[];
|
buildParameters: AutofillBuildParameter[];
|
||||||
onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void;
|
onSubmit: (buildParameters: WorkspaceBuildParameter[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
} from "components/Form/Form";
|
} from "components/Form/Form";
|
||||||
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput";
|
import { RichParameterInput } from "components/RichParameterInput/RichParameterInput";
|
||||||
import {
|
import {
|
||||||
|
AutofillBuildParameter,
|
||||||
getInitialRichParameterValues,
|
getInitialRichParameterValues,
|
||||||
useValidationSchemaForRichParameters,
|
useValidationSchemaForRichParameters,
|
||||||
} from "utils/richParameters";
|
} from "utils/richParameters";
|
||||||
|
@ -27,7 +28,7 @@ export type WorkspaceParametersFormValues = {
|
||||||
interface WorkspaceParameterFormProps {
|
interface WorkspaceParameterFormProps {
|
||||||
workspace: Workspace;
|
workspace: Workspace;
|
||||||
templateVersionRichParameters: TemplateVersionParameter[];
|
templateVersionRichParameters: TemplateVersionParameter[];
|
||||||
buildParameters: WorkspaceBuildParameter[];
|
autofillParams: AutofillBuildParameter[];
|
||||||
isSubmitting: boolean;
|
isSubmitting: boolean;
|
||||||
canChangeVersions: boolean;
|
canChangeVersions: boolean;
|
||||||
error: unknown;
|
error: unknown;
|
||||||
|
@ -40,7 +41,7 @@ export const WorkspaceParametersForm: FC<WorkspaceParameterFormProps> = ({
|
||||||
onCancel,
|
onCancel,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
templateVersionRichParameters,
|
templateVersionRichParameters,
|
||||||
buildParameters,
|
autofillParams,
|
||||||
error,
|
error,
|
||||||
canChangeVersions,
|
canChangeVersions,
|
||||||
isSubmitting,
|
isSubmitting,
|
||||||
|
@ -50,7 +51,7 @@ export const WorkspaceParametersForm: FC<WorkspaceParameterFormProps> = ({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
rich_parameter_values: getInitialRichParameterValues(
|
rich_parameter_values: getInitialRichParameterValues(
|
||||||
templateVersionRichParameters,
|
templateVersionRichParameters,
|
||||||
buildParameters,
|
autofillParams,
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
validationSchema: Yup.object({
|
validationSchema: Yup.object({
|
||||||
|
|
|
@ -24,6 +24,7 @@ import { EmptyState } from "components/EmptyState/EmptyState";
|
||||||
import Button from "@mui/material/Button";
|
import Button from "@mui/material/Button";
|
||||||
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined";
|
import OpenInNewOutlined from "@mui/icons-material/OpenInNewOutlined";
|
||||||
import { docs } from "utils/docs";
|
import { docs } from "utils/docs";
|
||||||
|
import { AutofillBuildParameter } from "utils/richParameters";
|
||||||
|
|
||||||
const WorkspaceParametersPage: FC = () => {
|
const WorkspaceParametersPage: FC = () => {
|
||||||
const workspace = useWorkspaceSettings();
|
const workspace = useWorkspaceSettings();
|
||||||
|
@ -126,7 +127,12 @@ export const WorkspaceParametersPageView: FC<
|
||||||
<WorkspaceParametersForm
|
<WorkspaceParametersForm
|
||||||
workspace={workspace}
|
workspace={workspace}
|
||||||
canChangeVersions={canChangeVersions}
|
canChangeVersions={canChangeVersions}
|
||||||
buildParameters={data.buildParameters}
|
autofillParams={data.buildParameters.map(
|
||||||
|
(p): AutofillBuildParameter => ({
|
||||||
|
...p,
|
||||||
|
source: "active_build",
|
||||||
|
}),
|
||||||
|
)}
|
||||||
templateVersionRichParameters={data.templateVersionRichParameters}
|
templateVersionRichParameters={data.templateVersionRichParameters}
|
||||||
error={submitError}
|
error={submitError}
|
||||||
isSubmitting={isSubmitting}
|
isSubmitting={isSubmitting}
|
||||||
|
|
|
@ -46,7 +46,7 @@ test("getInitialRichParameterValues return default value when default build para
|
||||||
const cpuParameter = templateParameters[0];
|
const cpuParameter = templateParameters[0];
|
||||||
const [cpuParameterInitialValue] = getInitialRichParameterValues(
|
const [cpuParameterInitialValue] = getInitialRichParameterValues(
|
||||||
templateParameters,
|
templateParameters,
|
||||||
[{ name: cpuParameter.name, value: "100" }],
|
[{ name: cpuParameter.name, value: "100", source: "user_history" }],
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(cpuParameterInitialValue.value).toBe(cpuParameter.default_value);
|
expect(cpuParameterInitialValue.value).toBe(cpuParameter.default_value);
|
||||||
|
|
|
@ -4,25 +4,39 @@ import {
|
||||||
} from "api/typesGenerated";
|
} from "api/typesGenerated";
|
||||||
import * as Yup from "yup";
|
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 = (
|
export const getInitialRichParameterValues = (
|
||||||
templateParameters: TemplateVersionParameter[],
|
templateParams: TemplateVersionParameter[],
|
||||||
buildParameters?: WorkspaceBuildParameter[],
|
autofillParams?: AutofillBuildParameter[],
|
||||||
): WorkspaceBuildParameter[] => {
|
): WorkspaceBuildParameter[] => {
|
||||||
return templateParameters.map((parameter) => {
|
return templateParams.map((parameter) => {
|
||||||
const existentBuildParameter = buildParameters?.find(
|
// Short-circuit for ephemeral parameters, which are always reset to
|
||||||
(p) => p.name === parameter.name,
|
// the template-defined default.
|
||||||
);
|
if (parameter.ephemeral) {
|
||||||
const shouldReturnTheDefaultValue =
|
|
||||||
!existentBuildParameter ||
|
|
||||||
!isValidValue(parameter, existentBuildParameter) ||
|
|
||||||
parameter.ephemeral;
|
|
||||||
if (shouldReturnTheDefaultValue) {
|
|
||||||
return {
|
return {
|
||||||
name: parameter.name,
|
name: parameter.name,
|
||||||
value: parameter.default_value,
|
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