chore(root): migrate to biome
This commit is contained in:
@@ -7,4 +7,6 @@ node_modules
|
|||||||
npm-debug.log
|
npm-debug.log
|
||||||
README.md
|
README.md
|
||||||
.next
|
.next
|
||||||
.git
|
.git
|
||||||
|
tmp
|
||||||
|
converage
|
||||||
6
.vscode/extensions.json
vendored
6
.vscode/extensions.json
vendored
@@ -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
35
.vscode/settings.json
vendored
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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('');
|
||||||
|
|||||||
@@ -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', {
|
||||||
|
|||||||
@@ -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));
|
||||||
|
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
|||||||
@@ -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' });
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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') {
|
||||||
|
|||||||
@@ -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];
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
3
apps/dashboard/.gitignore
vendored
3
apps/dashboard/.gitignore
vendored
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const Conversions = ({ projectId }: Props) => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return <EventsTable query={query} />;
|
return <EventsTable query={query} />;
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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{' '}
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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} />;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ const RealtimeReloader = ({ projectId }: Props) => {
|
|||||||
maxWait: 15000,
|
maxWait: 15000,
|
||||||
delay: 15000,
|
delay: 15000,
|
||||||
},
|
},
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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';
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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', {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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`,
|
||||||
),
|
),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -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(','),
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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 },
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>();
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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>{' '}
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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
Reference in New Issue
Block a user