Skip to content

feat: allow for default presets #18445

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 13 commits into from
Jun 24, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat: full implementation
Signed-off-by: Danny Kopping <dannykopping@gmail.com>
  • Loading branch information
dannykopping committed Jun 19, 2025
commit e9a218cce7ca0d5d37647a0ff1630cad68eee407
3 changes: 3 additions & 0 deletions coderd/apidoc/docs.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions coderd/apidoc/swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions coderd/database/dbmem/dbmem.go
Original file line number Diff line number Diff line change
Expand Up @@ -9135,6 +9135,7 @@ func (q *FakeQuerier) InsertPreset(_ context.Context, arg database.InsertPresetP
Valid: true,
},
PrebuildStatus: database.PrebuildStatusHealthy,
IsDefault: arg.IsDefault,
}
q.presets = append(q.presets, preset)
return preset, nil
Expand Down
5 changes: 3 additions & 2 deletions coderd/presets.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,9 @@ func (api *API) templateVersionPresets(rw http.ResponseWriter, r *http.Request)
var res []codersdk.Preset
for _, preset := range presets {
sdkPreset := codersdk.Preset{
ID: preset.ID,
Name: preset.Name,
ID: preset.ID,
Name: preset.Name,
Default: preset.IsDefault,
}
for _, presetParam := range presetParams {
if presetParam.TemplateVersionPresetID != preset.ID {
Expand Down
92 changes: 92 additions & 0 deletions coderd/presets_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package coderd_test

import (
"slices"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/coder/coder/v2/coderd/coderdtest"
Expand Down Expand Up @@ -138,3 +140,93 @@ func TestTemplateVersionPresets(t *testing.T) {
})
}
}

func TestTemplateVersionPresetsDefault(t *testing.T) {
t.Parallel()

type expectedPreset struct {
name string
isDefault bool
}

cases := []struct {
name string
presets []database.InsertPresetParams
expected []expectedPreset
}{
{
name: "no presets",
presets: nil,
expected: nil,
},
{
name: "single default preset",
presets: []database.InsertPresetParams{
{Name: "Default Preset", IsDefault: true},
},
expected: []expectedPreset{
{name: "Default Preset", isDefault: true},
},
},
{
name: "single non-default preset",
presets: []database.InsertPresetParams{
{Name: "Regular Preset", IsDefault: false},
},
expected: []expectedPreset{
{name: "Regular Preset", isDefault: false},
},
},
{
name: "mixed presets",
presets: []database.InsertPresetParams{
{Name: "Default Preset", IsDefault: true},
{Name: "Regular Preset", IsDefault: false},
},
expected: []expectedPreset{
{name: "Default Preset", isDefault: true},
{name: "Regular Preset", isDefault: false},
},
},
}

for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)

client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)

// Create presets
for _, preset := range tc.presets {
preset.TemplateVersionID = version.ID
_ = dbgen.Preset(t, db, preset)
}

// Get presets via API
userSubject, _, err := httpmw.UserRBACSubject(ctx, db, user.UserID, rbac.ScopeAll)
require.NoError(t, err)
userCtx := dbauthz.As(ctx, userSubject)

gotPresets, err := client.TemplateVersionPresets(userCtx, version.ID)
require.NoError(t, err)

// Verify results
require.Len(t, gotPresets, len(tc.expected))

for _, expected := range tc.expected {
found := slices.ContainsFunc(gotPresets, func(preset codersdk.Preset) bool {
if preset.Name != expected.name {
return false
}

return assert.Equal(t, expected.isDefault, preset.Default)
})
require.True(t, found, "Expected preset %s not found", expected.name)
}
})
}
}
1 change: 1 addition & 0 deletions codersdk/presets.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Preset struct {
ID uuid.UUID
Name string
Parameters []PresetParameter
Default bool
}

type PresetParameter struct {
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/api/schemas.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 10 additions & 8 deletions docs/reference/api/templates.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ replace github.com/aquasecurity/trivy => github.com/coder/trivy v0.0.0-202505271
// https://github.com/spf13/afero/pull/487
replace github.com/spf13/afero => github.com/aslilac/afero v0.0.0-20250403163713-f06e86036696

// TODO: replace once we cut release.
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

Might make sense to wait until that is merged + tagged first?

replace github.com/coder/terraform-provider-coder/v2 => github.com/coder/terraform-provider-coder/v2 v2.5.4-0.20250618121935-71097ea9c886

require (
cdr.dev/slog v1.6.2-0.20241112041820-0ec81e6e67bb
cloud.google.com/go/compute/metadata v0.7.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -928,8 +928,8 @@ github.com/coder/tailscale v1.1.1-0.20250611020837-f14d20d23d8c h1:d/qBIi3Ez7Kko
github.com/coder/tailscale v1.1.1-0.20250611020837-f14d20d23d8c/go.mod h1:l7ml5uu7lFh5hY28lGYM4b/oFSmuPHYX6uk4RAu23Lc=
github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e h1:JNLPDi2P73laR1oAclY6jWzAbucf70ASAvf5mh2cME0=
github.com/coder/terraform-config-inspect v0.0.0-20250107175719-6d06d90c630e/go.mod h1:Gz/z9Hbn+4KSp8A2FBtNszfLSdT2Tn/uAKGuVqqWmDI=
github.com/coder/terraform-provider-coder/v2 v2.5.3 h1:EwqIIQKe/j8bsR4WyDJ3bD0dVdkfVqJ43TwClyGneUU=
github.com/coder/terraform-provider-coder/v2 v2.5.3/go.mod h1:kqP2MW/OF5u3QBRPDt84vn1izKjncICFfv26nSb781I=
github.com/coder/terraform-provider-coder/v2 v2.5.4-0.20250618121935-71097ea9c886 h1:Oo9rlMzBaI+dCr/LQhTQF0H4IJes24tGb+7XRN2aiD4=
github.com/coder/terraform-provider-coder/v2 v2.5.4-0.20250618121935-71097ea9c886/go.mod h1:WrdLSbihuzH1RZhwrU+qmkqEhUbdZT/sjHHdarm5b5g=
github.com/coder/trivy v0.0.0-20250527170238-9416a59d7019 h1:MHkv/W7l9eRAN9gOG0qZ1TLRGWIIfNi92273vPAQ8Fs=
github.com/coder/trivy v0.0.0-20250527170238-9416a59d7019/go.mod h1:eqk+w9RLBmbd/cB5XfPZFuVn77cf/A6fB7qmEVeSmXk=
github.com/coder/websocket v1.8.13 h1:f3QZdXy7uGVz+4uCJy2nTZyM0yTBj8yANEHhqlXZ9FE=
Expand Down
4 changes: 4 additions & 0 deletions site/e2e/provisionerGenerated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions site/src/api/typesGenerated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,7 @@ export const PresetsButNoneSelected: Story = {
{
ID: "preset-1",
Name: "Preset 1",
Default: false,
Parameters: [
{
Name: MockTemplateVersionParameter1.name,
Expand All @@ -135,6 +136,7 @@ export const PresetsButNoneSelected: Story = {
{
ID: "preset-2",
Name: "Preset 2",
Default: true,
Parameters: [
{
Name: MockTemplateVersionParameter2.name,
Expand Down
13 changes: 12 additions & 1 deletion site/src/pages/CreateWorkspacePage/CreateWorkspacePageView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,13 +156,24 @@ export const CreateWorkspacePageView: FC<CreateWorkspacePageViewProps> = ({
setPresetOptions([
{ label: "None", value: "" },
...presets.map((preset) => ({
label: preset.Name,
label: preset.Default ? `${preset.Name} (Default)` : preset.Name,
value: preset.ID,
})),
]);
}, [presets]);

const [selectedPresetIndex, setSelectedPresetIndex] = useState(0);

// Set default preset when presets are loaded
useEffect(() => {
const defaultPreset = presets.find((preset) => preset.Default);
if (defaultPreset) {
// +1 because "None" is at index 0
const defaultIndex =
presets.findIndex((preset) => preset.ID === defaultPreset.ID) + 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

It looks like this fine, but I've been caught by findIndex before while working on presets. Is there any way that findIndex might return -1 here?

setSelectedPresetIndex(defaultIndex);
}
}, [presets]);
const [presetParameterNames, setPresetParameterNames] = useState<string[]>(
[],
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -190,13 +190,25 @@ export const CreateWorkspacePageViewExperimental: FC<
setPresetOptions([
{ label: "None", value: "None" },
...presets.map((preset) => ({
label: preset.Name,
label: preset.Default ? `${preset.Name} (Default)` : preset.Name,
value: preset.ID,
})),
]);
}, [presets]);

const [selectedPresetIndex, setSelectedPresetIndex] = useState(0);

// Set default preset when presets are loaded
useEffect(() => {
const defaultPreset = presets.find((preset) => preset.Default);
if (defaultPreset) {
// +1 because "None" is at index 0
const defaultIndex =
presets.findIndex((preset) => preset.ID === defaultPreset.ID) + 1;
Copy link
Member

Choose a reason for hiding this comment

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

...or I guess it's because everything is this presets array ends up getting offset by one for having "none" at the beginning? that feels really weird, it'd make a lot more sense to me if the numbers were preserved and "none" was represented by null or something

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed; I think we need a wider refactor of the implementation. For now though, I'm going to have to use what's present. @SasSwart WDYT?

Copy link
Contributor

Choose a reason for hiding this comment

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

Coping with the none preset in the FE has felt weird from the start. I'm considering the idea that "None" should be a preset returned like any other from the endpoint that lists presets. it would simplify the FE quite a bit and centralize how me manage it.

setSelectedPresetIndex(defaultIndex);
}
}, [presets]);

const [presetParameterNames, setPresetParameterNames] = useState<string[]>(
[],
);
Expand Down Expand Up @@ -555,6 +567,7 @@ export const CreateWorkspacePageViewExperimental: FC<
<div className="flex flex-col gap-4">
<div className="max-w-lg">
<Select
value={presetOptions[selectedPresetIndex]?.value}
onValueChange={(option) => {
const index = presetOptions.findIndex(
(preset) => preset.value === option,
Expand Down
Loading