Skip to content

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

Merged
merged 24 commits into from
Jan 30, 2024
Merged

feat: add user-level parameter autofill #11731

merged 24 commits into from
Jan 30, 2024

Conversation

ammario
Copy link
Member

@ammario ammario commented Jan 21, 2024

Screenshot 2024-01-21 at 2.57.23 PM.png

TODO:

  • Add backend endpoint
  • Get frontend working
  • Write frontend tests
  • Update docs

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:

  • 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 comment was marked as off-topic.

@ammario ammario marked this pull request as ready for review January 21, 2024 20:03
@ammario ammario requested a review from BrunoQuaresma January 21, 2024 23:08
@ammario ammario changed the title feat: add user-level parameter auto-fill feat: add user-level parameter autofill Jan 21, 2024
@ammario
Copy link
Member Author

ammario commented Jan 21, 2024

@BrunoQuaresma there are some minor testing issues I have to fix, but curious for your review on the core of the implementation.

@ammario ammario requested a review from mtojek January 22, 2024 04:05
Copy link
Collaborator

@BrunoQuaresma BrunoQuaresma left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

FE looks good!

Copy link
Member

@mtojek mtojek left a 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";
Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

@mtojek mtojek requested a review from spikecurtis January 22, 2024 13:49
Copy link
Contributor

@spikecurtis spikecurtis left a 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.


userWorkspaceBuildParameters := make([]database.GetUserWorkspaceBuildParametersRow, 0)
for _, wbp := range q.workspaceBuildParameters {
userWorkspaceBuildParameters = append(userWorkspaceBuildParameters, database.GetUserWorkspaceBuildParametersRow{
Copy link
Contributor

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

Copy link
Member

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).

Copy link
Member Author

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.

Copy link
Member

@mtojek mtojek left a 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.


userWorkspaceBuildParameters := make([]database.GetUserWorkspaceBuildParametersRow, 0)
for _, wbp := range q.workspaceBuildParameters {
userWorkspaceBuildParameters = append(userWorkspaceBuildParameters, database.GetUserWorkspaceBuildParametersRow{
Copy link
Member

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
Copy link
Member

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:

  1. name = "marcin"
  2. name = "john"

depending on database behavior, the engine can mix order, which is usually the source of flakiness in our tests.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Comment on lines +165 to +170
{
{
["url"]: "value supplied by URL.",
["user_history"]: "recently used value.",
}[autofillSource]
}
Copy link
Member

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.",
};

Copy link
Member Author

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

Copy link
Member

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",
Copy link
Member

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",
}

Copy link
Member Author

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?

Copy link
Member

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(
Copy link
Member

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) {
Copy link
Member

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.

Copy link
Member Author

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?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Validation in dbmem.go?

Copy link
Member

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
Copy link
Member

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
Copy link
Member

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.

Copy link
Member Author

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>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
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).

Copy link
Member Author

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.

@ammario ammario force-pushed the user-params branch 4 times, most recently from 027d4f3 to e37022a Compare January 23, 2024 20:18
@ammario
Copy link
Member Author

ammario commented Jan 23, 2024

I'm a bit crunched for time so I'm going to address the important issues and then re-request @mtojek review.

@ammario ammario force-pushed the user-params branch 2 times, most recently from bdd9f7f to 2c1cbbb Compare January 23, 2024 20:33
@ammario ammario requested a review from mtojek January 23, 2024 20:47
Copy link

github-actions bot commented Jan 23, 2024


✔️ PR 11731 Updated successfully.
🚀 Access the credentials here.

cc: @ammario

@bpmct
Copy link
Member

bpmct commented Jan 23, 2024

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:

  • This introduces a significant change to the default property for coder_parameter. I think we should change the provider docs to reflect how that value will only be the default for the first workspace a user creates in the web UI
  • For folks using lots of Coder parameters in their templates (e.g. they are treating it kind of like a service catalog), this behavior feels quite unnatural. I would be surprised if I logged into GCP to create an instance and it had my previous values filled out. For this reason, I suggest we either make autofill parameters opt-in or opt-out. My preference is opt-in as I can think of significantly fewer parameters that benefit from autocomplete vs ones where it's not necessary/common.

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:
I see our users often getting confused by the difference between workspace proxies and regions within a template. I think there is an opportunity for us to simplify these concepts for developers, even though it is, under the hood, different and complex. @kylecarbs has mentioned using lat/long coordinates and a magic parameter to match folks to the closet region for a template

dotfiles:
dotfiles is a power user feature. I like the idea of some parameters (e.g. dotfiles, group_id) having memory across multiple templates down the road but not showing up in the core "create workspace" form or showing that they are autofilled from user settings and can be overridden, kind of like an address in checkout form

Copy link
Member

@mtojek mtojek left a 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 👍

Comment on lines +165 to +170
{
{
["url"]: "value supplied by URL.",
["user_history"]: "recently used value.",
}[autofillSource]
}
Copy link
Member

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",
Copy link
Member

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";
Copy link
Member

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
Copy link
Member

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) {
Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var sdkParams []codersdk.UserParameter
sdkParams := []codersdk.UserParameter{}

To handle the no rows case with empty array vs null, unless httpapi Write already handles it.

@ammario ammario merged commit adbb025 into main Jan 30, 2024
@ammario ammario deleted the user-params branch January 30, 2024 22:02
@github-actions github-actions bot locked and limited conversation to collaborators Jan 30, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants