diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index 7db238c4..227352ec 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -106,6 +106,16 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Generate tags + id: tags + run: | + # Sanitize branch name by replacing / with - + BRANCH_NAME=$(echo "${{ github.ref_name }}" | sed 's/\//-/g') + # Get first 4 characters of commit SHA + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-4) + echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -125,8 +135,7 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max tags: | - ghcr.io/${{ env.repo_owner }}/api:latest - ghcr.io/${{ env.repo_owner }}/api:${{ github.sha }} + ghcr.io/${{ env.repo_owner }}/api:${{ steps.tags.outputs.branch_name }}-${{ steps.tags.outputs.short_sha }} build-args: | DATABASE_URL=postgresql://dummy:dummy@localhost:5432/dummy @@ -140,6 +149,16 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Generate tags + id: tags + run: | + # Sanitize branch name by replacing / with - + BRANCH_NAME=$(echo "${{ github.ref_name }}" | sed 's/\//-/g') + # Get first 4 characters of commit SHA + SHORT_SHA=$(echo "${{ github.sha }}" | cut -c1-4) + echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + echo "short_sha=$SHORT_SHA" >> $GITHUB_OUTPUT + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -159,7 +178,6 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max tags: | - ghcr.io/${{ env.repo_owner }}/worker:latest - ghcr.io/${{ env.repo_owner }}/worker:${{ github.sha }} + ghcr.io/${{ env.repo_owner }}/worker:${{ steps.tags.outputs.branch_name }}-${{ steps.tags.outputs.short_sha }} build-args: | DATABASE_URL=postgresql://dummy:dummy@localhost:5432/dummy diff --git a/.gitignore b/.gitignore index 96b7ab03..c2f17021 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ .secrets +packages/db/src/generated/prisma # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore packages/sdk/profileId.txt @@ -168,6 +169,9 @@ dist .vscode-test +# Wrangler build artifacts and cache +.wrangler/ + # yarn v2 .yarn/cache diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile index 292eda4c..a4efad54 100644 --- a/apps/api/Dockerfile +++ b/apps/api/Dockerfile @@ -1,4 +1,4 @@ -ARG NODE_VERSION=20.15.1 +ARG NODE_VERSION=22.20.0 FROM node:${NODE_VERSION}-slim AS base @@ -43,6 +43,7 @@ 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/ +COPY patches ./patches # BUILD FROM base AS build @@ -66,6 +67,8 @@ RUN pnpm codegen && \ # PROD FROM base AS prod +ENV npm_config_build_from_source=true + RUN apt-get update && apt-get install -y --no-install-recommends \ python3 \ make \ @@ -75,12 +78,14 @@ WORKDIR /app COPY --from=build /app/package.json ./ COPY --from=build /app/pnpm-lock.yaml ./ RUN pnpm install --frozen-lockfile --prod && \ + pnpm rebuild && \ pnpm store prune # FINAL FROM base AS runner ENV NODE_ENV=production +ENV npm_config_build_from_source=true WORKDIR /app @@ -106,6 +111,7 @@ 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 +COPY --from=build /app/tooling/typescript ./tooling/typescript RUN pnpm db:codegen WORKDIR /app/apps/api diff --git a/apps/api/package.json b/apps/api/package.json index 06814391..e42cc243 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,11 +1,12 @@ { "name": "@openpanel/api", "version": "0.0.4", + "type": "module", "scripts": { - "dev": "dotenv -e ../../.env -c -v WATCH=1 tsup", + "dev": "dotenv -e ../../.env -c -v WATCH=1 tsdown", "testing": "API_PORT=3333 pnpm dev", - "start": "node dist/index.js", - "build": "rm -rf dist && tsup", + "start": "dotenv -e ../../.env node dist/index.js", + "build": "rm -rf dist && tsdown", "gen:bots": "jiti scripts/get-bots.ts && biome format --write src/bots/bots.ts", "typecheck": "tsc --noEmit" }, @@ -31,15 +32,13 @@ "@openpanel/redis": "workspace:*", "@openpanel/trpc": "workspace:*", "@openpanel/validation": "workspace:*", - "@trpc/server": "^10.45.2", + "@trpc/server": "^11.6.0", "ai": "^4.2.10", - "bcrypt": "^5.1.1", "fast-json-stable-hash": "^1.0.3", "fastify": "^5.2.1", "fastify-metrics": "^12.1.0", "fastify-raw-body": "^5.0.0", "groupmq": "1.0.0-next.19", - "ico-to-png": "^0.2.2", "jsonwebtoken": "^9.0.2", "ramda": "^0.29.1", "request-ip": "^3.3.0", @@ -65,7 +64,7 @@ "@types/uuid": "^10.0.0", "@types/ws": "^8.5.14", "js-yaml": "^4.1.0", - "tsup": "^7.2.0", - "typescript": "^5.2.2" + "tsdown": "^0.14.2", + "typescript": "catalog:" } } diff --git a/apps/api/scripts/get-bots.ts b/apps/api/scripts/get-bots.ts index f72561ff..992a0fe2 100644 --- a/apps/api/scripts/get-bots.ts +++ b/apps/api/scripts/get-bots.ts @@ -1,5 +1,10 @@ import fs from 'node:fs'; import path from 'node:path'; +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); import yaml from 'js-yaml'; async function main() { diff --git a/apps/api/scripts/mock.ts b/apps/api/scripts/mock.ts index 0e61a55e..a4bdbf7c 100644 --- a/apps/api/scripts/mock.ts +++ b/apps/api/scripts/mock.ts @@ -493,9 +493,11 @@ async function main() { const [type, file = 'mock-basic.json'] = process.argv.slice(2); switch (type) { - case 'send': - await triggerEvents(require(`./${file}`)); + case 'send': { + const data = await import(`./${file}`, { assert: { type: 'json' } }); + await triggerEvents(data.default); break; + } case 'sim': await simultaneousRequests(); break; diff --git a/apps/api/src/controllers/ai.controller.ts b/apps/api/src/controllers/ai.controller.ts index 20631af6..6162d036 100644 --- a/apps/api/src/controllers/ai.controller.ts +++ b/apps/api/src/controllers/ai.controller.ts @@ -112,7 +112,7 @@ export async function chat( await db.chat.create({ data: { - messages: messagesToSave.slice(-10), + messages: messagesToSave.slice(-10) as any, projectId, }, }); diff --git a/apps/api/src/controllers/misc.controller.ts b/apps/api/src/controllers/misc.controller.ts index 99323e33..e441b484 100644 --- a/apps/api/src/controllers/misc.controller.ts +++ b/apps/api/src/controllers/misc.controller.ts @@ -1,53 +1,231 @@ +import crypto from 'node:crypto'; import { logger } from '@/utils/logger'; import { parseUrlMeta } from '@/utils/parseUrlMeta'; import type { FastifyReply, FastifyRequest } from 'fastify'; -import icoToPng from 'ico-to-png'; import sharp from 'sharp'; import { getClientIp } from '@/utils/get-client-ip'; -import { createHash } from '@openpanel/common/server'; import { TABLE_NAMES, ch, chQuery, formatClickhouseDate } from '@openpanel/db'; import { getGeoLocation } from '@openpanel/geo'; -import { cacheable, getCache, getRedisCache } from '@openpanel/redis'; +import { getCache, getRedisCache } from '@openpanel/redis'; interface GetFaviconParams { url: string; } -async function getImageBuffer(url: string) { +// Configuration +const TTL_SECONDS = 60 * 60 * 24; // 24h +const MAX_BYTES = 1_000_000; // 1MB cap +const USER_AGENT = 'OpenPanel-FaviconProxy/1.0 (+https://openpanel.dev)'; + +// Helper functions +function createCacheKey(url: string, prefix = 'favicon'): string { + const hash = crypto.createHash('sha256').update(url).digest('hex'); + return `${prefix}:v2:${hash}`; +} + +function validateUrl(raw?: string): URL | null { try { - const res = await fetch(url); - const contentType = res.headers.get('content-type'); + if (!raw) throw new Error('Missing ?url'); + const url = new URL(raw); + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + throw new Error('Only http/https URLs are allowed'); + } + return url; + } catch (error) { + return null; + } +} - if (!contentType?.includes('image')) { - return null; +// Binary cache functions (more efficient than base64) +async function getFromCacheBinary( + key: string, +): Promise<{ buffer: Buffer; contentType: string } | null> { + const redis = getRedisCache(); + const [bufferBase64, contentType] = await Promise.all([ + redis.get(key), + redis.get(`${key}:ctype`), + ]); + + if (!bufferBase64 || !contentType) return null; + return { buffer: Buffer.from(bufferBase64, 'base64'), contentType }; +} + +async function setToCacheBinary( + key: string, + buffer: Buffer, + contentType: string, +): Promise { + const redis = getRedisCache(); + await Promise.all([ + redis.set(key, buffer.toString('base64'), 'EX', TTL_SECONDS), + redis.set(`${key}:ctype`, contentType, 'EX', TTL_SECONDS), + ]); +} + +// Fetch image with timeout and size limits +async function fetchImage( + url: URL, +): Promise<{ buffer: Buffer; contentType: string; status: number }> { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 10000); // 10s timeout + + try { + const response = await fetch(url.toString(), { + redirect: 'follow', + signal: controller.signal, + headers: { + 'user-agent': USER_AGENT, + accept: 'image/*,*/*;q=0.8', + }, + }); + + clearTimeout(timeout); + + if (!response.ok) { + return { + buffer: Buffer.alloc(0), + contentType: 'text/plain', + status: response.status, + }; } - if (!res.ok) { - return null; + // Size guard + const contentLength = Number(response.headers.get('content-length') ?? '0'); + if (contentLength > MAX_BYTES) { + throw new Error(`Remote file too large: ${contentLength} bytes`); } - if (contentType === 'image/x-icon' || url.endsWith('.ico')) { - const arrayBuffer = await res.arrayBuffer(); - const buffer = Buffer.from(arrayBuffer); - return await icoToPng(buffer, 30); + const arrayBuffer = await response.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + + // Additional size check for actual content + if (buffer.length > MAX_BYTES) { + throw new Error('Remote file exceeded size limit'); } - return await sharp(await res.arrayBuffer()) + const contentType = + response.headers.get('content-type') || 'application/octet-stream'; + return { buffer, contentType, status: 200 }; + } catch (error) { + clearTimeout(timeout); + return { buffer: Buffer.alloc(0), contentType: 'text/plain', status: 500 }; + } +} + +// Check if URL is an ICO file +function isIcoFile(url: string, contentType?: string): boolean { + return url.toLowerCase().endsWith('.ico') || contentType === 'image/x-icon'; +} +function isSvgFile(url: string, contentType?: string): boolean { + return url.toLowerCase().endsWith('.svg') || contentType === 'image/svg+xml'; +} + +// Process image with Sharp (resize to 30x30 PNG) +async function processImage( + buffer: Buffer, + originalUrl?: string, + contentType?: string, +): Promise { + // If it's an ICO file, just return it as-is (no conversion needed) + if (originalUrl && isIcoFile(originalUrl, contentType)) { + logger.info('Serving ICO file directly', { + originalUrl, + bufferSize: buffer.length, + }); + return buffer; + } + + if (originalUrl && isSvgFile(originalUrl, contentType)) { + logger.info('Serving SVG file directly', { + originalUrl, + bufferSize: buffer.length, + }); + return buffer; + } + + // If buffer isnt to big just return it as well + if (buffer.length < 5000) { + logger.info('Serving image directly without processing', { + originalUrl, + bufferSize: buffer.length, + }); + return buffer; + } + + try { + // For other formats, process with Sharp + return await sharp(buffer) .resize(30, 30, { fit: 'cover', }) .png() .toBuffer(); } catch (error) { - logger.error('Failed to get image from url', { - error, - url, + logger.warn('Sharp failed to process image, trying fallback', { + error: error instanceof Error ? error.message : 'Unknown error', + originalUrl, + bufferSize: buffer.length, }); + + // If Sharp fails, try to create a simple fallback image + return createFallbackImage(); } } -const imageExtensions = ['svg', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'ico']; +// Create a simple transparent fallback image when Sharp can't process the original +function createFallbackImage(): Buffer { + // 1x1 transparent PNG + return Buffer.from( + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=', + 'base64', + ); +} + +// Process OG image with Sharp (resize to 300px width) +async function processOgImage( + buffer: Buffer, + originalUrl?: string, + contentType?: string, +): Promise { + // If buffer is small enough, return it as-is + if (buffer.length < 10000) { + logger.info('Serving OG image directly without processing', { + originalUrl, + bufferSize: buffer.length, + }); + return buffer; + } + + try { + // For OG images, process with Sharp to 300px width, maintaining aspect ratio + return await sharp(buffer) + .resize(300, null, { + fit: 'inside', + withoutEnlargement: true, + }) + .png() + .toBuffer(); + } catch (error) { + logger.warn('Sharp failed to process OG image, trying fallback', { + error: error instanceof Error ? error.message : 'Unknown error', + originalUrl, + bufferSize: buffer.length, + }); + + // If Sharp fails, try to create a simple fallback image + return createFallbackImage(); + } +} + +// Check if URL is a direct image +function isDirectImage(url: URL): boolean { + const imageExtensions = ['svg', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'ico']; + return ( + imageExtensions.some((ext) => url.pathname.endsWith(`.${ext}`)) || + url.toString().includes('googleusercontent.com') + ); +} export async function getFavicon( request: FastifyRequest<{ @@ -55,68 +233,110 @@ export async function getFavicon( }>, reply: FastifyReply, ) { - function sendBuffer(buffer: Buffer, cacheKey?: string) { - if (cacheKey) { - getRedisCache().set(`favicon:${cacheKey}`, buffer.toString('base64')); + try { + const url = validateUrl(request.query.url); + if (!url) { + return createFallbackImage(); } - reply.header('Cache-Control', 'public, max-age=604800'); - reply.header('Expires', new Date(Date.now() + 604800000).toUTCString()); - reply.type('image/png'); - return reply.send(buffer); - } - if (!request.query.url) { - return reply.status(404).send('Not found'); - } + const cacheKey = createCacheKey(url.toString()); - const url = decodeURIComponent(request.query.url); - - if (imageExtensions.find((ext) => url.endsWith(ext))) { - const cacheKey = createHash(url, 32); - const cache = await getRedisCache().get(`favicon:${cacheKey}`); - if (cache) { - return sendBuffer(Buffer.from(cache, 'base64')); + // Check cache first + const cached = await getFromCacheBinary(cacheKey); + if (cached) { + reply.header('Content-Type', cached.contentType); + reply.header('Cache-Control', 'public, max-age=604800, immutable'); + return reply.send(cached.buffer); } - const buffer = await getImageBuffer(url); - if (buffer && buffer.byteLength > 0) { - return sendBuffer(buffer, cacheKey); + + let imageUrl: URL; + + // If it's a direct image URL, use it directly + if (isDirectImage(url)) { + imageUrl = url; + } else { + // For website URLs, extract favicon from HTML + const meta = await parseUrlMeta(url.toString()); + if (meta?.favicon) { + imageUrl = new URL(meta.favicon); + } else { + // Fallback to Google's favicon service + const { hostname } = url; + imageUrl = new URL( + `https://www.google.com/s2/favicons?domain=${hostname}&sz=256`, + ); + } } - } - const { hostname } = new URL(url); - const cache = await getRedisCache().get(`favicon:${hostname}`); + // Fetch the image + const { buffer, contentType, status } = await fetchImage(imageUrl); - if (cache) { - return sendBuffer(Buffer.from(cache, 'base64')); - } - - const meta = await parseUrlMeta(url); - if (meta?.favicon) { - const buffer = await getImageBuffer(meta.favicon); - if (buffer && buffer.byteLength > 0) { - return sendBuffer(buffer, hostname); + if (status !== 200 || buffer.length === 0) { + return reply.send(createFallbackImage()); } - } - const buffer = await getImageBuffer( - 'https://www.iconsdb.com/icons/download/orange/warning-128.png', - ); - if (buffer && buffer.byteLength > 0) { - return sendBuffer(buffer, hostname); - } + // Process the image (resize to 30x30 PNG, or serve ICO as-is) + const processedBuffer = await processImage( + buffer, + imageUrl.toString(), + contentType, + ); - return reply.status(404).send('Not found'); + // Determine the correct content type for caching and response + const isIco = isIcoFile(imageUrl.toString(), contentType); + const responseContentType = isIco ? 'image/x-icon' : contentType; + + // Cache the result with correct content type + await setToCacheBinary(cacheKey, processedBuffer, responseContentType); + + reply.header('Content-Type', responseContentType); + reply.header('Cache-Control', 'public, max-age=3600, immutable'); + return reply.send(processedBuffer); + } catch (error: any) { + logger.error('Favicon fetch error', { + error: error.message, + url: request.query.url, + }); + + const message = + process.env.NODE_ENV === 'production' + ? 'Bad request' + : (error?.message ?? 'Error'); + reply.header('Cache-Control', 'no-store'); + return reply.status(400).send(message); + } } export async function clearFavicons( request: FastifyRequest, reply: FastifyReply, ) { - const keys = await getRedisCache().keys('favicon:*'); + const redis = getRedisCache(); + const keys = await redis.keys('favicon:*'); + + // Delete both the binary data and content-type keys for (const key of keys) { - await getRedisCache().del(key); + await redis.del(key); + await redis.del(`${key}:ctype`); } - return reply.status(404).send('OK'); + + return reply.status(200).send('OK'); +} + +export async function clearOgImages( + request: FastifyRequest, + reply: FastifyReply, +) { + const redis = getRedisCache(); + const keys = await redis.keys('og:*'); + + // Delete both the binary data and content-type keys + for (const key of keys) { + await redis.del(key); + await redis.del(`${key}:ctype`); + } + + return reply.status(200).send('OK'); } export async function ping( @@ -181,3 +401,77 @@ export async function getGeo(request: FastifyRequest, reply: FastifyReply) { const geo = await getGeoLocation(ip); return reply.status(200).send(geo); } + +export async function getOgImage( + request: FastifyRequest<{ + Querystring: { + url: string; + }; + }>, + reply: FastifyReply, +) { + try { + const url = validateUrl(request.query.url); + if (!url) { + return getFavicon(request, reply); + } + const cacheKey = createCacheKey(url.toString(), 'og'); + + // Check cache first + const cached = await getFromCacheBinary(cacheKey); + if (cached) { + reply.header('Content-Type', cached.contentType); + reply.header('Cache-Control', 'public, max-age=604800, immutable'); + return reply.send(cached.buffer); + } + + let imageUrl: URL; + + // If it's a direct image URL, use it directly + if (isDirectImage(url)) { + imageUrl = url; + } else { + // For website URLs, extract OG image from HTML + const meta = await parseUrlMeta(url.toString()); + if (meta?.ogImage) { + imageUrl = new URL(meta.ogImage); + } else { + // No OG image found, return a fallback + return getFavicon(request, reply); + } + } + + // Fetch the image + const { buffer, contentType, status } = await fetchImage(imageUrl); + + if (status !== 200 || buffer.length === 0) { + return getFavicon(request, reply); + } + + // Process the image (resize to 1200x630 for OG standards, or serve as-is if reasonable size) + const processedBuffer = await processOgImage( + buffer, + imageUrl.toString(), + contentType, + ); + + // Cache the result + await setToCacheBinary(cacheKey, processedBuffer, 'image/png'); + + reply.header('Content-Type', 'image/png'); + reply.header('Cache-Control', 'public, max-age=3600, immutable'); + return reply.send(processedBuffer); + } catch (error: any) { + logger.error('OG image fetch error', { + error: error.message, + url: request.query.url, + }); + + const message = + process.env.NODE_ENV === 'production' + ? 'Bad request' + : (error?.message ?? 'Error'); + reply.header('Cache-Control', 'no-store'); + return reply.status(400).send(message); + } +} diff --git a/apps/api/src/controllers/oauth-callback.controller.tsx b/apps/api/src/controllers/oauth-callback.controller.tsx index 2b45adcc..77e79eca 100644 --- a/apps/api/src/controllers/oauth-callback.controller.tsx +++ b/apps/api/src/controllers/oauth-callback.controller.tsx @@ -76,7 +76,9 @@ async function handleExistingUser({ sessionToken, session.expiresAt, ); - return reply.redirect(process.env.NEXT_PUBLIC_DASHBOARD_URL!); + return reply.redirect( + process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL!, + ); } async function handleNewUser({ @@ -138,7 +140,9 @@ async function handleNewUser({ sessionToken, session.expiresAt, ); - return reply.redirect(process.env.NEXT_PUBLIC_DASHBOARD_URL!); + return reply.redirect( + process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL!, + ); } // Provider-specific user fetching @@ -348,7 +352,9 @@ export async function googleCallback(req: FastifyRequest, reply: FastifyReply) { } function redirectWithError(reply: FastifyReply, error: LogError | unknown) { - const url = new URL(process.env.NEXT_PUBLIC_DASHBOARD_URL!); + const url = new URL( + process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL!, + ); url.pathname = '/login'; if (error instanceof LogError) { url.searchParams.set('error', error.message); diff --git a/apps/api/src/controllers/webhook.controller.ts b/apps/api/src/controllers/webhook.controller.ts index 70db830b..36785c44 100644 --- a/apps/api/src/controllers/webhook.controller.ts +++ b/apps/api/src/controllers/webhook.controller.ts @@ -1,5 +1,10 @@ import fs from 'node:fs'; import path from 'node:path'; +import { dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); import { db, getOrganizationByProjectIdCached } from '@openpanel/db'; import { sendSlackNotification, @@ -100,7 +105,7 @@ export async function slackWebhook( }); return reply.redirect( - `${process.env.NEXT_PUBLIC_DASHBOARD_URL}/${organizationId}/${projectId}/settings/integrations?tab=installed`, + `${process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL}/${organizationId}/${projectId}/settings/integrations?tab=installed`, ); } catch (err) { request.log.error(err); @@ -184,7 +189,7 @@ export async function polarWebhook( data: { subscriptionId: event.data.id, subscriptionCustomerId: event.data.customer.id, - subscriptionPriceId: event.data.priceId, + subscriptionPriceId: event.data.prices[0]?.id ?? null, subscriptionProductId: event.data.productId, subscriptionStatus: event.data.status, subscriptionStartsAt: event.data.currentPeriodStart, diff --git a/apps/api/src/hooks/request-logging.hook.ts b/apps/api/src/hooks/request-logging.hook.ts index 5f3a8150..3d2c9961 100644 --- a/apps/api/src/hooks/request-logging.hook.ts +++ b/apps/api/src/hooks/request-logging.hook.ts @@ -7,7 +7,7 @@ const ignoreMethods = ['OPTIONS']; const getTrpcInput = ( request: FastifyRequest, ): Record | undefined => { - const input = path(['query', 'input'], request); + const input = path(['query', 'input'], request); try { return typeof input === 'string' ? JSON.parse(input).json : input; } catch (e) { diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts index e960f0d9..83680b72 100644 --- a/apps/api/src/index.ts +++ b/apps/api/src/index.ts @@ -95,15 +95,13 @@ const startServer = async () => { if (isPrivatePath) { // Allow multiple dashboard domains const allowedOrigins = [ - process.env.NEXT_PUBLIC_DASHBOARD_URL, + process.env.DASHBOARD_URL || process.env.NEXT_PUBLIC_DASHBOARD_URL, ...(process.env.API_CORS_ORIGINS?.split(',') ?? []), ].filter(Boolean); const origin = req.headers.origin; const isAllowed = origin && allowedOrigins.includes(origin); - logger.info('Allowed origins', { allowedOrigins, origin, isAllowed }); - return callback(null, { origin: isAllowed ? origin : false, credentials: true, @@ -160,6 +158,12 @@ const startServer = async () => { router: appRouter, createContext: createContext, onError(ctx) { + if ( + ctx.error.code === 'UNAUTHORIZED' && + ctx.path === 'organization.list' + ) { + return; + } ctx.req.log.error('trpc error', { error: ctx.error, path: ctx.path, @@ -191,7 +195,10 @@ const startServer = async () => { instance.get('/healthz/live', liveness); instance.get('/healthz/ready', readiness); instance.get('/', (_request, reply) => - reply.send({ name: 'openpanel sdk api' }), + reply.send({ + status: 'ok', + message: 'Successfully running OpenPanel.dev API', + }), ); }); diff --git a/apps/api/src/routes/misc.router.ts b/apps/api/src/routes/misc.router.ts index 842c85d4..db5ffba6 100644 --- a/apps/api/src/routes/misc.router.ts +++ b/apps/api/src/routes/misc.router.ts @@ -20,6 +20,18 @@ const miscRouter: FastifyPluginCallback = async (fastify) => { handler: controller.getFavicon, }); + fastify.route({ + method: 'GET', + url: '/og', + handler: controller.getOgImage, + }); + + fastify.route({ + method: 'GET', + url: '/og/clear', + handler: controller.clearOgImages, + }); + fastify.route({ method: 'GET', url: '/favicon/clear', diff --git a/apps/api/src/utils/parseUrlMeta.ts b/apps/api/src/utils/parseUrlMeta.ts index cda031a0..b9de388c 100644 --- a/apps/api/src/utils/parseUrlMeta.ts +++ b/apps/api/src/utils/parseUrlMeta.ts @@ -5,12 +5,16 @@ function fallbackFavicon(url: string) { } function findBestFavicon(favicons: UrlMetaData['favicons']) { - const match = favicons.find( - (favicon) => - favicon.rel === 'shortcut icon' || - favicon.rel === 'icon' || - favicon.rel === 'apple-touch-icon', - ); + const match = favicons + .sort((a, b) => { + return a.rel.length - b.rel.length; + }) + .find( + (favicon) => + favicon.rel === 'shortcut icon' || + favicon.rel === 'icon' || + favicon.rel === 'apple-touch-icon', + ); if (match) { return match.href; @@ -18,11 +22,32 @@ function findBestFavicon(favicons: UrlMetaData['favicons']) { return null; } +function findBestOgImage(data: UrlMetaData): string | null { + // Priority order for OG images + const candidates = [ + data['og:image:secure_url'], + data['og:image:url'], + data['og:image'], + data['twitter:image:src'], + data['twitter:image'], + ]; + + for (const candidate of candidates) { + if (candidate?.trim()) { + return candidate.trim(); + } + } + + return null; +} + function transform(data: UrlMetaData, url: string) { const favicon = findBestFavicon(data.favicons); + const ogImage = findBestOgImage(data); return { favicon: favicon ? new URL(favicon, url).toString() : fallbackFavicon(url), + ogImage: ogImage ? new URL(ogImage, url).toString() : null, }; } @@ -32,6 +57,11 @@ interface UrlMetaData { href: string; sizes: string; }[]; + 'og:image'?: string; + 'og:image:url'?: string; + 'og:image:secure_url'?: string; + 'twitter:image'?: string; + 'twitter:image:src'?: string; } export async function parseUrlMeta(url: string) { @@ -42,6 +72,7 @@ export async function parseUrlMeta(url: string) { } catch (err) { return { favicon: fallbackFavicon(url), + ogImage: null, }; } } diff --git a/apps/api/tsdown.config.ts b/apps/api/tsdown.config.ts new file mode 100644 index 00000000..80837b37 --- /dev/null +++ b/apps/api/tsdown.config.ts @@ -0,0 +1,23 @@ +import { defineConfig } from 'tsdown'; +import type { Options } from 'tsdown'; + +const options: Options = { + clean: true, + entry: ['src/index.ts'], + noExternal: [/^@openpanel\/.*$/u, /^@\/.*$/u], + external: ['@hyperdx/node-opentelemetry', 'winston', '@node-rs/argon2'], + sourcemap: true, + platform: 'node', + shims: true, + inputOptions: { + jsx: 'react', + }, +}; + +if (process.env.WATCH) { + options.watch = ['src', '../../packages']; + options.onSuccess = 'node --enable-source-maps dist/index.js'; + options.minify = false; +} + +export default defineConfig(options); diff --git a/apps/api/tsup.config.ts b/apps/api/tsup.config.ts deleted file mode 100644 index d1bf3ea8..00000000 --- a/apps/api/tsup.config.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { defineConfig } from 'tsup'; -import type { Options } from 'tsup'; - -const options: Options = { - clean: true, - entry: ['src/index.ts'], - noExternal: [/^@openpanel\/.*$/u, /^@\/.*$/u], - external: [ - '@hyperdx/node-opentelemetry', - 'winston', - '@node-rs/argon2', - 'bcrypt', - ], - ignoreWatch: ['../../**/{.git,node_modules,dist}/**'], - sourcemap: true, - splitting: false, -}; - -if (process.env.WATCH) { - options.watch = ['src/**/*', '../../packages/**/*']; - - options.onSuccess = 'node dist/index.js'; - options.minify = false; -} - -export default defineConfig(options); diff --git a/apps/dashboard/.gitignore b/apps/dashboard/.gitignore deleted file mode 100644 index 04424e48..00000000 --- a/apps/dashboard/.gitignore +++ /dev/null @@ -1,39 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# database -/prisma/db.sqlite -/prisma/db.sqlite-journal - -# next.js -/.next/ -/out/ -next-env.d.ts - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* - -# local env files -# do not commit any .env files to git, except for the .env.example file. https://create.t3.gg/en/usage/env-variables#using-environment-variables -.env -.env*.local - -# vercel -.vercel - -# typescript -*.tsbuildinfo diff --git a/apps/dashboard/.sentryclirc b/apps/dashboard/.sentryclirc deleted file mode 100644 index ddbffd3c..00000000 --- a/apps/dashboard/.sentryclirc +++ /dev/null @@ -1,3 +0,0 @@ - -[auth] -token=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 diff --git a/apps/dashboard/Dockerfile b/apps/dashboard/Dockerfile deleted file mode 100644 index 3d25011e..00000000 --- a/apps/dashboard/Dockerfile +++ /dev/null @@ -1,103 +0,0 @@ -ARG NODE_VERSION=20.15.1 - -FROM node:${NODE_VERSION}-slim AS base - -# FIX: Bad workaround (https://github.com/nodejs/corepack/issues/612) -ENV COREPACK_INTEGRITY_KEYS=0 - -ENV SKIP_ENV_VALIDATION="1" - -ARG DATABASE_URL -ENV DATABASE_URL=$DATABASE_URL - -ARG ENABLE_INSTRUMENTATION_HOOK -ENV ENABLE_INSTRUMENTATION_HOOK=$ENABLE_INSTRUMENTATION_HOOK - -ARG NEXT_PUBLIC_SELF_HOSTED -ENV NEXT_PUBLIC_SELF_HOSTED=$NEXT_PUBLIC_SELF_HOSTED - -ENV PNPM_HOME="/pnpm" -ENV PATH="$PNPM_HOME:$PATH" - -# Install necessary dependencies for prisma -RUN apt-get update && apt-get install -y \ - openssl \ - libssl3 \ - curl \ - && rm -rf /var/lib/apt/lists/* - -RUN corepack enable - -WORKDIR /app - -ARG CACHE_BUST -RUN echo "CACHE BUSTER: $CACHE_BUST" - -COPY package.json package.json -COPY pnpm-lock.yaml pnpm-lock.yaml -COPY pnpm-workspace.yaml pnpm-workspace.yaml -COPY apps/dashboard/package.json apps/dashboard/package.json -COPY packages/db/package.json packages/db/package.json -COPY packages/json/package.json packages/json/package.json -COPY packages/redis/package.json packages/redis/package.json -COPY packages/queue/package.json packages/queue/package.json -COPY packages/common/package.json packages/common/package.json -COPY packages/auth/package.json packages/auth/package.json -COPY packages/email/package.json packages/email/package.json -COPY packages/constants/package.json packages/constants/package.json -COPY packages/payments/package.json packages/payments/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 -FROM base AS build - -WORKDIR /app -RUN pnpm install --frozen-lockfile --ignore-scripts - -COPY apps/dashboard apps/dashboard -COPY packages packages -COPY tooling tooling -RUN pnpm db:codegen - -WORKDIR /app/apps/dashboard - -# Will be replaced on runtime -ENV NEXT_PUBLIC_DASHBOARD_URL="__NEXT_PUBLIC_DASHBOARD_URL__" -ENV NEXT_PUBLIC_API_URL="__NEXT_PUBLIC_API_URL__" - -RUN pnpm run build - -# RUNNER -FROM base AS runner - -WORKDIR /app - -ENV NODE_ENV=production -ENV NEXT_TELEMETRY_DISABLED=1 - -RUN addgroup --system --gid 1001 nodejs -RUN adduser --system --uid 1001 nextjs - -# Set the correct permission for prerender cache -RUN mkdir .next -RUN chown nextjs:nodejs .next - -# Set the correct permissions for the entire /app directory -COPY --from=build --chown=nextjs:nodejs /app/apps/dashboard/.next/standalone ./ -COPY --from=build --chown=nextjs:nodejs /app/apps/dashboard/.next/static ./apps/dashboard/.next/static -COPY --from=build --chown=nextjs:nodejs /app/apps/dashboard/public ./apps/dashboard/public - -# Copy and set permissions for the entrypoint script -COPY --from=build --chown=nextjs:nodejs /app/apps/dashboard/entrypoint.sh ./entrypoint.sh -RUN chmod +x ./entrypoint.sh - -USER nextjs - -EXPOSE 3000 - -ENV PORT=3000 -ENV HOSTNAME=0.0.0.0 - -ENTRYPOINT [ "/app/entrypoint.sh", "node", "/app/apps/dashboard/server.js"] \ No newline at end of file diff --git a/apps/dashboard/README.md b/apps/dashboard/README.md deleted file mode 100644 index 5c6b25c4..00000000 --- a/apps/dashboard/README.md +++ /dev/null @@ -1 +0,0 @@ -# Dashboard diff --git a/apps/dashboard/components.json b/apps/dashboard/components.json deleted file mode 100644 index 9750ef2a..00000000 --- a/apps/dashboard/components.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "$schema": "https://ui.shadcn.com/schema.json", - "style": "default", - "rsc": false, - "tsx": true, - "tailwind": { - "config": "tailwind.config.ts", - "css": "src/styles/globals.css", - "baseColor": "slate", - "cssVariables": true - }, - "aliases": { - "components": "@/components", - "utils": "@/utils/cn" - } -} diff --git a/apps/dashboard/entrypoint.sh b/apps/dashboard/entrypoint.sh deleted file mode 100644 index 666f6eb5..00000000 --- a/apps/dashboard/entrypoint.sh +++ /dev/null @@ -1,33 +0,0 @@ -#!/bin/sh -set -e - -echo "> Replace env variable placeholders with runtime values..." - -# Define environment variables to check (space-separated string) -variables_to_replace="NEXT_PUBLIC_DASHBOARD_URL NEXT_PUBLIC_API_URL NEXT_PUBLIC_SELF_HOSTED" - -# Replace env variable placeholders with real values -for key in $variables_to_replace; do - value=$(eval echo \$"$key") - if [ -n "$value" ]; then - echo " - Searching for $key with value $value..." - # Use standard placeholder format for all variables - placeholder="__${key}__" - - # Run the replacement - find /app -type f \( -name "*.js" -o -name "*.html" \) | while read -r file; do - if grep -q "$placeholder" "$file"; then - echo " - Replacing in file: $file" - sed -i "s|$placeholder|$value|g" "$file" - fi - done - else - echo " - Skipping $key as it has no value set." - fi -done - -echo "> Done!" -echo "> Running $@" - -# Execute the container's main process (CMD in Dockerfile) -exec "$@" \ No newline at end of file diff --git a/apps/dashboard/next.config.mjs b/apps/dashboard/next.config.mjs deleted file mode 100644 index 6553dbba..00000000 --- a/apps/dashboard/next.config.mjs +++ /dev/null @@ -1,47 +0,0 @@ -// @ts-expect-error -import { PrismaPlugin } from '@prisma/nextjs-monorepo-workaround-plugin'; - -/** @type {import("next").NextConfig} */ -const config = { - output: 'standalone', - webpack: (config, { isServer }) => { - if (isServer) { - config.plugins = [...config.plugins, new PrismaPlugin()]; - } - - return config; - }, - reactStrictMode: true, - transpilePackages: [ - '@openpanel/queue', - '@openpanel/db', - '@openpanel/common', - '@openpanel/constants', - '@openpanel/redis', - '@openpanel/validation', - '@openpanel/email', - ], - eslint: { ignoreDuringBuilds: true }, - typescript: { ignoreBuildErrors: true }, - experimental: { - // Avoid "Critical dependency: the request of a dependency is an expression" - serverComponentsExternalPackages: [ - 'bullmq', - 'ioredis', - '@hyperdx/node-opentelemetry', - '@node-rs/argon2', - ], - instrumentationHook: !!process.env.ENABLE_INSTRUMENTATION_HOOK, - }, - /** - * If you are using `appDir` then you must comment the below `i18n` config out. - * - * @see https://github.com/vercel/next.js/issues/41980 - */ - i18n: { - locales: ['en'], - defaultLocale: 'en', - }, -}; - -export default config; diff --git a/apps/dashboard/package.json b/apps/dashboard/package.json deleted file mode 100644 index 372d9b18..00000000 --- a/apps/dashboard/package.json +++ /dev/null @@ -1,149 +0,0 @@ -{ - "name": "@openpanel/dashboard", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "rm -rf .next && pnpm with-env next dev", - "testing": "pnpm dev", - "build": "pnpm with-env next build", - "start": "pnpm with-env next start", - "typecheck": "tsc --noEmit", - "with-env": "dotenv -e ../../.env -c --" - }, - "dependencies": { - "@ai-sdk/react": "^1.2.5", - "@clickhouse/client": "^1.2.0", - "@dnd-kit/core": "^6.3.1", - "@dnd-kit/sortable": "^10.0.0", - "@dnd-kit/utilities": "^3.2.2", - "@hookform/resolvers": "^3.3.4", - "@hyperdx/node-opentelemetry": "^0.8.1", - "@openpanel/auth": "workspace:^", - "@openpanel/common": "workspace:^", - "@openpanel/constants": "workspace:^", - "@openpanel/db": "workspace:^", - "@openpanel/integrations": "workspace:^", - "@openpanel/json": "workspace:*", - "@openpanel/nextjs": "1.0.3", - "@openpanel/queue": "workspace:^", - "@openpanel/sdk-info": "workspace:^", - "@openpanel/validation": "workspace:^", - "@prisma/nextjs-monorepo-workaround-plugin": "^5.12.1", - "@radix-ui/react-accordion": "^1.1.2", - "@radix-ui/react-alert-dialog": "^1.0.5", - "@radix-ui/react-aspect-ratio": "^1.0.3", - "@radix-ui/react-avatar": "^1.0.4", - "@radix-ui/react-checkbox": "^1.0.4", - "@radix-ui/react-dialog": "^1.0.5", - "@radix-ui/react-dropdown-menu": "^2.0.6", - "@radix-ui/react-label": "^2.0.2", - "@radix-ui/react-popover": "^1.0.7", - "@radix-ui/react-portal": "^1.1.1", - "@radix-ui/react-progress": "^1.0.3", - "@radix-ui/react-radio-group": "^1.1.3", - "@radix-ui/react-scroll-area": "^1.0.5", - "@radix-ui/react-slot": "^1.0.2", - "@radix-ui/react-switch": "^1.0.3", - "@radix-ui/react-tabs": "^1.0.4", - "@radix-ui/react-toast": "^1.1.5", - "@radix-ui/react-toggle": "^1.0.3", - "@radix-ui/react-toggle-group": "^1.0.4", - "@radix-ui/react-tooltip": "^1.0.7", - "@reduxjs/toolkit": "^1.9.7", - "@t3-oss/env-nextjs": "^0.7.3", - "@tailwindcss/container-queries": "^0.1.1", - "@tailwindcss/typography": "^0.5.15", - "@tanstack/react-query": "^4.36.1", - "@tanstack/react-table": "^8.11.8", - "@tanstack/react-virtual": "^3.13.2", - "@trpc/client": "^10.45.2", - "@trpc/next": "^10.45.2", - "@trpc/react-query": "^10.45.2", - "@trpc/server": "^10.45.2", - "@types/d3": "^7.4.3", - "ai": "^4.2.10", - "bcrypt": "^5.1.1", - "bind-event-listener": "^3.0.0", - "class-variance-authority": "^0.7.0", - "clsx": "^2.1.0", - "cmdk": "^0.2.1", - "d3": "^7.8.5", - "date-fns": "^3.3.1", - "embla-carousel-react": "8.0.0-rc22", - "flag-icons": "^7.1.0", - "framer-motion": "^11.0.28", - "geist": "^1.3.1", - "hamburger-react": "^2.5.0", - "input-otp": "^1.2.4", - "javascript-time-ago": "^2.5.9", - "katex": "^0.16.21", - "lodash.debounce": "^4.0.8", - "lodash.isequal": "^4.5.0", - "lodash.throttle": "^4.1.1", - "lottie-react": "^2.4.0", - "lucide-react": "^0.513.0", - "mathjs": "^12.3.2", - "mitt": "^3.0.1", - "next": "14.2.1", - "next-auth": "^4.24.5", - "next-themes": "^0.2.1", - "nextjs-toploader": "^1.6.11", - "nuqs": "^2.0.2", - "prisma-error-enum": "^0.1.3", - "pushmodal": "^1.0.3", - "ramda": "^0.29.1", - "random-animal-name": "^0.1.1", - "rc-virtual-list": "^3.14.5", - "react": "18.2.0", - "react-animate-height": "^3.2.3", - "react-animated-numbers": "^0.18.0", - "react-day-picker": "^8.10.0", - "react-dom": "18.2.0", - "react-hook-form": "^7.50.1", - "react-in-viewport": "1.0.0-alpha.30", - "react-markdown": "^10.1.0", - "react-redux": "^8.1.3", - "react-responsive": "^9.0.2", - "react-simple-maps": "3.0.0", - "react-svg-worldmap": "2.0.0-alpha.16", - "react-syntax-highlighter": "^15.5.0", - "react-use-websocket": "^4.7.0", - "react-virtualized-auto-sizer": "^1.0.22", - "recharts": "^2.12.0", - "rehype-katex": "^7.0.1", - "remark-gfm": "^4.0.1", - "remark-highlight": "^0.1.1", - "remark-math": "^6.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.2", - "short-unique-id": "^5.0.3", - "slugify": "^1.6.6", - "sonner": "^1.4.0", - "sqlstring": "^2.3.3", - "superjson": "^1.13.3", - "tailwind-merge": "^1.14.0", - "tailwindcss-animate": "^1.0.7", - "usehooks-ts": "^2.14.0", - "zod": "catalog:" - }, - "devDependencies": { - "@openpanel/payments": "workspace:*", - "@openpanel/trpc": "workspace:*", - "@openpanel/tsconfig": "workspace:*", - "@types/bcrypt": "^5.0.2", - "@types/lodash.debounce": "^4.0.9", - "@types/lodash.isequal": "^4.5.8", - "@types/lodash.throttle": "^4.1.9", - "@types/node": "20.14.8", - "@types/ramda": "^0.29.10", - "@types/react": "^18.2.20", - "@types/react-dom": "^18.2.7", - "@types/react-simple-maps": "^3.0.4", - "@types/react-syntax-highlighter": "^15.5.11", - "@types/sqlstring": "^2.3.2", - "autoprefixer": "^10.4.17", - "postcss": "^8.4.35", - "tailwindcss": "^3.4.1", - "typescript": "^5.3.3" - } -} diff --git a/apps/dashboard/postcss.config.cjs b/apps/dashboard/postcss.config.cjs deleted file mode 100644 index e305dd92..00000000 --- a/apps/dashboard/postcss.config.cjs +++ /dev/null @@ -1,8 +0,0 @@ -const config = { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, -}; - -module.exports = config; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/chat/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/chat/page.tsx deleted file mode 100644 index 57dc2280..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/chat/page.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import Chat from '@/components/chat/chat'; -import { db, getOrganizationById } from '@openpanel/db'; -import type { UIMessage } from 'ai'; - -export default async function ChatPage({ - params, -}: { - params: { organizationSlug: string; projectId: string }; -}) { - const { projectId } = await params; - const [organization, chat] = await Promise.all([ - getOrganizationById(params.organizationSlug), - db.chat.findFirst({ - where: { - projectId, - }, - orderBy: { - createdAt: 'desc', - }, - }), - ]); - - const messages = ((chat?.messages as UIMessage[]) || []).slice(-10); - return ( - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/list-reports.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/list-reports.tsx deleted file mode 100644 index b6d322d6..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/list-reports.tsx +++ /dev/null @@ -1,181 +0,0 @@ -'use client'; - -import { FullPageEmptyState } from '@/components/full-page-empty-state'; -import { useOverviewOptions } from '@/components/overview/useOverviewOptions'; -import { ReportChart } from '@/components/report-chart'; -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { useAppParams } from '@/hooks/useAppParams'; -import { api, handleError } from '@/trpc/client'; -import { cn } from '@/utils/cn'; -import { - ChevronRight, - LayoutPanelTopIcon, - MoreHorizontal, - PlusIcon, - Trash, -} from 'lucide-react'; -import Link from 'next/link'; -import { useRouter } from 'next/navigation'; -import { toast } from 'sonner'; - -import { - getDefaultIntervalByDates, - getDefaultIntervalByRange, - timeWindows, -} from '@openpanel/constants'; -import type { IServiceDashboard, getReportsByDashboardId } from '@openpanel/db'; - -import { OverviewInterval } from '@/components/overview/overview-interval'; -import { OverviewRange } from '@/components/overview/overview-range'; - -interface ListReportsProps { - reports: Awaited>; - dashboard: IServiceDashboard; -} - -export function ListReports({ reports, dashboard }: ListReportsProps) { - const router = useRouter(); - const params = useAppParams<{ dashboardId: string }>(); - const { range, startDate, endDate, interval } = useOverviewOptions(); - const deletion = api.report.delete.useMutation({ - onError: handleError, - onSuccess() { - router.refresh(); - toast('Report deleted'); - }, - }); - return ( - <> -
-

{dashboard.name}

-
- - - -
-
-
- {reports.map((report) => { - const chartRange = report.range; - return ( -
- -
-
{report.name}
- {chartRange !== null && ( -
- - {timeWindows[chartRange].label} - - {startDate && endDate ? ( - Custom dates - ) : ( - range !== null && - chartRange !== range && ( - {timeWindows[range].label} - ) - )} -
- )} -
-
- - - - - - - { - event.stopPropagation(); - deletion.mutate({ - reportId: report.id, - }); - }} - > - - Delete - - - - - -
- -
- -
-
- ); - })} - {reports.length === 0 && ( - -

You can visualize your data with a report

- -
- )} -
- - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/page.tsx deleted file mode 100644 index 5b5ef077..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/[dashboardId]/page.tsx +++ /dev/null @@ -1,32 +0,0 @@ -import { Padding } from '@/components/ui/padding'; -import { notFound } from 'next/navigation'; - -import { getDashboardById, getReportsByDashboardId } from '@openpanel/db'; - -import { ListReports } from './list-reports'; - -interface PageProps { - params: { - projectId: string; - dashboardId: string; - }; -} - -export default async function Page({ - params: { projectId, dashboardId }, -}: PageProps) { - const [dashboard, reports] = await Promise.all([ - getDashboardById(dashboardId, projectId), - getReportsByDashboardId(dashboardId), - ]); - - if (!dashboard) { - return notFound(); - } - - return ( - - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/header.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/header.tsx deleted file mode 100644 index 9ddc769a..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/header.tsx +++ /dev/null @@ -1,22 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { pushModal } from '@/modals'; -import { PlusIcon } from 'lucide-react'; - -export function HeaderDashboards() { - return ( -
-

Dashboards

- -
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/index.tsx deleted file mode 100644 index 30c13f12..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/list-dashboards/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import FullPageLoadingState from '@/components/full-page-loading-state'; -import { Padding } from '@/components/ui/padding'; -import withSuspense from '@/hocs/with-suspense'; - -import { getDashboardsByProjectId } from '@openpanel/db'; - -import { HeaderDashboards } from './header'; -import { ListDashboards } from './list-dashboards'; - -interface Props { - projectId: string; -} - -const ListDashboardsServer = async ({ projectId }: Props) => { - const dashboards = await getDashboardsByProjectId(projectId); - - return ( - - - - - ); -}; - -export default withSuspense(ListDashboardsServer, FullPageLoadingState); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/page.tsx deleted file mode 100644 index 8787f9c2..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/dashboards/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import ListDashboardsServer from './list-dashboards'; - -interface PageProps { - params: { - projectId: string; - }; -} - -export default function Page({ params: { projectId } }: PageProps) { - return ; -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/conversions.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/conversions.tsx deleted file mode 100644 index 30725de5..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/conversions.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; - -import { TableButtons } from '@/components/data-table'; -import { EventsTable } from '@/components/events/table'; -import { EventsTableColumns } from '@/components/events/table/events-table-columns'; -import { api } from '@/trpc/client'; -import { Loader2Icon } from 'lucide-react'; - -type Props = { - projectId: string; - profileId?: string; -}; - -const Conversions = ({ projectId }: Props) => { - const query = api.event.conversions.useInfiniteQuery( - { - projectId, - }, - { - getNextPageParam: (lastPage) => lastPage.meta.next, - keepPreviousData: true, - }, - ); - - return ( -
- - - {query.isRefetching && ( -
- -
- )} -
- -
- ); -}; - -export default Conversions; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/events.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/events.tsx deleted file mode 100644 index ab612730..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/events.tsx +++ /dev/null @@ -1,93 +0,0 @@ -'use client'; - -import { TableButtons } from '@/components/data-table'; -import EventListener from '@/components/events/event-listener'; -import { EventsTable } from '@/components/events/table'; -import { EventsTableColumns } from '@/components/events/table/events-table-columns'; -import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons'; -import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer'; -import { Button } from '@/components/ui/button'; -import { - useEventQueryFilters, - useEventQueryNamesFilter, -} from '@/hooks/useEventQueryFilters'; -import { pushModal } from '@/modals'; -import { api } from '@/trpc/client'; -import { format } from 'date-fns'; -import { CalendarIcon, Loader2Icon } from 'lucide-react'; -import { parseAsIsoDateTime, useQueryState } from 'nuqs'; - -type Props = { - projectId: string; - profileId?: string; -}; - -const Events = ({ projectId, profileId }: Props) => { - const [filters] = useEventQueryFilters(); - const [startDate, setStartDate] = useQueryState( - 'startDate', - parseAsIsoDateTime, - ); - const [eventNames] = useEventQueryNamesFilter(); - - const [endDate, setEndDate] = useQueryState('endDate', parseAsIsoDateTime); - const query = api.event.events.useInfiniteQuery( - { - projectId, - filters, - events: eventNames, - profileId, - startDate: startDate || undefined, - endDate: endDate || undefined, - }, - { - getNextPageParam: (lastPage) => lastPage.meta.next, - keepPreviousData: true, - }, - ); - - return ( -
- - query.refetch()} /> - - - - - {query.isRefetching && ( -
- -
- )} -
- -
- ); -}; - -export default Events; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/page.tsx deleted file mode 100644 index 64c590f8..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/events/page.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import { PageTabs, PageTabsLink } from '@/components/page-tabs'; -import { Padding } from '@/components/ui/padding'; -import { parseAsStringEnum } from 'nuqs/server'; - -import Charts from './charts'; -import Conversions from './conversions'; -import Events from './events'; - -interface PageProps { - params: { - projectId: string; - }; - searchParams: Record; -} - -export default function Page({ - params: { projectId }, - searchParams, -}: PageProps) { - const tab = parseAsStringEnum(['events', 'conversions', 'charts']) - .withDefault('events') - .parseServerSide(searchParams.tab); - - return ( - <> - -
- - - Events - - - Conversions - - - Charts - - -
- {tab === 'events' && } - {tab === 'conversions' && } - {tab === 'charts' && } -
- - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-content.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-content.tsx deleted file mode 100644 index 316392aa..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-content.tsx +++ /dev/null @@ -1,33 +0,0 @@ -'use client'; - -import { cn } from '@/utils/cn'; -import { useSelectedLayoutSegments } from 'next/navigation'; - -const NOT_MIGRATED_PAGES = ['reports']; - -export default function LayoutContent({ - children, -}: { - children: React.ReactNode; -}) { - const segments = useSelectedLayoutSegments(); - - if (segments[0] && NOT_MIGRATED_PAGES.includes(segments[0])) { - return ( -
- {children} -
- ); - } - - return ( -
- {children} -
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-menu.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-menu.tsx deleted file mode 100644 index 8111744e..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-menu.tsx +++ /dev/null @@ -1,243 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { pushModal } from '@/modals'; -import { cn } from '@/utils/cn'; -import { - BanknoteIcon, - ChartLineIcon, - DollarSignIcon, - GanttChartIcon, - Globe2Icon, - HeartHandshakeIcon, - LayersIcon, - LayoutPanelTopIcon, - PlusIcon, - ScanEyeIcon, - ServerIcon, - SparklesIcon, - UsersIcon, - WallpaperIcon, -} from 'lucide-react'; -import type { LucideIcon } from 'lucide-react'; -import { usePathname } from 'next/navigation'; - -import { ProjectLink } from '@/components/links'; -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { CommandShortcut } from '@/components/ui/command'; -import { useNumber } from '@/hooks/useNumerFormatter'; -import type { IServiceDashboards, IServiceOrganization } from '@openpanel/db'; -import { differenceInDays, format } from 'date-fns'; - -function LinkWithIcon({ - href, - icon: Icon, - label, - active: overrideActive, - className, -}: { - href: string; - icon: LucideIcon; - label: React.ReactNode; - active?: boolean; - className?: string; -}) { - const pathname = usePathname(); - const active = overrideActive || href === pathname; - return ( - - -
{label}
-
- ); -} - -interface LayoutMenuProps { - dashboards: IServiceDashboards; - organization: IServiceOrganization; -} -export default function LayoutMenu({ - dashboards, - organization, -}: LayoutMenuProps) { - const number = useNumber(); - const { - isTrial, - isExpired, - isExceeded, - isCanceled, - subscriptionEndsAt, - subscriptionPeriodEventsCount, - subscriptionPeriodEventsLimit, - subscriptionProductId, - } = organization; - return ( - <> -
- {(subscriptionProductId === '036efa2a-b3b4-4c75-b24a-9cac6bb8893b' || - subscriptionProductId === 'a18b4bee-d3db-4404-be6f-fba2f042d9ed') && ( - - -
-
Free plan is removed
-
- We've removed the free plan. You can upgrade to a paid plan to - continue using OpenPanel. -
-
-
- )} - {process.env.NEXT_PUBLIC_SELF_HOSTED === 'true' && ( - - -
-
Become a supporter
-
-
- )} - {isTrial && subscriptionEndsAt && ( - - -
-
- Free trial ends in{' '} - {differenceInDays(subscriptionEndsAt, new Date())} days -
-
-
- )} - {isExpired && subscriptionEndsAt && ( - - -
-
Subscription expired
-
- You can still use OpenPanel but you won't have access to new - incoming data. -
-
-
- )} - {isCanceled && subscriptionEndsAt && ( - - -
-
Subscription canceled
-
- {differenceInDays(new Date(), subscriptionEndsAt)} days ago -
-
-
- )} - {isExceeded && subscriptionEndsAt && ( - - -
-
Events limit exceeded
-
- {number.format(subscriptionPeriodEventsCount)} /{' '} - {number.format(subscriptionPeriodEventsLimit)} -
-
-
- )} - - -
-
Ask AI
-
- ⌘K -
- - -
-
Create report
-
- ⌘J -
-
- - - - - - - -
-
-
Your dashboards
- -
-
- {dashboards.map((item) => ( - - ))} -
-
- {process.env.NEXT_PUBLIC_SELF_HOSTED === 'true' && ( -
-
- Self-hosted instance -
-
- )} - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-organization-selector.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-organization-selector.tsx deleted file mode 100644 index 6eabdaba..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-organization-selector.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; - -import { Combobox } from '@/components/ui/combobox'; -import { useAppParams } from '@/hooks/useAppParams'; -import { Building } from 'lucide-react'; -import { useRouter } from 'next/navigation'; - -import type { IServiceOrganization } from '@openpanel/db'; - -interface LayoutOrganizationSelectorProps { - organizations: IServiceOrganization[]; -} - -export default function LayoutOrganizationSelector({ - organizations, -}: LayoutOrganizationSelectorProps) { - const params = useAppParams(); - const router = useRouter(); - - const organization = organizations.find( - (item) => item.id === params.organizationId, - ); - - return ( - item.id) - .map((item) => ({ - label: item.name, - value: item.id, - })) ?? [] - } - onChange={(value) => { - router.push(`/${value}`); - }} - /> - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sidebar.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sidebar.tsx deleted file mode 100644 index afcc92a5..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sidebar.tsx +++ /dev/null @@ -1,90 +0,0 @@ -'use client'; - -import { LogoSquare } from '@/components/logo'; -import SettingsToggle from '@/components/settings-toggle'; -import { Button } from '@/components/ui/button'; -import { cn } from '@/utils/cn'; -import { MenuIcon, XIcon } from 'lucide-react'; -import { usePathname } from 'next/navigation'; -import { useEffect, useState } from 'react'; - -import type { - IServiceDashboards, - IServiceOrganization, - getProjectsByOrganizationId, -} from '@openpanel/db'; - -import { useAppParams } from '@/hooks/useAppParams'; -import Link from 'next/link'; -import LayoutMenu from './layout-menu'; -import LayoutProjectSelector from './layout-project-selector'; - -interface LayoutSidebarProps { - organizations: IServiceOrganization[]; - dashboards: IServiceDashboards; - projectId: string; - projects: Awaited>; -} -export function LayoutSidebar({ - organizations, - dashboards, - projects, -}: LayoutSidebarProps) { - const [active, setActive] = useState(false); - const pathname = usePathname(); - const { organizationId } = useAppParams(); - const organization = organizations.find((o) => o.id === organizationId)!; - - useEffect(() => { - setActive(false); - }, [pathname]); - - return ( - <> - - -
- - - - - -
-
- -
-
-
-
-
- - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header.tsx deleted file mode 100644 index 5c29c82d..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { cn } from '@/utils/cn'; - -interface StickyBelowHeaderProps { - children: React.ReactNode; - className?: string; -} - -export function StickyBelowHeader({ - children, - className, -}: StickyBelowHeaderProps) { - return ( -
- {children} -
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout.tsx deleted file mode 100644 index c4e14f6d..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/layout.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import { FullPageEmptyState } from '@/components/full-page-empty-state'; - -import { - getDashboardsByProjectId, - getOrganizations, - getProjects, -} from '@openpanel/db'; - -import { auth } from '@openpanel/auth/nextjs'; -import LayoutContent from './layout-content'; -import { LayoutSidebar } from './layout-sidebar'; -import SideEffects from './side-effects'; - -interface AppLayoutProps { - children: React.ReactNode; - params: { - organizationSlug: string; - projectId: string; - }; -} - -export default async function AppLayout({ - children, - params: { organizationSlug: organizationId, projectId }, -}: AppLayoutProps) { - const { userId } = await auth(); - const [organizations, projects, dashboards] = await Promise.all([ - getOrganizations(userId), - getProjects({ organizationId, userId }), - getDashboardsByProjectId(projectId), - ]); - - const organization = organizations.find((item) => item.id === organizationId); - - if (!organization) { - return ( - - The organization you were looking for could not be found. - - ); - } - - if (!projects.find((item) => item.id === projectId)) { - return ( - - The project you were looking for could not be found. - - ); - } - - return ( -
- - {children} - -
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/page-layout.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/page-layout.tsx deleted file mode 100644 index 7589b4ef..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/page-layout.tsx +++ /dev/null @@ -1,15 +0,0 @@ -interface PageLayoutProps { - title: React.ReactNode; -} - -function PageLayout({ title }: PageLayoutProps) { - return ( - <> -
-
{title}
-
- - ); -} - -export default PageLayout; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/page.tsx deleted file mode 100644 index 933771d0..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/page.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import { PageTabs, PageTabsLink } from '@/components/page-tabs'; -import { Padding } from '@/components/ui/padding'; -import { parseAsStringEnum } from 'nuqs/server'; - -import { Pages } from './pages'; - -interface PageProps { - params: { - projectId: string; - }; - searchParams: { - tab: string; - }; -} - -export default function Page({ - params: { projectId }, - searchParams, -}: PageProps) { - const tab = parseAsStringEnum(['pages', 'trends']) - .withDefault('pages') - .parseServerSide(searchParams.tab); - - return ( - - - - Pages - - - {tab === 'pages' && } - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/pages.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/pages.tsx deleted file mode 100644 index 3195af0f..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/pages/pages.tsx +++ /dev/null @@ -1,193 +0,0 @@ -'use client'; - -import { TableButtons } from '@/components/data-table'; -import { Input } from '@/components/ui/input'; -import { useDebounceValue } from '@/hooks/useDebounceValue'; -import { type RouterOutputs, api } from '@/trpc/client'; -import { parseAsInteger, useQueryState } from 'nuqs'; - -import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer'; -import { OverviewInterval } from '@/components/overview/overview-interval'; -import { OverviewRange } from '@/components/overview/overview-range'; -import { useOverviewOptions } from '@/components/overview/useOverviewOptions'; -import { Pagination } from '@/components/pagination'; -import { ReportChart } from '@/components/report-chart'; -import { useEventQueryFilters } from '@/hooks/useEventQueryFilters'; -import { useNumber } from '@/hooks/useNumerFormatter'; -import type { IChartRange, IInterval } from '@openpanel/validation'; -import { memo } from 'react'; - -export function Pages({ projectId }: { projectId: string }) { - const take = 20; - const { range, interval } = useOverviewOptions(); - const [filters, setFilters] = useEventQueryFilters(); - const [cursor, setCursor] = useQueryState( - 'cursor', - parseAsInteger.withDefault(0), - ); - const [search, setSearch] = useQueryState('search', { - defaultValue: '', - shallow: true, - }); - const debouncedSearch = useDebounceValue(search, 500); - const query = api.event.pages.useQuery( - { - projectId, - cursor, - take, - search: debouncedSearch, - range, - interval, - filters, - }, - { - keepPreviousData: true, - }, - ); - const data = query.data ?? []; - - return ( - <> - - - - - { - setSearch(e.target.value); - setCursor(0); - }} - /> - -
- {data.map((page) => { - return ( - - ); - })} -
-
- -
- - ); -} - -const PageCard = memo( - ({ - page, - range, - interval, - projectId, - }: { - page: RouterOutputs['event']['pages'][number]; - range: IChartRange; - interval: IInterval; - projectId: string; - }) => { - const number = useNumber(); - return ( -
-
-
-
- {page.title} -
- - {page.path} - -
-
-
-
-
- {number.formatWithUnit(page.avg_duration, 'min')} -
-
- duration -
-
-
-
- {number.formatWithUnit(page.bounce_rate / 100, '%')} -
-
- bounce rate -
-
-
-
- {number.format(page.sessions)} -
-
- sessions -
-
-
- -
- ); - }, -); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/most-events/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/most-events/index.tsx deleted file mode 100644 index 7220cdae..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/most-events/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import withLoadingWidget from '@/hocs/with-loading-widget'; -import { escape } from 'sqlstring'; - -import { TABLE_NAMES, chQuery } from '@openpanel/db'; - -import MostEvents from './most-events'; - -type Props = { - projectId: string; - profileId: string; -}; - -const MostEventsServer = async ({ projectId, profileId }: Props) => { - const data = await chQuery<{ count: number; name: string }>( - `SELECT count(*) as count, name FROM ${TABLE_NAMES.events} WHERE name NOT IN ('screen_view', 'session_start', 'session_end') AND project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY name ORDER BY count DESC`, - ); - return ; -}; - -export default withLoadingWidget(MostEventsServer); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx deleted file mode 100644 index 347296a4..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/page.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import ClickToCopy from '@/components/click-to-copy'; -import { ProfileAvatar } from '@/components/profiles/profile-avatar'; -import { Padding } from '@/components/ui/padding'; -import { getProfileName } from '@/utils/getters'; -import { notFound } from 'next/navigation'; - -import { getProfileById, getProfileByIdCached } from '@openpanel/db'; - -import MostEventsServer from './most-events'; -import PopularRoutesServer from './popular-routes'; -import ProfileActivityServer from './profile-activity'; -import ProfileCharts from './profile-charts'; -import Events from './profile-events'; -import ProfileMetrics from './profile-metrics'; - -interface PageProps { - params: { - projectId: string; - profileId: string; - }; - searchParams: { - events?: string; - cursor?: string; - f?: string; - startDate: string; - endDate: string; - }; -} - -export default async function Page({ - params: { projectId, profileId }, -}: PageProps) { - const profile = await getProfileById( - decodeURIComponent(profileId), - projectId, - ); - - if (!profile) { - return notFound(); - } - - return ( - -
- -
- -

- {getProfileName(profile)} -

-
-
-
-
-
-
- -
-
- -
-
- -
-
- -
- - -
-
- -
-
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/popular-routes/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/popular-routes/index.tsx deleted file mode 100644 index c01eca22..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/popular-routes/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import withLoadingWidget from '@/hocs/with-loading-widget'; -import { escape } from 'sqlstring'; - -import { TABLE_NAMES, chQuery } from '@openpanel/db'; - -import PopularRoutes from './popular-routes'; - -type Props = { - projectId: string; - profileId: string; -}; - -const PopularRoutesServer = async ({ projectId, profileId }: Props) => { - const data = await chQuery<{ count: number; path: string }>( - `SELECT count(*) as count, path FROM ${TABLE_NAMES.events} WHERE name = 'screen_view' AND project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY path ORDER BY count DESC`, - ); - return ; -}; - -export default withLoadingWidget(PopularRoutesServer); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/index.tsx deleted file mode 100644 index 61aa195f..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import withLoadingWidget from '@/hocs/with-loading-widget'; -import { escape } from 'sqlstring'; - -import { TABLE_NAMES, chQuery } from '@openpanel/db'; - -import ProfileActivity from './profile-activity'; - -type Props = { - projectId: string; - profileId: string; -}; - -const ProfileActivityServer = async ({ projectId, profileId }: Props) => { - const data = await chQuery<{ count: number; date: string }>( - `SELECT count(*) as count, toStartOfDay(created_at) as date FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} and profile_id = ${escape(profileId)} GROUP BY date ORDER BY date DESC`, - ); - return ; -}; - -export default withLoadingWidget(ProfileActivityServer); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/profile-activity.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/profile-activity.tsx deleted file mode 100644 index d6949c81..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-activity/profile-activity.tsx +++ /dev/null @@ -1,164 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { - Widget, - WidgetBody, - WidgetHead, - WidgetTitle, -} from '@/components/widget'; -import { cn } from '@/utils/cn'; -import { - addMonths, - eachDayOfInterval, - endOfMonth, - format, - formatISO, - isSameMonth, - startOfMonth, - subMonths, -} from 'date-fns'; -import { ActivityIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; -import { useState } from 'react'; - -type Props = { - data: { count: number; date: string }[]; -}; - -const ProfileActivity = ({ data }: Props) => { - const [startDate, setStartDate] = useState(startOfMonth(new Date())); - const endDate = endOfMonth(startDate); - return ( - - - Activity -
- - - -
-
- -
-
-
- {format(subMonths(startDate, 3), 'MMMM yyyy')} -
-
- {eachDayOfInterval({ - start: startOfMonth(subMonths(startDate, 3)), - end: endOfMonth(subMonths(startDate, 3)), - }).map((date) => { - const hit = data.find((item) => - item.date.includes( - formatISO(date, { representation: 'date' }), - ), - ); - return ( -
- ); - })} -
-
-
-
- {format(subMonths(startDate, 2), 'MMMM yyyy')} -
-
- {eachDayOfInterval({ - start: startOfMonth(subMonths(startDate, 2)), - end: endOfMonth(subMonths(startDate, 2)), - }).map((date) => { - const hit = data.find((item) => - item.date.includes( - formatISO(date, { representation: 'date' }), - ), - ); - return ( -
- ); - })} -
-
-
-
- {format(subMonths(startDate, 1), 'MMMM yyyy')} -
-
- {eachDayOfInterval({ - start: startOfMonth(subMonths(startDate, 1)), - end: endOfMonth(subMonths(startDate, 1)), - }).map((date) => { - const hit = data.find((item) => - item.date.includes( - formatISO(date, { representation: 'date' }), - ), - ); - return ( -
- ); - })} -
-
-
-
{format(startDate, 'MMMM yyyy')}
-
- {eachDayOfInterval({ - start: startDate, - end: endDate, - }).map((date) => { - const hit = data.find((item) => - item.date.includes( - formatISO(date, { representation: 'date' }), - ), - ); - return ( -
- ); - })} -
-
-
- - - ); -}; - -export default ProfileActivity; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-charts.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-charts.tsx deleted file mode 100644 index b293d674..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-charts.tsx +++ /dev/null @@ -1,106 +0,0 @@ -'use client'; - -import { ReportChart } from '@/components/report-chart'; -import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; -import { memo } from 'react'; - -import type { IChartProps } from '@openpanel/validation'; - -type Props = { - profileId: string; - projectId: string; -}; - -const ProfileCharts = ({ profileId, projectId }: Props) => { - const pageViewsChart: IChartProps = { - projectId, - chartType: 'linear', - events: [ - { - segment: 'event', - filters: [ - { - id: 'profile_id', - name: 'profile_id', - operator: 'is', - value: [profileId], - }, - ], - id: 'A', - name: 'screen_view', - displayName: 'Events', - }, - ], - breakdowns: [ - { - id: 'path', - name: 'path', - }, - ], - lineType: 'monotone', - interval: 'day', - name: 'Events', - range: '30d', - previous: false, - metric: 'sum', - }; - - const eventsChart: IChartProps = { - projectId, - chartType: 'linear', - events: [ - { - segment: 'event', - filters: [ - { - id: 'profile_id', - name: 'profile_id', - operator: 'is', - value: [profileId], - }, - ], - id: 'A', - name: '*', - displayName: 'Events', - }, - ], - breakdowns: [ - { - id: 'name', - name: 'name', - }, - ], - lineType: 'monotone', - interval: 'day', - name: 'Events', - range: '30d', - previous: false, - metric: 'sum', - }; - - return ( - <> - - - Page views - - - - - - - - Events per day - - - - - - - ); -}; - -// No clue why I need to check for equality here -export default memo(ProfileCharts, (a, b) => { - return a.profileId === b.profileId && a.projectId === b.projectId; -}); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-events.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-events.tsx deleted file mode 100644 index 586527b8..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-events.tsx +++ /dev/null @@ -1,55 +0,0 @@ -'use client'; - -import { TableButtons } from '@/components/data-table'; -import { EventsTable } from '@/components/events/table'; -import { EventsTableColumns } from '@/components/events/table/events-table-columns'; -import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons'; -import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer'; -import { useEventQueryFilters } from '@/hooks/useEventQueryFilters'; -import { api } from '@/trpc/client'; -import { Loader2Icon } from 'lucide-react'; - -type Props = { - projectId: string; - profileId: string; -}; - -const Events = ({ projectId, profileId }: Props) => { - const [filters] = useEventQueryFilters(); - const query = api.event.events.useInfiniteQuery( - { - projectId, - filters, - profileId, - }, - { - getNextPageParam: (lastPage) => lastPage.meta.next, - keepPreviousData: true, - }, - ); - - return ( -
- - - - - {query.isRefetching && ( -
- -
- )} -
- -
- ); -}; - -export default Events; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/index.tsx deleted file mode 100644 index 36900b68..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import withSuspense from '@/hocs/with-suspense'; - -import type { IServiceProfile } from '@openpanel/db'; -import { getProfileMetrics } from '@openpanel/db'; - -import ProfileMetrics from './profile-metrics'; - -type Props = { - projectId: string; - profile: IServiceProfile; -}; - -const ProfileMetricsServer = async ({ projectId, profile }: Props) => { - const data = await getProfileMetrics(profile.id, projectId); - return ; -}; - -export default withSuspense(ProfileMetricsServer, () => null); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/profile-metrics.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/profile-metrics.tsx deleted file mode 100644 index 73ec37fd..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/[profileId]/profile-metrics/profile-metrics.tsx +++ /dev/null @@ -1,123 +0,0 @@ -'use client'; - -import { ListPropertiesIcon } from '@/components/events/list-properties-icon'; -import { useNumber } from '@/hooks/useNumerFormatter'; -import { cn } from '@/utils/cn'; -import { formatDateTime, utc } from '@/utils/date'; -import { formatDistanceToNow } from 'date-fns'; -import { parseAsStringEnum, useQueryState } from 'nuqs'; - -import type { IProfileMetrics, IServiceProfile } from '@openpanel/db'; - -type Props = { - data: IProfileMetrics; - profile: IServiceProfile; -}; - -function Card({ title, value }: { title: string; value: string }) { - return ( -
-
{title}
-
{value}
-
- ); -} - -function Info({ title, value }: { title: string; value: string }) { - return ( -
-
{title}
-
- {value - ? typeof value === 'string' - ? value - : JSON.stringify(value) - : '-'} -
-
- ); -} - -const ProfileMetrics = ({ data, profile }: Props) => { - const [tab, setTab] = useQueryState( - 'tab', - parseAsStringEnum(['profile', 'properties']).withDefault('profile'), - ); - const number = useNumber(); - return ( -
-
-
-
- -
- -
-
- {tab === 'profile' && ( - <> - - - - - - - - )} - {tab === 'properties' && - Object.entries(profile.properties) - .filter(([key, value]) => value !== undefined) - .map(([key, value]) => ( - - ))} -
-
- - - - - - -
-
- ); -}; - -export default ProfileMetrics; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/page.tsx deleted file mode 100644 index 1f1bb38b..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/page.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import { PageTabs, PageTabsLink } from '@/components/page-tabs'; -import { Padding } from '@/components/ui/padding'; -import { parseAsStringEnum } from 'nuqs/server'; - -import PowerUsers from './power-users'; -import Profiles from './profiles'; - -interface PageProps { - params: { - projectId: string; - }; - searchParams: Record; -} - -export default function Page({ - params: { projectId }, - searchParams, -}: PageProps) { - const tab = parseAsStringEnum(['profiles', 'power-users']) - .withDefault('profiles') - .parseServerSide(searchParams.tab); - - return ( - <> - -
- - - Profiles - - - Power users - - -
- {tab === 'profiles' && } - {tab === 'power-users' && } -
- - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/power-users.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/power-users.tsx deleted file mode 100644 index e4da65eb..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/power-users.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; - -import { ProfilesTable } from '@/components/profiles/table'; -import { api } from '@/trpc/client'; -import { parseAsInteger, useQueryState } from 'nuqs'; - -type Props = { - projectId: string; - profileId?: string; -}; - -const Events = ({ projectId }: Props) => { - const [cursor, setCursor] = useQueryState( - 'cursor', - parseAsInteger.withDefault(0), - ); - const query = api.profile.powerUsers.useQuery( - { - cursor, - projectId, - take: 50, - // filters, - }, - { - keepPreviousData: true, - }, - ); - - return ( -
- -
- ); -}; - -export default Events; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-last-seen/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-last-seen/index.tsx deleted file mode 100644 index 2f74747f..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profile-last-seen/index.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from '@/components/ui/tooltip'; -import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; -import { cn } from '@/utils/cn'; -import { escape } from 'sqlstring'; - -import { TABLE_NAMES, chQuery } from '@openpanel/db'; - -interface Props { - projectId: string; -} - -export default async function ProfileLastSeenServer({ projectId }: Props) { - interface Row { - days: number; - count: number; - } - // Days since last event from users - // group by days - const res = await chQuery( - `SELECT age('days',created_at, now()) as days, count(distinct profile_id) as count FROM ${TABLE_NAMES.events} where project_id = ${escape(projectId)} group by days order by days ASC LIMIT 51`, - ); - - const maxValue = Math.max(...res.map((x) => x.count)); - const minValue = Math.min(...res.map((x) => x.count)); - const calculateRatio = (currentValue: number) => - Math.max( - 0.1, - Math.min(1, (currentValue - minValue) / (maxValue - minValue)), - ); - - const renderItem = (item: Row) => ( -
- - -
- - - {item.count} profiles last seen{' '} - {item.days === 0 ? 'today' : `${item.days} days ago`} - - -
{item.days}
-
- ); - - return ( - - -
Last seen
-
- -
- {res.map(renderItem)} -
-
DAYS
-
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profiles.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profiles.tsx deleted file mode 100644 index e751ae26..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/profiles/profiles.tsx +++ /dev/null @@ -1,51 +0,0 @@ -'use client'; - -import { TableButtons } from '@/components/data-table'; -import { ProfilesTable } from '@/components/profiles/table'; -import { Input } from '@/components/ui/input'; -import { useDebounceValue } from '@/hooks/useDebounceValue'; -import { api } from '@/trpc/client'; -import { parseAsInteger, useQueryState } from 'nuqs'; - -type Props = { - projectId: string; - profileId?: string; -}; - -const Events = ({ projectId }: Props) => { - const [cursor, setCursor] = useQueryState( - 'cursor', - parseAsInteger.withDefault(0), - ); - const [search, setSearch] = useQueryState('search', { - defaultValue: '', - shallow: true, - }); - const debouncedSearch = useDebounceValue(search, 500); - const query = api.profile.list.useQuery( - { - cursor, - projectId, - take: 50, - search: debouncedSearch, - }, - { - keepPreviousData: true, - }, - ); - - return ( -
- - setSearch(e.target.value)} - placeholder="Search profiles" - /> - - -
- ); -}; - -export default Events; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/coordinates.ts b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/coordinates.ts deleted file mode 100644 index 7e430cae..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/coordinates.ts +++ /dev/null @@ -1,222 +0,0 @@ -export type Coordinate = { - lat: number; - long: number; -}; - -export function haversineDistance( - coord1: Coordinate, - coord2: Coordinate, -): number { - const R = 6371; // Earth's radius in kilometers - const lat1Rad = coord1.lat * (Math.PI / 180); - const lat2Rad = coord2.lat * (Math.PI / 180); - const deltaLatRad = (coord2.lat - coord1.lat) * (Math.PI / 180); - const deltaLonRad = (coord2.long - coord1.long) * (Math.PI / 180); - - const a = - Math.sin(deltaLatRad / 2) * Math.sin(deltaLatRad / 2) + - Math.cos(lat1Rad) * - Math.cos(lat2Rad) * - Math.sin(deltaLonRad / 2) * - Math.sin(deltaLonRad / 2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - - return R * c; // Distance in kilometers -} - -export function findFarthestPoints( - coordinates: Coordinate[], -): [Coordinate, Coordinate] { - if (coordinates.length < 2) { - throw new Error('At least two coordinates are required'); - } - - let maxDistance = 0; - let point1: Coordinate = coordinates[0]!; - let point2: Coordinate = coordinates[1]!; - - for (let i = 0; i < coordinates.length; i++) { - for (let j = i + 1; j < coordinates.length; j++) { - const distance = haversineDistance(coordinates[i]!, coordinates[j]!); - if (distance > maxDistance) { - maxDistance = distance; - point1 = coordinates[i]!; - point2 = coordinates[j]!; - } - } - } - - return [point1, point2]; -} - -export function getAverageCenter(coordinates: Coordinate[]): Coordinate { - if (coordinates.length === 0) { - return { long: 0, lat: 20 }; - } - - let sumLong = 0; - let sumLat = 0; - - for (const coord of coordinates) { - sumLong += coord.long; - sumLat += coord.lat; - } - - const avgLat = sumLat / coordinates.length; - const avgLong = sumLong / coordinates.length; - - return { long: avgLong, lat: avgLat }; -} - -function sortCoordinates(a: Coordinate, b: Coordinate): number { - return a.long === b.long ? a.lat - b.lat : a.long - b.long; -} - -function cross(o: Coordinate, a: Coordinate, b: Coordinate): number { - return ( - (a.long - o.long) * (b.lat - o.lat) - (a.lat - o.lat) * (b.long - o.long) - ); -} - -// convex hull -export function getOuterMarkers(coordinates: Coordinate[]): Coordinate[] { - const sorted = coordinates.sort(sortCoordinates); - - if (sorted.length <= 3) return sorted; - - const lower: Coordinate[] = []; - for (const coord of sorted) { - while ( - lower.length >= 2 && - cross(lower[lower.length - 2]!, lower[lower.length - 1]!, coord) <= 0 - ) { - lower.pop(); - } - lower.push(coord); - } - - const upper: Coordinate[] = []; - for (let i = coordinates.length - 1; i >= 0; i--) { - while ( - upper.length >= 2 && - cross(upper[upper.length - 2]!, upper[upper.length - 1]!, sorted[i]!) <= 0 - ) { - upper.pop(); - } - upper.push(sorted[i]!); - } - - upper.pop(); - lower.pop(); - return lower.concat(upper); -} - -export function calculateCentroid(polygon: Coordinate[]): Coordinate { - if (polygon.length < 3) { - throw new Error('At least three points are required to form a polygon.'); - } - - let area = 0; - let centroidLat = 0; - let centroidLong = 0; - - for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) { - const x0 = polygon[j]!.long; - const y0 = polygon[j]!.lat; - const x1 = polygon[i]!.long; - const y1 = polygon[i]!.lat; - const a = x0 * y1 - x1 * y0; - area += a; - centroidLong += (x0 + x1) * a; - centroidLat += (y0 + y1) * a; - } - - area = area / 2; - if (area === 0) { - // This should not happen for a proper convex hull - throw new Error('Area of the polygon is zero, check the coordinates.'); - } - - centroidLat /= 6 * area; - centroidLong /= 6 * area; - - return { lat: centroidLat, long: centroidLong }; -} - -export function calculateGeographicMidpoint( - coordinate: Coordinate[], -): Coordinate { - let minLat = Number.POSITIVE_INFINITY; - let maxLat = Number.NEGATIVE_INFINITY; - let minLong = Number.POSITIVE_INFINITY; - let maxLong = Number.NEGATIVE_INFINITY; - - for (const { lat, long } of coordinate) { - if (lat < minLat) minLat = lat; - if (lat > maxLat) maxLat = lat; - if (long < minLong) minLong = long; - if (long > maxLong) maxLong = long; - } - - // Handling the wrap around the international date line - let midLong: number; - if (maxLong > minLong) { - midLong = (maxLong + minLong) / 2; - } else { - // Adjust calculation when spanning the dateline - midLong = ((maxLong + 360 + minLong) / 2) % 360; - } - - const midLat = (maxLat + minLat) / 2; - - return { lat: midLat, long: midLong }; -} - -export function clusterCoordinates(coordinates: Coordinate[], radius = 25) { - const clusters: { - center: Coordinate; - count: number; - members: Coordinate[]; - }[] = []; - const visited = new Set(); - - coordinates.forEach((coord, idx) => { - if (!visited.has(idx)) { - const cluster = { - members: [coord], - center: { lat: coord.lat, long: coord.long }, - count: 0, - }; - - coordinates.forEach((otherCoord, otherIdx) => { - if ( - !visited.has(otherIdx) && - haversineDistance(coord, otherCoord) <= radius - ) { - cluster.members.push(otherCoord); - visited.add(otherIdx); - cluster.count++; - } - }); - - // Calculate geographic center for the cluster - cluster.center = cluster.members.reduce( - (center, cur) => { - return { - lat: center.lat + cur.lat / cluster.members.length, - long: center.long + cur.long / cluster.members.length, - }; - }, - { lat: 0, long: 0 }, - ); - - clusters.push(cluster); - } - }); - - return clusters.map((cluster) => ({ - center: cluster.center, - count: cluster.count, - members: cluster.members, - })); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/index.tsx deleted file mode 100644 index 59700e8a..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/index.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { subMinutes } from 'date-fns'; -import { escape } from 'sqlstring'; - -import { TABLE_NAMES, chQuery, formatClickhouseDate } from '@openpanel/db'; - -import type { Coordinate } from './coordinates'; -import Map from './map'; - -type Props = { - projectId: string; -}; -const RealtimeMap = async ({ projectId }: Props) => { - const res = await chQuery( - `SELECT DISTINCT city, longitude as long, latitude as lat FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} AND created_at >= '${formatClickhouseDate(subMinutes(new Date(), 30))}' ORDER BY created_at DESC`, - ); - - return ; -}; - -export default RealtimeMap; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/map.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/map.tsx deleted file mode 100644 index 793031da..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/map/map.tsx +++ /dev/null @@ -1,185 +0,0 @@ -'use client'; - -import { useFullscreen } from '@/components/fullscreen-toggle'; -import { Tooltiper } from '@/components/ui/tooltip'; -import { cn } from '@/utils/cn'; -import { bind } from 'bind-event-listener'; -import { useTheme } from 'next-themes'; -import { Fragment, useEffect, useRef, useState } from 'react'; -import { - ComposableMap, - Geographies, - Geography, - Marker, -} from 'react-simple-maps'; - -import type { Coordinate } from './coordinates'; -import { - calculateGeographicMidpoint, - clusterCoordinates, - getAverageCenter, - getOuterMarkers, -} from './coordinates'; -import { - CustomZoomableGroup, - GEO_MAP_URL, - determineZoom, - getBoundingBox, - useAnimatedState, -} from './map.helpers'; -import { calculateMarkerSize } from './markers'; - -type Props = { - markers: Coordinate[]; -}; -const Map = ({ markers }: Props) => { - const [isFullscreen] = useFullscreen(); - const showCenterMarker = false; - const ref = useRef(null); - const [size, setSize] = useState<{ width: number; height: number } | null>( - null, - ); - - // const { markers, toggle } = useActiveMarkers(_m); - const hull = getOuterMarkers(markers); - const center = - hull.length < 2 - ? getAverageCenter(markers) - : calculateGeographicMidpoint(hull); - const boundingBox = getBoundingBox(hull); - const [zoom] = useAnimatedState( - markers.length === 1 - ? 1 - : determineZoom(boundingBox, size ? size?.height / size?.width : 1), - ); - - const [long] = useAnimatedState(center.long); - const [lat] = useAnimatedState(center.lat); - - useEffect(() => { - return bind(window, { - type: 'resize', - listener() { - if (ref.current) { - setSize({ - width: ref.current.clientWidth, - height: ref.current.clientHeight, - }); - } - }, - }); - }, []); - - useEffect(() => { - if (ref.current) { - setSize({ - width: ref.current.clientWidth, - height: ref.current.clientHeight, - }); - } - }, []); - - const adjustSizeBasedOnZoom = (size: number) => { - const minMultiplier = 1; - const maxMultiplier = 7; - - // Linearly interpolate the multiplier based on the zoom level - const multiplier = - maxMultiplier - ((zoom - 1) * (maxMultiplier - minMultiplier)) / (20 - 1); - - return size * multiplier; - }; - - const theme = useTheme(); - - return ( -
- {size === null ? ( - <> - ) : ( - <> - - - - {({ geographies }) => - geographies.map((geo) => ( - - )) - } - - {showCenterMarker && ( - - - - )} - {clusterCoordinates(markers).map((marker) => { - const size = adjustSizeBasedOnZoom( - calculateMarkerSize(marker.count), - ); - const coordinates: [number, number] = [ - marker.center.long, - marker.center.lat, - ]; - return ( - - - - - - - - - - - ); - })} - - - - )} - {/* */} -
- ); -}; - -export default Map; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/page.tsx deleted file mode 100644 index 12e6485f..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/page.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { - Fullscreen, - FullscreenClose, - FullscreenOpen, -} from '@/components/fullscreen-toggle'; -import { ReportChart } from '@/components/report-chart'; -import { Suspense } from 'react'; - -import RealtimeMap from './map'; -import RealtimeLiveEventsServer from './realtime-live-events'; -import { RealtimeLiveHistogram } from './realtime-live-histogram'; -import RealtimeReloader from './realtime-reloader'; - -type Props = { - params: { - projectId: string; - }; -}; -export default function Page({ params: { projectId } }: Props) { - return ( - <> - - - - - - - -
- -
- -
-
- -
-
-
-
-
-
Pages
-
- -
-
-
-
Cities
-
- -
-
-
-
Referrers
-
- -
-
-
- - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-events/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-events/index.tsx deleted file mode 100644 index 6fc9523f..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-events/index.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { escape } from 'sqlstring'; - -import { TABLE_NAMES, getEvents } from '@openpanel/db'; - -import LiveEvents from './live-events'; - -type Props = { - projectId: string; - limit?: number; -}; -const RealtimeLiveEventsServer = async ({ projectId, limit = 30 }: Props) => { - const events = await getEvents( - `SELECT * FROM ${TABLE_NAMES.events} WHERE created_at > now() - INTERVAL 2 HOUR AND project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT ${limit}`, - { - profile: true, - }, - ); - return ; -}; - -export default RealtimeLiveEventsServer; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-events/live-events.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-events/live-events.tsx deleted file mode 100644 index 75cc06e6..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-live-events/live-events.tsx +++ /dev/null @@ -1,46 +0,0 @@ -'use client'; - -import { EventListItem } from '@/components/events/event-list-item'; -import useWS from '@/hooks/useWS'; -import { AnimatePresence, motion } from 'framer-motion'; -import { useState } from 'react'; - -import type { IServiceEvent, IServiceEventMinimal } from '@openpanel/db'; - -type Props = { - events: (IServiceEventMinimal | IServiceEvent)[]; - projectId: string; - limit: number; -}; - -const RealtimeLiveEvents = ({ events, projectId, limit }: Props) => { - const [state, setState] = useState(events ?? []); - useWS( - `/live/events/${projectId}`, - (event) => { - setState((p) => [event, ...p].slice(0, limit)); - }, - ); - return ( - -
- {state.map((event) => ( - -
- -
-
- ))} -
-
- ); -}; - -export default RealtimeLiveEvents; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-reloader.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-reloader.tsx deleted file mode 100644 index 688d9d9c..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/realtime/realtime-reloader.tsx +++ /dev/null @@ -1,35 +0,0 @@ -'use client'; - -import useWS from '@/hooks/useWS'; -import { useQueryClient } from '@tanstack/react-query'; -import { useRouter } from 'next/navigation'; - -type Props = { - projectId: string; -}; - -const RealtimeReloader = ({ projectId }: Props) => { - const client = useQueryClient(); - const router = useRouter(); - - useWS( - `/live/events/${projectId}`, - () => { - if (!document.hidden) { - client.refetchQueries({ - type: 'active', - }); - } - }, - { - debounce: { - maxWait: 60000, - delay: 60000, - }, - }, - ); - - return null; -}; - -export default RealtimeReloader; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/[reportId]/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/[reportId]/page.tsx deleted file mode 100644 index 4e9a7bc3..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/[reportId]/page.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout'; -import EditReportName from '@/components/report/edit-report-name'; -import { notFound } from 'next/navigation'; - -import { getReportById } from '@openpanel/db'; - -import ReportEditor from '../report-editor'; - -interface PageProps { - params: { - projectId: string; - reportId: string; - }; -} - -export default async function Page({ params: { reportId } }: PageProps) { - const report = await getReportById(reportId); - - if (!report) { - return notFound(); - } - - return ( - <> - } /> - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/page.tsx deleted file mode 100644 index 9ac0223e..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/reports/page.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout'; -import EditReportName from '@/components/report/edit-report-name'; - -import ReportEditor from './report-editor'; - -export default function Page() { - return ( - <> - } /> - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/chart.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/chart.tsx deleted file mode 100644 index ca1659bb..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/chart.tsx +++ /dev/null @@ -1,103 +0,0 @@ -'use client'; - -import { - useXAxisProps, - useYAxisProps, -} from '@/components/report-chart/common/axis'; -import { getChartColor } from '@/utils/theme'; -import { - Area, - AreaChart, - Tooltip as RechartTooltip, - ResponsiveContainer, - XAxis, - YAxis, -} from 'recharts'; - -type Props = { - data: { users: number; days: number }[]; -}; - -function Tooltip(props: any) { - const payload = props.payload?.[0]?.payload; - - if (!payload) { - return null; - } - return ( -
-
-
- Days since last seen -
-
{payload.days}
-
-
-
Active users
-
{payload.users}
-
-
- ); -} - -const Chart = ({ data }: Props) => { - const xAxisProps = useXAxisProps(); - const yAxisProps = useYAxisProps(); - return ( -
- - - - - - - - - - } /> - - - - - - -
- ); -}; - -export default Chart; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/index.tsx deleted file mode 100644 index b654d370..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/last-active-users/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Widget, WidgetHead } from '@/components/widget'; -import withLoadingWidget from '@/hocs/with-loading-widget'; - -import { getRetentionLastSeenSeries } from '@openpanel/db'; - -import Chart from './chart'; - -type Props = { - projectId: string; -}; - -const LastActiveUsersServer = async ({ projectId }: Props) => { - const res = await getRetentionLastSeenSeries({ projectId }); - - return ( - - - Last time in days a user was active - - - - ); -}; - -export default withLoadingWidget(LastActiveUsersServer); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/page.tsx deleted file mode 100644 index 39544215..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { Padding } from '@/components/ui/padding'; -import { AlertCircleIcon } from 'lucide-react'; - -import LastActiveUsersServer from './last-active-users'; -import RollingActiveUsers from './rolling-active-users'; -import UsersRetentionSeries from './users-retention-series'; -import WeeklyCohortsServer from './weekly-cohorts'; - -type Props = { - params: { - projectId: string; - }; -}; - -const Retention = ({ params: { projectId } }: Props) => { - return ( - -

Retention

-
- - - Experimental feature - -

- This page is an experimental feature and we'll be working - hard to make it even better. Stay tuned! -

-

- Please DM me on{' '} - - Discord - {' '} - or{' '} - - X/Twitter - {' '} - if you notice any issues. -

-
-
- - - - Retention info - - This information is only relevant if you supply a user ID to the - SDK! - - - - {/* */} - -
-
- ); -}; - -export default Retention; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/chart.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/chart.tsx deleted file mode 100644 index 6b01d806..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/chart.tsx +++ /dev/null @@ -1,146 +0,0 @@ -'use client'; - -import { - useXAxisProps, - useYAxisProps, -} from '@/components/report-chart/common/axis'; -import { getChartColor } from '@/utils/theme'; -import { - Area, - AreaChart, - Tooltip as RechartTooltip, - ResponsiveContainer, - XAxis, - YAxis, -} from 'recharts'; - -import type { IServiceRetentionRollingActiveUsers } from '@openpanel/db'; - -type Props = { - data: { - daily: IServiceRetentionRollingActiveUsers[]; - weekly: IServiceRetentionRollingActiveUsers[]; - monthly: IServiceRetentionRollingActiveUsers[]; - }; -}; - -function Tooltip(props: any) { - const payload = props.payload?.[2]?.payload; - - if (!payload) { - return null; - } - return ( -
-
{payload.date}
-
-
- Monthly active users -
-
{payload.mau}
-
-
-
Weekly active users
-
{payload.wau}
-
-
-
Daily active users
-
{payload.dau}
-
-
- ); -} - -const Chart = ({ data }: Props) => { - const rechartData = data.daily.map((d) => ({ - date: new Date(d.date).getTime(), - dau: d.users, - wau: data.weekly.find((w) => w.date === d.date)?.users, - mau: data.monthly.find((m) => m.date === d.date)?.users, - })); - const xAxisProps = useXAxisProps({ interval: 'day' }); - const yAxisProps = useYAxisProps(); - return ( -
- - - - - - - - - - - - - - - - - - } /> - - - - - - - - -
- ); -}; - -export default Chart; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/index.tsx deleted file mode 100644 index f0e1db08..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/rolling-active-users/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { Widget, WidgetHead } from '@/components/widget'; -import withLoadingWidget from '@/hocs/with-loading-widget'; - -import { getRollingActiveUsers } from '@openpanel/db'; - -import Chart from './chart'; - -type Props = { - projectId: string; -}; - -const RollingActiveUsersServer = async ({ projectId }: Props) => { - const series = await Promise.all([ - await getRollingActiveUsers({ projectId, days: 1 }), - await getRollingActiveUsers({ projectId, days: 7 }), - await getRollingActiveUsers({ projectId, days: 30 }), - ]); - - return ( - - - Rolling active users - - - - ); -}; - -export default withLoadingWidget(RollingActiveUsersServer); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/chart.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/chart.tsx deleted file mode 100644 index 8ecd28e6..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/chart.tsx +++ /dev/null @@ -1,117 +0,0 @@ -'use client'; - -import { - useXAxisProps, - useYAxisProps, -} from '@/components/report-chart/common/axis'; -import { useFormatDateInterval } from '@/hooks/useFormatDateInterval'; -import { formatDate } from '@/utils/date'; -import { getChartColor } from '@/utils/theme'; -import { - Area, - AreaChart, - Tooltip as RechartTooltip, - ResponsiveContainer, - XAxis, - YAxis, -} from 'recharts'; - -import { round } from '@openpanel/common'; - -type Props = { - data: { - date: string; - active_users: number; - retained_users: number; - retention: number; - }[]; -}; - -function Tooltip({ payload }: any) { - const { date, active_users, retained_users, retention } = - payload?.[0]?.payload || {}; - const formatDate = useFormatDateInterval('day'); - if (!date) { - return null; - } - return ( -
-
-
{formatDate(new Date(date))}
-
-
-
Active Users
-
{active_users}
-
-
-
Retained Users
-
{retained_users}
-
-
-
Retention
-
{round(retention, 2)}%
-
-
- ); -} - -const Chart = ({ data }: Props) => { - const xAxisProps = useXAxisProps(); - const yAxisProps = useYAxisProps(); - return ( -
- - - - - - - - - - } /> - - - formatDate(new Date(m))} - allowDuplicatedCategory={false} - label={{ - value: 'DATE', - position: 'insideBottom', - offset: 0, - fontSize: 10, - }} - /> - - - -
- ); -}; - -export default Chart; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/index.tsx deleted file mode 100644 index 9a87469e..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/users-retention-series/index.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { Widget, WidgetHead } from '@/components/widget'; -import withLoadingWidget from '@/hocs/with-loading-widget'; - -import { getRetentionSeries } from '@openpanel/db'; - -import Chart from './chart'; - -type Props = { - projectId: string; -}; - -const UsersRetentionSeries = async ({ projectId }: Props) => { - const res = await getRetentionSeries({ projectId }); - - return ( - - - Stickyness / Retention (%) - - - - ); -}; - -export default withLoadingWidget(UsersRetentionSeries); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/weekly-cohorts/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/weekly-cohorts/index.tsx deleted file mode 100644 index db4c7a54..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/retention/weekly-cohorts/index.tsx +++ /dev/null @@ -1,132 +0,0 @@ -import { Widget, WidgetHead } from '@/components/widget'; -import { WidgetTableHead } from '@/components/widget-table'; -import withLoadingWidget from '@/hocs/with-loading-widget'; -import { cn } from '@/utils/cn'; - -import { getRetentionCohortTable } from '@openpanel/db'; - -type Props = { - projectId: string; -}; - -const Cell = ({ value, ratio }: { value: number; ratio: number }) => { - return ( - -
-
{value}
- - ); -}; - -const WeeklyCohortsServer = async ({ projectId }: Props) => { - const res = await getRetentionCohortTable({ projectId }); - - const minValue = 0; - const maxValue = Math.max( - ...res.flatMap((row) => [ - row.period_0, - row.period_1, - row.period_2, - row.period_3, - row.period_4, - row.period_5, - row.period_6, - row.period_7, - row.period_8, - row.period_9, - ]), - ); - - const calculateRatio = (currentValue: number) => - currentValue === 0 - ? 0 - : Math.max( - 0.1, - Math.min(1, (currentValue - minValue) / (maxValue - minValue)), - ); - - return ( - - - Weekly Cohorts - -
-
- - - - - - - - - - - - - - - - - - {res.map((row) => ( - - - - - - - - - - - - - - ))} - -
Week0123456789
- {row.first_seen} -
-
-
-
- ); -}; - -export default withLoadingWidget(WeeklyCohortsServer); diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/integrations/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/integrations/page.tsx deleted file mode 100644 index 53851847..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/integrations/page.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { ActiveIntegrations } from '@/components/integrations/active-integrations'; -import { AllIntegrations } from '@/components/integrations/all-integrations'; -import { PageTabs, PageTabsLink } from '@/components/page-tabs'; -import { Padding } from '@/components/ui/padding'; -import { parseAsStringEnum } from 'nuqs/server'; - -interface PageProps { - params: { - projectId: string; - }; - searchParams: { - tab: string; - }; -} - -export default function Page({ - params: { projectId }, - searchParams, -}: PageProps) { - const tab = parseAsStringEnum(['installed', 'available']) - .withDefault('available') - .parseServerSide(searchParams.tab); - return ( - -
-

Your integrations

- -
- -
-

Available integrations

- -
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/loading.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/loading.tsx deleted file mode 100644 index 9cf2cf50..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/loading.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import FullPageLoadingState from '@/components/full-page-loading-state'; - -export default FullPageLoadingState; 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 deleted file mode 100644 index 93d20930..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/notifications/page.tsx +++ /dev/null @@ -1,40 +0,0 @@ -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/server'; - -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/(app)/[organizationSlug]/[projectId]/settings/organization/invites/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/invites/index.tsx deleted file mode 100644 index 9484d973..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/invites/index.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { TableButtons } from '@/components/data-table'; -import { InvitesTable } from '@/components/settings/invites'; - -import { getInvites, getProjectsByOrganizationId } from '@openpanel/db'; - -import CreateInvite from './create-invite'; - -interface Props { - organizationId: string; -} - -const InvitesServer = async ({ organizationId }: Props) => { - const [invites, projects] = await Promise.all([ - getInvites(organizationId), - getProjectsByOrganizationId(organizationId), - ]); - - return ( -
- - - - -
- ); -}; - -export default InvitesServer; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/loading.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/loading.tsx deleted file mode 100644 index 9cf2cf50..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/loading.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import FullPageLoadingState from '@/components/full-page-loading-state'; - -export default FullPageLoadingState; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/members/index.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/members/index.tsx deleted file mode 100644 index 971809b0..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/members/index.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { MembersTable } from '@/components/settings/members'; - -import { getMembers, getProjectsByOrganizationId } from '@openpanel/db'; - -interface Props { - organizationId: string; -} - -const MembersServer = async ({ organizationId }: Props) => { - const [members, projects] = await Promise.all([ - getMembers(organizationId), - getProjectsByOrganizationId(organizationId), - ]); - - return ; -}; - -export default MembersServer; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/billing.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/billing.tsx deleted file mode 100644 index ff2ba82c..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/billing.tsx +++ /dev/null @@ -1,302 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogTitle, -} from '@/components/ui/dialog'; -import { Switch } from '@/components/ui/switch'; -import { Tooltiper } from '@/components/ui/tooltip'; -import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; -import { WidgetTable } from '@/components/widget-table'; -import { useAppParams } from '@/hooks/useAppParams'; -import useWS from '@/hooks/useWS'; -import { showConfirm } from '@/modals'; -import { api } from '@/trpc/client'; -import type { IServiceOrganization } from '@openpanel/db'; -import { useOpenPanel } from '@openpanel/nextjs'; -import type { IPolarPrice } from '@openpanel/payments'; -import { Loader2Icon } from 'lucide-react'; -import { useRouter } from 'next/navigation'; -import { useQueryState } from 'nuqs'; -import { useEffect, useMemo, useState } from 'react'; -import { toast } from 'sonner'; - -type Props = { - organization: IServiceOrganization; -}; - -export default function Billing({ organization }: Props) { - const router = useRouter(); - const { projectId } = useAppParams(); - const op = useOpenPanel(); - const [customerSessionToken, setCustomerSessionToken] = useQueryState( - 'customer_session_token', - ); - const productsQuery = api.subscription.products.useQuery({ - organizationId: organization.id, - }); - - useWS(`/live/organization/${organization.id}`, (event) => { - router.refresh(); - }); - - const [recurringInterval, setRecurringInterval] = useState<'year' | 'month'>( - (organization.subscriptionInterval as 'year' | 'month') || 'month', - ); - - const products = useMemo(() => { - return (productsQuery.data || []) - .filter((product) => product.recurringInterval === recurringInterval) - .filter((product) => product.prices.some((p) => p.amountType !== 'free')); - }, [productsQuery.data, recurringInterval]); - - useEffect(() => { - if (organization.subscriptionInterval) { - setRecurringInterval( - organization.subscriptionInterval as 'year' | 'month', - ); - } - }, [organization.subscriptionInterval]); - - useEffect(() => { - if (customerSessionToken) { - op.track('subscription_created'); - } - }, [customerSessionToken]); - - function renderBillingTable() { - if (productsQuery.isLoading) { - return ( -
- -
- ); - } - if (productsQuery.isError) { - return ( -
- Issues loading all tiers -
- ); - } - return ( - item.id} - columns={[ - { - name: 'Tier', - className: 'text-left', - width: 'auto', - render(item) { - return
{item.name}
; - }, - }, - { - name: 'Price', - width: 'auto', - render(item) { - const price = item.prices[0]; - if (!price) { - return null; - } - - if (price.amountType === 'free') { - return null; - // return ( - //
- //
- // Free - // - //
- //
- // ); - } - - if (price.amountType !== 'fixed') { - return null; - } - - return ( -
-
- - {new Intl.NumberFormat('en-US', { - style: 'currency', - currency: price.priceCurrency, - minimumFractionDigits: 0, - maximumFractionDigits: 1, - }).format(price.priceAmount / 100)} - {' / '} - {recurringInterval === 'year' ? 'year' : 'month'} - - -
-
- ); - }, - }, - ]} - /> - ); - } - - return ( - <> - - - Billing -
- - {recurringInterval === 'year' - ? 'Yearly (2 months free)' - : 'Monthly'} - - - setRecurringInterval(checked ? 'year' : 'month') - } - /> -
-
- -
- {renderBillingTable()} -
-

Do you need higher limits?

-

- Reach out to{' '} - - hello@openpanel.dev - {' '} - and we'll help you out. -

-
-
-
-
- { - setCustomerSessionToken(null); - if (!open) { - router.refresh(); - } - }} - > - - Subscription created - - We have registered your subscription. It'll be activated within a - couple of seconds. - - - - - - - - - - ); -} - -function CheckoutButton({ - price, - organization, - projectId, - disabled, -}: { - price: IPolarPrice; - organization: IServiceOrganization; - projectId: string; - disabled?: string | null; -}) { - const op = useOpenPanel(); - const isCurrentPrice = organization.subscriptionPriceId === price.id; - const checkout = api.subscription.checkout.useMutation({ - onSuccess(data) { - if (data?.url) { - window.location.href = data.url; - } else { - toast.success('Subscription updated', { - description: 'It might take a few seconds to update', - }); - } - }, - }); - - const isCanceled = - organization.subscriptionStatus === 'active' && - isCurrentPrice && - organization.subscriptionCanceledAt; - const isActive = - organization.subscriptionStatus === 'active' && isCurrentPrice; - - return ( - - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/usage.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/usage.tsx deleted file mode 100644 index 736cb0d2..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/organization/usage.tsx +++ /dev/null @@ -1,288 +0,0 @@ -'use client'; - -import { - useXAxisProps, - useYAxisProps, -} from '@/components/report-chart/common/axis'; -import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; -import { useNumber } from '@/hooks/useNumerFormatter'; -import { api } from '@/trpc/client'; -import { formatDate } from '@/utils/date'; -import { getChartColor } from '@/utils/theme'; -import { sum } from '@openpanel/common'; -import type { IServiceOrganization } from '@openpanel/db'; -import { Loader2Icon } from 'lucide-react'; -import { - Bar, - BarChart, - CartesianGrid, - Tooltip as RechartTooltip, - ReferenceLine, - ResponsiveContainer, - XAxis, - YAxis, -} from 'recharts'; - -type Props = { - organization: IServiceOrganization; -}; - -function Card({ title, value }: { title: string; value: string }) { - return ( -
-
{title}
-
{value}
-
- ); -} - -export default function Usage({ organization }: Props) { - const number = useNumber(); - const xAxisProps = useXAxisProps({ interval: 'day' }); - const yAxisProps = useYAxisProps({}); - const usageQuery = api.subscription.usage.useQuery({ - organizationId: organization.id, - }); - - const wrapper = (node: React.ReactNode) => ( - - - Usage - - {node} - - ); - - if (usageQuery.isLoading) { - return wrapper( -
- -
, - ); - } - if (usageQuery.isError) { - return wrapper( -
- Issues loading usage data -
, - ); - } - - const subscriptionPeriodEventsLimit = organization.hasSubscription - ? organization.subscriptionPeriodEventsLimit - : 0; - const subscriptionPeriodEventsCount = organization.hasSubscription - ? organization.subscriptionPeriodEventsCount - : 0; - - const domain = [ - 0, - Math.max( - subscriptionPeriodEventsLimit, - subscriptionPeriodEventsCount, - ...usageQuery.data.map((item) => item.count), - ), - ] as [number, number]; - - domain[1] += domain[1] * 0.05; - - return wrapper( - <> -
- {organization.hasSubscription ? ( - <> - - - - - - ) : ( - <> -
- -
-
- item.count)), - )} - /> -
- - )} -
-
- - ({ - date: new Date(item.day).getTime(), - count: item.count, - limit: subscriptionPeriodEventsLimit, - total: subscriptionPeriodEventsCount, - }))} - barSize={8} - > - - - - - - - } - cursor={{ - stroke: 'hsl(var(--def-400))', - fill: 'hsl(var(--def-200))', - }} - /> - {organization.hasSubscription && ( - <> - - 1000 - ? 'insideTop' - : 'insideBottom', - fontSize: 12, - }} - /> - - )} - - - - - - -
- , - ); -} - -function Tooltip(props: any) { - const number = useNumber(); - const payload = props.payload?.[0]?.payload; - - if (!payload) { - return null; - } - return ( -
-
- {formatDate(payload.date)} -
- {payload.limit !== 0 && ( -
-
-
-
Your tier limit
-
- {number.format(payload.limit)} -
-
-
- )} - {payload.total !== 0 && ( -
-
-
-
- Total events count -
-
- {number.format(payload.total)} -
-
-
- )} -
-
-
-
Events this day
-
- {number.format(payload.count)} -
-
-
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/page.tsx deleted file mode 100644 index b09e763c..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/organization/page.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { FullPageEmptyState } from '@/components/full-page-empty-state'; -import { PageTabs, PageTabsLink } from '@/components/page-tabs'; -import { Padding } from '@/components/ui/padding'; -import { ShieldAlertIcon } from 'lucide-react'; -import { notFound } from 'next/navigation'; -import { parseAsStringEnum } from 'nuqs/server'; - -import { auth } from '@openpanel/auth/nextjs'; -import { db } from '@openpanel/db'; - -import InvitesServer from './invites'; -import MembersServer from './members'; -import Billing from './organization/billing'; -import { BillingFaq } from './organization/billing-faq'; -import CurrentSubscription from './organization/current-subscription'; -import Organization from './organization/organization'; -import Usage from './organization/usage'; - -interface PageProps { - params: { - organizationSlug: string; - }; - searchParams: Record; -} - -export default async function Page({ - params: { organizationSlug: organizationId }, - searchParams, -}: PageProps) { - const isBillingEnabled = process.env.NEXT_PUBLIC_SELF_HOSTED !== 'true'; - const tab = parseAsStringEnum(['org', 'billing', 'members', 'invites']) - .withDefault('org') - .parseServerSide(searchParams.tab); - const session = await auth(); - const organization = await db.organization.findUnique({ - where: { - id: organizationId, - members: { - some: { - userId: session.userId, - }, - }, - }, - include: { - members: { - select: { - role: true, - userId: true, - }, - }, - }, - }); - - if (!organization) { - return notFound(); - } - - const member = organization.members.find( - (member) => member.userId === session.userId, - ); - - const hasAccess = member?.role === 'org:admin'; - - if (!hasAccess) { - return ( - - You do not have access to this page. You need to be an admin of this - organization to access this page. - - ); - } - - return ( - - - - Organization - - {isBillingEnabled && ( - - Billing - - )} - - Members - - - Invites - - - - {tab === 'org' && } - {tab === 'billing' && isBillingEnabled && ( -
-
- - - -
- -
- )} - {tab === 'members' && } - {tab === 'invites' && } -
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/page.tsx deleted file mode 100644 index 22b9c8b1..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/page.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from './organization/page'; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/edit-profile.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/edit-profile.tsx deleted file mode 100644 index fe828887..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/edit-profile.tsx +++ /dev/null @@ -1,89 +0,0 @@ -'use client'; - -import { InputWithLabel } from '@/components/forms/input-with-label'; -import { Button } from '@/components/ui/button'; -import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; -import { api, handleError } from '@/trpc/client'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useRouter } from 'next/navigation'; -import { useForm } from 'react-hook-form'; -import { toast } from 'sonner'; -import { z } from 'zod'; - -import type { getUserById } from '@openpanel/db'; - -const validator = z.object({ - firstName: z.string().min(2), - lastName: z.string().min(2), - email: z.string().email(), -}); - -type IForm = z.infer; -interface EditProfileProps { - profile: Awaited>; -} -export default function EditProfile({ profile }: EditProfileProps) { - const router = useRouter(); - - const { register, handleSubmit, reset, formState } = useForm({ - resolver: zodResolver(validator), - defaultValues: { - firstName: profile.firstName ?? '', - lastName: profile.lastName ?? '', - email: profile.email ?? '', - }, - }); - - const mutation = api.user.update.useMutation({ - onSuccess(res) { - toast('Profile updated', { - description: 'Your profile has been updated.', - }); - reset({ - firstName: res.firstName ?? '', - lastName: res.lastName ?? '', - email: res.email, - }); - router.refresh(); - }, - onError: handleError, - }); - - return ( -
{ - mutation.mutate(values); - })} - > - - - Your profile - - - - - - - - -
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/loading.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/loading.tsx deleted file mode 100644 index 9cf2cf50..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/loading.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import FullPageLoadingState from '@/components/full-page-loading-state'; - -export default FullPageLoadingState; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/logout.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/logout.tsx deleted file mode 100644 index 71578074..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/logout.tsx +++ /dev/null @@ -1,18 +0,0 @@ -'use client'; - -import SignOutButton from '@/components/sign-out-button'; -import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; - -export function Logout() { - return ( - - - Sad part - - -

Sometimes you need to go. See you next time

- -
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/page.tsx deleted file mode 100644 index ee5865e3..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/profile/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { Padding } from '@/components/ui/padding'; -import { auth } from '@openpanel/auth/nextjs'; -import { getUserById } from '@openpanel/db'; - -import EditProfile from './edit-profile'; - -export default async function Page() { - const { userId } = await auth(); - const profile = await getUserById(userId!); - - return ( - -

Profile

- -
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/loading.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/loading.tsx deleted file mode 100644 index 9cf2cf50..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/loading.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import FullPageLoadingState from '@/components/full-page-loading-state'; - -export default FullPageLoadingState; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/page.tsx deleted file mode 100644 index 37699219..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/page.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Padding } from '@/components/ui/padding'; - -import { - db, - getClientsByOrganizationId, - getProjectWithClients, - getProjectsByOrganizationId, -} from '@openpanel/db'; - -import { notFound } from 'next/navigation'; -import DeleteProject from './delete-project'; -import EditProjectDetails from './edit-project-details'; -import EditProjectFilters from './edit-project-filters'; -import ProjectClients from './project-clients'; - -interface PageProps { - params: { - projectId: string; - }; -} - -export default async function Page({ params: { projectId } }: PageProps) { - const project = await getProjectWithClients(projectId); - - if (!project) { - notFound(); - } - - return ( - -
-
-

{project.name}

-
- - - - -
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/project-clients.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/project-clients.tsx deleted file mode 100644 index e202ece9..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/projects/project-clients.tsx +++ /dev/null @@ -1,45 +0,0 @@ -'use client'; - -import { ClientsTable } from '@/components/clients/table'; -import { Button } from '@/components/ui/button'; -import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; -import { pushModal } from '@/modals'; -import type { - IServiceClientWithProject, - IServiceProjectWithClients, -} from '@openpanel/db'; -import { PlusIcon } from 'lucide-react'; -import { omit } from 'ramda'; - -type Props = { project: IServiceProjectWithClients }; - -export default function ProjectClients({ project }: Props) { - return ( - - - Clients - - - - ({ - ...item, - project: omit(['clients'], item), - })) as unknown as IServiceClientWithProject[], - isFetching: false, - isLoading: false, - }} - /> - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/list-references.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/list-references.tsx deleted file mode 100644 index d493af55..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/list-references.tsx +++ /dev/null @@ -1,29 +0,0 @@ -'use client'; - -import { DataTable } from '@/components/data-table'; -import { columns } from '@/components/references/table'; -import { Button } from '@/components/ui/button'; -import { Padding } from '@/components/ui/padding'; -import { pushModal } from '@/modals'; -import { PlusIcon } from 'lucide-react'; - -import type { IServiceReference } from '@openpanel/db'; - -interface ListProjectsProps { - data: IServiceReference[]; -} - -export default function ListReferences({ data }: ListProjectsProps) { - return ( - -
-

References

- -
- -
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/loading.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/loading.tsx deleted file mode 100644 index 9cf2cf50..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/loading.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import FullPageLoadingState from '@/components/full-page-loading-state'; - -export default FullPageLoadingState; diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/page.tsx deleted file mode 100644 index e32df231..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/settings/references/page.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import { getReferences } from '@openpanel/db'; - -import ListReferences from './list-references'; - -interface PageProps { - params: { - projectId: string; - }; -} - -export default async function Page({ params: { projectId } }: PageProps) { - const references = await getReferences({ - where: { - projectId, - }, - take: 50, - skip: 0, - }); - - return ( - <> - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-free-plan.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-free-plan.tsx deleted file mode 100644 index 133cdfe3..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-free-plan.tsx +++ /dev/null @@ -1,61 +0,0 @@ -'use client'; - -import { differenceInHours } from 'date-fns'; -import { useEffect, useState } from 'react'; - -import { ProjectLink } from '@/components/links'; -import { Dialog, DialogContent } from '@/components/ui/dialog'; -import { ModalHeader } from '@/modals/Modal/Container'; -import type { IServiceOrganization } from '@openpanel/db'; -import { useOpenPanel } from '@openpanel/nextjs'; -import { FREE_PRODUCT_IDS } from '@openpanel/payments'; -import Billing from './settings/organization/organization/billing'; - -interface SideEffectsProps { - organization: IServiceOrganization; -} - -export default function SideEffectsFreePlan({ - organization, -}: SideEffectsProps) { - const op = useOpenPanel(); - const willEndInHours = organization.subscriptionEndsAt - ? differenceInHours(organization.subscriptionEndsAt, new Date()) - : null; - const [isFreePlan, setIsFreePlan] = useState( - !!organization.subscriptionProductId && - FREE_PRODUCT_IDS.includes(organization.subscriptionProductId), - ); - - useEffect(() => { - if (isFreePlan) { - op.track('free_plan_removed'); - } - }, []); - - return ( - - - setIsFreePlan(false)} - title={'Free plan has been removed'} - text={ - <> - Please upgrade your plan to continue using OpenPanel. Select a - tier which is appropriate for your needs or{' '} - - manage billing - - - } - /> -
- -
-
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-timezone.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-timezone.tsx deleted file mode 100644 index b5b46ebf..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-timezone.tsx +++ /dev/null @@ -1,91 +0,0 @@ -'use client'; - -import { useState } from 'react'; - -import { Button } from '@/components/ui/button'; -import { Combobox } from '@/components/ui/combobox'; -import { Dialog, DialogContent, DialogFooter } from '@/components/ui/dialog'; -import { ModalHeader } from '@/modals/Modal/Container'; -import { api, handleError } from '@/trpc/client'; -import { TIMEZONES } from '@openpanel/common'; -import type { IServiceOrganization } from '@openpanel/db'; -import { toast } from 'sonner'; - -interface SideEffectsProps { - organization: IServiceOrganization; -} - -export default function SideEffectsTimezone({ - organization, -}: SideEffectsProps) { - const [isMissingTimezone, setIsMissingTimezone] = useState( - !organization.timezone, - ); - const defaultTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const [timezone, setTimezone] = useState( - TIMEZONES.includes(defaultTimezone) ? defaultTimezone : '', - ); - - const mutation = api.organization.update.useMutation({ - onSuccess(res) { - toast('Timezone updated', { - description: 'Your timezone has been updated.', - }); - window.location.reload(); - }, - onError: handleError, - }); - - return ( - - { - e.preventDefault(); - }} - onInteractOutside={(e) => { - e.preventDefault(); - }} - > - - We have introduced new features that requires your timezone. - Please select the timezone you want to use for your organization. - - } - /> - ({ - value: item, - label: item, - }))} - value={timezone} - onChange={setTimezone} - placeholder="Select a timezone" - searchable - size="lg" - className="w-full px-4" - /> - - - - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-trial.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-trial.tsx deleted file mode 100644 index e557ae2a..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects-trial.tsx +++ /dev/null @@ -1,67 +0,0 @@ -'use client'; - -import { differenceInHours } from 'date-fns'; -import { useEffect, useState } from 'react'; - -import { ProjectLink } from '@/components/links'; -import { Dialog, DialogContent } from '@/components/ui/dialog'; -import { ModalHeader } from '@/modals/Modal/Container'; -import type { IServiceOrganization } from '@openpanel/db'; -import { useOpenPanel } from '@openpanel/nextjs'; -import Billing from './settings/organization/organization/billing'; - -interface SideEffectsProps { - organization: IServiceOrganization; -} - -export default function SideEffectsTrial({ organization }: SideEffectsProps) { - const op = useOpenPanel(); - const willEndInHours = organization.subscriptionEndsAt - ? differenceInHours(organization.subscriptionEndsAt, new Date()) - : null; - - const [isTrialDialogOpen, setIsTrialDialogOpen] = useState( - willEndInHours !== null && - organization.subscriptionStatus === 'trialing' && - organization.subscriptionEndsAt !== null && - willEndInHours <= 48, - ); - - useEffect(() => { - if (isTrialDialogOpen) { - op.track('trial_expires_soon'); - } - }, [isTrialDialogOpen]); - - return ( - <> - - - setIsTrialDialogOpen(false)} - title={ - willEndInHours !== null && willEndInHours > 0 - ? `Your trial is ending in ${willEndInHours} hours` - : 'Your trial has ended' - } - text={ - <> - Please upgrade your plan to continue using OpenPanel. Select a - tier which is appropriate for your needs or{' '} - - manage billing - - - } - /> -
- -
-
-
- - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects.tsx deleted file mode 100644 index 07267c0d..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/[projectId]/side-effects.tsx +++ /dev/null @@ -1,41 +0,0 @@ -'use client'; - -import { differenceInHours } from 'date-fns'; -import { useEffect, useState } from 'react'; - -import { ProjectLink } from '@/components/links'; -import { Combobox } from '@/components/ui/combobox'; -import { Dialog, DialogContent } from '@/components/ui/dialog'; -import { ModalHeader } from '@/modals/Modal/Container'; -import type { IServiceOrganization } from '@openpanel/db'; -import { useOpenPanel } from '@openpanel/nextjs'; -import { FREE_PRODUCT_IDS } from '@openpanel/payments'; -import Billing from './settings/organization/organization/billing'; -import SideEffectsFreePlan from './side-effects-free-plan'; -import SideEffectsTimezone from './side-effects-timezone'; -import SideEffectsTrial from './side-effects-trial'; - -interface SideEffectsProps { - organization: IServiceOrganization; -} - -export default function SideEffects({ organization }: SideEffectsProps) { - const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - }, []); - - // Avoids hydration errors - if (!mounted) { - return null; - } - - return ( - <> - - - - - ); -} diff --git a/apps/dashboard/src/app/(app)/[organizationSlug]/page.tsx b/apps/dashboard/src/app/(app)/[organizationSlug]/page.tsx deleted file mode 100644 index 39946c91..00000000 --- a/apps/dashboard/src/app/(app)/[organizationSlug]/page.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { FullPageEmptyState } from '@/components/full-page-empty-state'; -import FullWidthNavbar from '@/components/full-width-navbar'; -import ProjectCard from '@/components/projects/project-card'; -import { redirect } from 'next/navigation'; - -import SettingsToggle from '@/components/settings-toggle'; -import { auth } from '@openpanel/auth/nextjs'; -import { getOrganizations, getProjects } from '@openpanel/db'; -import LayoutProjectSelector from './[projectId]/layout-project-selector'; - -interface PageProps { - params: { - organizationSlug: string; - }; -} - -export default async function Page({ - params: { organizationSlug: organizationId }, -}: PageProps) { - const { userId } = await auth(); - const [organizations, projects] = await Promise.all([ - getOrganizations(userId), - getProjects({ organizationId, userId }), - ]); - - const organization = organizations.find((org) => org.id === organizationId); - - if (!organization) { - return ( - - The organization you were looking for could not be found. - - ); - } - - if (projects.length === 0) { - return redirect('/onboarding/project'); - } - - if (projects.length === 1 && projects[0]) { - return redirect(`/${organizationId}/${projects[0].id}`); - } - - return ( -
- -
- - -
-
-
-
- {projects.map((item) => ( - - ))} -
-
-
- ); -} diff --git a/apps/dashboard/src/app/(app)/page.tsx b/apps/dashboard/src/app/(app)/page.tsx deleted file mode 100644 index d3228445..00000000 --- a/apps/dashboard/src/app/(app)/page.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { redirect } from 'next/navigation'; - -import { auth } from '@openpanel/auth/nextjs'; -import { getOrganizations } from '@openpanel/db'; - -export default async function Page() { - const { userId } = await auth(); - const organizations = await getOrganizations(userId); - - if (organizations.length > 0) { - return redirect(`/${organizations[0]?.id}`); - } - - return redirect('/onboarding/project'); -} diff --git a/apps/dashboard/src/app/(auth)/layout.tsx b/apps/dashboard/src/app/(auth)/layout.tsx deleted file mode 100644 index c1c2128a..00000000 --- a/apps/dashboard/src/app/(auth)/layout.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import LiveEventsServer from './live-events'; - -type Props = { - children: React.ReactNode; -}; - -const Page = ({ children }: Props) => { - return ( - <> -
-
-
- -
-
{children}
-
-
- - ); -}; - -export default Page; diff --git a/apps/dashboard/src/app/(auth)/live-events/index.tsx b/apps/dashboard/src/app/(auth)/live-events/index.tsx deleted file mode 100644 index e056be64..00000000 --- a/apps/dashboard/src/app/(auth)/live-events/index.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import LiveEvents from './live-events'; - -const LiveEventsServer = () => { - return ; -}; - -export default LiveEventsServer; diff --git a/apps/dashboard/src/app/(auth)/login/page.tsx b/apps/dashboard/src/app/(auth)/login/page.tsx deleted file mode 100644 index 9cbc906e..00000000 --- a/apps/dashboard/src/app/(auth)/login/page.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import { Or } from '@/components/auth/or'; -import { SignInEmailForm } from '@/components/auth/sign-in-email-form'; -import { SignInGithub } from '@/components/auth/sign-in-github'; -import { SignInGoogle } from '@/components/auth/sign-in-google'; -import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; -import { LinkButton } from '@/components/ui/button'; -import { auth } from '@openpanel/auth/nextjs'; -import { AlertCircle } from 'lucide-react'; -import { redirect } from 'next/navigation'; - -export default async function Page({ - searchParams, -}: { - searchParams: { error?: string; correlationId?: string }; -}) { - const session = await auth(); - const error = searchParams.error; - const correlationId = searchParams.correlationId; - - if (session.userId) { - return redirect('/'); - } - - return ( -
-
- {error && ( - - - Error - -

{error}

- {correlationId && ( - <> -

Correlation ID: {correlationId}

-

- Contact us if you have any issues.{' '} - - hello[at]openpanel.dev - -

- - )} -
-
- )} -
- - -
- -
- -
- - No account? Sign up today - -
-
- ); -} diff --git a/apps/dashboard/src/app/(auth)/reset-password/page.tsx b/apps/dashboard/src/app/(auth)/reset-password/page.tsx deleted file mode 100644 index 84f47245..00000000 --- a/apps/dashboard/src/app/(auth)/reset-password/page.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import { ResetPasswordForm } from '@/components/auth/reset-password-form'; -import { auth } from '@openpanel/auth/nextjs'; -import { redirect } from 'next/navigation'; - -export default async function Page() { - const session = await auth(); - - if (session.userId) { - return redirect('/'); - } - - return ( -
- -
- ); -} diff --git a/apps/dashboard/src/app/(onboarding)/layout.tsx b/apps/dashboard/src/app/(onboarding)/layout.tsx deleted file mode 100644 index 348ce998..00000000 --- a/apps/dashboard/src/app/(onboarding)/layout.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import FullWidthNavbar from '@/components/full-width-navbar'; - -import SkipOnboarding from './skip-onboarding'; -import Steps from './steps'; - -type Props = { - children: React.ReactNode; -}; - -const Page = ({ children }: Props) => { - return ( - <> -
-
-
-
-
- - - -
-
-
-
-
- Welcome to Openpanel -
-
- Get started -
-
- -
-
{children}
-
-
-
- - ); -}; - -export default Page; diff --git a/apps/dashboard/src/app/(onboarding)/onboarding-layout.tsx b/apps/dashboard/src/app/(onboarding)/onboarding-layout.tsx deleted file mode 100644 index 7d328940..00000000 --- a/apps/dashboard/src/app/(onboarding)/onboarding-layout.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { cn } from '@/utils/cn'; - -type Props = { - children: React.ReactNode; - className?: string; - title: string; - description?: React.ReactNode; -}; - -export const OnboardingDescription = ({ - children, - className, -}: Pick) => ( -
- {children} -
-); - -const OnboardingLayout = ({ - title, - description, - children, - className, -}: Props) => { - return ( -
-
-

{title}

- {description} -
- - {children} -
- ); -}; - -export default OnboardingLayout; diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx deleted file mode 100644 index a1e8cf99..00000000 --- a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/onboarding-connect.tsx +++ /dev/null @@ -1,70 +0,0 @@ -'use client'; - -import { ButtonContainer } from '@/components/button-container'; -import CopyInput from '@/components/forms/copy-input'; -import { LinkButton } from '@/components/ui/button'; -import { useClientSecret } from '@/hooks/useClientSecret'; -import { LockIcon } from 'lucide-react'; - -import type { IServiceProjectWithClients } from '@openpanel/db'; - -import OnboardingLayout, { - OnboardingDescription, -} from '../../../onboarding-layout'; -import ConnectApp from './connect-app'; -import ConnectBackend from './connect-backend'; -import ConnectWeb from './connect-web'; - -type Props = { - project: IServiceProjectWithClients; -}; - -const Connect = ({ project }: Props) => { - const client = project.clients[0]; - const [secret] = useClientSecret(); - - if (!client) { - return
Hmm, something fishy is going on. Please reload the page.
; - } - - return ( - - Let's connect your data sources to OpenPanel - - } - > -
-
- - Credentials -
- - -
- {project.types.map((type) => { - const Component = { - website: ConnectWeb, - app: ConnectApp, - backend: ConnectBackend, - }[type]; - - return ; - })} - -
- - Next - - - - ); -}; - -export default Connect; diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/page.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/page.tsx deleted file mode 100644 index 9569c2fb..00000000 --- a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/connect/page.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { getOrganizations, getProjectWithClients } from '@openpanel/db'; - -import { auth } from '@openpanel/auth/nextjs'; -import OnboardingConnect from './onboarding-connect'; - -type Props = { - params: { - projectId: string; - }; -}; - -const Connect = async ({ params: { projectId } }: Props) => { - const { userId } = await auth(); - const orgs = await getOrganizations(userId); - const organizationId = orgs[0]?.id; - if (!organizationId) { - throw new Error('No organization found'); - } - const project = await getProjectWithClients(projectId); - - if (!project) { - return
Hmm, something fishy is going on. Please reload the page.
; - } - - return ; -}; - -export default Connect; diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/onboarding-verify.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/onboarding-verify.tsx deleted file mode 100644 index 8264cc9a..00000000 --- a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/onboarding-verify.tsx +++ /dev/null @@ -1,155 +0,0 @@ -'use client'; - -import { ButtonContainer } from '@/components/button-container'; -import { LinkButton } from '@/components/ui/button'; -import { cn } from '@/utils/cn'; -import Link from 'next/link'; -import { useEffect, useState } from 'react'; - -import type { - IServiceClient, - IServiceEvent, - IServiceProjectWithClients, -} from '@openpanel/db'; - -import Syntax from '@/components/syntax'; -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, -} from '@/components/ui/accordion'; -import { useClientSecret } from '@/hooks/useClientSecret'; -import { clipboard } from '@/utils/clipboard'; -import { local } from 'd3'; -import OnboardingLayout, { - OnboardingDescription, -} from '../../../onboarding-layout'; -import VerifyListener from './onboarding-verify-listener'; - -type Props = { - project: IServiceProjectWithClients; - events: IServiceEvent[]; -}; - -const Verify = ({ project, events }: Props) => { - const [verified, setVerified] = useState(events.length > 0); - const client = project.clients[0]; - - if (!client) { - return
Hmm, something fishy is going on. Please reload the page.
; - } - - return ( - - Deploy your changes, as soon as you see events here, you're all - set! - - } - > - - - - - - - Back - - -
- {!verified && ( - - Skip for now - - )} - - - Your dashboard - -
-
-
- ); -}; - -export default Verify; - -function CurlPreview({ project }: { project: IServiceProjectWithClients }) { - const [secret] = useClientSecret(); - const client = project.clients[0]; - if (!client) { - return null; - } - - const payload: Record = { - type: 'track', - payload: { - name: 'screen_view', - properties: { - __title: `Testing OpenPanel - ${project.name}`, - __path: `${project.domain}`, - __referrer: `${process.env.NEXT_PUBLIC_DASHBOARD_URL}`, - }, - }, - }; - - if (project.types.includes('app')) { - payload.payload.properties.__path = '/'; - delete payload.payload.properties.__referrer; - } - - if (project.types.includes('backend')) { - payload.payload.name = 'test_event'; - payload.payload.properties = {}; - } - - const code = `curl -X POST ${process.env.NEXT_PUBLIC_API_URL}/track \\ --H "Content-Type: application/json" \\ --H "openpanel-client-id: ${client.id}" \\ --H "openpanel-client-secret: ${secret}" \\ --H "User-Agent: ${typeof window !== 'undefined' ? window.navigator.userAgent : ''}" \\ --d '${JSON.stringify(payload)}'`; - - return ( -
- - - { - clipboard(code, null); - }} - > - Try out the curl command - - - - - - -
- ); -} diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/page.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/page.tsx deleted file mode 100644 index 1d553e7f..00000000 --- a/apps/dashboard/src/app/(onboarding)/onboarding/[projectId]/verify/page.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { cookies } from 'next/headers'; -import { escape } from 'sqlstring'; - -import { - TABLE_NAMES, - getEvents, - getOrganizations, - getProjectWithClients, -} from '@openpanel/db'; - -import { auth } from '@openpanel/auth/nextjs'; -import OnboardingVerify from './onboarding-verify'; - -type Props = { - params: { - projectId: string; - }; -}; - -const Verify = async ({ params: { projectId } }: Props) => { - const { userId } = await auth(); - const orgs = await getOrganizations(userId); - const organizationId = orgs[0]?.id; - if (!organizationId) { - throw new Error('No organization found'); - } - const [project, events] = await Promise.all([ - await getProjectWithClients(projectId), - getEvents( - `SELECT * FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT 100`, - ), - ]); - - if (!project) { - return
Hmm, something fishy is going on. Please reload the page.
; - } - - return ; -}; - -export default Verify; diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/page.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/page.tsx deleted file mode 100644 index b4d55130..00000000 --- a/apps/dashboard/src/app/(onboarding)/onboarding/page.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { Or } from '@/components/auth/or'; -import { SignInGithub } from '@/components/auth/sign-in-github'; -import { SignInGoogle } from '@/components/auth/sign-in-google'; -import { SignUpEmailForm } from '@/components/auth/sign-up-email-form'; -import { auth } from '@openpanel/auth/nextjs'; -import { getInviteById } from '@openpanel/db'; -import Link from 'next/link'; -import { redirect } from 'next/navigation'; -import OnboardingLayout, { OnboardingDescription } from '../onboarding-layout'; - -const Page = async ({ - searchParams, -}: { searchParams: { inviteId: string } }) => { - const session = await auth(); - const inviteId = await searchParams.inviteId; - const invite = inviteId ? await getInviteById(inviteId) : null; - const hasInviteExpired = invite?.expiresAt && invite.expiresAt < new Date(); - if (session.userId) { - return redirect('/'); - } - - return ( -
- - Lets start with creating you account. By creating an account you - accept the{' '} - - Terms of Service - {' '} - and{' '} - - Privacy Policy - - . - - } - > - {invite && !hasInviteExpired && ( -
-

- Invitation to {invite.organization.name} -

-

- After you have created your account, you will be added to the - organization. -

-
- )} - {invite && hasInviteExpired && ( -
-

- Invitation to {invite.organization.name} has expired -

-

- The invitation has expired. Please contact the organization owner - to get a new invitation. -

-
- )} -
- - -
- -
-

Sign up with email

- -
-
-
- ); -}; - -export default Page; diff --git a/apps/dashboard/src/app/(onboarding)/onboarding/project/page.tsx b/apps/dashboard/src/app/(onboarding)/onboarding/project/page.tsx deleted file mode 100644 index d25c25db..00000000 --- a/apps/dashboard/src/app/(onboarding)/onboarding/project/page.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { auth } from '@openpanel/auth/nextjs'; -import { getOrganizations } from '@openpanel/db'; -import { OnboardingCreateProject } from './onboarding-create-project'; - -const Page = async () => { - const { userId } = await auth(); - const organizations = await getOrganizations(userId); - return ; -}; - -export default Page; diff --git a/apps/dashboard/src/app/(public)/share/overview/[id]/page.tsx b/apps/dashboard/src/app/(public)/share/overview/[id]/page.tsx deleted file mode 100644 index 70eff2ee..00000000 --- a/apps/dashboard/src/app/(public)/share/overview/[id]/page.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import { StickyBelowHeader } from '@/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header'; -import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons'; -import ServerLiveCounter from '@/components/overview/live-counter'; -import OverviewMetrics from '@/components/overview/overview-metrics'; -import OverviewTopDevices from '@/components/overview/overview-top-devices'; -import OverviewTopEvents from '@/components/overview/overview-top-events'; -import OverviewTopGeo from '@/components/overview/overview-top-geo'; -import OverviewTopPages from '@/components/overview/overview-top-pages'; -import OverviewTopSources from '@/components/overview/overview-top-sources'; -import { notFound } from 'next/navigation'; - -import { ShareEnterPassword } from '@/components/auth/share-enter-password'; -import { OverviewRange } from '@/components/overview/overview-range'; -import { getOrganizationById, getShareOverviewById } from '@openpanel/db'; -import { cookies } from 'next/headers'; - -interface PageProps { - params: { - id: string; - }; - searchParams: { - header: string; - }; -} - -export default async function Page({ - params: { id }, - searchParams, -}: PageProps) { - const share = await getShareOverviewById(id); - if (!share) { - return notFound(); - } - if (!share.public) { - return notFound(); - } - const projectId = share.projectId; - const organization = await getOrganizationById(share.organizationId); - - if (share.password) { - const cookie = cookies().get(`shared-overview-${share.id}`)?.value; - if (!cookie) { - return ; - } - } - - return ( -
- {searchParams.header !== '0' && ( -
-
- {organization?.name} -

{share.project?.name}

-
- - POWERED BY - openpanel.dev - -
- )} -
- -
-
- -
-
- -
-
- -
-
- - - - - - -
-
-
- ); -} diff --git a/apps/dashboard/src/app/api/healthcheck/route.tsx b/apps/dashboard/src/app/api/healthcheck/route.tsx deleted file mode 100644 index f8da9360..00000000 --- a/apps/dashboard/src/app/api/healthcheck/route.tsx +++ /dev/null @@ -1,5 +0,0 @@ -export const dynamic = 'force-dynamic'; // no caching - -export async function GET(request: Request) { - return Response.json({ status: 'ok' }); -} diff --git a/apps/dashboard/src/app/favicon.ico b/apps/dashboard/src/app/favicon.ico deleted file mode 100644 index 403a07ef..00000000 Binary files a/apps/dashboard/src/app/favicon.ico and /dev/null differ diff --git a/apps/dashboard/src/app/global-error.tsx b/apps/dashboard/src/app/global-error.tsx deleted file mode 100644 index 2b7fa1b3..00000000 --- a/apps/dashboard/src/app/global-error.tsx +++ /dev/null @@ -1,20 +0,0 @@ -'use client'; - -import { useEffect } from 'react'; - -export default function GlobalError({ - error, -}: { - error: Error & { digest?: string }; - reset: () => void; -}) { - useEffect(() => {}, [error]); - - return ( - - -

Something went wrong

- - - ); -} diff --git a/apps/dashboard/src/app/layout.tsx b/apps/dashboard/src/app/layout.tsx deleted file mode 100644 index a4f84b64..00000000 --- a/apps/dashboard/src/app/layout.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { cn } from '@/utils/cn'; -import NextTopLoader from 'nextjs-toploader'; - -import Providers from './providers'; - -import '@/styles/globals.css'; -import 'flag-icons/css/flag-icons.min.css'; -import 'katex/dist/katex.min.css'; - -import { GeistMono } from 'geist/font/mono'; -import { GeistSans } from 'geist/font/sans'; - -export const metadata = { - title: 'Overview - Openpanel.dev', -}; - -export const viewport = { - width: 'device-width', - initialScale: 1, - maximumScale: 1, - userScalable: 1, -}; - -export default function RootLayout({ - children, -}: { - children: React.ReactNode; -}) { - return ( - - - - {children} - - - ); -} diff --git a/apps/dashboard/src/app/maintenance/page.tsx b/apps/dashboard/src/app/maintenance/page.tsx deleted file mode 100644 index a0529fad..00000000 --- a/apps/dashboard/src/app/maintenance/page.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { CalendarCogIcon } from 'lucide-react'; - -export default function Maintenance() { - return ( -
-
- -
- Oh no! -
-

Maintenance

-

- We're doing a planned maintenance. Please check back later. -

-
-
- ); -} diff --git a/apps/dashboard/src/app/manifest.ts b/apps/dashboard/src/app/manifest.ts deleted file mode 100644 index 4fd3b21a..00000000 --- a/apps/dashboard/src/app/manifest.ts +++ /dev/null @@ -1,16 +0,0 @@ -import type { MetadataRoute } from 'next'; - -export const dynamic = 'static'; - -export default function manifest(): MetadataRoute.Manifest { - return { - id: process.env.NEXT_PUBLIC_DASHBOARD_URL, - name: 'Openpanel.dev', - short_name: 'Openpanel.dev', - description: '', - start_url: '/', - display: 'standalone', - background_color: '#fff', - theme_color: '#fff', - }; -} diff --git a/apps/dashboard/src/app/providers.tsx b/apps/dashboard/src/app/providers.tsx deleted file mode 100644 index 1ea1cbec..00000000 --- a/apps/dashboard/src/app/providers.tsx +++ /dev/null @@ -1,90 +0,0 @@ -'use client'; - -import { NotificationProvider } from '@/components/notifications/notification-provider'; -import { TooltipProvider } from '@/components/ui/tooltip'; -import { ModalProvider } from '@/modals'; -import type { AppStore } from '@/redux'; -import makeStore from '@/redux'; -import { api } from '@/trpc/client'; -import { OpenPanelComponent } from '@openpanel/nextjs'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { httpLink } from '@trpc/client'; -import { ThemeProvider } from 'next-themes'; -import { NuqsAdapter } from 'nuqs/adapters/next/app'; -import { useRef, useState } from 'react'; -import { Provider as ReduxProvider } from 'react-redux'; -import { Toaster } from 'sonner'; -import superjson from 'superjson'; - -function AllProviders({ children }: { children: React.ReactNode }) { - const [queryClient] = useState( - () => - new QueryClient({ - defaultOptions: { - queries: { - networkMode: 'always', - refetchOnMount: true, - refetchOnWindowFocus: false, - }, - }, - }), - ); - const [trpcClient] = useState(() => - api.createClient({ - transformer: superjson, - links: [ - httpLink({ - url: `${process.env.NEXT_PUBLIC_API_URL}/trpc`, - fetch(url, options) { - return fetch(url, { - ...options, - credentials: 'include', - mode: 'cors', - }); - }, - }), - ], - }), - ); - - const storeRef = useRef(); - if (!storeRef.current) { - // Create the store instance the first time this renders - storeRef.current = makeStore(); - } - - return ( - - - {process.env.NEXT_PUBLIC_OP_CLIENT_ID && ( - - )} - - - - - {children} - - - - - - - - - - ); -} - -export default function Providers({ children }: { children: React.ReactNode }) { - return {children}; -} diff --git a/apps/dashboard/src/app/robots.txt b/apps/dashboard/src/app/robots.txt deleted file mode 100644 index 77470cb3..00000000 --- a/apps/dashboard/src/app/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Disallow: / \ No newline at end of file diff --git a/apps/dashboard/src/components/auth/or.tsx b/apps/dashboard/src/components/auth/or.tsx deleted file mode 100644 index c6bd2450..00000000 --- a/apps/dashboard/src/components/auth/or.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { cn } from '@/utils/cn'; - -export function Or({ className }: { className?: string }) { - return ( -
-
- OR -
-
- ); -} diff --git a/apps/dashboard/src/components/auth/reset-password-form.tsx b/apps/dashboard/src/components/auth/reset-password-form.tsx deleted file mode 100644 index d696ef3b..00000000 --- a/apps/dashboard/src/components/auth/reset-password-form.tsx +++ /dev/null @@ -1,57 +0,0 @@ -'use client'; - -import { InputWithLabel } from '@/components/forms/input-with-label'; -import { Button } from '@/components/ui/button'; -import { api } from '@/trpc/client'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { zResetPassword } from '@openpanel/validation'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { useForm } from 'react-hook-form'; -import { toast } from 'sonner'; -import type { z } from 'zod'; - -const validator = zResetPassword; -type IForm = z.infer; - -export function ResetPasswordForm() { - const searchParams = useSearchParams(); - const token = searchParams.get('token') ?? null; - const router = useRouter(); - const mutation = api.auth.resetPassword.useMutation({ - onSuccess() { - toast.success('Password reset successfully', { - description: 'You can now login with your new password', - }); - router.push('/login'); - }, - onError(error) { - toast.error(error.message); - }, - }); - - const form = useForm({ - resolver: zodResolver(validator), - defaultValues: { - token: token ?? '', - password: '', - }, - }); - - const onSubmit = form.handleSubmit(async (data) => { - mutation.mutate(data); - }); - - return ( -
-
- - - -
- ); -} diff --git a/apps/dashboard/src/components/clients/client-actions.tsx b/apps/dashboard/src/components/clients/client-actions.tsx deleted file mode 100644 index f4bdc151..00000000 --- a/apps/dashboard/src/components/clients/client-actions.tsx +++ /dev/null @@ -1,73 +0,0 @@ -'use client'; - -import { pushModal, showConfirm } from '@/modals'; -import { api } from '@/trpc/client'; -import { clipboard } from '@/utils/clipboard'; -import { MoreHorizontal } from 'lucide-react'; -import { useRouter } from 'next/navigation'; -import { toast } from 'sonner'; - -import type { IServiceClientWithProject } from '@openpanel/db'; - -import { Button } from '../ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '../ui/dropdown-menu'; - -export function ClientActions(client: IServiceClientWithProject) { - const { id } = client; - const router = useRouter(); - const deletion = api.client.remove.useMutation({ - onSuccess() { - toast('Success', { - description: 'Client revoked, incoming requests will be rejected.', - }); - router.refresh(); - }, - }); - return ( - - - - - - Actions - clipboard(id)}> - Copy client ID - - { - pushModal('EditClient', client); - }} - > - Edit - - - { - showConfirm({ - title: 'Revoke client', - text: 'Are you sure you want to revoke this client? This action cannot be undone.', - onConfirm() { - deletion.mutate({ - id, - }); - }, - }); - }} - > - Revoke - - - - ); -} diff --git a/apps/dashboard/src/components/clients/table/columns.tsx b/apps/dashboard/src/components/clients/table/columns.tsx deleted file mode 100644 index fc3b70c8..00000000 --- a/apps/dashboard/src/components/clients/table/columns.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { EventIcon } from '@/components/events/event-icon'; -import { ProjectLink } from '@/components/links'; -import { SerieIcon } from '@/components/report-chart/common/serie-icon'; -import { TooltipComplete } from '@/components/tooltip-complete'; -import { useNumber } from '@/hooks/useNumerFormatter'; -import { pushModal } from '@/modals'; -import { formatDateTime, formatTime } from '@/utils/date'; -import { getProfileName } from '@/utils/getters'; -import type { ColumnDef } from '@tanstack/react-table'; -import { isToday } from 'date-fns'; - -import { ACTIONS } from '@/components/data-table'; -import type { IServiceClientWithProject, IServiceEvent } from '@openpanel/db'; -import { ClientActions } from '../client-actions'; - -export function useColumns() { - const number = useNumber(); - const columns: ColumnDef[] = [ - { - accessorKey: 'name', - header: 'Name', - cell: ({ row }) => { - return
{row.original.name}
; - }, - }, - { - accessorKey: 'id', - header: 'Client ID', - cell: ({ row }) =>
{row.original.id}
, - }, - // { - // accessorKey: 'secret', - // header: 'Secret', - // cell: (info) => - //
- - // }, - { - accessorKey: 'createdAt', - header: 'Created at', - cell({ row }) { - const date = row.original.createdAt; - return ( -
{isToday(date) ? formatTime(date) : formatDateTime(date)}
- ); - }, - }, - { - id: ACTIONS, - header: 'Actions', - cell: ({ row }) => , - }, - ]; - - return columns; -} diff --git a/apps/dashboard/src/components/clients/table/index.tsx b/apps/dashboard/src/components/clients/table/index.tsx deleted file mode 100644 index e92c1d9e..00000000 --- a/apps/dashboard/src/components/clients/table/index.tsx +++ /dev/null @@ -1,67 +0,0 @@ -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, PlusIcon } from 'lucide-react'; -import type { Dispatch, SetStateAction } from 'react'; - -import type { IServiceClientWithProject } from '@openpanel/db'; - -import { useAppParams } from '@/hooks/useAppParams'; -import { pushModal } from '@/modals'; -import { useColumns } from './columns'; - -type Props = { - query: UseQueryResult; - cursor: number; - setCursor: Dispatch>; -}; - -export const ClientsTable = ({ query, ...props }: Props) => { - const columns = useColumns(); - const { data, isFetching, isLoading } = query; - - if (isLoading) { - return ; - } - - if (data?.length === 0) { - return ( - -

Could not find any clients

-
- {'cursor' in props && props.cursor !== 0 && ( - - )} - -
-
- ); - } - - return ( - <> - - {'cursor' in props && ( - - )} - - ); -}; diff --git a/apps/dashboard/src/components/dark-mode-toggle.tsx b/apps/dashboard/src/components/dark-mode-toggle.tsx deleted file mode 100644 index 428c6c31..00000000 --- a/apps/dashboard/src/components/dark-mode-toggle.tsx +++ /dev/null @@ -1,43 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { MoonIcon, SunIcon } from 'lucide-react'; -import { useTheme } from 'next-themes'; -import * as React from 'react'; - -interface Props { - className?: string; -} - -export default function DarkModeToggle({ className }: Props) { - const { setTheme } = useTheme(); - - return ( - - - - - - setTheme('light')}> - Light - - setTheme('dark')}> - Dark - - {/* setTheme('system')}> - System - */} - - - ); -} diff --git a/apps/dashboard/src/components/data-table.tsx b/apps/dashboard/src/components/data-table.tsx deleted file mode 100644 index c4b106e2..00000000 --- a/apps/dashboard/src/components/data-table.tsx +++ /dev/null @@ -1,120 +0,0 @@ -'use client'; - -import { cn } from '@/utils/cn'; -import { - flexRender, - getCoreRowModel, - useReactTable, -} from '@tanstack/react-table'; -import type { ColumnDef, RowData } from '@tanstack/react-table'; - -import { Grid, GridBody, GridCell, GridHeader, GridRow } from './grid-table'; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; -} - -declare module '@tanstack/react-table' { - // eslint-disable-next-line - interface ColumnMeta { - className?: string; - } -} - -export const ACTIONS = '__actions__'; - -export function TableButtons({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { - return ( -
- {children} -
- ); -} - -export function DataTable({ columns, data }: DataTableProps) { - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - }); - - return ( - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => { - if (header.column.id === ACTIONS) { - return ( - - Actions - - ); - } - - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} - - ))} - - - {table.getRowModel().rows?.length ? ( - table.getRowModel().rows.map((row) => ( - - {row.getVisibleCells().map((cell) => { - if (cell.column.id === ACTIONS) { - return ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ); - } - - return ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ); - })} - - )) - ) : ( - - -
No results.
-
-
- )} -
-
- ); -} diff --git a/apps/dashboard/src/components/events/event-field-value.tsx b/apps/dashboard/src/components/events/event-field-value.tsx deleted file mode 100644 index 4d584562..00000000 --- a/apps/dashboard/src/components/events/event-field-value.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { fancyMinutes } from '@/hooks/useNumerFormatter'; -import { formatDateTime, formatTime } from '@/utils/date'; -import type { IServiceEvent } from '@openpanel/db'; -import { isToday } from 'date-fns'; -import { SerieIcon } from '../report-chart/common/serie-icon'; - -export function EventFieldValue({ - name, - value, - event, -}: { - name: keyof IServiceEvent; - value: any; - event: IServiceEvent; -}) { - if (!value) { - return null; - } - - if (value instanceof Date) { - return isToday(value) ? formatTime(value) : formatDateTime(value); - } - - switch (name) { - case 'osVersion': - return ( -
- - {value} -
- ); - case 'browserVersion': - return ( -
- - {value} -
- ); - case 'city': - return ( -
- - {value} -
- ); - case 'region': - return ( -
- - {value} -
- ); - case 'properties': - return JSON.stringify(value); - case 'country': - case 'browser': - case 'os': - case 'brand': - case 'model': - case 'device': - return ( -
- - {value} -
- ); - case 'duration': - return ( -
- ({value}ms){' '} - {fancyMinutes(value / 1000)} -
- ); - default: - return value; - } -} diff --git a/apps/dashboard/src/components/events/table/events-data-table.tsx b/apps/dashboard/src/components/events/table/events-data-table.tsx deleted file mode 100644 index 9dc2dc3c..00000000 --- a/apps/dashboard/src/components/events/table/events-data-table.tsx +++ /dev/null @@ -1,160 +0,0 @@ -'use client'; - -import { GridCell } from '@/components/grid-table'; -import { cn } from '@/utils/cn'; -import { - flexRender, - getCoreRowModel, - useReactTable, -} from '@tanstack/react-table'; -import type { ColumnDef } from '@tanstack/react-table'; -import { useWindowVirtualizer } from '@tanstack/react-virtual'; -import throttle from 'lodash.throttle'; -import { useEffect, useRef, useState } from 'react'; -import { useEventsTableColumns } from './events-table-columns'; - -interface DataTableProps { - columns: ColumnDef[]; - data: TData[]; -} - -export function EventsDataTable({ - columns, - data, -}: DataTableProps) { - const [visibleColumns] = useEventsTableColumns(); - const table = useReactTable({ - data, - columns, - getCoreRowModel: getCoreRowModel(), - }); - - const parentRef = useRef(null); - const [scrollMargin, setScrollMargin] = useState(0); - const { rows } = table.getRowModel(); - - const virtualizer = useWindowVirtualizer({ - count: rows.length, - estimateSize: () => 48, - scrollMargin, - overscan: 10, - }); - - useEffect(() => { - const updateScrollMargin = throttle(() => { - if (parentRef.current) { - setScrollMargin( - parentRef.current.getBoundingClientRect().top + window.scrollY, - ); - } - }, 500); - - // Initial calculation - updateScrollMargin(); - - // Listen for scroll and resize events - window.addEventListener('scroll', updateScrollMargin); - window.addEventListener('resize', updateScrollMargin); - - return () => { - window.removeEventListener('scroll', updateScrollMargin); - window.removeEventListener('resize', updateScrollMargin); - }; - }, []); // Empty dependency array since we're setting up listeners - - const visibleRows = virtualizer.getVirtualItems(); - - return ( -
-
-
- {table.getHeaderGroups().map((headerGroup) => ( -
- {headerGroup.headers - .filter((header) => visibleColumns.includes(header.id)) - .map((header) => { - return ( - - {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} - - ); - })} -
- ))} -
-
- {visibleRows.map((virtualRow, index) => { - const row = rows[virtualRow.index]!; - if (!row) { - return null; - } - - return ( -
- {row - .getVisibleCells() - .filter((cell) => visibleColumns.includes(cell.column.id)) - .map((cell) => { - return ( - - {flexRender( - cell.column.columnDef.cell, - cell.getContext(), - )} - - ); - })} -
- ); - })} -
-
-
-
-
- ); -} diff --git a/apps/dashboard/src/components/events/table/events-table-columns.tsx b/apps/dashboard/src/components/events/table/events-table-columns.tsx deleted file mode 100644 index 8ff858b6..00000000 --- a/apps/dashboard/src/components/events/table/events-table-columns.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { ColumnsIcon } from 'lucide-react'; -import { useQueryState } from 'nuqs'; -import { useLocalStorage } from 'usehooks-ts'; - -// Define available columns -const AVAILABLE_COLUMNS = [ - { id: 'name', label: 'Name' }, - { id: 'createdAt', label: 'Created at' }, - { id: 'profileId', label: 'Profile' }, - { id: 'country', label: 'Country' }, - { id: 'os', label: 'OS' }, - { id: 'browser', label: 'Browser' }, - { id: 'properties', label: 'Properties' }, - { id: 'sessionId', label: 'Session ID' }, - { id: 'deviceId', label: 'Device ID' }, -] as const; - -export function useEventsTableColumns() { - return useLocalStorage('@op:events-table-columns', [ - 'name', - 'createdAt', - 'profileId', - 'country', - 'os', - 'browser', - ]); -} - -export function EventsTableColumns() { - const [visibleColumns, setVisibleColumns] = useEventsTableColumns(); - - return ( - - - - - - Toggle columns - - {AVAILABLE_COLUMNS.map((column) => ( - { - setVisibleColumns( - checked - ? [...visibleColumns, column.id] - : visibleColumns.filter((id) => id !== column.id), - ); - }} - > - {column.label} - - ))} - - - ); -} diff --git a/apps/dashboard/src/components/events/table/index.tsx b/apps/dashboard/src/components/events/table/index.tsx deleted file mode 100644 index d37b0f6d..00000000 --- a/apps/dashboard/src/components/events/table/index.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import { FullPageEmptyState } from '@/components/full-page-empty-state'; -import { TableSkeleton } from '@/components/ui/table'; -import type { - UseInfiniteQueryResult, - UseQueryResult, -} from '@tanstack/react-query'; -import { GanttChartIcon, Loader2Icon } from 'lucide-react'; -import { useEffect, useRef, useState } from 'react'; - -import { pushModal, replaceWithModal, useOnPushModal } from '@/modals'; -import type { RouterOutputs } from '@/trpc/client'; -import { cn } from '@/utils/cn'; -import { shouldIgnoreKeypress } from '@/utils/should-ignore-keypress'; -import { bind } from 'bind-event-listener'; -import { useInViewport } from 'react-in-viewport'; -import { useColumns } from './columns'; -import { EventsDataTable } from './events-data-table'; - -type Props = - | { - query: UseInfiniteQueryResult; - } - | { - query: UseQueryResult; - }; - -export const EventsTable = ({ query, ...props }: Props) => { - const columns = useColumns(); - const { isLoading } = query; - const ref = useRef(null); - const { inViewport, enterCount } = useInViewport(ref, undefined, { - disconnectOnLeave: true, - }); - const isInfiniteQuery = 'fetchNextPage' in query; - const data = - (isInfiniteQuery - ? query.data?.pages?.flatMap((p) => p.items) - : query.data?.items) ?? []; - - const hasNextPage = isInfiniteQuery - ? query.data?.pages[query.data.pages.length - 1]?.meta.next - : query.data?.meta.next; - - const [eventId, setEventId] = useState(null); - useOnPushModal('EventDetails', (isOpen, props) => { - setEventId(isOpen ? props.id : null); - }); - - useEffect(() => { - return bind(window, { - type: 'keydown', - listener(event) { - if (shouldIgnoreKeypress(event)) { - return; - } - - if (event.key === 'ArrowLeft') { - const index = data.findIndex((p) => p.id === eventId); - if (index !== -1) { - const match = data[index - 1]; - if (match) { - replaceWithModal('EventDetails', match); - } - } - } - if (event.key === 'ArrowRight') { - const index = data.findIndex((p) => p.id === eventId); - if (index !== -1) { - const match = data[index + 1]; - if (match) { - replaceWithModal('EventDetails', match); - } else if ( - hasNextPage && - isInfiniteQuery && - data.length > 0 && - query.isFetchingNextPage === false - ) { - query.fetchNextPage(); - } - } - } - }, - }); - }, [data, eventId]); - - useEffect(() => { - if ( - hasNextPage && - isInfiniteQuery && - data.length > 0 && - inViewport && - enterCount > 0 && - query.isFetchingNextPage === false - ) { - query.fetchNextPage(); - } - }, [inViewport, enterCount, hasNextPage]); - - if (isLoading) { - return ; - } - - if (data.length === 0) { - return ( - -

Could not find any events

-
- ); - } - - return ( - <> - - {isInfiniteQuery && ( -
-
- -
-
- )} - - ); -}; diff --git a/apps/dashboard/src/components/links.tsx b/apps/dashboard/src/components/links.tsx deleted file mode 100644 index bb294943..00000000 --- a/apps/dashboard/src/components/links.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { useAppParams } from '@/hooks/useAppParams'; -import type { LinkProps } from 'next/link'; -import Link from 'next/link'; - -export function ProjectLink({ - children, - ...props -}: LinkProps & { - children: React.ReactNode; - className?: string; - title?: string; -}) { - const { organizationId, projectId } = useAppParams(); - if (typeof props.href === 'string') { - return ( - - {children} - - ); - } - - return

ProjectLink

; -} diff --git a/apps/dashboard/src/components/notifications/notification-provider.tsx b/apps/dashboard/src/components/notifications/notification-provider.tsx deleted file mode 100644 index fb50a6c2..00000000 --- a/apps/dashboard/src/components/notifications/notification-provider.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useAppParams } from '@/hooks/useAppParams'; -import useWS from '@/hooks/useWS'; -import type { Prisma } from '@openpanel/db'; -import { BellIcon } from 'lucide-react'; -import { toast } from 'sonner'; - -export function NotificationProvider() { - const { projectId } = useAppParams(); - - if (!projectId) return null; - - return ; -} - -export function InnerNotificationProvider({ - projectId, -}: { projectId: string }) { - useWS( - `/live/notifications/${projectId}`, - (notification) => { - toast(notification.title, { - description: notification.message, - icon: , - }); - }, - ); - - return null; -} diff --git a/apps/dashboard/src/components/notifications/notifications.tsx b/apps/dashboard/src/components/notifications/notifications.tsx deleted file mode 100644 index 82296022..00000000 --- a/apps/dashboard/src/components/notifications/notifications.tsx +++ /dev/null @@ -1,14 +0,0 @@ -'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/table/index.tsx b/apps/dashboard/src/components/notifications/table/index.tsx deleted file mode 100644 index 1c44f8b5..00000000 --- a/apps/dashboard/src/components/notifications/table/index.tsx +++ /dev/null @@ -1,63 +0,0 @@ -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 { RouterOutputs } from '@/trpc/client'; -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 notifications

- {'cursor' in props && props.cursor !== 0 && ( - - )} -
- ); - } - - return ( - <> - - {'cursor' in props && ( - - )} - - ); -}; diff --git a/apps/dashboard/src/components/overview/live-counter/index.tsx b/apps/dashboard/src/components/overview/live-counter/index.tsx deleted file mode 100644 index 4ee4473c..00000000 --- a/apps/dashboard/src/components/overview/live-counter/index.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import withSuspense from '@/hocs/with-suspense'; - -import { eventBuffer } from '@openpanel/db'; - -import type { LiveCounterProps } from './live-counter'; -import LiveCounter from './live-counter'; - -async function ServerLiveCounter(props: Omit) { - const count = await eventBuffer.getActiveVisitorCount(props.projectId); - return ; -} - -export default withSuspense(ServerLiveCounter, () =>
); diff --git a/apps/dashboard/src/components/overview/live-counter/live-counter.tsx b/apps/dashboard/src/components/overview/live-counter/live-counter.tsx deleted file mode 100644 index d03d91c2..00000000 --- a/apps/dashboard/src/components/overview/live-counter/live-counter.tsx +++ /dev/null @@ -1,85 +0,0 @@ -'use client'; - -import { TooltipComplete } from '@/components/tooltip-complete'; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from '@/components/ui/tooltip'; -import { useDebounceState } from '@/hooks/useDebounceState'; -import useWS from '@/hooks/useWS'; -import { cn } from '@/utils/cn'; -import { useQueryClient } from '@tanstack/react-query'; -import dynamic from 'next/dynamic'; -import { useRef, useState } from 'react'; -import { toast } from 'sonner'; - -export interface LiveCounterProps { - data: number; - projectId: string; -} - -const AnimatedNumbers = dynamic(() => import('react-animated-numbers'), { - ssr: false, - loading: () =>
0
, -}); - -const FIFTEEN_SECONDS = 1000 * 30; - -export default function LiveCounter({ data = 0, projectId }: LiveCounterProps) { - const client = useQueryClient(); - const counter = useDebounceState(data, 1000, { - maxWait: 5000, - }); - const lastRefresh = useRef(Date.now()); - - useWS(`/live/visitors/${projectId}`, (value) => { - if (!Number.isNaN(value)) { - counter.set(value); - if (Date.now() - lastRefresh.current > FIFTEEN_SECONDS) { - lastRefresh.current = Date.now(); - if (!document.hidden) { - toast('Refreshed data'); - client.refetchQueries({ - type: 'active', - }); - } - } - } - }); - - return ( - -
-
-
-
-
- ({ - type: 'spring', - duration: index + 0.3, - - damping: 10, - stiffness: 200, - })} - animateToNumber={counter.debounced} - locale="en" - /> -
- - ); -} diff --git a/apps/dashboard/src/components/overview/overview-hydrate-options.tsx b/apps/dashboard/src/components/overview/overview-hydrate-options.tsx deleted file mode 100644 index fe871aaf..00000000 --- a/apps/dashboard/src/components/overview/overview-hydrate-options.tsx +++ /dev/null @@ -1,22 +0,0 @@ -'use client'; - -import { getStorageItem } from '@/utils/storage'; -import { useEffect, useRef } from 'react'; -import { useOverviewOptions } from './useOverviewOptions'; - -export function OverviewHydrateOptions() { - const { setRange, range } = useOverviewOptions(); - const ref = useRef(false); - - useEffect(() => { - if (!ref.current) { - const range = getStorageItem('range', '7d'); - if (range !== '7d') { - setRange(range); - } - ref.current = true; - } - }, []); - - return null; -} diff --git a/apps/dashboard/src/components/overview/overview-live-histogram.tsx b/apps/dashboard/src/components/overview/overview-live-histogram.tsx deleted file mode 100644 index 2f9c06ef..00000000 --- a/apps/dashboard/src/components/overview/overview-live-histogram.tsx +++ /dev/null @@ -1,140 +0,0 @@ -'use client'; - -import { api } from '@/trpc/client'; -import { cn } from '@/utils/cn'; - -import type { IChartProps } from '@openpanel/validation'; - -import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip'; - -interface OverviewLiveHistogramProps { - projectId: string; -} - -export function OverviewLiveHistogram({ - projectId, -}: OverviewLiveHistogramProps) { - const report: IChartProps = { - projectId, - events: [ - { - segment: 'user', - filters: [ - { - id: '1', - name: 'name', - operator: 'is', - value: ['screen_view', 'session_start'], - }, - ], - id: 'A', - name: '*', - displayName: 'Active users', - }, - ], - chartType: 'histogram', - interval: 'minute', - range: '30min', - name: '', - metric: 'sum', - breakdowns: [], - lineType: 'monotone', - previous: false, - }; - const countReport: IChartProps = { - name: '', - projectId, - events: [ - { - segment: 'user', - filters: [], - id: 'A', - name: 'session_start', - }, - ], - breakdowns: [], - chartType: 'metric', - lineType: 'monotone', - interval: 'minute', - range: '30min', - previous: false, - metric: 'sum', - }; - - const res = api.chart.chart.useQuery(report); - const countRes = api.chart.chart.useQuery(countReport); - - const metrics = res.data?.series[0]?.metrics; - const minutes = (res.data?.series[0]?.data || []).slice(-30); - const liveCount = countRes.data?.series[0]?.metrics?.sum ?? 0; - - if (res.isInitialLoading || countRes.isInitialLoading) { - const staticArray = [ - 10, 25, 30, 45, 20, 5, 55, 18, 40, 12, 50, 35, 8, 22, 38, 42, 15, 28, 52, - 5, 48, 14, 32, 58, 7, 19, 33, 56, 24, 5, - ]; - - return ( - - {staticArray.map((percent, i) => ( -
- ))} - - ); - } - - if (!res.isSuccess && !countRes.isSuccess) { - return null; - } - - return ( - - {minutes.map((minute) => { - return ( - - -
- - -
{minute.count} active users
-
@ {new Date(minute.date).toLocaleTimeString()}
-
- - ); - })} - - ); -} - -interface WrapperProps { - children: React.ReactNode; - count: number; -} - -function Wrapper({ children, count }: WrapperProps) { - return ( -
-
- {count} unique vistors last 30 minutes -
-
- {children} -
-
- ); -} diff --git a/apps/dashboard/src/components/overview/overview-share/index.tsx b/apps/dashboard/src/components/overview/overview-share/index.tsx deleted file mode 100644 index e962b96a..00000000 --- a/apps/dashboard/src/components/overview/overview-share/index.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Button } from '@/components/ui/button'; -import withSuspense from '@/hocs/with-suspense'; - -import { getShareByProjectId } from '@openpanel/db'; - -import { OverviewShare } from './overview-share'; - -type Props = { - projectId: string; -}; - -const OverviewShareServer = async ({ projectId }: Props) => { - const share = await getShareByProjectId(projectId); - return ; -}; - -export default withSuspense(OverviewShareServer, () => ( - -)); diff --git a/apps/dashboard/src/components/overview/overview-top-bots.tsx b/apps/dashboard/src/components/overview/overview-top-bots.tsx deleted file mode 100644 index c273ebec..00000000 --- a/apps/dashboard/src/components/overview/overview-top-bots.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { api } from '@/trpc/client'; -import { useState } from 'react'; - -import { Pagination } from '../pagination'; -import { Tooltiper } from '../ui/tooltip'; -import { WidgetTable } from '../widget-table'; - -interface Props { - projectId: string; -} - -function getPath(path: string) { - try { - return new URL(path).pathname; - } catch { - return path; - } -} - -const OverviewTopBots = ({ projectId }: Props) => { - const [cursor, setCursor] = useState(0); - const res = api.event.bots.useQuery( - { projectId, cursor }, - { - keepPreviousData: true, - }, - ); - const data = res.data?.data ?? []; - const count = res.data?.count ?? 0; - - return ( - <> - item.id} - columns={[ - { - name: 'Path', - width: 'w-full', - render(item) { - return ( - - {getPath(item.path)} - - ); - }, - }, - { - name: 'Date', - width: '100px', - render(item) { - return ( -
- -
{item.name}
-
- -
{item.createdAt.toLocaleDateString()}
-
-
- ); - }, - }, - ]} - /> - - - ); -}; - -export default OverviewTopBots; diff --git a/apps/dashboard/src/components/overview/overview-top-events/index.tsx b/apps/dashboard/src/components/overview/overview-top-events/index.tsx deleted file mode 100644 index 6c9601c1..00000000 --- a/apps/dashboard/src/components/overview/overview-top-events/index.tsx +++ /dev/null @@ -1,16 +0,0 @@ -import { getConversionEventNames } from '@openpanel/db'; - -import type { OverviewTopEventsProps } from './overview-top-events'; -import OverviewTopEvents from './overview-top-events'; - -export default async function OverviewTopEventsServer({ - projectId, -}: Omit) { - const eventNames = await getConversionEventNames(projectId); - return ( - item.name)} - /> - ); -} diff --git a/apps/dashboard/src/components/page-tabs.tsx b/apps/dashboard/src/components/page-tabs.tsx deleted file mode 100644 index a17e9b64..00000000 --- a/apps/dashboard/src/components/page-tabs.tsx +++ /dev/null @@ -1,75 +0,0 @@ -'use client'; - -import { cn } from '@/utils/cn'; -import { motion } from 'framer-motion'; -import Link from 'next/link'; -import { useState } from 'react'; - -export function PageTabs({ - children, - className, -}: { - children: React.ReactNode; - className?: string; -}) { - return ( -
-
- {children} -
-
- ); -} - -export function PageTabsLink({ - href, - children, - isActive = false, -}: { - href: string; - children: React.ReactNode; - isActive?: boolean; -}) { - return ( -
- - {children} - - {isActive && ( - - )} -
- ); -} - -export function PageTabsItem({ - onClick, - children, - isActive = false, -}: { - onClick: () => void; - children: React.ReactNode; - isActive?: boolean; -}) { - return ( - - ); -} diff --git a/apps/dashboard/src/components/pagination.tsx b/apps/dashboard/src/components/pagination.tsx deleted file mode 100644 index 35a254b1..00000000 --- a/apps/dashboard/src/components/pagination.tsx +++ /dev/null @@ -1,82 +0,0 @@ -import { cn } from '@/utils/cn'; -import { - ChevronLeftIcon, - ChevronRightIcon, - ChevronsLeftIcon, - ChevronsRightIcon, -} from 'lucide-react'; -import type { Dispatch, SetStateAction } from 'react'; -import { useState } from 'react'; - -import { Button } from './ui/button'; - -export function usePagination(take: number) { - const [page, setPage] = useState(0); - return { - take, - skip: page * take, - setPage, - page, - paginate: (data: T[]): T[] => - data.slice(page * take, (page + 1) * take), - }; -} - -export function Pagination({ - take, - count, - cursor, - setCursor, - className, - size = 'base', - loading, -}: { - take: number; - count: number; - cursor: number; - setCursor: Dispatch>; - className?: string; - size?: 'sm' | 'base'; - loading?: boolean; -}) { - const lastCursor = Math.floor(count / take) - 1; - const isNextDisabled = count === 0 || lastCursor === cursor; - return ( -
- {size === 'base' && ( - - -
- ); -} diff --git a/apps/dashboard/src/components/profiles/table/index.tsx b/apps/dashboard/src/components/profiles/table/index.tsx deleted file mode 100644 index b98e3e76..00000000 --- a/apps/dashboard/src/components/profiles/table/index.tsx +++ /dev/null @@ -1,74 +0,0 @@ -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 isEqual from 'lodash.isequal'; -import { GanttChartIcon } from 'lucide-react'; -import { memo } from 'react'; -import type { Dispatch, SetStateAction } from 'react'; - -import type { IServiceProfile } from '@openpanel/db'; - -import { useColumns } from './columns'; - -type CommonProps = { - type?: 'profiles' | 'power-users'; - query: UseQueryResult; -}; -type Props = - | CommonProps - | (CommonProps & { - cursor: number; - setCursor: Dispatch>; - }); - -export const ProfilesTable = memo( - ({ type, query, ...props }: Props) => { - const columns = useColumns(type); - const { data, isFetching, isLoading } = query; - - if (isLoading) { - return ; - } - - if (data?.length === 0) { - return ( - -

Could not find any profiles

- {'cursor' in props && props.cursor !== 0 && ( - - )} -
- ); - } - - return ( - <> - - {'cursor' in props && ( - - )} - - ); - }, - (prevProps, nextProps) => { - return isEqual(prevProps.query.data, nextProps.query.data); - }, -); - -ProfilesTable.displayName = 'ProfilesTable'; diff --git a/apps/dashboard/src/components/projects/project-card.tsx b/apps/dashboard/src/components/projects/project-card.tsx deleted file mode 100644 index 8953aa05..00000000 --- a/apps/dashboard/src/components/projects/project-card.tsx +++ /dev/null @@ -1,114 +0,0 @@ -import { shortNumber } from '@/hooks/useNumerFormatter'; -import { Suspense } from 'react'; -import { escape } from 'sqlstring'; - -import type { IServiceProject } from '@openpanel/db'; -import { TABLE_NAMES, chQuery } from '@openpanel/db'; - -import { SettingsIcon } from 'lucide-react'; -import Link from 'next/link'; -import { ChartSSR } from '../chart-ssr'; -import { FadeIn } from '../fade-in'; -import { SerieIcon } from '../report-chart/common/serie-icon'; -import { LinkButton } from '../ui/button'; - -function ProjectCard({ id, domain, name, organizationId }: IServiceProject) { - // For some unknown reason I get when navigating back to this page when using - // Should be solved: https://github.com/vercel/next.js/issues/61336 - // But still get the error - return ( - - ); -} - -async function ProjectChart({ id }: { id: string }) { - const chart = await chQuery<{ value: number; date: string }>( - `SELECT - uniqHLL12(profile_id) as value, - toStartOfDay(created_at) as date - FROM ${TABLE_NAMES.sessions} - WHERE - sign = 1 AND - project_id = ${escape(id)} AND - created_at >= now() - interval '1 month' - GROUP BY date - ORDER BY date ASC - WITH FILL FROM toStartOfDay(now() - interval '1 month') - TO toStartOfDay(now()) - STEP INTERVAL 1 day - `, - ); - - return ( - - ({ ...d, date: new Date(d.date) }))} /> - - ); -} - -function Metric({ value, label }: { value: React.ReactNode; label: string }) { - return ( -
-
{label}
- {value} -
- ); -} - -async function ProjectMetrics({ id }: { id: string }) { - const [metrics] = await chQuery<{ - months_3: number; - month: number; - day: number; - }>( - ` - SELECT - uniqHLL12(if(created_at >= (now() - toIntervalMonth(6)), profile_id, null)) AS months_3, - uniqHLL12(if(created_at >= (now() - toIntervalMonth(1)), profile_id, null)) AS month, - uniqHLL12(if(created_at >= (now() - toIntervalDay(1)), profile_id, null)) AS day -FROM sessions -WHERE - project_id = ${escape(id)} AND - created_at >= (now() - toIntervalMonth(6)) - `, - ); - - return ( - - - - - - ); -} - -export default ProjectCard; diff --git a/apps/dashboard/src/components/references/table.tsx b/apps/dashboard/src/components/references/table.tsx deleted file mode 100644 index 175617d9..00000000 --- a/apps/dashboard/src/components/references/table.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { formatDate, formatDateTime } from '@/utils/date'; -import type { ColumnDef } from '@tanstack/react-table'; - -import type { IServiceReference } from '@openpanel/db'; - -export const columns: ColumnDef[] = [ - { - accessorKey: 'title', - header: 'Title', - }, - { - accessorKey: 'date', - header: 'Date', - cell({ row }) { - const date = row.original.date; - return
{formatDateTime(date)}
; - }, - }, - { - accessorKey: 'createdAt', - header: 'Created at', - cell({ row }) { - const date = row.original.createdAt; - return
{formatDate(date)}
; - }, - }, -]; diff --git a/apps/dashboard/src/components/settings-toggle.tsx b/apps/dashboard/src/components/settings-toggle.tsx deleted file mode 100644 index 2bf8a7fd..00000000 --- a/apps/dashboard/src/components/settings-toggle.tsx +++ /dev/null @@ -1,113 +0,0 @@ -'use client'; - -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { CheckIcon, MoreHorizontalIcon, PlusIcon } from 'lucide-react'; -import { useTheme } from 'next-themes'; -import * as React from 'react'; - -import { useAppParams } from '@/hooks/useAppParams'; -import { useLogout } from '@/hooks/useLogout'; -import { api } from '@/trpc/client'; -import { useRouter } from 'next/navigation'; -import { ProjectLink } from './links'; - -interface Props { - className?: string; -} - -export default function SettingsToggle({ className }: Props) { - const router = useRouter(); - const { setTheme, theme } = useTheme(); - const { projectId } = useAppParams(); - const logout = useLogout(); - - return ( - - - - - - {projectId && ( - <> - - - Create report - - - - - - - Settings - - - Organization - - - - - Project & Clients - - - - Your profile - - - References - - - - Notifications - - - - - Integrations - - - - - )} - - - Theme - {theme} - - - {['system', 'light', 'dark'].map((themeOption) => ( - setTheme(themeOption)} - className="capitalize" - > - {themeOption} - {theme === themeOption && ( - - )} - - ))} - - - - logout()}> - Logout - - - - ); -} diff --git a/apps/dashboard/src/components/settings/invites/columns.tsx b/apps/dashboard/src/components/settings/invites/columns.tsx deleted file mode 100644 index 2dbe319f..00000000 --- a/apps/dashboard/src/components/settings/invites/columns.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { TooltipComplete } from '@/components/tooltip-complete'; -import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '@/components/ui/dropdown-menu'; -import { api } from '@/trpc/client'; -import type { ColumnDef, Row } from '@tanstack/react-table'; -import { MoreHorizontalIcon } from 'lucide-react'; -import { useRouter } from 'next/navigation'; -import { pathOr } from 'ramda'; -import { toast } from 'sonner'; - -import { ACTIONS } from '@/components/data-table'; -import { clipboard } from '@/utils/clipboard'; -import type { IServiceInvite, IServiceProject } from '@openpanel/db'; - -export function useColumns( - projects: IServiceProject[], -): ColumnDef[] { - return [ - { - accessorKey: 'id', - }, - { - accessorKey: 'email', - header: 'Mail', - cell: ({ row }) => ( -
{row.original.email}
- ), - }, - { - accessorKey: 'role', - header: 'Role', - }, - { - accessorKey: 'createdAt', - header: 'Created', - cell: ({ row }) => ( - - {new Date(row.original.createdAt).toLocaleDateString()} - - ), - }, - { - accessorKey: 'projectAccess', - header: 'Access', - cell: ({ row }) => { - const access = row.original.projectAccess; - return ( - <> - {access.map((id) => { - const project = projects.find((p) => p.id === id); - if (!project) { - return ( - - Unknown - - ); - } - return ( - - {project.name} - - ); - })} - {access.length === 0 && ( - All projects - )} - - ); - }, - }, - { - id: ACTIONS, - cell: ({ row }) => { - return ; - }, - }, - ]; -} - -function ActionCell({ row }: { row: Row }) { - const router = useRouter(); - const revoke = api.organization.revokeInvite.useMutation({ - onSuccess() { - toast.success(`Invite for ${row.original.email} revoked`); - router.refresh(); - }, - onError() { - toast.error(`Failed to revoke invite for ${row.original.email}`); - }, - }); - - return ( - - - - ); -}; - -export default SignOutButton; diff --git a/apps/dashboard/src/components/ui/badge.tsx b/apps/dashboard/src/components/ui/badge.tsx deleted file mode 100644 index 0c56a752..00000000 --- a/apps/dashboard/src/components/ui/badge.tsx +++ /dev/null @@ -1,42 +0,0 @@ -'use client'; - -import { cn } from '@/utils/cn'; -import { cva } from 'class-variance-authority'; -import type { VariantProps } from 'class-variance-authority'; -import type * as React from 'react'; - -const badgeVariants = cva( - '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: { - default: - 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80', - secondary: - 'border-transparent bg-def-100 text-secondary-foreground hover:bg-def-100/80', - destructive: - 'border-transparent bg-destructive-foreground text-destructive hover:bg-destructive/80', - success: - '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: { - variant: 'default', - }, - }, -); - -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} - -function Badge({ className, variant, ...props }: BadgeProps) { - return ( -
- ); -} - -export { Badge, badgeVariants }; diff --git a/apps/dashboard/src/components/ui/button.tsx b/apps/dashboard/src/components/ui/button.tsx deleted file mode 100644 index 5e85f4ea..00000000 --- a/apps/dashboard/src/components/ui/button.tsx +++ /dev/null @@ -1,180 +0,0 @@ -'use client'; - -import { cn } from '@/utils/cn'; -import { Slot } from '@radix-ui/react-slot'; -import { cva } from 'class-variance-authority'; -import type { VariantProps } from 'class-variance-authority'; -import type { LucideIcon } from 'lucide-react'; -import { Loader2 } from 'lucide-react'; -import Link from 'next/link'; -import * as React from 'react'; - -const buttonVariants = cva( - 'inline-flex flex-shrink-0 select-none items-center justify-center whitespace-nowrap rounded-md font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 hover:translate-y-[-1px]', - { - variants: { - variant: { - default: 'bg-primary text-primary-foreground hover:bg-primary/90', - cta: 'bg-highlight text-white hover:bg-highlight/80', - destructive: - 'bg-destructive text-destructive-foreground hover:bg-destructive/90', - outline: - 'border border-input bg-card hover:bg-accent hover:text-accent-foreground', - secondary: 'bg-def-100 text-secondary-foreground hover:bg-def-100/80', - ghost: 'hover:bg-accent hover:text-accent-foreground', - link: 'text-primary underline-offset-4 hover:underline', - }, - size: { - default: 'h-10 px-4 py-2', - sm: 'h-8 rounded-md px-2', - lg: 'h-11 rounded-md px-8', - icon: 'h-8 w-8', - }, - }, - defaultVariants: { - variant: 'default', - size: 'sm', - }, - }, -); - -export interface ButtonProps - extends React.ButtonHTMLAttributes, - VariantProps { - asChild?: boolean; - loading?: boolean; - icon?: LucideIcon; - responsive?: boolean; - autoHeight?: boolean; -} - -function fixHeight({ - autoHeight, - size, -}: { autoHeight?: boolean; size: ButtonProps['size'] }) { - if (autoHeight) { - switch (size) { - case 'lg': - return 'h-auto min-h-11 py-2'; - case 'icon': - return 'h-auto min-h-8 py-1'; - case 'default': - return 'h-auto min-h-10 py-2'; - default: - return 'h-auto min-h-8 py-1'; - } - } - return ''; -} - -const Button = React.forwardRef( - ( - { - className, - variant, - size, - asChild = false, - children, - loading, - disabled, - icon, - responsive, - autoHeight, - ...props - }, - ref, - ) => { - const Comp = asChild ? Slot : 'button'; - const Icon = loading ? Loader2 : (icon ?? null); - return ( - - {Icon && ( - - )} - {responsive ? ( - {children} - ) : ( - children - )} - - ); - }, -); -Button.displayName = 'Button'; -Button.defaultProps = { - type: 'button', -}; -export interface LinkButtonProps - extends React.AnchorHTMLAttributes, - VariantProps { - asChild?: boolean; - loading?: boolean; - icon?: LucideIcon; - responsive?: boolean; - href: string; - prefetch?: boolean; -} - -const LinkButton = React.forwardRef< - typeof Link, - React.PropsWithoutRef ->( - ( - { - className, - variant, - size, - children, - loading, - icon, - responsive, - href, - ...props - }, - ref, - ) => { - const Icon = loading ? Loader2 : (icon ?? null); - return ( - - {Icon && ( - - )} - {responsive ? ( - {children} - ) : ( - children - )} - - ); - }, -); -LinkButton.displayName = 'LinkButton'; - -export { Button, LinkButton, buttonVariants }; diff --git a/apps/dashboard/src/components/ui/calendar.tsx b/apps/dashboard/src/components/ui/calendar.tsx deleted file mode 100644 index 5f07ea35..00000000 --- a/apps/dashboard/src/components/ui/calendar.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { buttonVariants } from '@/components/ui/button'; -import { cn } from '@/utils/cn'; -import { ChevronLeft, ChevronRight } from 'lucide-react'; -import type * as React from 'react'; -import { DayPicker } from 'react-day-picker'; - -export type CalendarProps = React.ComponentProps; - -function Calendar({ - className, - classNames, - showOutsideDays = true, - ...props -}: CalendarProps) { - return ( - , - IconRight: ({ ...props }) => , - }} - {...props} - /> - ); -} -Calendar.displayName = 'Calendar'; - -export { Calendar }; diff --git a/apps/dashboard/src/components/ui/command.tsx b/apps/dashboard/src/components/ui/command.tsx deleted file mode 100644 index 8c7d2b81..00000000 --- a/apps/dashboard/src/components/ui/command.tsx +++ /dev/null @@ -1,151 +0,0 @@ -'use client'; - -import { Dialog, DialogContent } from '@/components/ui/dialog'; -import { cn } from '@/utils/cn'; -import type { DialogProps } from '@radix-ui/react-dialog'; -import { Command as CommandPrimitive } from 'cmdk'; -import { Search } from 'lucide-react'; -import * as React from 'react'; - -const Command = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -Command.displayName = CommandPrimitive.displayName; - -type CommandDialogProps = DialogProps; - -const CommandDialog = ({ children, ...props }: CommandDialogProps) => { - return ( - - - - {children} - - - - ); -}; - -const CommandInput = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( -
- - -
-)); - -CommandInput.displayName = CommandPrimitive.Input.displayName; - -const CommandList = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); - -CommandList.displayName = CommandPrimitive.List.displayName; - -const CommandEmpty = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->((props, ref) => ( - -)); - -CommandEmpty.displayName = CommandPrimitive.Empty.displayName; - -const CommandGroup = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); - -CommandGroup.displayName = CommandPrimitive.Group.displayName; - -const CommandSeparator = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -CommandSeparator.displayName = CommandPrimitive.Separator.displayName; - -const CommandItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); - -CommandItem.displayName = CommandPrimitive.Item.displayName; - -const CommandShortcut = ({ - className, - ...props -}: React.HTMLAttributes) => { - return ( - - ); -}; -CommandShortcut.displayName = 'CommandShortcut'; - -export { - Command, - CommandDialog, - CommandInput, - CommandList, - CommandEmpty, - CommandGroup, - CommandItem, - CommandShortcut, - CommandSeparator, -}; diff --git a/apps/dashboard/src/components/ui/dialog.tsx b/apps/dashboard/src/components/ui/dialog.tsx deleted file mode 100644 index bda48607..00000000 --- a/apps/dashboard/src/components/ui/dialog.tsx +++ /dev/null @@ -1,120 +0,0 @@ -'use client'; - -import { cn } from '@/utils/cn'; -import * as DialogPrimitive from '@radix-ui/react-dialog'; -import { X } from 'lucide-react'; -import * as React from 'react'; - -const Dialog = DialogPrimitive.Root; - -const DialogTrigger = DialogPrimitive.Trigger; - -const DialogPortal = DialogPrimitive.Portal; - -const DialogClose = DialogPrimitive.Close; - -const DialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; - -const DialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - onClose?: () => void; - } ->(({ className, children, onClose, ...props }, ref) => ( - - - - {children} - - -)); -DialogContent.displayName = DialogPrimitive.Content.displayName; - -const DialogHeader = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-); -DialogHeader.displayName = 'DialogHeader'; - -const DialogFooter = ({ - className, - ...props -}: React.HTMLAttributes) => ( -
-); -DialogFooter.displayName = 'DialogFooter'; - -const DialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DialogTitle.displayName = DialogPrimitive.Title.displayName; - -const DialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -DialogDescription.displayName = DialogPrimitive.Description.displayName; - -export { - Dialog, - DialogPortal, - DialogOverlay, - DialogClose, - DialogTrigger, - DialogContent, - DialogHeader, - DialogFooter, - DialogTitle, - DialogDescription, -}; diff --git a/apps/dashboard/src/components/ui/scroll-area.tsx b/apps/dashboard/src/components/ui/scroll-area.tsx deleted file mode 100644 index 2775243e..00000000 --- a/apps/dashboard/src/components/ui/scroll-area.tsx +++ /dev/null @@ -1,56 +0,0 @@ -'use client'; - -import { cn } from '@/utils/cn'; -import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area'; -import * as React from 'react'; - -const ScrollArea = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & { - orientation?: 'vertical' | 'horizontal'; - } ->(({ className, children, orientation = 'vertical', ...props }, ref) => ( - - - {children} - - - - -)); -ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName; - -const ScrollBar = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, orientation = 'vertical', ...props }, ref) => ( - - - -)); -ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName; - -export { ScrollArea, ScrollBar }; diff --git a/apps/dashboard/src/components/ui/table.tsx b/apps/dashboard/src/components/ui/table.tsx deleted file mode 100644 index 307c984e..00000000 --- a/apps/dashboard/src/components/ui/table.tsx +++ /dev/null @@ -1,155 +0,0 @@ -'use client'; - -import { cn } from '@/utils/cn'; -import * as React from 'react'; - -const Table = React.forwardRef< - HTMLTableElement, - React.HTMLAttributes & { - overflow?: boolean; - } ->(({ className, overflow = true, ...props }, ref) => ( -
-
- - - -)); -Table.displayName = 'Table'; - -const TableHeader = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - -)); -TableHeader.displayName = 'TableHeader'; - -const TableBody = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - -)); -TableBody.displayName = 'TableBody'; - -const TableFooter = React.forwardRef< - HTMLTableSectionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - -)); -TableFooter.displayName = 'TableFooter'; - -const TableRow = React.forwardRef< - HTMLTableRowElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( - -)); -TableRow.displayName = 'TableRow'; - -const TableHead = React.forwardRef< - HTMLTableCellElement, - React.ThHTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); -TableHead.displayName = 'TableHead'; - -const TableCell = React.forwardRef< - HTMLTableCellElement, - React.TdHTMLAttributes ->(({ className, ...props }, ref) => ( - -)); -TableCell.displayName = 'TableCell'; - -const TableCaption = React.forwardRef< - HTMLTableCaptionElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)); -TableCaption.displayName = 'TableCaption'; - -export function TableSkeleton({ - rows = 10, - cols = 2, -}: { - rows?: number; - cols?: number; -}) { - return ( - - - - {Array.from({ length: cols }).map((_, j) => ( - -
- - ))} - - - - {Array.from({ length: rows }).map((_, i) => ( - - {Array.from({ length: cols }).map((_, j) => ( - -
- - ))} - - ))} - -
- ); -} - -export { - Table, - TableHeader, - TableBody, - TableFooter, - TableHead, - TableRow, - TableCell, - TableCaption, -}; diff --git a/apps/dashboard/src/env.mjs b/apps/dashboard/src/env.mjs deleted file mode 100644 index b25d1ae3..00000000 --- a/apps/dashboard/src/env.mjs +++ /dev/null @@ -1,47 +0,0 @@ -import { createEnv } from '@t3-oss/env-nextjs'; -import { z } from 'zod'; - -export const env = createEnv({ - /** - * Specify your server-side environment variables schema here. This way you can ensure the app - * isn't built with invalid env vars. - */ - server: { - DATABASE_URL: z - .string() - .url() - .refine( - (str) => !str.includes('DATABASE_URL'), - 'You forgot to change the default URL', - ), - NODE_ENV: z - .enum(['development', 'test', 'production']) - .default('development'), - }, - - /** - * Specify your client-side environment variables schema here. This way you can ensure the app - * isn't built with invalid env vars. To expose them to the client, prefix them with - * `NEXT_PUBLIC_`. - */ - client: {}, - - /** - * You can't destruct `process.env` as a regular object in the Next.js edge runtimes (e.g. - * middlewares) or client-side so we need to destruct manually. - */ - runtimeEnv: { - DATABASE_URL: process.env.DATABASE_URL, - NODE_ENV: process.env.NODE_ENV, - }, - /** - * Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially - * useful for Docker builds. - */ - skipValidation: !!process.env.SKIP_ENV_VALIDATION, - /** - * Makes it so that empty strings are treated as undefined. - * `SOME_VAR: z.string()` and `SOME_VAR=''` will throw an error. - */ - emptyStringAsUndefined: true, -}); diff --git a/apps/dashboard/src/hocs/with-loading-widget.tsx b/apps/dashboard/src/hocs/with-loading-widget.tsx deleted file mode 100644 index aa2972fb..00000000 --- a/apps/dashboard/src/hocs/with-loading-widget.tsx +++ /dev/null @@ -1,37 +0,0 @@ -import { Widget, WidgetHead } from '@/components/widget'; -import { cn } from '@/utils/cn'; -import { Suspense } from 'react'; - -const withLoadingWidget = (Component: React.ComponentType

) => { - const WithLoadingWidget: React.ComponentType

= (props) => { - return ( - - - Loading... - -

- - } - > - - - ); - }; - - WithLoadingWidget.displayName = `WithLoadingWidget(${Component.displayName})`; - - return WithLoadingWidget; -}; - -export default withLoadingWidget; diff --git a/apps/dashboard/src/hocs/with-suspense.tsx b/apps/dashboard/src/hocs/with-suspense.tsx deleted file mode 100644 index 3fb92273..00000000 --- a/apps/dashboard/src/hocs/with-suspense.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import { Suspense } from 'react'; - -const withSuspense = ( - Component: React.ComponentType

, - Fallback: React.ComponentType

, -) => { - const WithSuspense: React.ComponentType

= (props) => { - const fallback = ; - // return <>{fallback}; - return ( - - - - ); - }; - - WithSuspense.displayName = `WithSuspense(${Component.displayName})`; - - return WithSuspense; -}; - -export default withSuspense; diff --git a/apps/dashboard/src/hooks/useAppParams.ts b/apps/dashboard/src/hooks/useAppParams.ts deleted file mode 100644 index 83e3de54..00000000 --- a/apps/dashboard/src/hooks/useAppParams.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { useParams } from 'next/navigation'; - -type AppParams = { - organizationId: string; - projectId: string; -}; - -export function useAppParams() { - const params = useParams(); - return { - ...(params ?? {}), - organizationId: params?.organizationSlug, - projectId: params?.projectId, - } as T & AppParams; -} diff --git a/apps/dashboard/src/hooks/useAuth.tsx b/apps/dashboard/src/hooks/useAuth.tsx deleted file mode 100644 index 43504add..00000000 --- a/apps/dashboard/src/hooks/useAuth.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { api } from '@/trpc/client'; - -export function useAuth() { - return api.auth.session.useQuery(); -} diff --git a/apps/dashboard/src/hooks/useCursor.ts b/apps/dashboard/src/hooks/useCursor.ts deleted file mode 100644 index 12e890eb..00000000 --- a/apps/dashboard/src/hooks/useCursor.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { parseAsInteger, useQueryState } from 'nuqs'; -import { useTransition } from 'react'; - -import { useDebounceValue } from './useDebounceValue'; - -export function useCursor() { - const [loading, startTransition] = useTransition(); - const [cursor, setCursor] = useQueryState( - 'cursor', - parseAsInteger - .withOptions({ shallow: false, history: 'push', startTransition }) - .withDefault(0), - ); - return { - cursor, - setCursor, - loading, - }; -} - -export type UseDebouncedCursor = ReturnType; - -export function useDebouncedCursor() { - const [cursor, setCursor] = useQueryState( - 'cursor', - parseAsInteger.withDefault(0), - ); - const debouncedCursor = useDebounceValue(cursor, 200); - return { - value: cursor, - set: setCursor, - debounced: debouncedCursor, - }; -} diff --git a/apps/dashboard/src/hooks/useEventNames.ts b/apps/dashboard/src/hooks/useEventNames.ts deleted file mode 100644 index f98901c9..00000000 --- a/apps/dashboard/src/hooks/useEventNames.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { api } from '@/trpc/client'; - -export function useEventNames( - params: Parameters[0], -) { - const query = api.chart.events.useQuery(params, { - staleTime: 1000 * 60 * 10, - }); - return query.data ?? []; -} diff --git a/apps/dashboard/src/hooks/useEventProperties.ts b/apps/dashboard/src/hooks/useEventProperties.ts deleted file mode 100644 index 2b256db6..00000000 --- a/apps/dashboard/src/hooks/useEventProperties.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { RouterInputs } from '@/trpc/client'; -import { api } from '@/trpc/client'; -import type { UseQueryOptions } from '@tanstack/react-query'; - -export function useEventProperties( - params: RouterInputs['chart']['properties'], - options?: UseQueryOptions, -): string[] { - const query = api.chart.properties.useQuery(params, { - staleTime: 1000 * 60 * 10, - enabled: options?.enabled ?? true, - }); - - return query.data ?? []; -} diff --git a/apps/dashboard/src/hooks/useLogout.ts b/apps/dashboard/src/hooks/useLogout.ts deleted file mode 100644 index 6b7cda6b..00000000 --- a/apps/dashboard/src/hooks/useLogout.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { api } from '@/trpc/client'; -import { useRouter } from 'next/navigation'; - -export function useLogout() { - const router = useRouter(); - const signOut = api.auth.signOut.useMutation({ - onSuccess() { - setTimeout(() => { - router.push('/login'); - }, 0); - }, - }); - - return () => signOut.mutate(); -} diff --git a/apps/dashboard/src/hooks/useMappings.ts b/apps/dashboard/src/hooks/useMappings.ts deleted file mode 100644 index c32a1b4e..00000000 --- a/apps/dashboard/src/hooks/useMappings.ts +++ /dev/null @@ -1,13 +0,0 @@ -import mappings from '@/mappings.json'; - -export function useMappings() { - return (val: string | string[]): string => { - if (Array.isArray(val)) { - return val - .map((v) => mappings.find((item) => item.id === v)?.name ?? v) - .join(''); - } - - return mappings.find((item) => item.id === val)?.name ?? val; - }; -} diff --git a/apps/dashboard/src/hooks/useProfileProperties.ts b/apps/dashboard/src/hooks/useProfileProperties.ts deleted file mode 100644 index f61ef005..00000000 --- a/apps/dashboard/src/hooks/useProfileProperties.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { api } from '@/trpc/client'; - -export function useProfileProperties(projectId: string) { - const query = api.profile.properties.useQuery({ - projectId: projectId, - }); - - return query.data ?? []; -} diff --git a/apps/dashboard/src/hooks/useProfileValues.ts b/apps/dashboard/src/hooks/useProfileValues.ts deleted file mode 100644 index d850f6fa..00000000 --- a/apps/dashboard/src/hooks/useProfileValues.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { api } from '@/trpc/client'; - -export function useProfileValues(projectId: string, property: string) { - const query = api.profile.values.useQuery( - { - projectId: projectId, - property, - }, - { - staleTime: 1000 * 60 * 10, - }, - ); - - return query.data?.values ?? []; -} diff --git a/apps/dashboard/src/hooks/usePropertyValues.ts b/apps/dashboard/src/hooks/usePropertyValues.ts deleted file mode 100644 index 227b8e12..00000000 --- a/apps/dashboard/src/hooks/usePropertyValues.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { api } from '@/trpc/client'; - -export function usePropertyValues( - params: Parameters[0], -) { - const query = api.chart.values.useQuery(params, { - staleTime: 1000 * 60 * 10, - }); - - return query.data?.values ?? []; -} diff --git a/apps/dashboard/src/instrumentation.ts b/apps/dashboard/src/instrumentation.ts deleted file mode 100644 index c8c76462..00000000 --- a/apps/dashboard/src/instrumentation.ts +++ /dev/null @@ -1,10 +0,0 @@ -export async function register() { - if (process.env.NEXT_RUNTIME === 'nodejs' && process.env.HYPERDX_API_KEY) { - const { initSDK } = await import('@hyperdx/node-opentelemetry'); - initSDK({ - consoleCapture: true, - apiKey: process.env.HYPERDX_API_KEY, - service: 'dashboard', - }); - } -} diff --git a/apps/dashboard/src/mappings.json b/apps/dashboard/src/mappings.json deleted file mode 100644 index f2d7122d..00000000 --- a/apps/dashboard/src/mappings.json +++ /dev/null @@ -1,6 +0,0 @@ -[ - { - "id": "123", - "name": "123" - } -] diff --git a/apps/dashboard/src/middleware.ts b/apps/dashboard/src/middleware.ts deleted file mode 100644 index d7154a5b..00000000 --- a/apps/dashboard/src/middleware.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { COOKIE_MAX_AGE, COOKIE_OPTIONS } from '@openpanel/auth/constants'; -import { type NextRequest, NextResponse } from 'next/server'; - -function createRouteMatcher(patterns: string[]) { - // Convert route patterns to regex patterns - const regexPatterns = patterns.map((pattern) => { - // Replace route parameters (:id) with regex capture groups - const regexPattern = pattern - .replace(/\//g, '\\/') // Escape forward slashes - .replace(/:\w+/g, '([^/]+)') // Convert :param to capture groups - .replace(/\(\.\*\)\?/g, '(?:.*)?'); // Handle optional wildcards - - return new RegExp(`^${regexPattern}$`); - }); - - // Return a matcher function - return (req: { url: string }) => { - const pathname = new URL(req.url).pathname; - return regexPatterns.some((regex) => regex.test(pathname)); - }; -} - -// This example protects all routes including api/trpc routes -// Please edit this to allow other routes to be public as needed. -const isPublicRoute = createRouteMatcher([ - '/share/overview/:id', - '/login(.*)?', - '/reset-password(.*)?', - '/sso-callback(.*)?', - '/onboarding', - '/maintenance', - '/api/headers', -]); - -export default (request: NextRequest) => { - // Check for maintenance mode - if ( - process.env.MAINTENANCE === 'true' && - !request.nextUrl.pathname.startsWith('/maintenance') - ) { - return NextResponse.redirect(new URL('/maintenance', request.url)); - } - - if (request.method === 'GET') { - const response = NextResponse.next(); - const token = request.cookies.get('session')?.value ?? null; - - if (process.env.DEMO_USER_ID) { - return response; - } - - if (!isPublicRoute(request) && token === null) { - return NextResponse.redirect(new URL('/login', request.url)); - } - - if (token !== null) { - // Only extend cookie expiration on GET requests since we can be sure - // a new session wasn't set when handling the request. - response.cookies.set('session', token, { - maxAge: COOKIE_MAX_AGE, - ...COOKIE_OPTIONS, - }); - } - return response; - } - - const originHeader = request.headers.get('Origin'); - // NOTE: You may need to use `X-Forwarded-Host` instead - const hostHeader = request.headers.get('Host'); - if (originHeader === null || hostHeader === null) { - return new NextResponse(null, { - status: 403, - }); - } - let origin: URL; - try { - origin = new URL(originHeader); - } catch { - return new NextResponse(null, { - status: 403, - }); - } - if (origin.host !== hostHeader) { - return new NextResponse(null, { - status: 403, - }); - } - - return NextResponse.next(); -}; - -export const config = { - matcher: [ - '/((?!.+\\.[\\w]+$|_next).*)', - '/', - '/(api)(.*)', - '/(api|trpc)(.*)', - '/api/trpc(.*)', - ], -}; diff --git a/apps/dashboard/src/modals/SaveReport.tsx b/apps/dashboard/src/modals/SaveReport.tsx deleted file mode 100644 index e5b861e3..00000000 --- a/apps/dashboard/src/modals/SaveReport.tsx +++ /dev/null @@ -1,150 +0,0 @@ -import { ButtonContainer } from '@/components/button-container'; -import { InputWithLabel } from '@/components/forms/input-with-label'; -import { Button } from '@/components/ui/button'; -import { Combobox } from '@/components/ui/combobox'; -import { Label } from '@/components/ui/label'; -import { useAppParams } from '@/hooks/useAppParams'; -import { api, handleError } from '@/trpc/client'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useRouter, useSearchParams } from 'next/navigation'; -import { Controller, useForm } from 'react-hook-form'; -import { toast } from 'sonner'; -import { z } from 'zod'; - -import type { IChartProps } from '@openpanel/validation'; - -import { popModal } from '.'; -import { ModalContent, ModalHeader } from './Modal/Container'; - -type SaveReportProps = { - report: IChartProps; - disableRedirect?: boolean; -}; - -const validator = z.object({ - name: z.string().min(1, 'Required'), - dashboardId: z.string().min(1, 'Required'), -}); - -type IForm = z.infer; - -export default function SaveReport({ - report, - disableRedirect, -}: SaveReportProps) { - const router = useRouter(); - const { organizationId, projectId } = useAppParams(); - const searchParams = useSearchParams(); - const dashboardId = searchParams?.get('dashboardId') ?? undefined; - - const save = api.report.create.useMutation({ - onError: handleError, - onSuccess(res) { - const goToReport = () => { - router.push( - `/${organizationId}/${projectId}/reports/${ - res.id - }?${searchParams?.toString()}`, - ); - }; - - toast('Report created', { - description: `${res.name}`, - action: { - label: 'View report', - onClick: () => goToReport(), - }, - }); - - if (!disableRedirect) { - goToReport(); - } - - popModal(); - }, - }); - - const { register, handleSubmit, formState, control, setValue } = - useForm({ - resolver: zodResolver(validator), - defaultValues: { - name: report.name, - dashboardId, - }, - }); - - const dashboardMutation = api.dashboard.create.useMutation({ - onError: handleError, - onSuccess(res) { - setValue('dashboardId', res.id); - dashboardQuery.refetch(); - toast('Success', { - description: 'Dashboard created.', - }); - }, - }); - - const dashboardQuery = api.dashboard.list.useQuery({ - projectId, - }); - const dashboards = (dashboardQuery.data ?? []).map((item) => ({ - value: item.id, - label: item.name, - })); - - return ( - - -

{ - save.mutate({ - report: { - ...report, - name, - }, - ...values, - }); - })} - > - - { - return ( -
- - { - dashboardMutation.mutate({ - projectId, - name: value, - }); - }} - /> -
- ); - }} - /> - - - - - - - ); -} diff --git a/apps/dashboard/src/modals/Testimonial.tsx b/apps/dashboard/src/modals/Testimonial.tsx deleted file mode 100644 index 950a36a5..00000000 --- a/apps/dashboard/src/modals/Testimonial.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { Button } from '@/components/ui/button'; -import { Textarea } from '@/components/ui/textarea'; -import { useAppParams } from '@/hooks/useAppParams'; -import { api } from '@/trpc/client'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { useForm } from 'react-hook-form'; -import { toast } from 'sonner'; -import { z } from 'zod'; - -import { useOpenPanel } from '@openpanel/nextjs'; - -import { popModal } from '.'; -import { ModalContent } from './Modal/Container'; - -const validator = z.object({ - body: z.string().min(3), -}); - -type IForm = z.infer; - -const Testimonial = () => { - const op = useOpenPanel(); - const mutation = api.ticket.create.useMutation(); - const params = useAppParams(); - const form = useForm({ - resolver: zodResolver(validator), - }); - return ( - -
-

Review time 🫶

-

- Thank you so much for using Openpanel — it truly means a great deal to - me! If you're enjoying your experience, I'd be thrilled if - you could leave a quick review. 😇 -

-

- If you have any feedback or suggestions, I'd love to hear them as - well! 🚀 -

-
-
{ - try { - await mutation.mutateAsync({ - subject: 'New testimonial', - body, - meta: { - ...params, - }, - }); - toast.success('Thanks for your feedback 🚀'); - op.track('testimonials_sent'); - popModal(); - } catch (e) { - toast.error('Something went wrong. Please try again later.'); - } - })} - > -