From f65a6334032e0c451d9c266f93798abfc9ac65b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carl-Gerhard=20Lindesva=CC=88rd?= Date: Wed, 2 Oct 2024 22:12:05 +0200 Subject: [PATCH] feature(dashboard): add integrations and notifications --- apps/api/Dockerfile | 19 +- apps/api/package.json | 1 + apps/api/src/controllers/live.controller.ts | 77 +- .../api/src/controllers/webhook.controller.ts | 105 ++- apps/api/src/routes/live.router.ts | 10 + apps/api/src/routes/webhook.router.ts | 5 + apps/api/tsup.config.ts | 2 + apps/dashboard/Dockerfile | 1 + apps/dashboard/package.json | 1 + apps/dashboard/public/favicon.ico | Bin 23600 -> 15406 bytes apps/dashboard/public/logo-2.svg | 15 - apps/dashboard/public/logo-with-text.svg | 14 - apps/dashboard/public/logo.svg | 9 +- .../realtime/realtime-live-histogram.tsx | 4 +- .../settings/integrations/page.tsx | 37 + .../settings/notifications/page.tsx | 40 ++ .../onboarding/onboarding-tracking.tsx | 2 +- apps/dashboard/src/app/apple-touch-icon.png | Bin 4357 -> 0 bytes apps/dashboard/src/app/favicon.ico | Bin 15406 -> 15406 bytes apps/dashboard/src/app/providers.tsx | 2 + .../src/components/full-page-empty-state.tsx | 2 +- .../integrations/active-integrations.tsx | 118 ++++ .../integrations/all-integrations.tsx | 36 + .../forms/discord-integration.tsx | 91 +++ .../integrations/forms/slack-integration.tsx | 85 +++ .../forms/webhook-integration.tsx | 73 ++ .../integrations/integration-card.tsx | 144 ++++ .../components/integrations/integrations.tsx | 49 ++ apps/dashboard/src/components/links.tsx | 8 +- .../notifications/notification-provider.tsx | 17 + .../notifications/notification-rules.tsx | 74 ++ .../notifications/notifications.tsx | 14 + .../components/notifications/rule-card.tsx | 131 ++++ .../notifications/table/columns.tsx | 140 ++++ .../components/notifications/table/index.tsx | 64 ++ .../overview/overview-live-histogram.tsx | 4 +- apps/dashboard/src/components/ping.tsx | 28 + .../src/components/settings-toggle.tsx | 8 + apps/dashboard/src/components/skeleton.tsx | 5 + apps/dashboard/src/components/ui/badge.tsx | 3 +- apps/dashboard/src/components/ui/tooltip.tsx | 3 + apps/dashboard/src/hooks/useAppParams.ts | 2 + apps/dashboard/src/modals/AddClient.tsx | 4 +- apps/dashboard/src/modals/EditClient.tsx | 2 +- apps/dashboard/src/modals/add-integration.tsx | 101 +++ .../src/modals/add-notification-rule.tsx | 308 ++++++++ apps/dashboard/src/modals/index.tsx | 2 + apps/dashboard/src/styles/globals.css | 25 +- apps/public/public/clickable-demo.png | Bin 18424 -> 0 bytes apps/public/public/clickhouse.png | Bin 1335 -> 0 bytes apps/public/public/getdreams.png | Bin 7194 -> 0 bytes apps/public/public/kiddokitchen.png | Bin 9999 -> 0 bytes apps/public/public/logo-white.png | Bin 15797 -> 0 bytes apps/public/public/logo.jpg | Bin 0 -> 89092 bytes apps/public/public/logo.svg | 9 +- apps/public/src/app/favicon.ico | Bin 15406 -> 15406 bytes apps/worker/Dockerfile | 3 +- apps/worker/package.json | 1 + apps/worker/src/index.ts | 21 +- .../src/jobs/events.create-session-end.ts | 3 + apps/worker/src/jobs/events.incoming-event.ts | 14 +- apps/worker/src/jobs/notification.ts | 71 ++ apps/worker/tsup.config.ts | 1 + packages/common/src/object.ts | 11 +- packages/common/src/string.ts | 4 + packages/db/index.ts | 2 + packages/db/package.json | 2 + .../migration.sql | 71 ++ .../migration.sql | 14 + .../20240926175415_renaming/migration.sql | 14 + .../migration.sql | 55 ++ .../migration.sql | 8 + .../migration.sql | 2 + packages/db/prisma/schema.prisma | 64 ++ packages/db/src/services/chart.service.ts | 10 +- packages/db/src/services/event.service.ts | 2 +- .../db/src/services/notification.service.ts | 314 +++++++++ packages/db/src/services/project.service.ts | 3 + packages/db/src/types.ts | 13 + packages/db/tsconfig.json | 2 +- packages/integrations/index.ts | 2 + packages/integrations/package.json | 18 + packages/integrations/src/discord.ts | 34 + packages/integrations/src/slack.ts | 50 ++ packages/integrations/tsconfig.json | 12 + packages/queue/index.ts | 7 +- packages/queue/src/queues.ts | 19 +- packages/trpc/package.json | 3 +- packages/trpc/src/root.ts | 4 + packages/trpc/src/routers/integration.ts | 137 ++++ packages/trpc/src/routers/notification.ts | 150 ++++ packages/trpc/src/routers/project.ts | 12 +- packages/validation/src/index.ts | 116 ++++ pnpm-lock.yaml | 656 +++++++++++++++++- 94 files changed, 3692 insertions(+), 127 deletions(-) delete mode 100644 apps/dashboard/public/logo-2.svg delete mode 100644 apps/dashboard/public/logo-with-text.svg create mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/integrations/page.tsx create mode 100644 apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/notifications/page.tsx delete mode 100644 apps/dashboard/src/app/apple-touch-icon.png create mode 100644 apps/dashboard/src/components/integrations/active-integrations.tsx create mode 100644 apps/dashboard/src/components/integrations/all-integrations.tsx create mode 100644 apps/dashboard/src/components/integrations/forms/discord-integration.tsx create mode 100644 apps/dashboard/src/components/integrations/forms/slack-integration.tsx create mode 100644 apps/dashboard/src/components/integrations/forms/webhook-integration.tsx create mode 100644 apps/dashboard/src/components/integrations/integration-card.tsx create mode 100644 apps/dashboard/src/components/integrations/integrations.tsx create mode 100644 apps/dashboard/src/components/notifications/notification-provider.tsx create mode 100644 apps/dashboard/src/components/notifications/notification-rules.tsx create mode 100644 apps/dashboard/src/components/notifications/notifications.tsx create mode 100644 apps/dashboard/src/components/notifications/rule-card.tsx create mode 100644 apps/dashboard/src/components/notifications/table/columns.tsx create mode 100644 apps/dashboard/src/components/notifications/table/index.tsx create mode 100644 apps/dashboard/src/components/ping.tsx create mode 100644 apps/dashboard/src/components/skeleton.tsx create mode 100644 apps/dashboard/src/modals/add-integration.tsx create mode 100644 apps/dashboard/src/modals/add-notification-rule.tsx delete mode 100644 apps/public/public/clickable-demo.png delete mode 100644 apps/public/public/clickhouse.png delete mode 100644 apps/public/public/getdreams.png delete mode 100644 apps/public/public/kiddokitchen.png delete mode 100644 apps/public/public/logo-white.png create mode 100644 apps/public/public/logo.jpg create mode 100644 apps/worker/src/jobs/notification.ts create mode 100644 packages/db/prisma/migrations/20240925202841_notifications/migration.sql create mode 100644 packages/db/prisma/migrations/20240925204316_notification_send_to_app_and_email/migration.sql create mode 100644 packages/db/prisma/migrations/20240926175415_renaming/migration.sql create mode 100644 packages/db/prisma/migrations/20240927190558_rename_notification_controls/migration.sql create mode 100644 packages/db/prisma/migrations/20240927195752_add_name_to_rules/migration.sql create mode 100644 packages/db/prisma/migrations/20241001215748_add_payload_to_notifications/migration.sql create mode 100644 packages/db/src/services/notification.service.ts create mode 100644 packages/db/src/types.ts create mode 100644 packages/integrations/index.ts create mode 100644 packages/integrations/package.json create mode 100644 packages/integrations/src/discord.ts create mode 100644 packages/integrations/src/slack.ts create mode 100644 packages/integrations/tsconfig.json create mode 100644 packages/trpc/src/routers/integration.ts create mode 100644 packages/trpc/src/routers/notification.ts diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 57c31995..71cff5c8 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -3,14 +3,14 @@ ARG NODE_VERSION=20.15.1 FROM node:${NODE_VERSION}-slim AS base RUN corepack enable && apt-get update && \ -apt-get install -y --no-install-recommends \ -ca-certificates \ -openssl \ -libssl3 \ -curl \ -netcat-openbsd \ -&& apt-get clean && \ -rm -rf /var/lib/apt/lists/* + apt-get install -y --no-install-recommends \ + ca-certificates \ + openssl \ + libssl3 \ + curl \ + netcat-openbsd \ + && apt-get clean && \ + rm -rf /var/lib/apt/lists/* RUN curl -fsSL \ https://raw.githubusercontent.com/pressly/goose/master/install.sh |\ @@ -36,6 +36,7 @@ COPY packages/common/package.json packages/common/ COPY packages/sdks/sdk/package.json packages/sdks/sdk/ COPY packages/constants/package.json packages/constants/ COPY packages/validation/package.json packages/validation/ +COPY packages/integrations/package.json packages/integrations/ COPY packages/sdks/sdk/package.json packages/sdks/sdk/ # BUILD @@ -93,7 +94,7 @@ COPY --from=build /app/packages/common ./packages/common COPY --from=build /app/packages/sdks/sdk ./packages/sdks/sdk COPY --from=build /app/packages/constants ./packages/constants COPY --from=build /app/packages/validation ./packages/validation - +COPY --from=build /app/packages/integrations ./packages/integrations RUN pnpm db:codegen WORKDIR /app/apps/api diff --git a/apps/api/package.json b/apps/api/package.json index 00796534..209e74cc 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -17,6 +17,7 @@ "@openpanel/common": "workspace:*", "@openpanel/db": "workspace:*", "@openpanel/logger": "workspace:*", + "@openpanel/integrations": "workspace:^", "@openpanel/queue": "workspace:*", "@openpanel/redis": "workspace:*", "@openpanel/trpc": "workspace:*", diff --git a/apps/api/src/controllers/live.controller.ts b/apps/api/src/controllers/live.controller.ts index 9bc1645b..245da185 100644 --- a/apps/api/src/controllers/live.controller.ts +++ b/apps/api/src/controllers/live.controller.ts @@ -4,12 +4,11 @@ import superjson from 'superjson'; import type * as WebSocket from 'ws'; import { getSuperJson } from '@openpanel/common'; -import type { IServiceEvent } from '@openpanel/db'; +import type { IServiceEvent, Notification } from '@openpanel/db'; import { TABLE_NAMES, getEvents, getLiveVisitors, - getProfileById, getProfileByIdCached, transformMinimalEvent, } from '@openpanel/db'; @@ -169,3 +168,77 @@ export async function wsProjectEvents( getRedisSub().off('message', message as any); }); } + +export async function wsProjectNotifications( + connection: { + socket: WebSocket; + }, + req: FastifyRequest<{ + Params: { + projectId: string; + }; + Querystring: { + token?: string; + }; + }>, +) { + const { params, query } = req; + + if (!query.token) { + connection.socket.send('No token provided'); + connection.socket.close(); + return; + } + + const subscribeToEvent = 'notification'; + const decoded = validateClerkJwt(query.token); + const userId = decoded?.sub; + const access = await getProjectAccess({ + userId: userId!, + projectId: params.projectId, + }); + + if (!access) { + connection.socket.send('No access'); + connection.socket.close(); + return; + } + + getRedisSub().subscribe(subscribeToEvent); + + const message = async (channel: string, message: string) => { + const notification = getSuperJson(message); + if (notification?.projectId === params.projectId) { + connection.socket.send(superjson.stringify(notification)); + } + }; + + getRedisSub().on('message', message as any); + + connection.socket.on('close', () => { + getRedisSub().unsubscribe(subscribeToEvent); + getRedisSub().off('message', message as any); + }); +} + +export async function wsIntegrationsSlack( + connection: { + socket: WebSocket; + }, + req: FastifyRequest<{ + Querystring: { + organizationId?: string; + }; + }>, +) { + const subscribeToEvent = 'integrations:slack'; + getRedisSub().subscribe(subscribeToEvent); + const onMessage = (channel: string, message: string) => { + connection.socket.send(JSON.stringify('ok')); + }; + getRedisSub().on('message', onMessage); + connection.socket.on('close', () => { + getRedisSub().unsubscribe(subscribeToEvent); + getRedisSub().off('message', onMessage); + }); +} diff --git a/apps/api/src/controllers/webhook.controller.ts b/apps/api/src/controllers/webhook.controller.ts index 2eea1338..a12e4ef3 100644 --- a/apps/api/src/controllers/webhook.controller.ts +++ b/apps/api/src/controllers/webhook.controller.ts @@ -1,9 +1,15 @@ import type { WebhookEvent } from '@clerk/fastify'; +import { AccessLevel, db } from '@openpanel/db'; +import { + sendSlackNotification, + slackInstaller, +} from '@openpanel/integrations/src/slack'; +import { getRedisPub } from '@openpanel/redis'; +import { zSlackAuthResponse } from '@openpanel/validation'; import type { FastifyReply, FastifyRequest } from 'fastify'; import { pathOr } from 'ramda'; import { Webhook } from 'svix'; - -import { AccessLevel, db } from '@openpanel/db'; +import { z } from 'zod'; if (!process.env.CLERK_SIGNING_SECRET) { throw new Error('CLERK_SIGNING_SECRET is required'); @@ -152,3 +158,98 @@ export async function clerkWebhook( reply.send({ success: true }); } + +const paramsSchema = z.object({ + code: z.string(), + state: z.string(), +}); + +const metadataSchema = z.object({ + organizationId: z.string(), + integrationId: z.string(), +}); + +export async function slackWebhook( + request: FastifyRequest<{ + Querystring: WebhookEvent; + }>, + reply: FastifyReply, +) { + const parsedParams = paramsSchema.safeParse(request.query); + + if (!parsedParams.success) { + request.log.error('Invalid params', parsedParams); + return reply.status(400).send({ error: 'Invalid params' }); + } + + const veryfiedState = await slackInstaller.stateStore?.verifyStateParam( + new Date(), + parsedParams.data.state, + ); + const parsedMetadata = metadataSchema.safeParse( + JSON.parse(veryfiedState?.metadata ?? '{}'), + ); + + if (!parsedMetadata.success) { + request.log.error('Invalid metadata', parsedMetadata.error.errors); + return reply.status(400).send({ error: 'Invalid metadata' }); + } + + const slackOauthAccessUrl = [ + 'https://slack.com/api/oauth.v2.access', + `?client_id=${process.env.SLACK_CLIENT_ID}`, + `&client_secret=${process.env.SLACK_CLIENT_SECRET}`, + `&code=${parsedParams.data.code}`, + `&redirect_uri=${process.env.SLACK_OAUTH_REDIRECT_URL}`, + ].join(''); + + try { + const response = await fetch(slackOauthAccessUrl); + const json = await response.json(); + const parsedJson = zSlackAuthResponse.safeParse(json); + + if (!parsedJson.success) { + request.log.error( + { + zod: parsedJson, + json, + }, + 'Failed to parse slack auth response', + ); + return reply + .status(400) + .header('Content-Type', 'text/html') + .send('

Failed to exchange code for token

'); + } + + // Send a notification first to confirm the connection + await sendSlackNotification({ + webhookUrl: parsedJson.data.incoming_webhook.url, + message: + '๐Ÿ‘‹ Hello. You have successfully connected OpenPanel.dev to your Slack workspace.', + }); + + await db.integration.update({ + where: { + id: parsedMetadata.data.integrationId, + organizationId: parsedMetadata.data.organizationId, + }, + data: { + config: { + type: 'slack', + ...parsedJson.data, + }, + }, + }); + + getRedisPub().publish('integrations:slack', 'ok'); + + reply.send({ success: true }); + } catch (err) { + request.log.error(err); + return reply + .status(500) + .header('Content-Type', 'text/html') + .send('

Failed to exchange code for token

'); + } +} diff --git a/apps/api/src/routes/live.router.ts b/apps/api/src/routes/live.router.ts index ba71b301..4cd2766c 100644 --- a/apps/api/src/routes/live.router.ts +++ b/apps/api/src/routes/live.router.ts @@ -27,6 +27,16 @@ const liveRouter: FastifyPluginCallback = (fastify, opts, done) => { { websocket: true }, controller.wsProjectEvents, ); + fastify.get( + '/notifications/:projectId', + { websocket: true }, + controller.wsProjectNotifications, + ); + fastify.get( + '/integrations/slack', + { websocket: true }, + controller.wsIntegrationsSlack, + ); done(); }); diff --git a/apps/api/src/routes/webhook.router.ts b/apps/api/src/routes/webhook.router.ts index 2f9901d5..becb9fde 100644 --- a/apps/api/src/routes/webhook.router.ts +++ b/apps/api/src/routes/webhook.router.ts @@ -7,6 +7,11 @@ const webhookRouter: FastifyPluginCallback = (fastify, opts, done) => { url: '/clerk', handler: controller.clerkWebhook, }); + fastify.route({ + method: 'GET', + url: '/slack', + handler: controller.slackWebhook, + }); done(); }; diff --git a/apps/api/tsup.config.ts b/apps/api/tsup.config.ts index dd028707..ce3e972b 100644 --- a/apps/api/tsup.config.ts +++ b/apps/api/tsup.config.ts @@ -6,12 +6,14 @@ const options: Options = { entry: ['src/index.ts'], noExternal: [/^@openpanel\/.*$/u, /^@\/.*$/u], external: ['@hyperdx/node-opentelemetry', 'winston'], + ignoreWatch: ['../../**/{.git,node_modules}/**'], sourcemap: true, splitting: false, }; if (process.env.WATCH) { options.watch = ['src/**/*', '../../packages/**/*']; + options.onSuccess = 'node dist/index.js'; options.minify = false; } diff --git a/apps/dashboard/Dockerfile b/apps/dashboard/Dockerfile index db079e54..45f93443 100644 --- a/apps/dashboard/Dockerfile +++ b/apps/dashboard/Dockerfile @@ -36,6 +36,7 @@ COPY packages/queue/package.json packages/queue/package.json COPY packages/common/package.json packages/common/package.json COPY packages/constants/package.json packages/constants/package.json COPY packages/validation/package.json packages/validation/package.json +COPY packages/integrations/package.json packages/integrations/package.json COPY packages/sdks/sdk/package.json packages/sdks/sdk/package.json # BUILD diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json index 3c37d3b0..708d8518 100644 --- a/apps/dashboard/package.json +++ b/apps/dashboard/package.json @@ -19,6 +19,7 @@ "@openpanel/constants": "workspace:^", "@openpanel/db": "workspace:^", "@openpanel/nextjs": "1.0.3", + "@openpanel/integrations": "workspace:^", "@openpanel/queue": "workspace:^", "@openpanel/sdk-info": "workspace:^", "@openpanel/validation": "workspace:^", diff --git a/apps/dashboard/public/favicon.ico b/apps/dashboard/public/favicon.ico index b5336a48e6e7341e6e866c5f477d1a006dc6bf9e..403a07ef9b3f1dd8a2031c3d51aaa4e55c77cfc0 100644 GIT binary patch literal 15406 zcmeHNX>b%(5FU^J>JR^?EEA(Bf(oUgLV2L2Qsq%uVs1Yy_Bf>4Y!$buZL<(OF}N6D?z z93_Pj{6U64avz98?*`Y5=U9<^_Hasm<^IsG|?+`;jCswzNLqWX9F!buQE-UrU<16Y}S`XKUj z167WoofrqbKk z+D2<#_>X!JN8Fo8lY#!E_xgf-=CHoz86RTq7I4pbjjP}D>2MQAFR_oqe{Rt}VXlHS?qP-@Tk=K2K8OSbw|%L%k=knRXSN^3@~p`p*~`#Al)q&a zFMsh6?z?A6d0OpvPI?@q36FAhX?{?^!j+zUX==hW%trlUxIyN-eE!I+#=`lpsZ_CX^# zdv@U2gO|y#%c1S7IiBd>d%(Y@5<1Fo z0j|myK^%R5`gaF-$3u5hFAA zs0AnbkW*s8`>ycbdQkmtP$MCB4-?o}1!DP?Z3Fp~0~?kwnfir|#RK{B$(Kw%e5-c& zYG);Ik^h_e(271AWn?R1^vS222u}d-Ma{l;!V)>W!?GH>U&;nna~&ea7zBS<2B656 za+4L(J)HmR*$J4U$M;yWP|So`b~fHA4uNbuA;lE5`(_ps5%W0k9#}~vab5)H`%iPY za*aO_kICS`Ay&HkcU8Us(%3#2tMM{~ey_tAi?%onqGx@8z8?pUdrg3B3;l+*lbPV(6!Aidj1r_U|^zGgV=Jt_Y6$?!kaA^f!FP+XK}L5a0r zIo_v69U7md@%h6~egy?%v9v^SZ05Plh}G&jZ}LJ~wD8B{2=z(~59If17q?+tP%NW4sUaWR`)aWogfFmeK8r)- zXi;B*4ON_vHP`rQe^2q9=0tlmx#2k{&Am23RuGY>azM0;%W zb0+|`DnMUy=(n{A@tV=&yRE3fxPOyU+D|V|A4c6Id!|@iT8Gk3h&{4@8_@VS*BLpm z&9WL_%@m!M_Ec8-Qll6j(2=tmj$?|kpuIr532mOnj5Mxo%`&#ZyL?PS6|{fl=62lg zMUL)FpbPKbq_@#nXS&aHrt*LL30qev27u0gw8vBtC7_k^N0{Rv|W$qNf&Q2I_h7F3GmfS20AzSKA*_W@ibPF z@o?oBH$c96+J{i=vT=WHq@%ggJ?C|B&wiDggN-!yvwe4K+kkBYwhdhMHqe!Qkc6FL z9@1MsI~!d*7NRT14^Rw*17jh&QXAq}S^S0;gJOt<=*;7R*}E}*hVa|T)<8!!@E@4* B`s)Ay literal 23600 zcmeI4YmXFF5QckMt_#9~A|eR0un<@-0Td&mM&yGDL5La*i5i4>L8E>%(V+2{5q^?? z!ER>HQ`Oydy3aY?Jv$3Mf%G$Vs@|$o)6>(}&9a4TF zKg<4m_B6L%y0q?b`nly`mYvD{@~}K=I+8Co#VeoB^xtjhhAtfID%1d1b*N@D#yK!} z14@QRPE;>VwZegct&j;zCFe!c2jiPMbtw>+9Gnxjf1rHjk{8xxNJDMDdJq=5I^ksZ z;MZyd{u-)I`aOORAPrZvZ^3#H)=n5j@O~-q8$2{<3CDUIR)hJM8M6srVBHJ-2IZTI z`ioHw+6{ZligsHERT+^(&}&{F`=clDQx}Nk4%9n*j$%ty&$m-J--U*Qw~*cj{S5&- zUAs2xviLEZqF~D$OqSbISAMd4F{)u zRru`TrQEV42tNjG6x_Ay^V!0StWBHHeNZyUY9)Is$zmV*CR??KXYfxxTqE~9>#bl< zln%~2s)E*gyj!6(O}y=WYKALt!YZM#;n%XmhTjw(`1G{B64UUuG{sU`Ps}Kt#bSa! zgnVFyS8cx7y1h1$LJBvTjQ42Mr6y(z)BrB3Y1t5788Ro)BwZb==$UV;G1hxC&@^@` z8AUk`!c+&V(5kH-<+Jot_|HCh?9~db_c*sga@8oGfMSUxiEK-Y9C zta5mL4_gRmu_g}Dn)-O>gT$AR57;8%um;7#-ll2kv2bB?ii8JkB=H6=yIQck2br+X z1%xb31=DCwu1JCRE+`7lyE=(VI@ln(6}&Sk&1R`p$8;!;?UP@oqG@cLLy@1T#QnoO zel>L-B0_X15cPD<&2#>FANi z4!n#VC^k98W$N$p|Kk;+yb0rL?7-N8u> zC0*N2##f+QjdY#SpvyF_KB6YjPtZUh7m#Vb8pd&yW@f| z4R>_=d`y6x62cS<+)nFD=ChM%sBWGmLoEDBK=8UkM0!>vw)k4sWGA}kb{09&{tF6$ zNmISnScqt|PyPr?{0qq;TPIgIpODXlh1dz8lD?5_y})VJMBV|~R^&rpS|{t@4XdIy z8;WuuizyI-ZgZ>Ff%ZV1a{0&;y$;nBkP3t!oI=bJ?ScCeQ*uD&J3THQCFK@GAj4JoyXP%bdp#m@4(F?j~pc4#O4FJd9k!dfV-R`$J7@b zk8Py(r-)fKRUQ~8bs&_Uqaa4kamy|%g9#uXWYWQ*>n)iF>?=81o_NJC9EuZh#U=Xn z7lHBsE2cnLb6Q=NvajO*~BQ$s(JFmBR@A3 z(FIDEjS!M=!S<#$#EzjqaNAs66I4ppIUETR%fu5b8r^2cYj8NvIIGsRd8Rr z%u>!K$irt0c7fvWJ9%FS%kwPOMbH)eUS5(*XzZ63ZKX5mTU8`9cps$0=^iw~KpiOk zO{>zUEyroSq<+KthNAos8eujaIHErfQpz@39=v0)3+&P9O6%G9(Xaodzb@@fj;(zy zQMv~80=xAJfZ}~3F~N?M&PZ@bGy_OwQ5DPS#1LJ<_wM%{`dVtJ*!&j4VJq eAi2Y^nY=fz>F@5yV+Y0#j2##|Fm_;GJMceGE{$0L diff --git a/apps/dashboard/public/logo-2.svg b/apps/dashboard/public/logo-2.svg deleted file mode 100644 index 264890a2..00000000 --- a/apps/dashboard/public/logo-2.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/apps/dashboard/public/logo-with-text.svg b/apps/dashboard/public/logo-with-text.svg deleted file mode 100644 index 53198a73..00000000 --- a/apps/dashboard/public/logo-with-text.svg +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - - - - - diff --git a/apps/dashboard/public/logo.svg b/apps/dashboard/public/logo.svg index e45d0d95..82429a1b 100644 --- a/apps/dashboard/public/logo.svg +++ b/apps/dashboard/public/logo.svg @@ -1,5 +1,6 @@ - - - - + + + + + diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-histogram.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-histogram.tsx index 17e64b2e..f86d6aa7 100644 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-histogram.tsx +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-histogram.tsx @@ -81,7 +81,7 @@ export function RealtimeLiveHistogram({ {staticArray.map((percent, i) => (
))} @@ -101,7 +101,7 @@ export function RealtimeLiveHistogram({
+ + + Available + + + Installed + + + {tab === 'installed' && } + {tab === 'available' && } + + ); +} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/notifications/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/notifications/page.tsx new file mode 100644 index 00000000..1c2b306c --- /dev/null +++ b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/notifications/page.tsx @@ -0,0 +1,40 @@ +import { NotificationRules } from '@/components/notifications/notification-rules'; +import { Notifications } from '@/components/notifications/notifications'; +import { PageTabs, PageTabsLink } from '@/components/page-tabs'; +import { Padding } from '@/components/ui/padding'; +import { parseAsStringEnum } from 'nuqs'; + +interface PageProps { + params: { + projectId: string; + }; + searchParams: { + tab: string; + }; +} + +export default function Page({ + params: { projectId }, + searchParams, +}: PageProps) { + const tab = parseAsStringEnum(['notifications', 'rules']) + .withDefault('notifications') + .parseServerSide(searchParams.tab); + return ( + + + + Notifications + + + Rules + + + {tab === 'notifications' && } + {tab === 'rules' && } + + ); +} diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/onboarding-tracking.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/onboarding-tracking.tsx index eb1ad28d..448a843f 100644 --- a/apps/dashboard/src/app/(onboarding)/onboarding/onboarding-tracking.tsx +++ b/apps/dashboard/src/app/(onboarding)/onboarding/onboarding-tracking.tsx @@ -169,7 +169,7 @@ const Tracking = ({ placeholder="Add a domain" value={field.value?.split(',') ?? []} renderTag={(tag) => - tag === '*' ? 'Allow domains' : tag + tag === '*' ? 'Allow all domains' : tag } onChange={(newValue) => { field.onChange( diff --git a/apps/dashboard/src/app/apple-touch-icon.png b/apps/dashboard/src/app/apple-touch-icon.png deleted file mode 100644 index 3ad9fdadf28f5ec886e21acad1cc643221327260..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4357 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 diff --git a/apps/dashboard/src/app/favicon.ico b/apps/dashboard/src/app/favicon.ico index dd5ab726ed7448060d26d766f5a11f48e4c45a27..403a07ef9b3f1dd8a2031c3d51aaa4e55c77cfc0 100644 GIT binary patch literal 15406 zcmeHNX>b%(5FU^J>JR^?EEA(Bf(oUgLV2L2Qsq%uVs1Yy_Bf>4Y!$buZL<(OF}N6D?z z93_Pj{6U64avz98?*`Y5=U9<^_Hasm<^IsG|?+`;jCswzNLqWX9F!buQE-UrU<16Y}S`XKUj z167WoofrqbKk z+D2<#_>X!JN8Fo8lY#!E_xgf-=CHoz86RTq7I4pbjjP}D>2MQAFR_oqe{Rt}VXlHS?qP-@Tk=K2K8OSbw|%L%k=knRXSN^3@~p`p*~`#Al)q&a zFMsh6?z?A6d0OpvPI?@q36FAhX?{?^!j+zUX==hW%trlUxIyN-eE!I+#=`lpsZ_CX^# zdv@U2gO|y#%c1S7IiBd>d%(Y@5<1Fo z0j|myK^%R5`gaF-$3u5hFAA zs0AnbkW*s8`>ycbdQkmtP$MCB4-?o}1!DP?Z3Fp~0~?kwnfir|#RK{B$(Kw%e5-c& zYG);Ik^h_e(271AWn?R1^vS222u}d-Ma{l;!V)>W!?GH>U&;nna~&ea7zBS<2B656 za+4L(J)HmR*$J4U$M;yWP|So`b~fHA4uNbuA;lE5`(_ps5%W0k9#}~vab5)H`%iPY za*aO_kICS`Ay&HkcU8Us(%3#2tMM{~ey_tAi?%onqGx@8z8?pUdrg3B3;l+*lbPV(6!Aidj1r_U|^zGgV=Jt_Y6$?!kaA^f!FP+XK}L5a0r zIo_v69U7md@%h6~egy?%v9v^SZ05Plh}G&jZ}LJ~wD8B{2=z(~59If17q?+tP%NW4sUaWR`)aWogfFmeK8r)- zXi;B*4ON_vHP`rQe^2q9=0tlmx#2k{&Am23RuGY>azM0;%W zb0+|`DnMUy=(n{A@tV=&yRE3fxPOyU+D|V|A4c6Id!|@iT8Gk3h&{4@8_@VS*BLpm z&9WL_%@m!M_Ec8-Qll6j(2=tmj$?|kpuIr532mOnj5Mxo%`&#ZyL?PS6|{fl=62lg zMUL)FpbPKbq_@#nXS&aHrt*LL30qev27u0gw8vBtC7_k^N0{Rv|W$qNf&Q2I_h7F3GmfS20AzSKA*_W@ibPF z@o?oBH$c96+J{i=vT=WHq@%ggJ?C|B&wiDggN-!yvwe4K+kkBYwhdhMHqe!Qkc6FL z9@1MsI~!d*7NRT14^Rw*17jh&QXAq}S^S0;gJOt<=*;7R*}E}*hVa|T)<8!!@E@4* B`s)Ay literal 15406 zcmeHNX>b%p6dp@|_!EElKfkh3)ItkU5s#uQEq{2FQeK3Tf`kN$BXSpVSb}mYX8{U^ zOAAH05fCXw2_ak}Ik=C6Krq>v-Pzsj?(EF$KHuw253?&_ceAs@24}0fdUj@Bzwf<% z)7|e7gnNX0g&sWw>(#5{d7&UwKPU)A_zsdFMf^T? zxBRu0xc0b93ZW}48``e$vx`PvJPg7IHbVH1AHct6N=CZz&UzbksRcs&*F)4N;yB-D zL?2i`4b;$b<=LrAKD6$ z)79XwoCLc28pv}8ra?cI(a>Be)!PyOPmCr~Uj>16Q&ZA=XTJ+k@j3)|E`zA&2B;TL zLU7k|h@wu@;ZJ;hD=R>$*#XLtU7#G>&HBLF%;wQ9Tg#SDHs2u8-f~gM;3lmz1=wE1 zM(a~&KX;j{x4X2k0p*QlD{}_)G2~g_!Mi>Kx|DXB(OCA&os8d3ohS2l{&#VI&B(^? zA|B$%$3gyLF!+`Y2k)!_;9EY5HSwdTb8buWqEgVVUx28$HQxLlh=qgf+B^Hbobvb1 zd<%5k`wZTM0r^8T3b9ZCa?L+nwVCnO+nT|*v>ZI2yawvoBaA=jI(X;2pHum@D|L)N zox8a0lMRsk)V%YGjr;t$mA!WLG~*xob*;fiUhtub{~+VOm3;YFsrc(Hmq5Ex3+ybl zVFtL%9<~&Z)>waA`J;Y`J?Hir{&N$!ON%VSz~?_f|6dL<^dG4^2w^-ElMXlr-|~^R z@#pp@j_wUwQ`}bQoXO`e+dh^2oImji?W<%u&@P{{jz5k?dJX;doiV0wNtvPlME+~y z{BbU_m7CKmH9Oguddo#zdtL`|%#(P2-_yZg2YKNTpuNHS*#OXQTw?DkM}D@9zdXMf zeU5+FxKS_m(hXdHCB~oc0o?9*?^`y)GXA6of8}KKwVFFH;QH^AAQlPZ|F)a=XkS2_ z!EKB2`%CT9as8&-OB0?4|2GpM@a+t?4+J*Og22X^MkAltGwoGNOSi+`Aq&}z8|S0@;_n9b zo@_VHe|z>YN6(I)9UE|LARQYhe*`3q6-(ov18H187DtfA_6725S)(|zg>fs(_S<1a z;Q91T2<})4k(2vDJ%1e3Gc_!KNxgRIZ_J1O#o{v*gR+coIyz0`9g5jgzLxT~JU+sD z+09}YERG?!fpYv85G$U_EBq91;xVNJ>=bKZ_?37ZNC`pY#J;@3FOGVG<+;tcwZ=MD zzd-RbnvYl{j-TTAkyF3t6@KsM?|{~P*0346_G0dVVtwM6KG6Q-JZ3*U4-jcMlvnuu zE60G|+GOAlY?_l`+tS1rK*t&k1OJ)BU`H-1-Sxr0dO||X(f3tFBL1<@fO_tj34cv~ z21%m-PVmrJFC)cE^?df|j&|CCGCn$NKu z7F&0v#GG?r!}Ofw6*`k6Q~WCCspR>CFh}$T(B8po#kf7GHm|Rl;*Vf`pLzWkM?A*z zTpYi4?HtHC&b6E8B3t}47VRM%zuwvi{uQHa`;M)^Pq{^&`=hnUziPaF_*aa^^FyPd zKdQkr&ky-AT4RIp{U+NF(?8}7Z|c7Z=uBcBBbV3m+z(idDd(?Z&8Rf)S+)*}!yg47 z%13JsKmVAYyUTmBHJ|ECI6a+}uX3BQ8+S0l_9Gq9T9!D~8$2luSHbH069 zPpnZG%KR?vaxI=In$V}bjA!SgXfqdCJpt8GCp27Zw5w-8Il7zaoqV2Dddlf@tS6vz zB>60!k6#AQl$S7$(hsD`{lPQ!mH#$;N1D{1`DyN958YYpX~ZaUsym0jGyjeaI5yze zK$mTxThu&sHousAXF2%Jp6B^}s(qjui0)dy!D~SwS# M$oV-M=&}a>1+M;*ZU6uP diff --git a/apps/dashboard/src/app/providers.tsx b/apps/dashboard/src/app/providers.tsx index 403bd3a2..22b52187 100644 --- a/apps/dashboard/src/app/providers.tsx +++ b/apps/dashboard/src/app/providers.tsx @@ -15,6 +15,7 @@ import { Provider as ReduxProvider } from 'react-redux'; import { Toaster } from 'sonner'; import superjson from 'superjson'; +import { NotificationProvider } from '@/components/notifications/notification-provider'; import { OpenPanelComponent } from '@openpanel/nextjs'; function AllProviders({ children }: { children: React.ReactNode }) { @@ -76,6 +77,7 @@ function AllProviders({ children }: { children: React.ReactNode }) { {children} + diff --git a/apps/dashboard/src/components/full-page-empty-state.tsx b/apps/dashboard/src/components/full-page-empty-state.tsx index 23935f8d..e0f70426 100644 --- a/apps/dashboard/src/components/full-page-empty-state.tsx +++ b/apps/dashboard/src/components/full-page-empty-state.tsx @@ -5,7 +5,7 @@ import type { LucideIcon } from 'lucide-react'; interface FullPageEmptyStateProps { icon?: LucideIcon; title: string; - children: React.ReactNode; + children?: React.ReactNode; className?: string; } diff --git a/apps/dashboard/src/components/integrations/active-integrations.tsx b/apps/dashboard/src/components/integrations/active-integrations.tsx new file mode 100644 index 00000000..6dd74839 --- /dev/null +++ b/apps/dashboard/src/components/integrations/active-integrations.tsx @@ -0,0 +1,118 @@ +'use client'; + +import { useAppParams } from '@/hooks/useAppParams'; +import { pushModal, showConfirm } from '@/modals'; +import { api } from '@/trpc/client'; +import { useQueryClient } from '@tanstack/react-query'; +import { getQueryKey } from '@trpc/react-query'; +import { AnimatePresence, motion } from 'framer-motion'; +import { BoxSelectIcon } from 'lucide-react'; +import { useMemo } from 'react'; +import { PingBadge } from '../ping'; +import { Button } from '../ui/button'; +import { + IntegrationCard, + IntegrationCardFooter, + IntegrationCardLogo, + IntegrationCardSkeleton, +} from './integration-card'; +import { INTEGRATIONS } from './integrations'; + +export function ActiveIntegrations() { + const { organizationId } = useAppParams(); + const query = api.integration.list.useQuery({ + organizationId, + }); + const client = useQueryClient(); + const deletion = api.integration.delete.useMutation({ + onSuccess() { + client.refetchQueries( + getQueryKey(api.integration.list, { + organizationId, + }), + ); + }, + }); + + const data = useMemo(() => { + return (query.data || []) + .map((item) => { + const integration = INTEGRATIONS.find( + (integration) => integration.type === item.config.type, + )!; + return { + ...item, + integration, + }; + }) + .filter((item) => item.integration); + }, [query.data]); + + const isLoading = query.isLoading; + + return ( +
+ {isLoading && ( + <> + + + + + )} + {!isLoading && data.length === 0 && ( + + + + } + name="No integrations yet" + description="Integrations allow you to connect your systems to OpenPanel. You can add them in the available integrations section." + /> + )} + + {data.map((item) => { + return ( + + + + Connected +
+ + +
+
+
+
+ ); + })} +
+
+ ); +} diff --git a/apps/dashboard/src/components/integrations/all-integrations.tsx b/apps/dashboard/src/components/integrations/all-integrations.tsx new file mode 100644 index 00000000..713c5432 --- /dev/null +++ b/apps/dashboard/src/components/integrations/all-integrations.tsx @@ -0,0 +1,36 @@ +'use client'; + +import { Button } from '@/components/ui/button'; +import { pushModal } from '@/modals'; +import { PlugIcon, WebhookIcon } from 'lucide-react'; +import { IntegrationCard, IntegrationCardFooter } from './integration-card'; +import { INTEGRATIONS } from './integrations'; + +export function AllIntegrations() { + return ( +
+ {INTEGRATIONS.map((integration) => ( + + + + + + ))} +
+ ); +} diff --git a/apps/dashboard/src/components/integrations/forms/discord-integration.tsx b/apps/dashboard/src/components/integrations/forms/discord-integration.tsx new file mode 100644 index 00000000..27628f04 --- /dev/null +++ b/apps/dashboard/src/components/integrations/forms/discord-integration.tsx @@ -0,0 +1,91 @@ +import { InputWithLabel } from '@/components/forms/input-with-label'; +import { Button } from '@/components/ui/button'; +import { useAppParams } from '@/hooks/useAppParams'; +import { type RouterOutputs, api } from '@/trpc/client'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { sendTestDiscordNotification } from '@openpanel/integrations/src/discord'; +import { zCreateDiscordIntegration } from '@openpanel/validation'; +import { path, mergeDeepRight } from 'ramda'; +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import type { z } from 'zod'; + +type IForm = z.infer; + +export function DiscordIntegrationForm({ + defaultValues, + onSuccess, +}: { + defaultValues?: RouterOutputs['integration']['get']; + onSuccess: () => void; +}) { + const { organizationId } = useAppParams(); + const form = useForm({ + defaultValues: mergeDeepRight( + { + id: defaultValues?.id, + organizationId, + config: { + type: 'discord' as const, + url: '', + headers: {}, + }, + }, + defaultValues ?? {}, + ), + resolver: zodResolver(zCreateDiscordIntegration), + }); + const mutation = api.integration.createOrUpdate.useMutation({ + onSuccess, + onError() { + toast.error('Failed to create integration'); + }, + }); + + const handleSubmit = (values: IForm) => { + mutation.mutate(values); + }; + + const handleError = () => { + toast.error('Validation error'); + }; + + const handleTest = async () => { + const webhookUrl = form.getValues('config.url'); + if (!webhookUrl) { + return toast.error('Webhook URL is required'); + } + const res = await sendTestDiscordNotification(webhookUrl); + if (res.ok) { + toast.success('Test notification sent'); + } else { + toast.error('Failed to send test notification'); + } + }; + + return ( +
+ + +
+ + +
+ + ); +} diff --git a/apps/dashboard/src/components/integrations/forms/slack-integration.tsx b/apps/dashboard/src/components/integrations/forms/slack-integration.tsx new file mode 100644 index 00000000..a548d68f --- /dev/null +++ b/apps/dashboard/src/components/integrations/forms/slack-integration.tsx @@ -0,0 +1,85 @@ +import { InputWithLabel } from '@/components/forms/input-with-label'; +import { Button } from '@/components/ui/button'; +import { useAppParams } from '@/hooks/useAppParams'; +import useWS from '@/hooks/useWS'; +import { popModal } from '@/modals'; +import { type RouterOutputs, api } from '@/trpc/client'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { zCreateSlackIntegration } from '@openpanel/validation'; +import { useQueryClient } from '@tanstack/react-query'; +import { useRef } from 'react'; +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import type { z } from 'zod'; + +type IForm = z.infer; + +export function SlackIntegrationForm({ + defaultValues, + onSuccess, +}: { + defaultValues?: RouterOutputs['integration']['get']; + onSuccess: () => void; +}) { + const popup = useRef(null); + const { organizationId } = useAppParams(); + const client = useQueryClient(); + useWS('/live/integrations/slack', (res) => { + if (popup.current) { + popup.current.close(); + } + onSuccess(); + }); + const form = useForm({ + defaultValues: { + id: defaultValues?.id, + organizationId, + name: defaultValues?.name ?? '', + }, + resolver: zodResolver(zCreateSlackIntegration), + }); + const mutation = api.integration.createOrUpdateSlack.useMutation({ + async onSuccess(res) { + const url = res.slackInstallUrl; + const width = 600; + const height = 800; + const left = window.screenX + (window.outerWidth - width) / 2; + const top = window.screenY + (window.outerHeight - height) / 2.5; + popup.current = window.open( + url, + '', + `toolbar=no, location=no, directories=no, status=no, menubar=no, scrollbars=no, resizable=no, copyhistory=no, width=${width}, height=${height}, top=${top}, left=${left}`, + ); + + // The popup might have been blocked, so we redirect the user to the URL instead + if (!popup.current) { + window.location.href = url; + } + }, + onError() { + toast.error('Failed to create integration'); + }, + }); + + const handleSubmit = (values: IForm) => { + mutation.mutate(values); + }; + + const handleError = () => { + toast.error('Validation error'); + }; + + return ( +
+ + + + ); +} diff --git a/apps/dashboard/src/components/integrations/forms/webhook-integration.tsx b/apps/dashboard/src/components/integrations/forms/webhook-integration.tsx new file mode 100644 index 00000000..6a52d6ce --- /dev/null +++ b/apps/dashboard/src/components/integrations/forms/webhook-integration.tsx @@ -0,0 +1,73 @@ +import { InputWithLabel } from '@/components/forms/input-with-label'; +import { Button } from '@/components/ui/button'; +import { useAppParams } from '@/hooks/useAppParams'; +import { popModal } from '@/modals'; +import { type RouterOutputs, api } from '@/trpc/client'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { zCreateWebhookIntegration } from '@openpanel/validation'; +import { useQueryClient } from '@tanstack/react-query'; +import { path, mergeDeepRight } from 'ramda'; +import { useForm } from 'react-hook-form'; +import { toast } from 'sonner'; +import type { z } from 'zod'; + +type IForm = z.infer; + +export function WebhookIntegrationForm({ + defaultValues, + onSuccess, +}: { + defaultValues?: RouterOutputs['integration']['get']; + onSuccess: () => void; +}) { + const { organizationId } = useAppParams(); + const form = useForm({ + defaultValues: mergeDeepRight( + { + id: defaultValues?.id, + organizationId, + config: { + type: 'webhook' as const, + url: '', + headers: {}, + }, + }, + defaultValues ?? {}, + ), + resolver: zodResolver(zCreateWebhookIntegration), + }); + const client = useQueryClient(); + const mutation = api.integration.createOrUpdate.useMutation({ + onSuccess, + onError() { + toast.error('Failed to create integration'); + }, + }); + + const handleSubmit = (values: IForm) => { + mutation.mutate(values); + }; + + const handleError = () => { + toast.error('Validation error'); + }; + + return ( +
+ + + + + ); +} diff --git a/apps/dashboard/src/components/integrations/integration-card.tsx b/apps/dashboard/src/components/integrations/integration-card.tsx new file mode 100644 index 00000000..ab80513c --- /dev/null +++ b/apps/dashboard/src/components/integrations/integration-card.tsx @@ -0,0 +1,144 @@ +import { Skeleton } from '@/components/skeleton'; +import { cn } from '@/utils/cn'; +export function IntegrationCardFooter({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ( +
+ {children} +
+ ); +} + +export function IntegrationCardHeader({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ( +
+ {children} +
+ ); +} + +export function IntegrationCardHeaderButtons({ + children, + className, +}: { + children: React.ReactNode; + className?: string; +}) { + return ( +
+ {children} +
+ ); +} + +export function IntegrationCardLogoImage({ + src, + backgroundColor, +}: { + src: string; + backgroundColor: string; +}) { + return ( + + Integration Logo + + ); +} + +export function IntegrationCardLogo({ + children, + className, + ...props +}: { + children: React.ReactNode; +} & React.HTMLAttributes) { + return ( +
+ {children} +
+ ); +} + +export function IntegrationCard({ + icon, + name, + description, + children, +}: { + icon: React.ReactNode; + name: string; + description: string; + children?: React.ReactNode; +}) { + return ( +
+ + {children} +
+ ); +} + +export function IntegrationCardContent({ + icon, + name, + description, +}: { + icon: React.ReactNode; + name: string; + description: string; +}) { + return ( +
+ {icon} +
+

{name}

+

{description}

+
+
+ ); +} + +export function IntegrationCardSkeleton() { + return ( +
+
+ +
+ + + +
+
+
+ ); +} diff --git a/apps/dashboard/src/components/integrations/integrations.tsx b/apps/dashboard/src/components/integrations/integrations.tsx new file mode 100644 index 00000000..6c005e57 --- /dev/null +++ b/apps/dashboard/src/components/integrations/integrations.tsx @@ -0,0 +1,49 @@ +import type { IIntegrationConfig } from '@openpanel/db'; +import { WebhookIcon } from 'lucide-react'; +import { + IntegrationCardLogo, + IntegrationCardLogoImage, +} from './integration-card'; + +export const INTEGRATIONS: { + type: IIntegrationConfig['type']; + name: string; + description: string; + icon: React.ReactNode; +}[] = [ + { + type: 'slack', + name: 'Slack', + description: + 'Connect your Slack workspace to get notified when new issues are created.', + icon: ( + + ), + }, + { + type: 'discord', + name: 'Discord', + description: + 'Connect your Discord server to get notified when new issues are created.', + icon: ( + + ), + }, + { + type: 'webhook', + name: 'Webhook', + description: + 'Create a webhook to take actions in your own systems when new events are created.', + icon: ( + + + + ), + }, +]; diff --git a/apps/dashboard/src/components/links.tsx b/apps/dashboard/src/components/links.tsx index 3965cc1b..028ee400 100644 --- a/apps/dashboard/src/components/links.tsx +++ b/apps/dashboard/src/components/links.tsx @@ -13,7 +13,13 @@ export function ProjectLink({ const { organizationSlug, projectId } = useAppParams(); if (typeof props.href === 'string') { return ( - + {children} ); diff --git a/apps/dashboard/src/components/notifications/notification-provider.tsx b/apps/dashboard/src/components/notifications/notification-provider.tsx new file mode 100644 index 00000000..1e125893 --- /dev/null +++ b/apps/dashboard/src/components/notifications/notification-provider.tsx @@ -0,0 +1,17 @@ +import { useAppParams } from '@/hooks/useAppParams'; +import useWS from '@/hooks/useWS'; +import type { Notification } from '@openpanel/db'; +import { BellIcon } from 'lucide-react'; +import { toast } from 'sonner'; + +export function NotificationProvider() { + const { projectId } = useAppParams(); + useWS(`/live/notifications/${projectId}`, (notification) => { + toast(notification.title, { + description: notification.message, + icon: , + }); + }); + + return null; +} diff --git a/apps/dashboard/src/components/notifications/notification-rules.tsx b/apps/dashboard/src/components/notifications/notification-rules.tsx new file mode 100644 index 00000000..a32884ef --- /dev/null +++ b/apps/dashboard/src/components/notifications/notification-rules.tsx @@ -0,0 +1,74 @@ +'use client'; + +import { useAppParams } from '@/hooks/useAppParams'; +import { pushModal } from '@/modals'; +import { api } from '@/trpc/client'; +import { AnimatePresence, motion } from 'framer-motion'; +import { BoxSelectIcon, PlusIcon } from 'lucide-react'; +import { useMemo } from 'react'; +import { + IntegrationCard, + IntegrationCardLogo, + IntegrationCardSkeleton, +} from '../integrations/integration-card'; +import { Button } from '../ui/button'; +import { RuleCard } from './rule-card'; + +export function NotificationRules() { + const { projectId } = useAppParams(); + const query = api.notification.rules.useQuery({ + projectId, + }); + const data = useMemo(() => { + return query.data || []; + }, [query.data]); + + const isLoading = query.isLoading; + + return ( +
+
+ +
+
+ {isLoading && ( + <> + + + + + )} + {!isLoading && data.length === 0 && ( + + + + } + name="No integrations yet" + description="Integrations allow you to connect your systems to OpenPanel. You can add them in the available integrations section." + /> + )} + + {data.map((item) => { + return ( + + + + ); + })} + +
+
+ ); +} diff --git a/apps/dashboard/src/components/notifications/notifications.tsx b/apps/dashboard/src/components/notifications/notifications.tsx new file mode 100644 index 00000000..82296022 --- /dev/null +++ b/apps/dashboard/src/components/notifications/notifications.tsx @@ -0,0 +1,14 @@ +'use client'; + +import { useAppParams } from '@/hooks/useAppParams'; +import { api } from '@/trpc/client'; +import { NotificationsTable } from './table'; + +export function Notifications() { + const { projectId } = useAppParams(); + const query = api.notification.list.useQuery({ + projectId, + }); + + return ; +} diff --git a/apps/dashboard/src/components/notifications/rule-card.tsx b/apps/dashboard/src/components/notifications/rule-card.tsx new file mode 100644 index 00000000..8a8c3d87 --- /dev/null +++ b/apps/dashboard/src/components/notifications/rule-card.tsx @@ -0,0 +1,131 @@ +import { pushModal, showConfirm } from '@/modals'; +import { type RouterOutputs, api } from '@/trpc/client'; +import type { NotificationRule } from '@openpanel/db'; +import type { IChartRange, IInterval } from '@openpanel/validation'; +import { useQueryClient } from '@tanstack/react-query'; +import { getQueryKey } from '@trpc/react-query'; +import { AsteriskIcon, FilterIcon } from 'lucide-react'; +import { Fragment } from 'react'; +import { toast } from 'sonner'; +import { ColorSquare } from '../color-square'; +import { + IntegrationCardFooter, + IntegrationCardHeader, +} from '../integrations/integration-card'; +import { PingBadge } from '../ping'; +import { Badge } from '../ui/badge'; +import { Button } from '../ui/button'; +import { Tooltiper } from '../ui/tooltip'; + +function EventBadge({ + event, +}: { event: NotificationRule['config']['events'][number] }) { + return ( + + {event.filters.map((filter) => ( +
+ {filter.name} {filter.operator} {JSON.stringify(filter.value)} +
+ ))} +
+ } + > + + {event.name} + {Boolean(event.filters.length) && ( + + )} + + + ); +} + +export function RuleCard({ + rule, +}: { rule: RouterOutputs['notification']['rules'][number] }) { + const client = useQueryClient(); + const deletion = api.notification.deleteRule.useMutation({ + onSuccess() { + toast.success('Rule deleted'); + client.refetchQueries(getQueryKey(api.notification.rules)); + }, + }); + const renderConfig = () => { + switch (rule.config.type) { + case 'events': + return ( +
+
Get notified when
+ {rule.config.events.map((event) => ( + + ))} +
occurs
+
+ ); + case 'funnel': + return ( +
+
Get notified when a session has completed this funnel
+
+ {rule.config.events.map((event, index) => ( +
+ {index + 1} + +
+ ))} +
+
+ ); + } + }; + return ( +
+ +
{rule.name}
+
+
{renderConfig()}
+ +
+ {rule.integrations.map((integration) => ( + {integration.name} + ))} +
+
+ + +
+
+
+ ); +} diff --git a/apps/dashboard/src/components/notifications/table/columns.tsx b/apps/dashboard/src/components/notifications/table/columns.tsx new file mode 100644 index 00000000..e4eae86a --- /dev/null +++ b/apps/dashboard/src/components/notifications/table/columns.tsx @@ -0,0 +1,140 @@ +import { useNumber } from '@/hooks/useNumerFormatter'; +import { formatDateTime, formatTime } from '@/utils/date'; +import type { ColumnDef } from '@tanstack/react-table'; +import { isToday } from 'date-fns'; + +import { ProjectLink } from '@/components/links'; +import { PingBadge } from '@/components/ping'; +import { SerieIcon } from '@/components/report-chart/common/serie-icon'; +import type { RouterOutputs } from '@/trpc/client'; +import type { INotificationPayload } from '@openpanel/db'; + +function getEventFromPayload(payload: INotificationPayload | null) { + if (payload?.type === 'event') { + return payload.event; + } + if (payload?.type === 'funnel') { + return payload.funnel[0] || null; + } + return null; +} + +export function useColumns() { + const columns: ColumnDef[] = [ + { + accessorKey: 'title', + header: 'Title', + cell({ row }) { + const { title, isReadAt } = row.original; + return ( +
+ {isReadAt === null && Unread} + {title} +
+ ); + }, + }, + { + accessorKey: 'message', + header: 'Message', + cell({ row }) { + const { message } = row.original; + return ( +
+ {message} +
+ ); + }, + }, + { + accessorKey: 'country', + header: 'Country', + cell({ row }) { + const { payload } = row.original; + const event = getEventFromPayload(payload); + if (!event) { + return null; + } + return ( +
+ + {event.city} +
+ ); + }, + }, + { + accessorKey: 'os', + header: 'OS', + cell({ row }) { + const { payload } = row.original; + const event = getEventFromPayload(payload); + if (!event) { + return null; + } + return ( +
+ + {event.os} +
+ ); + }, + }, + { + accessorKey: 'browser', + header: 'Browser', + cell({ row }) { + const { payload } = row.original; + const event = getEventFromPayload(payload); + if (!event) { + return null; + } + return ( +
+ + {event.browser} +
+ ); + }, + }, + { + accessorKey: 'profile', + header: 'Profile', + cell({ row }) { + const { payload } = row.original; + const event = getEventFromPayload(payload); + if (!event) { + return null; + } + return ( + + {event.profileId} + + ); + }, + }, + { + accessorKey: 'createdAt', + header: 'Created at', + cell({ row }) { + const date = row.original.createdAt; + const rule = row.original.integration?.notificationRules[0]; + return ( +
+
{isToday(date) ? formatTime(date) : formatDateTime(date)}
+ {rule && ( +
+ Rule: {rule.name} +
+ )} +
+ ); + }, + }, + ]; + + return columns; +} diff --git a/apps/dashboard/src/components/notifications/table/index.tsx b/apps/dashboard/src/components/notifications/table/index.tsx new file mode 100644 index 00000000..6881543d --- /dev/null +++ b/apps/dashboard/src/components/notifications/table/index.tsx @@ -0,0 +1,64 @@ +import { DataTable } from '@/components/data-table'; +import { FullPageEmptyState } from '@/components/full-page-empty-state'; +import { Pagination } from '@/components/pagination'; +import { Button } from '@/components/ui/button'; +import { TableSkeleton } from '@/components/ui/table'; +import type { UseQueryResult } from '@tanstack/react-query'; +import { GanttChartIcon } from 'lucide-react'; +import type { Dispatch, SetStateAction } from 'react'; + +import type { Notification } from '@openpanel/db'; + +import { useColumns } from './columns'; + +type Props = + | { + query: UseQueryResult; + } + | { + query: UseQueryResult; + cursor: number; + setCursor: Dispatch>; + }; + +export const NotificationsTable = ({ query, ...props }: Props) => { + const columns = useColumns(); + const { data, isFetching, isLoading } = query; + + if (isLoading) { + return ; + } + + if (data?.length === 0) { + return ( + +

Could not find any events

+ {'cursor' in props && props.cursor !== 0 && ( + + )} +
+ ); + } + + return ( + <> + + {'cursor' in props && ( + + )} + + ); +}; diff --git a/apps/dashboard/src/components/overview/overview-live-histogram.tsx b/apps/dashboard/src/components/overview/overview-live-histogram.tsx index d9e37f59..2f9c06ef 100644 --- a/apps/dashboard/src/components/overview/overview-live-histogram.tsx +++ b/apps/dashboard/src/components/overview/overview-live-histogram.tsx @@ -79,7 +79,7 @@ export function OverviewLiveHistogram({ {staticArray.map((percent, i) => (
))} @@ -99,7 +99,7 @@ export function OverviewLiveHistogram({
+
+
+
+ ); +} + +export function PingBadge({ + children, + className, +}: { children: React.ReactNode; className?: string }) { + return ( + + + {children} + + ); +} diff --git a/apps/dashboard/src/components/settings-toggle.tsx b/apps/dashboard/src/components/settings-toggle.tsx index 75f0e7d4..37004e01 100644 --- a/apps/dashboard/src/components/settings-toggle.tsx +++ b/apps/dashboard/src/components/settings-toggle.tsx @@ -57,6 +57,14 @@ export default function SettingsToggle({ className }: Props) { References + + + Notifications + + + + Integrations + diff --git a/apps/dashboard/src/components/skeleton.tsx b/apps/dashboard/src/components/skeleton.tsx new file mode 100644 index 00000000..77671055 --- /dev/null +++ b/apps/dashboard/src/components/skeleton.tsx @@ -0,0 +1,5 @@ +import { cn } from '@/utils/cn'; + +export function Skeleton({ className }: { className?: string }) { + return
; +} diff --git a/apps/dashboard/src/components/ui/badge.tsx b/apps/dashboard/src/components/ui/badge.tsx index 6388c05f..0c56a752 100644 --- a/apps/dashboard/src/components/ui/badge.tsx +++ b/apps/dashboard/src/components/ui/badge.tsx @@ -6,7 +6,7 @@ import type { VariantProps } from 'class-variance-authority'; import type * as React from 'react'; const badgeVariants = cva( - 'inline-flex h-[20px] items-center rounded-full border px-2 text-sm font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', + 'inline-flex h-[20px] items-center rounded border px-2 text-sm font-mono transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', { variants: { variant: { @@ -20,6 +20,7 @@ const badgeVariants = cva( 'border-transparent bg-emerald-500 text-emerald-100 hover:bg-emerald-500/80', outline: 'text-foreground', muted: 'bg-def-100 text-foreground', + foregroundish: 'bg-foregroundish text-foregroundish-foreground', }, }, defaultVariants: { diff --git a/apps/dashboard/src/components/ui/tooltip.tsx b/apps/dashboard/src/components/ui/tooltip.tsx index b2ede636..3a932a74 100644 --- a/apps/dashboard/src/components/ui/tooltip.tsx +++ b/apps/dashboard/src/components/ui/tooltip.tsx @@ -42,6 +42,7 @@ interface TooltiperProps { side?: 'top' | 'right' | 'bottom' | 'left'; delayDuration?: number; sideOffset?: number; + disabled?: boolean; } export function Tooltiper({ asChild, @@ -52,7 +53,9 @@ export function Tooltiper({ side, delayDuration = 0, sideOffset = 10, + disabled = false, }: TooltiperProps) { + if (disabled) return children; return ( diff --git a/apps/dashboard/src/hooks/useAppParams.ts b/apps/dashboard/src/hooks/useAppParams.ts index 616c7893..875aa309 100644 --- a/apps/dashboard/src/hooks/useAppParams.ts +++ b/apps/dashboard/src/hooks/useAppParams.ts @@ -2,6 +2,7 @@ import { useParams } from 'next/navigation'; type AppParams = { organizationSlug: string; + organizationId: string; projectId: string; }; @@ -10,6 +11,7 @@ export function useAppParams() { return { ...(params ?? {}), organizationSlug: params?.organizationSlug, + organizationId: params?.organizationSlug, projectId: params?.projectId, } as T & AppParams; } diff --git a/apps/dashboard/src/modals/AddClient.tsx b/apps/dashboard/src/modals/AddClient.tsx index 74a7bfdd..ad9deafb 100644 --- a/apps/dashboard/src/modals/AddClient.tsx +++ b/apps/dashboard/src/modals/AddClient.tsx @@ -157,7 +157,9 @@ export default function AddClient(props: Props) { error={form.formState.errors.cors?.message} placeholder="Add a domain" value={field.value?.split(',') ?? []} - renderTag={(tag) => (tag === '*' ? 'Allow domains' : tag)} + renderTag={(tag) => + tag === '*' ? 'Allow all domains' : tag + } onChange={(newValue) => { field.onChange( newValue diff --git a/apps/dashboard/src/modals/EditClient.tsx b/apps/dashboard/src/modals/EditClient.tsx index fd9a9db4..2981e2bb 100644 --- a/apps/dashboard/src/modals/EditClient.tsx +++ b/apps/dashboard/src/modals/EditClient.tsx @@ -91,7 +91,7 @@ export default function EditClient({ error={formState.errors.cors?.message} placeholder="Add a domain" value={field.value?.split(',') ?? []} - renderTag={(tag) => (tag === '*' ? 'Allow domains' : tag)} + renderTag={(tag) => (tag === '*' ? 'Allow all domains' : tag)} onChange={(newValue) => { field.onChange( newValue diff --git a/apps/dashboard/src/modals/add-integration.tsx b/apps/dashboard/src/modals/add-integration.tsx new file mode 100644 index 00000000..1981c2c6 --- /dev/null +++ b/apps/dashboard/src/modals/add-integration.tsx @@ -0,0 +1,101 @@ +'use client'; + +import { api } from '@/trpc/client'; + +import { DiscordIntegrationForm } from '@/components/integrations/forms/discord-integration'; +import { SlackIntegrationForm } from '@/components/integrations/forms/slack-integration'; +import { WebhookIntegrationForm } from '@/components/integrations/forms/webhook-integration'; +import { IntegrationCardContent } from '@/components/integrations/integration-card'; +import { INTEGRATIONS } from '@/components/integrations/integrations'; +import { SheetContent } from '@/components/ui/sheet'; +import type { IIntegrationConfig } from '@openpanel/validation'; +import { useQueryClient } from '@tanstack/react-query'; +import { getQueryKey } from '@trpc/react-query'; +import { useQueryState } from 'nuqs'; +import { toast } from 'sonner'; +import { popModal } from '.'; +import { ModalHeader } from './Modal/Container'; + +interface Props { + id?: string; + type: IIntegrationConfig['type']; +} +export default function AddIntegration(props: Props) { + const query = api.integration.get.useQuery( + { + id: props.id ?? '', + }, + { + enabled: !!props.id, + }, + ); + + const integration = INTEGRATIONS.find((i) => i.type === props.type); + + const renderCard = () => { + if (!integration) { + return null; + } + return ( +
+ +
+ ); + }; + + const [tab, setTab] = useQueryState('tab', { + shallow: false, + }); + const client = useQueryClient(); + const handleSuccess = () => { + toast.success('Integration created'); + popModal(); + client.refetchQueries([ + getQueryKey(api.integration.list), + getQueryKey(api.integration.get, { id: props.id }), + ]); + if (tab !== undefined) { + setTab('installed'); + } + }; + + const renderForm = () => { + if (props.id && query.isLoading) { + return null; + } + + switch (integration?.type) { + case 'webhook': + return ( + + ); + case 'discord': + return ( + + ); + case 'slack': + return ( + + ); + default: + return null; + } + }; + + return ( + + + {renderCard()} + {renderForm()} + + ); +} diff --git a/apps/dashboard/src/modals/add-notification-rule.tsx b/apps/dashboard/src/modals/add-notification-rule.tsx new file mode 100644 index 00000000..45354f75 --- /dev/null +++ b/apps/dashboard/src/modals/add-notification-rule.tsx @@ -0,0 +1,308 @@ +'use client'; + +import { type RouterOutputs, api } from '@/trpc/client'; + +import { SheetContent } from '@/components/ui/sheet'; +import type { NotificationRule } from '@openpanel/db'; +import { useQueryClient } from '@tanstack/react-query'; +import { getQueryKey } from '@trpc/react-query'; +import { toast } from 'sonner'; +import { popModal } from '.'; +import { ModalHeader } from './Modal/Container'; + +import { ColorSquare } from '@/components/color-square'; +import { CheckboxItem } from '@/components/forms/checkbox-item'; +import { InputWithLabel, WithLabel } from '@/components/forms/input-with-label'; +import { PureFilterItem } from '@/components/report/sidebar/filters/FilterItem'; +import { Button } from '@/components/ui/button'; +import { Combobox } from '@/components/ui/combobox'; +import { ComboboxAdvanced } from '@/components/ui/combobox-advanced'; +import { useAppParams } from '@/hooks/useAppParams'; +import { useEventNames } from '@/hooks/useEventNames'; +import { useEventProperties } from '@/hooks/useEventProperties'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { shortId } from '@openpanel/common'; +import { + IChartEvent, + type IChartRange, + type IInterval, + zCreateNotificationRule, +} from '@openpanel/validation'; +import { + FilterIcon, + PlusIcon, + SaveIcon, + SmartphoneIcon, + TrashIcon, +} from 'lucide-react'; +import { + Controller, + type SubmitHandler, + type UseFormReturn, + useFieldArray, + useForm, + useWatch, +} from 'react-hook-form'; +import type { z } from 'zod'; + +interface Props { + rule?: RouterOutputs['notification']['rules'][number]; +} + +type IForm = z.infer; + +export default function AddNotificationRule({ rule }: Props) { + const client = useQueryClient(); + const { organizationId, projectId } = useAppParams(); + const form = useForm({ + resolver: zodResolver(zCreateNotificationRule), + defaultValues: { + id: rule?.id ?? '', + name: rule?.name ?? '', + sendToApp: rule?.sendToApp ?? false, + sendToEmail: rule?.sendToEmail ?? false, + integrations: + rule?.integrations.map((integration) => integration.id) ?? [], + projectId, + config: rule?.config ?? { + type: 'events', + events: [ + { + name: '', + segment: 'event', + filters: [], + }, + ], + }, + }, + }); + const mutation = api.notification.createOrUpdateRule.useMutation({ + onSuccess() { + toast.success( + rule ? 'Notification rule updated' : 'Notification rule created', + ); + client.refetchQueries( + getQueryKey(api.notification.rules, { + projectId, + }), + ); + popModal(); + }, + }); + + const eventsArray = useFieldArray({ + control: form.control, + name: 'config.events', + }); + + const onSubmit: SubmitHandler = (data) => { + mutation.mutate(data); + }; + + const integrationsQuery = api.integration.list.useQuery({ + organizationId, + }); + const integrations = integrationsQuery.data ?? []; + + return ( + + +
+ + + + ( + + )} + /> + + +
+ {eventsArray.fields.map((field, index) => { + return ( + eventsArray.remove(index)} + /> + ); + })} + +
+
+ + ( + + ({ + label: integration.name, + value: integration.id, + }))} + /> + + )} + /> + + + +
+ ); +} + +const interval: IInterval = 'day'; +const range: IChartRange = 'lastMonth'; + +function EventField({ + form, + index, + remove, +}: { + form: UseFormReturn; + index: number; + remove: () => void; +}) { + const { projectId } = useAppParams(); + const eventNames = useEventNames({ projectId, interval, range }); + const filtersArray = useFieldArray({ + control: form.control, + name: `config.events.${index}.filters`, + }); + const eventName = useWatch({ + control: form.control, + name: `config.events.${index}.name`, + }); + const properties = useEventProperties({ projectId, interval, range }); + + return ( +
+
+ {index + 1} + ( + ({ + label: item.name, + value: item.name, + }))} + /> + )} + /> + ({ + label: item, + value: item, + }))} + onChange={(value) => { + filtersArray.append({ + id: shortId(), + name: value, + operator: 'is', + value: [], + }); + }} + > +
+ {filtersArray.fields.map((filter, index) => { + return ( +
+ { + filtersArray.remove(index); + }} + onChangeValue={(value) => { + filtersArray.update(index, { + ...filter, + value, + }); + }} + onChangeOperator={(operator) => { + filtersArray.update(index, { + ...filter, + operator, + }); + }} + /> +
+ ); + })} +
+ ); +} diff --git a/apps/dashboard/src/modals/index.tsx b/apps/dashboard/src/modals/index.tsx index 32f8abd4..7a12025c 100644 --- a/apps/dashboard/src/modals/index.tsx +++ b/apps/dashboard/src/modals/index.tsx @@ -74,6 +74,8 @@ const modals = { Testimonial: dynamic(() => import('./Testimonial'), { loading: Loading, }), + AddIntegration: dynamic(() => import('./add-integration')), + AddNotificationRule: dynamic(() => import('./add-notification-rule')), }; export const { diff --git a/apps/dashboard/src/styles/globals.css b/apps/dashboard/src/styles/globals.css index 6317da65..9f5ca539 100644 --- a/apps/dashboard/src/styles/globals.css +++ b/apps/dashboard/src/styles/globals.css @@ -13,6 +13,7 @@ --background: 0 0% 100%; /* #FFFFFF */ --foreground: 222.2 84% 4.9%; /* #0C162A */ + --foregroundish: 226.49 3.06% 22.62%; --card: 0 0% 100%; /* #FFFFFF */ --card-foreground: 222.2 84% 4.9%; /* #0C162A */ @@ -52,6 +53,7 @@ --background: 0 0% 12.02%; /* #1e1e1e */ --foreground: 0 0% 98%; /* #fafafa */ + --foregroundish: 0 0% 79.23%; --card: 0 0% 10%; /* #1a1a1a */ --card-foreground: 0 0% 98%; /* #fafafa */ @@ -108,25 +110,18 @@ @apply overflow-hidden text-ellipsis whitespace-nowrap; } - .shine { - background-repeat: no-repeat; - background-position: -120px -120px, 0 0; - background-image: linear-gradient( - 0 0, - rgba(255, 255, 255, 0.2) 0%, - rgba(255, 255, 255, 0.2) 37%, - rgba(255, 255, 255, 0.8) 45%, - rgba(255, 255, 255, 0) 50% - ); - background-size: 250% 250%, 100% 100%; - transition: background-position 0s ease; + .heading { + @apply text-3xl font-semibold; } - .shine:hover { - background-position: 0 0, 0 0; - transition-duration: 0.5s; + .title { + @apply text-lg font-semibold; } + .subtitle { + @apply text-md font-medium; + } + .card { @apply rounded-md border border-border bg-card; } diff --git a/apps/public/public/clickable-demo.png b/apps/public/public/clickable-demo.png deleted file mode 100644 index fefdb7c936bd8765cf4fccf5eb69f3ba83a7a295..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 18424 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 diff --git a/apps/public/public/clickhouse.png b/apps/public/public/clickhouse.png deleted file mode 100644 index c748d0ed9bde62084f50fbb2e5901e47d9eb86ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1335 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yaTa()7Bet#3xhBt!>l*8o|0J>kxo13G978G?-`+jjA6qPQ{Nww%Gp)BA-ZJme zLf029VT-iG#Dq2~Ds^>mB&tr_5cI#1jZ=H#1~DC*EwN^ri<($6m#=)$mgzNr%jHRC z%DKz;Ugwb(vMYQqIdgU8-mvn|yT4capZjf-b1rl4%XRO?K5TE}`r*#h=%66L!9pR` zG%+gWww+~&e0|A+b7fUgVTTW|jVk}Yb!*w%qgSsi>#ffI{cZ8p)fc}7*#5Xsy0z-- zrmUoYFCWV9*}lBY?_bQ-SDBfy24+G1HdcY!;d*P;`sD3n-|sOE{muU;YHyU;y>;#K zk)hgQCbJU-wu*gz`EIgx-!#UQL9=^I>v%ueG-`oDZTlkZc z-_H8;Y@d|etHSgDChJCjyB_Rc7xs5oU~MUv~Ek6%M{XcZC|?|B_AT z=B#yFwrtkff)5Gr)93wfcI92Nnc?}myu~t@H8n)wdtt`uMSI?~hAsOHZ$ze$7dU zgZXjq_44aiz8_yL5A^)Cw&&{EkDpm*EAC`ZOgJ;|-Pb)I!`+wrUv(0uI0Zo*+roJ1 z&CNakSsM)ujvaSj-Ko7kDvJB;Ykmm{fBVhLzP+8j^5x6PS683bv)*sEr@&j+1gJ2~ zx99rq_xA&vkEn`KnvP*XMvY7%W;!8kwN&;mS#ATT%zOOUM)LeJ9 zT3*cEoWBdQK$-WN(yhO5^|zM3KKN?R+b>)4Qu5k&y?!^f^zT>ku;BQbx*hxeW$m(> zd(Z4>O~m&*)o0&NG`%?Y_f7xpva9A!K3+BN;UCq*Utc5(7qicrn6=2-{0K0MzHZ-Z ye0BBGV)mWK=g-I}Ss)qGEyuxQL}o%hnBREvaL9+dJX?U}9D}E;pUXO@geCwCIQrlK diff --git a/apps/public/public/getdreams.png b/apps/public/public/getdreams.png deleted file mode 100644 index f71225bd25df82b1b0e7fa229f63df2b98926448..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 7194 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 h;XVCtepvrAsYhnqyQNtPjHHt>V{8m(e_>>xB=c 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 O0E)6|GBr}>;r|cEBgTmU diff --git a/apps/public/public/logo-white.png b/apps/public/public/logo-white.png deleted file mode 100644 index 27f53ef76b7c96325b771016f9b28d290460a45a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15797 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 z zTMvS)2WcKn&;-hsn0EU7cJVZjy&9<`p<)TlG+2lw9+U7xutZK)2XwCy_bG19;1HLi z@~^t=MqJufFEu5;!MuEZ`Gp`cI&_W_+OHn2Zw~6vYm*@lXRO1RJ7`PyK_k`rFDj}k zDnmedq*kaCSzX>zxh?0=p>q>h`~7bvTXVjqb9t2s-(>ZiBarq3g;d0M0lv2weP|uj z0kCm~5YUEx5l%=QR8sdpGU&^iGV_(Pb|eqKG@iRb4V&#+HGA(&JYi+r9TuvJbKr~K6E48>bf!glsW&fFzgsNvmZ7hc*Y{%vLf-doQ zfvVqH@W&K}w%#3L7|Tk}M-me02ZR~(=)J>t+@Y?U_!gNVs^t&ucH{7oi8g=6$tPFK z5zs$vl8cg}=+?Ow!b@PpKdX){vtlM~2{a zB(PwoZfNuA>`lERTGw1v?|*}n#FJp8AW{|MX1M!se{09~XKh=A8HnkZj1n~I^IRLB zxZDUHAR@9{53Gb*95Va%jr7tZ2~E^eexX}d5@iwkVH5lX);-rM8Y(=S^Us@&m`;$O zCo^1LMeO&j{>+>1k4STX#5DQRPQ7@;kxDF@(Ua!|1ydZ_G;Vh=0us{b=_t3R0HQmq z!~k9WerpijFPyt8_-J=0qAfayI!#1E!)e<&3NS#_g4&d zl%9gM4Dh; z@=Fx?F;oQ2*Pb}O`#yVO@dW4(B}V5(-}2TeuWWr657DC+*|6x{6iYd~y8R$GQR*2J zX2;^%a#DIgFYdx`$lt{mOy4IeJ`R-|LEqhfK!uzMX1=Sd>m?zEf^!GR+ANJ^%!5B} zethgK-Swf=pRxCOdmROv_ZST6D*VK(uhT5@^bOE~lo1ip4NYq6|4$M&&}lB8{&1bh zeQdH!U|ePgROW(*tu$Y1aHEBNhcbgWzu9t$^4wy$=}N&6#QkymK=AS6bl+Wf1xVi6)&8 zrM&Cb**!8s~tI6=_x|ciJ5bTFl>4gw$bK?f9z35tT6c; z@&eKu^Gf)@kOTIPc`QHgLOq_dEhN!R$2S9fveC*1!uZ z{S2=XR8MsHh2bckHdply#!ATLwPkfgah^H;cuV@O^J3n(_$2tMQ`sj@SVF=e0(VyM z!&=wYj~@z{*lMxlEGKR=oDYmY1S;^Y>9t zjWyc4-Q;pFp(h^cUtF%ev)kaL<07VxQ7AZm$jJ`78B8C>EtBtdZ4JA4DPq36cP*~voz?qY%?4sx1&QWkb zoVf1ejA$$NnnU_nMT0X*DiJ|GUBsJv@4AnS%N7maBh9ZeCHeZu!}2H%G{Y^g0MdXe zKj81P6N6T(g+OA%-_Os-va0|A&jM<8*(vCD>f{V8HK(!u})sU*EB@aB=XjN&G+nqQ+mivZZCwZaL!-sL#Kn( zj`!tq_Tl%NAkURRnTNo8S*Hins@u80?W-OiWgdJJtrve!@cNn29S5CxQkspj%i=xw zZi;^BN!+|feWaJ`eaODH$~k0ekt#!6lNQkTlKLh-*o_2iUSB&T<`j&(bt7o)A6)!a z#wSU&J^*_aahEiRgsT!+bclRkGoE~}F;Pv*@pgG0KK0;%q9#;^L; z*{cgAoH;woRHb$iGRBO?en*QnI)3WkuKn3J&o&E2ckV^`oqC|b=oHy^<5#6~o-&Jp zXL3mwQW~eyOo|No^iT@No*3A;__~SjwpWprXpP4pK3fQO^(Fc9GeuS!Hb>2&ik#pq z=Ir2QL~rSO(2E{kk}nRumC&YL zBnZ+;XhsNOXRf{WUf=rG_pN=-TKmsAmr1yCUCGOv+|TJz0kp|?z{LXKA>h(~^Y+(C{+p+`bm`(FfS&To2Kj!89&nld(iQqk7d-$T0N~QK z|9Nh}zuqogzH;^2bqY!+qwb@U6PqGn-bW9Q%(5EK#?k&{ble3Gfn>)nAKOpdV5H$G3>+p!ks5j9uNy#axY3Ui6S?>#ric1is$g-N+ zI&^(QV^ecicTaC$|EGb$iOH$ynb|LM^BC;vx3%@}8=G7BgTtfalOKeir+>wD32^0q z583|=?C;_t55lF(SFc{VO7T}*mo5j9|E|zqy>?gnI>Q4)3I|`tdor&mnbhAFRCiMG z${OLA9sS0sS@`5I{P@2@`!t!Tq0k*EA#*mfN-;8v*!XJ z()9;5KnU>e@~@{~L-1=B{8|XVu;3RQ{$hk*Z1{@}f3e{&HvGkgzu52>8~$R$Uu^h` z4S%uWFE;$ehQHYG7aRU!!(VLpiw%FV;V(A)#fHDw@c-9rxJeXVsbYs7n&{JgTw=g0 z#vM0Z-#CI(yQ@^aLF+K(T-OI_)wQb!H`q zCf&jDOpiVerI{33tAc}aJ$EpD{P79iVr$<+33&ghzWF9NPEhd&k3aMZiAwJRaP1Ny zuKo}Ls`ZYO}qP|jA*g2NS7Yn^0b^fys`uhXIRUoNJgpg_L$SY-#%rl#_&&DZni!QM$|4i z{*i?Br+iDJe)9tRY0Z@4^U0mD>+sGFQ2q7BKWzvt{T)|eH?Br!KK+KLiv{XbEL0n* z->-bTf_Xm>*;T876}-Y%Nqj<*&p8JX?|d8#IEaf%kR9lqXD&>4QGQ1KzIocT_dAbm zLOR4iB6efJVOj5w+Zgs7l7A(=wN1N zyxXn=!V;vq3sc3t_-<_csc+$2537)59zQplwYXY366%{l8i(;&Cm3Ti1!^|UrB|sf zTpQXp92$dI^6D2Dn)3}JPi1EyKsVOO*W0uTdLpPbqe?fe3xGa%Edt)zoKNXUbzX6% zP1J7Vb;}CazfN${ql@UtzBWRtUfY9bDzZP9{IulobNd?}RHqeNJaTMsgWwlQ?64g5 z2b!XP1h=Kq^2nzpEVhoB4z^8xK^%Duq_#CMt|!lv#;%1Izm#u~*PUru2oaZGT(|(d z2?Dt-pGxMlT>x;_EBPc7b2_4lY7gD%okrCQ0JVPy>8aucAo?k0>{Q6VgUI^F1z_zu zxZnbCeH+I4r_Oo$nI=)cR?s^skd6v!ZA5kO$I_FA#tGPByXEtY=Sa#R`qmSN3xInf zTYvht9T#rI(>{OeXFVixeHLsFF<>2XpHn^jy8Zr#nY>zjFmFc<_UR1I(5@6zw|>(4 zU@-o8cgUrrCC?H|M^F^Nn0~`m?YhxcysjDR;kll7ZSl%2d!xxv{>2>WFGV*5MXo5h zeTsVtwm;L@tS`(W7$~96rfWzt@mgMzPZb0skz42>Xj?#hYEFJ~^r`HS`RmoIXAc>Z zl(s96A+AXW_MMaFLDh%(aiEoDp7}{_ojo8AUUkJj&LNsTX=2*~kBF3yO}gx*7ZQ?x zeaq@MSEoAy+jjLDCYnoqnid@zgCKXo@Gnj3dEVVX|Kw3GVbkK*1GT{0{K0mnB1WS= zU+R=rr)V4Cb`EaUS`tQi~XMx;9-`5LPHq`Le?g@p54sx@H!?(9!=U%nW>5 z*RD)>eF35pj-anEWqtF=PO>s!&v@d#)~x7?iO(_CZ}3#a zTPD8F<8^7B$iHdcJ86!7)I$Hd|IqrUJ7#-~d2>WveA#mYCcFxdgwvDQw*<8biMR_u z-!k3tlU?>npf^T8s4MGDfLLYZkllw5O*bRhWHM@sEpAaNolo*%?YOWupc#Vy8yWDO zF1ey=O99V6Mtt^UbdVuWue~_hnbcSFl$&nQ`sj4fIwp$T@dvi>q?@(a4e`A0d9r2u ztVx<_b8u^PtDoL(?jsZ6@ulCc0scSxMA1T|KhNlJ=a~nYsVH{Rg)hkvqI|jDbE0;6 z^bcV7O9rj@Y03#z#!21X9D=evK_2sMNxMEjztGTyuX-~!Ug?e#oK;$ml?mlW2pvuo z#PuAmvt<-3!njQ%;$dT)IK4*#47JVZD(9EiHM&~0dZybF9wx8)3kwdZq(J;INKl|v zy#xvoQGnMXOcxZk&*#^7T!ODGEyuLqiyX2zf8Q<%4Z-x*y%#RQ1$Cb>l0Q_;o7=gr z1Ak+u**r2JcEkCu<g@fu%t<{% zT!o^VXxjETU&}m+@YUyq(Rl`zw*C)162C%2HdeE-CZfI){*gm>c7>nRY{Bh%Eh`+1 z84bs~#*wUr0fq6eXC^=nBwBbU8GxURz4o0q5bQF>JK5G#bJ`OI%0^Zn`Zqv8?zl3I zVi?HBHpm(Jn@(NEyk=Y~sDIJzs1v^$=VC^W?d{6os93nv`NhI{Y4C@A%ZLlmEHn$e ze4x12Y-DIu3xa$ivf$469w!f60MK}PG@kvddBS&eyVyOGn%(IOz~X*Il^5OkJ`EJq zrFMP7;WLmK<9!nfQm?-NB)(+rnit5#oD0wj8GhTG*RLqfu@@;L{#i8$*tiB)cFpYb z&U5^pI9mItU#Z`OB%mnQ@90l!t8e`Suhfi2>6tCJDWaa9D^i7Zt_bzhz3OuKU;!n{ zNu>xoZ1f{qbmQ(`E6MtOV|!FZ{of^aw7LGjeQq@ANGLg!aAbG`l@H9%Mqg2CUPVQu2B5W zUgBg=7u9Q&N^453l*MS4%W+g-l`;!4MzX8xP0Jq6t|}54oKENnleZUV$%rJE7?t&P zV6pu!;eoGUcT0<#nnO_R!wDvWNH9NyW`O?!5K(2DYuezvD-3CQ)QV6r0mrkPvDVzpX6n2d>FUic>$oS?Nv7UOHG16fho{lrj~)7te_RgzT#lgM@oDGt0gFnM?v=T5@xDn= zrTn%#xR$h@TI%>=s;~-(T>lxNXW13|f+x7%3Kq>IG4|Nd_u7ami~Zp5TR{r@^|OCMtq3gCnMi*L zGN)b>EkQPyl{FiM+eHlroiSP1!6P1g5$yO%g7 zywM}xCe1#&3!A5Qx)hYT(SUiO$Oerlzs;vA{l)7kTK zn?`YEU^~Ou=Tqkc=b0CPl`wdnV9$`PY)=PIt92pl8g6$v-EWCCat6c!)61Hb*1p#~ zS-xLRl*iP!HMm-=waudSjUQz=8R0%9Db$0ym-x?tL^VuSV|l|LemQR3u5wMt@`*r~ zWS=RKc|*VAHx=sjE}0_o41V!k*I8*|A{(C$rRlyu-;QWcglT1|r-{C)PEiWEp{lp_ zHmu(@DKcRaLH%bJ#s>Q}2hUtKk&gFi01NI$buJU`{n)vUE14fT_KxDH>OFt4s5!CS z!3@+Y^%#&FLKj^CVw@hT@!*2%A(w=&ExiuD*Ymrv%_3~{0?=#Ya5Rcvjwze50P}X? zA}<#vB~V1v?5Ht3e%+i%6kbhp$S#W!_Xnq`#o43-g(t@)zslHO1*#45Qdo2Wwu^TamkeVH&auxdI11bC8rb%E9*4r>JJ(vKQl@W@@hbNZ7oc< zS#B5o^$BJ=?n%~s&3rz2f&^n+OW)@q?^RIb(j|NVgl}UBx`}^c$jiQ!KQQXI5*r zp4t$!((A%*rmVE;H{8L_8q&`k*6qmHRuyQKGT9;K9XeE_#!aXo!(i(9t5C7-7qcCg zLvLYCVAYs64aFYH7puOgE9Zd#|Z8+kG7V`xlQqs%VLvn6VNcX%WA~enJ zJ}zgY3>cC)=2MpwjI%M-y3r%mqX}$Zd-_~0=P;$H5TYCDrjBaOuUijNqfx|#bPr?n z$D%&`dN}Ma#sOe3`djLmOM3i@lj8q5{C+el_Vi=2BedN(>Up_?Ft$}ZMqeEtdrz$$w6-k zRSf4`zID9D^j(?5(&I6H3AWjNO0Y*s>##cf5$~}T+GVe_ubz{_0P9!5>>Nr7b0W~= zV^3gs=OqflByPKx=Vq%OYu4vEXsm&|I9e5Iqrrdj8uf8CVFVw3<^!3;ZpU|+Y69g^1Flf%m`I+;m750pAC9C^s7dm8fezHVLixPI+` z=QbVJKNl8Nmm`;HLV1Y}c+;p?Bo-)d&k_j#tO!0X$3wlzn$`RBjZ9HP5dFNyy*bFq z!t}np=xoD>npj!e#ct}_M2cvo!YCz>NL8LMI4I4T9n2p3gC}cMXaz?cQRFaLGcydV zW)_>~l4mw*gM_3YN4+#omF#XCxl`w%1gC}r>?)}ToMx)-($F_zan|bdY;hV_olLvW5@(AcBqB9 zas7bhVC^sL*5 zM?0%{IDhu)#)!XGI;&Vhxh(?H>o;^t7a5umTrVM44K&W07=#Q6>q!i)(mqob$laXo z)z!Nem1CQ`Q4Zty;mulho<$(9l&i1LU$rwna5r8lM)bV2`XZTGNwl0s({YZ$1}T?= z-r49tR&wh7hCDIW78$G3IBBA1Z@yj!H=(B3R6-urouONCumUxpiW(69Y)~xrQfX#% zr&*<`!cua5>byk|=h;1+CsnUOJ6h)j)AY3}Q8VS(1_BqQRE{5aJy7faYR5!S#6^Ad z>GxWJN5F3o*}bV#VwSt|C~*Bz4$;(NF1)eY6FaLKLS+%AzX_EUJ-K8bvEJ&`JGZlD z4#ZaSj(X@H>Xvxsxdz95oAd)4_hL~xXo0oo3oYKyUy|tf2WkkVD>6B?SJ+VGtIK91 zXM`?K<#?=P$?r05arE5OMdYK*2a&;#%r3lFZ$a-QPy*>Yt8(>Avb^Lb^;P9&M%3a1 z+z{=FCO@>&3AHQmPH&!v{0#~1w_phb0LOcsxK*XM*9`E|amk!b(FFv4ui)1y{?bQ|`47Hl)UV z@&oosC`an2Iu(i{wb;|2;H4o;n`Kk3xj&0TE&#H7V_|9%@J`N-C-9h)hR6ks{8(4p zvkk>`EKm5--Kkbt`E0Hy3sArFy=GndRB^REGGi?|*Vp z9m*=Cs_g2N;<&XU`{<^qWS`-4WMtoC zqu49ZxkRcEIUwp%lUR(F_AsSAVI&lQ>uVPNLbV7Qr0D zBEXyDk4}y>y(ue>eRdF zCKBkk(}wYzlOM@m2NG;3#E%1>NH)5quO9V?OfHQzhoHQN^e zcEQ&3LKuXj@TZzUjS59~yb8QkKRryWYM*aZP4^vI=y={j41iD2@9W$ z?Y${`(s=VGLaVzxO}`RW(=ba&q`y&Nty-lX=My~#78U7yQq8mwuomC*5Wcy`gkuap zP9l=1m2KOT(K%BGN z>Ut*YTZDTk&%{oh7q3B-ueZRE{^Yy*8g#6`Y_QKmj%R$Ue6#pUhm3XhdYN{Itkvqw zG!UZ%46G{EYu@K1%~)e|j91E>sxULt@i)oIO)>|0bz?q!QIYC)kUElTe4&??N_$@< zKxi_4-`%K~t&4i^B+jU**2Wd{&uRtJw(6Rx3JEcJPF3qO&c-a77SRQ0Wrf5O->69J zd_WTWd*5!YdoofhudT+btZ$g*@zPmUa<>T!PgH0YZJHJWfm{Se=3)S7<7Q@#(z$%HK(zVt(fGb?zf2*{IXPW8;7ha*4y0)TNR zidP>Z$VG5AZi42kr~c#)@6dT+Z!<*_Py<6Jz{uX#HYHYqTa+C*@ibtt|F8(308zE4 zsxd*Uj-Phi$cC2@4egV7w#FEy3d4G^qJu+oYsyieljq)JYTgc7bXM#soBi{qL+*ku z#Lhl$dOVym#75tS=cL{kd_bBWTva(jPFfv0Ns*P|PCrzL&J;pS%=xE7bm=ij-t+Vk zvGt#)G=GZ+bmQY1TAKy<~?uE0+=cz3ZsgHUX6{nN z0<{5`u}hrf+Dt2c#>>qJo#sg+{_y8QKfEaFI`nRO2(57fyw76rW{GK62Jn&a>{@D* zbkpcvP1In`Y{(>1Mc!9wK#-Q3qx`O2J{Y=^Ww5`nqiv4d7gZo!S$V^B9oZ*q85Jo|xs=d9?ND8HwG(sGg2n=-&Ei zwL667-|sefhTc>7teqx$=TN-vw!m%0?h_fgWGjfTje$S#0-y~^_RkXw4x z!?bf8DZJJSdKDdtZn zomX+9U=gMA!RC0Iezuv09>-wEX}RMB?l_18vVKvh&nTt%I6n08OX$DpnA{q8#EK$W z^qA-EOb-PR##iX*pcjB{R&}$OS?gN!wuxuOaXo<`I?r2df$*Cio9x2e6$Q%{9#rhm zi2h{0w;i)S_T0YksLT;^?*Cer*nMYsQwcfUY37k2k>WCP{Ir`w9w^}SI;CyXL3&CQ zvzU}$`#sLN_32?$jUE5TF^Msm9R;WG-1!KoU4ZL!TO~(bu%`ox|9ypeu}ZY9zh8a+ z0oez*c8iSbK-ll+>64ZOG)^9!7z4kAO~Rg;e6Y6kZodPQtX&39D8BI$s~Cj7?owbG zV)thIqD4DHU;e%41vuB!qj{!~RDaGLngLYBhIIPmbBXcL_%0TLBUxHd)LfvR;|fX* zmub(b^`S$LW3Gy1EWeU@a@bHcGzM))Lr3(-8idWIQ;YoUd$3#kj0bEgGWj_5QNKvU zngCu3!RaU!u?;%st)LyHZatAdOeG3dk9S#Ym6x)Y8>al6Hpa-ANOxVujX}L)Zv`?2MC=X0m3j5mdO@^9$u42{L81%5>H#}|Fi!X4`r>&X`D>Xy(C^hp(F8O8W^kBsW( zir`Z|e2E%>7*!Z=jT0NIwipk5{%ySf7RLVzhWP#clNaZiYKo+(C55Pu_E%vPWrS|? z>r47pBtC1c(jItYU+?$3~{UrtXi8Q9K2hcIL6h z1|F`CyF=Sfo*|X!-#z3$;W>My#k)*TnYQope9R3_vD8{aFXzXzR{+6Jq z-yA+B_|tq^rW`7Np2)ri$n{jTWshB9s+{b((bQ7eLVPyI^_* zSUUVJTi2~{69-BSvT7V>WEf|pD@~TobS&~}h5nDE58?qm-6!zc?_kdKx>1A{47wQc z(LT=HRd0--4U|O45_o&kV;NH$@aC^p8qt##`iqT+$NRJrsbzdM_eq5mZyxEn2p*GJ z$o>(WzEv)dQ3$Jxh917&^cyFQ4dbbmm#j4g>`F}Yz_~UW_AWFBPh=Hiy9T9Aq>g2d z1+74__>M4`Y_D`h+|b>d+@px#b2ln5f!1W_e9`W*OoY&3M=X8BiM;W%Vr}eGWXHFK zlj8F@5{T5_F0RH#7+V>NSLxbY*9I!D=H4+t5@>oixk7#6bkI2KPTB?nSF?IUv_?r z0ee&)?-kq4hs=g0=?4@nK3eoXcCuSd>(@{IPE5|wk^ zL0p!cEYGdlo!h!iYS+t5rJ59O8s*aMF3zxm16IwL@1{SggYM{aw2$}Sh$>$@-H!-m zb5z>DE%r=#s@rGD-b$)(Hmb$?wyn?LdbYx@4A|B;T?@VB z^9n^|#;~eqp1Jqt-d8tFN$?PiH|>u~5~G-^LvQ153dk!RGDpVw$1UZHZQ>#sSvBQ_ z`{MUGavsEVPenWhI_CnLBdnm!Wu{vWn^hY;lt6x%feKKs$0P^(BGn9MA#5iz>S>rA zm$U1})tqWE$jnh!ZOSVmjaHgUCUuKFeOfmpdS6k$7Cm{it2VRL*VWaxB&sLMDXN`B zW38%c!Js5w;q@*-$!ixAe=m|xdsRaN08gCWp6QvXi0+v|z3FijnW}ozV~$ioqh?Tw zMim~@9?IYftJ)NVQPG<4sGnc5S3TU_7(x9wLgaCTN&to$$ka3j-b+7C1oY{~E<3Qv09!nf}iDZN)iF@{FdxY0|Ts}le{)VX81e%f$-9Cxt zepmPt%qV{~nJ`6Lby~(XMR04&qE*^~p@Lj_r9OOMCrZg5Lv}3$$o@p|o;N9GPi1Q-U3|ZoqNx!on&?`!Q;5Iye7%EI{Z3NHke5bPd+l5V;VfT+4Fr3__KV!^u`hB#jVrcwY>rrEhcXt$4EYd15g}cHHP?LOd6=wr@XFu ze*~|#-Qy%8@#sF&VnIZw1Ekc!hfU>~QNV``6*(WAUrM1tmVZh|jm)DcA5 zkB4w3Xp_?ZC8R=9s@1XcnmN54MB>FXdD$fC3=Z&b9Unwh{x3%R&wF(!Zj&K2s{J|v zg}LGV5ySu#d`DRB%;QC$aicLE43L@Al2kZ#^9W5FdQd50YXYlOC3m(#Yu+4q#PVZC zk>y7kZj7h;Pv4utT}|yYAWZ85Pm-H*puJbvaLuB|BMFi4MH_)aqnzYak-9Y%r5I-A zQmhcTnEGhT5UHdWD?Q}H-cH?ao4JA`5N`@_rQ#aP3{jfzn^UaglIt*h2yl^?N+2&s z${Ks0jQyYik%^T=nU38LU$rANo$<|YtAPlf6!Y@>ZW!6NzH0oHPKAi|yyv*ON|z4& zY?Ro&18n3-{A|3~?zGh^i9tU-s|d8!Fn+dKmEvpt(I-9D)F+sN!C(&K1G7j4azDs5 zt-E_IOG5~AAlBE~RID;I=Zr2>Kwbb`ml9aupW1n$%yENi)j2)^qU*{w!DX`p3#m{0 zuh0w{ofZZmGaQg8q0YnA0;ZgHG-TR-to&^&7_Xbi^|nQe&ret@{&Q+Tz!P>`3uc!m z)PFtz#9RP=D-;@(PH`?~=NB3jR~i(z)fIQ2v??KUHe9)Gl1z7Aa?@F!npq+8)3hab z{*$$LWfMNfc;bk!+6}>f@65jnV_u(YZvkdS#lZAW*u8gJnO7>r`X%4MOp(7QUUd}} za*i3(Z)j|(i8aGtFDP_3{Ms22Mki*ofNVVH36_J~uwE}v@a|3uAmyF4aWqG|)b!B5_ zULoEw+YW0I&e}YMR{irWnpBIj1D@tpBX!?1@#+EqYIW#s+P{sL@a=8Y zi%$cojN((JGY0qY2v$W8i>LB}!eVN4R2s(JPjH`M88CSflY+fYo{e_Bhx!^Eo|A@3$oSY9@rn~b!pTP&L?o0ZqC%~>l?)UC?8|J#2Ub&+L@IgjV&SFbs?e-ZWS>Ck`u+=@D1k)`Aw^_Omf1nY)PXu>OWkD4 zm@LYggipp{lysB^8UBSylE}UkC}NBt(y=1~cj8H;30zqa?*F(S%QJ6xm*6R?Ito+` zhVWP875{kl>~@wL{N|$lyv@esqRU>ZDsuB}Tkl?h14=i4&wkvxUw&2bvWn%1hj)UK z!PXp{p_kv_Of%kn%F^|vZjMoyWxNVAWc9AW*WwIJwT#IIW*hV50`RKUW`*vU_YD(H zD*fc9@2;=1wb#(rAn_9pYx`ygChSrw@Tmg$a5bdVZQ`hK zJr-rFvS5e5fktmZU#x%a$Zt>LmUn6s3y_t(B7vSCW8J~1D$Bjet5l_t5 zHEYQMVK7^{(YF&*UuG%xmsTlu4&e9aoZZ~lQ2j71JqkmUNP|Bf_qkbQ2us+=V)?g?@+w;)~_N`v+bSh!>O316plzyG7Z%OZF>E4jE3hHqT{chw;$3P^Sz>PbcXw zi?)`tJy^U&Ruamd{!NA9V4%k|QW)%??3a@5mA|3ML-&v>sxYE!91+#+fHW>pk6;rC zC><|mrViNb64sOoH;Qvi(h=ok?tA(l;GcpAPw`Rh76Gx6&jeG(pF}ue?XMHYG}=D{ zH43(*qs6O^76@@hCD5d$brxZNr;ulOJIId@3(J=J*S@yO3RZXjBzbquEt&UNURP9Vm*unimez}8wT-96d+ zq~3 zAPxcolNi01CMnyKcX?W(5~3?e)&_?0bAq!EBwM!o;1-2%?e9L zcXP61rb>LUvS@I{S3p#-?7KOI{BbLaRjw*#Xlv9Xr39PuvW;CG3R5?X2}J7ph7}v- zl4Ppp4Bq@VRUT@<#Q4!m`yQ40%y%0cc7>ImS<~kwsf8QO_v1$qD3AGE;-;q)0+JP& zV!UL(ytFuUyRA64TxQW`?A}9KZ{D)Jr>?S8=@@AxVyNhiS07hOO?#cB8wZa*e0e0m z63%bdF39+%LddW{Jzi+A*lOC1Isn5C_A}Ja8ZWeFv`XWl)2)DMy!f+NGlIh(Wrem% zdW_Ib6%qy)>kO23-HJ^qnuziGLizw&$y&x~>N8@>pdqV>UElYcBtAmSwY*w#^T+Gi zP}{%k)GL0K65@d%PBA|n2P~Yq=c>WYNbGiZ^VOE3CXT-H`UCflHNgpk&GdMzd4`W- zc;lj`b$3HW*r-xhqC9OssALjhej}+#Qz@=iB)=7r08VpCj4w3Uf@&EljcqT}5a|n2 z%N5{#a>CeJ(AU{c=UVh51zWzK1?F7I0eFkv!gz|Y$Zpl95Rb*ms3Y~VvI3K9rk6}j z0A=r<04M;LK3@4liJ25!zu@)P7)RlxyDn{Zz zZIqTy4HgyKQ|Xjtnyhk2O7xQGutBfcZVJc4wxb*#ien`c(ANeYN{)P8EK}K%Leq8y zwfcHYZR>sQ?QJ@DO~kLQ+Fyg#V#Z>D4_54NL*XX4syLu%nXUWMUEE2n$H|MdRHSA5 zz;5LeIDz)h5zhNwwbAViq0+DUC81B6W*gz(o#53^62${wKbfuCk2e!#o-vJ!2@}aP*m8!I;HcsFI8O zvsHfRs5tnhVUl-x)uV6!d=b&@ERZOD&jc2T?C06ps~yn+RlHc9-Rt$I=H4Y6S&K zrDAl03S+Qwzu!HtuzH`6j7;rM77BL@cEB^$l6OihOm&>>|Iy0Bl^6+{p*7{U8G1A2 zX`HeO86SiU+8c*^mpjG6zRW3Yn&sd|mgoAG8>Wexp?m)UcK@1EFzdf|@0_QJfjsx; zLa?k|sr6`Y_D;780F`&wx%!G`d-}H;UN}Ihc`x=I!}wjo$;F7`LY&y+{13*JOyw{V1>D4iu-z_mB%8X^#+Gm zDHuWoy=mn)h$9%w3^F~zk4MeQl-1OnSf+8vq+T073YVrDqq4F(81FGjvKg=@{;U$) zt2=8!(QF5kdpDoC)wb3(#TPW9o9j~x8XNKiFJZH&-eS`fFTI^k{o@Wfd3|Rkk8~88 zs8TNyeOv`*E}cg3s{}$!VscedzO$?fffN{WccQ2z?SA?B-kh%!$*Ek zyw5P{1yWO|BoB#_LA(F~C zzc!3ApQ+$Y*T#*^y=B0#c3Fv~&0AaPMKnxjTjtVpAJ~vF39bub-hx(PD}Cd-9CM$s zV+K>9A(0N$IfP)0lxZ=otD9b$5kfHA$;p@rrM_1}|72y2Uz6nmpbivVA>g7YS4z!_ z3bI^%BVDovy3@+4?5M`OMmP1vZc}jBBoRDS4s?`jkDhnrBu@TAai4AgL=a2g$@MzZ zA`Q&{h&#HkAWRYG@V9<9@II=SJGZ8NwDGymw^xbvu>TkVcX7jDT7Z0r%qBagD|j`G*hKwlGKJ3ScM zn#qn6TJeA60$HBZTuva&b>gwlZuK~QI7+FtyB+^@l8T>m3K;?S#-=}B)a7zF47jN@~!+5cYJkY`iIPRYU3WimZGF;H(s;*8WIC`K} zvCGjGsVJgE5=BLo=F)a6#uuuvQG!M4?UU=L_y^*K?^ntwc__nwP(-vdtPmvPfy)Xe z)vX40X|-l~JVFuL^Cg_uk4>c+Vtsm6gOb(@BiG(-jWM3F+V@B8{vWmKzo#aO^&ldX zIf<$bw|%mV1D;l9-%MRuP9$+oE<$;*l5?%4$L_l`I(Kp230X6OP2?W2U|y!W_Jh4Z z5RL7?d0}yJZ{b$6MD@AkQP8QDFKd|c8c;$2TK@eHSuu6eB4*ihM2xRYsV}(i_p$-w zZr1?E{m{yy76tg}eAy|~z%N^w_M_`ytpH6fIM#G$ZsosvAzL`+n#B+ZuHO?-cZWPBvLVC!u}7OPOL z^;c^HP91Wgl@(F`=hEhj;<#Ck&|(K5YrQ$+f<9L~>8nozv(E*9;~Tp-Bd#41IV$sL zCafx~-@$oNpb}%0YN4Z9$-@|hV*lLh;MtYOd&7=dXqMbAo??y*^-ghBW^sxhtbNLF z!vdl(o}2BSHcPQhj$7qh6fr_xFEc`cjSyxzg(`k2&e@0ar&mV)2~nL?&Glo*{l)!b zi6F}M8<0NjH9{g;T)6?Y+uM?|tlQ|R?uu>^kY<@S$=^QLCDOQCNk+o%J6unF2ldyP z3=pT&L(|qbKYLqb*^St&XBiwG{$M$0-R({3>Pp}Dt{ON?Twle18J@fyKYU25@yHG& z-HY}8vSVtNYXg>LwSvZh?-l9@lD)8Gvibxzdaiu|c;rKPq4elJtprDoI|ereao8}5 z_z5vAq$K+ViY;s5ylklYowpS>k)8iIzvAigCbBTDkh`}*&+Mc1ismZPBS_*_n;q;h z)vJ$tnWTBxbV9tbrG?f|>?>wia3P`0;;#1iyXw3V{zJB(hwx_m-){nu{dN>S3%xUe zhhRoj^vYtNU(R=BxKm&JqcXohev$arSk}}&e?N~vjAegn=pN5UH8~$56rNbCp-G*` zs1zZ#e}gG0=5#y97a~;5)KMpdU35(HMFqwlqtKwLly6tKjzI@kNwCa7y?j+`bh4T+=&41{J z(j8^24(8n^i1#(Evy_*vXAED)>}2`)nmS~<9a;uRGv!Ps)_rt9Io7fv*zLExEb|MX zmfho6JLOLwnBO773*H2$Dah#eSv+l)WY{!FMGU<`QCql`HPp9_9RaFvWM|{|u)q1f zfAhl>_wpzu-et|NvsH>$-b_oXczt$x0mXt)`-s^;|cB=75F8A8en7>son$*C&^wbI4 zvY4i?#O6?9@ATv!sjlR6bua)$RzhWvN>hF7P5hLuH3@G?^FwaNqN6#E3n_QSCa=Zj z3Jo-g7$f%e<(1*tz@R_jC@$N4=eM!(wA^Y%A0Ye3TqaJjA4^VB(iqy<28xoi(qdf> z+Cqz9fBw6S{ajsgS(9mDOH>TW|fT=iPolj`6!r)KjL+ccG=tUz0(o2 z5{y)l2$3)3HP&XSnajZ>tR5&bl~YKmE@1=_82LbWL1;emX|$4;X3NpH3XnPy>M|9X zsW8)JLK-2M*2p#;Kbxj-lTK0U+&K!~DGFc7oHy8kuWYCZ!^|g+Dy&bE@sW9hvbrur9I^Wp!3u|_Gb^l zK0?}}Trl#d+T9Sg5Wzr)Q$I9&NEfl<3n_*t;POr)H&t$IatcF@fF z(bXKkSbM+tvt?DQIMP-R1%cde8$}m++E2i>42r-ZE1~cm(KFcWABSOPZurR^x8pB@)z9iB{G-S4TmZ)O(3 zN0K^yj_0bezp`Yk>mLsm!XJ}i(jfmNTrBh;vPtA zr`oCU$97VjjUTL#Cq6bc7iAvcw;@zvuUOkRlk|eGK-?Gt+Ewr9oji;CXFV2l>p`TO zVgz5VW{RCj{PU&DHD5ePDg@th+pB{k+!ZcG(Cls>V0L8p(|O65Z_$?6V!5M0Nm z8VI8KE*)PCu6IZ?OKs=3V)C_cZfuk|-+oz8kv&1t9MeYj zgnJC;wXk1@rLz;sdtN(1bak1zhB;O7$MN<(9ACmQdA!sFIt@!}=_tKkjl9+s)4?_6Arb3{^ zWrC6-rcUkld1)vsOt-H7;0_@bmmseCI7KU>6;JU;>qDgFSDVZr*g;j)@ji`ZjTg6# z*`M|6htCnGKhHo)Nx_K`M8obt5C17lyEwU1nN_q|IrL>Z24z%K=ymUq_9nzqmvn^E zxAa@G?*nNs)+jv_9rV}MN>eV;=Lo#r4%D=dMp2EP+fU=!wB@~+?(OX(R=nUZ^2h#5 zc=E4U1GllqX?736={EAwiTxlt6ww%eKm0{h7l_8=&*2)P3a%dMj8A0pNK=&V zxr;|cmsjTRJl5k%g8FwK^ab^!wtm#l8~hYJ>Oa*r!IpP(VzjQf-teinw6h2mACank z9Yyb&VTDf~l0t6|+WeHTv`RWTj4R~omx@al`;eh4FAe{)L`AUWD!lP9_K>JT-qMqW zOvZ@x=~}unR4BBg&2o~~`W=xY2O1@~#^-2Llu10#hHl4L;i=N!&a8iuw*JFIh_2*z z1T?%;ai_zR2Vg+{s(*S8P?>wtqbEsMLs2yonMbya>dJuCb1t*vB zH90SP20)aNv2}RxKdJy{lw7Gi>flLIN`!)orlAE+K-1}xC_p` zxupt~fm%e7PMV`Ek8!-IS`H{o0>-df#)Yv!|C&g#i0R7~TU9}f@MBs>wc2&^P zrJAk?jQzd~@wdA0qc)~Bal$V}f^9P9)>hb>bvT&=s~@@!6FmI{n>EbLx@ZGh-}HHZ z=+o=iz(i1XA)^x0&yk{r4!k-g38*TJ?d3gkSO6B>ghS`T- zyy$6N6(CW-?);b{Zhbj>=ce)dwYKQ#6WC!-^3$K!8uINVSuQ{$2r-;H?N^h>QPuCK zut#&d{fs~-`aX(=thv75y=>n}SIk)1?Tg2H+v9H>$8Mk6Rgv_}2(@H+;*nU2Bgzd< zpHeU+Rm09|*$I_%ac;-^%QSl;QwAr8k!p*Q(Tw(A!o?k{cVA7KaB)6haMx{Bx?1#h zWhDost{#H&Vb|Apzj%XxyGmrTE?K_sY7mOiug&V*C`VNv;M0ZI@ULFQ9Kq!JBI`!n zF4Vd!2S2Z!yYQm+<^Ga+st`3zei$c1vJkILQ&OXS>R!>8Al+ai?;p51psClq)Sey3 zE{9`;#HoB=Rq^ubthusEYMxi9mQMQZ`M$mPW#0F_7<~YV4#3%n*3AD-6!bT%g{X#z zY7kkV7FC97vH9}V~$UybVyb?7Voq$xltVyy+qV{ zGDejW1Ch7uZ;xxYL??WByY~@)qt!0UdBj&5X(C zy`3@`{ax4!Bp$LAJQ7vvU!dC1Z5g1ZpHScZf5mwJo6qRhL7Dyl@SFY*F@ate*G8qM zRs%|L$(s}{()90rVuUCr=0?THM&m)mG3O%dtE1Kgi-qZTBu_yx=8#p<^1S_%qsc{0 zi91;vygQd``xsT~-=1%ol>B%jcIrBgOz0?^eF+P7KX^J$KMKLBuNSD)dp^uKH!P1&mt(NFv5jp~ zZcs!M(OC{<(|tSoiwz51pCr1-QG&2enB4f*!dn5{cdDV#w&n zs<}?PCsH4Nf;MWW;H&~lEwZ5D*PxGqhJpgmW*uq`0a{Xa(fCO?mK=jE_47xpKh6R> z&l{X&p`(nVvT$IJh95?4DYio!sTvfcA<*H+^mF)4x5Ws=hX8x0HOChqgwn7fnYSaj7RLsp(Use=p?v%2Xrj7o&w~yVeODTngJ_AzwbjtJ?f#qdY5$H0`_R= z*r8TQ>I>xZqaV6EZ$MdTiONk)%UCx+gYd*g907hQ=oTbJ<-^VxHB4@u?|?SBk3d4Q zZ~@}ee|O{1^vU5&J}=NouA;N=5SI~U&w^jo`ccZI>cw_#S|=||1mZAN*sN#2LC5?lH4mXq?0 zR_SK(JsD!^bYYF_`2$&+1r2zf&}PlJ1^hhw#ux&}F>#&%_}2AS$C`}h8VNo=(*gDR zWyPk|%_RLT8O-w3JhQX8;hVf>BUWU0I3CdeI37pH4pW$?8B81KGKu|U#>-_yI3TzA zoK%unfQIKAM^^QdNS8sc@CBFhNKbk(@}h%t13fC zF)j8!TgnyI(T!Rui6e$(4tOFlE_eAnT9G9u;3GauK$kHfQ-w)G`2Nep9fjCgj>Ews zA7{4h*n&UKlF`=hHU=>5Om={OWYzn@F$y$Ho6i+lRTCnZo`L(uhN;uw*cbv@z;YELRO4Z)sKHpBN^6@ZfuyymwL@|Zrh&k&=I=ZF0 zB-h^SGf`KTpbu)nyK{)zaoC(ss$p~clFY^~tJp7cxvxOSsnDKY{=LldhC{|j6rdt) zDGXt4FeB$0kxX{%iSWK57K#}$qp|YLCojx?M|N+PQ8TNXqV5gw8R9^P!CiC8Wu6$V za8ud)6~Q`#_XiWH-*%8_+#jS|Tn9Gv%-8NNqPT)C+iclLbbo2>C1H;O^GE?99R)u- zof|E;c*mx>yd16hjL{LN`P3;y61RWxNv6i6)VG$KQi}`IQoe@eTV#gVmfGn#HfWx>SmpY0)Z3rloGQj$%Y zCU@nFJojjfa&cUHG-rUkf;uHFoduX5Z+}1_PYjJ-Va|dm&xgCHabKdTMziW`kD&vI z?k9icNFjM7(cPO8ix`fDTd*O>ooKQjwg)>5dVBcfLa|$C0hgL;dWY_tM6U&`Y6+T! zZ4b8q=k{)XJr=cCObPp^r@8O1PW9(+LXH_k)r>EdFlX16FM@;Ol=C=+FtoDIItT54 zZB=~-6{O4&68PPpqHNzc&$DQHjBhA_#i%k$pnezWw_n~HUlQR-OH$E^%ea8*{%Kg zM*sd7-5i#}CbA`6L460PbdSAtyJd>K0IC2mDyr_dr3H@FQ86LM;r6@*QMm1sos%xN zLw8Z?i2~VLfyN3ePHgqF`rKL%NaHcP((OFJHV8$ZR6`zLa0_N_XMRq)sTi9KdT9ro zFnRWGh1T<{`T1SS7OM<1ww&*^fo=uDIYHQ((6wJZQr?0^msu-Au`T|(pPpcoV!2~O z%GUNn=bJc$rZ9_tobBctYe$XsJ_oQX)|*6+weBai9`61`(mwnK0EpsZjdL+_`+Bds z8TI^OcgyH+HBfJB`OlK6e4GvL22ZE@vq0}8rbA0cJW4%I_a$%c^rM;*5yy6xj^T!%J?>0h8j|F_q{aDZz$#Y4EH8UHx zr+t_ihg(K^J0$PVEzR8s|1fA2U{vg=EibCs(3eQkhjP#c_3aT=mGn;=d%dJmKZv$m zsPT=(^~4wwOB_e0MXK~1F78S=^AV+YC$3(5em3-&{In{d%^wO0`R&zNL54T^zvaK# z_d5;E0m#2ME=NGmiddbh;D9fQ!7Y@8D*v?{vTwSBB(r{a& zyV2o(bhZN>J3QyLo%libH^QLVWylLZLyA`AQ?kr!TTB%L*-X?Y_2v4%^lAGt@)t*p z19GjLxaM+mzmBM7Fj|7LPSHw5TCVBnYg%2O$3DCl2D|gl-7nP)sphm!{@MWgLMYlN zPt!4lG~19^4T)q+KCYw)?T68h_wVXOEDofUmRWOZY^Zq^=;Qj*95)@hvvkBk@g~)w zFPE)0SKmc4!o-CC`SyEK0L<`xcfarH-qRqzDzDltX zGHuf&(cPqeSBUrYZ`2e(_=`CMdBEZ%kgsJq7o`v*#??;vt??nTq(jj|x@E0eeR-9z z(-YoRtB$X&aGj3Qi=BHhW(#xLin#42em=T^yx2_T^Dvd8X5ctq3*ASLxvDjqL=6D z*WbFbONtv?epX`El=P@5QYGC^HF%gk%byUX>)8TnBo959YI8tc{kDA}Q470l=RI%d z_h~}{h*>r()4WgMnMMw~Hgg^3zrr7jL$H>i*tJet{K0(O9P?D^Sgb~=sEyCd8y_W- zFHa~#($q9Fs$l2Lgy0N$`^^MP2icxGu35jLFP&ZPbLzRr3UT}^7`}~5Ck1*f#8XgK zcqNz9a^KTgWiuljQ1=UN5$WGM*$Wmd_~njHG9afaQ146{hgS-{01*#^=g)ay#8VEK zxx1Z;L!WH|ZjbEIJYDs*#usoj&$*T@K5PnnckN5m@@+6iGILJ&=q{ZuN<)clrK{-aIF z^34lnzF&NA3d`z`^v=#YyiU=U-n@R$HgPTqZL{9-fc1Psr@&z~MizB8HIo(nhj4cUFgI!+FA6XXJ9#(5W^qrNaOFfB z4=%cAnH`f_M`Nxgnh^Mjvrk zFN-zxioH|rV1RBLe5fwmjH6Fh$ezo?3-_5snl+=+(76ra;Z<_dAA7S_JVj5QwvSuS zSvf67Pv8DU<$Y7P*SplBH`T^Z)8B=oK01krNR`Cf@tK((ZTmP?K>O(o@$$@RCa6y1 zb*#3`0O;{Z?^Mpqn2#E{#5K8XR0LW6J**ME+zRm4WFvyAPC=r+PA)I+(Zr-QPVTa}?a{!6(zM%&3jmRw zeGABX#X+_pOZ3HaTLZIc3akEiG7Yqgs4oRue$~`BNJm7#;k`u$$Cw3g zQ=u>8OLcziG3+zJ}A?<^}Td$uXUK-@#8>SH=n#at8FV) zn_r}qo~F)d(j~bbMJtfoDx^B_)SaBnR(yP6B6mQ@hClx0$j-z;e5*T(ZKnEE_E#Iy zvxEzJIveX0MRdfy>KMmk=!Zr;I{Iag%NCm6w+g@^6&H+?o#WHftX1`K*#!K z+`NF0a#0189ks?B3pQ|2|iII90Ba@|8akgAk z`Ew>X|j=WIr00Jac@~yg!0AG7z;*hC|6-D z^V1!+-czy4VF_|4MsgdulTO_Z(YEz^MYex*`LW&GrLA_cIP9b{1N9&}B`j~MD!56> zSRi0sxF_(TrCeYHa6qlNwkB2_W8*@bPv)>GU_k?oy4!&})b%36!E@~qF* zg4tR=wFIxE%Qc=IOMq+Co!nEykeX#78FzEKlfJuQG9gEi9Ad@1okZT;X zj|9>Z6r*W$R!zd1E|J5!bh=Kqt69O6~^f2FqlR(q_Kx|lAeR~N7C3NyYP0Dketu1VpXErvA7Ru@&dNo zP9aseP&A+fb(Q*BT@tG9!?pRfAGnO&Ci%b0!{=Qz^Slz18e`cTYodG~)5DznA6w^= z9NwfE3P;k`YOsgHM}1XE5H5p7jmRmBpkGQHxm*`DqQYyOTwY#QU{~bO^rErG3gi4J z*OE6z)!j+d<67i5C8PROffmkt>roQ;v8<5w1>rFHd!w`}C*Ejg8`@a_a^R_ZC-_@E zY~IBS+6c8t#w!-b*$frL*}rI32{Y|Tlxm($ikp5|rKGB<+@m^vrR;3KNbh^^9CKMA zjSD#kQ*?KPs8PtNI&WRC95nY<2XVZ(GET*wxw(sG0kV$~s#Nn|YO$g%L)fElGsz4+ z9P&5rH{W)<>^k*g&}LjS94^RoI z&WH5s?Oh5AK=ZqFmHd@R-31+n9Tn}*QM8f7;@JmJJ;&-2Mf7Vdw}Vj3UQOfGLm$^Y z%q61^YP7k-@k*@I8FaoH#EQsihz6Z;l@$HdSH=EB<|}eIt|qb~FURphsi0lV#Ok2N zIq|1S^MP$$&Qi7PEKM@5AiI@fHld#CHsKJ_DorUp6*~u{My*NJ4{U=A0-NxQ^~23M zT*VQEu%Na8!!!3EjOr`_>g7LLrb@5#9rn8!mKj-3xRrmS5ue{&#Pu$w#yyqW#6nZN z7nCq#VjR4TW=~oFRp<-tM-h#X!_A1{tgUWx3cl!c!=;P$R+nO!!s{m zpxG6?B2i#^EaAoi2;hBLy~S!>Ijd`S*~685WNS-kc}*@<^pN>`_$xYQ^&LJKEZHokN;~RO83~|hsekeO|y+hHI{Chop5j5ZoY}5AQNp$@6BImGN_*&esCnEAf(Ag82jL8 z)N{a67mhD5j1%n6Vu)Yr&s9sEKdpp2UvFnBs$-i(aJWU3SJcOZHV!3qB`u*|S{tr-4O;ny{4Po-jit39(f>D4P+4J&cisg0ZDuMZ=Yq-LIbg=)H=oC_2ku zJUyCXc0D}DGGSLW*xx#BBjskrkYdYLMS-)!rQqF58=gDDZu3t~mDrED%br9WMMoRQ zrT2?T3EC5@a*rfYg_E+3^ysNYM(Rk!12KWP#s`B?Eai?9YXdT8+Xi&!r(9AMa6`ufHD&kvZ^wl@#V~ z_)VD$?p8E<_Sgb15T4SI&Q~2JbUlL@=zDqnR(W;J3u%sEEmBa*t6_7|ubFnFOI?Xl zNxZ$6hYzY&g~jlfedAzvFdOYYe=*ks3I`#r@?8w{Lu%uPoOc@FTFH)-~2kDxw% zfQsfxRewT!`>4`TebwYT7xgW6nVksGjV3H&vYoZ7h8(Nq1*NLDpkcUANf^?na$+N! zQJYJkwAA}8U?&Q*PJ}UkUV?xBiu?H~d)y^coE~5Sy`| zkRt%03)nw}mdB^Cn>hsAU6rqsdo;WcK;g>n!Z}dA^3mI!MAfTrocYu3{={rWd}gMc z1XXq3maW?W+pH0o;Vc+|*>K%@0JUlpT&W>d000|vKB2U4B<#^VH(`Xn;6R49|DFOR zduXQ+I{V-0-`Pjq0ZT<3W{iH4@viHm?qSOjYKkREF3^PF8miS?JXVJx@%t!Q9G1)f zAYPWntlv{c$F$oNeJzrgN+^e(WN5(BzMj*Gr|0f3?p{A)mR!f#?kphe-rzQNF7ESv z03#DkYaeiPCW$1mvTJ4fN!PInUxjloV{QkZn*Xx5dY zlNamq^fVTuu}ehOjb8%B-xv!DEVwOFMn{uGmkXzo7gtvo@2;#kJ3CK1ycKtG;lCYr z=uGHov)A#1@>#~-c1FAonm{hYp*vG2$8jM4N<5cLrf@vyu;v5p(|!J)X&Rkx8d!An zWA(zEv#-xHxW#M609?}MnK}a)*Z_QhM%W0OSdUl--V~P>5@?D%Ul1P99DlybbPzCy8OR?9b}mvK^Kr9YxkotKovOMcZYDSYTSI?G_THK>SJjk*#)u3S zl5Z}uXFx^AQ+@r!e_qOeD?`zZOuoac6q8uD&XHSRqrc(g*hesbY^J1~x=6^y)q7Uz zkTL>$jgQGlY7$2JniP3G$53bsbLv=vNwbzn9)FLlm{r4f66z#{4Zp@Mf06as-2JcC z(qpF-$x$Z=>^kI#6d5-iVNZWA;aT*sL9U8S{iB{Dm4tFZ9CuKIN!EAEeJy5*zuDg} zJuI4U+`iP7CORrMS1$U1Mb7*9q^RWm<2eTOD0PO!r(mx#rC#!s(m zDw2VW&+f_O6y%4!Z*o&=oQ$&~(s=H`Ph) zO*Ox{f);rBPY?V@KFfD=*ObCpNzFvw>57C7-gzQWO`6AzLt5R9@0F^Q%$yy)PzNXF zl8g@Z)tZ4UbPy-#G5+$+MxX=L?JGDfoQ^=p)?3=qw;eAzeWsmcx@z&wu+4G6)N;;$ zR(p~)-bOO%QI5zXt@U<1B-gafi;fNaHWs6&pJ1Ip5VBr#Qs+GM=pLK;BZ+Zaq3*l_ z)WIb$*uy27wN^i?h1aE(*CprFDS8uiwSDmePmGOe^7bD+gxQe*EXX`n(6wCqzB)X`5{;G08}?EUf0M-CH+hp|93v?Uz*R_bbW$4Vo$CoVE|32!0UN7 zIg%t$uS{>t$@40pvF==7S$IKA{j-)+Mdf2dQtco1>s?)EzmXH%MjB$lJ(U*;N9SjS zMIUjS@J{0skyU)*axe#qD8L@#F0U@^=(w1x(JTa?RsrD1EONydz2(y5spkR5Sk7=GM@fH6H~s z+1Y|PetjoB%#@?STYWV^$zE%1n6AaVKrWz;Pxmo~BAC48#zS0dkR^tbd*llfdzZtODavv;S34xIJe9{2vvFk^BwYj zMR$AZ??^{TbfNf~j3R~06?}3M2=j^qMu$t{Og0jG_GqF6{^-beyJKxud%{6DDfN{> z;`^WuYdS^p-O6$$FAjwUxsoP5y{9*?8;-(dOOzDkyPvJW4_NRYF&VPU1ZH3(=J_$g zpypAQ_=k1niR8zXjVn%#HGKJM=fZ|?6G#gPfqwY;C}(|8yDCXGGS&6HrzodejEgFu z@vddfvnfjxO>n-j*C|U?E6#ems*$&ry#-0PkG#H|WSmk`wq)zkwwYaYd;a#=y~%AH zL0OL)++wMlsHPM*ojlZZ!{7J@?v7G*6c@|D?z_ye-2FRm7GrBrTtmzDjX%GFf7g-! zrFG|Swjz70SJ4#st#>un z33RWWRk%{}F@js992*BGD_vPf(8#|ERE)_9At?E&;QIMajWpi5?|IVh;`pQTlCjcL zYL6z^c#DdLmL?9)Vnhr?N6!$2-C;ek|# z-W+~LHT)vf%C?-JsOaF8D-z{@5eeKM+^u<&7%u0a6Mt|_;@sfnboyU z{^wJV&+tc2jq|Ek$m3)BaDx{PZrTK~W$4H3tY3qByri+zqv%U`n7aXVDp%?H-&Knq zl!|zpxK;E}Qd7S6+ldKjg{E&-`l*g{Y^2jopFQ1hYJnW>!@W;UwlU4M>Ly|2Ecq@+ zg2Xk^3Pb^>l&FSLkU4dvfk(%=y zY@d`k-FdV*X7;(GirX!TayxdNzyhhEcqoD|p z$6podS@>QhK6p`NWv;^O@%ja(8KF&53!hUJZR^Y=C$t#%@^%CcY6+Mb@g4~WZ)Aes zO-BNku1u(-Z?4wFFxzvavT^lAWQrR9rX!z<;cgeK&xryvbR^xs`&c^VvI~|AzzJ>@ zkJLk}yal@6n5jtB>tp0qU+3AdE!cd$sUXtbWKg8K^r+8acwFKi^t}JR5^2dlRCxYS zKE-?(9-3ezZI&Gnh50F`x?jIfU2TNV5`!w=qW!3bsc(6RWa7()sUQzCH5IvCwhdZ> z#0v6|XKGM(e}P*Uhi#@N!^GUpl&NpzQ11+%}eHm6qu=bkLKCXiWit4iS26EXL(9dK zivXoV$4s_dyH#M_GH5x9fS@WQWQ06EgPoadhg-~C;n+}QSj`@d12g{R-+d1Q4O|aq zZbI8Km~iZtyZJm^pc^C$UxvG*w=XIIA9-gDFyV_NSP>%#z(m?USDLXaRE*duXp5fM zffa#Lnj*JszgRpAG4&*AwmZuY^J$WGT49f3~I zbR8#8$l6Y?-qXWV+y%86HZ~SBJ6|c|vw~beWmz$f*!ya~u%Pw{*H2X!Rc-}|8?8KY zfR@`w7Acjb<y1HT2Cr&c>ne^n904G&k} zF**Pv1c;U4<#gvVE*d=k(q>NyrM~n%cNo*?a(tTM zc)TJYyT-f8=cz*6GWgW%LPkY$lrGUusR%{gW$XBwjA2M!be2H2muq#@Zu3#LXV@^< z<+$D(mwSkBbP0XkPHlI<`>Wp`jR}#5dhD0561spQx&vX-fhp8*0D*xY`Ho^nLey7E z?cbv@1Bydd(fCM!?tJ%X+7Vz}|AL58}o;qR_ zf{Ja9Ta01F3P3T^ZF-9fMjWgHHvV;YxKzt7O+*to|DU_QSbt zQ_QA3tnu?g{kJ9f*H{AJ#{;~S6cZ*7K@Nx$A>A59lxsxSVDlR2-(I3#d-Ws4M>o zNq%0mKa4q*3NF{D)>=7jSC?{|sF2!XOTawoBR|Qk7gElhT04o>?`^z&nl}vBG}8Ba zwz*5j)p(=?0@H0%(>XE@qc1Am(8t8P*Gg?Fml3O*)sqJ>eWq=7dj2|}zt4U*e;>V} z+k@p6wlqqcQYg}8f7X6%NW-Y!mgtuI&5%?+(&w~Trn~bUv(5{*|Kp|o%|szV%6Y1; z4iRla4KBNRKAFV$HO``FfLuAyn{`&JVL8av;$RqpF9aCUnEqi!na z=`uw(l>2opF)ZEbU5rIFpMpy1NQZv*)c?cFu={@WuIYldP9%qO!d2;}-7AZItw)iH zD(=Kde52c^_I_TBv)dae1a}5;e`uR=_lmVFp^~oO-SHUlJN=>06U(`}q4H4%VUI11 zG@ajuxvxU@kGW^C1BVyoEZajxG~VWEiL~`wth-2Q^<>n=Q;tF^zl?jq!F`l5kallo zKc9=k4nOQnVeOjM4KMaW!8Mg-cV)y&U68g=hOAbH(0HlQu9z7zuDxCq~FjgK6Y zLF|fR)$-sCl&o6THlhR4aNtWIW-Ip>4`;`$2O$pSd{<~$L&kIM(HKV0{H166<&yf_ zLFQ#wn2FT)ssfC5IEVV<^<%BAXC%MvgDaEvrDS*Ft3y1#X}Rrj zBFmzGv*6e11kJ8v`}(%8{~QAR{GgaekmNJ%HGbo5ZM!Ub%{&b%i!W}f_w_DziZ48!og9t3y%HK22WR+ISndPKV=qUxcZGyk-N?TcoT zt9`qW+LD!-Y^nV!LVa*3gr2|GhyQk3`cekN@@_%)@=o@5^{p%vVS)PTmrmYK53C!f zvPLX`#$J@$HMgIqs1Cu$9oJ1MSd^De(kUm|+d#g3>o7MS)>rWx$$6&5O zaPPT*Jb~lINk>F*8W&vasva(LdnQ7TWWiQ z?k>Z$D^`{q`oVBN!RIX<_Hvy)swRT6vmgOWqvdM$S&I z9B&m?N8p258t8j50fg(ZEP7(r28MT$u}hI^leI-u>kWsNCOjD< z>e82MA==Ux2@4JcNidt`P+QdeW0$(P?{zJNVLQ+5Jp0RaH7fxh5hK-lV_A`RpkVs* z-+X1KP^ZZggac4YJEh1+kEe_gq-nh}3CtoV)s|^{eL0p!PEiL7ZKg~&!2pL0& z+ojJa@F_+qdfxW&lS7!ZBtCLyqBjiF+@pE#@gb|7621}NlK&!?(Jpi;u>IR%->k>Z z(Tz87_2o+m{dz8YG>(ISTh|EyOYk?rpY8h3@B6O|8fX{U0_|n{83Ch|dti@d`8|0T z^vq5EW*EcnwradGzIf`FG%Mx(;m$%uaW|I`e3t~Dhnco zNtLj6G%GX?R=4@dZ1|W6bpZLt&%gP9LAeAJKBq#kTxZF8yPjxU+{5@~iE)uSGf>{?i zEtDNnxKh+3!a*P7xNYLLm!{8!QxglKHOlR*_N5WUYwYsnSrM^A-rL2fUV*v)0?ohN z4!=Bz7@?yam4L6int&C&HoZnw_oYXHv09_BZWJs0>3RSl4k#>8>o*<(HuXUaHToOi zSARrQ#8Yngef{NQg3e=!?%yfH80r`jCW4oy1KA5uCXl-=*j);u4Xc6Keg&=0?9oi@ zqcjZdxIw~!f1%U0Zx?Kd{oG(Tt%bv%<{NdXg2Zu%^D)TY%rv&c?Wa!zLWj@u-Qb?@ zu+DyHS?dL~U~0Q-h#}pBf2|FSJdd3!nOu*=j3D6!IvA4?I6zg{kOOGu8F3z71>L*1 z$S)}c&6C>~l(hF~?xU!8Jz~k%Fy9Sfnm>8-|IskrjA7aRaq(c%hWyfr__aG;w~CV@-g7=}!3!&z7M|b_l3>!a+E~Ib zHkS(($WVtHB-AR9Jpo(y$awtPh{}`+0GW|>oOjuE$pQFF(&jbq{JpAR+)yp1gq(YrnkeF}-WrSRdc zMN)&9#P}T!hNOFT7exdIweI(Tq-gWwAcAjzaF^R#;dI-89IN}&d=l0nmHZ-1{3+jw zUqS{lB@~@EFE!Mi`R@NK{Kg!QbdDZDYEt8{cTiy&oNF$(!p*xWNcY$1W1sE93<@Y$ zhk7=!2OXB2#Qg?-bCRylbNm!46mYq3(_%gpUlwPwn9hK94 zkB@YF1P{nR+Q%36+5Jc%6sT{i=!vuDojqWH08+a81_i7$=%vs%#9+od!!xsNRzX~b zC1eeOl~U0w_um=zpV}rAXv#YAW{96obBvU)&zOG#Gy?OiCLHCy9~+-^WH({2tn@7Q zdF!&8rOp(b%Vl_eBd&LJo~i< z7QL{qG3wjZ`q_)0y&N?@=V-KL&kOkIc)IAvWta$E7+oATE1K(c^+k&rJ%Z+w32xLv*D^3CDIgv|gUAL7*fv=JAIj)b$1-+5B9qL4 z31XhT@s|DSs8CyR_Q$sx^ zs3M0U$9PyQW9-SVLIt`xcFGA0Sg(9n9aLw3L@nQlHP~F`pgo%PnA0c0F~IIBL>x!N zqelT7tR)n?3bH9hNAw|jHh3}PkNLG@xIeBwkq?^&7&wMwcUoAOxeBr@^gaphZLP}oC_V&gc zQ4MqaHizp8dCx4mDz!j+=|>Ea&9 zw~l}(r@Q=1a7#-DWe5L3c4N>vZ`Wj~R`r>Hx(5fQA2K9<3lJtv9-W#GCcp|f zy)*`n5YVcV5$KagI1ftRR#Z_PXJeUfmiGnP1$(1Rg7Y+^&e7l0C5N{tD@7lhe}<=2 z)H>cCK46yDpDXT&9-HXR4SAdWVYs)xzMVeUy~Yz=Q=AH@NA2WL)TlcC=e6^v*!7o= z`acdrcM-(jvD=a#P;fgo8oPOl$OlDWbzQfW<#l&?q?oCu^SR&YdY0g8T-Yrn(9J>6 z_>Gv5F7LklRiqDmI)Irili%)Ozk|N3U7QnDfts4}4ZTOR2Hf=;5fY~IW9Ps=dmjt- zE8RCn)IvQa|F15PZUuxVuCQ*M#O}Ea7UV`2&WmEVu?9i{R*79v^rJl*?KyRThbOn? zK~Ae!9E4{%*3m2a*rw9mUt^{p*XQ33wOzmhIJldQ*s1elH`$o)o`m&Pa2dPr_h>c) z2m$LDZfNEUNsihBZDzLV;?H8o4ahV?OG^g2+p?SZ zG-Hdcd2+`-Pp3?>}BR{#jE5>r zq=zSlyO;)Y#tVBb#?8kqK9yRGjapu56=kg#=6%=6TKN20{@1p)L!XO;jTQ_utrsiY2LoOZ*=a@gZWxOt^dbpp=P&K^Ef+B z)m%>1Z1AZze_GdyEliIDk+NVf7})MgJL=0r&dVS9;@4C6TTiCO#G+j=e4-_&?5i>B zG?UQnd#wZujvgdlwl}MtGaJR?&`M47hU_yzKau8SmWxP(s%!FhdbrY;`RUNjZ^YiYNBYK&EbJ$qtLY zXAEkT!`xkOXNCKNyJ0WN>Q(ui6RXA}h5jJKmOeI$dGw#%4F#-Ne=JmgeFbX|hy#n* zQq)H(8rgzmU$9b-s5N-C$StmX6z!xJ$=wz-*Q=}8*8=ktKLfkJu|(Ay-+6Wl{RHmv z_I{}$@!hqD@oSGs^fx7mP$kzt+^2lA*t%lv&|{YtNPc#~afvbugVB7J0TLLt&Ah75 z3AW*{*U7t@GVY}4Z{H(eA(dda@+uThBTBis2S_)5H7YB)Cw4V0OYkC2vat`U(a6KU zXPNf=>K0-2dMm{5KZ=e*udFEJK@3gd&s-07Jiti#f8E1+!Rpg5(+` zd(qhz2lMnu@V^_lpQh86|gwZr^v6lW$$GJJUwm7WYoX zdtT3##u?Smar!AnNOD+OfhqKk`bC-!&3aCT8*#i@@<8q;X0hJZ-RFS>bJRy05o^gL zO)vwdu{$+yFw6J|2?%H=JK6WVp5`TtKwl6P)>xR4bqY1Y-*uU+va9B(sEb>R=CSU( z-^JQdku3KIENm%LY+?4;Uqxz^?Akokc{Uu&taYj)GXG`(GRQs@bB;Li0=`T=ryN|p z793wpLE#4x?2RYsd#8GSDA#qGG`sNxPD;i?WX2~X)jm_1tItej&UBVXUZJ(n_8~~p;Fowk8ZiXP-l$JgP_Cxca1VW1jcxpnxm@A zDe`_>KfAF*9r-y)fkp>jB>j>9@^*|a&AwKFKNMtoowH+|%VIQFDoeBq9v^is=n)&H z>_GK^m@`%C$3fP1#!!w^V`V#~o3L!(#N!FNVS^6#g=9JWOvZ6TRiGpvz#+QCJx$qWDE=KE!ra zC-PqF<-W#Q1zE$-Ue{A7o|HPY}j_OAMWmv-Us}q{NvqTAV3MT5KuxSLztW z9_HZ~CS^tMTo7+h2FlMq4ie7B(mA@@!h_{SsK$qNc?9O8j>$pQZ?dv?(E>c=7K)q9c>z&U7X2!%>vZXQ| z?Gs#2!!06OQwHc@@Ut`P$h@&8ZV0Ize*@483m7gX-+c6`Ek_gYbIK#D5BIbktALz& zJc(^lr~Kh&u5i4QQJ#QeykmdL4^Doyj)#jrm;^ig;wJy)zw)*}zpQ_;c+7*XE_}}mSF&>tpUc%0h@yO+;pOBlj=^0}Pb!n|u@1m!Se}YcL;yRzWl}$Q;Jp zR_hFc58KAc@2cQHun_WT*dC3S?$n>4AN&G4HRig_vY!DeFLk|F;7ke}jZb#s-Q~?qn zbcebhIRj?pd|Cx(>3iWo4(?@ywJF$TFVN|-ARkLEz-&E(xqeRG{D+@s{^I%t3+{MV z)vx2ii2W0ohljjLjQV6@K5{o*aNtfUm?6lY z$c<_jxmBQGlxM-IE}5Lm0r!1`FOVb{niFWHP)kR&kC8AUYA_=4Mt_iyOKL2Mgc`^aq@_4MS&BpWT{Z{#E^OS<*K zvZe6dRwZeIMWW=V69`6ffLYq_x@9UZKzNUpnq8jPyP}Wk_mw8J(2}k?;22YS+exLfsNv!F zL9}dLA|q)+hA9n%?B$h{{QR{EsXgHi$nG~Y8hU)8z1L<`V-(KaS~x3zI4UZ;;GEFm z!t9{^N3Y3Jwguay;_TRes3uEkYf9;SmeMus>&eZ3DE+_Kd+)d=*KJ)?rv>Rs?@?*e zdo_}&Oh7=0^iBk%gd$ynluVH(RjEqLM2L|Z>7YP_P^2qWia>&ZR1<1|5brnd*?X-y z_j0Yf_g&}S-#TZXKlp<$l;r)scf4af<9VL36Eva^xS%PIya7gddj%aT?8E|3`)RWw z=5i(bnE}q0mS$6^zd!gfhrBi$uV@bZu9Sd@4qr`=aP^DJSFYZwY*vsDkO76PoN=qkE`z!N`l)6Zxq^L~PEQ>{B~Oit#u*zN8?8frnN0 zU?;n5ZwB#qLq!0yLxk6kAvw3h^O^Sz!qklLlkp6PTlOkGW7m z#0SxoA|j95<1Z=S3`Q!HS(#^VtO@Qt4i%9IK#s7UZH=e#QJ~Ud{)sOrBT0U%`8p_f zgeS!8o%5a1mU^AE1{iDhAiGJXwB_YWh+>iQqBg&-QqaWK1V5#x6MU!a&a0s#R?(6H z?(92~L(|R@`YJuoTq=w!#Q+DpAUGb7MZ%%1su`)h4`?M2;U8?pzsv__3Atj&UhmW=C;E9PuFZfO9yN%($ZKX&3U@JIu)Lou~IJ1QBmQrJw2_<$Q8ZVHA>$K0QQK9!S<|4`?TEt)ETW*+?Jm( z;7DuV2lS8dpeZym(S85o4=d^4-`yD?ie>mj0>$6{O~`IIRTjH4REVP%$HhVRL%yF_ zqwAzwbW{bfsVbuH(F@>$b_S|l2l)m=`cwV?FU_y`%Yk<0LMSJd=>FuPw;HR3|_j}c|RWmM$ zmu0HE?bdkc+0`+IU75?`Sm5Nrwzg5oL{&~*l|TWOJ4HVJ&mAvlh*oF3zTMWaEVcdC zp{IU&2S-5;ZM5wn#c3D5{i@}*F$c6Xqggvq9$kSozEo*w^~MIO#_qg5^F2%98Bl7vVPHZVC*E3YU%4h~DAVa$TdGrEykN?n2p4Pe z<^JLV4km**x%omX*?-D`A48?yZQJY>NM=d;!#x0$0UI_u5H3oF?Y%fIiqv>~nevv5 zBZhP^@``NhCK4e$YgUm0QAY2lH^7Rr3#8+xG6LR_-7njkigY#ZnnHpJP)4fe{5>1L z)YE=LtP@G0k#Do_QY1B!7ThZAl!DODtSmFq_C7tIG|>0>47j=vP--669F;F?4FvM} z%zbk9uUn2y{5Z?w7#xc!h0dS_O(R&(V&7_o|GmxspNe3RS|CEesLy7x075X=J$INw zVZ(L7Fb!0cfcWu~=XA94z>%XzUv~kBk{NacS^^Aae}(u+QGPrA@WX#hb3)E9d^@Ot z9W^442|)XrHV8dvM((E~_bHIf$aL5?kQROh8s5#P4q~u7Ks@_PEd39E@6_HS+Bu*P zCO*e~{B$TDb!b9(LsL8iLQK!&?Bgsj5Ds5%3-7ZQeWT_9yW&$e(C{99-Mv==BToK3 zzKIjE%@w!E535BU53?S_AdF>)fnR;UpOCmj7&-=4{jiOE{lF?2x8;qlG?$Nuk#$Nr}R#NXSUf2ytiZRh@-6B+G5MhW2lJqU30PJkl-lqoYREMPZ4go_SczR92VB$xFhwSzGjPSC-_ze}_z- zgH8$cb@dmZa}I~%LIs1Sp}E=Dh)F8LFossauED{TKDS)j)S>3+^877tU!-eC<6)TX z#5-3D?;`+R*mn?YxOLoxC2a5i_CuKA0O*81O3kdNbq&;R(Cx*Te!8kHPZ6@v|H^pA z`M0ebRf5b&45;R58KtYTaw4umwnA=~xcNDA?8qVH-ww4_6gkan?f8*z&vExQJ#3}t z|JMdm9grSOav*VOQ3#OBy~X#zj1Se-!62k_CR;$B}ewmeQ)1Y zNMSO)#i1^V^XBX)TLC-A2e8lCXZ_FeI()VXBnnp))LV|PZTSLvW`ui(LfBYIx|w4c zxUY0?!oas7Y{i`_IdPPS{OOMn)xW;3^ncRB`-f8#Gy$UWw_q7lC#M&1v1(C4smZE2 zm>IHh%fMvk!=80NhS@Mo*2||18(qzQz{?uGQ9>U-4$RqQjR3sqbmgUerTK(TyJx zMV9tyEOI331cVvq2I441)qI^*rz8>%?1VjXJKnIy*70y8w>`c~%I`6~@GHagbNbug zPkjBhzSiB2VngI-*0;P=5+Tflb2a$&`5$Bn6&a6LXL%X#!{3&PyG?8Zrm)NsuHjON z4R5tU&k4D-Jt@|hGESVuqintUHSAU{Y_*V@^!JvTZZm|+e?T}0hD>XBy%}%Y^JSu* zAjk9>^zr*Nk5b{JZG-w4HIRZx*3|OtC;EKzdcM-ey7HS|}fR>a4utxecE^0!L5wMZ~QL7)UkqDhzw3zcIt5ooZO-GHDn zl2p_eacxp10+9T(16BNm_3&#I#v2h{YJKvD!Hb#R7N}s_0Ma~_7>S!IqS*g23H}%F z=l;*nAD$uSXNI_nwJsi)hBHbn85!zQEQr%QeW_2C4d(b8RC2Ntrz?Yagr<`Y2n?ox; zRCeL;7qH82sIb-0Rm^GajVA9%DPtodtFEhNW$CwAd_Pg5`Tc}!>e`PIoGYic06;p9 z)Q*gYy@n;!O<(n`oedWuFF$M*xqSKR%yHsIgX5X80>Sw1-rA9H-@uYty1S31cMd8A zvKo>=YeR*s?D)85MDJ_P(lAZ1-Bb5GH3o>rU6wkogGB!@ zD*S$*XlUg8XJ{<@G$n8^oiY^SmFr_=2lx#z|FqNf$eRXwJ=$zVT(fw#q&o+C+{`Z3 z)oLr3l|%~!N9r8}9{ti-z^Nq1+^Codn{|@cm|$P{x_!;*nHApE#>k5Ka+7P8`)$+k zfo*-grxIb%HMt^7iz@^K>S|uZ)TbmCi@5x(G`v|^VwYZSgr(e1QLqM1_18u3*gEe`wvZ|TuO+`)NS@6*`~o?7;2Uyx(4vp+1>797PKLF+lvcJl|@Or0YXM zfJXG*t7au(DvBfW0?=J(BX#IcO}x?ez@1;nU@7pnbUZg|V!eC;Dh~=%p}OpBzIXJQ zzgwe@4U3^PTpU;yeN&idJu8&KPt&&?ByD*a_U2s$H6DWkSxd-=?F}z|(%sarRHmT0 zwH2XbX79!fdrdcyo+>n1(2Uc+cb5KiD^8N_2(xkI5JGAPD5gLxspmI9+V1`s%v}~k zyty8(SM0E2HgxLFdQ-g}w$|?E*{_%c4eliqM?g00&vi8#fr_JGq|;I$ae;Xa8tpJb zW_Sa)AvZ%H%roE0aB?(p*nIIy3-7Yq!99>hZ;b$k0~^=>TD_dv>s|@bV=xKLJ#1pO zl=xX+tR6D3O%#6!)*<1#Ta_rj#5J`Ms>wW*!>fR8Vna|ADsJ&XMJicwzmHvyBl*;T z_pqITi;2qHPQ~Pum$&c1yFtk7OvCdpaPOeSn0RHX0XaJg2eh}hp6h-nK|rz?#!!Ss`^wCM$aSy4z;Mu_K2!Dn zwqxg@(lYT4`BAhUz0?|QFmrN0CXy}k*B|B*NKquoAC#JIPFX&JP!;u00rNGuNi`ip zN|}~c{!G9b3pSt9c)YN5#Z5k3t^WD1mqTK`LNb4Wxw!$_VWfM?4N^06H`VA$G=o9{K4HqF7xGff425fpN7&gC{U&r5GLVp$#_LwL|?S$x0-?$u{IL#q4pnzSx{} zjWn#P2FB#tm!%8qU%1i~)|^cXSvzKeYd2;{&+F`zkrDrVAku0f z73fA+MxF=kg`d7$b5@v<$Qge(oOG?Y)GgG9E@$z!NsUjRVw;ocqOtzk=`T-rXU^mz zoyaI=w|fR0q=QyAp2tlAuL%AYTqgid=$ei#{<;0s2^p`fI=&6 zjv^Pb{@8#1+v5VE<4Uk7CY^vxIAuf%>1gxiuUG}P{R4!g=P1AiUl8q|VJluMT zYSh%28j*asvY+fvfS#$Yt_@+M;O(n#%2n{b5O3cT0^BrB{C#{m=K4_v-?+b0%n0|~ znYhF!$?iNRoqqdFBN$m*N=W>RlBK3$c!^nx$;0y1+oc&BIaYn}UY2~@w7AvQ^M0Rb=vf-tkEt`-1HTpe{`A~N)Z=o|QaInt$lecx5%iB~KiuYk ztdtMg<`8)W+XN?+fYmtpDFBQr5AGd3c6z=oH8`Q`k< z3$AUE2ue?`^mK>u#XJ$T-bu??GlM}YR<6)ER!nC9aI5pGNJXU8{XEoC9a0~Z9rFh? z_`hRXz+&lor`MqRNpJ5>*E<2{gr8jha^XEV!AR`)9=VZ9(8y<%F|Ai&p0!?X$RfG* z7;=4NMM_q!6E3*+Z1>#IoMVAJd7ylzu`M zWc>WZLtrQ$BpMi}u0jSuHM1X4BMOI#JfC)0$VsE;1jn;0;OUw-vkqGymDyRZ+3MiC zrTsiDxvG0OK9+R=)~Ia)fcHd1=W`IR-_j+gnxr^EC~r(gdXSfPmU)%6^ey(pl1G3- z#j4GDr9{_4er&Z%DLN-@OKCm*WJjnLM5KS^$ezn3E|a0e#+YR_sBAdhNVi_77CuoV zmXmDj=cAm1T;4dcrS+lC1N(?Ixm5yqqYP8}qQgr*+U*5mDPtQuI01y>R_MeHvfI<= z;yST39!J3&J+k-ieyl5VFw}YTe6#id=tSdM*%5{KSvfyTt1kO%rV1&NjI23}X{Dvk z)!?OMb?|+ AkDP4+zZxXP806eT(<=4AA_;g0Ka+hs}WP{sNFeeN_j6QAMi+BRZ_ z0NjP7l#vDE)w7YcO^{WQDAE6bWvCSw*9>qUP&qW&BK%D%cOS_@10nE4TuQS9X_`Qb z3n4=J5YzS})T@LRw)f)s5hbf;<>$-#r1X5<;pg0%aHGDWQFx=*vCFq{jSbcH1qkZB z%S0wrj3RjAvHBxHJ(-?>TP(r~{Z)0-UkYRK*9U!WNLol)Gh!Wu#;2f%&3|&XqpU zGm8xJL9*45oUg6AwM7;?U33g&M-4yA(C#P4Lf9&`&#W4Qjn{a4n?X5KN;ryN%$Bjn zMx}t5S%bsWHHV`rY-`bbb&--0?|<0B@1))&4pM9wOYq9swuTZ%~RH3kZIks;^5gsWxi41rKf2ltI0B}A7( z%9jwm>Y8$CssRRQk6wM1O-$(wp`W~?Sl$47PP9z{$+i*w>!v*+Z`wcR!&i5Vx|rou zYuyj35cp2bh7_x)#aYa;iyHr)@NH4OBK z11z*+?;@%8yU&fS(&jI%lQeW4qIyZ@JIAx^a(L5rr@|M8p8;OpN70wSQ7>QB_~sMU zO-?XATUD6xFLgj`4oxL&BVfSvO(+H9Ws^SpFJ zY5@F>f_Y6U$7Nl zWRZ_csm<)C>F+0+B84c4?T%Oc#@Q1nSCwL2vYhSEmTU%DS7o|x$Y+MXRchBTK-{_y zDgf^kFEV5-LNCOY!5D3mAE4ef>*E0n^rg(|q;HJUs*>3)^x{L>oHvY05qY9sSH|(9 zi{UkZHLfYfl4G?!Xn`=cPZ#yqMgRT04A!D2=_LXIqybMmO#;OQ!?+OLowoU{sf2GV zU$fF2S07Xs1$5}0slAYrP`K1spe@*ft~GOTFl4S(>@9E%$zK#u%UI&wjCe)}$U{lu z&AZ%qQ&h`62(KF-7|xcM7Qd+SWS@!ZUB29Nx0a#Xs0Z zk2$uG4j6uF=+Pxr-HaCZXlF3XO>&;1jRU;pXE4$&6f>=p#tDswohOcfjgD?2-Wpg3 z57^2{?i*5kiyb9<)S{@ufw4A~Uk=ziwM3&+H*AP}_n-B>n65pyiJC&E%%ML%i@!pV9{dWZrcK!L$Q={>wM@z{IL@S_uHys&x0 zf_zK(Ouql|!yVyyxW9YyGp5xRZZnyA`v(3F>&FHR=8jI5xxHJSZ0Qs)J2#-*^13Rc;pNJryUjkoylMOR+v_^H7l$(sYLRT5v`?sZ zr$3$O{~a@7Kp&M0;9?JU6< z@QdW&=T0lHdKL4kcx8nZk~O8KBpaU*HVlHD^t*SVXDOL}bAqnb6=sw3HNn!^Jd08{ z6uJg;+eb4;^v$}>@EXw1Uy4^}{U7B6`Zge_(+}EQT_xh`zMYnGBRox{^CNi9iETaEixXE_9S;eE9lL~x}Yc?m1I@gxnY%K z@pho$sy^K=bSdsU=^k8KRR2gbG|R@ppw%Sb>5{B(sT+d(N_(xv(kr6G8|!`%jOH`2 z5kp!As*en#`(p2&)h9DZN~m=ikd~FqFIn(YDX!Rfow>e9R~)+1V;tkk(^V(=R9|iZ z5bpvE#He3r!^rB2t_I=|6I>0+7s+U&e{IxJlN9sF29O~ zJigPo3uXT0sY>T;(M#TnA8tj# z+H`vBWUZ;eE0T@o=^3lTZyp|8hmB#rT>9&diRe4JjBEqr;LvF)s|E;5_$AdC(y^@N zHFx4r(#7UsFg>O{o00j;1J`Z4+F8l5jfdV}TLofiOA=t*hJXL&X+W~xbp=)Y_iB@X$s2hDu#W4~^GyZ3dbAZ|L4_XFn2c7&~ZY1N+M(Izu# zEK$It77>z?CpeQF=PaQ{wG?zoK_13~J|B=p?knA;W=JfRFHZu1S_F3DZ;%*)ei zh>yPVVX3s5Ds+}}tsS$MeUbvlQ|V$`8Cdyq$6ppE4f@Ncj(du`OR9T_UzsPR7|^~FX@*eWW9v$26c zcF(3EUgJ@}lSHRvS=?5G-Kn&ZG(WQ>ukkGRXT#y!G4`rg8BHbCLTDGsM~{*9>aDyg z49yfR0epnbt9s(LfNe~8Q2LVpy*w^3;bB3H`M^U5lH$oUE#kYqWIIVqEVq6 z?y*Z#aGy<|#gwqHco7693X!MJ@u82`=5m$4m|JO_TCV~Ht+d`P9y1NvKo z>ql0Ryᒦ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 - - - + + + + + diff --git a/apps/public/src/app/favicon.ico b/apps/public/src/app/favicon.ico index dd5ab726ed7448060d26d766f5a11f48e4c45a27..403a07ef9b3f1dd8a2031c3d51aaa4e55c77cfc0 100644 GIT binary patch literal 15406 zcmeHNX>b%(5FU^J>JR^?EEA(Bf(oUgLV2L2Qsq%uVs1Yy_Bf>4Y!$buZL<(OF}N6D?z z93_Pj{6U64avz98?*`Y5=U9<^_Hasm<^IsG|?+`;jCswzNLqWX9F!buQE-UrU<16Y}S`XKUj z167WoofrqbKk z+D2<#_>X!JN8Fo8lY#!E_xgf-=CHoz86RTq7I4pbjjP}D>2MQAFR_oqe{Rt}VXlHS?qP-@Tk=K2K8OSbw|%L%k=knRXSN^3@~p`p*~`#Al)q&a zFMsh6?z?A6d0OpvPI?@q36FAhX?{?^!j+zUX==hW%trlUxIyN-eE!I+#=`lpsZ_CX^# zdv@U2gO|y#%c1S7IiBd>d%(Y@5<1Fo z0j|myK^%R5`gaF-$3u5hFAA zs0AnbkW*s8`>ycbdQkmtP$MCB4-?o}1!DP?Z3Fp~0~?kwnfir|#RK{B$(Kw%e5-c& zYG);Ik^h_e(271AWn?R1^vS222u}d-Ma{l;!V)>W!?GH>U&;nna~&ea7zBS<2B656 za+4L(J)HmR*$J4U$M;yWP|So`b~fHA4uNbuA;lE5`(_ps5%W0k9#}~vab5)H`%iPY za*aO_kICS`Ay&HkcU8Us(%3#2tMM{~ey_tAi?%onqGx@8z8?pUdrg3B3;l+*lbPV(6!Aidj1r_U|^zGgV=Jt_Y6$?!kaA^f!FP+XK}L5a0r zIo_v69U7md@%h6~egy?%v9v^SZ05Plh}G&jZ}LJ~wD8B{2=z(~59If17q?+tP%NW4sUaWR`)aWogfFmeK8r)- zXi;B*4ON_vHP`rQe^2q9=0tlmx#2k{&Am23RuGY>azM0;%W zb0+|`DnMUy=(n{A@tV=&yRE3fxPOyU+D|V|A4c6Id!|@iT8Gk3h&{4@8_@VS*BLpm z&9WL_%@m!M_Ec8-Qll6j(2=tmj$?|kpuIr532mOnj5Mxo%`&#ZyL?PS6|{fl=62lg zMUL)FpbPKbq_@#nXS&aHrt*LL30qev27u0gw8vBtC7_k^N0{Rv|W$qNf&Q2I_h7F3GmfS20AzSKA*_W@ibPF z@o?oBH$c96+J{i=vT=WHq@%ggJ?C|B&wiDggN-!yvwe4K+kkBYwhdhMHqe!Qkc6FL z9@1MsI~!d*7NRT14^Rw*17jh&QXAq}S^S0;gJOt<=*;7R*}E}*hVa|T)<8!!@E@4* B`s)Ay literal 15406 zcmeHNX>b%p6dp@|_!EElKfkh3)ItkU5s#uQEq{2FQeK3Tf`kN$BXSpVSb}mYX8{U^ zOAAH05fCXw2_ak}Ik=C6Krq>v-Pzsj?(EF$KHuw253?&_ceAs@24}0fdUj@Bzwf<% z)7|e7gnNX0g&sWw>(#5{d7&UwKPU)A_zsdFMf^T? zxBRu0xc0b93ZW}48``e$vx`PvJPg7IHbVH1AHct6N=CZz&UzbksRcs&*F)4N;yB-D zL?2i`4b;$b<=LrAKD6$ z)79XwoCLc28pv}8ra?cI(a>Be)!PyOPmCr~Uj>16Q&ZA=XTJ+k@j3)|E`zA&2B;TL zLU7k|h@wu@;ZJ;hD=R>$*#XLtU7#G>&HBLF%;wQ9Tg#SDHs2u8-f~gM;3lmz1=wE1 zM(a~&KX;j{x4X2k0p*QlD{}_)G2~g_!Mi>Kx|DXB(OCA&os8d3ohS2l{&#VI&B(^? zA|B$%$3gyLF!+`Y2k)!_;9EY5HSwdTb8buWqEgVVUx28$HQxLlh=qgf+B^Hbobvb1 zd<%5k`wZTM0r^8T3b9ZCa?L+nwVCnO+nT|*v>ZI2yawvoBaA=jI(X;2pHum@D|L)N zox8a0lMRsk)V%YGjr;t$mA!WLG~*xob*;fiUhtub{~+VOm3;YFsrc(Hmq5Ex3+ybl zVFtL%9<~&Z)>waA`J;Y`J?Hir{&N$!ON%VSz~?_f|6dL<^dG4^2w^-ElMXlr-|~^R z@#pp@j_wUwQ`}bQoXO`e+dh^2oImji?W<%u&@P{{jz5k?dJX;doiV0wNtvPlME+~y z{BbU_m7CKmH9Oguddo#zdtL`|%#(P2-_yZg2YKNTpuNHS*#OXQTw?DkM}D@9zdXMf zeU5+FxKS_m(hXdHCB~oc0o?9*?^`y)GXA6of8}KKwVFFH;QH^AAQlPZ|F)a=XkS2_ z!EKB2`%CT9as8&-OB0?4|2GpM@a+t?4+J*Og22X^MkAltGwoGNOSi+`Aq&}z8|S0@;_n9b zo@_VHe|z>YN6(I)9UE|LARQYhe*`3q6-(ov18H187DtfA_6725S)(|zg>fs(_S<1a z;Q91T2<})4k(2vDJ%1e3Gc_!KNxgRIZ_J1O#o{v*gR+coIyz0`9g5jgzLxT~JU+sD z+09}YERG?!fpYv85G$U_EBq91;xVNJ>=bKZ_?37ZNC`pY#J;@3FOGVG<+;tcwZ=MD zzd-RbnvYl{j-TTAkyF3t6@KsM?|{~P*0346_G0dVVtwM6KG6Q-JZ3*U4-jcMlvnuu zE60G|+GOAlY?_l`+tS1rK*t&k1OJ)BU`H-1-Sxr0dO||X(f3tFBL1<@fO_tj34cv~ z21%m-PVmrJFC)cE^?df|j&|CCGCn$NKu z7F&0v#GG?r!}Ofw6*`k6Q~WCCspR>CFh}$T(B8po#kf7GHm|Rl;*Vf`pLzWkM?A*z zTpYi4?HtHC&b6E8B3t}47VRM%zuwvi{uQHa`;M)^Pq{^&`=hnUziPaF_*aa^^FyPd zKdQkr&ky-AT4RIp{U+NF(?8}7Z|c7Z=uBcBBbV3m+z(idDd(?Z&8Rf)S+)*}!yg47 z%13JsKmVAYyUTmBHJ|ECI6a+}uX3BQ8+S0l_9Gq9T9!D~8$2luSHbH069 zPpnZG%KR?vaxI=In$V}bjA!SgXfqdCJpt8GCp27Zw5w-8Il7zaoqV2Dddlf@tS6vz zB>60!k6#AQl$S7$(hsD`{lPQ!mH#$;N1D{1`DyN958YYpX~ZaUsym0jGyjeaI5yze zK$mTxThu&sHousAXF2%Jp6B^}s(qjui0)dy!D~SwS# M$oV-M=&}a>1+M;*ZU6uP diff --git a/apps/worker/Dockerfile b/apps/worker/Dockerfile index 9623dc0a..0f6895b2 100644 --- a/apps/worker/Dockerfile +++ b/apps/worker/Dockerfile @@ -29,6 +29,7 @@ COPY packages/queue/package.json ./packages/queue/ COPY packages/logger/package.json ./packages/logger/ COPY packages/common/package.json ./packages/common/ COPY packages/constants/package.json ./packages/constants/ +COPY packages/integrations/package.json packages/integrations/ # BUILD FROM base AS build @@ -70,7 +71,7 @@ COPY --from=build /app/packages/redis ./packages/redis COPY --from=build /app/packages/logger ./packages/logger COPY --from=build /app/packages/queue ./packages/queue COPY --from=build /app/packages/common ./packages/common - +COPY --from=build /app/packages/integrations ./packages/integrations RUN pnpm db:codegen WORKDIR /app/apps/worker diff --git a/apps/worker/package.json b/apps/worker/package.json index 15585f2b..1b118e92 100644 --- a/apps/worker/package.json +++ b/apps/worker/package.json @@ -13,6 +13,7 @@ "@bull-board/express": "5.21.0", "@openpanel/common": "workspace:*", "@openpanel/db": "workspace:*", + "@openpanel/integrations": "workspace:^", "@openpanel/logger": "workspace:*", "@openpanel/queue": "workspace:*", "@openpanel/redis": "workspace:*", diff --git a/apps/worker/src/index.ts b/apps/worker/src/index.ts index efaee662..8b0dc106 100644 --- a/apps/worker/src/index.ts +++ b/apps/worker/src/index.ts @@ -7,11 +7,17 @@ import express from 'express'; import { createInitialSalts } from '@openpanel/db'; import type { CronQueueType } from '@openpanel/queue'; -import { cronQueue, eventsQueue, sessionsQueue } from '@openpanel/queue'; +import { + cronQueue, + eventsQueue, + notificationQueue, + sessionsQueue, +} from '@openpanel/queue'; import { getRedisQueue } from '@openpanel/redis'; import { cronJob } from './jobs/cron'; import { eventsJob } from './jobs/events'; +import { notificationJob } from './jobs/notification'; import { sessionsJob } from './jobs/sessions'; import { register } from './metrics'; import { logger } from './utils/logger'; @@ -34,12 +40,18 @@ async function start() { workerOptions, ); const cronWorker = new Worker(cronQueue.name, cronJob, workerOptions); + const notificationWorker = new Worker( + notificationQueue.name, + notificationJob, + workerOptions, + ); createBullBoard({ queues: [ new BullMQAdapter(eventsQueue), new BullMQAdapter(sessionsQueue), new BullMQAdapter(cronQueue), + new BullMQAdapter(notificationQueue), ], serverAdapter: serverAdapter, }); @@ -62,7 +74,12 @@ async function start() { console.log(`For the UI, open http://localhost:${PORT}/`); }); - const workers = [sessionsWorker, eventsWorker, cronWorker]; + const workers = [ + sessionsWorker, + eventsWorker, + cronWorker, + notificationWorker, + ]; workers.forEach((worker) => { worker.on('error', (error) => { logger.error('worker error', { diff --git a/apps/worker/src/jobs/events.create-session-end.ts b/apps/worker/src/jobs/events.create-session-end.ts index f61e897f..5580edad 100644 --- a/apps/worker/src/jobs/events.create-session-end.ts +++ b/apps/worker/src/jobs/events.create-session-end.ts @@ -6,6 +6,7 @@ import { getTime } from '@openpanel/common'; import { type IServiceEvent, TABLE_NAMES, + checkNotificationRulesForSessionEnd, createEvent, eventBuffer, getEvents, @@ -138,6 +139,8 @@ export async function createSessionEnd( throw new Error('No last event found'); } + await checkNotificationRulesForSessionEnd(events); + return createEvent({ ...sessionStart, properties: { diff --git a/apps/worker/src/jobs/events.incoming-event.ts b/apps/worker/src/jobs/events.incoming-event.ts index cee1e1ae..32adf6fa 100644 --- a/apps/worker/src/jobs/events.incoming-event.ts +++ b/apps/worker/src/jobs/events.incoming-event.ts @@ -7,17 +7,17 @@ import { v4 as uuid } from 'uuid'; import { logger } from '@/utils/logger'; import { getTime, isSameDomain, parsePath } from '@openpanel/common'; import type { IServiceCreateEventPayload } from '@openpanel/db'; -import { createEvent } from '@openpanel/db'; +import { checkNotificationRulesForEvent, createEvent } from '@openpanel/db'; import { getLastScreenViewFromProfileId } from '@openpanel/db/src/services/event.service'; +import type { + EventsQueuePayloadCreateSessionEnd, + EventsQueuePayloadIncomingEvent, +} from '@openpanel/queue'; import { findJobByPrefix, sessionsQueue, sessionsQueueEvents, } from '@openpanel/queue'; -import type { - EventsQueuePayloadCreateSessionEnd, - EventsQueuePayloadIncomingEvent, -} from '@openpanel/queue'; import { getRedisQueue } from '@openpanel/redis'; const GLOBAL_PROPERTIES = ['__path', '__referrer']; @@ -101,6 +101,8 @@ export async function incomingEvent(job: Job) { sdkVersion, }; + await checkNotificationRulesForEvent(payload); + return createEvent(payload); } @@ -185,6 +187,8 @@ export async function incomingEvent(job: Job) { }); } + await checkNotificationRulesForEvent(payload); + return createEvent(payload); } diff --git a/apps/worker/src/jobs/notification.ts b/apps/worker/src/jobs/notification.ts new file mode 100644 index 00000000..e6795d23 --- /dev/null +++ b/apps/worker/src/jobs/notification.ts @@ -0,0 +1,71 @@ +import type { Job } from 'bullmq'; + +import { setSuperJson } from '@openpanel/common'; +import { db } from '@openpanel/db'; +import { sendDiscordNotification } from '@openpanel/integrations/src/discord'; +import { sendSlackNotification } from '@openpanel/integrations/src/slack'; +import type { NotificationQueuePayload } from '@openpanel/queue'; +import { getRedisPub } from '@openpanel/redis'; + +export async function notificationJob(job: Job) { + switch (job.data.type) { + case 'sendNotification': { + const { notification } = job.data.payload; + + if (notification.sendToApp) { + getRedisPub().publish('notification', setSuperJson(notification)); + // empty for now + return; + } + + if (notification.sendToEmail) { + // empty for now + return; + } + + if (!notification.integrationId) { + throw new Error('No integrationId provided'); + } + + const integration = await db.integration.findUniqueOrThrow({ + where: { + id: notification.integrationId, + }, + }); + + switch (integration.config.type) { + case 'webhook': { + return fetch(integration.config.url, { + method: 'POST', + headers: { + ...(integration.config.headers ?? {}), + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + title: notification.title, + message: notification.message, + }), + }); + } + case 'discord': { + return sendDiscordNotification({ + webhookUrl: integration.config.url, + message: [ + `๐Ÿ”” **${notification.title}**`, + notification.message, + ].join('\n'), + }); + } + + case 'slack': { + return sendSlackNotification({ + webhookUrl: integration.config.incoming_webhook.url, + message: [`๐Ÿ”” *${notification.title}*`, notification.message].join( + '\n', + ), + }); + } + } + } + } +} diff --git a/apps/worker/tsup.config.ts b/apps/worker/tsup.config.ts index 34f14180..464d750a 100644 --- a/apps/worker/tsup.config.ts +++ b/apps/worker/tsup.config.ts @@ -6,6 +6,7 @@ const options: Options = { entry: ['src/index.ts'], noExternal: [/^@openpanel\/.*$/u, /^@\/.*$/u], external: ['@hyperdx/node-opentelemetry', 'winston'], + ignoreWatch: ['../../**/{.git,node_modules}/**'], sourcemap: true, splitting: false, }; diff --git a/packages/common/src/object.ts b/packages/common/src/object.ts index aa51a957..405156cd 100644 --- a/packages/common/src/object.ts +++ b/packages/common/src/object.ts @@ -47,17 +47,16 @@ export function getSafeJson(str: string): T | null { export function getSuperJson(str: string): T | null { const json = getSafeJson(str); - if ( - typeof json === 'object' && - json !== null && - 'json' in json && - 'meta' in json - ) { + if (typeof json === 'object' && json !== null && 'json' in json) { return superjson.parse(str); } return json; } +export function setSuperJson(str: Record): string { + return superjson.stringify(str); +} + type AnyObject = Record; export function deepMergeObjects(target: AnyObject, source: AnyObject): T { diff --git a/packages/common/src/string.ts b/packages/common/src/string.ts index 8bbd7a4a..aee2527d 100644 --- a/packages/common/src/string.ts +++ b/packages/common/src/string.ts @@ -1,3 +1,7 @@ export function stripTrailingSlash(url: string) { return url.replace(/\/+$/, ''); } + +export function stripLeadingAndTrailingSlashes(url: string) { + return url.replace(/^[/]+|[/]+$/g, ''); +} diff --git a/packages/db/index.ts b/packages/db/index.ts index c6be61c3..05990e53 100644 --- a/packages/db/index.ts +++ b/packages/db/index.ts @@ -15,4 +15,6 @@ export * from './src/services/user.service'; export * from './src/services/reference.service'; export * from './src/services/id.service'; export * from './src/services/retention.service'; +export * from './src/services/notification.service'; export * from './src/buffers'; +export * from './src/types'; diff --git a/packages/db/package.json b/packages/db/package.json index 5890d7c2..9e33a713 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -18,9 +18,11 @@ "@openpanel/common": "workspace:*", "@openpanel/constants": "workspace:*", "@openpanel/logger": "workspace:*", + "@openpanel/queue": "workspace:^", "@openpanel/redis": "workspace:*", "@openpanel/validation": "workspace:*", "@prisma/client": "^5.1.1", + "prisma-json-types-generator": "^3.1.1", "ramda": "^0.29.1", "sqlstring": "^2.3.3", "superjson": "^1.13.3", diff --git a/packages/db/prisma/migrations/20240925202841_notifications/migration.sql b/packages/db/prisma/migrations/20240925202841_notifications/migration.sql new file mode 100644 index 00000000..e733e8cb --- /dev/null +++ b/packages/db/prisma/migrations/20240925202841_notifications/migration.sql @@ -0,0 +1,71 @@ +-- CreateEnum +CREATE TYPE "IntegrationType" AS ENUM ('app', 'mail', 'custom'); + +-- CreateTable +CREATE TABLE "notification_settings" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "projectId" TEXT NOT NULL, + "settings" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "notification_settings_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "notifications" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "projectId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "message" TEXT NOT NULL, + "isReadAt" TIMESTAMP(3), + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "integrationId" UUID, + "integrationType" "IntegrationType" NOT NULL, + + CONSTRAINT "notifications_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "integrations" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "name" TEXT NOT NULL, + "type" TEXT NOT NULL, + "config" JSONB NOT NULL, + "organizationId" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "integrations_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_IntegrationToNotificationControl" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_IntegrationToNotificationControl_AB_unique" ON "_IntegrationToNotificationControl"("A", "B"); + +-- CreateIndex +CREATE INDEX "_IntegrationToNotificationControl_B_index" ON "_IntegrationToNotificationControl"("B"); + +-- AddForeignKey +ALTER TABLE "notification_settings" ADD CONSTRAINT "notification_settings_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "notifications" ADD CONSTRAINT "notifications_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "notifications" ADD CONSTRAINT "notifications_integrationId_fkey" FOREIGN KEY ("integrationId") REFERENCES "integrations"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "integrations" ADD CONSTRAINT "integrations_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_IntegrationToNotificationControl" ADD CONSTRAINT "_IntegrationToNotificationControl_A_fkey" FOREIGN KEY ("A") REFERENCES "integrations"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_IntegrationToNotificationControl" ADD CONSTRAINT "_IntegrationToNotificationControl_B_fkey" FOREIGN KEY ("B") REFERENCES "notification_settings"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/db/prisma/migrations/20240925204316_notification_send_to_app_and_email/migration.sql b/packages/db/prisma/migrations/20240925204316_notification_send_to_app_and_email/migration.sql new file mode 100644 index 00000000..aa38f132 --- /dev/null +++ b/packages/db/prisma/migrations/20240925204316_notification_send_to_app_and_email/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - You are about to drop the column `integrationType` on the `notifications` table. All the data in the column will be lost. + +*/ +-- AlterTable +ALTER TABLE "notification_settings" ADD COLUMN "sendToApp" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "sendToEmail" BOOLEAN NOT NULL DEFAULT false; + +-- AlterTable +ALTER TABLE "notifications" DROP COLUMN "integrationType", +ADD COLUMN "sendToApp" BOOLEAN NOT NULL DEFAULT false, +ADD COLUMN "sendToEmail" BOOLEAN NOT NULL DEFAULT false; diff --git a/packages/db/prisma/migrations/20240926175415_renaming/migration.sql b/packages/db/prisma/migrations/20240926175415_renaming/migration.sql new file mode 100644 index 00000000..c9f6544d --- /dev/null +++ b/packages/db/prisma/migrations/20240926175415_renaming/migration.sql @@ -0,0 +1,14 @@ +/* + Warnings: + + - You are about to drop the column `type` on the `integrations` table. All the data in the column will be lost. + - You are about to drop the column `settings` on the `notification_settings` table. All the data in the column will be lost. + - Added the required column `config` to the `notification_settings` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "integrations" DROP COLUMN "type"; + +-- AlterTable +ALTER TABLE "notification_settings" DROP COLUMN "settings", +ADD COLUMN "config" JSONB NOT NULL; diff --git a/packages/db/prisma/migrations/20240927190558_rename_notification_controls/migration.sql b/packages/db/prisma/migrations/20240927190558_rename_notification_controls/migration.sql new file mode 100644 index 00000000..392a3e7f --- /dev/null +++ b/packages/db/prisma/migrations/20240927190558_rename_notification_controls/migration.sql @@ -0,0 +1,55 @@ +/* + Warnings: + + - You are about to drop the `_IntegrationToNotificationControl` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `notification_settings` table. If the table is not empty, all the data it contains will be lost. + +*/ +-- DropForeignKey +ALTER TABLE "_IntegrationToNotificationControl" DROP CONSTRAINT "_IntegrationToNotificationControl_A_fkey"; + +-- DropForeignKey +ALTER TABLE "_IntegrationToNotificationControl" DROP CONSTRAINT "_IntegrationToNotificationControl_B_fkey"; + +-- DropForeignKey +ALTER TABLE "notification_settings" DROP CONSTRAINT "notification_settings_projectId_fkey"; + +-- DropTable +DROP TABLE "_IntegrationToNotificationControl"; + +-- DropTable +DROP TABLE "notification_settings"; + +-- CreateTable +CREATE TABLE "notification_rules" ( + "id" UUID NOT NULL DEFAULT gen_random_uuid(), + "projectId" TEXT NOT NULL, + "sendToApp" BOOLEAN NOT NULL DEFAULT false, + "sendToEmail" BOOLEAN NOT NULL DEFAULT false, + "config" JSONB NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "notification_rules_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_IntegrationToNotificationRule" ( + "A" UUID NOT NULL, + "B" UUID NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_IntegrationToNotificationRule_AB_unique" ON "_IntegrationToNotificationRule"("A", "B"); + +-- CreateIndex +CREATE INDEX "_IntegrationToNotificationRule_B_index" ON "_IntegrationToNotificationRule"("B"); + +-- AddForeignKey +ALTER TABLE "notification_rules" ADD CONSTRAINT "notification_rules_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "projects"("id") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_IntegrationToNotificationRule" ADD CONSTRAINT "_IntegrationToNotificationRule_A_fkey" FOREIGN KEY ("A") REFERENCES "integrations"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_IntegrationToNotificationRule" ADD CONSTRAINT "_IntegrationToNotificationRule_B_fkey" FOREIGN KEY ("B") REFERENCES "notification_rules"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/db/prisma/migrations/20240927195752_add_name_to_rules/migration.sql b/packages/db/prisma/migrations/20240927195752_add_name_to_rules/migration.sql new file mode 100644 index 00000000..5812fcc6 --- /dev/null +++ b/packages/db/prisma/migrations/20240927195752_add_name_to_rules/migration.sql @@ -0,0 +1,8 @@ +/* + Warnings: + + - Added the required column `name` to the `notification_rules` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "notification_rules" ADD COLUMN "name" TEXT NOT NULL; diff --git a/packages/db/prisma/migrations/20241001215748_add_payload_to_notifications/migration.sql b/packages/db/prisma/migrations/20241001215748_add_payload_to_notifications/migration.sql new file mode 100644 index 00000000..8ea8501b --- /dev/null +++ b/packages/db/prisma/migrations/20241001215748_add_payload_to_notifications/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "notifications" ADD COLUMN "payload" JSONB; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index 714d993c..181e6314 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -5,6 +5,10 @@ generator client { provider = "prisma-client-js" } +generator json { + provider = "prisma-json-types-generator" +} + datasource db { provider = "postgresql" url = env("DATABASE_URL") @@ -30,6 +34,7 @@ model Organization { Client Client[] Dashboard Dashboard[] ShareOverview ShareOverview[] + integrations Integration[] @@map("organizations") } @@ -87,6 +92,9 @@ model Project { references Reference[] access ProjectAccess[] + notificationRules NotificationRule[] + notifications Notification[] + createdAt DateTime @default(now()) updatedAt DateTime @default(now()) @updatedAt @@ -299,3 +307,59 @@ model Reference { @@map("references") } + +enum IntegrationType { + app + mail + custom +} + +model NotificationRule { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + name String + projectId String + project Project @relation(fields: [projectId], references: [id]) + integrations Integration[] + sendToApp Boolean @default(false) + sendToEmail Boolean @default(false) + /// [IPrismaNotificationRuleConfig] + config Json + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + + @@map("notification_rules") +} + +model Notification { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + projectId String + project Project @relation(fields: [projectId], references: [id]) + title String + message String + isReadAt DateTime? + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + sendToApp Boolean @default(false) + sendToEmail Boolean @default(false) + integration Integration? @relation(fields: [integrationId], references: [id]) + integrationId String? @db.Uuid + /// [IPrismaNotificationPayload] + payload Json? + + @@map("notifications") +} + +model Integration { + id String @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid + name String + /// [IPrismaIntegrationConfig] + config Json + organization Organization @relation(fields: [organizationId], references: [id]) + organizationId String + notificationRules NotificationRule[] + notifications Notification[] + createdAt DateTime @default(now()) + updatedAt DateTime @default(now()) @updatedAt + + @@map("integrations") +} diff --git a/packages/db/src/services/chart.service.ts b/packages/db/src/services/chart.service.ts index c2ae3c3a..6fa93a4b 100644 --- a/packages/db/src/services/chart.service.ts +++ b/packages/db/src/services/chart.service.ts @@ -1,6 +1,9 @@ import { escape } from 'sqlstring'; -import { getTimezoneFromDateString } from '@openpanel/common'; +import { + getTimezoneFromDateString, + stripLeadingAndTrailingSlashes, +} from '@openpanel/common'; import type { IChartEventFilter, IGetChartDataInput, @@ -328,7 +331,10 @@ export function getEventFiltersWhereClause(filters: IChartEventFilter[]) { } case 'regex': { where[id] = value - .map((val) => `match(${name}, ${escape(String(val).trim())})`) + .map( + (val) => + `match(${name}, ${escape(stripLeadingAndTrailingSlashes(String(val)).trim())})`, + ) .join(' OR '); break; } diff --git a/packages/db/src/services/event.service.ts b/packages/db/src/services/event.service.ts index b1aecdca..45f7effb 100644 --- a/packages/db/src/services/event.service.ts +++ b/packages/db/src/services/event.service.ts @@ -17,8 +17,8 @@ import type { EventMeta, Prisma } from '../prisma-client'; import { db } from '../prisma-client'; import { createSqlBuilder } from '../sql-builder'; import { getEventFiltersWhereClause } from './chart.service'; -import { getProfiles, upsertProfile } from './profile.service'; import type { IServiceProfile } from './profile.service'; +import { getProfiles, upsertProfile } from './profile.service'; export type IImportedEvent = Omit< IClickhouseEvent, diff --git a/packages/db/src/services/notification.service.ts b/packages/db/src/services/notification.service.ts new file mode 100644 index 00000000..82b9fadc --- /dev/null +++ b/packages/db/src/services/notification.service.ts @@ -0,0 +1,314 @@ +import { stripLeadingAndTrailingSlashes } from '@openpanel/common'; +import { notificationQueue } from '@openpanel/queue'; +import { cacheable } from '@openpanel/redis'; +import type { IChartEvent, IChartEventFilter } from '@openpanel/validation'; +import { pathOr } from 'ramda'; +import { + type Integration, + type Notification, + Prisma, + db, +} from '../prisma-client'; +import type { + IServiceCreateEventPayload, + IServiceEvent, +} from './event.service'; +import { getProjectByIdCached } from './project.service'; + +type ICreateNotification = Pick< + Notification, + 'projectId' | 'title' | 'message' | 'integrationId' | 'payload' +>; + +export type INotificationPayload = + | { + type: 'event'; + event: IServiceCreateEventPayload; + } + | { + type: 'funnel'; + funnel: IServiceEvent[]; + }; + +export const APP_NOTIFICATION_INTEGRATION_ID = 'app'; +export const EMAIL_NOTIFICATION_INTEGRATION_ID = 'email'; + +export const BASE_INTEGRATIONS: Integration[] = [ + { + id: APP_NOTIFICATION_INTEGRATION_ID, + name: 'App', + createdAt: new Date(), + updatedAt: new Date(), + config: { + type: APP_NOTIFICATION_INTEGRATION_ID, + }, + organizationId: '', + }, + { + id: EMAIL_NOTIFICATION_INTEGRATION_ID, + name: 'Email', + createdAt: new Date(), + updatedAt: new Date(), + config: { + type: EMAIL_NOTIFICATION_INTEGRATION_ID, + }, + organizationId: '', + }, +]; + +export const isBaseIntegration = (id: string) => + BASE_INTEGRATIONS.find((i) => i.id === id); + +export const getNotificationRulesByProjectId = cacheable( + function getNotificationRulesByProjectId(projectId: string) { + return db.notificationRule.findMany({ + where: { + projectId, + }, + select: { + id: true, + name: true, + sendToApp: true, + sendToEmail: true, + config: true, + integrations: { + select: { + id: true, + }, + }, + }, + }); + }, + 60 * 24, +); + +function getIntegration(integrationId: string | null) { + if (integrationId === APP_NOTIFICATION_INTEGRATION_ID) { + return { + integrationId: null, + sendToApp: true, + sendToEmail: false, + }; + } + + if (integrationId === EMAIL_NOTIFICATION_INTEGRATION_ID) { + return { + integrationId: null, + sendToApp: false, + sendToEmail: true, + }; + } + + return { + sendToApp: false, + sendToEmail: false, + integrationId, + }; +} + +export async function createNotification(notification: ICreateNotification) { + const res = await db.notification.create({ + data: { + title: notification.title, + message: notification.message, + projectId: notification.projectId, + payload: notification.payload || Prisma.DbNull, + ...getIntegration(notification.integrationId), + }, + }); + + return triggerNotification(res); +} + +export function triggerNotification(notification: Notification) { + return notificationQueue.add('sendNotification', { + type: 'sendNotification', + payload: { + notification, + }, + }); +} + +function matchEventFilters( + event: IServiceCreateEventPayload, + filters: IChartEventFilter[], +) { + return filters.every((filter) => { + const { name, value, operator } = filter; + + if (value.length === 0) return true; + + if (name === 'has_profile') { + if (value.includes('true')) { + return event.profileId !== event.deviceId; + } + return event.profileId === event.deviceId; + } + + const propertyValue = ( + name.startsWith('properties.') + ? pathOr('', name.split('.'), event) + : pathOr('', [name], event) + ).trim(); + + switch (operator) { + case 'is': + return value.includes(propertyValue); + case 'isNot': + return !value.includes(propertyValue); + case 'contains': + return value.some((val) => propertyValue.includes(String(val))); + case 'doesNotContain': + return !value.some((val) => propertyValue.includes(String(val))); + case 'startsWith': + return value.some((val) => propertyValue.startsWith(String(val))); + case 'endsWith': + return value.some((val) => propertyValue.endsWith(String(val))); + case 'regex': { + return value + .map((val) => stripLeadingAndTrailingSlashes(String(val))) + .some((val) => new RegExp(val).test(propertyValue)); + } + default: + return false; + } + }); +} + +export function matchEvent( + event: IServiceCreateEventPayload, + chartEvent: IChartEvent, +) { + if (event.name !== chartEvent.name) { + return false; + } + + if (chartEvent.filters.length > 0) { + return matchEventFilters(event, chartEvent.filters); + } + + return true; +} + +export async function checkNotificationRulesForEvent( + payload: IServiceCreateEventPayload, +) { + const project = await getProjectByIdCached(payload.projectId); + const rules = await getNotificationRulesByProjectId(payload.projectId); + await Promise.all( + rules.flatMap((rule) => { + if (rule.config.type === 'events') { + const match = rule.config.events.find((event) => { + return matchEvent(payload, event); + }); + + if (!match) { + return []; + } + + const notification = { + title: `You received a new "${payload.name}" event`, + message: project?.name ? `Project: ${project?.name}` : '', + projectId: payload.projectId, + payload: { + type: 'event', + event: payload, + }, + } as const; + + const promises = rule.integrations.map((integration) => + createNotification({ + ...notification, + integrationId: integration.id, + }), + ); + + if (rule.sendToApp) { + promises.push( + createNotification({ + ...notification, + integrationId: APP_NOTIFICATION_INTEGRATION_ID, + }), + ); + } + + if (rule.sendToEmail) { + promises.push( + createNotification({ + ...notification, + integrationId: EMAIL_NOTIFICATION_INTEGRATION_ID, + }), + ); + } + + return promises; + } + }), + ); +} + +export async function checkNotificationRulesForSessionEnd( + events: IServiceEvent[], +) { + const sortedEvents = events.sort( + (a, b) => a.createdAt.getTime() - b.createdAt.getTime(), + ); + const projectId = sortedEvents[0]?.projectId; + if (!projectId) return null; + + const [project, rules] = await Promise.all([ + getProjectByIdCached(projectId), + getNotificationRulesByProjectId(projectId), + ]); + + const funnelRules = rules.filter((rule) => rule.config.type === 'funnel'); + + const notificationPromises = funnelRules.flatMap((rule) => { + // Match funnel events + let funnelIndex = 0; + const matchedEvents = []; + for (const event of sortedEvents) { + if (matchEvent(event, rule.config.events[funnelIndex]!)) { + matchedEvents.push(event); + funnelIndex++; + if (funnelIndex === rule.config.events.length) break; + } + } + + // If funnel not completed, skip this rule + if (funnelIndex < rule.config.events.length) return []; + + // Create notification object + const notification = { + title: `Funnel "${rule.name}" completed`, + message: project?.name ? `Project: ${project?.name}` : '', + projectId, + payload: { type: 'funnel', funnel: matchedEvents } as const, + }; + + // Generate notification promises + return [ + ...rule.integrations.map((integration) => + createNotification({ ...notification, integrationId: integration.id }), + ), + ...(rule.sendToApp + ? [ + createNotification({ + ...notification, + integrationId: APP_NOTIFICATION_INTEGRATION_ID, + }), + ] + : []), + ...(rule.sendToEmail + ? [ + createNotification({ + ...notification, + integrationId: EMAIL_NOTIFICATION_INTEGRATION_ID, + }), + ] + : []), + ]; + }); + + await Promise.all(notificationPromises); +} diff --git a/packages/db/src/services/project.service.ts b/packages/db/src/services/project.service.ts index 67d4b696..de2bdc3a 100644 --- a/packages/db/src/services/project.service.ts +++ b/packages/db/src/services/project.service.ts @@ -1,5 +1,6 @@ import { auth } from '@clerk/nextjs/server'; +import { cacheable } from '@openpanel/redis'; import type { Prisma, Project } from '../prisma-client'; import { db } from '../prisma-client'; @@ -24,6 +25,8 @@ export async function getProjectById(id: string) { return res; } +export const getProjectByIdCached = cacheable(getProjectById, 60 * 60 * 24); + export async function getProjectWithClients(id: string) { const res = await db.project.findUnique({ where: { diff --git a/packages/db/src/types.ts b/packages/db/src/types.ts new file mode 100644 index 00000000..d65f7ca9 --- /dev/null +++ b/packages/db/src/types.ts @@ -0,0 +1,13 @@ +import type { + IIntegrationConfig, + INotificationRuleConfig, +} from '@openpanel/validation'; +import type { INotificationPayload } from './services/notification.service'; + +declare global { + namespace PrismaJson { + type IPrismaNotificationRuleConfig = INotificationRuleConfig; + type IPrismaIntegrationConfig = IIntegrationConfig; + type IPrismaNotificationPayload = INotificationPayload; + } +} diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json index a291eef2..56c2e45b 100644 --- a/packages/db/tsconfig.json +++ b/packages/db/tsconfig.json @@ -7,6 +7,6 @@ }, "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" }, - "include": ["."], + "include": [".", "./src/types.ts"], "exclude": ["node_modules"] } diff --git a/packages/integrations/index.ts b/packages/integrations/index.ts new file mode 100644 index 00000000..3190b54d --- /dev/null +++ b/packages/integrations/index.ts @@ -0,0 +1,2 @@ +// Empty, import directly from src/ +export {}; diff --git a/packages/integrations/package.json b/packages/integrations/package.json new file mode 100644 index 00000000..f7c3b5e6 --- /dev/null +++ b/packages/integrations/package.json @@ -0,0 +1,18 @@ +{ + "name": "@openpanel/integrations", + "version": "0.0.1", + "main": "index.ts", + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@slack/bolt": "^3.18.0", + "@slack/oauth": "^3.0.0" + }, + "devDependencies": { + "@openpanel/tsconfig": "workspace:*", + "@openpanel/validation": "workspace:*", + "@types/node": "^18.16.0", + "typescript": "^5.2.2" + } +} diff --git a/packages/integrations/src/discord.ts b/packages/integrations/src/discord.ts new file mode 100644 index 00000000..58087f5f --- /dev/null +++ b/packages/integrations/src/discord.ts @@ -0,0 +1,34 @@ +// Cred to (@OpenStatusHQ) https://github.com/openstatusHQ/openstatus/blob/main/packages/notifications/discord/src/index.ts + +export function sendDiscordNotification({ + webhookUrl, + message, +}: { + webhookUrl: string; + message: string; +}) { + return fetch(webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + content: message, + avatar_url: 'https://openpanel.dev/logo.jpg', + username: 'OpenPanel Notifications', + }), + }).catch((err) => { + return { + ok: false, + json: () => Promise.resolve({}), + }; + }); +} + +export function sendTestDiscordNotification(webhookUrl: string) { + return sendDiscordNotification({ + webhookUrl, + message: + '**๐Ÿงช Test [OpenPanel.dev]()**\nIf you can read this, your Slack webhook is functioning correctly!\n', + }); +} diff --git a/packages/integrations/src/slack.ts b/packages/integrations/src/slack.ts new file mode 100644 index 00000000..4b02f385 --- /dev/null +++ b/packages/integrations/src/slack.ts @@ -0,0 +1,50 @@ +// Cred to (@c_alares) https://github.com/christianalares/seventy-seven/blob/main/packages/integrations/src/slack/index.ts + +import { LogLevel, App as SlackApp } from '@slack/bolt'; +import { InstallProvider } from '@slack/oauth'; + +const SLACK_CLIENT_ID = process.env.SLACK_CLIENT_ID; +const SLACK_CLIENT_SECRET = process.env.SLACK_CLIENT_SECRET; +const SLACK_OAUTH_REDIRECT_URL = process.env.SLACK_OAUTH_REDIRECT_URL; +const SLACK_STATE_SECRET = process.env.SLACK_STATE_SECRET; + +export const slackInstaller = new InstallProvider({ + clientId: SLACK_CLIENT_ID!, + clientSecret: SLACK_CLIENT_SECRET!, + stateSecret: SLACK_STATE_SECRET, + logLevel: process.env.NODE_ENV === 'development' ? LogLevel.DEBUG : undefined, +}); + +export const getSlackInstallUrl = ({ + integrationId, + organizationId, +}: { integrationId: string; organizationId: string }) => { + return slackInstaller.generateInstallUrl({ + scopes: [ + 'incoming-webhook', + 'chat:write', + 'chat:write.public', + 'team:read', + ], + redirectUri: SLACK_OAUTH_REDIRECT_URL, + metadata: JSON.stringify({ integrationId, organizationId }), + }); +}; + +export function sendSlackNotification({ + webhookUrl, + message, +}: { + webhookUrl: string; + message: string; +}) { + return fetch(webhookUrl, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + text: message, + }), + }); +} diff --git a/packages/integrations/tsconfig.json b/packages/integrations/tsconfig.json new file mode 100644 index 00000000..b1ff68c0 --- /dev/null +++ b/packages/integrations/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "@openpanel/tsconfig/base.json", + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + }, + "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" + }, + "include": ["."], + "exclude": ["node_modules"] +} diff --git a/packages/queue/index.ts b/packages/queue/index.ts index 9db794d7..773320ba 100644 --- a/packages/queue/index.ts +++ b/packages/queue/index.ts @@ -1,9 +1,4 @@ -export { - eventsQueue, - cronQueue, - sessionsQueue, - sessionsQueueEvents, -} from './src/queues'; +export * from './src/queues'; export type * from './src/queues'; export { findJobByPrefix } from './src/utils'; export type { JobsOptions } from 'bullmq'; diff --git a/packages/queue/src/queues.ts b/packages/queue/src/queues.ts index 434c8bd7..9f82d13d 100644 --- a/packages/queue/src/queues.ts +++ b/packages/queue/src/queues.ts @@ -1,6 +1,6 @@ import { Queue, QueueEvents } from 'bullmq'; -import type { IServiceEvent } from '@openpanel/db'; +import type { IServiceEvent, Notification } from '@openpanel/db'; import { getRedisQueue } from '@openpanel/redis'; import type { TrackPayload } from '@openpanel/sdk'; @@ -90,3 +90,20 @@ export const cronQueue = new Queue('cron', { removeOnComplete: 10, }, }); + +export type NotificationQueuePayload = { + type: 'sendNotification'; + payload: { + notification: Notification; + }; +}; + +export const notificationQueue = new Queue( + 'notification', + { + connection: getRedisQueue(), + defaultJobOptions: { + removeOnComplete: 10, + }, + }, +); diff --git a/packages/trpc/package.json b/packages/trpc/package.json index 9fec4155..952b3671 100644 --- a/packages/trpc/package.json +++ b/packages/trpc/package.json @@ -10,8 +10,9 @@ "@openpanel/common": "workspace:*", "@openpanel/constants": "workspace:*", "@openpanel/db": "workspace:*", - "@openpanel/validation": "workspace:*", + "@openpanel/integrations": "workspace:^", "@openpanel/redis": "workspace:*", + "@openpanel/validation": "workspace:*", "@seventy-seven/sdk": "0.0.0-beta.2", "@trpc/server": "^10.45.1", "date-fns": "^3.3.1", diff --git a/packages/trpc/src/root.ts b/packages/trpc/src/root.ts index 6f6e1212..a9964385 100644 --- a/packages/trpc/src/root.ts +++ b/packages/trpc/src/root.ts @@ -2,6 +2,8 @@ import { chartRouter } from './routers/chart'; import { clientRouter } from './routers/client'; import { dashboardRouter } from './routers/dashboard'; import { eventRouter } from './routers/event'; +import { integrationRouter } from './routers/integration'; +import { notificationRouter } from './routers/notification'; import { onboardingRouter } from './routers/onboarding'; import { organizationRouter } from './routers/organization'; import { profileRouter } from './routers/profile'; @@ -32,6 +34,8 @@ export const appRouter = createTRPCRouter({ onboarding: onboardingRouter, reference: referenceRouter, ticket: ticketRouter, + notification: notificationRouter, + integration: integrationRouter, }); // export type definition of API diff --git a/packages/trpc/src/routers/integration.ts b/packages/trpc/src/routers/integration.ts new file mode 100644 index 00000000..f6270b02 --- /dev/null +++ b/packages/trpc/src/routers/integration.ts @@ -0,0 +1,137 @@ +import { z } from 'zod'; + +import { BASE_INTEGRATIONS, db } from '@openpanel/db'; + +import { getSlackInstallUrl } from '@openpanel/integrations/src/slack'; +import { + type ISlackConfig, + zCreateDiscordIntegration, + zCreateSlackIntegration, + zCreateWebhookIntegration, +} from '@openpanel/validation'; +import { getOrganizationAccessCached } from '../access'; +import { TRPCAccessError } from '../errors'; +import { createTRPCRouter, protectedProcedure } from '../trpc'; + +export const integrationRouter = createTRPCRouter({ + get: protectedProcedure + .input(z.object({ id: z.string() })) + .query(async ({ input, ctx }) => { + const integration = await db.integration.findUniqueOrThrow({ + where: { + id: input.id, + }, + }); + + const access = await getOrganizationAccessCached({ + userId: ctx.session.userId, + organizationId: integration.organizationId, + }); + + if (!access) { + throw TRPCAccessError('You do not have access to this project'); + } + + return integration; + }), + list: protectedProcedure + .input(z.object({ organizationId: z.string() })) + .query(async ({ input }) => { + const integrations = await db.integration.findMany({ + where: { + organizationId: input.organizationId, + }, + }); + + return [...BASE_INTEGRATIONS, ...integrations]; + }), + createOrUpdateSlack: protectedProcedure + .input(zCreateSlackIntegration) + .mutation(async ({ input }) => { + if (input.id) { + const res = await db.integration.update({ + where: { + id: input.id, + organizationId: input.organizationId, + }, + data: { + name: input.name, + // This is empty and will be filled by the webhook + config: {} as ISlackConfig, + }, + }); + + return { + ...res, + slackInstallUrl: await getSlackInstallUrl({ + integrationId: res.id, + organizationId: input.organizationId, + }), + }; + } + + const res = await db.integration.create({ + data: { + name: input.name, + organizationId: input.organizationId, + // This is empty and will be filled by the webhook + config: {} as ISlackConfig, + }, + }); + + return { + ...res, + slackInstallUrl: await getSlackInstallUrl({ + integrationId: res.id, + organizationId: input.organizationId, + }), + }; + }), + createOrUpdate: protectedProcedure + .input(z.union([zCreateDiscordIntegration, zCreateWebhookIntegration])) + .mutation(async ({ input }) => { + if (input.id) { + return db.integration.update({ + where: { + id: input.id, + organizationId: input.organizationId, + }, + data: { + name: input.name, + config: input.config, + }, + }); + } + return db.integration.create({ + data: { + name: input.name, + organizationId: input.organizationId, + config: input.config, + }, + }); + }), + delete: protectedProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ input: { id }, ctx }) => { + const integration = await db.integration.findUniqueOrThrow({ + where: { + id, + }, + }); + + const access = await getOrganizationAccessCached({ + userId: ctx.session.userId, + organizationId: integration.organizationId, + }); + + if (!access) { + throw TRPCAccessError('You do not have access to this project'); + } + + return db.integration.delete({ + where: { + id, + }, + }); + }), +}); diff --git a/packages/trpc/src/routers/notification.ts b/packages/trpc/src/routers/notification.ts new file mode 100644 index 00000000..38b38e20 --- /dev/null +++ b/packages/trpc/src/routers/notification.ts @@ -0,0 +1,150 @@ +import { z } from 'zod'; + +import { + APP_NOTIFICATION_INTEGRATION_ID, + BASE_INTEGRATIONS, + EMAIL_NOTIFICATION_INTEGRATION_ID, + db, + getNotificationRulesByProjectId, + isBaseIntegration, +} from '@openpanel/db'; +import { zCreateNotificationRule } from '@openpanel/validation'; + +import { getProjectAccess } from '../access'; +import { TRPCAccessError } from '../errors'; +import { createTRPCRouter, protectedProcedure } from '../trpc'; + +export const notificationRouter = createTRPCRouter({ + list: protectedProcedure + .input(z.object({ projectId: z.string() })) + .query(async ({ input }) => { + return db.notification.findMany({ + where: { + projectId: input.projectId, + sendToApp: true, + }, + orderBy: { + createdAt: 'desc', + }, + include: { + integration: { + include: { + notificationRules: { + select: { + name: true, + }, + }, + }, + }, + }, + take: 100, + }); + }), + rules: protectedProcedure + .input(z.object({ projectId: z.string() })) + .query(async ({ input }) => { + return db.notificationRule + .findMany({ + where: { + projectId: input.projectId, + }, + include: { + integrations: true, + }, + orderBy: { + createdAt: 'desc', + }, + }) + .then((rules) => { + return rules.map((rule) => { + return { + ...rule, + integrations: [ + ...BASE_INTEGRATIONS.filter((integration) => { + return ( + (integration.id === APP_NOTIFICATION_INTEGRATION_ID && + rule.sendToApp) || + (integration.id === EMAIL_NOTIFICATION_INTEGRATION_ID && + rule.sendToEmail) + ); + }), + ...rule.integrations, + ], + }; + }); + }); + }), + createOrUpdateRule: protectedProcedure + .input(zCreateNotificationRule) + .mutation(async ({ input }) => { + // Clear the cache for the project + await getNotificationRulesByProjectId.clear(input.projectId); + + if (input.id) { + return db.notificationRule.update({ + where: { + id: input.id, + }, + data: { + name: input.name, + projectId: input.projectId, + sendToApp: !!input.integrations.find( + (id) => id === APP_NOTIFICATION_INTEGRATION_ID, + ), + sendToEmail: !!input.integrations.find( + (id) => id === EMAIL_NOTIFICATION_INTEGRATION_ID, + ), + integrations: { + set: input.integrations + .filter((id) => !isBaseIntegration(id)) + .map((id) => ({ id })), + }, + config: input.config, + }, + }); + } + + return db.notificationRule.create({ + data: { + name: input.name, + projectId: input.projectId, + sendToApp: !!input.integrations.find( + (id) => id === APP_NOTIFICATION_INTEGRATION_ID, + ), + sendToEmail: !!input.integrations.find( + (id) => id === EMAIL_NOTIFICATION_INTEGRATION_ID, + ), + integrations: { + connect: input.integrations + .filter((id) => !isBaseIntegration(id)) + .map((id) => ({ id })), + }, + config: input.config, + }, + }); + }), + deleteRule: protectedProcedure + .input(z.object({ id: z.string() })) + .mutation(async ({ input: { id }, ctx }) => { + const rule = await db.notificationRule.findUniqueOrThrow({ + where: { + id, + }, + }); + + const access = await getProjectAccess({ + userId: ctx.session.userId, + projectId: rule.projectId, + }); + + if (!access) { + throw TRPCAccessError('You do not have access to this project'); + } + + return db.notificationRule.delete({ + where: { + id, + }, + }); + }), +}); diff --git a/packages/trpc/src/routers/project.ts b/packages/trpc/src/routers/project.ts index ca0b570b..8be7d864 100644 --- a/packages/trpc/src/routers/project.ts +++ b/packages/trpc/src/routers/project.ts @@ -1,6 +1,11 @@ import { z } from 'zod'; -import { db, getId, getProjectsByOrganizationSlug } from '@openpanel/db'; +import { + db, + getId, + getProjectByIdCached, + getProjectsByOrganizationSlug, +} from '@openpanel/db'; import { getProjectAccess } from '../access'; import { TRPCAccessError } from '../errors'; @@ -34,8 +39,7 @@ export const projectRouter = createTRPCRouter({ if (!access) { throw TRPCAccessError('You do not have access to this project'); } - - return db.project.update({ + const res = await db.project.update({ where: { id: input.id, }, @@ -43,6 +47,8 @@ export const projectRouter = createTRPCRouter({ name: input.name, }, }); + await getProjectByIdCached.clear(input.id); + return res; }), create: protectedProcedure .input( diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index 3392a47e..b6085c4b 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -148,3 +148,119 @@ export const zOnboardingProject = z } } }); + +export const zSlackAuthResponse = z.object({ + ok: z.literal(true), + app_id: z.string(), + authed_user: z.object({ + id: z.string(), + }), + scope: z.string(), + token_type: z.literal('bot'), + access_token: z.string(), + bot_user_id: z.string(), + team: z.object({ + id: z.string(), + name: z.string(), + }), + incoming_webhook: z.object({ + channel: z.string(), + channel_id: z.string(), + configuration_url: z.string().url(), + url: z.string().url(), + }), +}); + +export const zSlackConfig = z + .object({ + type: z.literal('slack'), + }) + .merge(zSlackAuthResponse); + +export type ISlackConfig = z.infer; + +export const zWebhookConfig = z.object({ + type: z.literal('webhook'), + url: z.string().url(), + headers: z.record(z.string()), + payload: z.record(z.string(), z.unknown()), +}); +export type IWebhookConfig = z.infer; + +export const zDiscordConfig = z.object({ + type: z.literal('discord'), + url: z.string().url(), +}); +export type IDiscordConfig = z.infer; + +export const zAppConfig = z.object({ + type: z.literal('app'), +}); +export type IAppConfig = z.infer; + +export const zEmailConfig = z.object({ + type: z.literal('email'), +}); +export type IEmailConfig = z.infer; + +export type IIntegrationConfig = + | ISlackConfig + | IDiscordConfig + | IWebhookConfig + | IAppConfig + | IEmailConfig; + +const zCreateIntegration = z.object({ + id: z.string().optional(), + name: z.string().min(1), + organizationId: z.string().min(1), +}); + +export const zCreateSlackIntegration = zCreateIntegration; + +export const zCreateWebhookIntegration = zCreateIntegration.merge( + z.object({ + config: zWebhookConfig, + }), +); + +export const zCreateDiscordIntegration = zCreateIntegration.merge( + z.object({ + config: zDiscordConfig, + }), +); + +export const zNotificationRuleEventConfig = z.object({ + type: z.literal('events'), + events: z.array(zChartEvent), +}); + +export type INotificationRuleEventConfig = z.infer< + typeof zNotificationRuleEventConfig +>; + +export const zNotificationRuleFunnelConfig = z.object({ + type: z.literal('funnel'), + events: z.array(zChartEvent).min(1), +}); + +export type INotificationRuleFunnelConfig = z.infer< + typeof zNotificationRuleFunnelConfig +>; + +export const zNotificationRuleConfig = z.discriminatedUnion('type', [ + zNotificationRuleEventConfig, + zNotificationRuleFunnelConfig, +]); + +export type INotificationRuleConfig = z.infer; + +export const zCreateNotificationRule = z.object({ + id: z.string().optional(), + name: z.string().min(1), + config: zNotificationRuleConfig, + integrations: z.array(z.string()), + sendToApp: z.boolean(), + sendToEmail: z.boolean(), + projectId: z.string(), +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ab1d9759..10c30f65 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -51,6 +51,9 @@ importers: '@openpanel/db': specifier: workspace:* version: link:../../packages/db + '@openpanel/integrations': + specifier: workspace:^ + version: link:../../packages/integrations '@openpanel/logger': specifier: workspace:* version: link:../../packages/logger @@ -172,6 +175,9 @@ importers: '@openpanel/db': specifier: workspace:^ version: link:../../packages/db + '@openpanel/integrations': + specifier: workspace:^ + version: link:../../packages/integrations '@openpanel/nextjs': specifier: 1.0.3 version: 1.0.3(next@14.2.1)(react-dom@18.2.0)(react@18.2.0) @@ -676,6 +682,9 @@ importers: '@openpanel/db': specifier: workspace:* version: link:../../packages/db + '@openpanel/integrations': + specifier: workspace:^ + version: link:../../packages/integrations '@openpanel/logger': specifier: workspace:* version: link:../../packages/logger @@ -862,6 +871,9 @@ importers: '@openpanel/logger': specifier: workspace:* version: link:../logger + '@openpanel/queue': + specifier: workspace:^ + version: link:../queue '@openpanel/redis': specifier: workspace:* version: link:../redis @@ -871,6 +883,9 @@ importers: '@prisma/client': specifier: ^5.1.1 version: 5.9.1(prisma@5.9.1) + prisma-json-types-generator: + specifier: ^3.1.1 + version: 3.1.1(prisma@5.9.1)(typescript@5.3.3) ramda: specifier: ^0.29.1 version: 0.29.1 @@ -906,6 +921,28 @@ importers: specifier: ^5.2.2 version: 5.3.3 + packages/integrations: + dependencies: + '@slack/bolt': + specifier: ^3.18.0 + version: 3.21.4 + '@slack/oauth': + specifier: ^3.0.0 + version: 3.0.1 + devDependencies: + '@openpanel/tsconfig': + specifier: workspace:* + version: link:../../tooling/typescript + '@openpanel/validation': + specifier: workspace:* + version: link:../validation + '@types/node': + specifier: ^18.16.0 + version: 18.19.17 + typescript: + specifier: ^5.2.2 + version: 5.3.3 + packages/logger: dependencies: '@hyperdx/node-opentelemetry': @@ -1015,7 +1052,7 @@ importers: packages/sdks/nextjs: dependencies: '@openpanel/web': - specifier: 1.0.0-local + specifier: 1.0.1-local version: link:../web next: specifier: ^12.0.0 || ^13.0.0 || ^14.0.0 @@ -1116,6 +1153,9 @@ importers: '@openpanel/db': specifier: workspace:* version: link:../db + '@openpanel/integrations': + specifier: workspace:^ + version: link:../integrations '@openpanel/redis': specifier: workspace:* version: link:../redis @@ -1359,7 +1399,7 @@ packages: '@babel/core': 7.23.9 '@babel/helper-compilation-targets': 7.23.6 '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.4 + debug: 4.3.7 lodash.debounce: 4.0.8 resolve: 1.22.8 transitivePeerDependencies: @@ -3086,7 +3126,7 @@ packages: resolution: {integrity: sha512-Txumi3td7J4A/xTTwlssKieHKTGl3j4A1tglBx72auZ49YK7ePY6XZricgIg9mnZT4xPfA+UPCUdnhRuEFDL+w==} requiresBuild: true dependencies: - tslib: 2.6.2 + tslib: 2.7.0 dev: false optional: true @@ -3516,7 +3556,7 @@ packages: rimraf: 2.7.1 sudo-prompt: 8.2.5 tmp: 0.0.33 - tslib: 2.6.2 + tslib: 2.7.0 transitivePeerDependencies: - supports-color dev: false @@ -5807,6 +5847,10 @@ packages: prisma: 5.9.1 dev: false + /@prisma/debug@5.20.0: + resolution: {integrity: sha512-oCx79MJ4HSujokA8S1g0xgZUGybD4SyIOydoHMngFYiwEwYDQ5tBQkK5XoEHuwOYDKUOKRn/J0MEymckc4IgsQ==} + dev: false + /@prisma/debug@5.9.1: resolution: {integrity: sha512-yAHFSFCg8KVoL0oRUno3m60GAjsUKYUDkQ+9BA2X2JfVR3kRVSJFc/GpQ2fSORi4pSHZR9orfM4UC9OVXIFFTA==} @@ -5829,6 +5873,12 @@ packages: '@prisma/engines-version': 5.9.0-32.23fdc5965b1e05fc54e5f26ed3de66776b93de64 '@prisma/get-platform': 5.9.1 + /@prisma/generator-helper@5.20.0: + resolution: {integrity: sha512-37Aibw0wVRQgQVtCdNAIN71YFnSQfvetok7vd95KKkYkQRbEx94gsvPDpyN9Mw7p3IwA3nFgPfLc3jBRztUkKw==} + dependencies: + '@prisma/debug': 5.20.0 + dev: false + /@prisma/get-platform@5.9.1: resolution: {integrity: sha512-6OQsNxTyhvG+T2Ksr8FPFpuPeL4r9u0JF0OZHUBI/Uy9SS43sPyAIutt4ZEAyqWQt104ERh70EZedkHZKsnNbg==} dependencies: @@ -7735,6 +7785,134 @@ packages: '@sinonjs/commons': 3.0.1 dev: false + /@slack/bolt@3.21.4: + resolution: {integrity: sha512-4PqOuHXcVt8KxjKiLdLIqZp8285zdiYLj7HrrKvVHnUNbkD0l16HZxtMfIEe07REQ+vmM1mrqCiZqe9dPAMucA==} + engines: {node: '>=14.21.3', npm: '>=6.14.18'} + dependencies: + '@slack/logger': 4.0.0 + '@slack/oauth': 2.6.3 + '@slack/socket-mode': 1.3.6 + '@slack/types': 2.14.0 + '@slack/web-api': 6.12.1 + '@types/express': 4.17.21 + '@types/promise.allsettled': 1.0.6 + '@types/tsscmp': 1.0.2 + axios: 1.7.7 + express: 4.19.2 + path-to-regexp: 8.2.0 + promise.allsettled: 1.0.7 + raw-body: 2.5.2 + tsscmp: 1.0.6 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + dev: false + + /@slack/logger@3.0.0: + resolution: {integrity: sha512-DTuBFbqu4gGfajREEMrkq5jBhcnskinhr4+AnfJEk48zhVeEv3XnUKGIX98B74kxhYsIMfApGGySTn7V3b5yBA==} + engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} + dependencies: + '@types/node': 20.14.12 + dev: false + + /@slack/logger@4.0.0: + resolution: {integrity: sha512-Wz7QYfPAlG/DR+DfABddUZeNgoeY7d1J39OCR2jR+v7VBsB8ezulDK5szTnDDPDwLH5IWhLvXIHlCFZV7MSKgA==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + dependencies: + '@types/node': 20.14.12 + dev: false + + /@slack/oauth@2.6.3: + resolution: {integrity: sha512-1amXs6xRkJpoH6zSgjVPgGEJXCibKNff9WNDijcejIuVy1HFAl1adh7lehaGNiHhTWfQkfKxBiF+BGn56kvoFw==} + engines: {node: '>=12.13.0', npm: '>=6.12.0'} + dependencies: + '@slack/logger': 3.0.0 + '@slack/web-api': 6.12.1 + '@types/jsonwebtoken': 8.5.9 + '@types/node': 20.14.12 + jsonwebtoken: 9.0.2 + lodash.isstring: 4.0.1 + transitivePeerDependencies: + - debug + dev: false + + /@slack/oauth@3.0.1: + resolution: {integrity: sha512-TuR9PI6bYKX6qHC7FQI4keMnhj45TNfSNQtTU3mtnHUX4XLM2dYLvRkUNADyiLTle2qu2rsOQtCIsZJw6H0sDA==} + engines: {node: '>=18', npm: '>=8.6.0'} + dependencies: + '@slack/logger': 4.0.0 + '@slack/web-api': 7.5.0 + '@types/jsonwebtoken': 9.0.6 + '@types/node': 20.14.12 + jsonwebtoken: 9.0.2 + lodash.isstring: 4.0.1 + transitivePeerDependencies: + - debug + dev: false + + /@slack/socket-mode@1.3.6: + resolution: {integrity: sha512-G+im7OP7jVqHhiNSdHgv2VVrnN5U7KY845/5EZimZkrD4ZmtV0P3BiWkgeJhPtdLuM7C7i6+M6h6Bh+S4OOalA==} + engines: {node: '>=12.13.0', npm: '>=6.12.0'} + dependencies: + '@slack/logger': 3.0.0 + '@slack/web-api': 6.12.1 + '@types/node': 20.14.12 + '@types/ws': 7.4.7 + eventemitter3: 5.0.1 + finity: 0.5.4 + ws: 7.5.9 + transitivePeerDependencies: + - bufferutil + - debug + - utf-8-validate + dev: false + + /@slack/types@2.14.0: + resolution: {integrity: sha512-n0EGm7ENQRxlXbgKSrQZL69grzg1gHLAVd+GlRVQJ1NSORo0FrApR7wql/gaKdu2n4TO83Sq/AmeUOqD60aXUA==} + engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} + dev: false + + /@slack/web-api@6.12.1: + resolution: {integrity: sha512-dXHyHkvvziqkDdZlPRnUl/H2uvnUmdJ5B7kxiH1HIgHe18vcbUk1zjU/XCZgJFhxGeq5Zwa95Z+SbNW9mbRhtw==} + engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} + dependencies: + '@slack/logger': 3.0.0 + '@slack/types': 2.14.0 + '@types/is-stream': 1.1.0 + '@types/node': 20.14.12 + axios: 1.7.7 + eventemitter3: 3.1.2 + form-data: 2.5.1 + is-electron: 2.2.2 + is-stream: 1.1.0 + p-queue: 6.6.2 + p-retry: 4.6.2 + transitivePeerDependencies: + - debug + dev: false + + /@slack/web-api@7.5.0: + resolution: {integrity: sha512-e1aRwbdnTVz0uQownF8UoyrQFdSs3uXtkPYWCpcb3fW3KuTEGvmEtVzAvj9gqNSlgpWj0o6is7AdptQCELd/rQ==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + dependencies: + '@slack/logger': 4.0.0 + '@slack/types': 2.14.0 + '@types/node': 20.14.12 + '@types/retry': 0.12.0 + axios: 1.7.7 + eventemitter3: 5.0.1 + form-data: 4.0.0 + is-electron: 2.2.2 + is-stream: 2.0.1 + p-queue: 6.6.2 + p-retry: 4.6.2 + retry: 0.13.1 + transitivePeerDependencies: + - debug + dev: false + /@stablelib/base64@1.0.1: resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} dev: false @@ -7746,14 +7924,14 @@ packages: /@swc/helpers@0.5.2: resolution: {integrity: sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==} dependencies: - tslib: 2.6.2 + tslib: 2.7.0 dev: false /@swc/helpers@0.5.5: resolution: {integrity: sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==} dependencies: '@swc/counter': 0.1.3 - tslib: 2.6.2 + tslib: 2.7.0 dev: false /@t3-oss/env-core@0.7.3(typescript@5.3.3)(zod@3.22.4): @@ -8248,6 +8426,12 @@ packages: /@types/http-errors@2.0.4: resolution: {integrity: sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==} + /@types/is-stream@1.1.0: + resolution: {integrity: sha512-jkZatu4QVbR60mpIzjINmtS1ZF4a/FqdTUTBeQDVOQ2PYyidtwFKr0B5G6ERukKwliq+7mIXvxyppwzG5EgRYg==} + dependencies: + '@types/node': 20.14.12 + dev: false + /@types/istanbul-lib-coverage@2.0.6: resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} dev: false @@ -8272,11 +8456,16 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: false + /@types/jsonwebtoken@8.5.9: + resolution: {integrity: sha512-272FMnFGzAVMGtu9tkr29hRL6bZj4Zs1KZNeHLnKqAvp06tAIcarTMwOh8/8bz4FmKRcMxZhZNeUAQsNLoiPhg==} + dependencies: + '@types/node': 20.14.12 + dev: false + /@types/jsonwebtoken@9.0.6: resolution: {integrity: sha512-/5hndP5dCjloafCXns6SZyESp3Ldq7YjH3zwzwczYnjxIT0Fqzk5ROSYVGfFyczIue7IUEj8hkvLbPoLQ18vQw==} dependencies: '@types/node': 20.14.12 - dev: true /@types/katex@0.16.7: resolution: {integrity: sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==} @@ -8412,6 +8601,10 @@ packages: '@types/node': 20.14.12 dev: true + /@types/promise.allsettled@1.0.6: + resolution: {integrity: sha512-wA0UT0HeT2fGHzIFV9kWpYz5mdoyLxKrTgMdZQM++5h6pYAFH73HXcQhefg24nD1yivUFEn5KU+EF4b+CXJ4Wg==} + dev: false + /@types/prop-types@15.7.11: resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==} @@ -8466,6 +8659,10 @@ packages: '@types/node': 20.14.12 dev: true + /@types/retry@0.12.0: + resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + dev: false + /@types/scheduler@0.16.8: resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==} @@ -8508,6 +8705,10 @@ packages: resolution: {integrity: sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw==} dev: false + /@types/tsscmp@1.0.2: + resolution: {integrity: sha512-cy7BRSU8GYYgxjcx0Py+8lo5MthuDhlyu076KUcYzVNXL23luYgRHkMG2fIFEc6neckeh/ntP82mw+U4QjZq+g==} + dev: false + /@types/ua-parser-js@0.7.39: resolution: {integrity: sha512-P/oDfpofrdtF5xw433SPALpdSchtJmY7nsJItf8h3KXqOslkbySh8zq4dSWXH2oTjRvJ5PczVEoCZPow6GicLg==} dev: true @@ -8528,6 +8729,12 @@ packages: resolution: {integrity: sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==} dev: true + /@types/ws@7.4.7: + resolution: {integrity: sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==} + dependencies: + '@types/node': 20.14.12 + dev: false + /@types/ws@8.5.10: resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} dependencies: @@ -8597,7 +8804,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.6.0 + semver: 7.6.3 tsutils: 3.21.0(typescript@5.3.3) typescript: 5.3.3 transitivePeerDependencies: @@ -8704,7 +8911,7 @@ packages: resolution: {integrity: sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==} engines: {node: '>= 14'} dependencies: - debug: 4.3.4 + debug: 4.3.7 transitivePeerDependencies: - supports-color dev: false @@ -8942,6 +9149,18 @@ packages: es-shim-unscopables: 1.0.2 dev: false + /array.prototype.map@1.0.7: + resolution: {integrity: sha512-XpcFfLoBEAhezrrNw1V+yLXkE7M6uR7xJEsxbG6c/V9v043qurwVJB9r9UTnoSioFDoz1i1VOydpWGmJpfVZbg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-array-method-boxes-properly: 1.0.0 + es-object-atoms: 1.0.0 + is-string: 1.0.7 + dev: false + /array.prototype.tosorted@1.1.3: resolution: {integrity: sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==} dependencies: @@ -8978,7 +9197,7 @@ packages: resolution: {integrity: sha512-c27loCv9QkZinsa5ProX751khO9DJl/AcB5c2KNtA6NRvHKS0PgLfcftz72KVq504vB0Gku5s2kUZzDBvQWvHg==} engines: {node: '>=4'} dependencies: - tslib: 2.6.2 + tslib: 2.7.0 dev: false /astral-regex@1.0.0: @@ -9056,6 +9275,13 @@ packages: engines: {node: '>= 0.4'} dev: false + /available-typed-arrays@1.0.7: + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} + dependencies: + possible-typed-array-names: 1.0.0 + dev: false + /avvio@8.3.0: resolution: {integrity: sha512-VBVH0jubFr9LdFASy/vNtm5giTrnbVquWBhT0fyizuNK2rQ7e7ONU2plZQWUNqtE1EmxFEb+kbSkFRkstiaS9Q==} dependencies: @@ -9072,6 +9298,16 @@ packages: engines: {node: '>=4'} dev: false + /axios@1.7.7: + resolution: {integrity: sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==} + dependencies: + follow-redirects: 1.15.9 + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + dev: false + /axobject-query@3.2.1: resolution: {integrity: sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==} dependencies: @@ -10477,6 +10713,33 @@ packages: engines: {node: '>= 12'} dev: false + /data-view-buffer@1.0.1: + resolution: {integrity: sha512-0lht7OugA5x3iJLOWFhWK/5ehONdprk0ISXqVFn/NFrDu+cuc8iADFrGQz5BnRK7LLU3JmkbXSxaqX+/mXYtUA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: false + + /data-view-byte-length@1.0.1: + resolution: {integrity: sha512-4J7wRJD3ABAzr8wP+OcIcqq2dlUKp4DVflx++hs5h5ZKydWMI6/D/fAot+yh6g2tHh8fLFTvNOaVN357NvSrOQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: false + + /data-view-byte-offset@1.0.0: + resolution: {integrity: sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-data-view: 1.0.1 + dev: false + /date-fns@3.3.1: resolution: {integrity: sha512-y8e109LYGgoQDveiEBD3DYXKba1jWf5BA8YU1FL5Tvm0BTdEfy54WLCwnuYWZNnzzvALy/QQ4Hov+Q9RVRv+Zw==} @@ -10772,7 +11035,7 @@ packages: resolution: {integrity: sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==} dependencies: no-case: 3.0.4 - tslib: 2.6.2 + tslib: 2.7.0 dev: false /dotenv-cli@7.3.0: @@ -10989,6 +11252,58 @@ packages: which-typed-array: 1.1.14 dev: false + /es-abstract@1.23.3: + resolution: {integrity: sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==} + engines: {node: '>= 0.4'} + dependencies: + array-buffer-byte-length: 1.0.1 + arraybuffer.prototype.slice: 1.0.3 + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + data-view-buffer: 1.0.1 + data-view-byte-length: 1.0.1 + data-view-byte-offset: 1.0.0 + es-define-property: 1.0.0 + es-errors: 1.3.0 + es-object-atoms: 1.0.0 + es-set-tostringtag: 2.0.3 + es-to-primitive: 1.2.1 + function.prototype.name: 1.1.6 + get-intrinsic: 1.2.4 + get-symbol-description: 1.0.2 + globalthis: 1.0.3 + gopd: 1.0.1 + has-property-descriptors: 1.0.2 + has-proto: 1.0.3 + has-symbols: 1.0.3 + hasown: 2.0.2 + internal-slot: 1.0.7 + is-array-buffer: 3.0.4 + is-callable: 1.2.7 + is-data-view: 1.0.1 + is-negative-zero: 2.0.3 + is-regex: 1.1.4 + is-shared-array-buffer: 1.0.3 + is-string: 1.0.7 + is-typed-array: 1.1.13 + is-weakref: 1.0.2 + object-inspect: 1.13.1 + object-keys: 1.1.1 + object.assign: 4.1.5 + regexp.prototype.flags: 1.5.2 + safe-array-concat: 1.1.2 + safe-regex-test: 1.0.3 + string.prototype.trim: 1.2.9 + string.prototype.trimend: 1.0.8 + string.prototype.trimstart: 1.0.8 + typed-array-buffer: 1.0.2 + typed-array-byte-length: 1.0.1 + typed-array-byte-offset: 1.0.2 + typed-array-length: 1.0.6 + unbox-primitive: 1.0.2 + which-typed-array: 1.1.15 + dev: false + /es-array-method-boxes-properly@1.0.0: resolution: {integrity: sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==} dev: false @@ -11005,6 +11320,20 @@ packages: engines: {node: '>= 0.4'} dev: false + /es-get-iterator@1.1.3: + resolution: {integrity: sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + is-arguments: 1.1.1 + is-map: 2.0.2 + is-set: 2.0.2 + is-string: 1.0.7 + isarray: 2.0.5 + stop-iteration-iterator: 1.0.0 + dev: false + /es-iterator-helpers@1.0.17: resolution: {integrity: sha512-lh7BsUqelv4KUbR5a/ZTaGGIMLCjPGPqJ6q+Oq24YP0RdyptX1uzm4vvaqzk7Zx3bpl/76YLTTDj9L7uYQ92oQ==} engines: {node: '>= 0.4'} @@ -11026,6 +11355,13 @@ packages: safe-array-concat: 1.1.0 dev: false + /es-object-atoms@1.0.0: + resolution: {integrity: sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==} + engines: {node: '>= 0.4'} + dependencies: + es-errors: 1.3.0 + dev: false + /es-set-tostringtag@2.0.2: resolution: {integrity: sha512-BuDyupZt65P9D2D2vA/zqcI3G5xRsklm5N3xCwuiy+/vKy8i0ifdsQP1sLgO4tZDSCaQUSnmC48khknGMV3D2Q==} engines: {node: '>= 0.4'} @@ -11035,6 +11371,15 @@ packages: hasown: 2.0.1 dev: false + /es-set-tostringtag@2.0.3: + resolution: {integrity: sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==} + engines: {node: '>= 0.4'} + dependencies: + get-intrinsic: 1.2.4 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + dev: false + /es-shim-unscopables@1.0.2: resolution: {integrity: sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==} dependencies: @@ -11494,10 +11839,18 @@ packages: engines: {node: '>=6'} dev: false + /eventemitter3@3.1.2: + resolution: {integrity: sha512-tvtQIeLVHjDkJYnzf2dgVMxfuSGJeM/7UCG17TT4EumTfNtF+0nebF/4zWOIkCreAbtNqhGEboB6BWrwqNaw4Q==} + dev: false + /eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} dev: false + /eventemitter3@5.0.1: + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + dev: false + /events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} @@ -12017,6 +12370,10 @@ packages: micromatch: 4.0.5 dev: false + /finity@0.5.4: + resolution: {integrity: sha512-3l+5/1tuw616Lgb0QBimxfdd2TqaDGpfCBpfX6EqtFmqUV3FtQnVEX4Aa62DagYEqnsTIjZcTfbq9msDbXYgyA==} + dev: false + /flag-icons@7.1.0: resolution: {integrity: sha512-AH4v++19bpC5P3Wh767top4wylJYJCWkFnvNiDqGHDxqSqdMZ49jpLXp8PWBHTTXaNQ+/A+QPrOwyiIGaiIhmw==} dev: false @@ -12055,6 +12412,16 @@ packages: resolution: {integrity: sha512-Rwix9pBtC1Nuy5wysTmKy+UjbDJpIfg8eHjw0rjZ1mX4GNLz1Bmd16uDpI3Gk1i70Fgcs8Csg2lPm8HULFg9DQ==} dev: false + /follow-redirects@1.15.9: + resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + dev: false + /fontfaceobserver@2.3.0: resolution: {integrity: sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==} dev: false @@ -12072,6 +12439,15 @@ packages: cross-spawn: 7.0.3 signal-exit: 4.1.0 + /form-data@2.5.1: + resolution: {integrity: sha512-m21N3WOmEEURgk6B9GLOE4RuWOFf28Lhh9qGYeNlGq4VDXUlJy2th2slBNU8Gp8EzloYZOibZJ7t5ecIrFSjVA==} + engines: {node: '>= 0.12'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + /form-data@3.0.1: resolution: {integrity: sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==} engines: {node: '>= 6'} @@ -12081,6 +12457,15 @@ packages: mime-types: 2.1.35 dev: false + /form-data@4.0.0: + resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} + engines: {node: '>= 6'} + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + mime-types: 2.1.35 + dev: false + /format@0.2.2: resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} engines: {node: '>=0.4.x'} @@ -12119,7 +12504,7 @@ packages: dependencies: react: 18.2.0 react-dom: 18.2.0(react@18.2.0) - tslib: 2.6.2 + tslib: 2.7.0 optionalDependencies: '@emotion/is-prop-valid': 0.8.8 dev: false @@ -12471,7 +12856,7 @@ packages: graphql: ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 dependencies: graphql: 15.8.0 - tslib: 2.6.2 + tslib: 2.7.0 dev: false /graphql@15.8.0: @@ -12527,6 +12912,11 @@ packages: engines: {node: '>= 0.4'} dev: false + /has-proto@1.0.3: + resolution: {integrity: sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==} + engines: {node: '>= 0.4'} + dev: false + /has-symbols@1.0.3: resolution: {integrity: sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==} engines: {node: '>= 0.4'} @@ -12558,6 +12948,13 @@ packages: dependencies: function-bind: 1.1.2 + /hasown@2.0.2: + resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} + engines: {node: '>= 0.4'} + dependencies: + function-bind: 1.1.2 + dev: false + /hast-util-from-dom@5.0.0: resolution: {integrity: sha512-d6235voAp/XR3Hh5uy7aGLbM3S4KamdW0WEgOaU1YoewnuYw4HXb5eRtv9g65m/RFGEfUY1Mw4UqCc5Y8L4Stg==} dependencies: @@ -13015,6 +13412,14 @@ packages: is-decimal: 2.0.1 dev: false + /is-arguments@1.1.1: + resolution: {integrity: sha512-8Q7EARjzEnKpt/PCD7e1cgUS0a6X8u5tdSiMqXhojOdoV9TsMsiO+9VLC5vAmO8N7/GmXn7yjR8qnA6bVAEzfA==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + has-tostringtag: 1.0.2 + dev: false + /is-array-buffer@3.0.4: resolution: {integrity: sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==} engines: {node: '>= 0.4'} @@ -13077,6 +13482,13 @@ packages: dependencies: hasown: 2.0.1 + /is-data-view@1.0.1: + resolution: {integrity: sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==} + engines: {node: '>= 0.4'} + dependencies: + is-typed-array: 1.1.13 + dev: false + /is-date-object@1.0.5: resolution: {integrity: sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==} engines: {node: '>= 0.4'} @@ -13103,6 +13515,10 @@ packages: hasBin: true dev: false + /is-electron@2.2.2: + resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==} + dev: false + /is-extendable@0.1.1: resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} engines: {node: '>=0.10.0'} @@ -13181,6 +13597,11 @@ packages: engines: {node: '>= 0.4'} dev: false + /is-negative-zero@2.0.3: + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} + dev: false + /is-number-object@1.0.7: resolution: {integrity: sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==} engines: {node: '>= 0.4'} @@ -13248,6 +13669,13 @@ packages: call-bind: 1.0.7 dev: false + /is-shared-array-buffer@1.0.3: + resolution: {integrity: sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + dev: false + /is-ssh@1.4.0: resolution: {integrity: sha512-x7+VxdxOdlV3CYpjvRLBv5Lo9OJerlYanjwFrPR9fuGPjCiNiCzFgAWpiLAohSbsnH4ZAys3SBh+hq5rJosxUQ==} dependencies: @@ -13346,6 +13774,17 @@ packages: engines: {node: '>=0.10.0'} dev: false + /iterate-iterator@1.0.2: + resolution: {integrity: sha512-t91HubM4ZDQ70M9wqp+pcNpu8OyJ9UAtXntT/Bcsvp5tZMnz9vRa+IunKXeI8AnfZMTv0jNuVEmGeLSMjVvfPw==} + dev: false + + /iterate-value@1.0.2: + resolution: {integrity: sha512-A6fMAio4D2ot2r/TYzr4yUWrmwNdsN5xL7+HUiyACE4DXm+q8HtPcnFTp+NnW3k4N05tZ7FVYFFb2CR13NxyHQ==} + dependencies: + es-get-iterator: 1.1.3 + iterate-iterator: 1.0.2 + dev: false + /iterator.prototype@1.1.2: resolution: {integrity: sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==} dependencies: @@ -14054,7 +14493,7 @@ packages: /lower-case@2.0.2: resolution: {integrity: sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==} dependencies: - tslib: 2.6.2 + tslib: 2.7.0 dev: false /lowlight@1.20.0: @@ -15524,7 +15963,7 @@ packages: resolution: {integrity: sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==} dependencies: lower-case: 2.0.2 - tslib: 2.6.2 + tslib: 2.7.0 dev: false /nocache@3.0.4: @@ -15965,6 +16404,29 @@ packages: aggregate-error: 3.1.0 dev: false + /p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + dev: false + + /p-retry@4.6.2: + resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} + engines: {node: '>=8'} + dependencies: + '@types/retry': 0.12.0 + retry: 0.13.1 + dev: false + + /p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + dependencies: + p-finally: 1.0.0 + dev: false + /p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} @@ -16102,6 +16564,11 @@ packages: resolution: {integrity: sha512-JLyh7xT1kizaEvcaXOQwOc2/Yhw6KZOvPf1S8401UyLk86CU79LN3vl7ztXGm/pZ+YjoyAJ4rxmHwbkBXJX+yw==} dev: false + /path-to-regexp@8.2.0: + resolution: {integrity: sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==} + engines: {node: '>=16'} + dev: false + /path-type@4.0.0: resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} engines: {node: '>=8'} @@ -16223,6 +16690,11 @@ packages: engines: {node: '>=4.0.0'} dev: false + /possible-typed-array-names@1.0.0: + resolution: {integrity: sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==} + engines: {node: '>= 0.4'} + dev: false + /postcss-import@15.1.0(postcss@8.4.35): resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==} engines: {node: '>=14.0.0'} @@ -16376,6 +16848,20 @@ packages: engines: {node: '>=10'} dev: false + /prisma-json-types-generator@3.1.1(prisma@5.9.1)(typescript@5.3.3): + resolution: {integrity: sha512-LYVBKWcnh6SSPf6jNJuQEmByyZtj79kuxzilCF8962ViBNciBzk+pQ8qt7HR7lFUIOEN3sUeOOhJe1RuytGk6A==} + engines: {node: '>=14.0'} + hasBin: true + peerDependencies: + prisma: ^5.20 + typescript: ^5.6.2 + dependencies: + '@prisma/generator-helper': 5.20.0 + prisma: 5.9.1 + tslib: 2.7.0 + typescript: 5.3.3 + dev: false + /prisma@5.9.1: resolution: {integrity: sha512-Hy/8KJZz0ELtkw4FnG9MS9rNWlXcJhf98Z2QMqi0QiVMoS8PzsBkpla0/Y5hTlob8F3HeECYphBjqmBxrluUrQ==} engines: {node: '>=16.13'} @@ -16441,6 +16927,18 @@ packages: optional: true dev: false + /promise.allsettled@1.0.7: + resolution: {integrity: sha512-hezvKvQQmsFkOdrZfYxUxkyxl8mgFQeT259Ajj9PXdbg9VzBCWrItOev72JyWxkCD5VSSqAeHmlN3tWx4DlmsA==} + engines: {node: '>= 0.4'} + dependencies: + array.prototype.map: 1.0.7 + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + get-intrinsic: 1.2.4 + iterate-value: 1.0.2 + dev: false + /promise@7.3.1: resolution: {integrity: sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==} dependencies: @@ -16510,6 +17008,10 @@ packages: ipaddr.js: 1.9.1 dev: false + /proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + dev: false + /pseudomap@1.0.2: resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} dev: false @@ -16862,7 +17364,7 @@ packages: '@types/react': 18.2.56 react: 18.2.0 react-style-singleton: 2.2.1(@types/react@18.2.56)(react@18.2.0) - tslib: 2.6.2 + tslib: 2.7.0 dev: false /react-remove-scroll@2.5.4(@types/react@18.2.56)(react@18.2.0): @@ -16879,7 +17381,7 @@ packages: react: 18.2.0 react-remove-scroll-bar: 2.3.4(@types/react@18.2.56)(react@18.2.0) react-style-singleton: 2.2.1(@types/react@18.2.56)(react@18.2.0) - tslib: 2.6.2 + tslib: 2.7.0 use-callback-ref: 1.3.1(@types/react@18.2.56)(react@18.2.0) use-sidecar: 1.1.2(@types/react@18.2.56)(react@18.2.0) dev: false @@ -16969,7 +17471,7 @@ packages: get-nonce: 1.0.1 invariant: 2.2.4 react: 18.2.0 - tslib: 2.6.2 + tslib: 2.7.0 dev: false /react-svg-worldmap@2.0.0-alpha.16(react-dom@18.2.0)(react@18.2.0): @@ -17115,7 +17617,7 @@ packages: ast-types: 0.15.2 esprima: 4.0.1 source-map: 0.6.1 - tslib: 2.6.2 + tslib: 2.7.0 dev: false /recharts-scale@0.4.5: @@ -17471,6 +17973,11 @@ packages: engines: {node: '>=4'} dev: false + /retry@0.13.1: + resolution: {integrity: sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==} + engines: {node: '>= 4'} + dev: false + /reusify@1.0.4: resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} engines: {iojs: '>=1.0.0', node: '>=0.10.0'} @@ -17557,7 +18064,7 @@ packages: /rxjs@7.8.1: resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} dependencies: - tslib: 2.6.2 + tslib: 2.7.0 dev: false /sade@1.8.1: @@ -17577,6 +18084,16 @@ packages: isarray: 2.0.5 dev: false + /safe-array-concat@1.1.2: + resolution: {integrity: sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==} + engines: {node: '>=0.4'} + dependencies: + call-bind: 1.0.7 + get-intrinsic: 1.2.4 + has-symbols: 1.0.3 + isarray: 2.0.5 + dev: false + /safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} dev: false @@ -17908,7 +18425,7 @@ packages: resolution: {integrity: sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==} dependencies: dot-case: 3.0.4 - tslib: 2.6.2 + tslib: 2.7.0 dev: false /snakecase-keys@5.4.4: @@ -18051,6 +18568,13 @@ packages: resolution: {integrity: sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==} dev: false + /stop-iteration-iterator@1.0.0: + resolution: {integrity: sha512-iCGQj+0l0HOdZ2AEeBADlsRC+vsnDsZsbdSiH1yNSjcfKM7fdpCMfqAL/dwF5BLiw/XhRft/Wax6zQbhq2BcjQ==} + engines: {node: '>= 0.4'} + dependencies: + internal-slot: 1.0.7 + dev: false + /stream-buffers@2.2.0: resolution: {integrity: sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==} engines: {node: '>= 0.10.0'} @@ -18104,6 +18628,16 @@ packages: es-abstract: 1.22.4 dev: false + /string.prototype.trim@1.2.9: + resolution: {integrity: sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-abstract: 1.23.3 + es-object-atoms: 1.0.0 + dev: false + /string.prototype.trimend@1.0.7: resolution: {integrity: sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==} dependencies: @@ -18112,6 +18646,14 @@ packages: es-abstract: 1.22.4 dev: false + /string.prototype.trimend@1.0.8: + resolution: {integrity: sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: false + /string.prototype.trimstart@1.0.7: resolution: {integrity: sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==} dependencies: @@ -18120,6 +18662,15 @@ packages: es-abstract: 1.22.4 dev: false + /string.prototype.trimstart@1.0.8: + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + define-properties: 1.2.1 + es-object-atoms: 1.0.0 + dev: false + /string_decoder@1.1.1: resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} dependencies: @@ -18627,6 +19178,10 @@ packages: requiresBuild: true dev: false + /tslib@2.7.0: + resolution: {integrity: sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==} + dev: false + /tsscmp@1.0.6: resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} engines: {node: '>=0.6.x'} @@ -18743,6 +19298,15 @@ packages: is-typed-array: 1.1.13 dev: false + /typed-array-buffer@1.0.2: + resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + es-errors: 1.3.0 + is-typed-array: 1.1.13 + dev: false + /typed-array-byte-length@1.0.0: resolution: {integrity: sha512-Or/+kvLxNpeQ9DtSydonMxCx+9ZXOswtwJn17SNLvhptaXYDJvkFFP5zbfU/uLmvnBJlI4yrnXRxpdWH/M5tNA==} engines: {node: '>= 0.4'} @@ -18753,6 +19317,17 @@ packages: is-typed-array: 1.1.13 dev: false + /typed-array-byte-length@1.0.1: + resolution: {integrity: sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + dev: false + /typed-array-byte-offset@1.0.0: resolution: {integrity: sha512-RD97prjEt9EL8YgAgpOkf3O4IF9lhJFr9g0htQkm0rchFp/Vx7LW5Q8fSXXub7BXAODyUQohRMyOc3faCPd0hg==} engines: {node: '>= 0.4'} @@ -18764,6 +19339,18 @@ packages: is-typed-array: 1.1.13 dev: false + /typed-array-byte-offset@1.0.2: + resolution: {integrity: sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + dev: false + /typed-array-length@1.0.4: resolution: {integrity: sha512-KjZypGq+I/H7HI5HlOoGHkWUUGq+Q0TPhQurLbyrVrvnKTBgzLhIJ7j6J/XTQOi0d1RjyZ0wdas8bKs2p0x3Ng==} dependencies: @@ -18772,6 +19359,18 @@ packages: is-typed-array: 1.1.13 dev: false + /typed-array-length@1.0.6: + resolution: {integrity: sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==} + engines: {node: '>= 0.4'} + dependencies: + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-proto: 1.0.3 + is-typed-array: 1.1.13 + possible-typed-array-names: 1.0.0 + dev: false + /typed-function@4.1.1: resolution: {integrity: sha512-Pq1DVubcvibmm8bYcMowjVnnMwPVMeh0DIdA8ad8NZY2sJgapANJmiigSUwlt+EgXxpfIv8MWrQXTIzkfYZLYQ==} engines: {node: '>= 14'} @@ -19063,7 +19662,7 @@ packages: dependencies: '@types/react': 18.2.56 react: 18.2.0 - tslib: 2.6.2 + tslib: 2.7.0 dev: false /use-sidecar@1.1.2(@types/react@18.2.56)(react@18.2.0): @@ -19079,7 +19678,7 @@ packages: '@types/react': 18.2.56 detect-node-es: 1.1.0 react: 18.2.0 - tslib: 2.6.2 + tslib: 2.7.0 dev: false /use-sync-external-store@1.2.0(react@18.2.0): @@ -19344,6 +19943,17 @@ packages: has-tostringtag: 1.0.2 dev: false + /which-typed-array@1.1.15: + resolution: {integrity: sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==} + engines: {node: '>= 0.4'} + dependencies: + available-typed-arrays: 1.0.7 + call-bind: 1.0.7 + for-each: 0.3.3 + gopd: 1.0.1 + has-tostringtag: 1.0.2 + dev: false + /which@1.3.1: resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} hasBin: true