mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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:
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user