chore(root): migrate to biome

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-09-16 12:20:40 +02:00
parent 1f6e198336
commit 32e91959f6
383 changed files with 1943 additions and 3085 deletions

View File

@@ -7,4 +7,6 @@ node_modules
npm-debug.log npm-debug.log
README.md README.md
.next .next
.git .git
tmp
converage

View File

@@ -1,7 +1,3 @@
{ {
"recommendations": [ "recommendations": ["yoavbls.pretty-ts-errors"]
"dbaeumer.vscode-eslint",
"esbenp.prettier-vscode",
"yoavbls.pretty-ts-errors"
]
} }

35
.vscode/settings.json vendored
View File

@@ -1,21 +1,34 @@
{ {
"editor.codeActionsOnSave": { "prettier.enable": false,
"source.fixAll.eslint": "explicit" "eslint.enable": false,
"files.associations": {
"*.css": "tailwindcss"
},
"editor.codeActionsOnSave": {
"source.organizeImports.biome": "explicit"
},
"[prisma]": {
"editor.defaultFormatter": "Prisma.prisma"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[json]": {
"editor.defaultFormatter": "biomejs.biome"
}, },
"editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"eslint.workingDirectories": [ "tailwindCSS.experimental.configFile": "./packages/ui/tailwind.config.ts",
{ "pattern": "apps/*/" }, "tailwindCSS.experimental.classRegex": [
{ "pattern": "packages/*/" }, ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
{ "pattern": "tooling/*/" } ["cx\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
], ],
"typescript.enablePromptUseWorkspaceTsdk": true, "typescript.enablePromptUseWorkspaceTsdk": true,
"typescript.tsdk": "node_modules/typescript/lib", "typescript.tsdk": "node_modules/typescript/lib",
"typescript.preferences.autoImportFileExcludePatterns": [ "typescript.preferences.autoImportFileExcludePatterns": [
"next/router.d.ts", "next/router.d.ts",
"next/dist/client/router.d.ts" "next/dist/client/router.d.ts"
], ]
"[sql]": {
"editor.defaultFormatter": "adpyke.vscode-sql-formatter"
}
} }

View File

@@ -38,9 +38,6 @@ COPY packages/constants/package.json packages/constants/
COPY packages/validation/package.json packages/validation/ COPY packages/validation/package.json packages/validation/
COPY packages/sdks/sdk/package.json packages/sdks/sdk/ COPY packages/sdks/sdk/package.json packages/sdks/sdk/
# Patches
COPY patches patches
# BUILD # BUILD
FROM base AS build FROM base AS build

View File

@@ -6,8 +6,6 @@
"testing": "API_PORT=3333 pnpm dev", "testing": "API_PORT=3333 pnpm dev",
"start": "node dist/index.js", "start": "node dist/index.js",
"build": "rm -rf dist && tsup", "build": "rm -rf dist && tsup",
"lint": "eslint .",
"format": "prettier --check \"**/*.{mjs,ts,md,json}\"",
"typecheck": "tsc --noEmit" "typecheck": "tsc --noEmit"
}, },
"dependencies": { "dependencies": {
@@ -40,8 +38,6 @@
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@openpanel/eslint-config": "workspace:*",
"@openpanel/prettier-config": "workspace:*",
"@openpanel/sdk": "workspace:*", "@openpanel/sdk": "workspace:*",
"@openpanel/tsconfig": "workspace:*", "@openpanel/tsconfig": "workspace:*",
"@types/jsonwebtoken": "^9.0.6", "@types/jsonwebtoken": "^9.0.6",
@@ -51,16 +47,7 @@
"@types/ua-parser-js": "^0.7.39", "@types/ua-parser-js": "^0.7.39",
"@types/uuid": "^9.0.8", "@types/uuid": "^9.0.8",
"@types/ws": "^8.5.10", "@types/ws": "^8.5.10",
"eslint": "^8.48.0",
"prettier": "^3.0.3",
"tsup": "^7.2.0", "tsup": "^7.2.0",
"typescript": "^5.2.2" "typescript": "^5.2.2"
}, }
"eslintConfig": { }
"root": true,
"extends": [
"@openpanel/eslint-config/base"
]
},
"prettier": "@openpanel/prettier-config"
}

View File

@@ -1,5 +1,5 @@
import fs from 'fs'; import fs from 'node:fs';
import path from 'path'; import path from 'node:path';
function transform(data: any) { function transform(data: any) {
const obj: Record<string, unknown> = {}; const obj: Record<string, unknown> = {};
@@ -22,7 +22,7 @@ async function main() {
// Get document, or throw exception on error // Get document, or throw exception on error
try { try {
const data = await fetch( const data = await fetch(
'https://s3-eu-west-1.amazonaws.com/snowplow-hosted-assets/third-party/referer-parser/referers-latest.json' 'https://s3-eu-west-1.amazonaws.com/snowplow-hosted-assets/third-party/referer-parser/referers-latest.json',
).then((res) => res.json()); ).then((res) => res.json());
fs.writeFileSync( fs.writeFileSync(
@@ -34,11 +34,11 @@ async function main() {
`// The orginal referers.yml is based on Piwik's SearchEngines.php and Socials.php, copyright 2012 Matthieu Aubry and available under the GNU General Public License v3.`, `// The orginal referers.yml is based on Piwik's SearchEngines.php and Socials.php, copyright 2012 Matthieu Aubry and available under the GNU General Public License v3.`,
'', '',
`const referrers: Record<string, { type: string, name: string }> = ${JSON.stringify( `const referrers: Record<string, { type: string, name: string }> = ${JSON.stringify(
transform(data) transform(data),
)} as const;`, )} as const;`,
'export default referrers;', 'export default referrers;',
].join('\n'), ].join('\n'),
'utf-8' 'utf-8',
); );
} catch (e) { } catch (e) {
console.log(e); console.log(e);

View File

@@ -1,8 +1,8 @@
import { ch, chQuery, TABLE_NAMES } from '@openpanel/db'; import { TABLE_NAMES, ch, chQuery } from '@openpanel/db';
async function main() { async function main() {
const projects = await chQuery( const projects = await chQuery(
`SELECT distinct project_id FROM ${TABLE_NAMES.events} ORDER BY project_id` `SELECT distinct project_id FROM ${TABLE_NAMES.events} ORDER BY project_id`,
); );
const withOrigin = []; const withOrigin = [];
@@ -10,10 +10,10 @@ async function main() {
try { try {
const [eventWithOrigin, eventWithoutOrigin] = await Promise.all([ const [eventWithOrigin, eventWithoutOrigin] = await Promise.all([
await chQuery( await chQuery(
`SELECT * FROM ${TABLE_NAMES.events} WHERE origin != '' AND project_id = '${project.project_id}' ORDER BY created_at DESC LIMIT 1` `SELECT * FROM ${TABLE_NAMES.events} WHERE origin != '' AND project_id = '${project.project_id}' ORDER BY created_at DESC LIMIT 1`,
), ),
await chQuery( await chQuery(
`SELECT * FROM ${TABLE_NAMES.events} WHERE origin = '' AND project_id = '${project.project_id}' AND path != '' ORDER BY created_at DESC LIMIT 1` `SELECT * FROM ${TABLE_NAMES.events} WHERE origin = '' AND project_id = '${project.project_id}' AND path != '' ORDER BY created_at DESC LIMIT 1`,
), ),
]); ]);
@@ -22,7 +22,7 @@ async function main() {
console.log(`- Origin: ${eventWithOrigin[0].origin}`); console.log(`- Origin: ${eventWithOrigin[0].origin}`);
withOrigin.push(project.project_id); withOrigin.push(project.project_id);
const events = await chQuery( const events = await chQuery(
`SELECT count(*) as count FROM ${TABLE_NAMES.events} WHERE project_id = '${project.project_id}' AND path != '' AND origin = ''` `SELECT count(*) as count FROM ${TABLE_NAMES.events} WHERE project_id = '${project.project_id}' AND path != '' AND origin = ''`,
); );
console.log(`🤠🤠🤠🤠 Will update ${events[0]?.count} events`); console.log(`🤠🤠🤠🤠 Will update ${events[0]?.count} events`);
await ch.command({ await ch.command({
@@ -35,20 +35,20 @@ async function main() {
if (!eventWithOrigin[0] && eventWithoutOrigin[0]) { if (!eventWithOrigin[0] && eventWithoutOrigin[0]) {
console.log( console.log(
`😧 Project ${project.project_id} has no events with origin (last event ${eventWithoutOrigin[0].created_at})` `😧 Project ${project.project_id} has no events with origin (last event ${eventWithoutOrigin[0].created_at})`,
); );
console.log('- NO ORIGIN'); console.log('- NO ORIGIN');
} }
if (!eventWithOrigin[0] && !eventWithoutOrigin[0]) { if (!eventWithOrigin[0] && !eventWithoutOrigin[0]) {
console.log( console.log(
`🔥 WARNING: Project ${project.project_id} has no events at all?!?!?!` `🔥 WARNING: Project ${project.project_id} has no events at all?!?!?!`,
); );
} }
if (eventWithOrigin[0] && !eventWithoutOrigin[0]) { if (eventWithOrigin[0] && !eventWithoutOrigin[0]) {
console.log( console.log(
`✅ Project ${project.project_id} has all events with origin!!!` `✅ Project ${project.project_id} has all events with origin!!!`,
); );
} }
console.log(''); console.log('');

View File

@@ -1,7 +1,7 @@
import { getClientIp, parseIp } from '@/utils/parseIp'; import { getClientIp, parseIp } from '@/utils/parseIp';
import type { FastifyReply, FastifyRequest } from 'fastify'; import type { FastifyReply, FastifyRequest } from 'fastify';
import { generateDeviceId } from '@openpanel/common'; import { generateDeviceId } from '@openpanel/common/server';
import { getSalts } from '@openpanel/db'; import { getSalts } from '@openpanel/db';
import { eventsQueue } from '@openpanel/queue'; import { eventsQueue } from '@openpanel/queue';
import { getRedisCache } from '@openpanel/redis'; import { getRedisCache } from '@openpanel/redis';
@@ -13,7 +13,7 @@ export async function postEvent(
request: FastifyRequest<{ request: FastifyRequest<{
Body: PostEventPayload; Body: PostEventPayload;
}>, }>,
reply: FastifyReply reply: FastifyReply,
) { ) {
const ip = getClientIp(request)!; const ip = getClientIp(request)!;
const ua = request.headers['user-agent']!; const ua = request.headers['user-agent']!;
@@ -44,7 +44,7 @@ export async function postEvent(
'locked', 'locked',
'EX', 'EX',
10, 10,
'NX' 'NX',
); );
eventsQueue.add('event', { eventsQueue.add('event', {

View File

@@ -19,7 +19,7 @@ async function getProjectId(
projectId?: string; projectId?: string;
}; };
}>, }>,
reply: FastifyReply reply: FastifyReply,
) { ) {
let projectId = request.query.projectId || request.query.project_id; let projectId = request.query.projectId || request.query.project_id;
@@ -77,7 +77,7 @@ const eventsScheme = z.object({
includes: z includes: z
.preprocess( .preprocess(
(arg) => (typeof arg === 'string' ? [arg] : arg), (arg) => (typeof arg === 'string' ? [arg] : arg),
z.array(z.string()) z.array(z.string()),
) )
.optional(), .optional(),
}); });
@@ -86,7 +86,7 @@ export async function events(
request: FastifyRequest<{ request: FastifyRequest<{
Querystring: z.infer<typeof eventsScheme>; Querystring: z.infer<typeof eventsScheme>;
}>, }>,
reply: FastifyReply reply: FastifyReply,
) { ) {
const query = eventsScheme.safeParse(request.query); const query = eventsScheme.safeParse(request.query);
@@ -118,7 +118,7 @@ export async function events(
meta: false, meta: false,
...query.data.includes?.reduce( ...query.data.includes?.reduce(
(acc, key) => ({ ...acc, [key]: true }), (acc, key) => ({ ...acc, [key]: true }),
{} {},
), ),
}, },
}; };
@@ -154,7 +154,7 @@ export async function charts(
request: FastifyRequest<{ request: FastifyRequest<{
Querystring: Record<string, string>; Querystring: Record<string, string>;
}>, }>,
reply: FastifyReply reply: FastifyReply,
) { ) {
const query = chartSchemeFull.safeParse(parseQueryString(request.query)); const query = chartSchemeFull.safeParse(parseQueryString(request.query));

View File

@@ -2,13 +2,13 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
import { toDots } from '@openpanel/common'; import { toDots } from '@openpanel/common';
import type { IClickhouseEvent } from '@openpanel/db'; import type { IClickhouseEvent } from '@openpanel/db';
import { ch, formatClickhouseDate, TABLE_NAMES } from '@openpanel/db'; import { TABLE_NAMES, ch, formatClickhouseDate } from '@openpanel/db';
export async function importEvents( export async function importEvents(
request: FastifyRequest<{ request: FastifyRequest<{
Body: IClickhouseEvent[]; Body: IClickhouseEvent[];
}>, }>,
reply: FastifyReply reply: FastifyReply,
) { ) {
const importedAt = formatClickhouseDate(new Date()); const importedAt = formatClickhouseDate(new Date());
const values: IClickhouseEvent[] = request.body.map((event) => { const values: IClickhouseEvent[] = request.body.map((event) => {

View File

@@ -6,11 +6,11 @@ import type * as WebSocket from 'ws';
import { getSuperJson } from '@openpanel/common'; import { getSuperJson } from '@openpanel/common';
import type { IServiceEvent } from '@openpanel/db'; import type { IServiceEvent } from '@openpanel/db';
import { import {
TABLE_NAMES,
getEvents, getEvents,
getLiveVisitors, getLiveVisitors,
getProfileById, getProfileById,
getProfileByIdCached, getProfileByIdCached,
TABLE_NAMES,
transformMinimalEvent, transformMinimalEvent,
} from '@openpanel/db'; } from '@openpanel/db';
import { getRedisCache, getRedisPub, getRedisSub } from '@openpanel/redis'; import { getRedisCache, getRedisPub, getRedisSub } from '@openpanel/redis';
@@ -26,10 +26,10 @@ export async function testVisitors(
projectId: string; projectId: string;
}; };
}>, }>,
reply: FastifyReply reply: FastifyReply,
) { ) {
const events = await getEvents( const events = await getEvents(
`SELECT * FROM ${TABLE_NAMES.events} LIMIT 500` `SELECT * FROM ${TABLE_NAMES.events} LIMIT 500`,
); );
const event = events[Math.floor(Math.random() * events.length)]; const event = events[Math.floor(Math.random() * events.length)];
if (!event) { if (!event) {
@@ -41,7 +41,7 @@ export async function testVisitors(
`live:event:${event.projectId}:${Math.random() * 1000}`, `live:event:${event.projectId}:${Math.random() * 1000}`,
'', '',
'EX', 'EX',
10 10,
); );
reply.status(202).send(event); reply.status(202).send(event);
} }
@@ -52,10 +52,10 @@ export async function testEvents(
projectId: string; projectId: string;
}; };
}>, }>,
reply: FastifyReply reply: FastifyReply,
) { ) {
const events = await getEvents( const events = await getEvents(
`SELECT * FROM ${TABLE_NAMES.events} LIMIT 500` `SELECT * FROM ${TABLE_NAMES.events} LIMIT 500`,
); );
const event = events[Math.floor(Math.random() * events.length)]; const event = events[Math.floor(Math.random() * events.length)];
if (!event) { if (!event) {
@@ -73,7 +73,7 @@ export function wsVisitors(
Params: { Params: {
projectId: string; projectId: string;
}; };
}> }>,
) { ) {
const { params } = req; const { params } = req;
@@ -122,7 +122,7 @@ export async function wsProjectEvents(
token?: string; token?: string;
type?: string; type?: string;
}; };
}> }>,
) { ) {
const { params, query } = req; const { params, query } = req;
const { token } = query; const { token } = query;
@@ -147,7 +147,7 @@ export async function wsProjectEvents(
if (event?.projectId === params.projectId) { if (event?.projectId === params.projectId) {
const profile = await getProfileByIdCached( const profile = await getProfileByIdCached(
event.profileId, event.profileId,
event.projectId event.projectId,
); );
connection.socket.send( connection.socket.send(
superjson.stringify( superjson.stringify(
@@ -156,8 +156,8 @@ export async function wsProjectEvents(
...event, ...event,
profile, profile,
} }
: transformMinimalEvent(event) : transformMinimalEvent(event),
) ),
); );
} }
}; };

View File

@@ -4,8 +4,8 @@ import type { FastifyReply, FastifyRequest } from 'fastify';
import icoToPng from 'ico-to-png'; import icoToPng from 'ico-to-png';
import sharp from 'sharp'; import sharp from 'sharp';
import { createHash } from '@openpanel/common'; import { createHash } from '@openpanel/common/server';
import { ch, formatClickhouseDate, TABLE_NAMES } from '@openpanel/db'; import { TABLE_NAMES, ch, formatClickhouseDate } from '@openpanel/db';
import { getRedisCache } from '@openpanel/redis'; import { getRedisCache } from '@openpanel/redis';
interface GetFaviconParams { interface GetFaviconParams {
@@ -38,7 +38,7 @@ async function getImageBuffer(url: string) {
.png() .png()
.toBuffer(); .toBuffer();
} catch (error) { } catch (error) {
logger.error(`Failed to get image from url`, { logger.error('Failed to get image from url', {
error, error,
url, url,
}); });
@@ -51,7 +51,7 @@ export async function getFavicon(
request: FastifyRequest<{ request: FastifyRequest<{
Querystring: GetFaviconParams; Querystring: GetFaviconParams;
}>, }>,
reply: FastifyReply reply: FastifyReply,
) { ) {
function sendBuffer(buffer: Buffer, cacheKey?: string) { function sendBuffer(buffer: Buffer, cacheKey?: string) {
if (cacheKey) { if (cacheKey) {
@@ -95,7 +95,7 @@ export async function getFavicon(
} }
const buffer = await getImageBuffer( const buffer = await getImageBuffer(
'https://www.iconsdb.com/icons/download/orange/warning-128.png' 'https://www.iconsdb.com/icons/download/orange/warning-128.png',
); );
if (buffer && buffer.byteLength > 0) { if (buffer && buffer.byteLength > 0) {
return sendBuffer(buffer, hostname); return sendBuffer(buffer, hostname);
@@ -106,7 +106,7 @@ export async function getFavicon(
export async function clearFavicons( export async function clearFavicons(
request: FastifyRequest, request: FastifyRequest,
reply: FastifyReply reply: FastifyReply,
) { ) {
const keys = await getRedisCache().keys('favicon:*'); const keys = await getRedisCache().keys('favicon:*');
for (const key of keys) { for (const key of keys) {
@@ -122,7 +122,7 @@ export async function ping(
count: number; count: number;
}; };
}>, }>,
reply: FastifyReply reply: FastifyReply,
) { ) {
try { try {
await ch.insert({ await ch.insert({
@@ -137,7 +137,7 @@ export async function ping(
format: 'JSONEachRow', format: 'JSONEachRow',
}); });
reply.status(200).send({ reply.status(200).send({
message: `Success`, message: 'Success',
count: request.body.count, count: request.body.count,
domain: request.body.domain, domain: request.body.domain,
}); });

View File

@@ -13,7 +13,7 @@ export async function updateProfile(
request: FastifyRequest<{ request: FastifyRequest<{
Body: UpdateProfilePayload; Body: UpdateProfilePayload;
}>, }>,
reply: FastifyReply reply: FastifyReply,
) { ) {
const { profileId, properties, ...rest } = request.body; const { profileId, properties, ...rest } = request.body;
const projectId = request.projectId; const projectId = request.projectId;
@@ -41,7 +41,7 @@ export async function incrementProfileProperty(
request: FastifyRequest<{ request: FastifyRequest<{
Body: IncrementProfilePayload; Body: IncrementProfilePayload;
}>, }>,
reply: FastifyReply reply: FastifyReply,
) { ) {
const { profileId, property, value } = request.body; const { profileId, property, value } = request.body;
const projectId = request.projectId; const projectId = request.projectId;
@@ -51,19 +51,19 @@ export async function incrementProfileProperty(
return reply.status(404).send('Not found'); return reply.status(404).send('Not found');
} }
const parsed = parseInt( const parsed = Number.parseInt(
pathOr<string>('0', property.split('.'), profile.properties), pathOr<string>('0', property.split('.'), profile.properties),
10 10,
); );
if (isNaN(parsed)) { if (Number.isNaN(parsed)) {
return reply.status(400).send('Not number'); return reply.status(400).send('Not number');
} }
profile.properties = assocPath( profile.properties = assocPath(
property.split('.'), property.split('.'),
parsed + value, parsed + value,
profile.properties profile.properties,
); );
await upsertProfile({ await upsertProfile({
@@ -80,7 +80,7 @@ export async function decrementProfileProperty(
request: FastifyRequest<{ request: FastifyRequest<{
Body: IncrementProfilePayload; Body: IncrementProfilePayload;
}>, }>,
reply: FastifyReply reply: FastifyReply,
) { ) {
const { profileId, property, value } = request.body; const { profileId, property, value } = request.body;
const projectId = request.projectId; const projectId = request.projectId;
@@ -90,19 +90,19 @@ export async function decrementProfileProperty(
return reply.status(404).send('Not found'); return reply.status(404).send('Not found');
} }
const parsed = parseInt( const parsed = Number.parseInt(
pathOr<string>('0', property.split('.'), profile.properties), pathOr<string>('0', property.split('.'), profile.properties),
10 10,
); );
if (isNaN(parsed)) { if (Number.isNaN(parsed)) {
return reply.status(400).send('Not number'); return reply.status(400).send('Not number');
} }
profile.properties = assocPath( profile.properties = assocPath(
property.split('.'), property.split('.'),
parsed - value, parsed - value,
profile.properties profile.properties,
); );
await upsertProfile({ await upsertProfile({

View File

@@ -2,9 +2,9 @@ import type { GeoLocation } from '@/utils/parseIp';
import { getClientIp, parseIp } from '@/utils/parseIp'; import { getClientIp, parseIp } from '@/utils/parseIp';
import { parseUserAgent } from '@/utils/parseUserAgent'; import { parseUserAgent } from '@/utils/parseUserAgent';
import type { FastifyReply, FastifyRequest } from 'fastify'; import type { FastifyReply, FastifyRequest } from 'fastify';
import { assocPath, path, pathOr, pick } from 'ramda'; import { path, assocPath, pathOr, pick } from 'ramda';
import { generateDeviceId } from '@openpanel/common'; import { generateDeviceId } from '@openpanel/common/server';
import { import {
createProfileAlias, createProfileAlias,
getProfileById, getProfileById,
@@ -31,21 +31,21 @@ export function getStringHeaders(headers: FastifyRequest['headers']) {
'openpanel-sdk-version', 'openpanel-sdk-version',
'openpanel-client-id', 'openpanel-client-id',
], ],
headers headers,
) ),
).reduce( ).reduce(
(acc, [key, value]) => ({ (acc, [key, value]) => ({
...acc, ...acc,
[key]: value ? String(value) : undefined, [key]: value ? String(value) : undefined,
}), }),
{} {},
); );
} }
function getIdentity(body: TrackHandlerPayload): IdentifyPayload | undefined { function getIdentity(body: TrackHandlerPayload): IdentifyPayload | undefined {
const identity = path<IdentifyPayload>( const identity = path<IdentifyPayload>(
['properties', '__identify'], ['properties', '__identify'],
body.payload body.payload,
); );
return ( return (
@@ -62,7 +62,7 @@ export async function handler(
request: FastifyRequest<{ request: FastifyRequest<{
Body: TrackHandlerPayload; Body: TrackHandlerPayload;
}>, }>,
reply: FastifyReply reply: FastifyReply,
) { ) {
const ip = const ip =
path<string>(['properties', '__ip'], request.body.payload) || path<string>(['properties', '__ip'], request.body.payload) ||
@@ -129,7 +129,7 @@ export async function handler(
projectId, projectId,
geo, geo,
ua, ua,
}) }),
); );
} }
@@ -196,7 +196,7 @@ async function track({
'locked', 'locked',
'EX', 'EX',
10, 10,
'NX' 'NX',
); );
eventsQueue.add('event', { eventsQueue.add('event', {
@@ -269,19 +269,19 @@ async function increment({
throw new Error('Not found'); throw new Error('Not found');
} }
const parsed = parseInt( const parsed = Number.parseInt(
pathOr<string>('0', property.split('.'), profile.properties), pathOr<string>('0', property.split('.'), profile.properties),
10 10,
); );
if (isNaN(parsed)) { if (Number.isNaN(parsed)) {
throw new Error('Not number'); throw new Error('Not number');
} }
profile.properties = assocPath( profile.properties = assocPath(
property.split('.'), property.split('.'),
parsed + (value || 1), parsed + (value || 1),
profile.properties profile.properties,
); );
await upsertProfile({ await upsertProfile({
@@ -305,19 +305,19 @@ async function decrement({
throw new Error('Not found'); throw new Error('Not found');
} }
const parsed = parseInt( const parsed = Number.parseInt(
pathOr<string>('0', property.split('.'), profile.properties), pathOr<string>('0', property.split('.'), profile.properties),
10 10,
); );
if (isNaN(parsed)) { if (Number.isNaN(parsed)) {
throw new Error('Not number'); throw new Error('Not number');
} }
profile.properties = assocPath( profile.properties = assocPath(
property.split('.'), property.split('.'),
parsed - (value || 1), parsed - (value || 1),
profile.properties profile.properties,
); );
await upsertProfile({ await upsertProfile({

View File

@@ -33,7 +33,7 @@ export async function clerkWebhook(
request: FastifyRequest<{ request: FastifyRequest<{
Body: WebhookEvent; Body: WebhookEvent;
}>, }>,
reply: FastifyReply reply: FastifyReply,
) { ) {
const payload = request.body; const payload = request.body;
const verified = verify(payload, request.headers); const verified = verify(payload, request.headers);
@@ -49,7 +49,7 @@ export async function clerkWebhook(
if (!email) { if (!email) {
return Response.json( return Response.json(
{ message: 'No email address found' }, { message: 'No email address found' },
{ status: 400 } { status: 400 },
); );
} }

View File

@@ -1,4 +1,4 @@
import zlib from 'zlib'; import zlib from 'node:zlib';
import { clerkPlugin } from '@clerk/fastify'; import { clerkPlugin } from '@clerk/fastify';
import compress from '@fastify/compress'; import compress from '@fastify/compress';
import cookie from '@fastify/cookie'; import cookie from '@fastify/cookie';
@@ -11,7 +11,7 @@ import metricsPlugin from 'fastify-metrics';
import { path } from 'ramda'; import { path } from 'ramda';
import { generateId, round } from '@openpanel/common'; import { generateId, round } from '@openpanel/common';
import { chQuery, db, TABLE_NAMES } from '@openpanel/db'; import { TABLE_NAMES, chQuery, db } from '@openpanel/db';
import type { IServiceClient } from '@openpanel/db'; import type { IServiceClient } from '@openpanel/db';
import { eventsQueue } from '@openpanel/queue'; import { eventsQueue } from '@openpanel/queue';
import { getRedisCache, getRedisPub } from '@openpanel/redis'; import { getRedisCache, getRedisPub } from '@openpanel/redis';
@@ -48,7 +48,7 @@ async function withTimings<T>(promise: Promise<T>) {
} }
} }
const port = parseInt(process.env.API_PORT || '3000', 10); const port = Number.parseInt(process.env.API_PORT || '3000', 10);
const startServer = async () => { const startServer = async () => {
logger.info('Starting server'); logger.info('Starting server');
@@ -65,7 +65,7 @@ const startServer = async () => {
}); });
const getTrpcInput = ( const getTrpcInput = (
request: FastifyRequest request: FastifyRequest,
): Record<string, unknown> | undefined => { ): Record<string, unknown> | undefined => {
const input = path(['query', 'input'], request); const input = path(['query', 'input'], request);
try { try {
@@ -125,14 +125,14 @@ const startServer = async () => {
fastify.addContentTypeParser( fastify.addContentTypeParser(
'application/json', 'application/json',
{ parseAs: 'buffer' }, { parseAs: 'buffer' },
function (req, body, done) { (req, body, done) => {
const isGzipped = req.headers['content-encoding'] === 'gzip'; const isGzipped = req.headers['content-encoding'] === 'gzip';
if (isGzipped) { if (isGzipped) {
zlib.gunzip(body, (err, decompressedBody) => { zlib.gunzip(body, (err, decompressedBody) => {
console.log( console.log(
'decompressedBody', 'decompressedBody',
decompressedBody.toString().slice(0, 100) decompressedBody.toString().slice(0, 100),
); );
if (err) { if (err) {
done(err); done(err);
@@ -153,7 +153,7 @@ const startServer = async () => {
done(new Error('Invalid JSON')); done(new Error('Invalid JSON'));
} }
} }
} },
); );
await fastify.register(metricsPlugin, { endpoint: '/metrics' }); await fastify.register(metricsPlugin, { endpoint: '/metrics' });
@@ -211,7 +211,7 @@ const startServer = async () => {
const dbRes = await withTimings(db.project.findFirst()); const dbRes = await withTimings(db.project.findFirst());
const queueRes = await withTimings(eventsQueue.getCompleted()); const queueRes = await withTimings(eventsQueue.getCompleted());
const chRes = await withTimings( const chRes = await withTimings(
chQuery(`SELECT * FROM ${TABLE_NAMES.events} LIMIT 1`) chQuery(`SELECT * FROM ${TABLE_NAMES.events} LIMIT 1`),
); );
const status = redisRes && dbRes && queueRes && chRes ? 200 : 500; const status = redisRes && dbRes && queueRes && chRes ? 200 : 500;

View File

@@ -14,7 +14,7 @@ const eventRouter: FastifyPluginCallback = (fastify, opts, done) => {
req: FastifyRequest<{ req: FastifyRequest<{
Body: PostEventPayload; Body: PostEventPayload;
}>, }>,
reply reply,
) => { ) => {
try { try {
const client = await validateSdkRequest(req.headers).catch((error) => { const client = await validateSdkRequest(req.headers).catch((error) => {
@@ -51,7 +51,7 @@ const eventRouter: FastifyPluginCallback = (fastify, opts, done) => {
reply.status(401).send(); reply.status(401).send();
return; return;
} }
} },
); );
fastify.route({ fastify.route({

View File

@@ -15,11 +15,14 @@ const eventRouter: FastifyPluginCallback = (fastify, opts, done) => {
error: 'Unauthorized', error: 'Unauthorized',
message: 'Client ID seems to be malformed', message: 'Client ID seems to be malformed',
}); });
} else if (e instanceof Error) { }
if (e instanceof Error) {
return reply return reply
.status(401) .status(401)
.send({ error: 'Unauthorized', message: e.message }); .send({ error: 'Unauthorized', message: e.message });
} }
return reply return reply
.status(401) .status(401)
.send({ error: 'Unauthorized', message: 'Unexpected error' }); .send({ error: 'Unauthorized', message: 'Unexpected error' });

View File

@@ -15,11 +15,14 @@ const importRouter: FastifyPluginCallback = (fastify, opts, done) => {
error: 'Unauthorized', error: 'Unauthorized',
message: 'Client ID seems to be malformed', message: 'Client ID seems to be malformed',
}); });
} else if (e instanceof Error) { }
if (e instanceof Error) {
return reply return reply
.status(401) .status(401)
.send({ error: 'Unauthorized', message: e.message }); .send({ error: 'Unauthorized', message: e.message });
} }
return reply return reply
.status(401) .status(401)
.send({ error: 'Unauthorized', message: 'Unexpected error' }); .send({ error: 'Unauthorized', message: 'Unexpected error' });

View File

@@ -20,12 +20,12 @@ const liveRouter: FastifyPluginCallback = (fastify, opts, done) => {
fastify.get( fastify.get(
'/visitors/:projectId', '/visitors/:projectId',
{ websocket: true }, { websocket: true },
controller.wsVisitors controller.wsVisitors,
); );
fastify.get( fastify.get(
'/events/:projectId', '/events/:projectId',
{ websocket: true }, { websocket: true },
controller.wsProjectEvents controller.wsProjectEvents,
); );
done(); done();
}); });

View File

@@ -14,7 +14,7 @@ const trackRouter: FastifyPluginCallback = (fastify, opts, done) => {
req: FastifyRequest<{ req: FastifyRequest<{
Body: TrackHandlerPayload; Body: TrackHandlerPayload;
}>, }>,
reply reply,
) => { ) => {
try { try {
const client = await validateSdkRequest(req.headers).catch((error) => { const client = await validateSdkRequest(req.headers).catch((error) => {
@@ -55,7 +55,7 @@ const trackRouter: FastifyPluginCallback = (fastify, opts, done) => {
reply.status(401).send(); reply.status(401).send();
return; return;
} }
} },
); );
fastify.route({ fastify.route({

View File

@@ -1,7 +1,7 @@
import type { RawRequestDefaultExpression } from 'fastify'; import type { RawRequestDefaultExpression } from 'fastify';
import jwt from 'jsonwebtoken'; import jwt from 'jsonwebtoken';
import { verifyPassword } from '@openpanel/common'; import { verifyPassword } from '@openpanel/common/server';
import type { Client, IServiceClient } from '@openpanel/db'; import type { Client, IServiceClient } from '@openpanel/db';
import { ClientType, db } from '@openpanel/db'; import { ClientType, db } from '@openpanel/db';
@@ -24,7 +24,7 @@ export class SdkAuthError extends Error {
clientId?: string; clientId?: string;
clientSecret?: string; clientSecret?: string;
origin?: string; origin?: string;
} },
) { ) {
super(message); super(message);
this.name = 'SdkAuthError'; this.name = 'SdkAuthError';
@@ -33,7 +33,7 @@ export class SdkAuthError extends Error {
} }
export async function validateSdkRequest( export async function validateSdkRequest(
headers: RawRequestDefaultExpression['headers'] headers: RawRequestDefaultExpression['headers'],
): Promise<Client> { ): Promise<Client> {
const clientIdNew = headers['openpanel-client-id'] as string; const clientIdNew = headers['openpanel-client-id'] as string;
const clientIdOld = headers['mixan-client-id'] as string; const clientIdOld = headers['mixan-client-id'] as string;
@@ -48,7 +48,7 @@ export async function validateSdkRequest(
clientId, clientId,
clientSecret: clientSecret:
typeof clientSecret === 'string' typeof clientSecret === 'string'
? clientSecret.slice(0, 5) + '...' + clientSecret.slice(-5) ? `${clientSecret.slice(0, 5)}...${clientSecret.slice(-5)}`
: 'none', : 'none',
origin, origin,
}); });
@@ -99,7 +99,7 @@ export async function validateSdkRequest(
} }
export async function validateExportRequest( export async function validateExportRequest(
headers: RawRequestDefaultExpression['headers'] headers: RawRequestDefaultExpression['headers'],
): Promise<IServiceClient> { ): Promise<IServiceClient> {
const clientId = headers['openpanel-client-id'] as string; const clientId = headers['openpanel-client-id'] as string;
const clientSecret = (headers['openpanel-client-secret'] as string) || ''; const clientSecret = (headers['openpanel-client-secret'] as string) || '';
@@ -129,7 +129,7 @@ export async function validateExportRequest(
} }
export async function validateImportRequest( export async function validateImportRequest(
headers: RawRequestDefaultExpression['headers'] headers: RawRequestDefaultExpression['headers'],
): Promise<IServiceClient> { ): Promise<IServiceClient> {
const clientId = headers['openpanel-client-id'] as string; const clientId = headers['openpanel-client-id'] as string;
const clientSecret = (headers['openpanel-client-secret'] as string) || ''; const clientSecret = (headers['openpanel-client-secret'] as string) || '';
@@ -165,7 +165,7 @@ export function validateClerkJwt(token?: string) {
try { try {
const decoded = jwt.verify( const decoded = jwt.verify(
token, token,
process.env.CLERK_PUBLIC_PEM_KEY!.replace(/\\n/g, '\n') process.env.CLERK_PUBLIC_PEM_KEY!.replace(/\\n/g, '\n'),
); );
if (typeof decoded === 'object') { if (typeof decoded === 'object') {

View File

@@ -4,8 +4,11 @@ export const parseQueryString = (obj: Record<string, any>): any => {
return Object.fromEntries( return Object.fromEntries(
Object.entries(obj).map(([k, v]) => { Object.entries(obj).map(([k, v]) => {
if (typeof v === 'object') return [k, parseQueryString(v)]; if (typeof v === 'object') return [k, parseQueryString(v)];
if (/^-?[0-9]+(\.[0-9]+)?$/i.test(v) && !isNaN(parseFloat(v))) if (
return [k, parseFloat(v)]; /^-?[0-9]+(\.[0-9]+)?$/i.test(v) &&
!Number.isNaN(Number.parseFloat(v))
)
return [k, Number.parseFloat(v)];
if (v === 'true') return [k, true]; if (v === 'true') return [k, true];
if (v === 'false') return [k, false]; if (v === 'false') return [k, false];
if (typeof v === 'string') { if (typeof v === 'string') {
@@ -15,6 +18,6 @@ export const parseQueryString = (obj: Record<string, any>): any => {
return [k, v]; return [k, v];
} }
return [k, null]; return [k, null];
}) }),
); );
}; };

View File

@@ -9,7 +9,7 @@ function findBestFavicon(favicons: UrlMetaData['favicons']) {
(favicon) => (favicon) =>
favicon.rel === 'shortcut icon' || favicon.rel === 'shortcut icon' ||
favicon.rel === 'icon' || favicon.rel === 'icon' ||
favicon.rel === 'apple-touch-icon' favicon.rel === 'apple-touch-icon',
); );
if (match) { if (match) {

View File

@@ -71,7 +71,7 @@ const userAgentServerList = [
function isServer(userAgent: string) { function isServer(userAgent: string) {
const match = userAgentServerList.some((server) => const match = userAgentServerList.some((server) =>
userAgent.toLowerCase().includes(server.toLowerCase()) userAgent.toLowerCase().includes(server.toLowerCase()),
); );
if (match) { if (match) {
return true; return true;
@@ -83,15 +83,15 @@ function isServer(userAgent: string) {
export function getDevice(ua: string) { export function getDevice(ua: string) {
const mobile1 = const mobile1 =
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test( /(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(
ua ua,
); );
const mobile2 = const mobile2 =
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test( /1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw-(n|u)|c55\/|capi|ccwa|cdm-|cell|chtm|cldc|cmd-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc-s|devi|dica|dmob|do(c|p)o|ds(12|-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(-|_)|g1 u|g560|gene|gf-5|g-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd-(m|p|t)|hei-|hi(pt|ta)|hp( i|ip)|hs-c|ht(c(-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i-(20|go|ma)|i230|iac( |-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|-[a-w])|libw|lynx|m1-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|-([1-8]|c))|phil|pire|pl(ay|uc)|pn-2|po(ck|rt|se)|prox|psio|pt-g|qa-a|qc(07|12|21|32|60|-[2-7]|i-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h-|oo|p-)|sdk\/|se(c(-|0|1)|47|mc|nd|ri)|sgh-|shar|sie(-|m)|sk-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h-|v-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl-|tdg-|tel(i|m)|tim-|t-mo|to(pl|sh)|ts(70|m-|m3|m5)|tx-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas-|your|zeto|zte-/i.test(
ua.slice(0, 4) ua.slice(0, 4),
); );
const tablet = const tablet =
/tablet|ipad|android(?!.*mobile)|xoom|sch-i800|kindle|silk|playbook/i.test( /tablet|ipad|android(?!.*mobile)|xoom|sch-i800|kindle|silk|playbook/i.test(
ua ua,
); );
if (mobile1 || mobile2) { if (mobile1 || mobile2) {

View File

@@ -26,9 +26,6 @@ next-env.d.ts
# debug # debug
npm-debug.log* npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*
# local env files # 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 # 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

View File

@@ -37,7 +37,6 @@ COPY packages/common/package.json packages/common/package.json
COPY packages/constants/package.json packages/constants/package.json COPY packages/constants/package.json packages/constants/package.json
COPY packages/validation/package.json packages/validation/package.json COPY packages/validation/package.json packages/validation/package.json
COPY packages/sdks/sdk/package.json packages/sdks/sdk/package.json COPY packages/sdks/sdk/package.json packages/sdks/sdk/package.json
COPY patches patches
# BUILD # BUILD
FROM base AS build FROM base AS build

View File

@@ -30,7 +30,11 @@ const config = {
typescript: { ignoreBuildErrors: true }, typescript: { ignoreBuildErrors: true },
experimental: { experimental: {
// Avoid "Critical dependency: the request of a dependency is an expression" // Avoid "Critical dependency: the request of a dependency is an expression"
serverComponentsExternalPackages: ['bullmq', 'ioredis'], serverComponentsExternalPackages: [
'bullmq',
'ioredis',
'@hyperdx/node-opentelemetry',
],
instrumentationHook: !!process.env.ENABLE_INSTRUMENTATION_HOOK, instrumentationHook: !!process.env.ENABLE_INSTRUMENTATION_HOOK,
}, },
/** /**

View File

@@ -7,8 +7,6 @@
"testing": "pnpm dev", "testing": "pnpm dev",
"build": "pnpm with-env next build", "build": "pnpm with-env next build",
"start": "next start", "start": "next start",
"lint": "eslint .",
"format": "prettier --check \"**/*.{tsx,mjs,ts,md,json}\"",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"with-env": "dotenv -e ../../.env -c --" "with-env": "dotenv -e ../../.env -c --"
}, },
@@ -76,7 +74,7 @@
"lucide-react": "^0.331.0", "lucide-react": "^0.331.0",
"mathjs": "^12.3.2", "mathjs": "^12.3.2",
"mitt": "^3.0.1", "mitt": "^3.0.1",
"next": "~14.2.1", "next": "14.2.1",
"next-auth": "^4.24.5", "next-auth": "^4.24.5",
"next-themes": "^0.2.1", "next-themes": "^0.2.1",
"nextjs-toploader": "^1.6.11", "nextjs-toploader": "^1.6.11",
@@ -112,8 +110,6 @@
"zod": "^3.22.4" "zod": "^3.22.4"
}, },
"devDependencies": { "devDependencies": {
"@openpanel/eslint-config": "workspace:*",
"@openpanel/prettier-config": "workspace:*",
"@openpanel/trpc": "workspace:*", "@openpanel/trpc": "workspace:*",
"@openpanel/tsconfig": "workspace:*", "@openpanel/tsconfig": "workspace:*",
"@types/bcrypt": "^5.0.2", "@types/bcrypt": "^5.0.2",
@@ -127,26 +123,9 @@
"@types/react-simple-maps": "^3.0.4", "@types/react-simple-maps": "^3.0.4",
"@types/react-syntax-highlighter": "^15.5.11", "@types/react-syntax-highlighter": "^15.5.11",
"@types/sqlstring": "^2.3.2", "@types/sqlstring": "^2.3.2",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"autoprefixer": "^10.4.17", "autoprefixer": "^10.4.17",
"eslint": "^8.56.0",
"postcss": "^8.4.35", "postcss": "^8.4.35",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.11",
"tailwindcss": "^3.4.1", "tailwindcss": "^3.4.1",
"typescript": "^5.3.3" "typescript": "^5.3.3"
}, }
"ct3aMetadata": { }
"initVersion": "7.21.0"
},
"eslintConfig": {
"root": true,
"extends": [
"@openpanel/eslint-config/base",
"@openpanel/eslint-config/react",
"@openpanel/eslint-config/nextjs"
]
},
"prettier": "@openpanel/prettier-config"
}

View File

@@ -30,7 +30,7 @@ import {
getDefaultIntervalByRange, getDefaultIntervalByRange,
timeWindows, timeWindows,
} from '@openpanel/constants'; } from '@openpanel/constants';
import type { getReportsByDashboardId, IServiceDashboard } from '@openpanel/db'; import type { IServiceDashboard, getReportsByDashboardId } from '@openpanel/db';
import { OverviewReportRange } from '../../overview-sticky-header'; import { OverviewReportRange } from '../../overview-sticky-header';
@@ -64,7 +64,7 @@ export function ListReports({ reports, dashboard }: ListReportsProps) {
params.projectId params.projectId
}/reports?${new URLSearchParams({ }/reports?${new URLSearchParams({
dashboardId: params.dashboardId, dashboardId: params.dashboardId,
}).toString()}` }).toString()}`,
); );
}} }}
> >
@@ -163,7 +163,7 @@ export function ListReports({ reports, dashboard }: ListReportsProps) {
params.projectId params.projectId
}/reports?${new URLSearchParams({ }/reports?${new URLSearchParams({
dashboardId: params.dashboardId, dashboardId: params.dashboardId,
}).toString()}` }).toString()}`,
) )
} }
className="mt-14" className="mt-14"

View File

@@ -79,7 +79,7 @@ export function ListDashboards({ dashboards }: ListDashboardsProps) {
{dashboards.map((item) => { {dashboards.map((item) => {
const visibleReports = item.reports.slice( const visibleReports = item.reports.slice(
0, 0,
item.reports.length > 6 ? 5 : 6 item.reports.length > 6 ? 5 : 6,
); );
return ( return (
<Card key={item.id} hover> <Card key={item.id} hover>
@@ -97,7 +97,7 @@ export function ListDashboards({ dashboards }: ListDashboardsProps) {
<div <div
className={cn( className={cn(
'mt-4 grid gap-2', 'mt-4 grid gap-2',
'grid-cols-1 @sm:grid-cols-2' 'grid-cols-1 @sm:grid-cols-2',
)} )}
> >
{visibleReports.map((report) => { {visibleReports.map((report) => {
@@ -145,6 +145,7 @@ export function ListDashboards({ dashboards }: ListDashboardsProps) {
<CardActions> <CardActions>
<CardActionsItem className="w-full" asChild> <CardActionsItem className="w-full" asChild>
<button <button
type="button"
onClick={() => { onClick={() => {
pushModal('EditDashboard', item); pushModal('EditDashboard', item);
}} }}
@@ -155,6 +156,7 @@ export function ListDashboards({ dashboards }: ListDashboardsProps) {
</CardActionsItem> </CardActionsItem>
<CardActionsItem className="w-full text-destructive" asChild> <CardActionsItem className="w-full text-destructive" asChild>
<button <button
type="button"
onClick={() => { onClick={() => {
deletion.mutate({ deletion.mutate({
id: item.id, id: item.id,

View File

@@ -15,7 +15,7 @@ const Conversions = ({ projectId }: Props) => {
}, },
{ {
keepPreviousData: true, keepPreviousData: true,
} },
); );
return <EventsTable query={query} />; return <EventsTable query={query} />;

View File

@@ -23,7 +23,7 @@ const Events = ({ projectId, profileId }: Props) => {
const [eventNames] = useEventQueryNamesFilter(); const [eventNames] = useEventQueryNamesFilter();
const [cursor, setCursor] = useQueryState( const [cursor, setCursor] = useQueryState(
'cursor', 'cursor',
parseAsInteger.withDefault(0) parseAsInteger.withDefault(0),
); );
const query = api.event.events.useQuery( const query = api.event.events.useQuery(
{ {
@@ -36,7 +36,7 @@ const Events = ({ projectId, profileId }: Props) => {
}, },
{ {
keepPreviousData: true, keepPreviousData: true,
} },
); );
return ( return (

View File

@@ -27,16 +27,16 @@ export default function Page({
<Padding> <Padding>
<div className="mb-4"> <div className="mb-4">
<PageTabs> <PageTabs>
<PageTabsLink href={`?tab=events`} isActive={tab === 'events'}> <PageTabsLink href={'?tab=events'} isActive={tab === 'events'}>
Events Events
</PageTabsLink> </PageTabsLink>
<PageTabsLink <PageTabsLink
href={`?tab=conversions`} href={'?tab=conversions'}
isActive={tab === 'conversions'} isActive={tab === 'conversions'}
> >
Conversions Conversions
</PageTabsLink> </PageTabsLink>
<PageTabsLink href={`?tab=charts`} isActive={tab === 'charts'}> <PageTabsLink href={'?tab=charts'} isActive={tab === 'charts'}>
Charts Charts
</PageTabsLink> </PageTabsLink>
</PageTabs> </PageTabs>

View File

@@ -1,6 +1,5 @@
'use client'; 'use client';
import { useEffect } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import { pushModal } from '@/modals'; import { pushModal } from '@/modals';
@@ -19,6 +18,7 @@ import {
import type { LucideProps } from 'lucide-react'; import type { LucideProps } from 'lucide-react';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { useEffect } from 'react';
import type { IServiceDashboards } from '@openpanel/db'; import type { IServiceDashboards } from '@openpanel/db';
@@ -42,7 +42,7 @@ function LinkWithIcon({
className={cn( className={cn(
'text-text flex items-center gap-2 rounded-md px-3 py-2 font-medium transition-all hover:bg-def-200', 'text-text flex items-center gap-2 rounded-md px-3 py-2 font-medium transition-all hover:bg-def-200',
active && 'bg-def-200', active && 'bg-def-200',
className className,
)} )}
href={href} href={href}
> >

View File

@@ -18,7 +18,7 @@ export default function LayoutOrganizationSelector({
const router = useRouter(); const router = useRouter();
const organization = organizations.find( const organization = organizations.find(
(item) => item.id === params.organizationSlug (item) => item.id === params.organizationSlug,
); );
return ( return (

View File

@@ -1,6 +1,5 @@
'use client'; 'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Combobox } from '@/components/ui/combobox'; import { Combobox } from '@/components/ui/combobox';
import { import {
@@ -24,6 +23,7 @@ import {
PlusIcon, PlusIcon,
} from 'lucide-react'; } from 'lucide-react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import { useState } from 'react';
import type { import type {
getCurrentOrganizations, getCurrentOrganizations,
@@ -50,7 +50,7 @@ export default function LayoutProjectSelector({
const split = pathname const split = pathname
.replace( .replace(
`/${organizationSlug}/${projectId}`, `/${organizationSlug}/${projectId}`,
`/${organizationSlug}/${newProjectId}` `/${organizationSlug}/${newProjectId}`,
) )
.split('/'); .split('/');
// slicing here will remove everything after /{orgId}/{projectId}/dashboards [slice here] /xxx/xxx/xxx // slicing here will remove everything after /{orgId}/{projectId}/dashboards [slice here] /xxx/xxx/xxx

View File

@@ -1,6 +1,5 @@
'use client'; 'use client';
import { useEffect, useState } from 'react';
import { LogoSquare } from '@/components/logo'; import { LogoSquare } from '@/components/logo';
import SettingsToggle from '@/components/settings-toggle'; import SettingsToggle from '@/components/settings-toggle';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@@ -8,11 +7,12 @@ import { cn } from '@/utils/cn';
import { Rotate as Hamburger } from 'hamburger-react'; import { Rotate as Hamburger } from 'hamburger-react';
import { MenuIcon, XIcon } from 'lucide-react'; import { MenuIcon, XIcon } from 'lucide-react';
import { usePathname } from 'next/navigation'; import { usePathname } from 'next/navigation';
import { useEffect, useState } from 'react';
import type { import type {
getProjectsByOrganizationSlug,
IServiceDashboards, IServiceDashboards,
IServiceOrganization, IServiceOrganization,
getProjectsByOrganizationSlug,
} from '@openpanel/db'; } from '@openpanel/db';
import LayoutMenu from './layout-menu'; import LayoutMenu from './layout-menu';
@@ -40,19 +40,20 @@ export function LayoutSidebar({
return ( return (
<> <>
<button <button
type="button"
onClick={() => setActive(false)} onClick={() => setActive(false)}
className={cn( className={cn(
'fixed bottom-0 left-0 right-0 top-0 z-50 backdrop-blur-sm transition-opacity', 'fixed bottom-0 left-0 right-0 top-0 z-50 backdrop-blur-sm transition-opacity',
active active
? 'pointer-events-auto opacity-100' ? 'pointer-events-auto opacity-100'
: 'pointer-events-none opacity-0' : 'pointer-events-none opacity-0',
)} )}
/> />
<div <div
className={cn( className={cn(
'fixed left-0 top-0 z-50 flex h-screen w-72 flex-col border-r border-border bg-card transition-transform', 'fixed left-0 top-0 z-50 flex h-screen w-72 flex-col border-r border-border bg-card transition-transform',
'-translate-x-72 lg:-translate-x-0', // responsive '-translate-x-72 lg:-translate-x-0', // responsive
active && 'translate-x-0' // force active on mobile active && 'translate-x-0', // force active on mobile
)} )}
> >
<div className="absolute -right-12 flex h-16 items-center lg:hidden"> <div className="absolute -right-12 flex h-16 items-center lg:hidden">
@@ -77,7 +78,7 @@ export function LayoutSidebar({
<LayoutMenu dashboards={dashboards} /> <LayoutMenu dashboards={dashboards} />
</div> </div>
<div className="fixed bottom-0 left-0 right-0"> <div className="fixed bottom-0 left-0 right-0">
<div className="h-8 w-full bg-gradient-to-t from-card to-card/0"></div> <div className="h-8 w-full bg-gradient-to-t from-card to-card/0" />
</div> </div>
</div> </div>
</> </>

View File

@@ -13,7 +13,7 @@ export function StickyBelowHeader({
<div <div
className={cn( className={cn(
'top-0 z-20 border-b border-border bg-card md:sticky', 'top-0 z-20 border-b border-border bg-card md:sticky',
className className,
)} )}
> >
{children} {children}

View File

@@ -1,11 +1,11 @@
'use client'; 'use client';
import { memo } from 'react';
import { ReportChart } from '@/components/report-chart'; import { ReportChart } from '@/components/report-chart';
import { useNumber } from '@/hooks/useNumerFormatter'; import { useNumber } from '@/hooks/useNumerFormatter';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import isEqual from 'lodash.isequal'; import isEqual from 'lodash.isequal';
import { ExternalLinkIcon } from 'lucide-react'; import { ExternalLinkIcon } from 'lucide-react';
import { memo } from 'react';
import type { IServicePage } from '@openpanel/db'; import type { IServicePage } from '@openpanel/db';
@@ -38,7 +38,7 @@ export const PagesTable = memo(
className={cn( className={cn(
cell, cell,
'center-center font-mono text-lg font-semibold', 'center-center font-mono text-lg font-semibold',
index === data.length - 1 && 'rounded-bl-md' index === data.length - 1 && 'rounded-bl-md',
)} )}
> >
{number.short(item.count)} {number.short(item.count)}
@@ -46,7 +46,7 @@ export const PagesTable = memo(
<div <div
className={cn( className={cn(
cell, cell,
'flex w-80 flex-col justify-center gap-2 text-left' 'flex w-80 flex-col justify-center gap-2 text-left',
)} )}
> >
<span className="truncate font-medium">{item.title}</span> <span className="truncate font-medium">{item.title}</span>
@@ -68,7 +68,7 @@ export const PagesTable = memo(
className={cn( className={cn(
cell, cell,
'p-1', 'p-1',
index === data.length - 1 && 'rounded-br-md' index === data.length - 1 && 'rounded-br-md',
)} )}
> >
<ReportChart <ReportChart
@@ -122,7 +122,7 @@ export const PagesTable = memo(
}, },
(prevProps, nextProps) => { (prevProps, nextProps) => {
return isEqual(prevProps.data, nextProps.data); return isEqual(prevProps.data, nextProps.data);
} },
); );
PagesTable.displayName = 'PagesTable'; PagesTable.displayName = 'PagesTable';

View File

@@ -15,7 +15,7 @@ export function Pages({ projectId }: { projectId: string }) {
const take = 20; const take = 20;
const [cursor, setCursor] = useQueryState( const [cursor, setCursor] = useQueryState(
'cursor', 'cursor',
parseAsInteger.withDefault(0) parseAsInteger.withDefault(0),
); );
const [search, setSearch] = useQueryState('search', { const [search, setSearch] = useQueryState('search', {
defaultValue: '', defaultValue: '',
@@ -31,7 +31,7 @@ export function Pages({ projectId }: { projectId: string }) {
}, },
{ {
keepPreviousData: true, keepPreviousData: true,
} },
); );
const data = query.data ?? []; const data = query.data ?? [];
@@ -56,7 +56,7 @@ export function Pages({ projectId }: { projectId: string }) {
className="mt-2" className="mt-2"
setCursor={setCursor} setCursor={setCursor}
cursor={cursor} cursor={cursor}
count={Infinity} count={Number.POSITIVE_INFINITY}
take={take} take={take}
loading={query.isFetching} loading={query.isFetching}
/> />

View File

@@ -1,7 +1,7 @@
import withLoadingWidget from '@/hocs/with-loading-widget'; import withLoadingWidget from '@/hocs/with-loading-widget';
import { escape } from 'sqlstring'; import { escape } from 'sqlstring';
import { chQuery, TABLE_NAMES } from '@openpanel/db'; import { TABLE_NAMES, chQuery } from '@openpanel/db';
import MostEvents from './most-events'; import MostEvents from './most-events';
@@ -12,7 +12,7 @@ type Props = {
const MostEventsServer = async ({ projectId, profileId }: Props) => { const MostEventsServer = async ({ projectId, profileId }: Props) => {
const data = await chQuery<{ count: number; name: string }>( 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` `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 <MostEvents data={data} />; return <MostEvents data={data} />;
}; };

View File

@@ -22,7 +22,7 @@ const MostEvents = ({ data }: Props) => {
style={{ style={{
width: `${(item.count / max) * 100}%`, width: `${(item.count / max) * 100}%`,
}} }}
></div> />
<div className="relative flex justify-between "> <div className="relative flex justify-between ">
<div>{item.name}</div> <div>{item.name}</div>
<div>{item.count}</div> <div>{item.count}</div>

View File

@@ -1,7 +1,7 @@
import withLoadingWidget from '@/hocs/with-loading-widget'; import withLoadingWidget from '@/hocs/with-loading-widget';
import { escape } from 'sqlstring'; import { escape } from 'sqlstring';
import { chQuery, TABLE_NAMES } from '@openpanel/db'; import { TABLE_NAMES, chQuery } from '@openpanel/db';
import PopularRoutes from './popular-routes'; import PopularRoutes from './popular-routes';
@@ -12,7 +12,7 @@ type Props = {
const PopularRoutesServer = async ({ projectId, profileId }: Props) => { const PopularRoutesServer = async ({ projectId, profileId }: Props) => {
const data = await chQuery<{ count: number; path: string }>( 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` `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 <PopularRoutes data={data} />; return <PopularRoutes data={data} />;
}; };

View File

@@ -22,7 +22,7 @@ const PopularRoutes = ({ data }: Props) => {
style={{ style={{
width: `${(item.count / max) * 100}%`, width: `${(item.count / max) * 100}%`,
}} }}
></div> />
<div className="relative flex justify-between "> <div className="relative flex justify-between ">
<div>{item.path}</div> <div>{item.path}</div>
<div>{item.count}</div> <div>{item.count}</div>

View File

@@ -1,7 +1,7 @@
import withLoadingWidget from '@/hocs/with-loading-widget'; import withLoadingWidget from '@/hocs/with-loading-widget';
import { escape } from 'sqlstring'; import { escape } from 'sqlstring';
import { chQuery, TABLE_NAMES } from '@openpanel/db'; import { TABLE_NAMES, chQuery } from '@openpanel/db';
import ProfileActivity from './profile-activity'; import ProfileActivity from './profile-activity';
@@ -12,7 +12,7 @@ type Props = {
const ProfileActivityServer = async ({ projectId, profileId }: Props) => { const ProfileActivityServer = async ({ projectId, profileId }: Props) => {
const data = await chQuery<{ count: number; date: string }>( 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` `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 <ProfileActivity data={data} />; return <ProfileActivity data={data} />;
}; };

View File

@@ -1,6 +1,5 @@
'use client'; 'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
Widget, Widget,
@@ -20,6 +19,7 @@ import {
subMonths, subMonths,
} from 'date-fns'; } from 'date-fns';
import { ActivityIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react'; import { ActivityIcon, ChevronLeftIcon, ChevronRightIcon } from 'lucide-react';
import { useState } from 'react';
type Props = { type Props = {
data: { count: number; date: string }[]; data: { count: number; date: string }[];
@@ -64,17 +64,17 @@ const ProfileActivity = ({ data }: Props) => {
}).map((date) => { }).map((date) => {
const hit = data.find((item) => const hit = data.find((item) =>
item.date.includes( item.date.includes(
formatISO(date, { representation: 'date' }) formatISO(date, { representation: 'date' }),
) ),
); );
return ( return (
<div <div
key={date.toISOString()} key={date.toISOString()}
className={cn( className={cn(
'aspect-square w-full rounded', 'aspect-square w-full rounded',
hit ? 'bg-highlight' : 'bg-def-200' hit ? 'bg-highlight' : 'bg-def-200',
)} )}
></div> />
); );
})} })}
</div> </div>
@@ -90,17 +90,17 @@ const ProfileActivity = ({ data }: Props) => {
}).map((date) => { }).map((date) => {
const hit = data.find((item) => const hit = data.find((item) =>
item.date.includes( item.date.includes(
formatISO(date, { representation: 'date' }) formatISO(date, { representation: 'date' }),
) ),
); );
return ( return (
<div <div
key={date.toISOString()} key={date.toISOString()}
className={cn( className={cn(
'aspect-square w-full rounded', 'aspect-square w-full rounded',
hit ? 'bg-highlight' : 'bg-def-200' hit ? 'bg-highlight' : 'bg-def-200',
)} )}
></div> />
); );
})} })}
</div> </div>
@@ -116,17 +116,17 @@ const ProfileActivity = ({ data }: Props) => {
}).map((date) => { }).map((date) => {
const hit = data.find((item) => const hit = data.find((item) =>
item.date.includes( item.date.includes(
formatISO(date, { representation: 'date' }) formatISO(date, { representation: 'date' }),
) ),
); );
return ( return (
<div <div
key={date.toISOString()} key={date.toISOString()}
className={cn( className={cn(
'aspect-square w-full rounded', 'aspect-square w-full rounded',
hit ? 'bg-highlight' : 'bg-def-200' hit ? 'bg-highlight' : 'bg-def-200',
)} )}
></div> />
); );
})} })}
</div> </div>
@@ -140,17 +140,17 @@ const ProfileActivity = ({ data }: Props) => {
}).map((date) => { }).map((date) => {
const hit = data.find((item) => const hit = data.find((item) =>
item.date.includes( item.date.includes(
formatISO(date, { representation: 'date' }) formatISO(date, { representation: 'date' }),
) ),
); );
return ( return (
<div <div
key={date.toISOString()} key={date.toISOString()}
className={cn( className={cn(
'aspect-square w-full rounded', 'aspect-square w-full rounded',
hit ? 'bg-highlight' : 'bg-def-200' hit ? 'bg-highlight' : 'bg-def-200',
)} )}
></div> />
); );
})} })}
</div> </div>

View File

@@ -1,8 +1,8 @@
'use client'; 'use client';
import { memo } from 'react';
import { ReportChart } from '@/components/report-chart'; import { ReportChart } from '@/components/report-chart';
import { Widget, WidgetBody, WidgetHead } from '@/components/widget'; import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
import { memo } from 'react';
import type { IChartProps } from '@openpanel/validation'; import type { IChartProps } from '@openpanel/validation';

View File

@@ -24,7 +24,7 @@ const Events = ({ projectId, profileId }: Props) => {
const [eventNames] = useEventQueryNamesFilter(); const [eventNames] = useEventQueryNamesFilter();
const [cursor, setCursor] = useQueryState( const [cursor, setCursor] = useQueryState(
'cursor', 'cursor',
parseAsInteger.withDefault(0) parseAsInteger.withDefault(0),
); );
const query = api.event.events.useQuery( const query = api.event.events.useQuery(
{ {
@@ -37,7 +37,7 @@ const Events = ({ projectId, profileId }: Props) => {
}, },
{ {
keepPreviousData: true, keepPreviousData: true,
} },
); );
return ( return (

View File

@@ -35,7 +35,7 @@ function Info({ title, value }: { title: string; value: string }) {
const ProfileMetrics = ({ data, profile }: Props) => { const ProfileMetrics = ({ data, profile }: Props) => {
const [tab, setTab] = useQueryState( const [tab, setTab] = useQueryState(
'tab', 'tab',
parseAsStringEnum(['profile', 'properties']).withDefault('profile') parseAsStringEnum(['profile', 'properties']).withDefault('profile'),
); );
const number = useNumber(); const number = useNumber();
return ( return (
@@ -44,24 +44,26 @@ const ProfileMetrics = ({ data, profile }: Props) => {
<div className="col-span-2 @xl:col-span-3 @4xl:col-span-6"> <div className="col-span-2 @xl:col-span-3 @4xl:col-span-6">
<div className="row border-b"> <div className="row border-b">
<button <button
type="button"
onClick={() => setTab('profile')} onClick={() => setTab('profile')}
className={cn( className={cn(
'p-4', 'p-4',
'opacity-50', 'opacity-50',
tab === 'profile' && tab === 'profile' &&
'border-b border-foreground text-foreground opacity-100' 'border-b border-foreground text-foreground opacity-100',
)} )}
> >
Profile Profile
</button> </button>
<div className="h-full w-px bg-border" /> <div className="h-full w-px bg-border" />
<button <button
type="button"
onClick={() => setTab('properties')} onClick={() => setTab('properties')}
className={cn( className={cn(
'p-4', 'p-4',
'opacity-50', 'opacity-50',
tab === 'properties' && tab === 'properties' &&
'border-b border-foreground text-foreground opacity-100' 'border-b border-foreground text-foreground opacity-100',
)} )}
> >
Properties Properties
@@ -81,15 +83,12 @@ const ProfileMetrics = ({ data, profile }: Props) => {
<ListPropertiesIcon {...profile.properties} /> <ListPropertiesIcon {...profile.properties} />
</> </>
)} )}
{tab === 'properties' && ( {tab === 'properties' &&
<> Object.entries(profile.properties)
{Object.entries(profile.properties) .filter(([key, value]) => value !== undefined)
.filter(([key, value]) => value !== undefined) .map(([key, value]) => (
.map(([key, value]) => ( <Info key={key} title={key} value={value as string} />
<Info key={key} title={key} value={value as string} /> ))}
))}
</>
)}
</div> </div>
</div> </div>
<Card <Card

View File

@@ -26,11 +26,11 @@ export default function Page({
<Padding> <Padding>
<div className="mb-4"> <div className="mb-4">
<PageTabs> <PageTabs>
<PageTabsLink href={`?tab=profiles`} isActive={tab === 'profiles'}> <PageTabsLink href={'?tab=profiles'} isActive={tab === 'profiles'}>
Profiles Profiles
</PageTabsLink> </PageTabsLink>
<PageTabsLink <PageTabsLink
href={`?tab=power-users`} href={'?tab=power-users'}
isActive={tab === 'power-users'} isActive={tab === 'power-users'}
> >
Power users Power users

View File

@@ -12,7 +12,7 @@ type Props = {
const Events = ({ projectId }: Props) => { const Events = ({ projectId }: Props) => {
const [cursor, setCursor] = useQueryState( const [cursor, setCursor] = useQueryState(
'cursor', 'cursor',
parseAsInteger.withDefault(0) parseAsInteger.withDefault(0),
); );
const query = api.profile.powerUsers.useQuery( const query = api.profile.powerUsers.useQuery(
{ {
@@ -23,7 +23,7 @@ const Events = ({ projectId }: Props) => {
}, },
{ {
keepPreviousData: true, keepPreviousData: true,
} },
); );
return ( return (

View File

@@ -7,7 +7,7 @@ import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { escape } from 'sqlstring'; import { escape } from 'sqlstring';
import { chQuery, TABLE_NAMES } from '@openpanel/db'; import { TABLE_NAMES, chQuery } from '@openpanel/db';
interface Props { interface Props {
projectId: string; projectId: string;
@@ -21,7 +21,7 @@ export default async function ProfileLastSeenServer({ projectId }: Props) {
// Days since last event from users // Days since last event from users
// group by days // group by days
const res = await chQuery<Row>( const res = await chQuery<Row>(
`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` `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 maxValue = Math.max(...res.map((x) => x.count));
@@ -29,7 +29,7 @@ export default async function ProfileLastSeenServer({ projectId }: Props) {
const calculateRatio = (currentValue: number) => const calculateRatio = (currentValue: number) =>
Math.max( Math.max(
0.1, 0.1,
Math.min(1, (currentValue - minValue) / (maxValue - minValue)) Math.min(1, (currentValue - minValue) / (maxValue - minValue)),
); );
const renderItem = (item: Row) => ( const renderItem = (item: Row) => (
@@ -41,7 +41,7 @@ export default async function ProfileLastSeenServer({ projectId }: Props) {
style={{ style={{
opacity: calculateRatio(item.count), opacity: calculateRatio(item.count),
}} }}
></div> />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{item.count} profiles last seen{' '} {item.count} profiles last seen{' '}

View File

@@ -15,7 +15,7 @@ type Props = {
const Events = ({ projectId }: Props) => { const Events = ({ projectId }: Props) => {
const [cursor, setCursor] = useQueryState( const [cursor, setCursor] = useQueryState(
'cursor', 'cursor',
parseAsInteger.withDefault(0) parseAsInteger.withDefault(0),
); );
const [search, setSearch] = useQueryState('search', { const [search, setSearch] = useQueryState('search', {
defaultValue: '', defaultValue: '',
@@ -31,7 +31,7 @@ const Events = ({ projectId }: Props) => {
}, },
{ {
keepPreviousData: true, keepPreviousData: true,
} },
); );
return ( return (

View File

@@ -5,7 +5,7 @@ export type Coordinate = {
export function haversineDistance( export function haversineDistance(
coord1: Coordinate, coord1: Coordinate,
coord2: Coordinate coord2: Coordinate,
): number { ): number {
const R = 6371; // Earth's radius in kilometers const R = 6371; // Earth's radius in kilometers
const lat1Rad = coord1.lat * (Math.PI / 180); const lat1Rad = coord1.lat * (Math.PI / 180);
@@ -25,7 +25,7 @@ export function haversineDistance(
} }
export function findFarthestPoints( export function findFarthestPoints(
coordinates: Coordinate[] coordinates: Coordinate[],
): [Coordinate, Coordinate] { ): [Coordinate, Coordinate] {
if (coordinates.length < 2) { if (coordinates.length < 2) {
throw new Error('At least two coordinates are required'); throw new Error('At least two coordinates are required');
@@ -80,12 +80,12 @@ function cross(o: Coordinate, a: Coordinate, b: Coordinate): number {
// convex hull // convex hull
export function getOuterMarkers(coordinates: Coordinate[]): Coordinate[] { export function getOuterMarkers(coordinates: Coordinate[]): Coordinate[] {
coordinates = coordinates.sort(sortCoordinates); const sorted = coordinates.sort(sortCoordinates);
if (coordinates.length <= 3) return coordinates; if (sorted.length <= 3) return sorted;
const lower: Coordinate[] = []; const lower: Coordinate[] = [];
for (const coord of coordinates) { for (const coord of sorted) {
while ( while (
lower.length >= 2 && lower.length >= 2 &&
cross(lower[lower.length - 2]!, lower[lower.length - 1]!, coord) <= 0 cross(lower[lower.length - 2]!, lower[lower.length - 1]!, coord) <= 0
@@ -99,15 +99,11 @@ export function getOuterMarkers(coordinates: Coordinate[]): Coordinate[] {
for (let i = coordinates.length - 1; i >= 0; i--) { for (let i = coordinates.length - 1; i >= 0; i--) {
while ( while (
upper.length >= 2 && upper.length >= 2 &&
cross( cross(upper[upper.length - 2]!, upper[upper.length - 1]!, sorted[i]!) <= 0
upper[upper.length - 2]!,
upper[upper.length - 1]!,
coordinates[i]!
) <= 0
) { ) {
upper.pop(); upper.pop();
} }
upper.push(coordinates[i]!); upper.push(sorted[i]!);
} }
upper.pop(); upper.pop();
@@ -148,22 +144,22 @@ export function calculateCentroid(polygon: Coordinate[]): Coordinate {
} }
export function calculateGeographicMidpoint( export function calculateGeographicMidpoint(
coordinate: Coordinate[] coordinate: Coordinate[],
): Coordinate { ): Coordinate {
let minLat = Infinity, let minLat = Number.POSITIVE_INFINITY;
maxLat = -Infinity, let maxLat = Number.NEGATIVE_INFINITY;
minLong = Infinity, let minLong = Number.POSITIVE_INFINITY;
maxLong = -Infinity; let maxLong = Number.NEGATIVE_INFINITY;
coordinate.forEach(({ lat, long }) => { for (const { lat, long } of coordinate) {
if (lat < minLat) minLat = lat; if (lat < minLat) minLat = lat;
if (lat > maxLat) maxLat = lat; if (lat > maxLat) maxLat = lat;
if (long < minLong) minLong = long; if (long < minLong) minLong = long;
if (long > maxLong) maxLong = long; if (long > maxLong) maxLong = long;
}); }
// Handling the wrap around the international date line // Handling the wrap around the international date line
let midLong; let midLong: number;
if (maxLong > minLong) { if (maxLong > minLong) {
midLong = (maxLong + minLong) / 2; midLong = (maxLong + minLong) / 2;
} else { } else {
@@ -211,7 +207,7 @@ export function clusterCoordinates(coordinates: Coordinate[], radius = 25) {
long: center.long + cur.long / cluster.members.length, long: center.long + cur.long / cluster.members.length,
}; };
}, },
{ lat: 0, long: 0 } { lat: 0, long: 0 },
); );
clusters.push(cluster); clusters.push(cluster);

View File

@@ -1,7 +1,7 @@
import { subMinutes } from 'date-fns'; import { subMinutes } from 'date-fns';
import { escape } from 'sqlstring'; import { escape } from 'sqlstring';
import { chQuery, formatClickhouseDate, TABLE_NAMES } from '@openpanel/db'; import { TABLE_NAMES, chQuery, formatClickhouseDate } from '@openpanel/db';
import type { Coordinate } from './coordinates'; import type { Coordinate } from './coordinates';
import Map from './map'; import Map from './map';
@@ -11,7 +11,7 @@ type Props = {
}; };
const RealtimeMap = async ({ projectId }: Props) => { const RealtimeMap = async ({ projectId }: Props) => {
const res = await chQuery<Coordinate>( const res = await chQuery<Coordinate>(
`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` `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 <Map markers={res} />; return <Map markers={res} />;

View File

@@ -49,7 +49,7 @@ export const getBoundingBox = (coordinates: Coordinate[]) => {
export const determineZoom = ( export const determineZoom = (
bbox: ReturnType<typeof getBoundingBox>, bbox: ReturnType<typeof getBoundingBox>,
aspectRatio = 1.0 aspectRatio = 1.0,
): number => { ): number => {
const latDiff = bbox.maxLat - bbox.minLat; const latDiff = bbox.maxLat - bbox.minLat;
const longDiff = bbox.maxLong - bbox.minLong; const longDiff = bbox.maxLong - bbox.minLong;

View File

@@ -1,11 +1,11 @@
'use client'; 'use client';
import { Fragment, useEffect, useRef, useState } from 'react';
import { useFullscreen } from '@/components/fullscreen-toggle'; import { useFullscreen } from '@/components/fullscreen-toggle';
import { Tooltiper } from '@/components/ui/tooltip'; import { Tooltiper } from '@/components/ui/tooltip';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { bind } from 'bind-event-listener'; import { bind } from 'bind-event-listener';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import { Fragment, useEffect, useRef, useState } from 'react';
import { import {
ComposableMap, ComposableMap,
Geographies, Geographies,
@@ -22,8 +22,8 @@ import {
} from './coordinates'; } from './coordinates';
import { import {
CustomZoomableGroup, CustomZoomableGroup,
determineZoom,
GEO_MAP_URL, GEO_MAP_URL,
determineZoom,
getBoundingBox, getBoundingBox,
useAnimatedState, useAnimatedState,
} from './map.helpers'; } from './map.helpers';
@@ -37,7 +37,7 @@ const Map = ({ markers }: Props) => {
const showCenterMarker = false; const showCenterMarker = false;
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const [size, setSize] = useState<{ width: number; height: number } | null>( const [size, setSize] = useState<{ width: number; height: number } | null>(
null null,
); );
// const { markers, toggle } = useActiveMarkers(_m); // const { markers, toggle } = useActiveMarkers(_m);
@@ -50,7 +50,7 @@ const Map = ({ markers }: Props) => {
const [zoom] = useAnimatedState( const [zoom] = useAnimatedState(
markers.length === 1 markers.length === 1
? 20 ? 20
: determineZoom(boundingBox, size ? size?.height / size?.width : 1) : determineZoom(boundingBox, size ? size?.height / size?.width : 1),
); );
const [long] = useAnimatedState(center.long); const [long] = useAnimatedState(center.long);
@@ -99,7 +99,7 @@ const Map = ({ markers }: Props) => {
<div <div
className={cn( className={cn(
'fixed bottom-0 left-0 right-0 top-0', 'fixed bottom-0 left-0 right-0 top-0',
!isFullscreen && 'lg:left-72' !isFullscreen && 'lg:left-72',
)} )}
ref={ref} ref={ref}
> >
@@ -142,7 +142,7 @@ const Map = ({ markers }: Props) => {
)} )}
{clusterCoordinates(markers).map((marker) => { {clusterCoordinates(markers).map((marker) => {
const size = adjustSizeBasedOnZoom( const size = adjustSizeBasedOnZoom(
calculateMarkerSize(marker.count) calculateMarkerSize(marker.count),
); );
const coordinates: [number, number] = [ const coordinates: [number, number] = [
marker.center.long, marker.center.long,

View File

@@ -11,7 +11,7 @@ const useActiveMarkers = (initialMarkers: Coordinate[]) => {
// Cut the array in half randomly to simulate changes in active markers // Cut the array in half randomly to simulate changes in active markers
const selected = shuffled.slice( const selected = shuffled.slice(
0, 0,
Math.floor(Math.random() * shuffled.length) + 1 Math.floor(Math.random() * shuffled.length) + 1,
); );
setActiveMarkers(selected); setActiveMarkers(selected);
}, [activeMarkers]); }, [activeMarkers]);

View File

@@ -1,10 +1,10 @@
import { Suspense } from 'react';
import { import {
Fullscreen, Fullscreen,
FullscreenClose, FullscreenClose,
FullscreenOpen, FullscreenOpen,
} from '@/components/fullscreen-toggle'; } from '@/components/fullscreen-toggle';
import { ReportChart } from '@/components/report-chart'; import { ReportChart } from '@/components/report-chart';
import { Suspense } from 'react';
import RealtimeMap from './map'; import RealtimeMap from './map';
import RealtimeLiveEventsServer from './realtime-live-events'; import RealtimeLiveEventsServer from './realtime-live-events';

View File

@@ -1,6 +1,6 @@
import { escape } from 'sqlstring'; import { escape } from 'sqlstring';
import { getEvents, TABLE_NAMES } from '@openpanel/db'; import { TABLE_NAMES, getEvents } from '@openpanel/db';
import LiveEvents from './live-events'; import LiveEvents from './live-events';
@@ -13,7 +13,7 @@ const RealtimeLiveEventsServer = async ({ projectId, limit = 30 }: Props) => {
`SELECT * FROM ${TABLE_NAMES.events} WHERE created_at > now() - INTERVAL 2 HOUR AND project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT ${limit}`, `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, profile: true,
} },
); );
return <LiveEvents events={events} projectId={projectId} limit={limit} />; return <LiveEvents events={events} projectId={projectId} limit={limit} />;
}; };

View File

@@ -1,9 +1,9 @@
'use client'; 'use client';
import { useState } from 'react';
import { EventListItem } from '@/components/events/event-list-item'; import { EventListItem } from '@/components/events/event-list-item';
import useWS from '@/hooks/useWS'; import useWS from '@/hooks/useWS';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { useState } from 'react';
import type { IServiceEvent, IServiceEventMinimal } from '@openpanel/db'; import type { IServiceEvent, IServiceEventMinimal } from '@openpanel/db';
@@ -19,7 +19,7 @@ const RealtimeLiveEvents = ({ events, projectId, limit }: Props) => {
`/live/events/${projectId}`, `/live/events/${projectId}`,
(event) => { (event) => {
setState((p) => [event, ...p].slice(0, limit)); setState((p) => [event, ...p].slice(0, limit));
} },
); );
return ( return (
<AnimatePresence mode="popLayout" initial={false}> <AnimatePresence mode="popLayout" initial={false}>

View File

@@ -71,18 +71,16 @@ export function RealtimeLiveHistogram({
const liveCount = countRes.data?.series[0]?.metrics?.sum ?? 0; const liveCount = countRes.data?.series[0]?.metrics?.sum ?? 0;
if (res.isInitialLoading || countRes.isInitialLoading || liveCount === 0) { if (res.isInitialLoading || countRes.isInitialLoading || liveCount === 0) {
// prettier-ignore
const staticArray = [ const staticArray = [
10, 25, 30, 45, 20, 5, 55, 18, 40, 12, 10, 25, 30, 45, 20, 5, 55, 18, 40, 12, 50, 35, 8, 22, 38, 42, 15, 28, 52,
50, 35, 8, 22, 38, 42, 15, 28, 52, 5, 5, 48, 14, 32, 58, 7, 19, 33, 56, 24, 5,
48, 14, 32, 58, 7, 19, 33, 56, 24, 5
]; ];
return ( return (
<Wrapper count={0}> <Wrapper count={0}>
{staticArray.map((percent, i) => ( {staticArray.map((percent, i) => (
<div <div
key={i} key={i as number}
className="flex-1 animate-pulse rounded bg-def-200" className="flex-1 animate-pulse rounded bg-def-200"
style={{ height: `${percent}%` }} style={{ height: `${percent}%` }}
/> />
@@ -104,7 +102,7 @@ export function RealtimeLiveHistogram({
<div <div
className={cn( className={cn(
'flex-1 rounded transition-all ease-in-out hover:scale-110', 'flex-1 rounded transition-all ease-in-out hover:scale-110',
minute.count === 0 ? 'bg-def-200' : 'bg-highlight' minute.count === 0 ? 'bg-def-200' : 'bg-highlight',
)} )}
style={{ style={{
height: height:

View File

@@ -27,7 +27,7 @@ const RealtimeReloader = ({ projectId }: Props) => {
maxWait: 15000, maxWait: 15000,
delay: 15000, delay: 15000,
}, },
} },
); );
return null; return null;

View File

@@ -1,6 +1,5 @@
'use client'; 'use client';
import { useEffect } from 'react';
import { StickyBelowHeader } from '@/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header'; import { StickyBelowHeader } from '@/app/(app)/[organizationSlug]/[projectId]/layout-sticky-below-header';
import { ReportChart } from '@/components/report-chart'; import { ReportChart } from '@/components/report-chart';
import { ReportChartType } from '@/components/report/ReportChartType'; import { ReportChartType } from '@/components/report/ReportChartType';
@@ -25,6 +24,7 @@ import { useDispatch, useSelector } from '@/redux';
import { bind } from 'bind-event-listener'; import { bind } from 'bind-event-listener';
import { endOfDay, startOfDay } from 'date-fns'; import { endOfDay, startOfDay } from 'date-fns';
import { GanttChartSquareIcon } from 'lucide-react'; import { GanttChartSquareIcon } from 'lucide-react';
import { useEffect } from 'react';
import type { IServiceReport } from '@openpanel/db'; import type { IServiceReport } from '@openpanel/db';

View File

@@ -55,12 +55,12 @@ const Chart = ({ data }: Props) => {
offset="0%" offset="0%"
stopColor={getChartColor(0)} stopColor={getChartColor(0)}
stopOpacity={0.8} stopOpacity={0.8}
></stop> />
<stop <stop
offset="100%" offset="100%"
stopColor={getChartColor(0)} stopColor={getChartColor(0)}
stopOpacity={0.1} stopOpacity={0.1}
></stop> />
</linearGradient> </linearGradient>
</defs> </defs>
@@ -70,7 +70,7 @@ const Chart = ({ data }: Props) => {
dataKey="users" dataKey="users"
stroke={getChartColor(0)} stroke={getChartColor(0)}
strokeWidth={2} strokeWidth={2}
fill={`url(#bg)`} fill={'url(#bg)'}
isAnimationActive={false} isAnimationActive={false}
/> />
<XAxis <XAxis

View File

@@ -72,36 +72,36 @@ const Chart = ({ data }: Props) => {
offset="0%" offset="0%"
stopColor={getChartColor(0)} stopColor={getChartColor(0)}
stopOpacity={0.8} stopOpacity={0.8}
></stop> />
<stop <stop
offset="100%" offset="100%"
stopColor={getChartColor(0)} stopColor={getChartColor(0)}
stopOpacity={0.1} stopOpacity={0.1}
></stop> />
</linearGradient> </linearGradient>
<linearGradient id="wau" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="wau" x1="0" y1="0" x2="0" y2="1">
<stop <stop
offset="0%" offset="0%"
stopColor={getChartColor(1)} stopColor={getChartColor(1)}
stopOpacity={0.8} stopOpacity={0.8}
></stop> />
<stop <stop
offset="100%" offset="100%"
stopColor={getChartColor(1)} stopColor={getChartColor(1)}
stopOpacity={0.1} stopOpacity={0.1}
></stop> />
</linearGradient> </linearGradient>
<linearGradient id="mau" x1="0" y1="0" x2="0" y2="1"> <linearGradient id="mau" x1="0" y1="0" x2="0" y2="1">
<stop <stop
offset="0%" offset="0%"
stopColor={getChartColor(2)} stopColor={getChartColor(2)}
stopOpacity={0.8} stopOpacity={0.8}
></stop> />
<stop <stop
offset="100%" offset="100%"
stopColor={getChartColor(2)} stopColor={getChartColor(2)}
stopOpacity={0.1} stopOpacity={0.1}
></stop> />
</linearGradient> </linearGradient>
</defs> </defs>
@@ -111,21 +111,21 @@ const Chart = ({ data }: Props) => {
dataKey="dau" dataKey="dau"
stroke={getChartColor(0)} stroke={getChartColor(0)}
strokeWidth={2} strokeWidth={2}
fill={`url(#dau)`} fill={'url(#dau)'}
isAnimationActive={false} isAnimationActive={false}
/> />
<Area <Area
dataKey="wau" dataKey="wau"
stroke={getChartColor(1)} stroke={getChartColor(1)}
strokeWidth={2} strokeWidth={2}
fill={`url(#wau)`} fill={'url(#wau)'}
isAnimationActive={false} isAnimationActive={false}
/> />
<Area <Area
dataKey="mau" dataKey="mau"
stroke={getChartColor(2)} stroke={getChartColor(2)}
strokeWidth={2} strokeWidth={2}
fill={`url(#mau)`} fill={'url(#mau)'}
isAnimationActive={false} isAnimationActive={false}
/> />
<XAxis {...xAxisProps} dataKey="date" /> <XAxis {...xAxisProps} dataKey="date" />

View File

@@ -70,12 +70,12 @@ const Chart = ({ data }: Props) => {
offset="0%" offset="0%"
stopColor={getChartColor(0)} stopColor={getChartColor(0)}
stopOpacity={0.8} stopOpacity={0.8}
></stop> />
<stop <stop
offset="100%" offset="100%"
stopColor={getChartColor(0)} stopColor={getChartColor(0)}
stopOpacity={0.1} stopOpacity={0.1}
></stop> />
</linearGradient> </linearGradient>
</defs> </defs>
@@ -85,7 +85,7 @@ const Chart = ({ data }: Props) => {
dataKey="retention" dataKey="retention"
stroke={getChartColor(0)} stroke={getChartColor(0)}
strokeWidth={2} strokeWidth={2}
fill={`url(#bg)`} fill={'url(#bg)'}
isAnimationActive={false} isAnimationActive={false}
/> />
<XAxis <XAxis

View File

@@ -17,7 +17,7 @@ const Cell = ({ value, ratio }: { value: number; ratio: number }) => {
<div <div
className="absolute inset-0 z-0 bg-highlight" className="absolute inset-0 z-0 bg-highlight"
style={{ opacity: ratio }} style={{ opacity: ratio }}
></div> />
<div className="relative z-10">{value}</div> <div className="relative z-10">{value}</div>
</td> </td>
); );
@@ -39,7 +39,7 @@ const WeeklyCohortsServer = async ({ projectId }: Props) => {
row.period_7, row.period_7,
row.period_8, row.period_8,
row.period_9, row.period_9,
]) ]),
); );
const calculateRatio = (currentValue: number) => const calculateRatio = (currentValue: number) =>
@@ -47,7 +47,7 @@ const WeeklyCohortsServer = async ({ projectId }: Props) => {
? 0 ? 0
: Math.max( : Math.max(
0.1, 0.1,
Math.min(1, (currentValue - minValue) / (maxValue - minValue)) Math.min(1, (currentValue - minValue) / (maxValue - minValue)),
); );
return ( return (

View File

@@ -6,7 +6,6 @@ import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import { import {
closeSheet,
Sheet, Sheet,
SheetContent, SheetContent,
SheetDescription, SheetDescription,
@@ -14,6 +13,7 @@ import {
SheetHeader, SheetHeader,
SheetTitle, SheetTitle,
SheetTrigger, SheetTrigger,
closeSheet,
} from '@/components/ui/sheet'; } from '@/components/ui/sheet';
import { useAppParams } from '@/hooks/useAppParams'; import { useAppParams } from '@/hooks/useAppParams';
import { api } from '@/trpc/client'; import { api } from '@/trpc/client';

View File

@@ -52,7 +52,7 @@ export default async function Page({
} }
const member = organization.members.find( const member = organization.members.find(
(member) => member.userId === session.userId (member) => member.userId === session.userId,
); );
const hasAccess = member?.role === 'org:admin'; const hasAccess = member?.role === 'org:admin';
@@ -69,13 +69,13 @@ export default async function Page({
return ( return (
<Padding> <Padding>
<PageTabs className="mb-4"> <PageTabs className="mb-4">
<PageTabsLink href={`?tab=org`} isActive={tab === 'org'}> <PageTabsLink href={'?tab=org'} isActive={tab === 'org'}>
Organization Organization
</PageTabsLink> </PageTabsLink>
<PageTabsLink href={`?tab=members`} isActive={tab === 'members'}> <PageTabsLink href={'?tab=members'} isActive={tab === 'members'}>
Members Members
</PageTabsLink> </PageTabsLink>
<PageTabsLink href={`?tab=invites`} isActive={tab === 'invites'}> <PageTabsLink href={'?tab=invites'} isActive={tab === 'invites'}>
Invites Invites
</PageTabsLink> </PageTabsLink>
</PageTabs> </PageTabs>

View File

@@ -10,9 +10,7 @@ export function Logout() {
<span className="title">Sad part</span> <span className="title">Sad part</span>
</WidgetHead> </WidgetHead>
<WidgetBody> <WidgetBody>
<p className="mb-4"> <p className="mb-4">Sometimes you need to go. See you next time</p>
Sometimes you need to go. See you next time
</p>
<SignOutButton /> <SignOutButton />
</WidgetBody> </WidgetBody>
</Widget> </Widget>

View File

@@ -44,7 +44,7 @@ export default function ListProjects({ projects, clients }: ListProjectsProps) {
<Accordion type="single" collapsible className="-mx-4"> <Accordion type="single" collapsible className="-mx-4">
{projects.map((project) => { {projects.map((project) => {
const pClients = clients.filter( const pClients = clients.filter(
(client) => client.projectId === project.id (client) => client.projectId === project.id,
); );
return ( return (
<AccordionItem <AccordionItem
@@ -61,7 +61,7 @@ export default function ListProjects({ projects, clients }: ListProjectsProps) {
: 'No clients created yet'} : 'No clients created yet'}
</span> </span>
</div> </div>
<div className="mx-4"></div> <div className="mx-4" />
</AccordionTrigger> </AccordionTrigger>
<AccordionContent className="px-4"> <AccordionContent className="px-4">
<ProjectActions {...project} /> <ProjectActions {...project} />
@@ -91,6 +91,7 @@ export default function ListProjects({ projects, clients }: ListProjectsProps) {
); );
})} })}
<button <button
type="button"
onClick={() => { onClick={() => {
pushModal('AddClient', { pushModal('AddClient', {
projectId: project.id, projectId: project.id,

View File

@@ -1,9 +1,9 @@
'use client'; 'use client';
import { useEffect } from 'react';
import { pushModal, useOnPushModal } from '@/modals'; import { pushModal, useOnPushModal } from '@/modals';
import { useUser } from '@clerk/nextjs'; import { useUser } from '@clerk/nextjs';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';
import { useEffect } from 'react';
import { useOpenPanel } from '@openpanel/nextjs'; import { useOpenPanel } from '@openpanel/nextjs';
@@ -12,7 +12,7 @@ export default function SideEffects() {
const { user } = useUser(); const { user } = useUser();
const accountAgeInDays = differenceInDays( const accountAgeInDays = differenceInDays(
new Date(), new Date(),
user?.createdAt || new Date() user?.createdAt || new Date(),
); );
useOnPushModal('Testimonial', (open) => { useOnPushModal('Testimonial', (open) => {
if (!open) { if (!open) {

View File

@@ -1,8 +1,8 @@
'use client'; 'use client';
import { useEffect, useState } from 'react';
import { EventListItem } from '@/components/events/event-list-item'; import { EventListItem } from '@/components/events/event-list-item';
import { AnimatePresence, motion } from 'framer-motion'; import { AnimatePresence, motion } from 'framer-motion';
import { useEffect, useState } from 'react';
import type { IServiceEventMinimal } from '@openpanel/db'; import type { IServiceEventMinimal } from '@openpanel/db';

View File

@@ -11,8 +11,8 @@ const Page = ({ children }: Props) => {
return ( return (
<> <>
<div className="fixed inset-0 hidden md:grid md:grid-cols-[30vw_1fr] lg:grid-cols-[30vw_1fr]"> <div className="fixed inset-0 hidden md:grid md:grid-cols-[30vw_1fr] lg:grid-cols-[30vw_1fr]">
<div className=""></div> <div className="" />
<div className="border-l border-border bg-card"></div> <div className="border-l border-border bg-card" />
</div> </div>
<div className="relative min-h-screen bg-card md:bg-transparent"> <div className="relative min-h-screen bg-card md:bg-transparent">
<FullWidthNavbar> <FullWidthNavbar>

View File

@@ -21,6 +21,7 @@ const ConnectApp = ({ client }: Props) => {
<div className="mt-4 grid gap-4 md:grid-cols-2"> <div className="mt-4 grid gap-4 md:grid-cols-2">
{frameworks.app.map((framework) => ( {frameworks.app.map((framework) => (
<button <button
type="button"
onClick={() => onClick={() =>
pushModal('Instructions', { pushModal('Instructions', {
framework, framework,

View File

@@ -21,6 +21,7 @@ const ConnectBackend = ({ client }: Props) => {
<div className="mt-4 grid gap-4 md:grid-cols-2"> <div className="mt-4 grid gap-4 md:grid-cols-2">
{frameworks.backend.map((framework) => ( {frameworks.backend.map((framework) => (
<button <button
type="button"
onClick={() => onClick={() =>
pushModal('Instructions', { pushModal('Instructions', {
framework, framework,

View File

@@ -21,6 +21,7 @@ const ConnectWeb = ({ client }: Props) => {
<div className="mt-4 grid gap-4 md:grid-cols-2"> <div className="mt-4 grid gap-4 md:grid-cols-2">
{frameworks.website.map((framework) => ( {frameworks.website.map((framework) => (
<button <button
type="button"
onClick={() => onClick={() =>
pushModal('Instructions', { pushModal('Instructions', {
framework, framework,

View File

@@ -1,12 +1,12 @@
'use client'; 'use client';
import { useState } from 'react';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import useWS from '@/hooks/useWS'; import useWS from '@/hooks/useWS';
import { pushModal } from '@/modals'; import { pushModal } from '@/modals';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { timeAgo } from '@/utils/date'; import { timeAgo } from '@/utils/date';
import { CheckCircle2Icon, CheckIcon, Loader2 } from 'lucide-react'; import { CheckCircle2Icon, CheckIcon, Loader2 } from 'lucide-react';
import { useState } from 'react';
import type { import type {
IServiceClient, IServiceClient,
@@ -28,7 +28,7 @@ const VerifyListener = ({ client, events: _events, onVerified }: Props) => {
(data) => { (data) => {
setEvents((prev) => [...prev, data]); setEvents((prev) => [...prev, data]);
onVerified(true); onVerified(true);
} },
); );
const isConnected = events.length > 0; const isConnected = events.length > 0;
@@ -70,7 +70,7 @@ const VerifyListener = ({ client, events: _events, onVerified }: Props) => {
'my-6 flex gap-6 rounded-xl p-4 md:p-6', 'my-6 flex gap-6 rounded-xl p-4 md:p-6',
isConnected isConnected
? 'bg-emerald-100 dark:bg-emerald-700' ? 'bg-emerald-100 dark:bg-emerald-700'
: 'bg-blue-100 dark:bg-blue-700' : 'bg-blue-100 dark:bg-blue-700',
)} )}
> >
{renderIcon()} {renderIcon()}
@@ -87,7 +87,7 @@ const VerifyListener = ({ client, events: _events, onVerified }: Props) => {
</div> </div>
)} )}
{events.slice(-5).map((event, index) => ( {events.slice(-5).map((event, index) => (
<div key={index} className="flex items-center gap-2 "> <div key={event.id} className="flex items-center gap-2 ">
<CheckIcon size={14} />{' '} <CheckIcon size={14} />{' '}
<span className="font-medium">{event.name}</span>{' '} <span className="font-medium">{event.name}</span>{' '}
<span className="ml-auto text-emerald-800"> <span className="ml-auto text-emerald-800">
@@ -108,6 +108,7 @@ const VerifyListener = ({ client, events: _events, onVerified }: Props) => {
<div className="mt-2 text-sm text-muted-foreground"> <div className="mt-2 text-sm text-muted-foreground">
You can{' '} You can{' '}
<button <button
type="button"
className="underline" className="underline"
onClick={() => { onClick={() => {
pushModal('OnboardingTroubleshoot', { pushModal('OnboardingTroubleshoot', {

View File

@@ -1,10 +1,10 @@
'use client'; 'use client';
import { useState } from 'react';
import { ButtonContainer } from '@/components/button-container'; import { ButtonContainer } from '@/components/button-container';
import { LinkButton } from '@/components/ui/button'; import { LinkButton } from '@/components/ui/button';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import Link from 'next/link'; import Link from 'next/link';
import { useState } from 'react';
import type { IServiceEvent, IServiceProjectWithClients } from '@openpanel/db'; import type { IServiceEvent, IServiceProjectWithClients } from '@openpanel/db';
@@ -84,7 +84,7 @@ const Verify = ({ project, events }: Props) => {
size="lg" size="lg"
className={cn( className={cn(
'min-w-28 self-start', 'min-w-28 self-start',
!verified && 'pointer-events-none select-none opacity-20' !verified && 'pointer-events-none select-none opacity-20',
)} )}
> >
Your dashboard Your dashboard

View File

@@ -2,10 +2,10 @@ import { cookies } from 'next/headers';
import { escape } from 'sqlstring'; import { escape } from 'sqlstring';
import { import {
TABLE_NAMES,
getCurrentOrganizations, getCurrentOrganizations,
getEvents, getEvents,
getProjectWithClients, getProjectWithClients,
TABLE_NAMES,
} from '@openpanel/db'; } from '@openpanel/db';
import OnboardingVerify from './onboarding-verify'; import OnboardingVerify from './onboarding-verify';
@@ -25,7 +25,7 @@ const Verify = async ({ params: { projectId } }: Props) => {
const [project, events] = await Promise.all([ const [project, events] = await Promise.all([
await getProjectWithClients(projectId), await getProjectWithClients(projectId),
getEvents( getEvents(
`SELECT * FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} LIMIT 100` `SELECT * FROM ${TABLE_NAMES.events} WHERE project_id = ${escape(projectId)} LIMIT 100`,
), ),
]); ]);

View File

@@ -1,6 +1,5 @@
'use client'; 'use client';
import { useEffect } from 'react';
import AnimateHeight from '@/components/animate-height'; import AnimateHeight from '@/components/animate-height';
import { ButtonContainer } from '@/components/button-container'; import { ButtonContainer } from '@/components/button-container';
import { CheckboxItem } from '@/components/forms/checkbox-item'; import { CheckboxItem } from '@/components/forms/checkbox-item';
@@ -19,6 +18,7 @@ import {
SmartphoneIcon, SmartphoneIcon,
} from 'lucide-react'; } from 'lucide-react';
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
import type { SubmitHandler } from 'react-hook-form'; import type { SubmitHandler } from 'react-hook-form';
import { Controller, useForm, useWatch } from 'react-hook-form'; import { Controller, useForm, useWatch } from 'react-hook-form';
import type { z } from 'zod'; import type { z } from 'zod';
@@ -185,7 +185,7 @@ const Tracking = ({
} }
return `https://${trimmed}`; return `https://${trimmed}`;
}) })
.join(',') .join(','),
); );
}} }}
/> />

View File

@@ -1,11 +1,11 @@
'use client'; 'use client';
import { useEffect } from 'react';
import { showConfirm } from '@/modals'; import { showConfirm } from '@/modals';
import { api } from '@/trpc/client'; import { api } from '@/trpc/client';
import { useAuth } from '@clerk/nextjs'; import { useAuth } from '@clerk/nextjs';
import { ChevronLastIcon } from 'lucide-react'; import { ChevronLastIcon } from 'lucide-react';
import { usePathname, useRouter } from 'next/navigation'; import { usePathname, useRouter } from 'next/navigation';
import { useEffect } from 'react';
const SkipOnboarding = () => { const SkipOnboarding = () => {
const router = useRouter(); const router = useRouter();
@@ -20,6 +20,7 @@ const SkipOnboarding = () => {
return ( return (
<button <button
type="button"
onClick={() => { onClick={() => {
if (res.data?.canSkip && res.data?.url) { if (res.data?.canSkip && res.data?.url) {
router.push(res.data.url); router.push(res.data.url);

View File

@@ -39,7 +39,7 @@ function useSteps(path: string) {
]; ];
const matchIndex = steps.findLastIndex((step) => const matchIndex = steps.findLastIndex((step) =>
path.match(new RegExp(step.match)) path.match(new RegExp(step.match)),
); );
return steps.map((step, index) => { return steps.map((step, index) => {
@@ -59,17 +59,17 @@ const Steps = ({ className }: Props) => {
const currentIndex = steps.findIndex((i) => i.status === 'current'); const currentIndex = steps.findIndex((i) => i.status === 'current');
return ( return (
<div className="relative"> <div className="relative">
<div className="absolute bottom-4 left-4 top-4 w-px bg-def-200"></div> <div className="absolute bottom-4 left-4 top-4 w-px bg-def-200" />
<div <div
className="absolute left-4 top-4 w-px bg-highlight" className="absolute left-4 top-4 w-px bg-highlight"
style={{ style={{
height: `calc(${((currentIndex + 1) / steps.length) * 100}% - 3.5rem)`, height: `calc(${((currentIndex + 1) / steps.length) * 100}% - 3.5rem)`,
}} }}
></div> />
<div <div
className={cn( className={cn(
'relative flex gap-4 overflow-hidden md:-ml-3 md:flex-col md:gap-8', 'relative flex gap-4 overflow-hidden md:-ml-3 md:flex-col md:gap-8',
className className,
)} )}
> >
{steps.map((step, index) => ( {steps.map((step, index) => (
@@ -80,32 +80,31 @@ const Steps = ({ className }: Props) => {
'rounded-xl border border-border bg-card', 'rounded-xl border border-border bg-card',
step.status === 'completed' && step.status === 'completed' &&
index !== currentIndex - 1 && index !== currentIndex - 1 &&
'max-md:hidden' 'max-md:hidden',
)} )}
key={step.name} key={step.name}
> >
<div <div
className={cn( className={cn(
'relative flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-white' 'relative flex h-8 w-8 shrink-0 items-center justify-center rounded-full text-white',
)} )}
> >
<div <div
className={cn( className={cn(
'absolute inset-0 z-0 rounded-full bg-highlight', 'absolute inset-0 z-0 rounded-full bg-highlight',
step.status === 'pending' && 'bg-def-400' step.status === 'pending' && 'bg-def-400',
)} )}
></div> />
{step.status === 'current' && ( {step.status === 'current' && (
<div className="absolute inset-1 z-0 animate-ping-slow rounded-full bg-highlight"></div> <div className="absolute inset-1 z-0 animate-ping-slow rounded-full bg-highlight" />
)} )}
<div className="relative"> <div className="relative">
{step.status === 'completed' && <CheckCheckIcon size={14} />} {step.status === 'completed' && <CheckCheckIcon size={14} />}
{/* {step.status === 'current' && ( {/* {step.status === 'current' && (
<ArrowRightCircleIcon size={14} /> <ArrowRightCircleIcon size={14} />
)} */} )} */}
{(step.status === 'pending' || step.status === 'current') && ( {(step.status === 'pending' || step.status === 'current') &&
<>{index + 1}</> index + 1}
)}
</div> </div>
</div> </div>

View File

@@ -15,7 +15,7 @@ export async function POST(request: Request) {
if (!email) { if (!email) {
return Response.json( return Response.json(
{ message: 'No email address found' }, { message: 'No email address found' },
{ status: 400 } { status: 400 },
); );
} }

View File

@@ -4,7 +4,7 @@ import NextTopLoader from 'nextjs-toploader';
import Providers from './providers'; import Providers from './providers';
import '@/styles/globals.css'; import '@/styles/globals.css';
import '/node_modules/flag-icons/css/flag-icons.min.css'; import 'flag-icons/css/flag-icons.min.css';
import { GeistMono } from 'geist/font/mono'; import { GeistMono } from 'geist/font/mono';
import { GeistSans } from 'geist/font/sans'; import { GeistSans } from 'geist/font/sans';
@@ -31,7 +31,7 @@ export default function RootLayout({
className={cn( className={cn(
'grainy min-h-screen bg-def-100 font-sans text-base antialiased', 'grainy min-h-screen bg-def-100 font-sans text-base antialiased',
GeistSans.variable, GeistSans.variable,
GeistMono.variable GeistMono.variable,
)} )}
> >
<NextTopLoader <NextTopLoader

View File

@@ -1,6 +1,5 @@
'use client'; 'use client';
import React, { useRef, useState } from 'react';
import { TooltipProvider } from '@/components/ui/tooltip'; import { TooltipProvider } from '@/components/ui/tooltip';
import { ModalProvider } from '@/modals'; import { ModalProvider } from '@/modals';
import type { AppStore } from '@/redux'; import type { AppStore } from '@/redux';
@@ -10,6 +9,8 @@ import { ClerkProvider, useAuth } from '@clerk/nextjs';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { httpLink } from '@trpc/client'; import { httpLink } from '@trpc/client';
import { ThemeProvider } from 'next-themes'; import { ThemeProvider } from 'next-themes';
import type React from 'react';
import { useRef, useState } from 'react';
import { Provider as ReduxProvider } from 'react-redux'; import { Provider as ReduxProvider } from 'react-redux';
import { Toaster } from 'sonner'; import { Toaster } from 'sonner';
import superjson from 'superjson'; import superjson from 'superjson';
@@ -28,7 +29,7 @@ function AllProviders({ children }: { children: React.ReactNode }) {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
}, },
}, },
}) }),
); );
const [trpcClient] = useState(() => const [trpcClient] = useState(() =>
api.createClient({ api.createClient({
@@ -47,7 +48,7 @@ function AllProviders({ children }: { children: React.ReactNode }) {
}, },
}), }),
], ],
}) }),
); );
const storeRef = useRef<AppStore>(); const storeRef = useRef<AppStore>();

View File

@@ -21,7 +21,7 @@ export function Card({ children, hover, className }: CardProps) {
className={cn( className={cn(
'card relative', 'card relative',
hover && 'transition-all hover:-translate-y-0.5', hover && 'transition-all hover:-translate-y-0.5',
className className,
)} )}
> >
{children} {children}

View File

@@ -51,23 +51,23 @@ export function ChartSSR({
> >
<defs> <defs>
<linearGradient <linearGradient
id={`gradient`} id={'gradient'}
x1="0" x1="0"
y1="0" y1="0"
x2="0" x2="0"
y2="100%" y2="100%"
gradientUnits="userSpaceOnUse" gradientUnits="userSpaceOnUse"
> >
<stop offset="0%" stopColor={'blue'} stopOpacity={0.2}></stop> <stop offset="0%" stopColor={'blue'} stopOpacity={0.2} />
<stop offset="50%" stopColor={'blue'} stopOpacity={0.05}></stop> <stop offset="50%" stopColor={'blue'} stopOpacity={0.05} />
<stop offset="100%" stopColor={'blue'} stopOpacity={0}></stop> <stop offset="100%" stopColor={'blue'} stopOpacity={0} />
</linearGradient> </linearGradient>
</defs> </defs>
{/* Gradient area */} {/* Gradient area */}
{pathArea && ( {pathArea && (
<path <path
d={pathArea} d={pathArea}
fill={`url(#gradient)`} fill={'url(#gradient)'}
vectorEffect="non-scaling-stroke" vectorEffect="non-scaling-stroke"
/> />
)} )}

View File

@@ -39,6 +39,7 @@ export function CreateClientSuccess({ id, secret, cors }: Props) {
target="_blank" target="_blank"
href="https://docs.openpanel.dev" href="https://docs.openpanel.dev"
className="underline" className="underline"
rel="noreferrer"
> >
documentation documentation
</a>{' '} </a>{' '}

View File

@@ -8,7 +8,7 @@ export function ColorSquare({ children, className }: ColorSquareProps) {
<div <div
className={cn( className={cn(
'flex h-5 w-5 flex-shrink-0 items-center justify-center rounded bg-blue-600 text-sm font-medium text-white [.mini_&]:h-4 [.mini_&]:w-4 [.mini_&]:text-[0.6rem]', 'flex h-5 w-5 flex-shrink-0 items-center justify-center rounded bg-blue-600 text-sm font-medium text-white [.mini_&]:h-4 [.mini_&]:w-4 [.mini_&]:text-[0.6rem]',
className className,
)} )}
> >
{children} {children}

View File

@@ -1,6 +1,5 @@
'use client'; 'use client';
import * as React from 'react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { import {
DropdownMenu, DropdownMenu,
@@ -10,6 +9,7 @@ import {
} from '@/components/ui/dropdown-menu'; } from '@/components/ui/dropdown-menu';
import { MoonIcon, SunIcon } from 'lucide-react'; import { MoonIcon, SunIcon } from 'lucide-react';
import { useTheme } from 'next-themes'; import { useTheme } from 'next-themes';
import * as React from 'react';
interface Props { interface Props {
className?: string; className?: string;

View File

@@ -47,7 +47,7 @@ export function DataTable<TData>({ columns, data }: DataTableProps<TData>) {
? null ? null
: flexRender( : flexRender(
header.column.columnDef.header, header.column.columnDef.header,
header.getContext() header.getContext(),
)} )}
</GridCell> </GridCell>
))} ))}

View File

@@ -22,7 +22,7 @@ export function Dot({ className, size = 8, animated }: DotProps) {
<div <div
className={cn( className={cn(
'relative', 'relative',
filterCn(['bg-', 'animate-', 'group-hover/row'], className) filterCn(['bg-', 'animate-', 'group-hover/row'], className),
)} )}
style={style} style={style}
> >
@@ -30,14 +30,14 @@ export function Dot({ className, size = 8, animated }: DotProps) {
className={cn( className={cn(
'absolute !m-0 rounded-full', 'absolute !m-0 rounded-full',
animated !== false && 'animate-ping', animated !== false && 'animate-ping',
className className,
)} )}
style={style} style={style}
/> />
<div <div
className={cn( className={cn(
'absolute !m-0 rounded-full', 'absolute !m-0 rounded-full',
filterCn(['animate-', 'group-hover/row'], className) filterCn(['animate-', 'group-hover/row'], className),
)} )}
style={style} style={style}
/> />

View File

@@ -51,6 +51,7 @@ export function EventListItem(props: EventListItemProps) {
return ( return (
<> <>
<button <button
type="button"
onClick={() => { onClick={() => {
if (!isMinimal) { if (!isMinimal) {
pushModal('EventDetails', { pushModal('EventDetails', {
@@ -61,7 +62,7 @@ export function EventListItem(props: EventListItemProps) {
className={cn( className={cn(
'card hover:bg-light-background flex w-full items-center justify-between rounded-lg p-4 transition-colors', 'card hover:bg-light-background flex w-full items-center justify-between rounded-lg p-4 transition-colors',
meta?.conversion && meta?.conversion &&
`bg-${meta.color}-50 dark:bg-${meta.color}-900 hover:bg-${meta.color}-100 dark:hover:bg-${meta.color}-700` `bg-${meta.color}-50 dark:bg-${meta.color}-900 hover:bg-${meta.color}-100 dark:hover:bg-${meta.color}-700`,
)} )}
> >
<div> <div>

View File

@@ -38,6 +38,7 @@ export default function EventListener({
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<button <button
type="button"
onClick={() => { onClick={() => {
counter.set(0); counter.set(0);
onRefresh(); onRefresh();
@@ -47,14 +48,14 @@ export default function EventListener({
<div className="relative"> <div className="relative">
<div <div
className={cn( className={cn(
'h-3 w-3 animate-ping rounded-full bg-emerald-500 opacity-100 transition-all' 'h-3 w-3 animate-ping rounded-full bg-emerald-500 opacity-100 transition-all',
)} )}
></div> />
<div <div
className={cn( className={cn(
'absolute left-0 top-0 h-3 w-3 rounded-full bg-emerald-500 transition-all' 'absolute left-0 top-0 h-3 w-3 rounded-full bg-emerald-500 transition-all',
)} )}
></div> />
</div> </div>
{counter.debounced === 0 ? ( {counter.debounced === 0 ? (
'Listening' 'Listening'

View File

@@ -52,6 +52,7 @@ export function useColumns() {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<TooltipComplete content="Click to edit" side="left"> <TooltipComplete content="Click to edit" side="left">
<button <button
type="button"
className="transition-transform hover:scale-105" className="transition-transform hover:scale-105"
onClick={() => { onClick={() => {
pushModal('EditEvent', { pushModal('EditEvent', {
@@ -68,6 +69,7 @@ export function useColumns() {
</TooltipComplete> </TooltipComplete>
<span className="flex gap-2"> <span className="flex gap-2">
<button <button
type="button"
onClick={() => { onClick={() => {
pushModal('EventDetails', { pushModal('EventDetails', {
id: row.original.id, id: row.original.id,

View File

@@ -1,4 +1,3 @@
import type { Dispatch, SetStateAction } from 'react';
import { DataTable } from '@/components/data-table'; import { DataTable } from '@/components/data-table';
import { FullPageEmptyState } from '@/components/full-page-empty-state'; import { FullPageEmptyState } from '@/components/full-page-empty-state';
import { Pagination } from '@/components/pagination'; import { Pagination } from '@/components/pagination';
@@ -7,6 +6,7 @@ import { TableSkeleton } from '@/components/ui/table';
import type { UseQueryResult } from '@tanstack/react-query'; import type { UseQueryResult } from '@tanstack/react-query';
import { GanttChartIcon } from 'lucide-react'; import { GanttChartIcon } from 'lucide-react';
import { column } from 'mathjs'; import { column } from 'mathjs';
import type { Dispatch, SetStateAction } from 'react';
import type { IServiceEvent } from '@openpanel/db'; import type { IServiceEvent } from '@openpanel/db';
@@ -55,7 +55,7 @@ export const EventsTable = ({ query, ...props }: Props) => {
className="mt-2" className="mt-2"
setCursor={props.setCursor} setCursor={props.setCursor}
cursor={props.cursor} cursor={props.cursor}
count={Infinity} count={Number.POSITIVE_INFINITY}
take={50} take={50}
loading={isFetching} loading={isFetching}
/> />

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import { useEffect, useRef } from 'react';
import { cn } from '@/utils/cn'; import { cn } from '@/utils/cn';
import { useEffect, useRef } from 'react';
type Props = { type Props = {
className?: string; className?: string;

Some files were not shown because too many files have changed in this diff Show More