realtimex-crm 0.8.1 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/realtimex-crm.js +56 -32
- package/dist/assets/{DealList-B3alafQv.js → DealList-DbwJCRGl.js} +2 -2
- package/dist/assets/{DealList-B3alafQv.js.map → DealList-DbwJCRGl.js.map} +1 -1
- package/dist/assets/index-C__S90Gb.css +1 -0
- package/dist/assets/index-mE-upBfc.js +166 -0
- package/dist/assets/index-mE-upBfc.js.map +1 -0
- package/dist/index.html +1 -1
- package/dist/stats.html +1 -1
- package/package.json +3 -1
- package/src/components/atomic-crm/activities/ActivitiesPage.tsx +16 -0
- package/src/components/atomic-crm/activities/ActivityFeed.tsx +212 -0
- package/src/components/atomic-crm/activities/FileUpload.tsx +359 -0
- package/src/components/atomic-crm/contacts/ContactShow.tsx +28 -10
- package/src/components/atomic-crm/integrations/ApiKeysTab.tsx +184 -0
- package/src/components/atomic-crm/integrations/CreateApiKeyDialog.tsx +217 -0
- package/src/components/atomic-crm/integrations/CreateChannelDialog.tsx +139 -0
- package/src/components/atomic-crm/integrations/IngestionChannelsTab.tsx +188 -0
- package/src/components/atomic-crm/integrations/IntegrationsPage.tsx +46 -0
- package/src/components/atomic-crm/integrations/WebhooksTab.tsx +402 -0
- package/src/components/atomic-crm/layout/Header.tsx +14 -1
- package/src/components/atomic-crm/root/CRM.tsx +2 -0
- package/src/components/ui/alert-dialog.tsx +155 -0
- package/src/lib/api-key-utils.ts +22 -0
- package/supabase/fix_webhook_hardcoded.sql +34 -0
- package/supabase/functions/_shared/apiKeyAuth.ts +171 -0
- package/supabase/functions/_shared/ingestionGuard.ts +128 -0
- package/supabase/functions/_shared/utils.ts +1 -1
- package/supabase/functions/_shared/webhookSignature.ts +23 -0
- package/supabase/functions/api-v1-activities/index.ts +137 -0
- package/supabase/functions/api-v1-companies/index.ts +166 -0
- package/supabase/functions/api-v1-contacts/index.ts +171 -0
- package/supabase/functions/api-v1-deals/index.ts +166 -0
- package/supabase/functions/ingest-activity/.well-known/supabase/config.toml +4 -0
- package/supabase/functions/ingest-activity/index.ts +261 -0
- package/supabase/functions/webhook-dispatcher/index.ts +133 -0
- package/supabase/migrations/20251219120000_api_integrations.sql +133 -0
- package/supabase/migrations/20251219120100_webhook_triggers.sql +176 -0
- package/supabase/migrations/20251219120200_webhook_cron.sql +26 -0
- package/supabase/migrations/20251220120000_realtime_ingestion.sql +154 -0
- package/supabase/migrations/20251221000000_contact_matching.sql +94 -0
- package/supabase/migrations/20251221000001_fix_ingestion_providers_rls.sql +23 -0
- package/supabase/migrations/20251221000002_fix_contact_matching_jsonb.sql +67 -0
- package/supabase/migrations/20251221000003_fix_email_matching.sql +70 -0
- package/supabase/migrations/20251221000004_time_based_work_stealing.sql +99 -0
- package/supabase/migrations/20251221000005_realtime_functions.sql +73 -0
- package/supabase/migrations/20251221000006_enable_pg_net.sql +3 -0
- package/supabase/migrations/20251222075019_enable_extensions_and_configure_cron.sql +86 -0
- package/supabase/migrations/20251222094036_large_payload_storage.sql +213 -0
- package/supabase/migrations/20251222094247_large_payload_cron.sql +28 -0
- package/supabase/migrations/20251222220000_fix_large_payload_types.sql +72 -0
- package/supabase/migrations/20251223000000_enable_realtime_all_crm_tables.sql +50 -0
- package/supabase/migrations/20251223185638_remove_large_payload_storage.sql +54 -0
- package/dist/assets/index-CHk72bAf.css +0 -1
- package/dist/assets/index-mhAjQ1_w.js +0 -153
- package/dist/assets/index-mhAjQ1_w.js.map +0 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { useDataProvider, useNotify, useGetIdentity } from "ra-core";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
6
|
+
import { Plus, Trash2, Copy } from "lucide-react";
|
|
7
|
+
import { CreateApiKeyDialog } from "./CreateApiKeyDialog";
|
|
8
|
+
import { Badge } from "@/components/ui/badge";
|
|
9
|
+
import { format } from "date-fns";
|
|
10
|
+
import {
|
|
11
|
+
AlertDialog,
|
|
12
|
+
AlertDialogAction,
|
|
13
|
+
AlertDialogCancel,
|
|
14
|
+
AlertDialogContent,
|
|
15
|
+
AlertDialogDescription,
|
|
16
|
+
AlertDialogFooter,
|
|
17
|
+
AlertDialogHeader,
|
|
18
|
+
AlertDialogTitle,
|
|
19
|
+
} from "@/components/ui/alert-dialog";
|
|
20
|
+
|
|
21
|
+
export const ApiKeysTab = () => {
|
|
22
|
+
const [showCreateDialog, setShowCreateDialog] = useState(false);
|
|
23
|
+
const [keyToDelete, setKeyToDelete] = useState<number | null>(null);
|
|
24
|
+
const dataProvider = useDataProvider();
|
|
25
|
+
const notify = useNotify();
|
|
26
|
+
const queryClient = useQueryClient();
|
|
27
|
+
const { identity: _identity } = useGetIdentity();
|
|
28
|
+
|
|
29
|
+
const { data: apiKeys, isLoading } = useQuery({
|
|
30
|
+
queryKey: ["api_keys"],
|
|
31
|
+
queryFn: async () => {
|
|
32
|
+
const { data } = await dataProvider.getList("api_keys", {
|
|
33
|
+
pagination: { page: 1, perPage: 100 },
|
|
34
|
+
sort: { field: "created_at", order: "DESC" },
|
|
35
|
+
filter: {},
|
|
36
|
+
});
|
|
37
|
+
return data;
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const deleteMutation = useMutation({
|
|
42
|
+
mutationFn: async (id: number) => {
|
|
43
|
+
await dataProvider.delete("api_keys", { id, previousData: {} });
|
|
44
|
+
},
|
|
45
|
+
onSuccess: () => {
|
|
46
|
+
queryClient.invalidateQueries({ queryKey: ["api_keys"] });
|
|
47
|
+
notify("API key deleted successfully");
|
|
48
|
+
setKeyToDelete(null);
|
|
49
|
+
},
|
|
50
|
+
onError: () => {
|
|
51
|
+
notify("Failed to delete API key", { type: "error" });
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="space-y-4">
|
|
57
|
+
<div className="flex justify-between items-center">
|
|
58
|
+
<p className="text-sm text-muted-foreground">
|
|
59
|
+
API keys allow external applications to access your CRM data
|
|
60
|
+
programmatically.
|
|
61
|
+
</p>
|
|
62
|
+
<Button onClick={() => setShowCreateDialog(true)}>
|
|
63
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
64
|
+
Create API Key
|
|
65
|
+
</Button>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
68
|
+
{isLoading ? (
|
|
69
|
+
<Card>
|
|
70
|
+
<CardContent className="py-8 text-center text-muted-foreground">
|
|
71
|
+
Loading...
|
|
72
|
+
</CardContent>
|
|
73
|
+
</Card>
|
|
74
|
+
) : apiKeys && apiKeys.length > 0 ? (
|
|
75
|
+
<div className="space-y-3">
|
|
76
|
+
{apiKeys.map((key: any) => (
|
|
77
|
+
<ApiKeyCard
|
|
78
|
+
key={key.id}
|
|
79
|
+
apiKey={key}
|
|
80
|
+
onDelete={() => setKeyToDelete(key.id)}
|
|
81
|
+
/>
|
|
82
|
+
))}
|
|
83
|
+
</div>
|
|
84
|
+
) : (
|
|
85
|
+
<Card>
|
|
86
|
+
<CardContent className="py-12 text-center">
|
|
87
|
+
<p className="text-muted-foreground mb-4">No API keys yet</p>
|
|
88
|
+
<Button onClick={() => setShowCreateDialog(true)}>
|
|
89
|
+
<Plus className="h-4 w-4 mr-2" />
|
|
90
|
+
Create your first API key
|
|
91
|
+
</Button>
|
|
92
|
+
</CardContent>
|
|
93
|
+
</Card>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
<CreateApiKeyDialog
|
|
97
|
+
open={showCreateDialog}
|
|
98
|
+
onClose={() => setShowCreateDialog(false)}
|
|
99
|
+
/>
|
|
100
|
+
|
|
101
|
+
<AlertDialog
|
|
102
|
+
open={keyToDelete !== null}
|
|
103
|
+
onOpenChange={() => setKeyToDelete(null)}
|
|
104
|
+
>
|
|
105
|
+
<AlertDialogContent>
|
|
106
|
+
<AlertDialogHeader>
|
|
107
|
+
<AlertDialogTitle>Delete API Key?</AlertDialogTitle>
|
|
108
|
+
<AlertDialogDescription>
|
|
109
|
+
This will permanently delete this API key. Any applications using
|
|
110
|
+
this key will stop working immediately. This action cannot be
|
|
111
|
+
undone.
|
|
112
|
+
</AlertDialogDescription>
|
|
113
|
+
</AlertDialogHeader>
|
|
114
|
+
<AlertDialogFooter>
|
|
115
|
+
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
116
|
+
<AlertDialogAction
|
|
117
|
+
onClick={() => keyToDelete && deleteMutation.mutate(keyToDelete)}
|
|
118
|
+
className="bg-destructive hover:bg-destructive/90"
|
|
119
|
+
>
|
|
120
|
+
Delete
|
|
121
|
+
</AlertDialogAction>
|
|
122
|
+
</AlertDialogFooter>
|
|
123
|
+
</AlertDialogContent>
|
|
124
|
+
</AlertDialog>
|
|
125
|
+
</div>
|
|
126
|
+
);
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
const ApiKeyCard = ({
|
|
130
|
+
apiKey,
|
|
131
|
+
onDelete,
|
|
132
|
+
}: {
|
|
133
|
+
apiKey: any;
|
|
134
|
+
onDelete: () => void;
|
|
135
|
+
}) => {
|
|
136
|
+
const notify = useNotify();
|
|
137
|
+
|
|
138
|
+
const copyKey = () => {
|
|
139
|
+
navigator.clipboard.writeText(apiKey.key_prefix + "••••••••");
|
|
140
|
+
notify("Key prefix copied (full key only shown once at creation)");
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<Card>
|
|
145
|
+
<CardHeader className="pb-3">
|
|
146
|
+
<div className="flex justify-between items-start">
|
|
147
|
+
<div>
|
|
148
|
+
<CardTitle className="text-lg">{apiKey.name}</CardTitle>
|
|
149
|
+
<div className="flex gap-2 mt-2">
|
|
150
|
+
{apiKey.is_active ? (
|
|
151
|
+
<Badge variant="default">Active</Badge>
|
|
152
|
+
) : (
|
|
153
|
+
<Badge variant="secondary">Inactive</Badge>
|
|
154
|
+
)}
|
|
155
|
+
{apiKey.scopes && apiKey.scopes.length > 0 && (
|
|
156
|
+
<Badge variant="outline">{apiKey.scopes.join(", ")}</Badge>
|
|
157
|
+
)}
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<Button variant="ghost" size="icon" onClick={onDelete}>
|
|
161
|
+
<Trash2 className="h-4 w-4 text-destructive" />
|
|
162
|
+
</Button>
|
|
163
|
+
</div>
|
|
164
|
+
</CardHeader>
|
|
165
|
+
<CardContent className="space-y-2">
|
|
166
|
+
<div className="flex items-center gap-2 font-mono text-sm bg-muted p-2 rounded">
|
|
167
|
+
<span>{apiKey.key_prefix}••••••••••••••••••••</span>
|
|
168
|
+
<Button variant="ghost" size="icon" onClick={copyKey}>
|
|
169
|
+
<Copy className="h-4 w-4" />
|
|
170
|
+
</Button>
|
|
171
|
+
</div>
|
|
172
|
+
<div className="text-xs text-muted-foreground space-y-1">
|
|
173
|
+
<p>Created: {format(new Date(apiKey.created_at), "PPP")}</p>
|
|
174
|
+
{apiKey.last_used_at && (
|
|
175
|
+
<p>Last used: {format(new Date(apiKey.last_used_at), "PPp")}</p>
|
|
176
|
+
)}
|
|
177
|
+
{apiKey.expires_at && (
|
|
178
|
+
<p>Expires: {format(new Date(apiKey.expires_at), "PPP")}</p>
|
|
179
|
+
)}
|
|
180
|
+
</div>
|
|
181
|
+
</CardContent>
|
|
182
|
+
</Card>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useForm } from "react-hook-form";
|
|
3
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
4
|
+
import { useDataProvider, useNotify, useGetIdentity } from "ra-core";
|
|
5
|
+
import { generateApiKey, hashApiKey } from "@/lib/api-key-utils";
|
|
6
|
+
import {
|
|
7
|
+
Dialog,
|
|
8
|
+
DialogContent,
|
|
9
|
+
DialogDescription,
|
|
10
|
+
DialogHeader,
|
|
11
|
+
DialogTitle,
|
|
12
|
+
} from "@/components/ui/dialog";
|
|
13
|
+
import { Button } from "@/components/ui/button";
|
|
14
|
+
import { Input } from "@/components/ui/input";
|
|
15
|
+
import { Label } from "@/components/ui/label";
|
|
16
|
+
import { Checkbox } from "@/components/ui/checkbox";
|
|
17
|
+
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
18
|
+
import { Copy, CheckCircle } from "lucide-react";
|
|
19
|
+
|
|
20
|
+
interface CreateApiKeyDialogProps {
|
|
21
|
+
open: boolean;
|
|
22
|
+
onClose: () => void;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const AVAILABLE_SCOPES = [
|
|
26
|
+
{ value: "contacts:read", label: "Contacts: Read" },
|
|
27
|
+
{ value: "contacts:write", label: "Contacts: Write" },
|
|
28
|
+
{ value: "companies:read", label: "Companies: Read" },
|
|
29
|
+
{ value: "companies:write", label: "Companies: Write" },
|
|
30
|
+
{ value: "deals:read", label: "Deals: Read" },
|
|
31
|
+
{ value: "deals:write", label: "Deals: Write" },
|
|
32
|
+
{ value: "activities:write", label: "Activities: Write" },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
export const CreateApiKeyDialog = ({
|
|
36
|
+
open,
|
|
37
|
+
onClose,
|
|
38
|
+
}: CreateApiKeyDialogProps) => {
|
|
39
|
+
const [createdKey, setCreatedKey] = useState<string | null>(null);
|
|
40
|
+
const [copied, setCopied] = useState(false);
|
|
41
|
+
const dataProvider = useDataProvider();
|
|
42
|
+
const notify = useNotify();
|
|
43
|
+
const queryClient = useQueryClient();
|
|
44
|
+
const { identity } = useGetIdentity();
|
|
45
|
+
|
|
46
|
+
const { register, handleSubmit, watch, setValue, reset } = useForm({
|
|
47
|
+
defaultValues: {
|
|
48
|
+
name: "",
|
|
49
|
+
scopes: [] as string[],
|
|
50
|
+
expires_at: "",
|
|
51
|
+
},
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
const createMutation = useMutation({
|
|
55
|
+
mutationFn: async (values: any) => {
|
|
56
|
+
const apiKey = generateApiKey();
|
|
57
|
+
const keyHash = await hashApiKey(apiKey);
|
|
58
|
+
const keyPrefix = apiKey.substring(0, 12);
|
|
59
|
+
|
|
60
|
+
const { data } = await dataProvider.create("api_keys", {
|
|
61
|
+
data: {
|
|
62
|
+
name: values.name,
|
|
63
|
+
key_hash: keyHash,
|
|
64
|
+
key_prefix: keyPrefix,
|
|
65
|
+
scopes: values.scopes,
|
|
66
|
+
is_active: true,
|
|
67
|
+
expires_at: values.expires_at || null,
|
|
68
|
+
sales_id: identity?.id,
|
|
69
|
+
created_by_sales_id: identity?.id,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return { data, apiKey };
|
|
74
|
+
},
|
|
75
|
+
onSuccess: ({ apiKey }) => {
|
|
76
|
+
setCreatedKey(apiKey);
|
|
77
|
+
queryClient.invalidateQueries({ queryKey: ["api_keys"] });
|
|
78
|
+
notify("API key created successfully");
|
|
79
|
+
},
|
|
80
|
+
onError: () => {
|
|
81
|
+
notify("Failed to create API key", { type: "error" });
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
const handleClose = () => {
|
|
86
|
+
setCreatedKey(null);
|
|
87
|
+
setCopied(false);
|
|
88
|
+
reset();
|
|
89
|
+
onClose();
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const copyApiKey = () => {
|
|
93
|
+
if (createdKey) {
|
|
94
|
+
navigator.clipboard.writeText(createdKey);
|
|
95
|
+
setCopied(true);
|
|
96
|
+
setTimeout(() => setCopied(false), 2000);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const toggleScope = (scope: string) => {
|
|
101
|
+
const currentScopes = watch("scopes");
|
|
102
|
+
if (currentScopes.includes(scope)) {
|
|
103
|
+
setValue(
|
|
104
|
+
"scopes",
|
|
105
|
+
currentScopes.filter((s) => s !== scope)
|
|
106
|
+
);
|
|
107
|
+
} else {
|
|
108
|
+
setValue("scopes", [...currentScopes, scope]);
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<Dialog open={open} onOpenChange={handleClose}>
|
|
114
|
+
<DialogContent className="sm:max-w-[500px]">
|
|
115
|
+
<DialogHeader>
|
|
116
|
+
<DialogTitle>
|
|
117
|
+
{createdKey ? "API Key Created" : "Create API Key"}
|
|
118
|
+
</DialogTitle>
|
|
119
|
+
<DialogDescription>
|
|
120
|
+
{createdKey
|
|
121
|
+
? "Copy this key now - it won't be shown again!"
|
|
122
|
+
: "Create a new API key to access the CRM API"}
|
|
123
|
+
</DialogDescription>
|
|
124
|
+
</DialogHeader>
|
|
125
|
+
|
|
126
|
+
{createdKey ? (
|
|
127
|
+
<div className="space-y-4">
|
|
128
|
+
<Alert>
|
|
129
|
+
<AlertDescription>
|
|
130
|
+
Make sure to copy your API key now. You won't be able to see it
|
|
131
|
+
again!
|
|
132
|
+
</AlertDescription>
|
|
133
|
+
</Alert>
|
|
134
|
+
|
|
135
|
+
<div className="space-y-2">
|
|
136
|
+
<Label>Your API Key</Label>
|
|
137
|
+
<div className="flex gap-2">
|
|
138
|
+
<Input
|
|
139
|
+
value={createdKey}
|
|
140
|
+
readOnly
|
|
141
|
+
className="font-mono text-sm"
|
|
142
|
+
/>
|
|
143
|
+
<Button
|
|
144
|
+
type="button"
|
|
145
|
+
variant="outline"
|
|
146
|
+
size="icon"
|
|
147
|
+
onClick={copyApiKey}
|
|
148
|
+
>
|
|
149
|
+
{copied ? (
|
|
150
|
+
<CheckCircle className="h-4 w-4 text-green-500" />
|
|
151
|
+
) : (
|
|
152
|
+
<Copy className="h-4 w-4" />
|
|
153
|
+
)}
|
|
154
|
+
</Button>
|
|
155
|
+
</div>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div className="flex justify-end">
|
|
159
|
+
<Button onClick={handleClose}>Done</Button>
|
|
160
|
+
</div>
|
|
161
|
+
</div>
|
|
162
|
+
) : (
|
|
163
|
+
<form
|
|
164
|
+
onSubmit={handleSubmit((values) => createMutation.mutate(values))}
|
|
165
|
+
>
|
|
166
|
+
<div className="space-y-4">
|
|
167
|
+
<div className="space-y-2">
|
|
168
|
+
<Label htmlFor="name">Name</Label>
|
|
169
|
+
<Input
|
|
170
|
+
id="name"
|
|
171
|
+
placeholder="e.g., Production API Key"
|
|
172
|
+
{...register("name", { required: true })}
|
|
173
|
+
/>
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div className="space-y-2">
|
|
177
|
+
<Label>Scopes</Label>
|
|
178
|
+
<div className="space-y-2">
|
|
179
|
+
{AVAILABLE_SCOPES.map((scope) => (
|
|
180
|
+
<div key={scope.value} className="flex items-center space-x-2">
|
|
181
|
+
<Checkbox
|
|
182
|
+
id={scope.value}
|
|
183
|
+
checked={watch("scopes").includes(scope.value)}
|
|
184
|
+
onCheckedChange={() => toggleScope(scope.value)}
|
|
185
|
+
/>
|
|
186
|
+
<label htmlFor={scope.value} className="text-sm cursor-pointer">
|
|
187
|
+
{scope.label}
|
|
188
|
+
</label>
|
|
189
|
+
</div>
|
|
190
|
+
))}
|
|
191
|
+
</div>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<div className="space-y-2">
|
|
195
|
+
<Label htmlFor="expires_at">Expiration (optional)</Label>
|
|
196
|
+
<Input
|
|
197
|
+
id="expires_at"
|
|
198
|
+
type="date"
|
|
199
|
+
{...register("expires_at")}
|
|
200
|
+
/>
|
|
201
|
+
</div>
|
|
202
|
+
|
|
203
|
+
<div className="flex justify-end gap-2">
|
|
204
|
+
<Button type="button" variant="outline" onClick={handleClose}>
|
|
205
|
+
Cancel
|
|
206
|
+
</Button>
|
|
207
|
+
<Button type="submit" disabled={createMutation.isPending}>
|
|
208
|
+
Create
|
|
209
|
+
</Button>
|
|
210
|
+
</div>
|
|
211
|
+
</div>
|
|
212
|
+
</form>
|
|
213
|
+
)}
|
|
214
|
+
</DialogContent>
|
|
215
|
+
</Dialog>
|
|
216
|
+
);
|
|
217
|
+
};
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
3
|
+
import { useDataProvider, useNotify } from "ra-core";
|
|
4
|
+
import { Button } from "@/components/ui/button";
|
|
5
|
+
import {
|
|
6
|
+
Dialog,
|
|
7
|
+
DialogContent,
|
|
8
|
+
DialogDescription,
|
|
9
|
+
DialogHeader,
|
|
10
|
+
DialogTitle,
|
|
11
|
+
DialogFooter,
|
|
12
|
+
} from "@/components/ui/dialog";
|
|
13
|
+
import { Input } from "@/components/ui/input";
|
|
14
|
+
import { Label } from "@/components/ui/label";
|
|
15
|
+
import {
|
|
16
|
+
Select,
|
|
17
|
+
SelectContent,
|
|
18
|
+
SelectItem,
|
|
19
|
+
SelectTrigger,
|
|
20
|
+
SelectValue,
|
|
21
|
+
} from "@/components/ui/select";
|
|
22
|
+
import { Loader2 } from "lucide-react";
|
|
23
|
+
|
|
24
|
+
interface CreateChannelDialogProps {
|
|
25
|
+
open: boolean;
|
|
26
|
+
onClose: () => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const CreateChannelDialog = ({
|
|
30
|
+
open,
|
|
31
|
+
onClose,
|
|
32
|
+
}: CreateChannelDialogProps) => {
|
|
33
|
+
const [providerCode, setProviderCode] = useState<string>("twilio");
|
|
34
|
+
const [name, setName] = useState("");
|
|
35
|
+
const [authToken, setAuthToken] = useState("");
|
|
36
|
+
const dataProvider = useDataProvider();
|
|
37
|
+
const notify = useNotify();
|
|
38
|
+
const queryClient = useQueryClient();
|
|
39
|
+
|
|
40
|
+
const createMutation = useMutation({
|
|
41
|
+
mutationFn: async () => {
|
|
42
|
+
// Generate a cryptographically secure random ingestion key
|
|
43
|
+
const ingestionKey = "ik_live_" + crypto.randomUUID().replace(/-/g, '');
|
|
44
|
+
|
|
45
|
+
const config: any = {};
|
|
46
|
+
if (authToken) {
|
|
47
|
+
config.auth_token = authToken;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
await dataProvider.create("ingestion_providers", {
|
|
51
|
+
data: {
|
|
52
|
+
name,
|
|
53
|
+
provider_code: providerCode,
|
|
54
|
+
is_active: true,
|
|
55
|
+
config,
|
|
56
|
+
ingestion_key: ingestionKey
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
onSuccess: () => {
|
|
61
|
+
queryClient.invalidateQueries({ queryKey: ["ingestion_providers"] });
|
|
62
|
+
notify("Ingestion Channel created successfully");
|
|
63
|
+
onClose();
|
|
64
|
+
// Reset form
|
|
65
|
+
setName("");
|
|
66
|
+
setAuthToken("");
|
|
67
|
+
setProviderCode("twilio");
|
|
68
|
+
},
|
|
69
|
+
onError: (error: Error) => {
|
|
70
|
+
notify(`Failed to create channel: ${error.message}`, { type: "error" });
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<Dialog open={open} onOpenChange={onClose}>
|
|
76
|
+
<DialogContent className="sm:max-w-[425px]">
|
|
77
|
+
<DialogHeader>
|
|
78
|
+
<DialogTitle>Add Ingestion Channel</DialogTitle>
|
|
79
|
+
<DialogDescription>
|
|
80
|
+
Configure a new source for incoming activities.
|
|
81
|
+
</DialogDescription>
|
|
82
|
+
</DialogHeader>
|
|
83
|
+
<div className="grid gap-4 py-4">
|
|
84
|
+
<div className="grid gap-2">
|
|
85
|
+
<Label htmlFor="name">Channel Name</Label>
|
|
86
|
+
<Input
|
|
87
|
+
id="name"
|
|
88
|
+
placeholder="e.g. US Support Line"
|
|
89
|
+
value={name}
|
|
90
|
+
onChange={(e) => setName(e.target.value)}
|
|
91
|
+
/>
|
|
92
|
+
</div>
|
|
93
|
+
<div className="grid gap-2">
|
|
94
|
+
<Label htmlFor="provider">Provider</Label>
|
|
95
|
+
<Select value={providerCode} onValueChange={setProviderCode}>
|
|
96
|
+
<SelectTrigger>
|
|
97
|
+
<SelectValue placeholder="Select provider" />
|
|
98
|
+
</SelectTrigger>
|
|
99
|
+
<SelectContent>
|
|
100
|
+
<SelectItem value="twilio">Twilio (Voice/SMS)</SelectItem>
|
|
101
|
+
<SelectItem value="postmark">Postmark (Email)</SelectItem>
|
|
102
|
+
<SelectItem value="generic">Generic / Internal</SelectItem>
|
|
103
|
+
</SelectContent>
|
|
104
|
+
</Select>
|
|
105
|
+
</div>
|
|
106
|
+
|
|
107
|
+
{providerCode === "twilio" && (
|
|
108
|
+
<div className="grid gap-2">
|
|
109
|
+
<Label htmlFor="token">Auth Token (Validation)</Label>
|
|
110
|
+
<Input
|
|
111
|
+
id="token"
|
|
112
|
+
type="password"
|
|
113
|
+
placeholder="Twilio Auth Token"
|
|
114
|
+
value={authToken}
|
|
115
|
+
onChange={(e) => setAuthToken(e.target.value)}
|
|
116
|
+
/>
|
|
117
|
+
<p className="text-xs text-muted-foreground">Required to validate inbound requests.</p>
|
|
118
|
+
</div>
|
|
119
|
+
)}
|
|
120
|
+
|
|
121
|
+
</div>
|
|
122
|
+
<DialogFooter>
|
|
123
|
+
<Button variant="outline" onClick={onClose}>
|
|
124
|
+
Cancel
|
|
125
|
+
</Button>
|
|
126
|
+
<Button
|
|
127
|
+
onClick={() => createMutation.mutate()}
|
|
128
|
+
disabled={!name || createMutation.isPending}
|
|
129
|
+
>
|
|
130
|
+
{createMutation.isPending && (
|
|
131
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
132
|
+
)}
|
|
133
|
+
Create Channel
|
|
134
|
+
</Button>
|
|
135
|
+
</DialogFooter>
|
|
136
|
+
</DialogContent>
|
|
137
|
+
</Dialog>
|
|
138
|
+
);
|
|
139
|
+
};
|