-
Notifications
You must be signed in to change notification settings - Fork 881
feat: add user-level parameter autofill #11731
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
This comment was marked as off-topic.
This comment was marked as off-topic.
@BrunoQuaresma there are some minor testing issues I have to fix, but curious for your review on the core of the implementation. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FE looks good!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
First round done, I focused mostly on the backend side and the database layer. I see that you use Graphite, so do you mind splitting it into chunks? Once the backend side is done, we can focus on FE logic.
I'd like to hear more voices around the API design, so adding @spikecurtis and @mafredri to evaluate user experience.
@@ -4,25 +4,43 @@ import { | |||
} from "api/typesGenerated"; | |||
import * as Yup from "yup"; | |||
|
|||
export type AutofillSource = "user_history" | "url" | "active_build"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I haven't reviewed the frontend side thoroughly yet, but I'd rather pull the source information from the backend side, either the previous build or the current one. URL will of course override the auto-fill.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That would involve a wider refactor that I think is out of scope for this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see. I assumed that it would be mostly a modification of the database query you'd like to introduce.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
API semantics seem fine to me. I could imagine us adding additional query parameters in the future to select a specific named parameter and/or return multiple previous values for multiple choice.
coderd/database/dbmem/dbmem.go
Outdated
|
||
userWorkspaceBuildParameters := make([]database.GetUserWorkspaceBuildParametersRow, 0) | ||
for _, wbp := range q.workspaceBuildParameters { | ||
userWorkspaceBuildParameters = append(userWorkspaceBuildParameters, database.GetUserWorkspaceBuildParametersRow{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this returns every build parameter for every user, and also includes a named parameter multiple times if there are multiple builds. You need to match the join and filtering behavior of the SQL query
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I second this comment. SQL query and dbmem.go
diverged. It should be relatively easy to catch these nit-picks with unit tests involving multiple workspaces, users, and ephemeral parameters (ephemeral = false
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Built out the unit test a little bit in the interest of getting this wrapped up today.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Third round, and... I have reviewed site
.
coderd/database/dbmem/dbmem.go
Outdated
|
||
userWorkspaceBuildParameters := make([]database.GetUserWorkspaceBuildParametersRow, 0) | ||
for _, wbp := range q.workspaceBuildParameters { | ||
userWorkspaceBuildParameters = append(userWorkspaceBuildParameters, database.GetUserWorkspaceBuildParametersRow{ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I second this comment. SQL query and dbmem.go
diverged. It should be relatively easy to catch these nit-picks with unit tests involving multiple workspaces, users, and ephemeral parameters (ephemeral = false
).
wbp.name, | ||
wbp.value, | ||
wb.created_at, | ||
ROW_NUMBER() OVER (PARTITION BY wbp.name ORDER BY wb.created_at DESC) as rn |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thinking loud - maybe we should extend ORDER BY
to sort by value too. I can a case where we have 2 workspace builds:
- name = "marcin"
- name = "john"
depending on database behavior, the engine can mix order, which is usually the source of flakiness in our tests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to select all data, instead we could remove the partition and simply do SELECT DISTINCT ON (wbp.name) wbp.name, ... ORDER BY wb.created_at DESC, ...
this lets Postgres eliminate rows early.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not going to address right now in the interest of haste. It's unclear how useful this feature will be in its current form.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect that @mafredri suggested a simpler, more performant query.
{ | ||
{ | ||
["url"]: "value supplied by URL.", | ||
["user_history"]: "recently used value.", | ||
}[autofillSource] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: you can extract this block to a separate const for readability purposes
{autofillSource && autofillSource !== "active_build" && (
<div css={{ marginTop: 4, fontSize: 12 }}>
🪄 Autofilled:{" "}
{autofillSourceLabel[autofillSource]}
</div>
)}
const autofillSourceLabel = {
url: "value supplied by URL.",
user_history: "recently used value.",
};
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hmm I think it's more followable to keep everything in the same JSX block
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it depends on which syntax you like more.
{ | ||
name: "first_parameter", | ||
value: "It works!", | ||
source: "user_history", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: maybe we should highlight that source is enum?
for example:
enum AutofillSource {
UserHistory = "user_history",
ActiveBuild = "active_build",
Foobar = "foobar",
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why would that be better?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{
name: "first_parameter",
value: "It works!",
source: "user_history",
}
Again, readability. When you see a block like the above one, it can be misleading that the source can take any string.
}; | ||
} | ||
|
||
const autofillParam = autofillParams?.find( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you can make it shorter:
const autofillParam = autofillParams?.find(({ name }) => name === parameter.name);
// Use a ternary operator for concise conditional logic
return {
name: parameter.name,
value: autofillParam && isValidValue(parameter, autofillParam) ? autofillParam.value : parameter.default_value,
};
@@ -3758,6 +3758,32 @@ func (q *FakeQuerier) GetUserLinksByUserID(_ context.Context, userID uuid.UUID) | |||
return uls, nil | |||
} | |||
|
|||
func (q *FakeQuerier) GetUserWorkspaceBuildParameters(_ context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Params validation is missing here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what do you expect to see?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validation in dbmem.go
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Copied from above. Code-gen should’ve added this 🤔
if err := validateDatabaseType(params); err != nil {
return database.UserLink{}, err
}
wbp.name, | ||
wbp.value, | ||
wb.created_at, | ||
ROW_NUMBER() OVER (PARTITION BY wbp.name ORDER BY wb.created_at DESC) as rn |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This needs to select all data, instead we could remove the partition and simply do SELECT DISTINCT ON (wbp.name) wbp.name, ... ORDER BY wb.created_at DESC, ...
this lets Postgres eliminate rows early.
|
||
## Create Autofill | ||
|
||
When the template doesn't specify default values, Coder may still autofill |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As a user, I might want the default value to be overridden, even when one is specified. Just a thought.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I'm a bit fuzzy on how these different concepts should override each other. I think we leave it as is and let users complain either way.
|
||
export type TemplateParametersSectionProps = { | ||
templateParameters: TemplateVersionParameter[]; | ||
autofillSources?: Record<string, AutofillSource>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
autofillSources?: Record<string, AutofillSource>; | |
autofillSources?: Record<string, AutofillSource?>; |
I imagine this change is more truthful, unless Records somehow override this (been a while since I wrote TS types).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, there is already the concept of non-existence built into the record type, and I don't the type to allow for an autofill without a specified source.
027d4f3
to
e37022a
Compare
I'm a bit crunched for time so I'm going to address the important issues and then re-request @mtojek review. |
bdd9f7f
to
2c1cbbb
Compare
After trying this, I think the result will be jarring to users. The majority of forms in web apps do not autofill all of a user's previous values. The address field in a billing checkout form is an exception, but I think this is often handled by the browser or a custom component/database that stores a user's billing info. (For example, Amazon.com stores a user's shipping addresses). For infrastructure provisioning platforms (e.g. GCP, Terraform Enterprise), service catalogs, or other products such as GitHub/Vercel, there is no autofill of last values. However, unlike Coder, there are no massively repetitive inputs. Turning to how other CDE products solve this problem, options such as "dotfiles URI" and "region" are managed in the user settings, not per template. Because of Coder's flexibility, this is more difficult. For example, different templates have different definitions of "region," if any, and there is no guarantee that every template will apply a user's dotfiles URI. For that reason, I think auto-filling specific parameters per template is an acceptable solution. However, I think that some enhancements could make this better-received:
As you mentioned in the PR description, we can revert if it creates more harm than good. Plus, all we can do is speculate. I'm fine shipping as-is, but wanted to share my concerns about this UX. I'm genuinely not sure if this will be ultimately more convenient (such as how VS Code opens up to your last project by default), but I am skeptical since I don't (remember) seeing this behavior in other software. I do see opportunities for us to reduce confusion (and manual input) around both of these common parameters: region: dotfiles: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As you mentioned in the PR description, we can revert if it creates more harm than good. Plus, all we can do is speculate. I'm fine shipping as-is, but wanted to share my concerns about this UX.
Yeah, this is purely speculation, and I'm also concerned about changing the default
behavior. It should be relatively easy to revert, although we will break the API unfortunately. This is something we have to acknowledge, @bpmct.
Anyway, it looks like the most important comments are addressed so I'm not blocking this. I'm curious to know if we must apply this one from @mafredri too.
Otherwise, LGTM 👍
{ | ||
{ | ||
["url"]: "value supplied by URL.", | ||
["user_history"]: "recently used value.", | ||
}[autofillSource] | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess it depends on which syntax you like more.
{ | ||
name: "first_parameter", | ||
value: "It works!", | ||
source: "user_history", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
{
name: "first_parameter",
value: "It works!",
source: "user_history",
}
Again, readability. When you see a block like the above one, it can be misleading that the source can take any string.
@@ -4,25 +4,43 @@ import { | |||
} from "api/typesGenerated"; | |||
import * as Yup from "yup"; | |||
|
|||
export type AutofillSource = "user_history" | "url" | "active_build"; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Oh, I see. I assumed that it would be mostly a modification of the database query you'd like to introduce.
wbp.name, | ||
wbp.value, | ||
wb.created_at, | ||
ROW_NUMBER() OVER (PARTITION BY wbp.name ORDER BY wb.created_at DESC) as rn |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect that @mafredri suggested a simpler, more performant query.
@@ -3758,6 +3758,32 @@ func (q *FakeQuerier) GetUserLinksByUserID(_ context.Context, userID uuid.UUID) | |||
return uls, nil | |||
} | |||
|
|||
func (q *FakeQuerier) GetUserWorkspaceBuildParameters(_ context.Context, params database.GetUserWorkspaceBuildParametersParams) ([]database.GetUserWorkspaceBuildParametersRow, error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Validation in dbmem.go
?
coderd/users.go
Outdated
return | ||
} | ||
|
||
var sdkParams []codersdk.UserParameter |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
var sdkParams []codersdk.UserParameter | |
sdkParams := []codersdk.UserParameter{} |
To handle the no rows case with empty array vs null, unless httpapi Write already handles it.
TODO:
Implementation
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: