mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat(site): wire template builder entrypoint from templates list page (#26756)
The templates list page "New template" button and the empty-state "View all starter templates" button now link to `/templates/new/builder` when the template builder is enabled in the deployment config, falling back to `/starter-templates` when disabled. The `TemplateBuilderPage` now checks `createTemplates` permission and redirects to `/templates` if the user lacks access. The `templateBuilderBases` query is also gated on `enabled` to avoid a 404 when the feature is disabled. <details> <summary>Implementation details</summary> - `TemplatesPage` fetches `deploymentConfig()` (only when user has `createTemplates` permission) and computes `templateBuilderEnabled` using `=== false` to default safely while loading. - `templateBuilderEnabled` is threaded through `TemplatesPageView` and `EmptyTemplates` to conditionally set link targets. - `TemplateBuilderPage` uses `useAuthenticated()` to check `permissions.createTemplates` before rendering. - Added Storybook story variants for the builder-enabled state. </details> > Generated by Coder Agents on behalf of @jeremyruppel
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
||||
templateBuilderBases,
|
||||
} from "#/api/queries/templateBuilder";
|
||||
import { Loader } from "#/components/Loader/Loader";
|
||||
import { useAuthenticated } from "#/hooks/useAuthenticated";
|
||||
import { linkToTemplate, useLinks } from "#/modules/navigation";
|
||||
import { pageTitle } from "#/utils/page";
|
||||
import { TemplateBuilderPageView } from "./TemplateBuilderPageView";
|
||||
@@ -16,17 +17,25 @@ import { toCreateTemplateRequest } from "./wizardState";
|
||||
const TemplateBuilderPage: FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const getLink = useLinks();
|
||||
const { permissions } = useAuthenticated();
|
||||
const { data, error, isLoading } = useQuery(deploymentConfig());
|
||||
const basesQuery = useQuery(templateBuilderBases());
|
||||
const createMutation = useMutation(createTemplateFromBuilder());
|
||||
|
||||
const builderDisabled = data?.config?.template_builder?.disabled ?? false;
|
||||
|
||||
const basesQuery = useQuery({
|
||||
...templateBuilderBases(),
|
||||
enabled: !builderDisabled && !isLoading && permissions.createTemplates,
|
||||
});
|
||||
|
||||
if (isLoading) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
// If the template builder is disabled in the deployment config,
|
||||
// redirect to the new template page.
|
||||
const builderDisabled = data?.config?.template_builder?.disabled ?? false;
|
||||
if (!permissions.createTemplates) {
|
||||
return <Navigate to="/templates" replace />;
|
||||
}
|
||||
|
||||
if (builderDisabled) {
|
||||
return <Navigate to="/templates/new" replace />;
|
||||
}
|
||||
|
||||
@@ -35,12 +35,14 @@ const findFeaturedExamples = (examples: TemplateExample[]) => {
|
||||
|
||||
interface EmptyTemplatesProps {
|
||||
canCreateTemplates: boolean;
|
||||
templateBuilderEnabled: boolean;
|
||||
examples: TemplateExample[];
|
||||
isUsingFilter: boolean;
|
||||
}
|
||||
|
||||
export const EmptyTemplates: FC<EmptyTemplatesProps> = ({
|
||||
canCreateTemplates,
|
||||
templateBuilderEnabled,
|
||||
examples,
|
||||
isUsingFilter,
|
||||
}) => {
|
||||
@@ -76,7 +78,13 @@ export const EmptyTemplates: FC<EmptyTemplatesProps> = ({
|
||||
))}
|
||||
</div>
|
||||
<Button size="sm" asChild className="rounded-full">
|
||||
<RouterLink to="/starter-templates">
|
||||
<RouterLink
|
||||
to={
|
||||
templateBuilderEnabled
|
||||
? "/templates/new/builder"
|
||||
: "/starter-templates"
|
||||
}
|
||||
>
|
||||
View all starter templates
|
||||
</RouterLink>
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { FC } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { useSearchParams } from "react-router";
|
||||
import { deploymentConfig } from "#/api/queries/deployment";
|
||||
import { workspacePermissionsByOrganization } from "#/api/queries/organizations";
|
||||
import { templateExamples, templates } from "#/api/queries/templates";
|
||||
import { type UseFilterResult, useFilter } from "#/components/Filter/Filter";
|
||||
@@ -33,6 +34,15 @@ const TemplatesPage: FC = () => {
|
||||
),
|
||||
);
|
||||
|
||||
const deploymentConfigQuery = useQuery({
|
||||
...deploymentConfig(),
|
||||
enabled: permissions.createTemplates,
|
||||
});
|
||||
const templateBuilderEnabled =
|
||||
deploymentConfigQuery.isSuccess &&
|
||||
!deploymentConfigQuery.data?.config?.template_builder?.disabled &&
|
||||
permissions.createTemplates;
|
||||
|
||||
const error =
|
||||
templatesQuery.error ||
|
||||
examplesQuery.error ||
|
||||
@@ -46,6 +56,7 @@ const TemplatesPage: FC = () => {
|
||||
filterState={filterState}
|
||||
showOrganizations={showOrganizations}
|
||||
canCreateTemplates={permissions.createTemplates}
|
||||
templateBuilderEnabled={templateBuilderEnabled}
|
||||
examples={examplesQuery.data}
|
||||
templates={templatesQuery.data}
|
||||
workspacePermissions={workspacePermissionsQuery.data}
|
||||
|
||||
@@ -31,6 +31,7 @@ const meta: Meta<typeof TemplatesPageView> = {
|
||||
component: TemplatesPageView,
|
||||
args: {
|
||||
filterState: defaultFilterProps,
|
||||
templateBuilderEnabled: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -99,6 +100,13 @@ export const WithTemplates: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const WithTemplatesBuilderEnabled: Story = {
|
||||
args: {
|
||||
...WithTemplates.args,
|
||||
templateBuilderEnabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const MultipleOrganizations: Story = {
|
||||
args: {
|
||||
...WithTemplates.args,
|
||||
@@ -159,6 +167,16 @@ export const EmptyCanCreate: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyCanCreateWithBuilder: Story = {
|
||||
args: {
|
||||
canCreateTemplates: true,
|
||||
templateBuilderEnabled: true,
|
||||
error: undefined,
|
||||
templates: [],
|
||||
examples: [MockTemplateExample, MockTemplateExample2],
|
||||
},
|
||||
};
|
||||
|
||||
export const EmptyCannotCreate: Story = {
|
||||
args: {
|
||||
error: undefined,
|
||||
|
||||
@@ -196,6 +196,7 @@ interface TemplatesPageViewProps {
|
||||
filterState: TemplateFilterState;
|
||||
showOrganizations: boolean;
|
||||
canCreateTemplates: boolean;
|
||||
templateBuilderEnabled: boolean;
|
||||
examples: TemplateExample[] | undefined;
|
||||
templates: Template[] | undefined;
|
||||
workspacePermissions: Record<string, WorkspacePermissions> | undefined;
|
||||
@@ -206,6 +207,7 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = ({
|
||||
filterState,
|
||||
showOrganizations,
|
||||
canCreateTemplates,
|
||||
templateBuilderEnabled,
|
||||
examples,
|
||||
templates,
|
||||
workspacePermissions,
|
||||
@@ -219,7 +221,13 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = ({
|
||||
actions={
|
||||
canCreateTemplates && (
|
||||
<Button asChild size="lg">
|
||||
<RouterLink to="/starter-templates">
|
||||
<RouterLink
|
||||
to={
|
||||
templateBuilderEnabled
|
||||
? "/templates/new/builder"
|
||||
: "/starter-templates"
|
||||
}
|
||||
>
|
||||
<PlusIcon />
|
||||
New template
|
||||
</RouterLink>
|
||||
@@ -266,6 +274,7 @@ export const TemplatesPageView: FC<TemplatesPageViewProps> = ({
|
||||
{isEmpty ? (
|
||||
<EmptyTemplates
|
||||
canCreateTemplates={canCreateTemplates}
|
||||
templateBuilderEnabled={templateBuilderEnabled}
|
||||
examples={examples ?? []}
|
||||
isUsingFilter={filterState.filter.used}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user