diff --git a/src/client/App.tsx b/src/client/App.tsx
index d440302..1587375 100644
--- a/src/client/App.tsx
+++ b/src/client/App.tsx
@@ -178,14 +178,16 @@ export const App = () => {
rel="noreferrer"
className="font-light text-content-secondary text-sm hover:text-content-primary"
>
+ (link opens in new tab)
Coder
+ (link opens in new tab)
Docs
@@ -195,7 +197,7 @@ export const App = () => {
{/* EDITOR */}
-
+
@@ -325,6 +327,7 @@ const ExampleSelector: FC = () => {
return (
+ (link opens in new tab)
{title}
diff --git a/src/client/Editor.tsx b/src/client/Editor.tsx
index 87c5fc8..09eb5e6 100644
--- a/src/client/Editor.tsx
+++ b/src/client/Editor.tsx
@@ -1,3 +1,13 @@
+import { Editor as MonacoEditor } from "@monaco-editor/react";
+import {
+ CheckIcon,
+ ChevronDownIcon,
+ CopyIcon,
+ FileJsonIcon,
+ SettingsIcon,
+ ZapIcon,
+} from "lucide-react";
+import { type FC, useEffect, useState } from "react";
import { Button } from "@/client/components/Button";
import {
DropdownMenu,
@@ -13,57 +23,38 @@ import {
TooltipContent,
TooltipTrigger,
} from "@/client/components/Tooltip";
+import { useEditor } from "@/client/contexts/editor";
import { useTheme } from "@/client/contexts/theme";
-import { multiSelect, radio, switchInput, textInput } from "@/client/snippets";
-import type { ParameterFormType } from "@/gen/types";
+import { type SnippetFunc, snippets } from "@/client/snippets";
+import type { ParameterWithSource } from "@/gen/types";
import { cn } from "@/utils/cn";
-import { Editor as MonacoEditor } from "@monaco-editor/react";
-import {
- CheckIcon,
- ChevronDownIcon,
- CopyIcon,
- FileJsonIcon,
- RadioIcon,
- SettingsIcon,
- SquareMousePointerIcon,
- TextCursorInputIcon,
- ToggleLeftIcon,
- ZapIcon,
-} from "lucide-react";
-import { type FC, useEffect, useRef, useState } from "react";
-import { useEditor } from "@/client/contexts/editor";
type EditorProps = {
code: string;
setCode: React.Dispatch>;
+ parameters: ParameterWithSource[];
};
-export const Editor: FC = ({ code, setCode }) => {
+export const Editor: FC = ({ code, setCode, parameters }) => {
const { appliedTheme } = useTheme();
const editorRef = useEditor();
- const [codeCopied, setCodeCopied] = useState(() => false);
- const copyTimeoutId = useRef | undefined>(
- undefined,
- );
-
const [tab, setTab] = useState(() => "code");
+ const [codeCopied, setCodeCopied] = useState(() => false);
+
const onCopy = () => {
navigator.clipboard.writeText(code);
setCodeCopied(() => true);
};
- const onAddSnippet = (formType: ParameterFormType) => {
- if (formType === "input") {
- setCode(`${code.trimEnd()}\n\n${textInput}\n`);
- } else if (formType === "radio") {
- setCode(`${code.trimEnd()}\n\n${radio}\n`);
- } else if (formType === "multi-select") {
- setCode(`${code.trimEnd()}\n\n${multiSelect}\n`);
- } else if (formType === "switch") {
- setCode(`${code.trimEnd()}\n\n${switchInput}\n`);
- }
+ const onAddSnippet = (name: string, snippet: SnippetFunc) => {
+ const nameCount = parameters.filter((p) => p.name.startsWith(name)).length;
+
+ const nextInOrder = 1 + Math.max(0, ...parameters.map((p) => p.order));
+ const newName = nameCount > 0 ? `${name}-${nameCount}` : name;
+ const newSnippet = snippet(newName, nextInOrder);
+ setCode(`${code.trimEnd()}\n\n${newSnippet}\n`);
};
useEffect(() => {
@@ -71,13 +62,11 @@ export const Editor: FC = ({ code, setCode }) => {
return;
}
- clearTimeout(copyTimeoutId.current);
-
- copyTimeoutId.current = setTimeout(() => {
+ const copyTimeoutId = setTimeout(() => {
setCodeCopied(() => false);
}, 1000);
- return () => clearTimeout(copyTimeoutId.current);
+ return () => clearTimeout(copyTimeoutId);
}, [codeCopied]);
return (
@@ -116,23 +105,17 @@ export const Editor: FC = ({ code, setCode }) => {
- onAddSnippet("input")}>
-
- Text input
-
- onAddSnippet("multi-select")}
- >
-
- Multi-select
-
- onAddSnippet("radio")}>
-
- Radio
-
- onAddSnippet("switch")}>
- Switches
-
+ {snippets.map(
+ ({ name, label, icon: Icon, snippet }, index) => (
+ onAddSnippet(name, snippet)}
+ >
+
+ {label}
+
+ ),
+ )}
diff --git a/src/client/Preview.tsx b/src/client/Preview.tsx
index 9ca2d27..f40c6eb 100644
--- a/src/client/Preview.tsx
+++ b/src/client/Preview.tsx
@@ -239,9 +239,12 @@ const PreviewEmptyState = () => {
+ (link opens in new tab)
Read the docs
diff --git a/src/client/snippets.ts b/src/client/snippets.ts
index 26b8fef..f32c7e9 100644
--- a/src/client/snippets.ts
+++ b/src/client/snippets.ts
@@ -1,3 +1,15 @@
+import {
+ LetterTextIcon,
+ type LucideIcon,
+ RadioIcon,
+ Rows3Icon,
+ Settings2Icon,
+ SquareMousePointerIcon,
+ TagIcon,
+ TextCursorInputIcon,
+ ToggleLeftIcon,
+} from "lucide-react";
+
export const defaultCode = `terraform {
required_providers {
coder = {
@@ -7,25 +19,104 @@ export const defaultCode = `terraform {
}
}`;
-export const textInput = `data "coder_parameter" "project-name" {
- display_name = "An input"
- name = "an-input"
- description = "What is the name of your project?"
- order = 4
+export type SnippetFunc = (name: string, order: number) => string;
+type Snippet = {
+ name: string;
+ label: string;
+ icon: LucideIcon;
+ snippet: SnippetFunc;
+};
+
+export const input: SnippetFunc = (
+ name,
+ order,
+) => `data "coder_parameter" "text-input" {
+ name = "${name}"
+ display_name = "A text input"
+ description = "This parameter can be used to input text."
+ order = ${order}
+
+ styling = jsonencode({
+ placeholder = "A placeholder that will appear if the input value is empty"
+ })
form_type = "input"
type = "string"
default = "An input value"
}`;
-export const radio = `data "coder_parameter" "radio" {
- name = "radio"
- display_name = "An example of a radio input"
- description = "The next parameter supports a single value."
- type = "string"
- form_type = "radio"
- order = 1
- default = "option-1"
+export const textarea: SnippetFunc = (
+ name,
+ order,
+) => `data "coder_parameter" "textarea" {
+ name = "${name}"
+ display_name = "A textarea input"
+ description = "This parameter can be used to input multiple lines of text"
+ order = ${order}
+
+ styling = jsonencode({
+ placeholder = "A placeholder that will appear if the input value is empty"
+ })
+
+ form_type = "textarea"
+ type = "string"
+ default = "An input value"
+}`;
+
+export const radio: SnippetFunc = (
+ name,
+ order,
+) => `data "coder_parameter" "radio" {
+ name = "${name}"
+ display_name = "A radio input"
+ description = "This parameter supports selecting a single value out of a list of options"
+ order = ${order}
+
+ type = "string"
+ form_type = "radio"
+ default = "option-1"
+
+ option {
+ name = "Option 1"
+ value = "option-1"
+ description = "A description for Option 1"
+ }
+
+ option {
+ name = "Option 2"
+ value = "option-2"
+ description = "A description for Option 2"
+ }
+
+ option {
+ name = "Option 3"
+ value = "option-3"
+ description = "A description for Option 3"
+ }
+
+ option {
+ name = "Option 4"
+ value = "option-4"
+ description = "A description for Option 4"
+ }
+}`;
+
+export const dropdown: SnippetFunc = (
+ name,
+ order,
+) => `data "coder_parameter" "dropdown" {
+ name = "${name}"
+ display_name = "A dropdown input"
+ description = "This parameter supports selecting a single value out of a list of options. Especially useful when you have a lot of options."
+ order = ${order}
+
+ styling = jsonencode({
+ placeholder = "A placeholder that will appear if the input value is empty"
+ })
+
+ type = "string"
+ form_type = "dropdown"
+ default = "option-1"
option {
name = "Option 1"
@@ -52,13 +143,17 @@ export const radio = `data "coder_parameter" "radio" {
}
}`;
-export const multiSelect = `data "coder_parameter" "multi-select" {
- name = "multi-select"
- display_name = "An example of a multi-select"
- description = "The next parameter supports multiple values."
+export const multiSelect: SnippetFunc = (
+ name,
+ order,
+) => `data "coder_parameter" "multi-select" {
+ name = "${name}"
+ display_name = "A multi-select input"
+ description = "This parameter supports selecting multiple values from a list of options"
+ order = ${order}
+
type = "list(string)"
form_type = "multi-select"
- order = 1
option {
name = "Option 1"
@@ -85,15 +180,101 @@ export const multiSelect = `data "coder_parameter" "multi-select" {
}
}`;
-export const switchInput = `data "coder_parameter" "switch" {
- name = "switch"
- display_name = "An example of a switch"
- description = "The next parameter can be on or off"
+export const tagSelect: SnippetFunc = (
+ name,
+ order,
+) => `data "coder_parameter" "tag-select" {
+ name = "${name}"
+ display_name = "A tag-select input"
+ description = "This parameter supports selecting multiple user inputed values at once"
+ order = ${order}
+
+ type = "list(string)"
+ form_type = "tag-select"
+}`;
+
+export const switchInput: SnippetFunc = (
+ name,
+ order,
+) => `data "coder_parameter" "switch" {
+ name = "${name}"
+ display_name = "A switch input"
+ description = "This parameter can be toggled between true and false"
+ order = ${order}
+
type = "bool"
form_type = "switch"
default = true
- order = 1
-}`
+}`;
+
+export const slider: SnippetFunc = (
+ name,
+ order,
+) => `data "coder_parameter" "slider" {
+ name = "${name}"
+ display_name = "A slider input"
+ description = "This parameter supports selecting a number within a given range"
+ type = "number"
+ form_type = "slider"
+ default = 6
+ order = ${order}
+
+ validation {
+ min = 1
+ max = 10
+ }
+}`;
+
+export const snippets: Snippet[] = [
+ {
+ name: "text-input",
+ label: "Text Input",
+ icon: TextCursorInputIcon,
+ snippet: input,
+ },
+ {
+ name: "textarea",
+ label: "Textarea",
+ icon: LetterTextIcon,
+ snippet: textarea,
+ },
+ {
+ name: "radio",
+ label: "Radio",
+ icon: RadioIcon,
+ snippet: radio,
+ },
+ {
+ name: "switch",
+ label: "Multi-select",
+ icon: SquareMousePointerIcon,
+ snippet: multiSelect,
+ },
+ {
+ name: "tag-select",
+ label: "Tag-select",
+ icon: TagIcon,
+ snippet: tagSelect,
+ },
+ {
+ name: "switch",
+ label: "Switch",
+ icon: ToggleLeftIcon,
+ snippet: switchInput,
+ },
+ {
+ name: "dropdown",
+ label: "Dropdown",
+ icon: Rows3Icon,
+ snippet: dropdown,
+ },
+ {
+ name: "slider",
+ label: "Slider",
+ icon: Settings2Icon,
+ snippet: slider,
+ },
+];
export const checkerModule = `
variable "solutions" {