Skip to content

Commit c92e720

Browse files
authored
Merge pull request #26 from coder/brett/remove-zustand
chore: remove zustand
2 parents a045b5c + 0854bb1 commit c92e720

File tree

15 files changed

+550
-393
lines changed

15 files changed

+550
-393
lines changed

preview/apitypes/apitypes.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package apitypes
33
import (
44
"time"
55

6+
"github.com/google/uuid"
67
"github.com/hashicorp/hcl/v2"
78

89
"github.com/coder/preview/types"
@@ -64,6 +65,7 @@ type FriendlyDiagnostic = types.FriendlyDiagnostic
6465

6566
type ParameterWithSource struct {
6667
types.Parameter
68+
Uuid uuid.UUID `json:"uuid"`
6769
TypeRange hcl.Range `json:"type_range"`
6870
DefRange hcl.Range `json:"def_range"`
6971
}
@@ -79,6 +81,7 @@ func WithSource(p []types.Parameter) []ParameterWithSource {
7981
src.TypeRange = param.Source.HCLBlock().TypeRange
8082
src.DefRange = param.Source.HCLBlock().DefRange
8183
}
84+
src.Uuid = uuid.New()
8285

8386
result = append(result, src)
8487
}

preview/main.go

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ import (
1616
"github.com/hashicorp/hcl/v2"
1717
"github.com/spf13/afero"
1818

19+
"github.com/coder/parameters-playground/preview/apitypes"
1920
"github.com/coder/preview"
2021
"github.com/coder/preview/types"
21-
"github.com/coder/parameters-playground/preview/apitypes"
2222
)
2323

2424
func main() {
@@ -60,25 +60,29 @@ func tfpreview(this js.Value, p []js.Value) (output any) {
6060
return err
6161
}
6262

63-
owner, err := workspaceOwner(p[1])
64-
if err != nil {
65-
return err
66-
}
67-
6863
handler := slog.NewJSONHandler(l, nil)
6964
logger := slog.New(handler)
7065

7166
var parameters map[string]string
72-
if len(p) >= 3 {
73-
params, err := jsValueToStringMap(p[2])
67+
if len(p) >= 2 {
68+
params, err := jsValueToStringMap(p[1])
7469
if err != nil {
7570
logger.Error("Unable to convert second prameter into map[string]string", "err", err)
7671
}
7772

7873
parameters = params
7974
} else {
8075
logger.Error(fmt.Sprintf("Expected 2 arguments but got %v", len(p)))
76+
}
77+
78+
owner := apitypes.WorkspaceOwner{}
79+
if len(p) >= 3 {
80+
o, err := workspaceOwner(p[2])
81+
if err != nil {
82+
logger.Error("Unable to convert third parameter into WorkspaceOwner", "err", err)
83+
}
8184

85+
owner = o
8286
}
8387

8488
pOutput, diags := preview.Preview(context.Background(), preview.Input{

public/build/preview.wasm

38.8 KB
Binary file not shown.

src/client/App.tsx

Lines changed: 152 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,46 @@
11
import { Editor } from "@/client/Editor";
22
import { Preview } from "@/client/Preview";
3-
import { Logo } from "@/client/components/Logo";
4-
import {
5-
ResizableHandle,
6-
ResizablePanelGroup,
7-
} from "@/client/components/Resizable";
8-
import { useStore } from "@/client/store";
3+
import { Button } from "@/client/components/Button";
94
import {
105
DropdownMenu,
116
DropdownMenuContent,
127
DropdownMenuItem,
138
DropdownMenuPortal,
149
DropdownMenuTrigger,
1510
} from "@/client/components/DropdownMenu";
11+
import { Logo } from "@/client/components/Logo";
1612
import {
17-
type FC,
18-
useCallback,
19-
useEffect,
20-
useMemo,
21-
useRef,
22-
useState,
23-
} from "react";
24-
import { useTheme } from "@/client/contexts/theme";
25-
import { MoonIcon, ShareIcon, SunIcon, SunMoonIcon } from "lucide-react";
26-
import { Button } from "@/client/components/Button";
13+
ResizableHandle,
14+
ResizablePanelGroup,
15+
} from "@/client/components/Resizable";
2716
import {
2817
Tooltip,
2918
TooltipContent,
3019
TooltipTrigger,
3120
} from "@/client/components/Tooltip";
21+
import { useTheme } from "@/client/contexts/theme";
22+
import { defaultCode } from "@/client/snippets";
23+
import type {
24+
ParameterWithSource,
25+
PreviewOutput,
26+
WorkspaceOwner,
27+
} from "@/gen/types";
3228
import { rpc } from "@/utils/rpc";
33-
import { useLoaderData, type LoaderFunctionArgs } from "react-router";
34-
import type {WorkspaceOwner} from "@/gen/types.ts";
35-
36-
type GoPreviewDef = (
37-
v: Record<string, string>,
38-
owner: WorkspaceOwner,
39-
params: Record<string, string>,
40-
) => Promise<string>;
41-
42-
// Extend the Window object to include the Go related code that is added from
43-
// wasm_exec.js and our loaded Go code.
44-
declare global {
45-
interface Window {
46-
// Loaded from wasm
47-
go_preview?: GoPreviewDef;
48-
Go: { new (): Go };
49-
CODE?: string;
50-
}
51-
}
52-
53-
declare class Go {
54-
argv: string[];
55-
env: { [envKey: string]: string };
56-
exit: (code: number) => void;
57-
importObject: WebAssembly.Imports;
58-
exited: boolean;
59-
mem: DataView;
60-
run(instance: WebAssembly.Instance): Promise<void>;
61-
}
29+
import {
30+
type WasmLoadState,
31+
getDynamicParametersOutput,
32+
initWasm,
33+
} from "@/utils/wasm";
34+
import isEqual from "lodash/isEqual";
35+
import { MoonIcon, ShareIcon, SunIcon, SunMoonIcon } from "lucide-react";
36+
import { type FC, useEffect, useMemo, useRef, useState } from "react";
37+
import { type LoaderFunctionArgs, useLoaderData } from "react-router";
38+
import { useDebouncedValue } from "./hooks/debounce";
39+
import { mockUsers } from "@/owner";
6240

41+
/**
42+
* Load the shared code if present.
43+
*/
6344
export const loader = async ({ params }: LoaderFunctionArgs) => {
6445
const { id } = params;
6546
if (!id) {
@@ -79,45 +60,116 @@ export const loader = async ({ params }: LoaderFunctionArgs) => {
7960
};
8061

8162
export const App = () => {
82-
const $wasmState = useStore((state) => state.wasmState);
83-
const $setWasmState = useStore((state) => state.setWasmState);
84-
const $setCode = useStore((store) => store.setCode);
85-
const code = useLoaderData<typeof loader>();
63+
const [wasmLoadState, setWasmLoadingState] = useState<WasmLoadState>(() => {
64+
if (window.go_preview) {
65+
return "loaded";
66+
}
67+
return "loading";
68+
});
69+
const loadedCode = useLoaderData<typeof loader>();
70+
const [code, setCode] = useState(loadedCode ?? defaultCode);
71+
const [debouncedCode, isDebouncing] = useDebouncedValue(code, 1000);
72+
const [parameterValues, setParameterValues] = useState<
73+
Record<string, string>
74+
>({});
75+
const [output, setOutput] = useState<PreviewOutput | null>(null);
76+
const [parameters, setParameters] = useState<ParameterWithSource[]>([]);
77+
const [owner, setOwner] = useState<WorkspaceOwner>(mockUsers.admin);
78+
79+
const onDownloadOutput = () => {
80+
const blob = new Blob([JSON.stringify(output, null, 2)], {
81+
type: "application/json",
82+
});
83+
84+
const url = URL.createObjectURL(blob);
85+
86+
const link = document.createElement("a");
87+
link.href = url;
88+
link.download = "output.json";
89+
document.body.appendChild(link);
90+
link.click();
91+
document.body.removeChild(link);
92+
93+
// Give the click event enough time to fire and then revoke the URL.
94+
// This method of doing it doesn't seem great but I'm not sure if there is a
95+
// better way.
96+
setTimeout(() => {
97+
URL.revokeObjectURL(url);
98+
}, 100);
99+
};
100+
101+
const onReset = () => {
102+
setParameterValues({});
103+
setParameters((curr) =>
104+
curr.map((p) => {
105+
p.uuid = window.crypto.randomUUID();
106+
return p;
107+
}),
108+
);
109+
};
86110

87111
useEffect(() => {
88-
if (!code) {
89-
return;
112+
if (!window.go_preview) {
113+
initWasm().then((loadState) => {
114+
setWasmLoadingState(loadState);
115+
});
116+
} else {
117+
// We assume that if `window.go_preview` has already created then the wasm
118+
// has already been initiated.
119+
setWasmLoadingState("loaded");
90120
}
121+
}, []);
122+
123+
useEffect(() => {
124+
setParameters((curr) => {
125+
const newParameters = output?.output?.parameters ?? [];
91126

92-
$setCode(code);
93-
}, [code, $setCode]);
127+
return newParameters.map((p) => {
128+
// Check if the parameter is already in the array and if it is then keep it.
129+
// This allows us to optimize React by not re-rendering parameters that haven't changed.
130+
//
131+
// We unset value because the value may not be in sync with what we have locally,
132+
// and we unset uuid because it's given a new random UUID every time.
133+
const existing = curr.find((currP) => {
134+
const currentParameterOmitValue = {
135+
...currP,
136+
value: undefined,
137+
uuid: undefined,
138+
};
139+
const existingParameterOmitValue = {
140+
...p,
141+
value: undefined,
142+
uuid: undefined,
143+
};
144+
145+
return isEqual(currentParameterOmitValue, existingParameterOmitValue);
146+
});
147+
148+
if (existing) {
149+
existing.value = p.value;
150+
return existing;
151+
}
152+
return p;
153+
});
154+
});
155+
}, [output]);
94156

95-
// biome-ignore lint/correctness/useExhaustiveDependencies: <explanation>
96157
useEffect(() => {
97-
const initWasm = async () => {
98-
try {
99-
const goWasm = new window.Go();
100-
const result = await WebAssembly.instantiateStreaming(
101-
fetch(
102-
import.meta.env.PROD
103-
? "/assets/build/preview.wasm"
104-
: "/build/preview.wasm",
105-
),
106-
goWasm.importObject,
107-
);
108-
109-
goWasm.run(result.instance);
110-
$setWasmState("loaded");
111-
} catch (e) {
112-
$setWasmState("error");
158+
if (wasmLoadState !== "loaded") {
159+
return;
160+
}
161+
162+
getDynamicParametersOutput(debouncedCode, parameterValues, owner)
163+
.catch((e) => {
113164
console.error(e);
114-
}
115-
};
165+
setWasmLoadingState("error");
116166

117-
if ($wasmState !== "loaded") {
118-
initWasm();
119-
}
120-
}, []);
167+
return null;
168+
})
169+
.then((output) => {
170+
setOutput(output);
171+
});
172+
}, [debouncedCode, parameterValues, wasmLoadState, owner]);
121173

122174
return (
123175
<main className="flex h-dvh w-screen flex-col items-center bg-surface-primary">
@@ -131,7 +183,7 @@ export const App = () => {
131183
</p>
132184
</div>
133185

134-
<ShareButton />
186+
<ShareButton code={code} />
135187
</div>
136188

137189
<div className="flex items-center gap-3">
@@ -163,14 +215,27 @@ export const App = () => {
163215
</div>
164216
</nav>
165217

166-
<ResizablePanelGroup aria-hidden={!$wasmState} direction={"horizontal"}>
218+
<ResizablePanelGroup direction={"horizontal"}>
167219
{/* EDITOR */}
168-
<Editor />
220+
<Editor code={code} setCode={setCode} />
169221

170222
<ResizableHandle className="bg-surface-quaternary" />
171223

172224
{/* PREVIEW */}
173-
<Preview />
225+
<Preview
226+
wasmLoadState={wasmLoadState}
227+
isDebouncing={isDebouncing}
228+
onDownloadOutput={onDownloadOutput}
229+
output={output}
230+
parameterValues={parameterValues}
231+
setParameterValues={setParameterValues}
232+
parameters={parameters}
233+
onReset={onReset}
234+
setOwner={(owner) => {
235+
onReset();
236+
setOwner(owner);
237+
}}
238+
/>
174239
</ResizablePanelGroup>
175240
</main>
176241
);
@@ -215,15 +280,17 @@ const ThemeSelector: FC = () => {
215280
);
216281
};
217282

218-
const ShareButton: FC = () => {
219-
const $code = useStore((state) => state.code);
283+
type ShareButtonProps = {
284+
code: string;
285+
};
286+
const ShareButton: FC<ShareButtonProps> = ({ code }) => {
220287
const [isCopied, setIsCopied] = useState(() => false);
221288
const timeoutId = useRef<ReturnType<typeof setTimeout>>(undefined);
222289

223-
const onShare = useCallback(async () => {
290+
const onShare = async () => {
224291
try {
225292
const { id } = await rpc.parameters
226-
.$post({ json: { code: $code } })
293+
.$post({ json: { code } })
227294
.then((res) => res.json());
228295

229296
const { protocol, host } = window.location;
@@ -235,7 +302,7 @@ const ShareButton: FC = () => {
235302
} catch (e) {
236303
console.error(e);
237304
}
238-
}, [$code]);
305+
};
239306

240307
useEffect(() => {
241308
if (!isCopied) {

0 commit comments

Comments
 (0)