Skip to content

Commit 7d03814

Browse files
committed
Implement apply source dialog
1 parent 39ecc28 commit 7d03814

File tree

2 files changed

+216
-28
lines changed

2 files changed

+216
-28
lines changed

client/dashboard/src/components/sources/ApplyEnvironmentDialogContent.tsx

Lines changed: 212 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,246 @@
1-
import { Button, Combobox, Dialog, Icon } from "@speakeasy-api/moonshine";
1+
import {
2+
Badge,
3+
Button,
4+
Combobox,
5+
Dialog,
6+
Icon,
7+
} from "@speakeasy-api/moonshine";
28
// import { Dialog } from "@/components/ui/dialog";
39
import { NamedAsset } from "./SourceCard";
410
import {
511
useCreateEnvironmentMutation,
612
useListEnvironments,
13+
useGetSourceEnvironment,
14+
useSetSourceEnvironmentLinkMutation,
15+
useDeleteSourceEnvironmentLinkMutation,
716
} from "@gram/client/react-query";
8-
import { useState } from "react";
17+
import { useEffect, useState } from "react";
18+
import { useSession } from "@/contexts/Auth";
19+
import { TriangleAlertIcon } from "lucide-react";
20+
import { GramError } from "@gram/client/models/errors/gramerror.js";
21+
import { useRoutes } from "@/routes";
922

1023
interface ApplyEnvironmentDialogContentProps {
1124
asset: NamedAsset;
25+
onClose: () => void;
1226
}
1327

14-
export function ApplyEnvironmentDialogContent({
15-
asset,
16-
}: ApplyEnvironmentDialogContentProps) {
17-
const result = useListEnvironments();
18-
const mutation = useCreateEnvironmentMutation();
28+
interface EnvironmentComboboxProps {
29+
activeEnvironmentId: string | undefined;
30+
setActiveEnvironmentId: (id: string | undefined) => void;
31+
}
1932

20-
const [envQuery, setEnvQuery] = useState<string | undefined>(undefined);
21-
const handleConfirm = () => {};
33+
function EnvironmentCombobox({
34+
activeEnvironmentId,
35+
setActiveEnvironmentId,
36+
}: EnvironmentComboboxProps) {
37+
const session = useSession();
38+
const routes = useRoutes();
39+
const environments = useListEnvironments();
2240

23-
return (
24-
<>
25-
<Dialog.Header>
26-
<Dialog.Title>Apply Environment</Dialog.Title>
27-
<Dialog.Description>
28-
Apply environment configuration to {asset.name}
29-
</Dialog.Description>
30-
</Dialog.Header>
41+
const mutation = useCreateEnvironmentMutation({
42+
onSuccess: (data) => {
43+
setActiveEnvironmentId(data.id);
44+
},
45+
onSettled: () => {
46+
environments.refetch();
47+
},
48+
});
3149

32-
<div className="py-4">
50+
const selectedEnvironment = environments.data?.environments?.find(
51+
(env) => env.id === activeEnvironmentId,
52+
);
53+
54+
return (
55+
<div className="flex flex-col gap-2">
56+
<div className="flex gap-2 items-center">
3357
<Combobox
34-
value={envQuery ?? ""}
58+
value={activeEnvironmentId ?? ""}
3559
placeholder="select or create"
36-
options={(result.data?.environments ?? []).map((env) => ({
60+
options={(environments.data?.environments ?? []).map((env) => ({
3761
value: env.id,
3862
label: env.name,
3963
}))}
40-
onValueChange={setEnvQuery}
64+
onValueChange={setActiveEnvironmentId}
4165
createOptions={{
4266
renderCreatePrompt: (query) => (
43-
<div
44-
className="flex items-center gap-2"
45-
onClick={() => alert("")}
46-
>
67+
<div className="flex items-center gap-2">
4768
<Icon name="plus" /> Create "{query}"
4869
</div>
4970
),
5071
handleCreate: (query) => {
51-
alert(`Creating environment "${query}"`);
72+
mutation.mutate({
73+
request: {
74+
createEnvironmentForm: {
75+
name: query,
76+
description: `environment for attaching to source`,
77+
entries: [],
78+
organizationId: session.activeOrganizationId,
79+
},
80+
},
81+
});
5282
},
5383
}}
84+
loading={environments.isLoading || mutation.isPending}
5485
/>
86+
{activeEnvironmentId && (
87+
<Button
88+
onClick={() => setActiveEnvironmentId(undefined)}
89+
variant="tertiary"
90+
size="sm"
91+
aria-label="Clear environment"
92+
>
93+
<Icon name="x" /> clear
94+
</Button>
95+
)}
5596
</div>
5697

98+
{selectedEnvironment && (
99+
<div className="space-y-2">
100+
<p className="text-sm text-muted-foreground">
101+
Variables in environment:
102+
</p>
103+
<div className="flex flex-wrap gap-2 items-center">
104+
{selectedEnvironment.entries.length > 0 ? (
105+
selectedEnvironment.entries.map((entry) => (
106+
<Badge key={entry.name}>{entry.name}</Badge>
107+
))
108+
) : (
109+
<div className="text-sm text-muted-foreground">Empty...</div>
110+
)}
111+
<routes.environments.environment.Link
112+
params={[selectedEnvironment.slug]}
113+
>
114+
<Button
115+
variant="tertiary"
116+
size="sm"
117+
aria-label="Edit environment"
118+
>
119+
<Icon name="eye" /> view
120+
</Button>
121+
</routes.environments.environment.Link>
122+
</div>
123+
</div>
124+
)}
125+
</div>
126+
);
127+
}
128+
129+
function useSourceEnvironmentData(asset: NamedAsset) {
130+
const environments = useListEnvironments();
131+
const sourceEnvironment = useGetSourceEnvironment(
132+
{
133+
sourceKind: asset.type === "openapi" ? "http" : asset.type,
134+
sourceSlug: asset.slug,
135+
},
136+
undefined,
137+
{
138+
retry: (_, err) => {
139+
if (err instanceof GramError && err.statusCode === 404) {
140+
return false;
141+
}
142+
return true;
143+
},
144+
throwOnError: false,
145+
},
146+
);
147+
148+
return {
149+
environments,
150+
sourceEnvironment,
151+
isLoading: environments.isLoading || sourceEnvironment.isLoading,
152+
};
153+
}
154+
155+
export function ApplyEnvironmentDialogContent({
156+
asset,
157+
onClose,
158+
}: ApplyEnvironmentDialogContentProps) {
159+
const { sourceEnvironment } = useSourceEnvironmentData(asset);
160+
161+
const [activeEnvironmentId, setActiveEnvironmentId] = useState<
162+
string | undefined
163+
>(undefined);
164+
165+
const [initialEnvironmentId, setInitialEnvironmentId] = useState<
166+
string | undefined
167+
>(undefined);
168+
169+
useEffect(() => {
170+
setActiveEnvironmentId(sourceEnvironment.data?.id);
171+
setInitialEnvironmentId(sourceEnvironment.data?.id);
172+
}, [sourceEnvironment.data?.id]);
173+
174+
const isDirty = activeEnvironmentId !== initialEnvironmentId;
175+
176+
const setSourceEnvironmentMutation = useSetSourceEnvironmentLinkMutation({
177+
onSettled: () => {
178+
sourceEnvironment.refetch();
179+
},
180+
});
181+
182+
const deleteSourceEnvironmentMutation =
183+
useDeleteSourceEnvironmentLinkMutation({
184+
onSettled: () => {
185+
sourceEnvironment.refetch();
186+
},
187+
});
188+
189+
const handleConfirm = () => {
190+
if (!activeEnvironmentId && isDirty) {
191+
deleteSourceEnvironmentMutation.mutate({
192+
request: {
193+
sourceKind: asset.type === "openapi" ? "http" : asset.type,
194+
sourceSlug: asset.slug,
195+
},
196+
});
197+
return;
198+
}
199+
200+
if (!activeEnvironmentId) return;
201+
202+
setSourceEnvironmentMutation.mutate({
203+
request: {
204+
setSourceEnvironmentLinkRequestBody: {
205+
sourceKind: asset.type === "openapi" ? "http" : asset.type,
206+
sourceSlug: asset.slug,
207+
environmentId: activeEnvironmentId,
208+
},
209+
},
210+
});
211+
};
212+
213+
return (
214+
<>
215+
<Dialog.Header>
216+
<Dialog.Title>Apply Environment</Dialog.Title>
217+
<Dialog.Description>
218+
<p className="text-warning">
219+
<TriangleAlertIcon className="inline mr-2 w-4 h-4" />
220+
Environments attached here will apply to all users of tools from
221+
this source
222+
</p>
223+
</Dialog.Description>
224+
</Dialog.Header>
225+
226+
<EnvironmentCombobox
227+
activeEnvironmentId={activeEnvironmentId}
228+
setActiveEnvironmentId={setActiveEnvironmentId}
229+
/>
230+
57231
<Dialog.Footer>
58-
<Button onClick={handleConfirm} variant="primary">
232+
<Button onClick={onClose} variant="secondary">
233+
Cancel
234+
</Button>
235+
<Button
236+
onClick={handleConfirm}
237+
variant="primary"
238+
disabled={
239+
!isDirty ||
240+
setSourceEnvironmentMutation.isPending ||
241+
deleteSourceEnvironmentMutation.isPending
242+
}
243+
>
59244
Apply Environment
60245
</Button>
61246
</Dialog.Footer>

client/dashboard/src/components/sources/Sources.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,10 @@ export default function Sources() {
231231
/>
232232
)}
233233
{dialogState.type === "apply-environment" && (
234-
<ApplyEnvironmentDialogContent asset={dialogState.asset} />
234+
<ApplyEnvironmentDialogContent
235+
asset={dialogState.asset}
236+
onClose={closeDialog}
237+
/>
235238
)}
236239
</Dialog.Content>
237240
</Dialog>

0 commit comments

Comments
 (0)