mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
feat: create experimental template embed page for dynamic params (#17999)
resolves coder/preview#58
This commit is contained in:
@@ -64,6 +64,7 @@
|
||||
"@radix-ui/react-radio-group": "1.2.3",
|
||||
"@radix-ui/react-scroll-area": "1.2.3",
|
||||
"@radix-ui/react-select": "2.1.4",
|
||||
"@radix-ui/react-separator": "1.1.7",
|
||||
"@radix-ui/react-slider": "1.2.2",
|
||||
"@radix-ui/react-slot": "1.1.1",
|
||||
"@radix-ui/react-switch": "1.1.1",
|
||||
|
||||
Generated
+78
@@ -106,6 +106,9 @@ importers:
|
||||
'@radix-ui/react-select':
|
||||
specifier: 2.1.4
|
||||
version: 2.1.4(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-separator':
|
||||
specifier: 1.1.7
|
||||
version: 1.1.7(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
'@radix-ui/react-slider':
|
||||
specifier: 1.2.2
|
||||
version: 1.2.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -1615,6 +1618,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.2':
|
||||
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==, tarball: https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-context@1.1.1':
|
||||
resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==, tarball: https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz}
|
||||
peerDependencies:
|
||||
@@ -1833,6 +1845,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-primitive@2.1.3':
|
||||
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==, tarball: https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-radio-group@1.2.3':
|
||||
resolution: {integrity: sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==, tarball: https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.3.tgz}
|
||||
peerDependencies:
|
||||
@@ -1898,6 +1923,19 @@ packages:
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-separator@1.1.7':
|
||||
resolution: {integrity: sha512-0HEb8R9E8A+jZjvmFCy/J4xhbXy3TV+9XSnGJ3KvTtjlIUy/YQ/p6UYZvi7YbeoeXdyU9+Y3scizK6hkY37baA==, tarball: https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.7.tgz}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
'@types/react-dom': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
'@types/react-dom':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-slider@1.2.2':
|
||||
resolution: {integrity: sha512-sNlU06ii1/ZcbHf8I9En54ZPW0Vil/yPVg4vQMcFNjrIx51jsHbFl1HYHQvCIWJSr1q0ZmA+iIs/ZTv8h7HHSA==, tarball: https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.2.tgz}
|
||||
peerDependencies:
|
||||
@@ -1938,6 +1976,15 @@ packages:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-slot@1.2.3':
|
||||
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==, tarball: https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz}
|
||||
peerDependencies:
|
||||
'@types/react': '*'
|
||||
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
|
||||
peerDependenciesMeta:
|
||||
'@types/react':
|
||||
optional: true
|
||||
|
||||
'@radix-ui/react-switch@1.1.1':
|
||||
resolution: {integrity: sha512-diPqDDoBcZPSicYoMWdWx+bCPuTRH4QSp9J+65IvtdS0Kuzt67bI6n32vCj8q6NZmYW/ah+2orOtMwcX5eQwIg==, tarball: https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.1.tgz}
|
||||
peerDependencies:
|
||||
@@ -7792,6 +7839,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.12
|
||||
|
||||
'@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.12)(react@18.3.1)':
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.12
|
||||
|
||||
'@radix-ui/react-context@1.1.1(@types/react@18.3.12)(react@18.3.1)':
|
||||
dependencies:
|
||||
react: 18.3.1
|
||||
@@ -8014,6 +8067,15 @@ snapshots:
|
||||
'@types/react': 18.3.12
|
||||
'@types/react-dom': 18.3.1
|
||||
|
||||
'@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/react-slot': 1.2.3(@types/react@18.3.12)(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.12
|
||||
'@types/react-dom': 18.3.1
|
||||
|
||||
'@radix-ui/react-radio-group@1.2.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.1
|
||||
@@ -8112,6 +8174,15 @@ snapshots:
|
||||
'@types/react': 18.3.12
|
||||
'@types/react-dom': 18.3.1
|
||||
|
||||
'@radix-ui/react-separator@1.1.7(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
react: 18.3.1
|
||||
react-dom: 18.3.1(react@18.3.1)
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.12
|
||||
'@types/react-dom': 18.3.1
|
||||
|
||||
'@radix-ui/react-slider@1.2.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/number': 1.1.0
|
||||
@@ -8152,6 +8223,13 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.12
|
||||
|
||||
'@radix-ui/react-slot@1.2.3(@types/react@18.3.12)(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.12)(react@18.3.1)
|
||||
react: 18.3.1
|
||||
optionalDependencies:
|
||||
'@types/react': 18.3.12
|
||||
|
||||
'@radix-ui/react-switch@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
|
||||
dependencies:
|
||||
'@radix-ui/primitive': 1.1.0
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import * as SeparatorPrimitive from "@radix-ui/react-separator";
|
||||
/**
|
||||
* Copied from shadc/ui on 06/20/2025
|
||||
* @see {@link https://ui.shadcn.com/docs/components/separator}
|
||||
*/
|
||||
import type * as React from "react";
|
||||
|
||||
import { cn } from "utils/cn";
|
||||
|
||||
function Separator({
|
||||
className,
|
||||
orientation = "horizontal",
|
||||
decorative = true,
|
||||
...props
|
||||
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
||||
return (
|
||||
<SeparatorPrimitive.Root
|
||||
data-slot="separator"
|
||||
decorative={decorative}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"bg-border shrink-0 data-[orientation=horizontal]:h-px data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-px",
|
||||
className,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Separator };
|
||||
@@ -0,0 +1,17 @@
|
||||
/**
|
||||
* Copied from shadc/ui on 06/20/2025
|
||||
* @see {@link https://ui.shadcn.com/docs/components/skeleton}
|
||||
*/
|
||||
import { cn } from "utils/cn";
|
||||
|
||||
function Skeleton({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="skeleton"
|
||||
className={cn("bg-surface-tertiary animate-pulse rounded-md", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Skeleton };
|
||||
@@ -52,7 +52,7 @@ interface DynamicParameterProps {
|
||||
onChange: (value: string) => void;
|
||||
disabled?: boolean;
|
||||
isPreset?: boolean;
|
||||
autofill: boolean;
|
||||
autofill?: boolean;
|
||||
}
|
||||
|
||||
export const DynamicParameter: FC<DynamicParameterProps> = ({
|
||||
@@ -873,7 +873,6 @@ interface DiagnosticsProps {
|
||||
diagnostics: PreviewParameter["diagnostics"];
|
||||
}
|
||||
|
||||
// Displays a diagnostic with a border, icon and background color
|
||||
export const Diagnostics: FC<DiagnosticsProps> = ({ diagnostics }) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
|
||||
@@ -29,8 +29,8 @@ import { type FormikContextType, useFormik } from "formik";
|
||||
import type { ExternalAuthPollingState } from "hooks/useExternalAuth";
|
||||
import { ArrowLeft, CircleHelp } from "lucide-react";
|
||||
import { useSyncFormParameters } from "modules/hooks/useSyncFormParameters";
|
||||
import { Diagnostics } from "modules/workspaces/DynamicParameter/DynamicParameter";
|
||||
import {
|
||||
Diagnostics,
|
||||
DynamicParameter,
|
||||
getInitialParameterValues,
|
||||
useValidationSchemaForDynamicParameters,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { templateByName } from "api/queries/templates";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Loader } from "components/Loader/Loader";
|
||||
import type { FC } from "react";
|
||||
import { useQuery } from "react-query";
|
||||
import { useParams } from "react-router-dom";
|
||||
import TemplateEmbedPage from "./TemplateEmbedPage";
|
||||
import TemplateEmbedPageExperimental from "./TemplateEmbedPageExperimental";
|
||||
|
||||
const TemplateEmbedExperimentRouter: FC = () => {
|
||||
const { organization: organizationName = "default", template: templateName } =
|
||||
useParams() as { organization?: string; template: string };
|
||||
const templateQuery = useQuery(
|
||||
templateByName(organizationName, templateName),
|
||||
);
|
||||
|
||||
if (templateQuery.isError) {
|
||||
return <ErrorAlert error={templateQuery.error} />;
|
||||
}
|
||||
if (!templateQuery.data) {
|
||||
return <Loader />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{templateQuery.data?.use_classic_parameter_flow ? (
|
||||
<TemplateEmbedPage />
|
||||
) : (
|
||||
<TemplateEmbedPageExperimental />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateEmbedExperimentRouter;
|
||||
@@ -97,8 +97,8 @@ export const TemplateEmbedPageView: FC<TemplateEmbedPageViewProps> = ({
|
||||
{!buttonValues || !templateParameters ? (
|
||||
<Loader />
|
||||
) : (
|
||||
<div css={{ display: "flex", alignItems: "flex-start", gap: 48 }}>
|
||||
<div css={{ flex: 1, maxWidth: 400 }}>
|
||||
<div className="flex items-start gap-12">
|
||||
<div className="max-w-3xl">
|
||||
<VerticalForm>
|
||||
<FormSection
|
||||
title="Creation mode"
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
import CheckOutlined from "@mui/icons-material/CheckOutlined";
|
||||
import FileCopyOutlined from "@mui/icons-material/FileCopyOutlined";
|
||||
import { API } from "api/api";
|
||||
import { DetailedError } from "api/errors";
|
||||
import type {
|
||||
DynamicParametersRequest,
|
||||
DynamicParametersResponse,
|
||||
FriendlyDiagnostic,
|
||||
PreviewParameter,
|
||||
Template,
|
||||
} from "api/typesGenerated";
|
||||
import { ErrorAlert } from "components/Alert/ErrorAlert";
|
||||
import { Button } from "components/Button/Button";
|
||||
import { Label } from "components/Label/Label";
|
||||
import { RadioGroup, RadioGroupItem } from "components/RadioGroup/RadioGroup";
|
||||
import { Separator } from "components/Separator/Separator";
|
||||
import { Skeleton } from "components/Skeleton/Skeleton";
|
||||
import { useAuthenticated } from "hooks";
|
||||
import { useEffectEvent } from "hooks/hookPolyfills";
|
||||
import { useClipboard } from "hooks/useClipboard";
|
||||
import {
|
||||
Diagnostics,
|
||||
DynamicParameter,
|
||||
} from "modules/workspaces/DynamicParameter/DynamicParameter";
|
||||
import { useTemplateLayoutContext } from "pages/TemplatePage/TemplateLayout";
|
||||
import { type FC, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Helmet } from "react-helmet-async";
|
||||
import { pageTitle } from "utils/page";
|
||||
|
||||
type ButtonValues = Record<string, string>;
|
||||
|
||||
const TemplateEmbedPageExperimental: FC = () => {
|
||||
const { template } = useTemplateLayoutContext();
|
||||
const { user: me } = useAuthenticated();
|
||||
const [latestResponse, setLatestResponse] =
|
||||
useState<DynamicParametersResponse | null>(null);
|
||||
const wsResponseId = useRef<number>(-1);
|
||||
const ws = useRef<WebSocket | null>(null);
|
||||
const [wsError, setWsError] = useState<Error | null>(null);
|
||||
|
||||
const sendMessage = useEffectEvent(
|
||||
(formValues: Record<string, string>, ownerId?: string) => {
|
||||
const request: DynamicParametersRequest = {
|
||||
id: wsResponseId.current + 1,
|
||||
owner_id: me.id,
|
||||
inputs: formValues,
|
||||
};
|
||||
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
|
||||
ws.current.send(JSON.stringify(request));
|
||||
wsResponseId.current = wsResponseId.current + 1;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const onMessage = useEffectEvent((response: DynamicParametersResponse) => {
|
||||
if (latestResponse && latestResponse?.id >= response.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
setLatestResponse(response);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!template.active_version_id || !me) {
|
||||
return;
|
||||
}
|
||||
|
||||
const socket = API.templateVersionDynamicParameters(
|
||||
template.active_version_id,
|
||||
me.id,
|
||||
{
|
||||
onMessage,
|
||||
onError: (error) => {
|
||||
if (ws.current === socket) {
|
||||
setWsError(error);
|
||||
}
|
||||
},
|
||||
onClose: () => {
|
||||
if (ws.current === socket) {
|
||||
setWsError(
|
||||
new DetailedError(
|
||||
"Websocket connection for dynamic parameters unexpectedly closed.",
|
||||
"Refresh the page to reset the form.",
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
ws.current = socket;
|
||||
|
||||
return () => {
|
||||
socket.close();
|
||||
};
|
||||
}, [template.active_version_id, onMessage, me]);
|
||||
|
||||
const sortedParams = useMemo(() => {
|
||||
if (!latestResponse?.parameters) {
|
||||
return [];
|
||||
}
|
||||
return [...latestResponse.parameters].sort((a, b) => a.order - b.order);
|
||||
}, [latestResponse?.parameters]);
|
||||
|
||||
const isLoading =
|
||||
ws.current?.readyState === WebSocket.CONNECTING || !latestResponse;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<title>{pageTitle(template.name)}</title>
|
||||
</Helmet>
|
||||
<TemplateEmbedPageView
|
||||
template={template}
|
||||
parameters={sortedParams}
|
||||
diagnostics={latestResponse?.diagnostics ?? []}
|
||||
error={wsError}
|
||||
sendMessage={sendMessage}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
interface TemplateEmbedPageViewProps {
|
||||
template: Template;
|
||||
parameters: PreviewParameter[];
|
||||
diagnostics: readonly FriendlyDiagnostic[];
|
||||
error: unknown;
|
||||
sendMessage: (message: Record<string, string>) => void;
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const TemplateEmbedPageView: FC<TemplateEmbedPageViewProps> = ({
|
||||
template,
|
||||
parameters,
|
||||
diagnostics,
|
||||
error,
|
||||
sendMessage,
|
||||
isLoading,
|
||||
}) => {
|
||||
const [formState, setFormState] = useState<{
|
||||
mode: "manual" | "auto";
|
||||
paramValues: Record<string, string>;
|
||||
}>({
|
||||
mode: "manual",
|
||||
paramValues: {},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (parameters) {
|
||||
const serverParamValues: Record<string, string> = {};
|
||||
for (const p of parameters) {
|
||||
const initialVal = p.value?.valid ? p.value.value : "";
|
||||
serverParamValues[p.name] = initialVal;
|
||||
}
|
||||
setFormState((prev) => ({ ...prev, paramValues: serverParamValues }));
|
||||
}
|
||||
}, [parameters]);
|
||||
|
||||
const buttonValues = useMemo(() => {
|
||||
const values: ButtonValues = { mode: formState.mode };
|
||||
for (const [key, value] of Object.entries(formState.paramValues)) {
|
||||
values[`param.${key}`] = value;
|
||||
}
|
||||
return values;
|
||||
}, [formState]);
|
||||
|
||||
const handleChange = (
|
||||
changedParamInfo: PreviewParameter,
|
||||
newValue: string,
|
||||
) => {
|
||||
const newParamValues = {
|
||||
...formState.paramValues,
|
||||
[changedParamInfo.name]: newValue,
|
||||
};
|
||||
setFormState((prev) => ({ ...prev, paramValues: newParamValues }));
|
||||
|
||||
const formInputsToSend: Record<string, string> = { ...newParamValues };
|
||||
for (const p of parameters) {
|
||||
if (!(p.name in formInputsToSend)) {
|
||||
formInputsToSend[p.name] = p.value?.valid ? p.value.value : "";
|
||||
}
|
||||
}
|
||||
|
||||
sendMessage(formInputsToSend);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex items-start gap-12">
|
||||
<div className="w-full flex flex-col gap-5 max-w-screen-md">
|
||||
{isLoading ? (
|
||||
<div className="flex flex-col gap-9">
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-5 w-1/3" />
|
||||
<Skeleton className="h-9 w-full" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-5 w-1/3" />
|
||||
<Skeleton className="h-9 w-full" />
|
||||
</div>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Skeleton className="h-5 w-1/3" />
|
||||
<Skeleton className="h-9 w-full" />
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{Boolean(error) && <ErrorAlert error={error} />}
|
||||
{diagnostics.length > 0 && (
|
||||
<Diagnostics diagnostics={diagnostics} />
|
||||
)}
|
||||
<div className="flex flex-col gap-9">
|
||||
<section className="flex flex-col gap-2">
|
||||
<div>
|
||||
<h2 className="text-lg font-bold m-0">Creation mode</h2>
|
||||
<p className="text-sm text-content-secondary m-0">
|
||||
When set to automatic mode, clicking the button will
|
||||
create the workspace automatically without displaying a
|
||||
form to the user.
|
||||
</p>
|
||||
</div>
|
||||
<RadioGroup
|
||||
value={formState.mode}
|
||||
onValueChange={(v) => {
|
||||
setFormState((prev) => ({
|
||||
...prev,
|
||||
mode: v as "manual" | "auto",
|
||||
}));
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="manual" id="manual" />
|
||||
<Label htmlFor={"manual"} className="cursor-pointer">
|
||||
Manual
|
||||
</Label>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<RadioGroupItem value="auto" id="automatic" />
|
||||
<Label htmlFor={"automatic"} className="cursor-pointer">
|
||||
Automatic
|
||||
</Label>
|
||||
</div>
|
||||
</RadioGroup>
|
||||
</section>
|
||||
|
||||
<Separator />
|
||||
|
||||
{parameters.length > 0 && (
|
||||
<div className="flex flex-col gap-9">
|
||||
{parameters.map((parameter) => {
|
||||
const isDisabled = parameter.styling?.disabled;
|
||||
return (
|
||||
<DynamicParameter
|
||||
key={parameter.name}
|
||||
parameter={parameter}
|
||||
onChange={(value) => handleChange(parameter, value)}
|
||||
disabled={isDisabled}
|
||||
value={formState.paramValues[parameter.name] || ""}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ButtonPreview template={template} buttonValues={buttonValues} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
function getClipboardCopyContent(
|
||||
templateName: string,
|
||||
organization: string,
|
||||
buttonValues: ButtonValues | undefined,
|
||||
): string {
|
||||
const deploymentUrl = `${window.location.protocol}//${window.location.host}`;
|
||||
const createWorkspaceUrl = `${deploymentUrl}/templates/${organization}/${templateName}/workspace`;
|
||||
const createWorkspaceParams = new URLSearchParams(buttonValues);
|
||||
const buttonUrl = `${createWorkspaceUrl}?${createWorkspaceParams.toString()}`;
|
||||
|
||||
return `[](${buttonUrl})`;
|
||||
}
|
||||
|
||||
interface ButtonPreviewProps {
|
||||
template: Template;
|
||||
buttonValues: ButtonValues | undefined;
|
||||
}
|
||||
|
||||
const ButtonPreview: FC<ButtonPreviewProps> = ({ template, buttonValues }) => {
|
||||
const clipboard = useClipboard({
|
||||
textToCopy: getClipboardCopyContent(
|
||||
template.name,
|
||||
template.organization_name,
|
||||
buttonValues,
|
||||
),
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className="sticky top-10 flex gap-16 h-96 flex-1 flex-col items-center justify-center
|
||||
rounded-lg border border-border border-solid bg-surface-secondary"
|
||||
>
|
||||
<img src="/open-in-coder.svg" alt="Open in Coder button" />
|
||||
<Button
|
||||
variant="default"
|
||||
onClick={clipboard.copyToClipboard}
|
||||
disabled={clipboard.showCopiedSuccess}
|
||||
>
|
||||
{clipboard.showCopiedSuccess ? <CheckOutlined /> : <FileCopyOutlined />}{" "}
|
||||
Copy button code
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TemplateEmbedPageExperimental;
|
||||
+6
-3
@@ -273,8 +273,11 @@ const ProvisionersPage = lazy(
|
||||
"./pages/OrganizationSettingsPage/OrganizationProvisionersPage/OrganizationProvisionersPage"
|
||||
),
|
||||
);
|
||||
const TemplateEmbedPage = lazy(
|
||||
() => import("./pages/TemplatePage/TemplateEmbedPage/TemplateEmbedPage"),
|
||||
const TemplateEmbedExperimentRouter = lazy(
|
||||
() =>
|
||||
import(
|
||||
"./pages/TemplatePage/TemplateEmbedPage/TemplateEmbedExperimentRouter"
|
||||
),
|
||||
);
|
||||
const TemplateInsightsPage = lazy(
|
||||
() =>
|
||||
@@ -346,7 +349,7 @@ const templateRouter = () => {
|
||||
<Route path="files" element={<TemplateFilesPage />} />
|
||||
<Route path="resources" element={<TemplateResourcesPage />} />
|
||||
<Route path="versions" element={<TemplateVersionsPage />} />
|
||||
<Route path="embed" element={<TemplateEmbedPage />} />
|
||||
<Route path="embed" element={<TemplateEmbedExperimentRouter />} />
|
||||
<Route path="insights" element={<TemplateInsightsPage />} />
|
||||
</Route>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user