feature(dashboard): add integrations and notifications
This commit is contained in:
@@ -157,7 +157,9 @@ export default function AddClient(props: Props) {
|
||||
error={form.formState.errors.cors?.message}
|
||||
placeholder="Add a domain"
|
||||
value={field.value?.split(',') ?? []}
|
||||
renderTag={(tag) => (tag === '*' ? 'Allow domains' : tag)}
|
||||
renderTag={(tag) =>
|
||||
tag === '*' ? 'Allow all domains' : tag
|
||||
}
|
||||
onChange={(newValue) => {
|
||||
field.onChange(
|
||||
newValue
|
||||
|
||||
@@ -91,7 +91,7 @@ export default function EditClient({
|
||||
error={formState.errors.cors?.message}
|
||||
placeholder="Add a domain"
|
||||
value={field.value?.split(',') ?? []}
|
||||
renderTag={(tag) => (tag === '*' ? 'Allow domains' : tag)}
|
||||
renderTag={(tag) => (tag === '*' ? 'Allow all domains' : tag)}
|
||||
onChange={(newValue) => {
|
||||
field.onChange(
|
||||
newValue
|
||||
|
||||
101
apps/dashboard/src/modals/add-integration.tsx
Normal file
101
apps/dashboard/src/modals/add-integration.tsx
Normal file
@@ -0,0 +1,101 @@
|
||||
'use client';
|
||||
|
||||
import { api } from '@/trpc/client';
|
||||
|
||||
import { DiscordIntegrationForm } from '@/components/integrations/forms/discord-integration';
|
||||
import { SlackIntegrationForm } from '@/components/integrations/forms/slack-integration';
|
||||
import { WebhookIntegrationForm } from '@/components/integrations/forms/webhook-integration';
|
||||
import { IntegrationCardContent } from '@/components/integrations/integration-card';
|
||||
import { INTEGRATIONS } from '@/components/integrations/integrations';
|
||||
import { SheetContent } from '@/components/ui/sheet';
|
||||
import type { IIntegrationConfig } from '@openpanel/validation';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { getQueryKey } from '@trpc/react-query';
|
||||
import { useQueryState } from 'nuqs';
|
||||
import { toast } from 'sonner';
|
||||
import { popModal } from '.';
|
||||
import { ModalHeader } from './Modal/Container';
|
||||
|
||||
interface Props {
|
||||
id?: string;
|
||||
type: IIntegrationConfig['type'];
|
||||
}
|
||||
export default function AddIntegration(props: Props) {
|
||||
const query = api.integration.get.useQuery(
|
||||
{
|
||||
id: props.id ?? '',
|
||||
},
|
||||
{
|
||||
enabled: !!props.id,
|
||||
},
|
||||
);
|
||||
|
||||
const integration = INTEGRATIONS.find((i) => i.type === props.type);
|
||||
|
||||
const renderCard = () => {
|
||||
if (!integration) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="card bg-def-100">
|
||||
<IntegrationCardContent {...integration} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const [tab, setTab] = useQueryState('tab', {
|
||||
shallow: false,
|
||||
});
|
||||
const client = useQueryClient();
|
||||
const handleSuccess = () => {
|
||||
toast.success('Integration created');
|
||||
popModal();
|
||||
client.refetchQueries([
|
||||
getQueryKey(api.integration.list),
|
||||
getQueryKey(api.integration.get, { id: props.id }),
|
||||
]);
|
||||
if (tab !== undefined) {
|
||||
setTab('installed');
|
||||
}
|
||||
};
|
||||
|
||||
const renderForm = () => {
|
||||
if (props.id && query.isLoading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
switch (integration?.type) {
|
||||
case 'webhook':
|
||||
return (
|
||||
<WebhookIntegrationForm
|
||||
defaultValues={query.data}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
);
|
||||
case 'discord':
|
||||
return (
|
||||
<DiscordIntegrationForm
|
||||
defaultValues={query.data}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
);
|
||||
case 'slack':
|
||||
return (
|
||||
<SlackIntegrationForm
|
||||
defaultValues={query.data}
|
||||
onSuccess={handleSuccess}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SheetContent className="[&>button.absolute]:hidden">
|
||||
<ModalHeader title="Create an integration" />
|
||||
{renderCard()}
|
||||
{renderForm()}
|
||||
</SheetContent>
|
||||
);
|
||||
}
|
||||
308
apps/dashboard/src/modals/add-notification-rule.tsx
Normal file
308
apps/dashboard/src/modals/add-notification-rule.tsx
Normal file
@@ -0,0 +1,308 @@
|
||||
'use client';
|
||||
|
||||
import { type RouterOutputs, api } from '@/trpc/client';
|
||||
|
||||
import { SheetContent } from '@/components/ui/sheet';
|
||||
import type { NotificationRule } from '@openpanel/db';
|
||||
import { useQueryClient } from '@tanstack/react-query';
|
||||
import { getQueryKey } from '@trpc/react-query';
|
||||
import { toast } from 'sonner';
|
||||
import { popModal } from '.';
|
||||
import { ModalHeader } from './Modal/Container';
|
||||
|
||||
import { ColorSquare } from '@/components/color-square';
|
||||
import { CheckboxItem } from '@/components/forms/checkbox-item';
|
||||
import { InputWithLabel, WithLabel } from '@/components/forms/input-with-label';
|
||||
import { PureFilterItem } from '@/components/report/sidebar/filters/FilterItem';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Combobox } from '@/components/ui/combobox';
|
||||
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
||||
import { useAppParams } from '@/hooks/useAppParams';
|
||||
import { useEventNames } from '@/hooks/useEventNames';
|
||||
import { useEventProperties } from '@/hooks/useEventProperties';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { shortId } from '@openpanel/common';
|
||||
import {
|
||||
IChartEvent,
|
||||
type IChartRange,
|
||||
type IInterval,
|
||||
zCreateNotificationRule,
|
||||
} from '@openpanel/validation';
|
||||
import {
|
||||
FilterIcon,
|
||||
PlusIcon,
|
||||
SaveIcon,
|
||||
SmartphoneIcon,
|
||||
TrashIcon,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
Controller,
|
||||
type SubmitHandler,
|
||||
type UseFormReturn,
|
||||
useFieldArray,
|
||||
useForm,
|
||||
useWatch,
|
||||
} from 'react-hook-form';
|
||||
import type { z } from 'zod';
|
||||
|
||||
interface Props {
|
||||
rule?: RouterOutputs['notification']['rules'][number];
|
||||
}
|
||||
|
||||
type IForm = z.infer<typeof zCreateNotificationRule>;
|
||||
|
||||
export default function AddNotificationRule({ rule }: Props) {
|
||||
const client = useQueryClient();
|
||||
const { organizationId, projectId } = useAppParams();
|
||||
const form = useForm<IForm>({
|
||||
resolver: zodResolver(zCreateNotificationRule),
|
||||
defaultValues: {
|
||||
id: rule?.id ?? '',
|
||||
name: rule?.name ?? '',
|
||||
sendToApp: rule?.sendToApp ?? false,
|
||||
sendToEmail: rule?.sendToEmail ?? false,
|
||||
integrations:
|
||||
rule?.integrations.map((integration) => integration.id) ?? [],
|
||||
projectId,
|
||||
config: rule?.config ?? {
|
||||
type: 'events',
|
||||
events: [
|
||||
{
|
||||
name: '',
|
||||
segment: 'event',
|
||||
filters: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
const mutation = api.notification.createOrUpdateRule.useMutation({
|
||||
onSuccess() {
|
||||
toast.success(
|
||||
rule ? 'Notification rule updated' : 'Notification rule created',
|
||||
);
|
||||
client.refetchQueries(
|
||||
getQueryKey(api.notification.rules, {
|
||||
projectId,
|
||||
}),
|
||||
);
|
||||
popModal();
|
||||
},
|
||||
});
|
||||
|
||||
const eventsArray = useFieldArray({
|
||||
control: form.control,
|
||||
name: 'config.events',
|
||||
});
|
||||
|
||||
const onSubmit: SubmitHandler<IForm> = (data) => {
|
||||
mutation.mutate(data);
|
||||
};
|
||||
|
||||
const integrationsQuery = api.integration.list.useQuery({
|
||||
organizationId,
|
||||
});
|
||||
const integrations = integrationsQuery.data ?? [];
|
||||
|
||||
return (
|
||||
<SheetContent className="[&>button.absolute]:hidden">
|
||||
<ModalHeader title={rule ? 'Edit rule' : 'Create rule'} />
|
||||
<form onSubmit={form.handleSubmit(onSubmit)} className="col gap-4">
|
||||
<InputWithLabel
|
||||
label="Rule name"
|
||||
placeholder="Eg. Sign ups on android"
|
||||
error={form.formState.errors.name?.message}
|
||||
{...form.register('name')}
|
||||
/>
|
||||
|
||||
<WithLabel
|
||||
label="Type"
|
||||
// @ts-expect-error
|
||||
error={form.formState.errors.config?.type.message}
|
||||
>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="config.type"
|
||||
render={({ field }) => (
|
||||
<Combobox
|
||||
{...field}
|
||||
className="w-full"
|
||||
placeholder="Select type"
|
||||
// @ts-expect-error
|
||||
error={form.formState.errors.config?.type.message}
|
||||
items={[
|
||||
{
|
||||
label: 'Events',
|
||||
value: 'events',
|
||||
},
|
||||
{
|
||||
label: 'Funnel',
|
||||
value: 'funnel',
|
||||
},
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</WithLabel>
|
||||
<WithLabel label="Events">
|
||||
<div className="col gap-2">
|
||||
{eventsArray.fields.map((field, index) => {
|
||||
return (
|
||||
<EventField
|
||||
key={field.id}
|
||||
form={form}
|
||||
index={index}
|
||||
remove={() => eventsArray.remove(index)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<Button
|
||||
className="self-start"
|
||||
variant={'outline'}
|
||||
icon={PlusIcon}
|
||||
onClick={() =>
|
||||
eventsArray.append({
|
||||
name: '',
|
||||
filters: [],
|
||||
segment: 'event',
|
||||
})
|
||||
}
|
||||
>
|
||||
Add event
|
||||
</Button>
|
||||
</div>
|
||||
</WithLabel>
|
||||
|
||||
<Controller
|
||||
control={form.control}
|
||||
name="integrations"
|
||||
render={({ field }) => (
|
||||
<WithLabel label="Integrations">
|
||||
<ComboboxAdvanced
|
||||
{...field}
|
||||
value={field.value ?? []}
|
||||
className="w-full"
|
||||
placeholder="Pick integrations"
|
||||
items={integrations.map((integration) => ({
|
||||
label: integration.name,
|
||||
value: integration.id,
|
||||
}))}
|
||||
/>
|
||||
</WithLabel>
|
||||
)}
|
||||
/>
|
||||
|
||||
<Button type="submit" icon={SaveIcon}>
|
||||
{rule ? 'Update' : 'Create'}
|
||||
</Button>
|
||||
</form>
|
||||
</SheetContent>
|
||||
);
|
||||
}
|
||||
|
||||
const interval: IInterval = 'day';
|
||||
const range: IChartRange = 'lastMonth';
|
||||
|
||||
function EventField({
|
||||
form,
|
||||
index,
|
||||
remove,
|
||||
}: {
|
||||
form: UseFormReturn<IForm>;
|
||||
index: number;
|
||||
remove: () => void;
|
||||
}) {
|
||||
const { projectId } = useAppParams();
|
||||
const eventNames = useEventNames({ projectId, interval, range });
|
||||
const filtersArray = useFieldArray({
|
||||
control: form.control,
|
||||
name: `config.events.${index}.filters`,
|
||||
});
|
||||
const eventName = useWatch({
|
||||
control: form.control,
|
||||
name: `config.events.${index}.name`,
|
||||
});
|
||||
const properties = useEventProperties({ projectId, interval, range });
|
||||
|
||||
return (
|
||||
<div className="border bg-def-100 rounded">
|
||||
<div className="row gap-2 items-center p-2">
|
||||
<ColorSquare>{index + 1}</ColorSquare>
|
||||
<Controller
|
||||
control={form.control}
|
||||
name={`config.events.${index}.name`}
|
||||
render={({ field }) => (
|
||||
<Combobox
|
||||
searchable
|
||||
className="flex-1"
|
||||
value={field.value}
|
||||
placeholder="Select event"
|
||||
onChange={field.onChange}
|
||||
items={eventNames.map((item) => ({
|
||||
label: item.name,
|
||||
value: item.name,
|
||||
}))}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Combobox
|
||||
searchable
|
||||
placeholder="Select a filter"
|
||||
value=""
|
||||
items={properties.map((item) => ({
|
||||
label: item,
|
||||
value: item,
|
||||
}))}
|
||||
onChange={(value) => {
|
||||
filtersArray.append({
|
||||
id: shortId(),
|
||||
name: value,
|
||||
operator: 'is',
|
||||
value: [],
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Button variant={'outline'} icon={FilterIcon} size={'icon'} />
|
||||
</Combobox>
|
||||
<Button
|
||||
onClick={() => {
|
||||
remove();
|
||||
}}
|
||||
variant={'outline'}
|
||||
className="text-destructive"
|
||||
icon={TrashIcon}
|
||||
size={'icon'}
|
||||
/>
|
||||
</div>
|
||||
{filtersArray.fields.map((filter, index) => {
|
||||
return (
|
||||
<div key={filter.id} className="p-2 border-t">
|
||||
<PureFilterItem
|
||||
eventName={eventName}
|
||||
filter={filter}
|
||||
range={range}
|
||||
startDate={null}
|
||||
endDate={null}
|
||||
interval={interval}
|
||||
onRemove={() => {
|
||||
filtersArray.remove(index);
|
||||
}}
|
||||
onChangeValue={(value) => {
|
||||
filtersArray.update(index, {
|
||||
...filter,
|
||||
value,
|
||||
});
|
||||
}}
|
||||
onChangeOperator={(operator) => {
|
||||
filtersArray.update(index, {
|
||||
...filter,
|
||||
operator,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -74,6 +74,8 @@ const modals = {
|
||||
Testimonial: dynamic(() => import('./Testimonial'), {
|
||||
loading: Loading,
|
||||
}),
|
||||
AddIntegration: dynamic(() => import('./add-integration')),
|
||||
AddNotificationRule: dynamic(() => import('./add-notification-rule')),
|
||||
};
|
||||
|
||||
export const {
|
||||
|
||||
Reference in New Issue
Block a user