Skip to content

Commit 14704de

Browse files
committed
feat: add page to update webhook
1 parent 71f594c commit 14704de

File tree

3 files changed

+286
-77
lines changed

3 files changed

+286
-77
lines changed

ui/src/components/CustomField.tsx

Lines changed: 86 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,11 @@ import React, { CSSProperties } from "react";
1111
import { Control, Controller, UseFormRegister } from "react-hook-form";
1212
import { capitalizeFirstLetter } from "~/utils/helper";
1313
import { MultiSelect } from "./multiselect";
14+
import Skeleton from "react-loading-skeleton";
1415

1516
type CustomFieldNameProps = {
1617
name: string;
18+
isLoading?: boolean;
1719
title?: string;
1820
disabled?: boolean;
1921
register: UseFormRegister<any>;
@@ -33,6 +35,7 @@ export const CustomFieldName = ({
3335
style = {},
3436
placeholder,
3537
options = [],
38+
isLoading = false,
3639
...props
3740
}: FormFieldProps &
3841
CustomFieldNameProps &
@@ -64,86 +67,92 @@ export const CustomFieldName = ({
6467
</FormMessage>
6568
</Flex>
6669
<FormControl asChild>
67-
<Controller
68-
defaultValue={props.defaultValue}
69-
name={name}
70-
control={control}
71-
render={({ field }) => {
72-
switch (variant) {
73-
case "textarea": {
74-
return (
75-
<textarea
76-
{...field}
77-
placeholder={
78-
placeholder ||
79-
`Enter your ${title?.toLowerCase() || name}`
80-
}
81-
style={style}
82-
/>
83-
);
84-
}
85-
case "select": {
86-
const { ref, ...rest } = field;
87-
return (
88-
<Select
89-
{...rest}
90-
onValueChange={(value: any) => field.onChange(value)}
91-
>
92-
<Select.Trigger
93-
ref={ref}
94-
style={{ height: "26px", width: "100%" }}
70+
{isLoading ? (
71+
<Skeleton />
72+
) : (
73+
<Controller
74+
defaultValue={props.defaultValue}
75+
name={name}
76+
control={control}
77+
render={({ field }) => {
78+
switch (variant) {
79+
case "textarea": {
80+
return (
81+
<textarea
82+
{...field}
83+
defaultValue={props?.defaultValue}
84+
placeholder={
85+
placeholder ||
86+
`Enter your ${title?.toLowerCase() || name}`
87+
}
88+
style={style}
89+
/>
90+
);
91+
}
92+
case "select": {
93+
const { ref, ...rest } = field;
94+
return (
95+
<Select
96+
{...rest}
97+
onValueChange={(value: any) => field.onChange(value)}
9598
>
96-
<Select.Value placeholder={`Select ${inputTitle}`} />
97-
</Select.Trigger>
98-
<Select.Content style={{ width: "320px" }}>
99-
<Select.Group>
100-
{options.map((opt) => (
101-
<Select.Item key={opt.value} value={opt.value}>
102-
{opt.label}
103-
</Select.Item>
104-
))}
105-
</Select.Group>
106-
</Select.Content>
107-
</Select>
108-
);
109-
}
110-
case "multiselect": {
111-
const { onChange, value, ...rest } = field;
112-
return (
113-
<MultiSelect<string>
114-
{...rest}
115-
options={options}
116-
onSelect={onChange}
117-
selected={value}
118-
/>
119-
);
120-
}
121-
case "switch": {
122-
const { onChange, value, ...rest } = field;
123-
return (
124-
<Switch
125-
{...rest}
126-
checked={value}
127-
onCheckedChange={onChange}
128-
/>
129-
);
130-
}
99+
<Select.Trigger
100+
ref={ref}
101+
style={{ height: "26px", width: "100%" }}
102+
>
103+
<Select.Value placeholder={`Select ${inputTitle}`} />
104+
</Select.Trigger>
105+
<Select.Content style={{ width: "320px" }}>
106+
<Select.Group>
107+
{options.map((opt) => (
108+
<Select.Item key={opt.value} value={opt.value}>
109+
{opt.label}
110+
</Select.Item>
111+
))}
112+
</Select.Group>
113+
</Select.Content>
114+
</Select>
115+
);
116+
}
117+
case "multiselect": {
118+
const { onChange, value, ...rest } = field;
119+
return (
120+
<MultiSelect<string>
121+
{...rest}
122+
options={options}
123+
onSelect={onChange}
124+
selected={value || props?.defaultValue}
125+
/>
126+
);
127+
}
128+
case "switch": {
129+
const { onChange, value, ...rest } = field;
130+
return (
131+
<Switch
132+
{...rest}
133+
defaultChecked={props?.defaultChecked}
134+
checked={value}
135+
onCheckedChange={onChange}
136+
/>
137+
);
138+
}
131139

132-
default: {
133-
return (
134-
<TextField
135-
{...field}
136-
placeholder={
137-
placeholder ||
138-
`Enter your ${title?.toLowerCase() || name}`
139-
}
140-
disabled={disabled}
141-
/>
142-
);
140+
default: {
141+
return (
142+
<TextField
143+
{...field}
144+
placeholder={
145+
placeholder ||
146+
`Enter your ${title?.toLowerCase() || name}`
147+
}
148+
disabled={disabled}
149+
/>
150+
);
151+
}
143152
}
144-
}
145-
}}
146-
/>
153+
}}
154+
/>
155+
)}
147156
</FormControl>
148157
</Flex>
149158
</FormField>
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
import { useCallback, useEffect, useState } from "react";
2+
import { Button, Flex, Sheet, Text } from "@raystack/apsara";
3+
import { useNavigate, useParams } from "react-router-dom";
4+
import { SheetHeader } from "~/components/sheet/header";
5+
import * as z from "zod";
6+
import { FormProvider, useForm } from "react-hook-form";
7+
import { zodResolver } from "@hookform/resolvers/zod";
8+
import { Form, FormSubmit } from "@radix-ui/react-form";
9+
import { CustomFieldName } from "~/components/CustomField";
10+
import events from "~/utils/webhook_events";
11+
import { SheetFooter } from "~/components/sheet/footer";
12+
import { useFrontier } from "@raystack/frontier/react";
13+
import { V1Beta1Webhook, V1Beta1WebhookRequestBody } from "@raystack/frontier";
14+
import { toast } from "sonner";
15+
16+
const UpdateWebhookSchema = z.object({
17+
url: z.string().trim().url(),
18+
description: z
19+
.string()
20+
.trim()
21+
.min(3, { message: "Must be 10 or more characters long" }),
22+
state: z.boolean().default(false),
23+
subscribed_events: z.array(z.string()).default([]),
24+
});
25+
26+
export type UpdateWebhook = z.infer<typeof UpdateWebhookSchema>;
27+
28+
export default function UpdateWebhooks() {
29+
const navigate = useNavigate();
30+
const { client } = useFrontier();
31+
32+
const [isSubmitting, setIsSubmitting] = useState(false);
33+
34+
const [isWebhookLoading, setIsWebhookLoading] = useState(false);
35+
const [webhook, setWebhook] = useState<V1Beta1Webhook>();
36+
37+
const { webhookId = "" } = useParams();
38+
39+
const onClose = useCallback(() => {
40+
navigate("/webhooks");
41+
}, [navigate]);
42+
43+
const methods = useForm<UpdateWebhook>({
44+
resolver: zodResolver(UpdateWebhookSchema),
45+
defaultValues: {},
46+
});
47+
48+
const onSubmit = async (data: UpdateWebhook) => {
49+
console.log("called");
50+
try {
51+
setIsSubmitting(true);
52+
const body: V1Beta1WebhookRequestBody = {
53+
...data,
54+
state: data.state ? "enabled" : "disabled",
55+
};
56+
const resp = await client?.adminServiceUpdateWebhook(webhookId, {
57+
body,
58+
});
59+
60+
if (resp?.data?.webhook) {
61+
toast.success("Webhook updated");
62+
}
63+
} catch (err) {
64+
toast.error("Something went wrong");
65+
} finally {
66+
setIsSubmitting(false);
67+
}
68+
};
69+
70+
useEffect(() => {
71+
async function getWebhookDetails(id: string) {
72+
try {
73+
setIsWebhookLoading(true);
74+
const resp = await client?.adminServiceListWebhooks();
75+
const webhooks = resp?.data?.webhooks || [];
76+
const webhookData = webhooks?.find((wb) => wb?.id === id);
77+
setWebhook(webhookData);
78+
methods?.reset({
79+
...webhookData,
80+
state: webhookData?.state === "enabled",
81+
});
82+
} catch (err) {
83+
console.error(err);
84+
} finally {
85+
setIsWebhookLoading(false);
86+
}
87+
}
88+
89+
if (webhookId) {
90+
getWebhookDetails(webhookId);
91+
}
92+
}, [client, methods, webhookId]);
93+
94+
return (
95+
<Sheet open={true}>
96+
<Sheet.Content
97+
side="right"
98+
// @ts-ignore
99+
style={{
100+
width: "30vw",
101+
padding: 0,
102+
borderRadius: "var(--pd-8)",
103+
boxShadow: "var(--shadow-sm)",
104+
}}
105+
close={false}
106+
>
107+
<FormProvider {...methods}>
108+
<Form onSubmit={methods.handleSubmit(onSubmit)}>
109+
<SheetHeader
110+
title="Update Webhook"
111+
onClick={onClose}
112+
data-test-id="admin-ui-update-webhook-close-btn"
113+
/>
114+
<Flex direction="column" gap="large" style={styles.main}>
115+
<CustomFieldName
116+
name="url"
117+
defaultValue={webhook?.url}
118+
register={methods.register}
119+
control={methods.control}
120+
variant="textarea"
121+
style={{ width: "100%" }}
122+
isLoading={isWebhookLoading}
123+
/>
124+
<CustomFieldName
125+
name="description"
126+
defaultValue={webhook?.description}
127+
register={methods.register}
128+
control={methods.control}
129+
variant="textarea"
130+
style={{ width: "100%" }}
131+
isLoading={isWebhookLoading}
132+
/>
133+
<CustomFieldName
134+
name="subscribed_events"
135+
defaultValue={webhook?.subscribed_events}
136+
register={methods.register}
137+
control={methods.control}
138+
variant="multiselect"
139+
options={events.map((e) => ({ label: e, value: e }))}
140+
isLoading={isWebhookLoading}
141+
/>
142+
<CustomFieldName
143+
name="state"
144+
defaultChecked={webhook?.state === "enabled"}
145+
register={methods.register}
146+
control={methods.control}
147+
variant="switch"
148+
isLoading={isWebhookLoading}
149+
/>
150+
</Flex>
151+
<SheetFooter>
152+
<FormSubmit asChild>
153+
<Button
154+
variant="primary"
155+
style={{ height: "inherit" }}
156+
disabled={isSubmitting || isWebhookLoading}
157+
data-test-id="admin-ui-submit-btn"
158+
>
159+
<Text
160+
size={4}
161+
style={{ color: "var(--foreground-inverted)" }}
162+
>
163+
{isSubmitting ? "Updating..." : "Update Webhook"}
164+
</Text>
165+
</Button>
166+
</FormSubmit>
167+
</SheetFooter>
168+
</Form>
169+
</FormProvider>
170+
</Sheet.Content>
171+
</Sheet>
172+
);
173+
}
174+
175+
const styles = {
176+
main: {
177+
padding: "32px",
178+
margin: 0,
179+
height: "calc(100vh - 125px)",
180+
overflow: "auto",
181+
},
182+
formfield: {
183+
width: "80%",
184+
marginBottom: "40px",
185+
},
186+
select: {
187+
height: "32px",
188+
borderRadius: "8px",
189+
padding: "8px",
190+
border: "none",
191+
backgroundColor: "transparent",
192+
"&:active,&:focus": {
193+
border: "none",
194+
outline: "none",
195+
boxShadow: "none",
196+
},
197+
},
198+
};

0 commit comments

Comments
 (0)