tambahan
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -164,6 +164,18 @@ exports.Prisma.WaHookScalarFieldEnum = {
|
|||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.Prisma.ChatFlowsScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
flows: 'flows',
|
||||||
|
defaultFlow: 'defaultFlow',
|
||||||
|
defaultData: 'defaultData',
|
||||||
|
active: 'active',
|
||||||
|
flowUrl: 'flowUrl',
|
||||||
|
flowToken: 'flowToken',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
};
|
||||||
|
|
||||||
exports.Prisma.SortOrder = {
|
exports.Prisma.SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc'
|
||||||
@@ -195,7 +207,8 @@ exports.Prisma.ModelName = {
|
|||||||
User: 'User',
|
User: 'User',
|
||||||
ApiKey: 'ApiKey',
|
ApiKey: 'ApiKey',
|
||||||
WebHook: 'WebHook',
|
WebHook: 'WebHook',
|
||||||
WaHook: 'WaHook'
|
WaHook: 'WaHook',
|
||||||
|
ChatFlows: 'ChatFlows'
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
1337
generated/prisma/index.d.ts
vendored
1337
generated/prisma/index.d.ts
vendored
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "prisma-client-b7159a5a3af13766d165b3bf5d09869b273a4102920455b6b9bd965ee512be7e",
|
"name": "prisma-client-0c787597671925c5dbd90480bfc5d45c3aa9b494d0bff63e2482cd62c5107b7b",
|
||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"types": "index.d.ts",
|
"types": "index.d.ts",
|
||||||
"browser": "default.js",
|
"browser": "default.js",
|
||||||
|
|||||||
@@ -53,3 +53,15 @@ model WaHook {
|
|||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ChatFlows {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
flows Json?
|
||||||
|
defaultFlow String?
|
||||||
|
defaultData Json?
|
||||||
|
active Boolean @default(true)
|
||||||
|
flowUrl String? @unique
|
||||||
|
flowToken String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|||||||
@@ -136,6 +136,18 @@ exports.Prisma.WaHookScalarFieldEnum = {
|
|||||||
updatedAt: 'updatedAt'
|
updatedAt: 'updatedAt'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
exports.Prisma.ChatFlowsScalarFieldEnum = {
|
||||||
|
id: 'id',
|
||||||
|
flows: 'flows',
|
||||||
|
defaultFlow: 'defaultFlow',
|
||||||
|
defaultData: 'defaultData',
|
||||||
|
active: 'active',
|
||||||
|
flowUrl: 'flowUrl',
|
||||||
|
flowToken: 'flowToken',
|
||||||
|
createdAt: 'createdAt',
|
||||||
|
updatedAt: 'updatedAt'
|
||||||
|
};
|
||||||
|
|
||||||
exports.Prisma.SortOrder = {
|
exports.Prisma.SortOrder = {
|
||||||
asc: 'asc',
|
asc: 'asc',
|
||||||
desc: 'desc'
|
desc: 'desc'
|
||||||
@@ -167,7 +179,8 @@ exports.Prisma.ModelName = {
|
|||||||
User: 'User',
|
User: 'User',
|
||||||
ApiKey: 'ApiKey',
|
ApiKey: 'ApiKey',
|
||||||
WebHook: 'WebHook',
|
WebHook: 'WebHook',
|
||||||
WaHook: 'WaHook'
|
WaHook: 'WaHook',
|
||||||
|
ChatFlows: 'ChatFlows'
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Create the Client
|
* Create the Client
|
||||||
@@ -216,13 +229,13 @@ const config = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"inlineSchema": "generator client {\n provider = \"prisma-client-js\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel User {\n id String @id @default(cuid())\n name String?\n email String? @unique\n password String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n ApiKey ApiKey[]\n}\n\nmodel ApiKey {\n id String @id @default(cuid())\n User User? @relation(fields: [userId], references: [id])\n userId String\n name String\n key String @unique @db.Text\n description String?\n expiredAt DateTime?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel WebHook {\n id String @id @default(cuid())\n name String?\n description String?\n url String\n payload String? @default(\"{}\")\n method String @default(\"POST\")\n headers String? @default(\"{}\")\n apiToken String?\n retries Int? @default(3)\n enabled Boolean @default(true)\n replay Boolean @default(false)\n replayKey String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel WaHook {\n id String @id @default(cuid())\n data Json? @db.Json\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n",
|
"inlineSchema": "generator client {\n provider = \"prisma-client-js\"\n output = \"../generated/prisma\"\n}\n\ndatasource db {\n provider = \"postgresql\"\n url = env(\"DATABASE_URL\")\n}\n\nmodel User {\n id String @id @default(cuid())\n name String?\n email String? @unique\n password String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n ApiKey ApiKey[]\n}\n\nmodel ApiKey {\n id String @id @default(cuid())\n User User? @relation(fields: [userId], references: [id])\n userId String\n name String\n key String @unique @db.Text\n description String?\n expiredAt DateTime?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel WebHook {\n id String @id @default(cuid())\n name String?\n description String?\n url String\n payload String? @default(\"{}\")\n method String @default(\"POST\")\n headers String? @default(\"{}\")\n apiToken String?\n retries Int? @default(3)\n enabled Boolean @default(true)\n replay Boolean @default(false)\n replayKey String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel WaHook {\n id String @id @default(cuid())\n data Json? @db.Json\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n\nmodel ChatFlows {\n id String @id @default(cuid())\n flows Json?\n defaultFlow String?\n defaultData Json?\n active Boolean @default(true)\n flowUrl String? @unique\n flowToken String?\n createdAt DateTime @default(now())\n updatedAt DateTime @updatedAt\n}\n",
|
||||||
"inlineSchemaHash": "f672201c199f0f043f3266324775af184d82ebd00379f5c23166510b25c889d0",
|
"inlineSchemaHash": "853d49417068ff1819c3dbb60ac7aeea59288f307f7939669c83fdd63bb495e3",
|
||||||
"copyEngine": true
|
"copyEngine": true
|
||||||
}
|
}
|
||||||
config.dirname = '/'
|
config.dirname = '/'
|
||||||
|
|
||||||
config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"ApiKey\",\"kind\":\"object\",\"type\":\"ApiKey\",\"relationName\":\"ApiKeyToUser\"}],\"dbName\":null},\"ApiKey\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"User\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ApiKeyToUser\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiredAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"WebHook\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"url\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"payload\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"method\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"headers\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"apiToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"retries\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"enabled\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"replay\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"replayKey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"WaHook\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"data\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
|
config.runtimeDataModel = JSON.parse("{\"models\":{\"User\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"email\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"password\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"ApiKey\",\"kind\":\"object\",\"type\":\"ApiKey\",\"relationName\":\"ApiKeyToUser\"}],\"dbName\":null},\"ApiKey\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"User\",\"kind\":\"object\",\"type\":\"User\",\"relationName\":\"ApiKeyToUser\"},{\"name\":\"userId\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"key\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"expiredAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"WebHook\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"name\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"description\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"url\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"payload\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"method\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"headers\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"apiToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"retries\",\"kind\":\"scalar\",\"type\":\"Int\"},{\"name\":\"enabled\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"replay\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"replayKey\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"WaHook\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"data\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null},\"ChatFlows\":{\"fields\":[{\"name\":\"id\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"flows\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"defaultFlow\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"defaultData\",\"kind\":\"scalar\",\"type\":\"Json\"},{\"name\":\"active\",\"kind\":\"scalar\",\"type\":\"Boolean\"},{\"name\":\"flowUrl\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"flowToken\",\"kind\":\"scalar\",\"type\":\"String\"},{\"name\":\"createdAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"},{\"name\":\"updatedAt\",\"kind\":\"scalar\",\"type\":\"DateTime\"}],\"dbName\":null}},\"enums\":{},\"types\":{}}")
|
||||||
defineDmmfProperty(exports.Prisma, config.runtimeDataModel)
|
defineDmmfProperty(exports.Prisma, config.runtimeDataModel)
|
||||||
config.engineWasm = {
|
config.engineWasm = {
|
||||||
getRuntime: async () => require('./query_engine_bg.js'),
|
getRuntime: async () => require('./query_engine_bg.js'),
|
||||||
|
|||||||
@@ -48,8 +48,20 @@ model WebHook {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model WaHook {
|
model WaHook {
|
||||||
id String @id @default(cuid())
|
id String @id @default(cuid())
|
||||||
data Json? @db.Json
|
data Json? @db.Json
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model ChatFlows {
|
||||||
|
id String @id @default(cuid())
|
||||||
|
flows Json?
|
||||||
|
defaultFlow String?
|
||||||
|
defaultData Json?
|
||||||
|
active Boolean @default(true)
|
||||||
|
flowUrl String? @unique
|
||||||
|
flowToken String?
|
||||||
|
createdAt DateTime @default(now())
|
||||||
|
updatedAt DateTime @updatedAt
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import AppRoutes from "./AppRoutes";
|
|||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
return (
|
return (
|
||||||
<MantineProvider>
|
<MantineProvider defaultColorScheme="dark">
|
||||||
<Notifications />
|
<Notifications />
|
||||||
<ModalsProvider>
|
<ModalsProvider>
|
||||||
<AppRoutes />
|
<AppRoutes />
|
||||||
|
|||||||
@@ -7,6 +7,9 @@ import WebhookHome from "./pages/sq/dashboard/webhook/webhook_home";
|
|||||||
import WebhookLayout from "./pages/sq/dashboard/webhook/webhook_layout";
|
import WebhookLayout from "./pages/sq/dashboard/webhook/webhook_layout";
|
||||||
import WajsHome from "./pages/sq/dashboard/wajs/wajs_home";
|
import WajsHome from "./pages/sq/dashboard/wajs/wajs_home";
|
||||||
import WajsLayout from "./pages/sq/dashboard/wajs/wajs_layout";
|
import WajsLayout from "./pages/sq/dashboard/wajs/wajs_layout";
|
||||||
|
import WaHookHome from "./pages/sq/dashboard/wa-hook/wa_hook_home";
|
||||||
|
import FlowWaHook from "./pages/sq/dashboard/wa-hook/flow_wa_hook";
|
||||||
|
import WaHookLayout from "./pages/sq/dashboard/wa-hook/wa_hook_layout";
|
||||||
import ApikeyPage from "./pages/sq/dashboard/apikey/apikey_page";
|
import ApikeyPage from "./pages/sq/dashboard/apikey/apikey_page";
|
||||||
import DashboardPage from "./pages/sq/dashboard/dashboard_page";
|
import DashboardPage from "./pages/sq/dashboard/dashboard_page";
|
||||||
import DashboardLayout from "./pages/sq/dashboard/dashboard_layout";
|
import DashboardLayout from "./pages/sq/dashboard/dashboard_layout";
|
||||||
@@ -48,6 +51,19 @@ export default function AppRoutes() {
|
|||||||
element={<WajsHome />}
|
element={<WajsHome />}
|
||||||
/>
|
/>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
|
<Route path="/sq/dashboard/wa-hook" element={<WaHookLayout />}>
|
||||||
|
<Route index element={<WaHookHome />} />
|
||||||
|
|
||||||
|
<Route
|
||||||
|
path="/sq/dashboard/wa-hook/wa-hook-home"
|
||||||
|
element={<WaHookHome />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/sq/dashboard/wa-hook/flow-wa-hook"
|
||||||
|
element={<FlowWaHook />}
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
<Route
|
<Route
|
||||||
path="/sq/dashboard/apikey/apikey"
|
path="/sq/dashboard/apikey/apikey"
|
||||||
element={<ApikeyPage />}
|
element={<ApikeyPage />}
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ const clientRoutes = {
|
|||||||
"/sq/dashboard/webhook/webhook-home": "/sq/dashboard/webhook/webhook-home",
|
"/sq/dashboard/webhook/webhook-home": "/sq/dashboard/webhook/webhook-home",
|
||||||
"/sq/dashboard/wajs": "/sq/dashboard/wajs",
|
"/sq/dashboard/wajs": "/sq/dashboard/wajs",
|
||||||
"/sq/dashboard/wajs/wajs-home": "/sq/dashboard/wajs/wajs-home",
|
"/sq/dashboard/wajs/wajs-home": "/sq/dashboard/wajs/wajs-home",
|
||||||
|
"/sq/dashboard/wa-hook": "/sq/dashboard/wa-hook",
|
||||||
|
"/sq/dashboard/wa-hook/wa-hook-home": "/sq/dashboard/wa-hook/wa-hook-home",
|
||||||
|
"/sq/dashboard/wa-hook/flow-wa-hook": "/sq/dashboard/wa-hook/flow-wa-hook",
|
||||||
"/sq/dashboard/apikey/apikey": "/sq/dashboard/apikey/apikey",
|
"/sq/dashboard/apikey/apikey": "/sq/dashboard/apikey/apikey",
|
||||||
"/sq/dashboard/dashboard": "/sq/dashboard/dashboard",
|
"/sq/dashboard/dashboard": "/sq/dashboard/dashboard",
|
||||||
"/login": "/login",
|
"/login": "/login",
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import WaRoute from "./server/routes/wa_route";
|
|||||||
import WebhookRoute from "./server/routes/webhook_route";
|
import WebhookRoute from "./server/routes/webhook_route";
|
||||||
import cors from "@elysiajs/cors";
|
import cors from "@elysiajs/cors";
|
||||||
import WaHookRoute from "./server/routes/wa_hook_route";
|
import WaHookRoute from "./server/routes/wa_hook_route";
|
||||||
|
import FlowRoute from "./server/routes/flow_route";
|
||||||
|
|
||||||
const Docs = new Elysia().use(
|
const Docs = new Elysia().use(
|
||||||
Swagger({
|
Swagger({
|
||||||
@@ -35,7 +36,7 @@ const Api = new Elysia({
|
|||||||
.use(ApiUser)
|
.use(ApiUser)
|
||||||
.use(WaRoute)
|
.use(WaRoute)
|
||||||
.use(WebhookRoute)
|
.use(WebhookRoute)
|
||||||
|
.use(FlowRoute);
|
||||||
|
|
||||||
const app = new Elysia()
|
const app = new Elysia()
|
||||||
.use(cors())
|
.use(cors())
|
||||||
|
|||||||
@@ -1,7 +1,15 @@
|
|||||||
|
import clientRoutes from "@/clientRoutes";
|
||||||
|
import { Button, Container } from "@mantine/core";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
|
const navigate = useNavigate();
|
||||||
return (
|
return (
|
||||||
<div>
|
<Container>
|
||||||
<h1>Home</h1>
|
<h1>Home</h1>
|
||||||
</div>
|
<Button onClick={() => navigate(clientRoutes["/sq/dashboard"])}>
|
||||||
|
Go to SQ
|
||||||
|
</Button>
|
||||||
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,25 +254,17 @@ function ListApiKey({ refresh }: { refresh: boolean }) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Table.Td>{apiKey.name}</Table.Td>
|
<Table.Td>{apiKey.name}</Table.Td>
|
||||||
<Table.Td c="#9A9A9A">
|
<Table.Td c="#9A9A9A">{apiKey.description || "—"}</Table.Td>
|
||||||
{apiKey.description || "—"}
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
{apiKey.expiredAt
|
{apiKey.expiredAt
|
||||||
? new Date(apiKey.expiredAt)
|
? new Date(apiKey.expiredAt).toISOString().split("T")[0]
|
||||||
.toISOString()
|
|
||||||
.split("T")[0]
|
|
||||||
: "—"}
|
: "—"}
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
{new Date(apiKey.createdAt)
|
{new Date(apiKey.createdAt).toISOString().split("T")[0]}
|
||||||
.toISOString()
|
|
||||||
.split("T")[0]}
|
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td>
|
<Table.Td>
|
||||||
{new Date(apiKey.updatedAt)
|
{new Date(apiKey.updatedAt).toISOString().split("T")[0]}
|
||||||
.toISOString()
|
|
||||||
.split("T")[0]}
|
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td align="right">
|
<Table.Td align="right">
|
||||||
<Group gap={4} justify="right">
|
<Group gap={4} justify="right">
|
||||||
@@ -301,7 +293,7 @@ function ListApiKey({ refresh }: { refresh: boolean }) {
|
|||||||
id: apiKey.id,
|
id: apiKey.id,
|
||||||
});
|
});
|
||||||
setApiKeys((prev) =>
|
setApiKeys((prev) =>
|
||||||
prev.filter((a) => a.id !== apiKey.id)
|
prev.filter((a) => a.id !== apiKey.id),
|
||||||
);
|
);
|
||||||
showNotification({
|
showNotification({
|
||||||
title: "Deleted",
|
title: "Deleted",
|
||||||
|
|||||||
@@ -258,6 +258,12 @@ function NavigationDashboard() {
|
|||||||
icon: <IconWebhook size={20} color="#00FFFF" />,
|
icon: <IconWebhook size={20} color="#00FFFF" />,
|
||||||
desc: "Incoming and outgoing event handlers",
|
desc: "Incoming and outgoing event handlers",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: clientRoutes["/sq/dashboard/wa-hook/wa-hook-home"],
|
||||||
|
label: "WA Hook",
|
||||||
|
icon: <IconWebhook size={20} color="#00FFFF" />,
|
||||||
|
desc: "WA Hook",
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
195
src/pages/sq/dashboard/wa-hook/flow_wa_hook.tsx
Normal file
195
src/pages/sq/dashboard/wa-hook/flow_wa_hook.tsx
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
import { useState, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
Container,
|
||||||
|
Stack,
|
||||||
|
Card,
|
||||||
|
Group,
|
||||||
|
Button,
|
||||||
|
Table,
|
||||||
|
TextInput,
|
||||||
|
PasswordInput,
|
||||||
|
ActionIcon,
|
||||||
|
Checkbox,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
Flex,
|
||||||
|
Loader,
|
||||||
|
} from "@mantine/core";
|
||||||
|
import { IconReload, IconCheck, IconCopy } from "@tabler/icons-react";
|
||||||
|
import { showNotification } from "@mantine/notifications";
|
||||||
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
|
import apiFetch from "@/lib/apiFetch";
|
||||||
|
|
||||||
|
export default function FlowWaHook() {
|
||||||
|
return (
|
||||||
|
<Container size="xl" px="md">
|
||||||
|
<Stack gap="xl">
|
||||||
|
<FlowWaHookForm />
|
||||||
|
<FlowWaHookList />
|
||||||
|
</Stack>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FlowWaHookList() {
|
||||||
|
const [flows, setFlows] = useState<{ id: string; name: string; type: string }[]>([]);
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [defaultFlow, setDefaultFlow] = useState("");
|
||||||
|
|
||||||
|
const loadFlows = useCallback(async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const { data, error } = await apiFetch.api.chatflows.find.get();
|
||||||
|
if (error) {
|
||||||
|
showNotification({ title: "Error", message: "Failed to load flows", color: "red" });
|
||||||
|
} else {
|
||||||
|
setFlows(data?.flows || []);
|
||||||
|
setDefaultFlow(data?.defaultFlow || "");
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
loadFlows();
|
||||||
|
}, [loadFlows]);
|
||||||
|
|
||||||
|
const syncFlows = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
const { error } = await apiFetch.api.chatflows.sync.get();
|
||||||
|
if (error) {
|
||||||
|
showNotification({ title: "Error", message: "Sync failed", color: "red" });
|
||||||
|
} else {
|
||||||
|
await loadFlows();
|
||||||
|
showNotification({ title: "Success", message: "Flows synchronized", color: "green" });
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setAsDefault = async (id: string) => {
|
||||||
|
setLoading(true);
|
||||||
|
const { error } = await apiFetch.api.chatflows.default.put({ id, defaultData: {} });
|
||||||
|
if (error) {
|
||||||
|
showNotification({ title: "Error", message: "Failed to set default flow", color: "red" });
|
||||||
|
} else {
|
||||||
|
await loadFlows();
|
||||||
|
showNotification({ title: "Success", message: "Default flow updated", color: "green" });
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card radius="md" p="lg" withBorder>
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Flex justify="space-between" align="center">
|
||||||
|
<Title order={3}>Flow Management</Title>
|
||||||
|
<Button leftSection={<IconReload size={16} />} onClick={syncFlows} loading={loading}>
|
||||||
|
Sync Flows
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
<Table striped highlightOnHover withTableBorder withColumnBorders>
|
||||||
|
<Table.Thead>
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Th>Name</Table.Th>
|
||||||
|
<Table.Th>Type</Table.Th>
|
||||||
|
<Table.Th>Default</Table.Th>
|
||||||
|
</Table.Tr>
|
||||||
|
</Table.Thead>
|
||||||
|
<Table.Tbody>
|
||||||
|
{flows.map((flow) => (
|
||||||
|
<Table.Tr key={flow.id} bg={defaultFlow === flow.id ? "dark.4" : undefined}>
|
||||||
|
<Table.Td>{flow.name}</Table.Td>
|
||||||
|
<Table.Td>{flow.type}</Table.Td>
|
||||||
|
<Table.Td>
|
||||||
|
<Checkbox checked={defaultFlow === flow.id} onChange={() => setAsDefault(flow.id)} />
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
))}
|
||||||
|
{!loading && flows.length === 0 && (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={3}>
|
||||||
|
<Text ta="center" c="dimmed">
|
||||||
|
No flows available
|
||||||
|
</Text>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
)}
|
||||||
|
{loading && (
|
||||||
|
<Table.Tr>
|
||||||
|
<Table.Td colSpan={3}>
|
||||||
|
<Flex justify="center" align="center" py="md">
|
||||||
|
<Loader size="sm" />
|
||||||
|
</Flex>
|
||||||
|
</Table.Td>
|
||||||
|
</Table.Tr>
|
||||||
|
)}
|
||||||
|
</Table.Tbody>
|
||||||
|
</Table>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function FlowWaHookForm() {
|
||||||
|
const [flowUrl, setFlowUrl] = useState("");
|
||||||
|
const [flowToken, setFlowToken] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
const loadCredentials = async () => {
|
||||||
|
const { data, error } = await apiFetch.api.chatflows["url-token"].get();
|
||||||
|
if (error) {
|
||||||
|
showNotification({ title: "Error", message: "Failed to load credentials", color: "red" });
|
||||||
|
} else {
|
||||||
|
setFlowUrl(data?.data?.flowUrl || "");
|
||||||
|
setFlowToken(data?.data?.flowToken || "");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadCredentials();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const saveCredentials = async () => {
|
||||||
|
if (!flowUrl || !flowToken) {
|
||||||
|
showNotification({ title: "Error", message: "URL and token are required", color: "red" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setLoading(true);
|
||||||
|
const { error } = await apiFetch.api.chatflows["url-token"].put({ flowUrl, flowToken });
|
||||||
|
if (error) {
|
||||||
|
showNotification({ title: "Error", message: "Failed to update credentials", color: "red" });
|
||||||
|
} else {
|
||||||
|
showNotification({ title: "Success", message: "Credentials updated", color: "green" });
|
||||||
|
}
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const copyToken = () => {
|
||||||
|
navigator.clipboard.writeText(flowToken);
|
||||||
|
showNotification({ title: "Copied", message: "Token copied to clipboard", color: "green" });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card radius="md" p="lg" withBorder>
|
||||||
|
<Stack gap="lg">
|
||||||
|
<Title order={3}>Flow Credentials</Title>
|
||||||
|
<Stack gap="md">
|
||||||
|
<TextInput label="Flow URL" placeholder="Enter flow URL" value={flowUrl} onChange={(e) => setFlowUrl(e.currentTarget.value)} />
|
||||||
|
<PasswordInput
|
||||||
|
label="Flow Token"
|
||||||
|
placeholder="Enter flow token"
|
||||||
|
value={flowToken}
|
||||||
|
onChange={(e) => setFlowToken(e.currentTarget.value)}
|
||||||
|
rightSection={
|
||||||
|
<ActionIcon onClick={copyToken}>
|
||||||
|
<IconCopy size={16} />
|
||||||
|
</ActionIcon>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
<Group justify="flex-end">
|
||||||
|
<Button leftSection={<IconCheck size={16} />} onClick={saveCredentials} loading={loading}>
|
||||||
|
Save Changes
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
src/pages/sq/dashboard/wa-hook/wa_hook_home.tsx
Normal file
29
src/pages/sq/dashboard/wa-hook/wa_hook_home.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import apiFetch from "@/lib/apiFetch";
|
||||||
|
import { Skeleton, Stack, Text, Title } from "@mantine/core";
|
||||||
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
export default function WaHookHome() {
|
||||||
|
const { data, error, isLoading, mutate } = useSWR("/wa-hook", apiFetch["wa-hook"].list.get,{
|
||||||
|
refreshInterval: 3000,
|
||||||
|
revalidateOnFocus: true,
|
||||||
|
revalidateOnReconnect: true,
|
||||||
|
revalidateIfStale: true,
|
||||||
|
refreshWhenHidden: true,
|
||||||
|
refreshWhenOffline: true,
|
||||||
|
dedupingInterval: 3000,
|
||||||
|
})
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
mutate()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (isLoading) return <Skeleton height={500} />
|
||||||
|
if (error) return <div>Error: {error.message}</div>
|
||||||
|
return (
|
||||||
|
<Stack>
|
||||||
|
<Title order={2}>WaHookHome</Title>
|
||||||
|
<pre>{JSON.stringify(data?.data?.list, null, 2)}</pre>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
src/pages/sq/dashboard/wa-hook/wa_hook_layout.tsx
Normal file
23
src/pages/sq/dashboard/wa-hook/wa_hook_layout.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import clientRoutes from "@/clientRoutes";
|
||||||
|
import { Button, Container, Group, Stack } from "@mantine/core";
|
||||||
|
import { Outlet, useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function WaHookLayout() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<Container size="xl" w={"100%"}>
|
||||||
|
<Group justify="flex-start" p={"md"}>
|
||||||
|
<Button
|
||||||
|
size="compact-xs"
|
||||||
|
radius={"lg"}
|
||||||
|
onClick={() =>
|
||||||
|
navigate(clientRoutes["/sq/dashboard/wa-hook/flow-wa-hook"])
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Flow WA Hook
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<Outlet />
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,15 +1,15 @@
|
|||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Card,
|
Card,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
Group,
|
Group,
|
||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Select,
|
Select,
|
||||||
Divider,
|
Divider,
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { IconCode, IconCheck, IconX } from "@tabler/icons-react";
|
import { IconCode, IconCheck, IconX } from "@tabler/icons-react";
|
||||||
@@ -38,279 +38,273 @@ Available variables:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export default function WebhookCreate() {
|
export default function WebhookCreate() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [description, setDescription] = useState("");
|
const [description, setDescription] = useState("");
|
||||||
const [url, setUrl] = useState("");
|
const [url, setUrl] = useState("");
|
||||||
const [method, setMethod] = useState("POST");
|
const [method, setMethod] = useState("POST");
|
||||||
const [headers, setHeaders] = useState(
|
const [headers, setHeaders] = useState(
|
||||||
JSON.stringify({ "Content-Type": "application/json" }, null, 2),
|
JSON.stringify({ "Content-Type": "application/json" }, null, 2),
|
||||||
);
|
);
|
||||||
const [payload, setPayload] = useState("{}");
|
const [payload, setPayload] = useState("{}");
|
||||||
const [apiToken, setApiToken] = useState("");
|
const [apiToken, setApiToken] = useState("");
|
||||||
const [enabled, setEnabled] = useState(true);
|
const [enabled, setEnabled] = useState(true);
|
||||||
const [replay, setReplay] = useState(false);
|
const [replay, setReplay] = useState(false);
|
||||||
const [replayKey, setReplayKey] = useState("");
|
const [replayKey, setReplayKey] = useState("");
|
||||||
|
|
||||||
const safeJson = (value: string) => {
|
const safeJson = (value: string) => {
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(JSON.parse(value || "{}"), null, 2);
|
return JSON.stringify(JSON.parse(value || "{}"), null, 2);
|
||||||
} catch {
|
} catch {
|
||||||
return value || "{}";
|
return value || "{}";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const previewCode = useMemo(() => {
|
const previewCode = useMemo(() => {
|
||||||
let headerObj: Record<string, string> = {};
|
let headerObj: Record<string, string> = {};
|
||||||
try {
|
try {
|
||||||
headerObj = JSON.parse(headers);
|
headerObj = JSON.parse(headers);
|
||||||
} catch { }
|
} catch {}
|
||||||
if (apiToken) headerObj["Authorization"] = `Bearer ${apiToken}`;
|
if (apiToken) headerObj["Authorization"] = `Bearer ${apiToken}`;
|
||||||
const prettyHeaders = safeJson(JSON.stringify(headerObj));
|
const prettyHeaders = safeJson(JSON.stringify(headerObj));
|
||||||
const prettyPayload = safeJson(payload);
|
const prettyPayload = safeJson(payload);
|
||||||
const includeBody = ["POST", "PUT", "PATCH"].includes(method.toUpperCase());
|
const includeBody = ["POST", "PUT", "PATCH"].includes(method.toUpperCase());
|
||||||
|
|
||||||
return `fetch("${url || "https://example.com/webhook"}", {
|
return `fetch("${url || "https://example.com/webhook"}", {
|
||||||
method: "${method}",
|
method: "${method}",
|
||||||
headers: ${prettyHeaders},${includeBody ? `\n body: ${prettyPayload},` : ""}
|
headers: ${prettyHeaders},${includeBody ? `\n body: ${prettyPayload},` : ""}
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(console.log)
|
.then(console.log)
|
||||||
.catch(console.error);`;
|
.catch(console.error);`;
|
||||||
}, [url, method, headers, payload, apiToken]);
|
}, [url, method, headers, payload, apiToken]);
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
const { data } = await apiFetch.api.webhook.create.post({
|
const { data } = await apiFetch.api.webhook.create.post({
|
||||||
name,
|
name,
|
||||||
description,
|
description,
|
||||||
apiToken,
|
apiToken,
|
||||||
url,
|
url,
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
payload,
|
payload,
|
||||||
enabled,
|
enabled,
|
||||||
replay,
|
replay,
|
||||||
replayKey,
|
replayKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data?.success) {
|
if (data?.success) {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: "Webhook Created",
|
title: "Webhook Created",
|
||||||
message: data.message,
|
message: data.message,
|
||||||
color: "teal",
|
color: "teal",
|
||||||
icon: <IconCheck />,
|
icon: <IconCheck />,
|
||||||
});
|
});
|
||||||
|
|
||||||
navigate(clientRoutes["/sq/dashboard/webhook"]);
|
navigate(clientRoutes["/sq/dashboard/webhook"]);
|
||||||
} else {
|
} else {
|
||||||
notifications.show({
|
notifications.show({
|
||||||
title: "Creation Failed",
|
title: "Creation Failed",
|
||||||
message: data?.message || "Unable to create webhook",
|
message: data?.message || "Unable to create webhook",
|
||||||
color: "red",
|
color: "red",
|
||||||
icon: <IconX />,
|
icon: <IconX />,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack style={{ backgroundColor: "#191919" }} p="xl">
|
<Stack style={{ backgroundColor: "#191919" }} p="xl">
|
||||||
<Stack
|
<Stack
|
||||||
gap="md"
|
gap="md"
|
||||||
maw={900}
|
maw={900}
|
||||||
mx="auto"
|
mx="auto"
|
||||||
bg="rgba(45,45,45,0.6)"
|
bg="rgba(45,45,45,0.6)"
|
||||||
p="xl"
|
p="xl"
|
||||||
style={{
|
style={{
|
||||||
borderRadius: "20px",
|
borderRadius: "20px",
|
||||||
backdropFilter: "blur(12px)",
|
backdropFilter: "blur(12px)",
|
||||||
border: "1px solid rgba(0,255,200,0.2)",
|
border: "1px solid rgba(0,255,200,0.2)",
|
||||||
// boxShadow: "0 0 25px rgba(0,255,200,0.15)",
|
// boxShadow: "0 0 25px rgba(0,255,200,0.15)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
<Title order={2} c="#EAEAEA" fw={600}>
|
<Title order={2} c="#EAEAEA" fw={600}>
|
||||||
Create Webhook
|
Create Webhook
|
||||||
</Title>
|
</Title>
|
||||||
<IconCode color="#00FFFF" size={28} />
|
<IconCode color="#00FFFF" size={28} />
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Divider color="rgba(0,255,200,0.2)" />
|
<Divider color="rgba(0,255,200,0.2)" />
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Name"
|
label="Name"
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Description"
|
label="Description"
|
||||||
placeholder="Description"
|
placeholder="Description"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Webhook URL"
|
label="Webhook URL"
|
||||||
placeholder="https://example.com/webhook"
|
placeholder="https://example.com/webhook"
|
||||||
value={url}
|
value={url}
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
label="HTTP Method"
|
label="HTTP Method"
|
||||||
placeholder="Select method"
|
placeholder="Select method"
|
||||||
value={method}
|
value={method}
|
||||||
onChange={(v) => setMethod(v || "POST")}
|
onChange={(v) => setMethod(v || "POST")}
|
||||||
data={["POST", "GET", "PUT", "PATCH", "DELETE"].map((v) => ({
|
data={["POST", "GET", "PUT", "PATCH", "DELETE"].map((v) => ({
|
||||||
value: v,
|
value: v,
|
||||||
label: v,
|
label: v,
|
||||||
}))}
|
}))}
|
||||||
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="API Token"
|
label="API Token"
|
||||||
placeholder="Bearer ..."
|
placeholder="Bearer ..."
|
||||||
value={apiToken}
|
value={apiToken}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setApiToken(e.target.value);
|
setApiToken(e.target.value);
|
||||||
try {
|
try {
|
||||||
const current = JSON.parse(headers);
|
const current = JSON.parse(headers);
|
||||||
if (!e.target.value) {
|
if (!e.target.value) {
|
||||||
delete current["Authorization"];
|
delete current["Authorization"];
|
||||||
} else {
|
} else {
|
||||||
current["Authorization"] = `Bearer ${e.target.value}`;
|
current["Authorization"] = `Bearer ${e.target.value}`;
|
||||||
}
|
}
|
||||||
setHeaders(JSON.stringify(current, null, 2));
|
setHeaders(JSON.stringify(current, null, 2));
|
||||||
} catch { }
|
} catch {}
|
||||||
}}
|
}}
|
||||||
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Text fw={600} c="#EAEAEA">
|
<Text fw={600} c="#EAEAEA">
|
||||||
Headers (JSON)
|
Headers (JSON)
|
||||||
</Text>
|
</Text>
|
||||||
<Editor
|
<Editor
|
||||||
theme="vs-dark"
|
theme="vs-dark"
|
||||||
height="20vh"
|
height="20vh"
|
||||||
language="json"
|
language="json"
|
||||||
value={headers}
|
value={headers}
|
||||||
onChange={(val) => setHeaders(val ?? "{}")}
|
onChange={(val) => setHeaders(val ?? "{}")}
|
||||||
options={{
|
options={{
|
||||||
minimap: { enabled: false },
|
minimap: { enabled: false },
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
scrollBeyondLastLine: false,
|
scrollBeyondLastLine: false,
|
||||||
lineNumbers: "off",
|
lineNumbers: "off",
|
||||||
automaticLayout: true,
|
automaticLayout: true,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Text fw={600} c="#EAEAEA">
|
|
||||||
Payload
|
|
||||||
</Text>
|
|
||||||
<Text size="xs" c="#9A9A9A" mb="xs">
|
|
||||||
{templateData}
|
|
||||||
</Text>
|
|
||||||
<Editor
|
|
||||||
theme="vs-dark"
|
|
||||||
height="35vh"
|
|
||||||
language="json"
|
|
||||||
value={payload}
|
|
||||||
onChange={(val) => setPayload(val ?? "{}")}
|
|
||||||
options={{
|
|
||||||
minimap: { enabled: false },
|
|
||||||
fontSize: 13,
|
|
||||||
scrollBeyondLastLine: false,
|
|
||||||
automaticLayout: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Checkbox
|
|
||||||
label="Enable Webhook"
|
|
||||||
checked={enabled}
|
|
||||||
onChange={(e) => setEnabled(e.currentTarget.checked)}
|
|
||||||
color="teal"
|
|
||||||
styles={{
|
|
||||||
label: { color: "#EAEAEA" },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Checkbox
|
|
||||||
label="Enable Replay"
|
|
||||||
checked={replay}
|
|
||||||
onChange={(e) => setReplay(e.currentTarget.checked)}
|
|
||||||
color="teal"
|
|
||||||
styles={{
|
|
||||||
label: { color: "#EAEAEA" },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<TextInput
|
|
||||||
description="Replay Key is used to identify the webhook example: data.text"
|
|
||||||
label="Replay Key"
|
|
||||||
placeholder="Replay Key"
|
|
||||||
value={replayKey}
|
|
||||||
onChange={(e) => setReplayKey(e.target.value)}
|
|
||||||
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
radius="xl"
|
|
||||||
p="md"
|
|
||||||
style={{
|
|
||||||
background: "rgba(25,25,25,0.6)",
|
|
||||||
border: "1px solid rgba(0,255,200,0.3)",
|
|
||||||
// boxShadow: "0 0 15px rgba(0,255,200,0.15)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Text fw={600} c="#EAEAEA">
|
|
||||||
Request Preview
|
|
||||||
</Text>
|
|
||||||
<Editor
|
|
||||||
theme="vs-dark"
|
|
||||||
height="35vh"
|
|
||||||
language="javascript"
|
|
||||||
value={previewCode}
|
|
||||||
options={{
|
|
||||||
readOnly: true,
|
|
||||||
minimap: { enabled: false },
|
|
||||||
fontSize: 13,
|
|
||||||
scrollBeyondLastLine: false,
|
|
||||||
automaticLayout: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Group justify="flex-end" mt="md">
|
|
||||||
<Button
|
|
||||||
onClick={() => navigate(clientRoutes["/sq/dashboard/webhook"])}
|
|
||||||
variant="subtle"
|
|
||||||
c="#EAEAEA"
|
|
||||||
styles={{
|
|
||||||
root: { backgroundColor: "#2D2D2D", borderColor: "#00FFC8" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={onSubmit}
|
|
||||||
style={{
|
|
||||||
background: "linear-gradient(90deg, #00FFC8, #00FFFF)",
|
|
||||||
color: "#191919",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Save Webhook
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text fw={600} c="#EAEAEA">
|
||||||
|
Payload
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="#9A9A9A" mb="xs">
|
||||||
|
{templateData}
|
||||||
|
</Text>
|
||||||
|
<Editor
|
||||||
|
theme="vs-dark"
|
||||||
|
height="35vh"
|
||||||
|
language="json"
|
||||||
|
value={payload}
|
||||||
|
onChange={(val) => setPayload(val ?? "{}")}
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: false },
|
||||||
|
fontSize: 13,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
automaticLayout: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label="Enable Webhook"
|
||||||
|
checked={enabled}
|
||||||
|
onChange={(e) => setEnabled(e.currentTarget.checked)}
|
||||||
|
color="teal"
|
||||||
|
styles={{
|
||||||
|
label: { color: "#EAEAEA" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Checkbox
|
||||||
|
label="Enable Replay"
|
||||||
|
checked={replay}
|
||||||
|
onChange={(e) => setReplay(e.currentTarget.checked)}
|
||||||
|
color="teal"
|
||||||
|
styles={{
|
||||||
|
label: { color: "#EAEAEA" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<TextInput
|
||||||
|
description="Replay Key is used to identify the webhook example: data.text"
|
||||||
|
label="Replay Key"
|
||||||
|
placeholder="Replay Key"
|
||||||
|
value={replayKey}
|
||||||
|
onChange={(e) => setReplayKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
radius="xl"
|
||||||
|
p="md"
|
||||||
|
style={{
|
||||||
|
background: "rgba(25,25,25,0.6)",
|
||||||
|
border: "1px solid rgba(0,255,200,0.3)",
|
||||||
|
// boxShadow: "0 0 15px rgba(0,255,200,0.15)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text fw={600} c="#EAEAEA">
|
||||||
|
Request Preview
|
||||||
|
</Text>
|
||||||
|
<Editor
|
||||||
|
theme="vs-dark"
|
||||||
|
height="35vh"
|
||||||
|
language="javascript"
|
||||||
|
value={previewCode}
|
||||||
|
options={{
|
||||||
|
readOnly: true,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
fontSize: 13,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
automaticLayout: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Group justify="flex-end" mt="md">
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate(clientRoutes["/sq/dashboard/webhook"])}
|
||||||
|
variant="subtle"
|
||||||
|
c="#EAEAEA"
|
||||||
|
styles={{
|
||||||
|
root: { backgroundColor: "#2D2D2D", borderColor: "#00FFC8" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={onSubmit}
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(90deg, #00FFC8, #00FFFF)",
|
||||||
|
color: "#191919",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save Webhook
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,18 @@ import { useMemo } from "react";
|
|||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import { IconCode, IconCheck, IconX } from "@tabler/icons-react";
|
import { IconCode, IconCheck, IconX } from "@tabler/icons-react";
|
||||||
import Editor from "@monaco-editor/react";
|
import Editor from "@monaco-editor/react";
|
||||||
import { Stack, Group, Title, Divider, TextInput, Select, Checkbox, Card, Button, Text } from "@mantine/core";
|
import {
|
||||||
|
Stack,
|
||||||
|
Group,
|
||||||
|
Title,
|
||||||
|
Divider,
|
||||||
|
TextInput,
|
||||||
|
Select,
|
||||||
|
Checkbox,
|
||||||
|
Card,
|
||||||
|
Button,
|
||||||
|
Text,
|
||||||
|
} from "@mantine/core";
|
||||||
import { modals } from "@mantine/modals";
|
import { modals } from "@mantine/modals";
|
||||||
import clientRoutes from "@/clientRoutes";
|
import clientRoutes from "@/clientRoutes";
|
||||||
import { useShallowEffect } from "@mantine/hooks";
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
@@ -18,344 +29,359 @@ Available variables:
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
export default function WebhookEdit() {
|
export default function WebhookEdit() {
|
||||||
const [searchParams] = useSearchParams();
|
const [searchParams] = useSearchParams();
|
||||||
const id = searchParams.get("id");
|
const id = searchParams.get("id");
|
||||||
const { data, error, isLoading, mutate } = useSWR("/", () => apiFetch.api.webhook.find({
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
id: id!
|
"/",
|
||||||
}).get(), {dedupingInterval: 3000})
|
() =>
|
||||||
const navigate = useNavigate();
|
apiFetch.api.webhook
|
||||||
|
.find({
|
||||||
|
id: id!,
|
||||||
|
})
|
||||||
|
.get(),
|
||||||
|
{ dedupingInterval: 3000 },
|
||||||
|
);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
mutate();
|
mutate();
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
if (isLoading) return <div>Loading...</div>;
|
if (isLoading) return <div>Loading...</div>;
|
||||||
if (error) return <div>Error: {error}</div>;
|
if (error) return <div>Error: {error}</div>;
|
||||||
if (!data?.data?.webhook) return <div>No data</div>;
|
if (!data?.data?.webhook) return <div>No data</div>;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Stack>
|
||||||
|
<Group justify="space-between">
|
||||||
<Group justify="space-between">
|
<Title order={2}>Edit Webhook</Title>
|
||||||
<Title order={2}>Edit Webhook</Title>
|
<Button
|
||||||
<Button variant="outline" onClick={() => {
|
variant="outline"
|
||||||
modals.openConfirmModal({
|
onClick={() => {
|
||||||
title: "Remove Webhook",
|
modals.openConfirmModal({
|
||||||
children: <Text>Are you sure you want to remove this webhook?</Text>,
|
title: "Remove Webhook",
|
||||||
confirmProps: { color: "red" },
|
children: (
|
||||||
labels: {
|
<Text>Are you sure you want to remove this webhook?</Text>
|
||||||
cancel: "Cancel",
|
),
|
||||||
confirm: "Remove",
|
confirmProps: { color: "red" },
|
||||||
},
|
labels: {
|
||||||
onConfirm: () => {
|
cancel: "Cancel",
|
||||||
apiFetch.api.webhook.remove({
|
confirm: "Remove",
|
||||||
id: id!
|
},
|
||||||
}).delete()
|
onConfirm: () => {
|
||||||
navigate(clientRoutes["/sq/dashboard/webhook"]);
|
apiFetch.api.webhook
|
||||||
},
|
.remove({
|
||||||
onCancel: () => {
|
id: id!,
|
||||||
navigate(clientRoutes["/sq/dashboard/webhook/webhook-edit"] + "?id=" + id);
|
})
|
||||||
},
|
.delete();
|
||||||
})
|
navigate(clientRoutes["/sq/dashboard/webhook"]);
|
||||||
}}>Remove</Button>
|
},
|
||||||
</Group>
|
onCancel: () => {
|
||||||
<EditView webhook={data.data?.webhook || null} />
|
navigate(
|
||||||
</Stack>
|
clientRoutes["/sq/dashboard/webhook/webhook-edit"] +
|
||||||
);
|
"?id=" +
|
||||||
|
id,
|
||||||
|
);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Remove
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
<EditView webhook={data.data?.webhook || null} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditView({ webhook }: { webhook: WebHook | null }) {
|
function EditView({ webhook }: { webhook: WebHook | null }) {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [name, setName] = useState(webhook?.name || "");
|
const [name, setName] = useState(webhook?.name || "");
|
||||||
const [description, setDescription] = useState(webhook?.description || "");
|
const [description, setDescription] = useState(webhook?.description || "");
|
||||||
const [url, setUrl] = useState(webhook?.url || "");
|
const [url, setUrl] = useState(webhook?.url || "");
|
||||||
const [method, setMethod] = useState(webhook?.method || "POST");
|
const [method, setMethod] = useState(webhook?.method || "POST");
|
||||||
const [headers, setHeaders] = useState(webhook?.headers || "{}");
|
const [headers, setHeaders] = useState(webhook?.headers || "{}");
|
||||||
const [payload, setPayload] = useState(webhook?.payload || "{}");
|
const [payload, setPayload] = useState(webhook?.payload || "{}");
|
||||||
const [apiToken, setApiToken] = useState(webhook?.apiToken || "");
|
const [apiToken, setApiToken] = useState(webhook?.apiToken || "");
|
||||||
const [enabled, setEnabled] = useState(webhook?.enabled || true);
|
const [enabled, setEnabled] = useState(webhook?.enabled || true);
|
||||||
const [replay, setReplay] = useState(webhook?.replay || false);
|
const [replay, setReplay] = useState(webhook?.replay || false);
|
||||||
const [replayKey, setReplayKey] = useState(webhook?.replayKey || "");
|
const [replayKey, setReplayKey] = useState(webhook?.replayKey || "");
|
||||||
|
|
||||||
const safeJson = (value: string) => {
|
const safeJson = (value: string) => {
|
||||||
try {
|
try {
|
||||||
return JSON.stringify(JSON.parse(value || "{}"), null, 2);
|
return JSON.stringify(JSON.parse(value || "{}"), null, 2);
|
||||||
} catch {
|
} catch {
|
||||||
return value || "{}";
|
return value || "{}";
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// useShallowEffect(() => {
|
// useShallowEffect(() => {
|
||||||
// let headerObj: Record<string, string> = {};
|
// let headerObj: Record<string, string> = {};
|
||||||
// try {
|
// try {
|
||||||
// headerObj = JSON.parse(headers);
|
// headerObj = JSON.parse(headers);
|
||||||
// } catch { }
|
// } catch { }
|
||||||
// if (apiToken) headerObj["Authorization"] = `Bearer ${apiToken}`;
|
// if (apiToken) headerObj["Authorization"] = `Bearer ${apiToken}`;
|
||||||
// setHeaders(JSON.stringify(headerObj, null, 2));
|
// setHeaders(JSON.stringify(headerObj, null, 2));
|
||||||
// }, [apiToken]);
|
// }, [apiToken]);
|
||||||
|
|
||||||
const previewCode = useMemo(() => {
|
const previewCode = useMemo(() => {
|
||||||
let headerObj: Record<string, string> = {};
|
let headerObj: Record<string, string> = {};
|
||||||
try {
|
try {
|
||||||
headerObj = JSON.parse(headers);
|
headerObj = JSON.parse(headers);
|
||||||
} catch { }
|
} catch {}
|
||||||
if (apiToken) headerObj["Authorization"] = `Bearer ${apiToken}`;
|
if (apiToken) headerObj["Authorization"] = `Bearer ${apiToken}`;
|
||||||
const prettyHeaders = safeJson(JSON.stringify(headerObj));
|
const prettyHeaders = safeJson(JSON.stringify(headerObj));
|
||||||
const prettyPayload = safeJson(payload);
|
const prettyPayload = safeJson(payload);
|
||||||
const includeBody = ["POST", "PUT", "PATCH"].includes(method.toUpperCase());
|
const includeBody = ["POST", "PUT", "PATCH"].includes(method.toUpperCase());
|
||||||
|
|
||||||
return `fetch("${url || "https://example.com/webhook"}", {
|
return `fetch("${url || "https://example.com/webhook"}", {
|
||||||
method: "${method}",
|
method: "${method}",
|
||||||
headers: ${prettyHeaders},${includeBody ? `\n body: ${prettyPayload},` : ""}
|
headers: ${prettyHeaders},${includeBody ? `\n body: ${prettyPayload},` : ""}
|
||||||
})
|
})
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(console.log)
|
.then(console.log)
|
||||||
.catch(console.error);`;
|
.catch(console.error);`;
|
||||||
}, [url, method, headers, payload, apiToken]);
|
}, [url, method, headers, payload, apiToken]);
|
||||||
|
|
||||||
async function onSubmit() {
|
async function onSubmit() {
|
||||||
if (!webhook?.id) {
|
if (!webhook?.id) {
|
||||||
return notifications.show({
|
return notifications.show({
|
||||||
title: "Webhook ID Not Found",
|
title: "Webhook ID Not Found",
|
||||||
message: "Unable to update webhook",
|
message: "Unable to update webhook",
|
||||||
color: "red",
|
color: "red",
|
||||||
icon: <IconX />,
|
icon: <IconX />,
|
||||||
});
|
});
|
||||||
}
|
|
||||||
const { data } = await apiFetch.api.webhook.update({
|
|
||||||
id: webhook?.id,
|
|
||||||
}).put({
|
|
||||||
name,
|
|
||||||
description,
|
|
||||||
apiToken,
|
|
||||||
url,
|
|
||||||
method,
|
|
||||||
headers,
|
|
||||||
payload,
|
|
||||||
enabled,
|
|
||||||
replay,
|
|
||||||
replayKey,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (data?.success) {
|
|
||||||
notifications.show({
|
|
||||||
title: "Webhook Created",
|
|
||||||
message: data.message,
|
|
||||||
color: "teal",
|
|
||||||
icon: <IconCheck />,
|
|
||||||
});
|
|
||||||
navigate(clientRoutes["/sq/dashboard/webhook"]);
|
|
||||||
} else {
|
|
||||||
notifications.show({
|
|
||||||
title: "Creation Failed",
|
|
||||||
message: data?.message || "Unable to create webhook",
|
|
||||||
color: "red",
|
|
||||||
icon: <IconX />,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
const { data } = await apiFetch.api.webhook
|
||||||
|
.update({
|
||||||
|
id: webhook?.id,
|
||||||
|
})
|
||||||
|
.put({
|
||||||
|
name,
|
||||||
|
description,
|
||||||
|
apiToken,
|
||||||
|
url,
|
||||||
|
method,
|
||||||
|
headers,
|
||||||
|
payload,
|
||||||
|
enabled,
|
||||||
|
replay,
|
||||||
|
replayKey,
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
if (data?.success) {
|
||||||
<Stack style={{ backgroundColor: "#191919" }} p="xl">
|
notifications.show({
|
||||||
<Stack
|
title: "Webhook Created",
|
||||||
gap="md"
|
message: data.message,
|
||||||
maw={900}
|
color: "teal",
|
||||||
mx="auto"
|
icon: <IconCheck />,
|
||||||
bg="rgba(45,45,45,0.6)"
|
});
|
||||||
p="xl"
|
navigate(clientRoutes["/sq/dashboard/webhook"]);
|
||||||
style={{
|
} else {
|
||||||
borderRadius: "20px",
|
notifications.show({
|
||||||
backdropFilter: "blur(12px)",
|
title: "Creation Failed",
|
||||||
border: "1px solid rgba(0,255,200,0.2)",
|
message: data?.message || "Unable to create webhook",
|
||||||
// boxShadow: "0 0 25px rgba(0,255,200,0.15)",
|
color: "red",
|
||||||
}}
|
icon: <IconX />,
|
||||||
>
|
});
|
||||||
<Group justify="space-between">
|
}
|
||||||
<Title order={2} c="#EAEAEA" fw={600}>
|
}
|
||||||
Create Webhook
|
|
||||||
</Title>
|
|
||||||
<IconCode color="#00FFFF" size={28} />
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Divider color="rgba(0,255,200,0.2)" />
|
return (
|
||||||
|
<Stack style={{ backgroundColor: "#191919" }} p="xl">
|
||||||
|
<Stack
|
||||||
|
gap="md"
|
||||||
|
maw={900}
|
||||||
|
mx="auto"
|
||||||
|
bg="rgba(45,45,45,0.6)"
|
||||||
|
p="xl"
|
||||||
|
style={{
|
||||||
|
borderRadius: "20px",
|
||||||
|
backdropFilter: "blur(12px)",
|
||||||
|
border: "1px solid rgba(0,255,200,0.2)",
|
||||||
|
// boxShadow: "0 0 25px rgba(0,255,200,0.15)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Title order={2} c="#EAEAEA" fw={600}>
|
||||||
|
Create Webhook
|
||||||
|
</Title>
|
||||||
|
<IconCode color="#00FFFF" size={28} />
|
||||||
|
</Group>
|
||||||
|
|
||||||
<TextInput
|
<Divider color="rgba(0,255,200,0.2)" />
|
||||||
label="Name"
|
|
||||||
placeholder="Name"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => setName(e.target.value)}
|
|
||||||
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Description"
|
label="Name"
|
||||||
placeholder="Description"
|
placeholder="Name"
|
||||||
value={description}
|
value={name}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Webhook URL"
|
label="Description"
|
||||||
placeholder="https://example.com/webhook"
|
placeholder="Description"
|
||||||
value={url}
|
value={description}
|
||||||
onChange={(e) => setUrl(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
/>
|
||||||
/>
|
|
||||||
|
|
||||||
<Select
|
<TextInput
|
||||||
label="HTTP Method"
|
label="Webhook URL"
|
||||||
placeholder="Select method"
|
placeholder="https://example.com/webhook"
|
||||||
value={method}
|
value={url}
|
||||||
onChange={(v) => setMethod(v || "POST")}
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
data={["POST", "GET", "PUT", "PATCH", "DELETE"].map((v) => ({
|
/>
|
||||||
value: v,
|
|
||||||
label: v,
|
|
||||||
}))}
|
|
||||||
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
<Select
|
||||||
label="API Token"
|
label="HTTP Method"
|
||||||
placeholder="Bearer ..."
|
placeholder="Select method"
|
||||||
value={apiToken}
|
value={method}
|
||||||
onChange={(e) => {
|
onChange={(v) => setMethod(v || "POST")}
|
||||||
setApiToken(e.target.value);
|
data={["POST", "GET", "PUT", "PATCH", "DELETE"].map((v) => ({
|
||||||
try {
|
value: v,
|
||||||
const current = JSON.parse(headers);
|
label: v,
|
||||||
if (!e.target.value) {
|
}))}
|
||||||
delete current["Authorization"];
|
/>
|
||||||
} else {
|
|
||||||
current["Authorization"] = `Bearer ${e.target.value}`;
|
|
||||||
}
|
|
||||||
setHeaders(JSON.stringify(current, null, 2));
|
|
||||||
} catch { }
|
|
||||||
}}
|
|
||||||
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Stack gap="xs">
|
<TextInput
|
||||||
<Text fw={600} c="#EAEAEA">
|
label="API Token"
|
||||||
Headers (JSON)
|
placeholder="Bearer ..."
|
||||||
</Text>
|
value={apiToken}
|
||||||
<Editor
|
onChange={(e) => {
|
||||||
theme="vs-dark"
|
setApiToken(e.target.value);
|
||||||
height="20vh"
|
try {
|
||||||
language="json"
|
const current = JSON.parse(headers);
|
||||||
value={headers}
|
if (!e.target.value) {
|
||||||
onChange={(val) => setHeaders(val ?? "{}")}
|
delete current["Authorization"];
|
||||||
options={{
|
} else {
|
||||||
minimap: { enabled: false },
|
current["Authorization"] = `Bearer ${e.target.value}`;
|
||||||
fontSize: 13,
|
}
|
||||||
scrollBeyondLastLine: false,
|
setHeaders(JSON.stringify(current, null, 2));
|
||||||
lineNumbers: "off",
|
} catch {}
|
||||||
automaticLayout: true,
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Text fw={600} c="#EAEAEA">
|
<Text fw={600} c="#EAEAEA">
|
||||||
Payload
|
Headers (JSON)
|
||||||
</Text>
|
</Text>
|
||||||
<Text size="xs" c="#9A9A9A" mb="xs">
|
<Editor
|
||||||
{templateData}
|
theme="vs-dark"
|
||||||
</Text>
|
height="20vh"
|
||||||
<Editor
|
language="json"
|
||||||
theme="vs-dark"
|
value={headers}
|
||||||
height="35vh"
|
onChange={(val) => setHeaders(val ?? "{}")}
|
||||||
language="json"
|
options={{
|
||||||
value={payload}
|
minimap: { enabled: false },
|
||||||
onChange={(val) => setPayload(val ?? "{}")}
|
fontSize: 13,
|
||||||
options={{
|
scrollBeyondLastLine: false,
|
||||||
minimap: { enabled: false },
|
lineNumbers: "off",
|
||||||
fontSize: 13,
|
automaticLayout: true,
|
||||||
scrollBeyondLastLine: false,
|
}}
|
||||||
automaticLayout: true,
|
/>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
|
|
||||||
<Checkbox
|
|
||||||
label="Enable Webhook"
|
|
||||||
checked={enabled}
|
|
||||||
onChange={(e) => setEnabled(e.target.checked as any)}
|
|
||||||
color="teal"
|
|
||||||
styles={{
|
|
||||||
label: { color: "#EAEAEA" },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Checkbox
|
|
||||||
label="Enable Replay"
|
|
||||||
checked={replay}
|
|
||||||
onChange={(e) => setReplay(e.target.checked as any)}
|
|
||||||
color="teal"
|
|
||||||
styles={{
|
|
||||||
label: { color: "#EAEAEA" },
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<TextInput
|
|
||||||
description="Replay Key is used to identify the webhook example: data.text"
|
|
||||||
label="Replay Key"
|
|
||||||
placeholder="Replay Key"
|
|
||||||
value={replayKey}
|
|
||||||
onChange={(e) => setReplayKey(e.target.value)}
|
|
||||||
|
|
||||||
/>
|
|
||||||
|
|
||||||
<Card
|
|
||||||
radius="xl"
|
|
||||||
p="md"
|
|
||||||
style={{
|
|
||||||
background: "rgba(25,25,25,0.6)",
|
|
||||||
border: "1px solid rgba(0,255,200,0.3)",
|
|
||||||
// boxShadow: "0 0 15px rgba(0,255,200,0.15)",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Text fw={600} c="#EAEAEA">
|
|
||||||
Request Preview
|
|
||||||
</Text>
|
|
||||||
<Editor
|
|
||||||
theme="vs-dark"
|
|
||||||
height="35vh"
|
|
||||||
language="javascript"
|
|
||||||
value={previewCode}
|
|
||||||
options={{
|
|
||||||
readOnly: true,
|
|
||||||
minimap: { enabled: false },
|
|
||||||
fontSize: 13,
|
|
||||||
scrollBeyondLastLine: false,
|
|
||||||
automaticLayout: true,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
<Group justify="flex-end" mt="md">
|
|
||||||
<Button
|
|
||||||
onClick={() => navigate(clientRoutes["/sq/dashboard/webhook"])}
|
|
||||||
variant="subtle"
|
|
||||||
c="#EAEAEA"
|
|
||||||
styles={{
|
|
||||||
root: { backgroundColor: "#2D2D2D", borderColor: "#00FFC8" },
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
onClick={onSubmit}
|
|
||||||
style={{
|
|
||||||
background: "linear-gradient(90deg, #00FFC8, #00FFFF)",
|
|
||||||
color: "#191919",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Save Webhook
|
|
||||||
</Button>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text fw={600} c="#EAEAEA">
|
||||||
|
Payload
|
||||||
|
</Text>
|
||||||
|
<Text size="xs" c="#9A9A9A" mb="xs">
|
||||||
|
{templateData}
|
||||||
|
</Text>
|
||||||
|
<Editor
|
||||||
|
theme="vs-dark"
|
||||||
|
height="35vh"
|
||||||
|
language="json"
|
||||||
|
value={payload}
|
||||||
|
onChange={(val) => setPayload(val ?? "{}")}
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: false },
|
||||||
|
fontSize: 13,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
automaticLayout: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label="Enable Webhook"
|
||||||
|
checked={enabled}
|
||||||
|
onChange={(e) => setEnabled(e.target.checked as any)}
|
||||||
|
color="teal"
|
||||||
|
styles={{
|
||||||
|
label: { color: "#EAEAEA" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Checkbox
|
||||||
|
label="Enable Replay"
|
||||||
|
checked={replay}
|
||||||
|
onChange={(e) => setReplay(e.target.checked as any)}
|
||||||
|
color="teal"
|
||||||
|
styles={{
|
||||||
|
label: { color: "#EAEAEA" },
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<TextInput
|
||||||
|
description="Replay Key is used to identify the webhook example: data.text"
|
||||||
|
label="Replay Key"
|
||||||
|
placeholder="Replay Key"
|
||||||
|
value={replayKey}
|
||||||
|
onChange={(e) => setReplayKey(e.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Card
|
||||||
|
radius="xl"
|
||||||
|
p="md"
|
||||||
|
style={{
|
||||||
|
background: "rgba(25,25,25,0.6)",
|
||||||
|
border: "1px solid rgba(0,255,200,0.3)",
|
||||||
|
// boxShadow: "0 0 15px rgba(0,255,200,0.15)",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Text fw={600} c="#EAEAEA">
|
||||||
|
Request Preview
|
||||||
|
</Text>
|
||||||
|
<Editor
|
||||||
|
theme="vs-dark"
|
||||||
|
height="35vh"
|
||||||
|
language="javascript"
|
||||||
|
value={previewCode}
|
||||||
|
options={{
|
||||||
|
readOnly: true,
|
||||||
|
minimap: { enabled: false },
|
||||||
|
fontSize: 13,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
automaticLayout: true,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Group justify="flex-end" mt="md">
|
||||||
|
<Button
|
||||||
|
onClick={() => navigate(clientRoutes["/sq/dashboard/webhook"])}
|
||||||
|
variant="subtle"
|
||||||
|
c="#EAEAEA"
|
||||||
|
styles={{
|
||||||
|
root: { backgroundColor: "#2D2D2D", borderColor: "#00FFC8" },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={onSubmit}
|
||||||
|
style={{
|
||||||
|
background: "linear-gradient(90deg, #00FFC8, #00FFFF)",
|
||||||
|
color: "#191919",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Save Webhook
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Group,
|
Group,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
Badge,
|
Badge,
|
||||||
Loader,
|
Loader,
|
||||||
Center,
|
Center,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
Stack,
|
Stack,
|
||||||
Divider,
|
Divider,
|
||||||
Button,
|
Button,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
IconLink,
|
IconLink,
|
||||||
IconCode,
|
IconCode,
|
||||||
IconKey,
|
IconKey,
|
||||||
IconCheck,
|
IconCheck,
|
||||||
IconX,
|
IconX,
|
||||||
IconRefresh,
|
IconRefresh,
|
||||||
IconEdit,
|
IconEdit,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconMessageReply,
|
IconMessageReply,
|
||||||
} from "@tabler/icons-react";
|
} from "@tabler/icons-react";
|
||||||
import { notifications } from "@mantine/notifications";
|
import { notifications } from "@mantine/notifications";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
@@ -32,218 +32,228 @@ import clientRoutes from "@/clientRoutes";
|
|||||||
import { useShallowEffect } from "@mantine/hooks";
|
import { useShallowEffect } from "@mantine/hooks";
|
||||||
|
|
||||||
export default function WebhookHome() {
|
export default function WebhookHome() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { data, error, isLoading, mutate } = useSWR(
|
const { data, error, isLoading, mutate } = useSWR(
|
||||||
"/",
|
"/",
|
||||||
apiFetch.api.webhook.list.get, { dedupingInterval: 3000, refreshInterval: 3000 });
|
apiFetch.api.webhook.list.get,
|
||||||
|
{ dedupingInterval: 3000, refreshInterval: 3000 },
|
||||||
|
);
|
||||||
|
|
||||||
const webhooks = useMemo(() => data?.data?.list ?? [], [data]);
|
const webhooks = useMemo(() => data?.data?.list ?? [], [data]);
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
mutate();
|
mutate();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
function ButtonCreate() {
|
|
||||||
return <Tooltip label="Create new webhook" withArrow color="teal">
|
|
||||||
<Button
|
|
||||||
radius="xl"
|
|
||||||
size="md"
|
|
||||||
leftSection={<IconPlus size={18} />}
|
|
||||||
variant="gradient"
|
|
||||||
gradient={{ from: "#00FFC8", to: "#00FFFF", deg: 135 }}
|
|
||||||
style={{
|
|
||||||
color: "#191919",
|
|
||||||
fontWeight: 600,
|
|
||||||
// boxShadow: "0 0 12px rgba(0,255,200,0.25)",
|
|
||||||
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
|
||||||
}}
|
|
||||||
onMouseEnter={(e) => {
|
|
||||||
e.currentTarget.style.transform = "translateY(-2px)";
|
|
||||||
e.currentTarget.style.boxShadow =
|
|
||||||
"0 0 20px rgba(0,255,200,0.4)";
|
|
||||||
}}
|
|
||||||
onMouseLeave={(e) => {
|
|
||||||
e.currentTarget.style.transform = "translateY(0)";
|
|
||||||
e.currentTarget.style.boxShadow =
|
|
||||||
"0 0 12px rgba(0,255,200,0.25)";
|
|
||||||
}}
|
|
||||||
onClick={() => navigate("/sq/dashboard/webhook/webhook-create")}
|
|
||||||
>
|
|
||||||
Create Webhook
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLoading)
|
|
||||||
return (
|
|
||||||
<Center h="100vh" bg="#191919">
|
|
||||||
<Loader color="teal" size="lg" />
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (error)
|
|
||||||
return (
|
|
||||||
<Center h="100vh" bg="#191919">
|
|
||||||
<Text c="#FF4B4B" fw={500}>
|
|
||||||
Failed to load webhooks. Please try again.
|
|
||||||
</Text>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!webhooks.length)
|
|
||||||
return (
|
|
||||||
<Center h="100vh" bg="#191919">
|
|
||||||
<Stack align="center" gap="sm">
|
|
||||||
<Text c="#9A9A9A" size="lg">
|
|
||||||
No webhooks found
|
|
||||||
</Text>
|
|
||||||
<Text c="#00FFC8" size="sm">
|
|
||||||
Connect your first webhook to start managing events
|
|
||||||
</Text>
|
|
||||||
<ButtonCreate />
|
|
||||||
</Stack>
|
|
||||||
</Center>
|
|
||||||
);
|
|
||||||
|
|
||||||
|
function ButtonCreate() {
|
||||||
return (
|
return (
|
||||||
<Stack style={{ backgroundColor: "#191919" }} p="xl">
|
<Tooltip label="Create new webhook" withArrow color="teal">
|
||||||
<Group justify="space-between" mb="lg">
|
<Button
|
||||||
<Title order={2} c="#EAEAEA" fw={600}>
|
radius="xl"
|
||||||
Webhook Manager
|
size="md"
|
||||||
</Title>
|
leftSection={<IconPlus size={18} />}
|
||||||
<ButtonCreate />
|
variant="gradient"
|
||||||
<Tooltip label="Refresh webhooks" withArrow color="cyan">
|
gradient={{ from: "#00FFC8", to: "#00FFFF", deg: 135 }}
|
||||||
<ActionIcon
|
style={{
|
||||||
variant="light"
|
color: "#191919",
|
||||||
size="lg"
|
fontWeight: 600,
|
||||||
radius="xl"
|
// boxShadow: "0 0 12px rgba(0,255,200,0.25)",
|
||||||
onClick={() => {
|
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
||||||
mutate();
|
}}
|
||||||
notifications.show({
|
onMouseEnter={(e) => {
|
||||||
title: "Refreshing data",
|
e.currentTarget.style.transform = "translateY(-2px)";
|
||||||
message: "Webhook list is being updated...",
|
e.currentTarget.style.boxShadow = "0 0 20px rgba(0,255,200,0.4)";
|
||||||
color: "teal",
|
}}
|
||||||
});
|
onMouseLeave={(e) => {
|
||||||
}}
|
e.currentTarget.style.transform = "translateY(0)";
|
||||||
>
|
e.currentTarget.style.boxShadow = "0 0 12px rgba(0,255,200,0.25)";
|
||||||
<IconRefresh color="#00FFFF" />
|
}}
|
||||||
</ActionIcon>
|
onClick={() => navigate("/sq/dashboard/webhook/webhook-create")}
|
||||||
</Tooltip>
|
>
|
||||||
|
Create Webhook
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
return (
|
||||||
|
<Center h="100vh" bg="#191919">
|
||||||
|
<Loader color="teal" size="lg" />
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error)
|
||||||
|
return (
|
||||||
|
<Center h="100vh" bg="#191919">
|
||||||
|
<Text c="#FF4B4B" fw={500}>
|
||||||
|
Failed to load webhooks. Please try again.
|
||||||
|
</Text>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!webhooks.length)
|
||||||
|
return (
|
||||||
|
<Center h="100vh" bg="#191919">
|
||||||
|
<Stack align="center" gap="sm">
|
||||||
|
<Text c="#9A9A9A" size="lg">
|
||||||
|
No webhooks found
|
||||||
|
</Text>
|
||||||
|
<Text c="#00FFC8" size="sm">
|
||||||
|
Connect your first webhook to start managing events
|
||||||
|
</Text>
|
||||||
|
<ButtonCreate />
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack style={{ backgroundColor: "#191919" }} p="xl">
|
||||||
|
<Group justify="space-between" mb="lg">
|
||||||
|
<Title order={2} c="#EAEAEA" fw={600}>
|
||||||
|
Webhook Manager
|
||||||
|
</Title>
|
||||||
|
<ButtonCreate />
|
||||||
|
<Tooltip label="Refresh webhooks" withArrow color="cyan">
|
||||||
|
<ActionIcon
|
||||||
|
variant="light"
|
||||||
|
size="lg"
|
||||||
|
radius="xl"
|
||||||
|
onClick={() => {
|
||||||
|
mutate();
|
||||||
|
notifications.show({
|
||||||
|
title: "Refreshing data",
|
||||||
|
message: "Webhook list is being updated...",
|
||||||
|
color: "teal",
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconRefresh color="#00FFFF" />
|
||||||
|
</ActionIcon>
|
||||||
|
</Tooltip>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Stack gap="md">
|
||||||
|
{webhooks.map((webhook) => (
|
||||||
|
<Card
|
||||||
|
key={webhook.id}
|
||||||
|
p="lg"
|
||||||
|
radius="xl"
|
||||||
|
style={{
|
||||||
|
background: "rgba(45,45,45,0.6)",
|
||||||
|
backdropFilter: "blur(12px)",
|
||||||
|
border: "1px solid rgba(0,255,200,0.2)",
|
||||||
|
// boxShadow: "0 0 12px rgba(0,255,200,0.15)",
|
||||||
|
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group justify="end" mb="sm">
|
||||||
|
<Group>
|
||||||
|
<IconLink color="#00FFFF" />
|
||||||
|
<Text c="#EAEAEA" fw={500} size="lg">
|
||||||
|
{webhook.name}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<ActionIcon
|
||||||
|
c={"teal"}
|
||||||
|
variant="light"
|
||||||
|
size="lg"
|
||||||
|
radius="xl"
|
||||||
|
onClick={() =>
|
||||||
|
navigate(
|
||||||
|
`${clientRoutes["/sq/dashboard/webhook/webhook-edit"]}?id=${webhook.id}`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<IconEdit />
|
||||||
|
</ActionIcon>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<Stack gap="md">
|
<Stack gap={"md"}>
|
||||||
{webhooks.map((webhook) => (
|
<Group>
|
||||||
<Card
|
<Badge
|
||||||
key={webhook.id}
|
color={webhook.enabled ? "teal" : "red"}
|
||||||
p="lg"
|
radius="xl"
|
||||||
radius="xl"
|
leftSection={
|
||||||
style={{
|
webhook.enabled ? (
|
||||||
background: "rgba(45,45,45,0.6)",
|
<IconCheck size={14} />
|
||||||
backdropFilter: "blur(12px)",
|
) : (
|
||||||
border: "1px solid rgba(0,255,200,0.2)",
|
<IconX size={14} />
|
||||||
// boxShadow: "0 0 12px rgba(0,255,200,0.15)",
|
)
|
||||||
transition: "transform 0.2s ease, box-shadow 0.2s ease",
|
}
|
||||||
}}
|
>
|
||||||
>
|
{webhook.enabled ? "Active" : "Disabled"}
|
||||||
<Group justify="end" mb="sm">
|
</Badge>
|
||||||
<Group>
|
<Badge
|
||||||
<IconLink color="#00FFFF" />
|
bg={"teal"}
|
||||||
<Text c="#EAEAEA" fw={500} size="lg">
|
leftSection={<IconMessageReply size={16} color="#00FFC8" />}
|
||||||
{webhook.name}
|
>
|
||||||
</Text>
|
{webhook.replay ? "Replay" : "Not Replay"}
|
||||||
</Group>
|
</Badge>
|
||||||
|
</Group>
|
||||||
<ActionIcon
|
<Text c="#9A9A9A" size="sm">
|
||||||
c={"teal"}
|
{webhook.description}
|
||||||
variant="light"
|
</Text>
|
||||||
size="lg"
|
|
||||||
radius="xl"
|
|
||||||
onClick={() => navigate(`${clientRoutes["/sq/dashboard/webhook/webhook-edit"]}?id=${webhook.id}`)}
|
|
||||||
>
|
|
||||||
<IconEdit />
|
|
||||||
</ActionIcon>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Stack gap={"md"}>
|
|
||||||
<Group>
|
|
||||||
<Badge
|
|
||||||
color={webhook.enabled ? "teal" : "red"}
|
|
||||||
radius="xl"
|
|
||||||
leftSection={
|
|
||||||
webhook.enabled ? (
|
|
||||||
<IconCheck size={14} />
|
|
||||||
) : (
|
|
||||||
<IconX size={14} />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{webhook.enabled ? "Active" : "Disabled"}
|
|
||||||
</Badge>
|
|
||||||
<Badge bg={"teal"} leftSection={<IconMessageReply size={16} color="#00FFC8" />}>
|
|
||||||
{webhook.replay ? "Replay" : "Not Replay"}
|
|
||||||
</Badge>
|
|
||||||
</Group>
|
|
||||||
<Text c="#9A9A9A" size="sm">{webhook.description}</Text>
|
|
||||||
</Stack>
|
|
||||||
<Divider color="rgba(0,255,200,0.2)" my="sm" />
|
|
||||||
|
|
||||||
<Stack gap="xs">
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconCode size={16} color="#00FFC8" />
|
|
||||||
<Text c="#9A9A9A" size="sm">
|
|
||||||
Method:
|
|
||||||
</Text>
|
|
||||||
<Text c="#EAEAEA" size="sm" fw={500}>
|
|
||||||
{webhook.method}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconLink size={16} color="#00FFC8" />
|
|
||||||
<Text c="#9A9A9A" size="sm">
|
|
||||||
URL:
|
|
||||||
</Text>
|
|
||||||
<Text c="#EAEAEA" size="sm" fw={500}>
|
|
||||||
{webhook.url}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group gap="xs">
|
|
||||||
<IconKey size={16} color="#00FFC8" />
|
|
||||||
<Text c="#9A9A9A" size="sm">
|
|
||||||
API Token:
|
|
||||||
</Text>
|
|
||||||
<Text c="#EAEAEA" size="sm" fw={500}>
|
|
||||||
{webhook.apiToken?.slice(0, 6) + "..." || "—"}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group gap="xs">
|
|
||||||
<Text c="#9A9A9A" size="sm">
|
|
||||||
Headers:
|
|
||||||
</Text>
|
|
||||||
<Text c="#EAEAEA" size="sm" fw={500}>
|
|
||||||
{Object.keys(webhook.headers || {}).length
|
|
||||||
? webhook.headers
|
|
||||||
: "No headers configured"}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
|
|
||||||
<Group gap="xs">
|
|
||||||
<Text c="#9A9A9A" size="sm">
|
|
||||||
Payload:
|
|
||||||
</Text>
|
|
||||||
<Text c="#EAEAEA" size="sm" fw={500}>
|
|
||||||
{Object.keys(webhook.payload || {}).length
|
|
||||||
? webhook.payload
|
|
||||||
: "Empty payload"}
|
|
||||||
</Text>
|
|
||||||
</Group>
|
|
||||||
</Stack>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
<Divider color="rgba(0,255,200,0.2)" my="sm" />
|
||||||
);
|
|
||||||
|
<Stack gap="xs">
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconCode size={16} color="#00FFC8" />
|
||||||
|
<Text c="#9A9A9A" size="sm">
|
||||||
|
Method:
|
||||||
|
</Text>
|
||||||
|
<Text c="#EAEAEA" size="sm" fw={500}>
|
||||||
|
{webhook.method}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconLink size={16} color="#00FFC8" />
|
||||||
|
<Text c="#9A9A9A" size="sm">
|
||||||
|
URL:
|
||||||
|
</Text>
|
||||||
|
<Text c="#EAEAEA" size="sm" fw={500}>
|
||||||
|
{webhook.url}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group gap="xs">
|
||||||
|
<IconKey size={16} color="#00FFC8" />
|
||||||
|
<Text c="#9A9A9A" size="sm">
|
||||||
|
API Token:
|
||||||
|
</Text>
|
||||||
|
<Text c="#EAEAEA" size="sm" fw={500}>
|
||||||
|
{webhook.apiToken?.slice(0, 6) + "..." || "—"}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group gap="xs">
|
||||||
|
<Text c="#9A9A9A" size="sm">
|
||||||
|
Headers:
|
||||||
|
</Text>
|
||||||
|
<Text c="#EAEAEA" size="sm" fw={500}>
|
||||||
|
{Object.keys(webhook.headers || {}).length
|
||||||
|
? webhook.headers
|
||||||
|
: "No headers configured"}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Group gap="xs">
|
||||||
|
<Text c="#9A9A9A" size="sm">
|
||||||
|
Payload:
|
||||||
|
</Text>
|
||||||
|
<Text c="#EAEAEA" size="sm" fw={500}>
|
||||||
|
{Object.keys(webhook.payload || {}).length
|
||||||
|
? webhook.payload
|
||||||
|
: "Empty payload"}
|
||||||
|
</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Group,
|
Group,
|
||||||
Stack,
|
Stack,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Divider,
|
Divider,
|
||||||
Container,
|
Container,
|
||||||
Paper,
|
Paper,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconPlus } from "@tabler/icons-react";
|
import { IconPlus } from "@tabler/icons-react";
|
||||||
import { useNavigate, Outlet } from "react-router-dom";
|
import { useNavigate, Outlet } from "react-router-dom";
|
||||||
|
|
||||||
export default function WebhookLayout() {
|
export default function WebhookLayout() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return <Outlet />;
|
||||||
<Outlet />
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
247
src/server/routes/flow_route.ts
Normal file
247
src/server/routes/flow_route.ts
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import Elysia, { t } from 'elysia'
|
||||||
|
import { prisma } from '../lib/prisma'
|
||||||
|
import _ from 'lodash'
|
||||||
|
|
||||||
|
const getUrlToken = async () => await prisma.chatFlows.findUnique({ where: { id: "1" }, select: { flowUrl: true, flowToken: true } })
|
||||||
|
|
||||||
|
const FlowRoute = new Elysia({
|
||||||
|
prefix: '/chatflows',
|
||||||
|
detail: { tags: ['chatflows'] },
|
||||||
|
})
|
||||||
|
.get('/sync', async ctx => {
|
||||||
|
const result = await getUrlToken()
|
||||||
|
if (!result) {
|
||||||
|
return { error: 'Flow URL and Token not found' }
|
||||||
|
}
|
||||||
|
const { flowUrl, flowToken } = result
|
||||||
|
const response = await fetch(flowUrl + '/chatflows', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
Authorization: 'Bearer ' + flowToken,
|
||||||
|
Accept: '*/*',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
return { error: 'Failed to fetch flows' }
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = await response.json()
|
||||||
|
|
||||||
|
const chatflows = await prisma.chatFlows.upsert({
|
||||||
|
where: {
|
||||||
|
id: "1",
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
flows: data,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
flows: data,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return { data: chatflows }
|
||||||
|
}, {
|
||||||
|
detail: {
|
||||||
|
summary: "Sync chatflows",
|
||||||
|
description: "Sync chatflows",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get('/find', async ctx => {
|
||||||
|
const result = await prisma.chatFlows.findUnique({
|
||||||
|
where: { id: "1" },
|
||||||
|
})
|
||||||
|
if (!result) {
|
||||||
|
return { flows: [], defaultFlow: null, flowUrl: null, flowToken: null }
|
||||||
|
}
|
||||||
|
const flows = _.orderBy(result?.flows as any[], ['type'], ['asc'])
|
||||||
|
const defaultFlow = result?.defaultFlow
|
||||||
|
const flowUrl = result?.flowUrl
|
||||||
|
const flowToken = result?.flowToken
|
||||||
|
return { flows, defaultFlow, flowUrl, flowToken }
|
||||||
|
}, {
|
||||||
|
detail: {
|
||||||
|
summary: "Find chatflows",
|
||||||
|
description: "Find chatflows",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.get("/default", async ctx => {
|
||||||
|
const result = await prisma.chatFlows.findUnique({
|
||||||
|
where: { id: "1" },
|
||||||
|
})
|
||||||
|
if (!result) {
|
||||||
|
return { defaultFlow: null, defaultData: null }
|
||||||
|
}
|
||||||
|
const defaultFlow = result?.defaultFlow
|
||||||
|
const defaultData = result?.defaultData
|
||||||
|
return { defaultFlow, defaultData }
|
||||||
|
}, {
|
||||||
|
detail: {
|
||||||
|
summary: "Get default chatflows",
|
||||||
|
description: "Get default chatflows",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.put(
|
||||||
|
'/default',
|
||||||
|
async ctx => {
|
||||||
|
const { id, defaultData } = ctx.body
|
||||||
|
|
||||||
|
const result = await prisma.chatFlows.update({
|
||||||
|
where: {
|
||||||
|
id: "1",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
defaultFlow: id,
|
||||||
|
defaultData: defaultData,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return { data: result }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: t.Object({
|
||||||
|
id: t.String(),
|
||||||
|
defaultData: t.Optional(t.Any()),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "Update default chatflows",
|
||||||
|
description: "Update default chatflows",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.post(
|
||||||
|
'/query',
|
||||||
|
async ctx => {
|
||||||
|
const { flowId, question } = ctx.body
|
||||||
|
const result = await chatFlowQuery({ flowId, question })
|
||||||
|
return { data: result }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: t.Object({
|
||||||
|
flowId: t.String(),
|
||||||
|
question: t.String(),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "Query chatflows",
|
||||||
|
description: "Query chatflows",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.put(
|
||||||
|
'/flow-active',
|
||||||
|
async ctx => {
|
||||||
|
const { active } = ctx.body
|
||||||
|
const result = await prisma.chatFlows.upsert({
|
||||||
|
where: {
|
||||||
|
id: "1",
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
active: active,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
active: active,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return { data: result }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: t.Object({
|
||||||
|
active: t.Boolean(),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "Update flow active",
|
||||||
|
description: "Update flow active",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.get('/url-token', async ctx => {
|
||||||
|
const result = await prisma.chatFlows.findUnique({
|
||||||
|
where: { id: "1" },
|
||||||
|
select: {
|
||||||
|
flowUrl: true,
|
||||||
|
flowToken: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if (!result) {
|
||||||
|
return { data: { flowUrl: null, flowToken: null } }
|
||||||
|
}
|
||||||
|
return { data: { flowUrl: result.flowUrl, flowToken: result.flowToken } }
|
||||||
|
}, {
|
||||||
|
detail: {
|
||||||
|
summary: "Get flow url and token",
|
||||||
|
description: "Get flow url and token",
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.put(
|
||||||
|
'/url-token',
|
||||||
|
async ctx => {
|
||||||
|
const { flowUrl, flowToken } = ctx.body
|
||||||
|
const result = await prisma.chatFlows.upsert({
|
||||||
|
where: {
|
||||||
|
id: "1",
|
||||||
|
},
|
||||||
|
update: {
|
||||||
|
flowUrl: flowUrl,
|
||||||
|
flowToken: flowToken,
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
id: "1",
|
||||||
|
flowUrl: flowUrl,
|
||||||
|
flowToken: flowToken,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return { data: result }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
body: t.Object({
|
||||||
|
flowUrl: t.String(),
|
||||||
|
flowToken: t.String(),
|
||||||
|
}),
|
||||||
|
detail: {
|
||||||
|
summary: "Update flow url and token",
|
||||||
|
description: "Update flow url and token",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.on('error', ctx => {
|
||||||
|
console.log(ctx.error)
|
||||||
|
return { error: ctx.error }
|
||||||
|
})
|
||||||
|
|
||||||
|
export default FlowRoute
|
||||||
|
|
||||||
|
async function chatFlowQuery({
|
||||||
|
flowId,
|
||||||
|
question,
|
||||||
|
}: {
|
||||||
|
flowId: string
|
||||||
|
question: string
|
||||||
|
}) {
|
||||||
|
try {
|
||||||
|
const resultUrlToken = await prisma.chatFlows.findUnique({ where: { id: "1" }, select: { flowUrl: true, flowToken: true } })
|
||||||
|
if (!resultUrlToken) {
|
||||||
|
return { error: 'Flow URL and Token not found' }
|
||||||
|
}
|
||||||
|
const { flowUrl, flowToken } = resultUrlToken
|
||||||
|
if (!flowUrl || !flowToken) {
|
||||||
|
return { error: 'Flow URL and Token not found' }
|
||||||
|
}
|
||||||
|
const response = await fetch(`${flowUrl}/prediction/${flowId}`, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${flowToken}`,
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
},
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({
|
||||||
|
question,
|
||||||
|
overrideConfig: {
|
||||||
|
sessionId: "1",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
const result = await response.text()
|
||||||
|
return JSON.parse(result).text
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error)
|
||||||
|
return 'Failed to fetch response'
|
||||||
|
}
|
||||||
|
}
|
||||||
13
x.sh
13
x.sh
@@ -1,8 +1,7 @@
|
|||||||
|
TOKEN="EAALP22EWyC4BPnZCfcPQNmD5pGLKV6Ao3GIeWZCc81aPivDFc2FXGA1ZBgrRGcB60LaZCdAr1sbnfP1ufrH3dGthxQzpf18BTjDZBkgG3vBiYZAMpHa7MEZBiRIUZCBe4BDXe8KV0r7DsDmQHJqhA3yZBDKPOL1PKJPEqIq40tLxPwMqWYg4o7xf0sBmZCzx2wI1KtJL8I20MV1ggldngHZCIcnOKDL0uPzDAhc2LAQuI7ZBsgZDZD"
|
||||||
|
MEDIA_ID="24893686766920074"
|
||||||
|
BUSINESS_PHONE_NUMBER_ID="783866307805501"
|
||||||
|
|
||||||
#!/bin/bash
|
curl 'https://graph.facebook.com/v19.0/$MEDIA_ID?phone_number_id=$BUSINESS_PHONE_NUMBER_ID' \
|
||||||
TOKEN="EAALP22EWyC4BPv7XnK1xSaZCWccblEoJFbHzPZAf5mlp4678lSM7cqhQl1ExATf8abrOpinvvFF6U6ruK2FsJqIk8wg6DiUz2fc0NYfcwjon3ng7I3C5HSDQHecgTiJLUBxfZAcvE4IIlhks722jakXaJpojlByo8QJ0CEURtzwEU1guFq7YTX3Et0ZCkbhkdftZCOGmpUKFjL5w5nUdd26Nd58YrLVZCoT8NKhxpWFQZDZD"
|
-H 'Authorization: Bearer $TOKEN' \
|
||||||
curl -i -X POST \
|
-H 'Content-Type: application/json'
|
||||||
https://graph.facebook.com/v22.0/838757782652201/messages \
|
|
||||||
-H 'Authorization: Bearer $TOKEN' \
|
|
||||||
-H 'Content-Type: application/json' \
|
|
||||||
-d '{ "messaging_product": "whatsapp", "to": "6289505046093", "type": "template", "template": { "name": "hello_world", "language": { "code": "en_US" } } }'
|
|
||||||
24
xx.ts
Normal file
24
xx.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import fetch from "node-fetch";
|
||||||
|
import fs from "fs";
|
||||||
|
|
||||||
|
const token = "EAALP22EWyC4BPrjshjjYBbPVKWp4Gp2ljkb7hCmgpZArLigB8XNmRoXBomDJm6aWnjpKpqehdVatbfFAHeGaQftGkNBp4Oyds9apr4lOQjG2YWYEzZC05ZAo7MARnfXn7FVua0iaeNMh2gunMZBd6pO58wjAUP3gqLiUrwASeOnJu5pW3tKg6fHubALBlQZDZD"; // dari Meta Developer > App > Access Token
|
||||||
|
const mediaId = "838467435201133"; // dari webhook
|
||||||
|
|
||||||
|
// 1. Dapatkan URL file asli
|
||||||
|
const mediaInfo = await fetch(
|
||||||
|
`https://graph.facebook.com/v19.0/${mediaId}?access_token=${token}`
|
||||||
|
).then(res => res.json()) as any;
|
||||||
|
|
||||||
|
// mediaInfo.url berisi link unduhan sementara
|
||||||
|
const fileUrl = mediaInfo.url;
|
||||||
|
|
||||||
|
const fileResponse = await fetch(fileUrl, {
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`, // wajib!
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const buffer = await fileResponse.arrayBuffer();
|
||||||
|
fs.writeFileSync("sticker.webp", Buffer.from(buffer));
|
||||||
|
|
||||||
|
console.log("Sticker berhasil diunduh!");
|
||||||
Reference in New Issue
Block a user