mirror of
https://github.com/coder/coder.git
synced 2026-10-31 02:43:21 +08:00
feat: add a template parameter settings page (#27559)
This commit is contained in:
@@ -66,6 +66,17 @@ To use the features described on this page in an existing template:
|
||||
|
||||
Dynamic Parameters features are backwards compatible, so all existing templates may be upgraded in-place.
|
||||
|
||||
## Data sources and cached template data
|
||||
|
||||
Coder reads Terraform `data` sources once, when it imports a template version, and stores the results.
|
||||
Rendering the form evaluates your parameter expressions against those stored results, so a `data` source whose underlying value changes in your cloud or cluster keeps returning the imported value.
|
||||
|
||||
The `coder_workspace_owner` data source is the exception.
|
||||
Coder substitutes the identity of the user filling in the form each time it renders, which is what makes [Identity-Aware Parameters](#identity-aware-parameters-premium) work.
|
||||
|
||||
To pick up a change to any other `data` source, [refresh the template data](../managing-templates/index.md#refresh-template-data).
|
||||
This imports the active version's source files again and publishes the result as a new active version.
|
||||
|
||||
## Features and Capabilities
|
||||
|
||||
Dynamic Parameters introduces three primary enhancements to the standard parameter system:
|
||||
@@ -797,13 +808,15 @@ Ensure that the following version requirements are met:
|
||||
|
||||
Enabling Dynamic Parameters on an existing template requires administrators to publish a new template version.
|
||||
This will resolve the necessary template metadata to render the form.
|
||||
To publish one without editing the template's Terraform, [refresh the template data](../managing-templates/index.md#refresh-template-data).
|
||||
|
||||
### Reverting to classic parameters
|
||||
|
||||
The classic parameter flow is deprecated and can no longer be enabled from the UI.
|
||||
A template can still opt out of Dynamic Parameters by setting the `use_classic_parameter_flow`
|
||||
field through the [templates API](../../../reference/api/templates.md#update-template-settings-by-id),
|
||||
but this opt-out will be removed in a future release.
|
||||
The classic parameter flow is deprecated and will be removed in a future release.
|
||||
If a template does not work with Dynamic Parameters, you can opt that template out.
|
||||
Select **Settings** > **Parameters** on the template, then select **Use parameter compatibility mode for workspace builds**.
|
||||
You can also set the `use_classic_parameter_flow` field through the
|
||||
[templates API](../../../reference/api/templates.md#update-template-settings-by-id).
|
||||
|
||||
If your template's parameters do not work with Dynamic Parameters, please
|
||||
[file an issue](https://github.com/coder/coder/issues/new?labels=parameters) with the `parameters` label.
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
# Working with templates
|
||||
|
||||
You create and edit Coder templates as
|
||||
[Terraform](https://developer.hashicorp.com/terraform/intro) configuration files (`.tf`) and
|
||||
any supporting files, like a README or configuration files for other services.
|
||||
[Terraform](https://developer.hashicorp.com/terraform/intro) configuration files
|
||||
(`.tf`) and any supporting files, like a README or configuration files for other
|
||||
services.
|
||||
|
||||
## Who creates templates?
|
||||
|
||||
@@ -16,8 +17,8 @@ You can give different users and groups access to templates with
|
||||
|
||||
## Creating templates
|
||||
|
||||
The [template builder](../creating-templates.md#template-builder) is
|
||||
the recommended way to create templates. It guides you through selecting a base
|
||||
The [template builder](../creating-templates.md#template-builder) is the
|
||||
recommended way to create templates. It guides you through selecting a base
|
||||
infrastructure template, adding modules (IDEs, tools, integrations), and
|
||||
configuring template settings without writing Terraform.
|
||||
|
||||
@@ -36,13 +37,12 @@ Coder starter templates are also available on our
|
||||
## Community Templates
|
||||
|
||||
As well as Coder's starter templates, you can see a list of community templates
|
||||
by our users
|
||||
[here](../../../../examples/templates/community-templates.md).
|
||||
by our users [here](../../../../examples/templates/community-templates.md).
|
||||
|
||||
## Editing templates
|
||||
|
||||
Our templates are meant to be modified for your use cases. You can edit
|
||||
any template's files directly in the Coder dashboard.
|
||||
Our templates are meant to be modified for your use cases. You can edit any
|
||||
template's files directly in the Coder dashboard.
|
||||
|
||||

|
||||
|
||||
@@ -77,18 +77,45 @@ automatically updated on the next startup.
|
||||
|
||||

|
||||
|
||||
## Refresh template data
|
||||
|
||||
Coder reads Terraform `data` sources once, when it imports a template version.
|
||||
Every workspace built from that version reuses those stored results.
|
||||
|
||||
Refreshing imports the active version's source files again and publishes the
|
||||
result as the new active version. Use it to pick up changes to a `data` source,
|
||||
or to give an older version the metadata that
|
||||
[Dynamic Parameters](../extending-templates/dynamic-parameters.md) needs.
|
||||
|
||||
To refresh a template's data:
|
||||
|
||||
1. Navigate to the template, then select **Settings** > **Parameters**.
|
||||
1. Select **Refresh template data**, then confirm.
|
||||
|
||||
The **Template data** section on that page shows which version is active and
|
||||
when Coder last imported it.
|
||||
|
||||
New workspaces use the refreshed version as soon as the import finishes.
|
||||
Workspaces that are already running stay on their current version until you
|
||||
update them.
|
||||
|
||||
Refreshing a template's data requires permission to update the template, which
|
||||
the [Template Admin](../../users/groups-roles.md#roles) role and above have.
|
||||
|
||||
## Workspace renaming
|
||||
|
||||
Workspace renaming is disabled by default on every template.
|
||||
Enable it per template, once you have confirmed that a rename won't destroy anything.
|
||||
Workspace renaming is disabled by default on every template. Enable it per
|
||||
template, once you have confirmed that a rename won't destroy anything.
|
||||
|
||||
Terraform exposes the workspace name as `data.coder_workspace.me.name`.
|
||||
If a template uses that value in an attribute Terraform can't change in place, renaming the workspace makes Terraform destroy the resource and create a new one on the next build.
|
||||
When that resource is the home volume, the developer loses their data.
|
||||
Terraform exposes the workspace name as `data.coder_workspace.me.name`. If a
|
||||
template uses that value in an attribute Terraform can't change in place,
|
||||
renaming the workspace makes Terraform destroy the resource and create a new one
|
||||
on the next build. When that resource is the home volume, the developer loses
|
||||
their data.
|
||||
|
||||
Other uses of the name are harmless.
|
||||
A Kubernetes label or an environment variable changes without replacing anything.
|
||||
Coder can't tell the two cases apart, so the decision stays with the template admin.
|
||||
Other uses of the name are harmless. A Kubernetes label or an environment
|
||||
variable changes without replacing anything. Coder can't tell the two cases
|
||||
apart, so the decision stays with the template admin.
|
||||
|
||||
### Check whether your template is safe to rename
|
||||
|
||||
@@ -98,27 +125,34 @@ Search the template for uses of the workspace name:
|
||||
grep -rn 'coder_workspace\.[a-z_]*\.name' .
|
||||
```
|
||||
|
||||
For each result, ask whether Terraform would replace the resource if that value changed.
|
||||
Attributes such as a volume name, a disk name, or an instance name usually force replacement.
|
||||
Labels, tags, and environment variables usually don't.
|
||||
For each result, ask whether Terraform would replace the resource if that value
|
||||
changed. Attributes such as a volume name, a disk name, or an instance name
|
||||
usually force replacement. Labels, tags, and environment variables usually
|
||||
don't.
|
||||
|
||||
If a resource would be replaced, reference an immutable identifier instead, such as `data.coder_workspace.me.id`.
|
||||
Refer to [Resource persistence](../extending-templates/resource-persistence.md) for the full set of practices.
|
||||
If a resource would be replaced, reference an immutable identifier instead, such
|
||||
as `data.coder_workspace.me.id`. Refer to
|
||||
[Resource persistence](../extending-templates/resource-persistence.md) for the
|
||||
full set of practices.
|
||||
|
||||
### Enable renaming for a template
|
||||
|
||||
1. Go to the template, then select **Settings**.
|
||||
1. On the **General** page, under **Operations**, select **Allow users to rename their workspaces**.
|
||||
1. On the **General** page, under **Operations**, select **Allow users to rename
|
||||
their workspaces**.
|
||||
1. Select **Save**.
|
||||
|
||||
Developers can then rename a workspace from the workspace's **Settings** page, or with `coder rename`.
|
||||
Developers can then rename a workspace from the workspace's **Settings** page,
|
||||
or with `coder rename`.
|
||||
|
||||
While the setting is off, the workspace name field is disabled and the API rejects renames.
|
||||
While the setting is off, the workspace name field is disabled and the API
|
||||
rejects renames.
|
||||
|
||||
> [!WARNING]
|
||||
> The deployment-wide `CODER_ALLOW_WORKSPACE_RENAMES` option is deprecated.
|
||||
> While it is set, renaming is enabled for every template in the deployment, and the per-template setting can't turn it off.
|
||||
> Unset it, then enable renaming on the templates that need it.
|
||||
> While it is set, renaming is enabled for every template in the deployment,
|
||||
> and the per-template setting can't turn it off. Unset it, then enable
|
||||
> renaming on the templates that need it.
|
||||
|
||||
## Delete templates
|
||||
|
||||
@@ -142,6 +176,8 @@ coder templates delete <template-name>
|
||||
## Next steps
|
||||
|
||||
- [Image management](./image-management.md)
|
||||
- [Dev Containers integration](../../integrations/devcontainers/integration.md) (recommended)
|
||||
- [Envbuilder](../../integrations/devcontainers/envbuilder/index.md) (alternative for environments without Docker)
|
||||
- [Dev Containers integration](../../integrations/devcontainers/integration.md)
|
||||
(recommended)
|
||||
- [Envbuilder](../../integrations/devcontainers/envbuilder/index.md)
|
||||
(alternative for environments without Docker)
|
||||
- [Change management](./change-management.md)
|
||||
|
||||
@@ -227,14 +227,38 @@ export const updateActiveTemplateVersion = (
|
||||
id: versionId,
|
||||
}),
|
||||
onSuccess: async () => {
|
||||
// invalidated because of `active_version_id`
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: templateByNameKey(template.organization_id, template.name),
|
||||
});
|
||||
await Promise.all([
|
||||
// invalidated because of `active_version_id`
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: templateByNameKey(
|
||||
template.organization_name,
|
||||
template.name,
|
||||
),
|
||||
}),
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: templateVersionsQueryKey(template.id),
|
||||
}),
|
||||
]);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const enableTemplateParameterCompatibilityMode = (
|
||||
template: Template,
|
||||
queryClient: QueryClient,
|
||||
) => {
|
||||
return {
|
||||
mutationFn: (compatibilityModeEnabled: boolean) =>
|
||||
API.updateTemplateMeta(template.id, {
|
||||
use_classic_parameter_flow: compatibilityModeEnabled,
|
||||
}),
|
||||
onSuccess: () =>
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: templateByNameKey(template.organization_name, template.name),
|
||||
}),
|
||||
};
|
||||
};
|
||||
|
||||
export const templaceACLAvailable = (
|
||||
templateId: string,
|
||||
options: UsersRequest,
|
||||
|
||||
@@ -16,6 +16,9 @@ export const Sidebar: React.FC = () => {
|
||||
<SettingsSidebarNavItem href="variables">
|
||||
Variables
|
||||
</SettingsSidebarNavItem>
|
||||
<SettingsSidebarNavItem href="parameters">
|
||||
Parameters
|
||||
</SettingsSidebarNavItem>
|
||||
<SettingsSidebarNavItem href="schedule">
|
||||
Schedule
|
||||
</SettingsSidebarNavItem>
|
||||
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
import {
|
||||
keepPreviousData,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "react-query";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
createAndBuildTemplateVersion,
|
||||
enableTemplateParameterCompatibilityMode,
|
||||
templateVersion,
|
||||
templateVersionsQueryKey,
|
||||
updateActiveTemplateVersion,
|
||||
} from "#/api/queries/templates";
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import { pageTitle } from "#/utils/page";
|
||||
import { useTemplateSettings } from "../TemplateSettingsLayout";
|
||||
import { TemplateParametersPageView } from "./TemplateParametersPageView";
|
||||
|
||||
const TemplateParametersPage: React.FC = () => {
|
||||
const { template, permissions } = useTemplateSettings();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const {
|
||||
data: activeVersion,
|
||||
error: activeVersionError,
|
||||
isLoading: isActiveVersionLoading,
|
||||
} = useQuery({
|
||||
...templateVersion(template.active_version_id),
|
||||
placeholderData: keepPreviousData,
|
||||
});
|
||||
const compatibilityModeMutation = useMutation(
|
||||
enableTemplateParameterCompatibilityMode(template, queryClient),
|
||||
);
|
||||
const createAndBuildMutation = useMutation(
|
||||
createAndBuildTemplateVersion(template.organization_name),
|
||||
);
|
||||
const promoteMutation = useMutation(
|
||||
updateActiveTemplateVersion(template, queryClient),
|
||||
);
|
||||
|
||||
if (activeVersionError) {
|
||||
return <ErrorAlert error={activeVersionError} />;
|
||||
}
|
||||
|
||||
if (isActiveVersionLoading || !activeVersion) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<title>{pageTitle(template.name, "Parameters")}</title>
|
||||
|
||||
<TemplateParametersPageView
|
||||
activeVersion={activeVersion}
|
||||
useClassicParameterFlow={template.use_classic_parameter_flow}
|
||||
canUpdate={permissions.canUpdateTemplate}
|
||||
isSaving={compatibilityModeMutation.isPending}
|
||||
isRefreshing={
|
||||
createAndBuildMutation.isPending || promoteMutation.isPending
|
||||
}
|
||||
error={
|
||||
compatibilityModeMutation.error ??
|
||||
createAndBuildMutation.error ??
|
||||
promoteMutation.error
|
||||
}
|
||||
onChangeClassicParameterFlow={async (useClassicParameterFlow) => {
|
||||
compatibilityModeMutation.reset();
|
||||
|
||||
try {
|
||||
await compatibilityModeMutation.mutateAsync(
|
||||
useClassicParameterFlow,
|
||||
);
|
||||
toast.success(
|
||||
useClassicParameterFlow
|
||||
? `${template.display_name} will use parameter compatibility mode.`
|
||||
: `${template.display_name} will use dynamic parameters.`,
|
||||
);
|
||||
} catch {}
|
||||
}}
|
||||
onRefresh={async () => {
|
||||
createAndBuildMutation.reset();
|
||||
promoteMutation.reset();
|
||||
|
||||
try {
|
||||
const newVersion = await createAndBuildMutation.mutateAsync({
|
||||
template_id: template.id,
|
||||
provisioner: "terraform",
|
||||
storage_method: "file",
|
||||
file_id: activeVersion.job.file_id,
|
||||
tags: activeVersion.job.tags,
|
||||
message: "Refreshed template data",
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: templateVersionsQueryKey(template.id),
|
||||
});
|
||||
await promoteMutation.mutateAsync(newVersion.id);
|
||||
toast.success(
|
||||
`Template "${template.name}" data refreshed successfully.`,
|
||||
);
|
||||
} catch {}
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateParametersPage;
|
||||
+107
@@ -0,0 +1,107 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import { expect, fn, userEvent, within } from "storybook/test";
|
||||
import { MockTemplateVersion, mockApiError } from "#/testHelpers/entities";
|
||||
import { TemplateParametersPageView } from "./TemplateParametersPageView";
|
||||
|
||||
const meta: Meta<typeof TemplateParametersPageView> = {
|
||||
title: "pages/TemplateSettingsPage/TemplateParametersPageView",
|
||||
component: TemplateParametersPageView,
|
||||
args: {
|
||||
activeVersion: MockTemplateVersion,
|
||||
useClassicParameterFlow: false,
|
||||
canUpdate: true,
|
||||
isSaving: false,
|
||||
isRefreshing: false,
|
||||
onChangeClassicParameterFlow: fn(),
|
||||
onRefresh: fn(),
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof TemplateParametersPageView>;
|
||||
|
||||
export const Example: Story = {};
|
||||
|
||||
export const Saving: Story = {
|
||||
args: {
|
||||
isSaving: true,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await expect(
|
||||
canvas.queryByRole("checkbox", {
|
||||
name: /use parameter compatibility mode for workspace builds/i,
|
||||
}),
|
||||
).not.toBeInTheDocument();
|
||||
await expect(
|
||||
canvas.getByRole("status", {
|
||||
name: /saving parameter compatibility mode/i,
|
||||
}),
|
||||
).toBeVisible();
|
||||
},
|
||||
};
|
||||
|
||||
export const Refreshing: Story = {
|
||||
args: {
|
||||
isRefreshing: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const NoPermission: Story = {
|
||||
args: {
|
||||
canUpdate: false,
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
await expect(
|
||||
canvas.getByRole("button", { name: /refresh template data/i }),
|
||||
).toBeDisabled();
|
||||
await expect(
|
||||
canvas.getByRole("checkbox", {
|
||||
name: /use parameter compatibility mode for workspace builds/i,
|
||||
}),
|
||||
).toBeDisabled();
|
||||
},
|
||||
};
|
||||
|
||||
export const Failed: Story = {
|
||||
args: {
|
||||
error: mockApiError({
|
||||
message: "Failed to import template version.",
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
export const OptsIntoClassicParameters: Story = {
|
||||
parameters: { pixel: { exclude: true } },
|
||||
play: async ({ args, canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const user = userEvent.setup();
|
||||
|
||||
const checkbox = canvas.getByRole("checkbox", {
|
||||
name: /use parameter compatibility mode for workspace builds/i,
|
||||
});
|
||||
await expect(checkbox).not.toBeChecked();
|
||||
|
||||
await user.click(checkbox);
|
||||
await expect(args.onChangeClassicParameterFlow).toHaveBeenCalledWith(true);
|
||||
},
|
||||
};
|
||||
|
||||
export const ConfirmsBeforeRefreshing: Story = {
|
||||
parameters: { pixel: { exclude: true } },
|
||||
play: async ({ args, canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const user = userEvent.setup();
|
||||
|
||||
await user.click(
|
||||
canvas.getByRole("button", { name: /refresh template data/i }),
|
||||
);
|
||||
|
||||
const dialog = within(await within(document.body).findByRole("dialog"));
|
||||
await expect(args.onRefresh).not.toHaveBeenCalled();
|
||||
|
||||
await user.click(dialog.getByRole("button", { name: "Refresh" }));
|
||||
await expect(args.onRefresh).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
};
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
import { useRef, useState } from "react";
|
||||
import type { TemplateVersion } from "#/api/typesGenerated";
|
||||
import { ErrorAlert } from "#/components/Alert/ErrorAlert";
|
||||
import { Badge } from "#/components/Badge/Badge";
|
||||
import { Button } from "#/components/Button/Button";
|
||||
import { Checkbox } from "#/components/Checkbox/Checkbox";
|
||||
import { ConfirmDialog } from "#/components/Dialog/ConfirmDialog/ConfirmDialog";
|
||||
import { Label } from "#/components/Label/Label";
|
||||
import { Link } from "#/components/Link/Link";
|
||||
import { Separator } from "#/components/Separator/Separator";
|
||||
import {
|
||||
SettingsHeader,
|
||||
SettingsHeaderDescription,
|
||||
SettingsHeaderTitle,
|
||||
} from "#/components/SettingsHeader/SettingsHeader";
|
||||
import { Spinner } from "#/components/Spinner/Spinner";
|
||||
import {
|
||||
StackLabel,
|
||||
StackLabelHelperText,
|
||||
} from "#/components/StackLabel/StackLabel";
|
||||
import { docs } from "#/utils/docs";
|
||||
import { formatDate } from "#/utils/time";
|
||||
|
||||
type TemplateParametersPageViewProps = {
|
||||
activeVersion: TemplateVersion;
|
||||
useClassicParameterFlow: boolean;
|
||||
canUpdate: boolean;
|
||||
isSaving: boolean;
|
||||
isRefreshing: boolean;
|
||||
error?: unknown;
|
||||
onChangeClassicParameterFlow: (useClassicParameterFlow: boolean) => void;
|
||||
onRefresh: () => void;
|
||||
};
|
||||
|
||||
export const TemplateParametersPageView: React.FC<
|
||||
TemplateParametersPageViewProps
|
||||
> = ({
|
||||
activeVersion,
|
||||
useClassicParameterFlow,
|
||||
canUpdate,
|
||||
isSaving,
|
||||
isRefreshing,
|
||||
error,
|
||||
onChangeClassicParameterFlow,
|
||||
onRefresh,
|
||||
}) => {
|
||||
const [isConfirmingRefresh, setIsConfirmingRefresh] = useState(false);
|
||||
const refreshButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const importedAt = activeVersion.job.completed_at;
|
||||
|
||||
return (
|
||||
<div className="flex max-w-prose flex-col gap-8">
|
||||
<SettingsHeader>
|
||||
<SettingsHeaderTitle>Parameters</SettingsHeaderTitle>
|
||||
<SettingsHeaderDescription>
|
||||
Control how this template's parameters are resolved when a workspace
|
||||
is created.
|
||||
</SettingsHeaderDescription>
|
||||
</SettingsHeader>
|
||||
|
||||
{error ? <ErrorAlert error={error} /> : null}
|
||||
|
||||
{!canUpdate && (
|
||||
<p className="m-0 text-sm text-content-secondary">
|
||||
You need permission to update this template to change these settings.
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-start">
|
||||
{/* sm + m-1 keeps the layout from shifting */}
|
||||
<Spinner
|
||||
size="sm"
|
||||
className="m-1 shrink-0"
|
||||
loading={isSaving}
|
||||
label="Saving parameter compatibility mode"
|
||||
>
|
||||
<Checkbox
|
||||
id="use_classic_parameter_flow"
|
||||
name="use_classic_parameter_flow"
|
||||
checked={useClassicParameterFlow}
|
||||
onCheckedChange={(checked) => {
|
||||
onChangeClassicParameterFlow(checked === true);
|
||||
}}
|
||||
disabled={!canUpdate}
|
||||
/>
|
||||
</Spinner>
|
||||
<StackLabel>
|
||||
<Label htmlFor="use_classic_parameter_flow">
|
||||
<span className="flex flex-row items-center gap-2">
|
||||
Use parameter compatibility mode for workspace builds
|
||||
<Badge size="sm" variant="warning">
|
||||
Deprecated
|
||||
</Badge>
|
||||
</span>
|
||||
</Label>
|
||||
<StackLabelHelperText>
|
||||
Turn this on only if this template does not work with dynamic
|
||||
parameters. Compatibility mode will force workspace builds to fall
|
||||
back to the older "Rich parameters" strategy, which is deprecated
|
||||
and will be removed in a future release.
|
||||
</StackLabelHelperText>
|
||||
<StackLabelHelperText>
|
||||
Dynamic parameters are the default since Coder 2.25, and let you use
|
||||
additional form types, identity-aware parameter values, conditional
|
||||
parameters, and data sources. In some cases it may be necessary to
|
||||
refresh template data for dynamic parameters to work properly.
|
||||
</StackLabelHelperText>
|
||||
<Link
|
||||
className="self-start text-xs"
|
||||
href={docs(
|
||||
"/admin/templates/extending-templates/dynamic-parameters",
|
||||
)}
|
||||
>
|
||||
Learn more
|
||||
</Link>
|
||||
</StackLabel>
|
||||
</div>
|
||||
|
||||
<Separator className="my-2" />
|
||||
|
||||
<section className="flex flex-col gap-4">
|
||||
<SettingsHeaderTitle level="h2" hierarchy="secondary">
|
||||
Template data
|
||||
</SettingsHeaderTitle>
|
||||
|
||||
<p className="m-0 text-sm text-content-secondary">
|
||||
Coder caches certain values (like Terraform <code>data</code> sources)
|
||||
when a new template version is created, and every workspace built from
|
||||
that version will reuse the same results. Refreshing will create a new
|
||||
template version, allowing cached values to be updated.
|
||||
</p>
|
||||
|
||||
<dl className="m-0 grid grid-cols-[max-content_1fr] gap-x-6 gap-y-2 text-sm">
|
||||
<dt className="text-content-secondary">Active version</dt>
|
||||
<dd className="m-0">{activeVersion.name}</dd>
|
||||
<dt className="text-content-secondary">Last imported</dt>
|
||||
<dd className="m-0">
|
||||
{importedAt ? formatDate(new Date(importedAt)) : "Unknown"}
|
||||
</dd>
|
||||
</dl>
|
||||
|
||||
<div className="pt-2">
|
||||
<Button
|
||||
ref={refreshButtonRef}
|
||||
disabled={!canUpdate || isRefreshing}
|
||||
onClick={() => setIsConfirmingRefresh(true)}
|
||||
>
|
||||
<Spinner loading={isRefreshing} />
|
||||
Refresh template data
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<ConfirmDialog
|
||||
open={isConfirmingRefresh}
|
||||
type="info"
|
||||
hideCancel={false}
|
||||
title="Refresh template data"
|
||||
description={
|
||||
<>
|
||||
This creates a new template version from the same source as{" "}
|
||||
<strong>{activeVersion.name}</strong> and makes it the active
|
||||
version. New workspaces will use it immediately. Existing workspaces
|
||||
keep running until they are updated.
|
||||
</>
|
||||
}
|
||||
confirmText="Refresh"
|
||||
onClose={() => setIsConfirmingRefresh(false)}
|
||||
onConfirm={() => {
|
||||
setIsConfirmingRefresh(false);
|
||||
onRefresh();
|
||||
}}
|
||||
// Radix returns focus to its trigger on close, and this dialog has
|
||||
// none, so focus would land on <body>.
|
||||
onCloseAutoFocus={(event) => {
|
||||
event.preventDefault();
|
||||
refreshButtonRef.current?.focus();
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -60,7 +60,14 @@ export const TemplateSettingsLayout: FC = () => {
|
||||
}
|
||||
|
||||
const error = templateQuery.isError || permissionsQuery.isError;
|
||||
const template = templateQuery.data;
|
||||
// We override `organization_name` here because we may have fallen back to
|
||||
// `"default"` while fetching if the deployment does not have the
|
||||
// organizations feature enabled, and so we need to make sure consumers do the
|
||||
// same when invalidating queries.
|
||||
const template = {
|
||||
...templateQuery.data,
|
||||
organization_name: organizationName,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "react-query";
|
||||
import { useNavigate, useParams } from "react-router";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
createAndBuildTemplateVersion,
|
||||
@@ -27,10 +27,6 @@ import { TemplateVariablesPageView } from "./TemplateVariablesPageView";
|
||||
|
||||
const TemplateVariablesPage: FC = () => {
|
||||
const getLink = useLinks();
|
||||
const { organization = "default", template: templateName } = useParams() as {
|
||||
organization?: string;
|
||||
template: string;
|
||||
};
|
||||
const { template } = useTemplateSettings();
|
||||
const navigate = useNavigate();
|
||||
const queryClient = useQueryClient();
|
||||
@@ -57,7 +53,7 @@ const TemplateVariablesPage: FC = () => {
|
||||
mutateAsync: sendCreateAndBuildTemplateVersion,
|
||||
error: buildError,
|
||||
isPending: isBuilding,
|
||||
} = useMutation(createAndBuildTemplateVersion(organization));
|
||||
} = useMutation(createAndBuildTemplateVersion(template.organization_id));
|
||||
const {
|
||||
mutateAsync: sendUpdateActiveTemplateVersion,
|
||||
error: publishError,
|
||||
@@ -106,7 +102,9 @@ const TemplateVariablesPage: FC = () => {
|
||||
publishError,
|
||||
}}
|
||||
onCancel={() => {
|
||||
navigate(getLink(linkToTemplate(organization, templateName)));
|
||||
navigate(
|
||||
getLink(linkToTemplate(template.organization_name, template.name)),
|
||||
);
|
||||
}}
|
||||
onSubmit={async (formData) => {
|
||||
const request = filterEmptySensitiveVariables(formData, variables);
|
||||
|
||||
@@ -234,6 +234,12 @@ const TemplateSchedulePage = lazy(
|
||||
"./pages/TemplateSettingsPage/TemplateSchedulePage/TemplateSchedulePage"
|
||||
),
|
||||
);
|
||||
const TemplateParametersPage = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./pages/TemplateSettingsPage/TemplateParametersPage/TemplateParametersPage"
|
||||
),
|
||||
);
|
||||
const TemplateSettingsPage = lazy(
|
||||
() =>
|
||||
import(
|
||||
@@ -499,6 +505,7 @@ const templateRouter = () => {
|
||||
<Route index element={<TemplateSettingsPage />} />
|
||||
<Route path="permissions" element={<TemplatePermissionsPage />} />
|
||||
<Route path="variables" element={<TemplateVariablesPage />} />
|
||||
<Route path="parameters" element={<TemplateParametersPage />} />
|
||||
<Route path="schedule" element={<TemplateSchedulePage />} />
|
||||
</Route>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user