rename mixan to OPENPANEL!
This commit is contained in:
48
README.md
48
README.md
@@ -1,10 +1,6 @@
|
|||||||
<p align="center">
|
# openpanel
|
||||||
<img src="images/mixan.svg">
|
|
||||||
</p>
|
|
||||||
|
|
||||||
# mixan
|
Openpanel is a simple analytics tool for logging events on web and react-native. My goal is to make a minimal mixpanel copy with the most basic features (for now).
|
||||||
|
|
||||||
Mixan is a simple analytics tool for logging events on web and react-native. My goal is to make a minimal mixpanel copy with the most basic features (for now).
|
|
||||||
|
|
||||||
- Easy to use
|
- Easy to use
|
||||||
- Fully responsive UI
|
- Fully responsive UI
|
||||||
@@ -71,24 +67,24 @@ As of today (~~2023-12-12~~ 2024-01-16) I have more then ~~1.2~~ 2.8 million eve
|
|||||||
- [x] Create web sdk
|
- [x] Create web sdk
|
||||||
- [x] Screen view function should take in title, path and parse query string (especially utm tags)
|
- [x] Screen view function should take in title, path and parse query string (especially utm tags)
|
||||||
|
|
||||||
## @mixan/sdk
|
## @openpanel/sdk
|
||||||
|
|
||||||
For pushing events
|
For pushing events
|
||||||
|
|
||||||
### Install
|
### Install
|
||||||
|
|
||||||
- npm: `npm install @mixan/sdk`
|
- npm: `npm install @openpanel/sdk`
|
||||||
- pnpm: `pnpm add @mixan/sdk`
|
- pnpm: `pnpm add @openpanel/sdk`
|
||||||
- yarn: `yarn add @mixan/sdk`
|
- yarn: `yarn add @openpanel/sdk`
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
```ts
|
```ts
|
||||||
import { MixanWeb } from '@mixan/web';
|
import { OpenpanelWeb } from '@openpanel/web';
|
||||||
|
|
||||||
// import { MixanNative } from '@mixan/sdk-native';
|
// import { OpenpanelNative } from '@openpanel/sdk-native';
|
||||||
|
|
||||||
const mixan = new MixanWeb({
|
const openpanel = new OpenpanelWeb({
|
||||||
clientId: 'uuid',
|
clientId: 'uuid',
|
||||||
url: 'http://localhost:8080/api/sdk',
|
url: 'http://localhost:8080/api/sdk',
|
||||||
batchInterval: 10000,
|
batchInterval: 10000,
|
||||||
@@ -96,7 +92,7 @@ const mixan = new MixanWeb({
|
|||||||
trackIp: true,
|
trackIp: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// const mixan = new MixanNative({
|
// const openpanel = new OpenpanelNative({
|
||||||
// clientId: 'uuid',
|
// clientId: 'uuid',
|
||||||
// clientSecret: 'uuid',
|
// clientSecret: 'uuid',
|
||||||
// url: 'http://localhost:8080/api/sdk',
|
// url: 'http://localhost:8080/api/sdk',
|
||||||
@@ -108,12 +104,12 @@ const mixan = new MixanWeb({
|
|||||||
// Call this before you send any events
|
// Call this before you send any events
|
||||||
// It will create a anonymous profile
|
// It will create a anonymous profile
|
||||||
// This profile will be merged if you call `setUser` in a later stage
|
// This profile will be merged if you call `setUser` in a later stage
|
||||||
mixan.init();
|
openpanel.init();
|
||||||
|
|
||||||
// tracks all outgoing links as a `link_out` event
|
// tracks all outgoing links as a `link_out` event
|
||||||
mixan.trackOutgoingLinks();
|
openpanel.trackOutgoingLinks();
|
||||||
|
|
||||||
mixan.setUser({
|
openpanel.setUser({
|
||||||
id: 'id',
|
id: 'id',
|
||||||
first_name: 'John',
|
first_name: 'John',
|
||||||
last_name: 'Doe',
|
last_name: 'Doe',
|
||||||
@@ -122,25 +118,25 @@ mixan.setUser({
|
|||||||
});
|
});
|
||||||
|
|
||||||
// will upsert 'app_open' on user property and increment it
|
// will upsert 'app_open' on user property and increment it
|
||||||
mixan.increment('app_open');
|
openpanel.increment('app_open');
|
||||||
// will upsert 'app_open' on user property and increment it by 10
|
// will upsert 'app_open' on user property and increment it by 10
|
||||||
mixan.increment('app_open', 10);
|
openpanel.increment('app_open', 10);
|
||||||
// will upsert 'app_open' on user property and decrement it by 2
|
// will upsert 'app_open' on user property and decrement it by 2
|
||||||
mixan.decrement('app_open', 2);
|
openpanel.decrement('app_open', 2);
|
||||||
|
|
||||||
// send a sign_in event
|
// send a sign_in event
|
||||||
mixan.event('sign_in');
|
openpanel.event('sign_in');
|
||||||
|
|
||||||
// send a sign_in event with properties
|
// send a sign_in event with properties
|
||||||
mixan.event('sign_in', {
|
openpanel.event('sign_in', {
|
||||||
provider: 'gmail',
|
provider: 'gmail',
|
||||||
});
|
});
|
||||||
|
|
||||||
// Screen view for web
|
// Screen view for web
|
||||||
mixan.screenView();
|
openpanel.screenView();
|
||||||
|
|
||||||
// Screen view for native
|
// Screen view for native
|
||||||
mixan.screenView('Article', {
|
openpanel.screenView('Article', {
|
||||||
id: '3',
|
id: '3',
|
||||||
title: 'Nice article here',
|
title: 'Nice article here',
|
||||||
});
|
});
|
||||||
@@ -148,10 +144,10 @@ mixan.screenView('Article', {
|
|||||||
// Call this when a user is logged out.
|
// Call this when a user is logged out.
|
||||||
// This will just make sure you do not send
|
// This will just make sure you do not send
|
||||||
// the associated profile id for the next events
|
// the associated profile id for the next events
|
||||||
mixan.clear();
|
openpanel.clear();
|
||||||
```
|
```
|
||||||
|
|
||||||
## @mixan/dashboard
|
## @openpanel/dashboard
|
||||||
|
|
||||||
A nextjs web app. Collects all events and your gui to analyze your data.
|
A nextjs web app. Collects all events and your gui to analyze your data.
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@mixan/api",
|
"name": "@openpanel/api",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "dotenv -e ../../.env -c -v WATCH=1 tsup",
|
"dev": "dotenv -e ../../.env -c -v WATCH=1 tsup",
|
||||||
@@ -14,10 +14,10 @@
|
|||||||
"@fastify/cors": "^9.0.0",
|
"@fastify/cors": "^9.0.0",
|
||||||
"@fastify/websocket": "^8.3.1",
|
"@fastify/websocket": "^8.3.1",
|
||||||
"@logtail/pino": "^0.4.19",
|
"@logtail/pino": "^0.4.19",
|
||||||
"@mixan/common": "workspace:*",
|
"@openpanel/common": "workspace:*",
|
||||||
"@mixan/db": "workspace:*",
|
"@openpanel/db": "workspace:*",
|
||||||
"@mixan/queue": "workspace:*",
|
"@openpanel/queue": "workspace:*",
|
||||||
"@mixan/redis": "workspace:*",
|
"@openpanel/redis": "workspace:*",
|
||||||
"fastify": "^4.25.2",
|
"fastify": "^4.25.2",
|
||||||
"ico-to-png": "^0.2.1",
|
"ico-to-png": "^0.2.1",
|
||||||
"pino": "^8.17.2",
|
"pino": "^8.17.2",
|
||||||
@@ -28,10 +28,10 @@
|
|||||||
"uuid": "^9.0.1"
|
"uuid": "^9.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mixan/eslint-config": "workspace:*",
|
"@openpanel/eslint-config": "workspace:*",
|
||||||
"@mixan/prettier-config": "workspace:*",
|
"@openpanel/prettier-config": "workspace:*",
|
||||||
"@mixan/sdk": "workspace:*",
|
"@openpanel/sdk": "workspace:*",
|
||||||
"@mixan/tsconfig": "workspace:*",
|
"@openpanel/tsconfig": "workspace:*",
|
||||||
"@types/ramda": "^0.29.6",
|
"@types/ramda": "^0.29.6",
|
||||||
"@types/ua-parser-js": "^0.7.39",
|
"@types/ua-parser-js": "^0.7.39",
|
||||||
"@types/uuid": "^9.0.8",
|
"@types/uuid": "^9.0.8",
|
||||||
@@ -44,8 +44,8 @@
|
|||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
"extends": [
|
"extends": [
|
||||||
"@mixan/eslint-config/base"
|
"@openpanel/eslint-config/base"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"prettier": "@mixan/prettier-config"
|
"prettier": "@openpanel/prettier-config"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,44 +1,44 @@
|
|||||||
import { omit, prop, uniqBy } from 'ramda';
|
import { omit, prop, uniqBy } from "ramda";
|
||||||
|
|
||||||
import { generateProfileId, getTime, toISOString } from '@mixan/common';
|
import { generateProfileId, getTime, toISOString } from "@openpanel/common";
|
||||||
import type { Event, IServiceCreateEventPayload } from '@mixan/db';
|
import type { Event, IServiceCreateEventPayload } from "@openpanel/db";
|
||||||
import {
|
import {
|
||||||
createEvent as createClickhouseEvent,
|
createEvent as createClickhouseEvent,
|
||||||
db,
|
db,
|
||||||
formatClickhouseDate,
|
formatClickhouseDate,
|
||||||
getSalts,
|
getSalts,
|
||||||
} from '@mixan/db';
|
} from "@openpanel/db";
|
||||||
|
|
||||||
import { parseIp } from '../src/utils/parseIp';
|
import { parseIp } from "../src/utils/parseIp";
|
||||||
import { parseUserAgent } from '../src/utils/parseUserAgent';
|
import { parseUserAgent } from "../src/utils/parseUserAgent";
|
||||||
|
|
||||||
const clean = omit([
|
const clean = omit([
|
||||||
'ip',
|
"ip",
|
||||||
'os',
|
"os",
|
||||||
'ua',
|
"ua",
|
||||||
'url',
|
"url",
|
||||||
'hash',
|
"hash",
|
||||||
'host',
|
"host",
|
||||||
'path',
|
"path",
|
||||||
'device',
|
"device",
|
||||||
'screen',
|
"screen",
|
||||||
'hostname',
|
"hostname",
|
||||||
'language',
|
"language",
|
||||||
'referrer',
|
"referrer",
|
||||||
'timezone',
|
"timezone",
|
||||||
]);
|
]);
|
||||||
async function main() {
|
async function main() {
|
||||||
const events = await db.event.findMany({
|
const events = await db.event.findMany({
|
||||||
where: {
|
where: {
|
||||||
project_id: '4e2798cb-e255-4e9d-960d-c9ad095aabd7',
|
project_id: "4e2798cb-e255-4e9d-960d-c9ad095aabd7",
|
||||||
name: 'screen_view',
|
name: "screen_view",
|
||||||
createdAt: {
|
createdAt: {
|
||||||
gte: new Date('2024-01-01'),
|
gte: new Date("2024-01-01"),
|
||||||
lt: new Date('2024-02-04'),
|
lt: new Date("2024-02-04"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: 'asc',
|
createdAt: "asc",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,7 +50,7 @@ async function main() {
|
|||||||
|
|
||||||
const properties = event.properties as Record<string, any>;
|
const properties = event.properties as Record<string, any>;
|
||||||
|
|
||||||
if (properties.ua?.includes('bot')) {
|
if (properties.ua?.includes("bot")) {
|
||||||
// console.log('IGNORE', event.id);
|
// console.log('IGNORE', event.id);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -67,20 +67,20 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Total users', Object.keys(grouped).length);
|
console.log("Total users", Object.keys(grouped).length);
|
||||||
|
|
||||||
let uidx = -1;
|
let uidx = -1;
|
||||||
for (const profile_id of Object.keys(grouped)) {
|
for (const profile_id of Object.keys(grouped)) {
|
||||||
uidx++;
|
uidx++;
|
||||||
console.log(`User index ${uidx}`);
|
console.log(`User index ${uidx}`);
|
||||||
|
|
||||||
const events = uniqBy(prop('createdAt'), grouped[profile_id] || []);
|
const events = uniqBy(prop("createdAt"), grouped[profile_id] || []);
|
||||||
|
|
||||||
if (events) {
|
if (events) {
|
||||||
let lastSessionStart = null;
|
let lastSessionStart = null;
|
||||||
let screenViews = 0;
|
let screenViews = 0;
|
||||||
let totalDuration = 0;
|
let totalDuration = 0;
|
||||||
console.log('new user...');
|
console.log("new user...");
|
||||||
let eidx = -1;
|
let eidx = -1;
|
||||||
for (const event of events) {
|
for (const event of events) {
|
||||||
eidx++;
|
eidx++;
|
||||||
@@ -93,7 +93,7 @@ async function main() {
|
|||||||
const projectId = event.project_id;
|
const projectId = event.project_id;
|
||||||
const path = properties.path!;
|
const path = properties.path!;
|
||||||
const ip = properties.ip!;
|
const ip = properties.ip!;
|
||||||
const origin = 'https://mixan.kiddo.se';
|
const origin = "https://openpanel.kiddo.se";
|
||||||
const ua = properties.ua!;
|
const ua = properties.ua!;
|
||||||
const uaInfo = parseUserAgent(ua);
|
const uaInfo = parseUserAgent(ua);
|
||||||
const salts = await getSalts();
|
const salts = await getSalts();
|
||||||
@@ -133,8 +133,8 @@ async function main() {
|
|||||||
? nextEvent.createdAt.getTime() - event.createdAt.getTime()
|
? nextEvent.createdAt.getTime() - event.createdAt.getTime()
|
||||||
: 0,
|
: 0,
|
||||||
path,
|
path,
|
||||||
referrer: properties?.referrer?.host ?? '', // TODO
|
referrer: properties?.referrer?.host ?? "", // TODO
|
||||||
referrerName: properties?.referrer?.host ?? '', // TODO
|
referrerName: properties?.referrer?.host ?? "", // TODO
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!prevEventAt) {
|
if (!prevEventAt) {
|
||||||
@@ -173,8 +173,8 @@ async function main() {
|
|||||||
async function createEvent(event: IServiceCreateEventPayload) {
|
async function createEvent(event: IServiceCreateEventPayload) {
|
||||||
console.log(
|
console.log(
|
||||||
`Create ${event.name} - ${event.path} - ${formatClickhouseDate(
|
`Create ${event.name} - ${event.path} - ${formatClickhouseDate(
|
||||||
event.createdAt
|
event.createdAt,
|
||||||
)} - ${event.duration / 1000} sec`
|
)} - ${event.duration / 1000} sec`,
|
||||||
);
|
);
|
||||||
await createClickhouseEvent(event);
|
await createClickhouseEvent(event);
|
||||||
}
|
}
|
||||||
@@ -183,7 +183,7 @@ async function createSessionStart(event: IServiceCreateEventPayload) {
|
|||||||
const session: IServiceCreateEventPayload = {
|
const session: IServiceCreateEventPayload = {
|
||||||
...event,
|
...event,
|
||||||
duration: 0,
|
duration: 0,
|
||||||
name: 'session_start',
|
name: "session_start",
|
||||||
createdAt: toISOString(getTime(event.createdAt) - 100),
|
createdAt: toISOString(getTime(event.createdAt) - 100),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -197,7 +197,7 @@ async function createSessionEnd(
|
|||||||
options: {
|
options: {
|
||||||
screenViews: number;
|
screenViews: number;
|
||||||
totalDuration: number;
|
totalDuration: number;
|
||||||
}
|
},
|
||||||
) {
|
) {
|
||||||
const properties: Record<string, unknown> = {};
|
const properties: Record<string, unknown> = {};
|
||||||
if (options.screenViews === 1) {
|
if (options.screenViews === 1) {
|
||||||
@@ -213,7 +213,7 @@ async function createSessionEnd(
|
|||||||
...sessionStart.properties,
|
...sessionStart.properties,
|
||||||
},
|
},
|
||||||
duration: options.totalDuration,
|
duration: options.totalDuration,
|
||||||
name: 'session_end',
|
name: "session_end",
|
||||||
createdAt: toISOString(prevEventAt.getTime() + 10),
|
createdAt: toISOString(prevEventAt.getTime() + 10),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import { logger, logInfo, noop } from '@/utils/logger';
|
import { logger, logInfo, noop } from "@/utils/logger";
|
||||||
import { getClientIp, parseIp } from '@/utils/parseIp';
|
import { getClientIp, parseIp } from "@/utils/parseIp";
|
||||||
import { getReferrerWithQuery, parseReferrer } from '@/utils/parseReferrer';
|
import { getReferrerWithQuery, parseReferrer } from "@/utils/parseReferrer";
|
||||||
import { isUserAgentSet, parseUserAgent } from '@/utils/parseUserAgent';
|
import { isUserAgentSet, parseUserAgent } from "@/utils/parseUserAgent";
|
||||||
import { isSameDomain, parsePath } from '@/utils/url';
|
import { isSameDomain, parsePath } from "@/utils/url";
|
||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||||
import { omit } from 'ramda';
|
import { omit } from "ramda";
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from "uuid";
|
||||||
|
|
||||||
import { generateDeviceId, getTime, toISOString } from '@mixan/common';
|
import { generateDeviceId, getTime, toISOString } from "@openpanel/common";
|
||||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
import type { IServiceCreateEventPayload } from "@openpanel/db";
|
||||||
import { createEvent, getEvents, getSalts } from '@mixan/db';
|
import { createEvent, getEvents, getSalts } from "@openpanel/db";
|
||||||
import type { JobsOptions } from '@mixan/queue';
|
import type { JobsOptions } from "@openpanel/queue";
|
||||||
import { eventsQueue } from '@mixan/queue';
|
import { eventsQueue } from "@openpanel/queue";
|
||||||
import { findJobByPrefix } from '@mixan/queue/src/utils';
|
import { findJobByPrefix } from "@openpanel/queue/src/utils";
|
||||||
import type { PostEventPayload } from '@mixan/sdk';
|
import type { PostEventPayload } from "@openpanel/sdk";
|
||||||
|
|
||||||
const SESSION_TIMEOUT = 1000 * 60 * 30;
|
const SESSION_TIMEOUT = 1000 * 60 * 30;
|
||||||
const SESSION_END_TIMEOUT = SESSION_TIMEOUT + 1000;
|
const SESSION_END_TIMEOUT = SESSION_TIMEOUT + 1000;
|
||||||
@@ -55,7 +55,7 @@ export async function postEvent(
|
|||||||
request: FastifyRequest<{
|
request: FastifyRequest<{
|
||||||
Body: PostEventPayload;
|
Body: PostEventPayload;
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply
|
reply: FastifyReply,
|
||||||
) {
|
) {
|
||||||
const contextLogger = createContextLogger(request);
|
const contextLogger = createContextLogger(request);
|
||||||
let deviceId: string | null = null;
|
let deviceId: string | null = null;
|
||||||
@@ -65,23 +65,23 @@ export async function postEvent(
|
|||||||
// replace thing is just for older sdks when we didn't have `__`
|
// replace thing is just for older sdks when we didn't have `__`
|
||||||
// remove when kiddokitchen app (24.09.02) is not used anymore
|
// remove when kiddokitchen app (24.09.02) is not used anymore
|
||||||
return (
|
return (
|
||||||
((properties[name] || properties[name.replace('__', '')]) as
|
((properties[name] || properties[name.replace("__", "")]) as
|
||||||
| string
|
| string
|
||||||
| null
|
| null
|
||||||
| undefined) ?? undefined
|
| undefined) ?? undefined
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
const profileId = body.profileId ?? '';
|
const profileId = body.profileId ?? "";
|
||||||
const createdAt = new Date(body.timestamp);
|
const createdAt = new Date(body.timestamp);
|
||||||
const url = getProperty('__path');
|
const url = getProperty("__path");
|
||||||
const { path, hash, query } = parsePath(url);
|
const { path, hash, query } = parsePath(url);
|
||||||
const referrer = isSameDomain(getProperty('__referrer'), url)
|
const referrer = isSameDomain(getProperty("__referrer"), url)
|
||||||
? null
|
? null
|
||||||
: parseReferrer(getProperty('__referrer'));
|
: parseReferrer(getProperty("__referrer"));
|
||||||
const utmReferrer = getReferrerWithQuery(query);
|
const utmReferrer = getReferrerWithQuery(query);
|
||||||
const ip = getClientIp(request)!;
|
const ip = getClientIp(request)!;
|
||||||
const origin = request.headers.origin!;
|
const origin = request.headers.origin!;
|
||||||
const ua = request.headers['user-agent']!;
|
const ua = request.headers["user-agent"]!;
|
||||||
const uaInfo = parseUserAgent(ua);
|
const uaInfo = parseUserAgent(ua);
|
||||||
const salts = await getSalts();
|
const salts = await getSalts();
|
||||||
const currentDeviceId = generateDeviceId({
|
const currentDeviceId = generateDeviceId({
|
||||||
@@ -101,66 +101,66 @@ export async function postEvent(
|
|||||||
|
|
||||||
if (isServerEvent) {
|
if (isServerEvent) {
|
||||||
const [event] = await withTiming(
|
const [event] = await withTiming(
|
||||||
'Get last event (server-event)',
|
"Get last event (server-event)",
|
||||||
getEvents(
|
getEvents(
|
||||||
`SELECT * FROM events WHERE name = 'screen_view' AND profile_id = '${profileId}' AND project_id = '${projectId}' ORDER BY created_at DESC LIMIT 1`
|
`SELECT * FROM events WHERE name = 'screen_view' AND profile_id = '${profileId}' AND project_id = '${projectId}' ORDER BY created_at DESC LIMIT 1`,
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
eventsQueue.add('event', {
|
eventsQueue.add("event", {
|
||||||
type: 'createEvent',
|
type: "createEvent",
|
||||||
payload: {
|
payload: {
|
||||||
name: body.name,
|
name: body.name,
|
||||||
deviceId: event?.deviceId || '',
|
deviceId: event?.deviceId || "",
|
||||||
sessionId: event?.sessionId || '',
|
sessionId: event?.sessionId || "",
|
||||||
profileId,
|
profileId,
|
||||||
projectId,
|
projectId,
|
||||||
properties: Object.assign(
|
properties: Object.assign(
|
||||||
{},
|
{},
|
||||||
omit(['__path', '__referrer'], properties),
|
omit(["__path", "__referrer"], properties),
|
||||||
{
|
{
|
||||||
hash,
|
hash,
|
||||||
query,
|
query,
|
||||||
}
|
},
|
||||||
),
|
),
|
||||||
createdAt,
|
createdAt,
|
||||||
country: event?.country ?? '',
|
country: event?.country ?? "",
|
||||||
city: event?.city ?? '',
|
city: event?.city ?? "",
|
||||||
region: event?.region ?? '',
|
region: event?.region ?? "",
|
||||||
continent: event?.continent ?? '',
|
continent: event?.continent ?? "",
|
||||||
os: event?.os ?? '',
|
os: event?.os ?? "",
|
||||||
osVersion: event?.osVersion ?? '',
|
osVersion: event?.osVersion ?? "",
|
||||||
browser: event?.browser ?? '',
|
browser: event?.browser ?? "",
|
||||||
browserVersion: event?.browserVersion ?? '',
|
browserVersion: event?.browserVersion ?? "",
|
||||||
device: event?.device ?? '',
|
device: event?.device ?? "",
|
||||||
brand: event?.brand ?? '',
|
brand: event?.brand ?? "",
|
||||||
model: event?.model ?? '',
|
model: event?.model ?? "",
|
||||||
duration: 0,
|
duration: 0,
|
||||||
path: event?.path ?? '',
|
path: event?.path ?? "",
|
||||||
referrer: event?.referrer ?? '',
|
referrer: event?.referrer ?? "",
|
||||||
referrerName: event?.referrerName ?? '',
|
referrerName: event?.referrerName ?? "",
|
||||||
referrerType: event?.referrerType ?? '',
|
referrerType: event?.referrerType ?? "",
|
||||||
profile: undefined,
|
profile: undefined,
|
||||||
meta: undefined,
|
meta: undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return reply.status(200).send('');
|
return reply.status(200).send("");
|
||||||
}
|
}
|
||||||
|
|
||||||
const [geo, sessionEndJobCurrentDeviceId, sessionEndJobPreviousDeviceId] =
|
const [geo, sessionEndJobCurrentDeviceId, sessionEndJobPreviousDeviceId] =
|
||||||
await withTiming(
|
await withTiming(
|
||||||
'Get geo and jobs from queue',
|
"Get geo and jobs from queue",
|
||||||
Promise.all([
|
Promise.all([
|
||||||
parseIp(ip),
|
parseIp(ip),
|
||||||
findJobByPrefix(
|
findJobByPrefix(
|
||||||
eventsQueue,
|
eventsQueue,
|
||||||
`sessionEnd:${projectId}:${currentDeviceId}:`
|
`sessionEnd:${projectId}:${currentDeviceId}:`,
|
||||||
),
|
),
|
||||||
findJobByPrefix(
|
findJobByPrefix(
|
||||||
eventsQueue,
|
eventsQueue,
|
||||||
`sessionEnd:${projectId}:${previousDeviceId}:`
|
`sessionEnd:${projectId}:${previousDeviceId}:`,
|
||||||
),
|
),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const createSessionStart =
|
const createSessionStart =
|
||||||
@@ -178,9 +178,9 @@ export async function postEvent(
|
|||||||
deviceId = currentDeviceId;
|
deviceId = currentDeviceId;
|
||||||
// Queue session end
|
// Queue session end
|
||||||
eventsQueue.add(
|
eventsQueue.add(
|
||||||
'event',
|
"event",
|
||||||
{
|
{
|
||||||
type: 'createSessionEnd',
|
type: "createSessionEnd",
|
||||||
payload: {
|
payload: {
|
||||||
deviceId,
|
deviceId,
|
||||||
},
|
},
|
||||||
@@ -188,27 +188,27 @@ export async function postEvent(
|
|||||||
{
|
{
|
||||||
delay: SESSION_END_TIMEOUT,
|
delay: SESSION_END_TIMEOUT,
|
||||||
jobId: `sessionEnd:${projectId}:${deviceId}:${Date.now()}`,
|
jobId: `sessionEnd:${projectId}:${deviceId}:${Date.now()}`,
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const [[sessionStartEvent], prevEventJob] = await withTiming(
|
const [[sessionStartEvent], prevEventJob] = await withTiming(
|
||||||
'Get session start event',
|
"Get session start event",
|
||||||
Promise.all([
|
Promise.all([
|
||||||
getEvents(
|
getEvents(
|
||||||
`SELECT * FROM events WHERE name = 'session_start' AND device_id = '${deviceId}' AND project_id = '${projectId}' ORDER BY created_at DESC LIMIT 1`
|
`SELECT * FROM events WHERE name = 'session_start' AND device_id = '${deviceId}' AND project_id = '${projectId}' ORDER BY created_at DESC LIMIT 1`,
|
||||||
),
|
),
|
||||||
findJobByPrefix(eventsQueue, `event:${projectId}:${deviceId}:`),
|
findJobByPrefix(eventsQueue, `event:${projectId}:${deviceId}:`),
|
||||||
])
|
]),
|
||||||
);
|
);
|
||||||
|
|
||||||
const payload: Omit<IServiceCreateEventPayload, 'id'> = {
|
const payload: Omit<IServiceCreateEventPayload, "id"> = {
|
||||||
name: body.name,
|
name: body.name,
|
||||||
deviceId,
|
deviceId,
|
||||||
profileId,
|
profileId,
|
||||||
projectId,
|
projectId,
|
||||||
sessionId: createSessionStart ? uuid() : sessionStartEvent?.sessionId ?? '',
|
sessionId: createSessionStart ? uuid() : sessionStartEvent?.sessionId ?? "",
|
||||||
properties: Object.assign({}, omit(['__path', '__referrer'], properties), {
|
properties: Object.assign({}, omit(["__path", "__referrer"], properties), {
|
||||||
hash,
|
hash,
|
||||||
query,
|
query,
|
||||||
}),
|
}),
|
||||||
@@ -227,27 +227,27 @@ export async function postEvent(
|
|||||||
duration: 0,
|
duration: 0,
|
||||||
path: path,
|
path: path,
|
||||||
referrer: referrer?.url,
|
referrer: referrer?.url,
|
||||||
referrerName: referrer?.name ?? utmReferrer?.name ?? '',
|
referrerName: referrer?.name ?? utmReferrer?.name ?? "",
|
||||||
referrerType: referrer?.type ?? utmReferrer?.type ?? '',
|
referrerType: referrer?.type ?? utmReferrer?.type ?? "",
|
||||||
profile: undefined,
|
profile: undefined,
|
||||||
meta: undefined,
|
meta: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const isDelayed = prevEventJob ? await prevEventJob?.isDelayed() : false;
|
const isDelayed = prevEventJob ? await prevEventJob?.isDelayed() : false;
|
||||||
|
|
||||||
if (isDelayed && prevEventJob && prevEventJob.data.type === 'createEvent') {
|
if (isDelayed && prevEventJob && prevEventJob.data.type === "createEvent") {
|
||||||
const prevEvent = prevEventJob.data.payload;
|
const prevEvent = prevEventJob.data.payload;
|
||||||
const duration = getTime(payload.createdAt) - getTime(prevEvent.createdAt);
|
const duration = getTime(payload.createdAt) - getTime(prevEvent.createdAt);
|
||||||
contextLogger.add('prevEvent', prevEvent);
|
contextLogger.add("prevEvent", prevEvent);
|
||||||
|
|
||||||
// Set path from prev screen_view event if current event is not a screen_view
|
// Set path from prev screen_view event if current event is not a screen_view
|
||||||
if (payload.name != 'screen_view') {
|
if (payload.name != "screen_view") {
|
||||||
payload.path = prevEvent.path;
|
payload.path = prevEvent.path;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (payload.name === 'screen_view') {
|
if (payload.name === "screen_view") {
|
||||||
if (duration < 0) {
|
if (duration < 0) {
|
||||||
contextLogger.send('duration is wrong', {
|
contextLogger.send("duration is wrong", {
|
||||||
payload,
|
payload,
|
||||||
duration,
|
duration,
|
||||||
});
|
});
|
||||||
@@ -255,21 +255,21 @@ export async function postEvent(
|
|||||||
// Skip update duration if it's wrong
|
// Skip update duration if it's wrong
|
||||||
// Seems like request is not in right order
|
// Seems like request is not in right order
|
||||||
await withTiming(
|
await withTiming(
|
||||||
'Update previous job with duration',
|
"Update previous job with duration",
|
||||||
prevEventJob.updateData({
|
prevEventJob.updateData({
|
||||||
type: 'createEvent',
|
type: "createEvent",
|
||||||
payload: {
|
payload: {
|
||||||
...prevEvent,
|
...prevEvent,
|
||||||
duration,
|
duration,
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await withTiming('Promote previous job', prevEventJob.promote());
|
await withTiming("Promote previous job", prevEventJob.promote());
|
||||||
}
|
}
|
||||||
} else if (payload.name !== 'screen_view') {
|
} else if (payload.name !== "screen_view") {
|
||||||
contextLogger.send('no previous job', {
|
contextLogger.send("no previous job", {
|
||||||
prevEventJob,
|
prevEventJob,
|
||||||
payload,
|
payload,
|
||||||
});
|
});
|
||||||
@@ -278,23 +278,23 @@ export async function postEvent(
|
|||||||
if (createSessionStart) {
|
if (createSessionStart) {
|
||||||
// We do not need to queue session_start
|
// We do not need to queue session_start
|
||||||
await withTiming(
|
await withTiming(
|
||||||
'Create session start event',
|
"Create session start event",
|
||||||
createEvent({
|
createEvent({
|
||||||
...payload,
|
...payload,
|
||||||
name: 'session_start',
|
name: "session_start",
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
createdAt: toISOString(getTime(payload.createdAt) - 100),
|
createdAt: toISOString(getTime(payload.createdAt) - 100),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const options: JobsOptions = {};
|
const options: JobsOptions = {};
|
||||||
if (payload.name === 'screen_view') {
|
if (payload.name === "screen_view") {
|
||||||
options.delay = SESSION_TIMEOUT;
|
options.delay = SESSION_TIMEOUT;
|
||||||
options.jobId = `event:${projectId}:${deviceId}:${Date.now()}`;
|
options.jobId = `event:${projectId}:${deviceId}:${Date.now()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
contextLogger.send('event is queued', {
|
contextLogger.send("event is queued", {
|
||||||
ip,
|
ip,
|
||||||
origin,
|
origin,
|
||||||
ua,
|
ua,
|
||||||
@@ -312,14 +312,14 @@ export async function postEvent(
|
|||||||
// Queue current event
|
// Queue current event
|
||||||
eventsQueue
|
eventsQueue
|
||||||
.add(
|
.add(
|
||||||
'event',
|
"event",
|
||||||
{
|
{
|
||||||
type: 'createEvent',
|
type: "createEvent",
|
||||||
payload,
|
payload,
|
||||||
},
|
},
|
||||||
options
|
options,
|
||||||
)
|
)
|
||||||
.catch(noop('Failed to queue event'));
|
.catch(noop("Failed to queue event"));
|
||||||
|
|
||||||
reply.status(202).send(deviceId);
|
reply.status(202).send(deviceId);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||||
import type * as WebSocket from 'ws';
|
import type * as WebSocket from "ws";
|
||||||
|
|
||||||
import { getSafeJson } from '@mixan/common';
|
import { getSafeJson } from "@openpanel/common";
|
||||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
import type { IServiceCreateEventPayload } from "@openpanel/db";
|
||||||
import { getEvents, getLiveVisitors } from '@mixan/db';
|
import { getEvents, getLiveVisitors } from "@openpanel/db";
|
||||||
import { redis, redisPub, redisSub } from '@mixan/redis';
|
import { redis, redisPub, redisSub } from "@openpanel/redis";
|
||||||
|
|
||||||
export function getLiveEventInfo(key: string) {
|
export function getLiveEventInfo(key: string) {
|
||||||
return key.split(':').slice(2) as [string, string];
|
return key.split(":").slice(2) as [string, string];
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function test(
|
export async function test(
|
||||||
@@ -16,20 +16,20 @@ export async function test(
|
|||||||
projectId: string;
|
projectId: string;
|
||||||
};
|
};
|
||||||
}>,
|
}>,
|
||||||
reply: FastifyReply
|
reply: FastifyReply,
|
||||||
) {
|
) {
|
||||||
const [event] = await getEvents(
|
const [event] = await getEvents(
|
||||||
`SELECT * FROM events WHERE project_id = '${req.params.projectId}' AND name = 'screen_view' LIMIT 1`
|
`SELECT * FROM events WHERE project_id = '${req.params.projectId}' AND name = 'screen_view' LIMIT 1`,
|
||||||
);
|
);
|
||||||
if (!event) {
|
if (!event) {
|
||||||
return reply.status(404).send('No event found');
|
return reply.status(404).send("No event found");
|
||||||
}
|
}
|
||||||
redisPub.publish('event', JSON.stringify(event));
|
redisPub.publish("event", JSON.stringify(event));
|
||||||
redis.set(
|
redis.set(
|
||||||
`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);
|
||||||
}
|
}
|
||||||
@@ -42,15 +42,15 @@ export function wsVisitors(
|
|||||||
Params: {
|
Params: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
};
|
};
|
||||||
}>
|
}>,
|
||||||
) {
|
) {
|
||||||
const { params } = req;
|
const { params } = req;
|
||||||
|
|
||||||
redisSub.subscribe('event');
|
redisSub.subscribe("event");
|
||||||
redisSub.psubscribe('__key*:expired');
|
redisSub.psubscribe("__key*:expired");
|
||||||
|
|
||||||
const message = (channel: string, message: string) => {
|
const message = (channel: string, message: string) => {
|
||||||
if (channel === 'event') {
|
if (channel === "event") {
|
||||||
const event = getSafeJson<IServiceCreateEventPayload>(message);
|
const event = getSafeJson<IServiceCreateEventPayload>(message);
|
||||||
if (event?.projectId === params.projectId) {
|
if (event?.projectId === params.projectId) {
|
||||||
getLiveVisitors(params.projectId).then((count) => {
|
getLiveVisitors(params.projectId).then((count) => {
|
||||||
@@ -68,14 +68,14 @@ export function wsVisitors(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
redisSub.on('message', message);
|
redisSub.on("message", message);
|
||||||
redisSub.on('pmessage', pmessage);
|
redisSub.on("pmessage", pmessage);
|
||||||
|
|
||||||
connection.socket.on('close', () => {
|
connection.socket.on("close", () => {
|
||||||
redisSub.unsubscribe('event');
|
redisSub.unsubscribe("event");
|
||||||
redisSub.punsubscribe('__key*:expired');
|
redisSub.punsubscribe("__key*:expired");
|
||||||
redisSub.off('message', message);
|
redisSub.off("message", message);
|
||||||
redisSub.off('pmessage', pmessage);
|
redisSub.off("pmessage", pmessage);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,11 +87,11 @@ export function wsEvents(
|
|||||||
Params: {
|
Params: {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
};
|
};
|
||||||
}>
|
}>,
|
||||||
) {
|
) {
|
||||||
const { params } = req;
|
const { params } = req;
|
||||||
|
|
||||||
redisSub.subscribe('event');
|
redisSub.subscribe("event");
|
||||||
|
|
||||||
const message = (channel: string, message: string) => {
|
const message = (channel: string, message: string) => {
|
||||||
const event = getSafeJson<IServiceCreateEventPayload>(message);
|
const event = getSafeJson<IServiceCreateEventPayload>(message);
|
||||||
@@ -100,10 +100,10 @@ export function wsEvents(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
redisSub.on('message', message);
|
redisSub.on("message", message);
|
||||||
|
|
||||||
connection.socket.on('close', () => {
|
connection.socket.on("close", () => {
|
||||||
redisSub.unsubscribe('event');
|
redisSub.unsubscribe("event");
|
||||||
redisSub.off('message', message);
|
redisSub.off("message", message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
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 '@mixan/common';
|
import { createHash } from "@openpanel/common";
|
||||||
import { redis } from '@mixan/redis';
|
import { redis } from "@openpanel/redis";
|
||||||
|
|
||||||
interface GetFaviconParams {
|
interface GetFaviconParams {
|
||||||
url: string;
|
url: string;
|
||||||
@@ -12,9 +12,9 @@ interface GetFaviconParams {
|
|||||||
async function getImageBuffer(url: string) {
|
async function getImageBuffer(url: string) {
|
||||||
try {
|
try {
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
const contentType = res.headers.get('content-type');
|
const contentType = res.headers.get("content-type");
|
||||||
|
|
||||||
if (!contentType?.includes('image')) {
|
if (!contentType?.includes("image")) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,7 +22,7 @@ async function getImageBuffer(url: string) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contentType === 'image/x-icon' || url.endsWith('.ico')) {
|
if (contentType === "image/x-icon" || url.endsWith(".ico")) {
|
||||||
const arrayBuffer = await res.arrayBuffer();
|
const arrayBuffer = await res.arrayBuffer();
|
||||||
const buffer = Buffer.from(arrayBuffer);
|
const buffer = Buffer.from(arrayBuffer);
|
||||||
return await icoToPng(buffer, 30);
|
return await icoToPng(buffer, 30);
|
||||||
@@ -30,36 +30,36 @@ async function getImageBuffer(url: string) {
|
|||||||
|
|
||||||
return await sharp(await res.arrayBuffer())
|
return await sharp(await res.arrayBuffer())
|
||||||
.resize(30, 30, {
|
.resize(30, 30, {
|
||||||
fit: 'cover',
|
fit: "cover",
|
||||||
})
|
})
|
||||||
.png()
|
.png()
|
||||||
.toBuffer();
|
.toBuffer();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('Failed to get image from url', url);
|
console.log("Failed to get image from url", url);
|
||||||
console.log(e);
|
console.log(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageExtensions = ['svg', 'png', 'jpg', 'jpeg', 'gif', 'webp', 'ico'];
|
const imageExtensions = ["svg", "png", "jpg", "jpeg", "gif", "webp", "ico"];
|
||||||
|
|
||||||
export async function getFavicon(
|
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) {
|
||||||
redis.set(`favicon:${cacheKey}`, buffer.toString('base64'));
|
redis.set(`favicon:${cacheKey}`, buffer.toString("base64"));
|
||||||
}
|
}
|
||||||
reply.type('image/png');
|
reply.type("image/png");
|
||||||
console.log('buffer', buffer.byteLength);
|
console.log("buffer", buffer.byteLength);
|
||||||
|
|
||||||
return reply.send(buffer);
|
return reply.send(buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!request.query.url) {
|
if (!request.query.url) {
|
||||||
return reply.status(404).send('Not found');
|
return reply.status(404).send("Not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const url = decodeURIComponent(request.query.url);
|
const url = decodeURIComponent(request.query.url);
|
||||||
@@ -69,7 +69,7 @@ export async function getFavicon(
|
|||||||
const cacheKey = createHash(url, 32);
|
const cacheKey = createHash(url, 32);
|
||||||
const cache = await redis.get(`favicon:${cacheKey}`);
|
const cache = await redis.get(`favicon:${cacheKey}`);
|
||||||
if (cache) {
|
if (cache) {
|
||||||
return sendBuffer(Buffer.from(cache, 'base64'));
|
return sendBuffer(Buffer.from(cache, "base64"));
|
||||||
}
|
}
|
||||||
const buffer = await getImageBuffer(url);
|
const buffer = await getImageBuffer(url);
|
||||||
if (buffer && buffer.byteLength > 0) {
|
if (buffer && buffer.byteLength > 0) {
|
||||||
@@ -80,7 +80,7 @@ export async function getFavicon(
|
|||||||
const { hostname, origin } = new URL(url);
|
const { hostname, origin } = new URL(url);
|
||||||
const cache = await redis.get(`favicon:${hostname}`);
|
const cache = await redis.get(`favicon:${hostname}`);
|
||||||
if (cache) {
|
if (cache) {
|
||||||
return sendBuffer(Buffer.from(cache, 'base64'));
|
return sendBuffer(Buffer.from(cache, "base64"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// TRY FAVICON.ICO
|
// TRY FAVICON.ICO
|
||||||
@@ -94,7 +94,7 @@ export async function getFavicon(
|
|||||||
|
|
||||||
function findFavicon(res: string) {
|
function findFavicon(res: string) {
|
||||||
const match = res.match(
|
const match = res.match(
|
||||||
/(\<link(.+?)image\/x-icon(.+?)\>|\<link(.+?)shortcut\sicon(.+?)\>)/
|
/(\<link(.+?)image\/x-icon(.+?)\>|\<link(.+?)shortcut\sicon(.+?)\>)/,
|
||||||
);
|
);
|
||||||
if (!match) {
|
if (!match) {
|
||||||
return null;
|
return null;
|
||||||
@@ -112,16 +112,16 @@ export async function getFavicon(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return reply.status(404).send('Not found');
|
return reply.status(404).send("Not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function clearFavicons(
|
export async function clearFavicons(
|
||||||
request: FastifyRequest,
|
request: FastifyRequest,
|
||||||
reply: FastifyReply
|
reply: FastifyReply,
|
||||||
) {
|
) {
|
||||||
const keys = await redis.keys('favicon:*');
|
const keys = await redis.keys("favicon:*");
|
||||||
for (const key of keys) {
|
for (const key of keys) {
|
||||||
await redis.del(key);
|
await redis.del(key);
|
||||||
}
|
}
|
||||||
return reply.status(404).send('OK');
|
return reply.status(404).send("OK");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
import { getClientIp, parseIp } from '@/utils/parseIp';
|
import { getClientIp, parseIp } from "@/utils/parseIp";
|
||||||
import { isUserAgentSet, parseUserAgent } from '@/utils/parseUserAgent';
|
import { isUserAgentSet, parseUserAgent } from "@/utils/parseUserAgent";
|
||||||
import type { FastifyReply, FastifyRequest } from 'fastify';
|
import type { FastifyReply, FastifyRequest } from "fastify";
|
||||||
import { assocPath, pathOr } from 'ramda';
|
import { assocPath, pathOr } from "ramda";
|
||||||
|
|
||||||
import { getProfileById, upsertProfile } from '@mixan/db';
|
import { getProfileById, upsertProfile } from "@openpanel/db";
|
||||||
import type { IncrementProfilePayload, UpdateProfilePayload } from '@mixan/sdk';
|
import type {
|
||||||
|
IncrementProfilePayload,
|
||||||
|
UpdateProfilePayload,
|
||||||
|
} from "@openpanel/sdk";
|
||||||
|
|
||||||
export async function updateProfile(
|
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;
|
||||||
const ip = getClientIp(request)!;
|
const ip = getClientIp(request)!;
|
||||||
const ua = request.headers['user-agent']!;
|
const ua = request.headers["user-agent"]!;
|
||||||
const uaInfo = parseUserAgent(ua);
|
const uaInfo = parseUserAgent(ua);
|
||||||
const geo = await parseIp(ip);
|
const geo = await parseIp(ip);
|
||||||
|
|
||||||
@@ -37,29 +40,29 @@ 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;
|
||||||
|
|
||||||
const profile = await getProfileById(profileId);
|
const profile = await getProfileById(profileId);
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
return reply.status(404).send('Not found');
|
return reply.status(404).send("Not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = parseInt(
|
const parsed = parseInt(
|
||||||
pathOr<string>('0', property.split('.'), profile.properties),
|
pathOr<string>("0", property.split("."), profile.properties),
|
||||||
10
|
10,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isNaN(parsed)) {
|
if (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({
|
||||||
@@ -75,29 +78,29 @@ 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;
|
||||||
|
|
||||||
const profile = await getProfileById(profileId);
|
const profile = await getProfileById(profileId);
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
return reply.status(404).send('Not found');
|
return reply.status(404).send("Not found");
|
||||||
}
|
}
|
||||||
|
|
||||||
const parsed = parseInt(
|
const parsed = parseInt(
|
||||||
pathOr<string>('0', property.split('.'), profile.properties),
|
pathOr<string>("0", property.split("."), profile.properties),
|
||||||
10
|
10,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (isNaN(parsed)) {
|
if (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({
|
||||||
|
|||||||
@@ -1,43 +1,43 @@
|
|||||||
import cors from '@fastify/cors';
|
import cors from "@fastify/cors";
|
||||||
import Fastify from 'fastify';
|
import Fastify from "fastify";
|
||||||
|
|
||||||
import { redisPub } from '@mixan/redis';
|
import { redisPub } from "@openpanel/redis";
|
||||||
|
|
||||||
import eventRouter from './routes/event.router';
|
import eventRouter from "./routes/event.router";
|
||||||
import liveRouter from './routes/live.router';
|
import liveRouter from "./routes/live.router";
|
||||||
import miscRouter from './routes/misc.router';
|
import miscRouter from "./routes/misc.router";
|
||||||
import profileRouter from './routes/profile.router';
|
import profileRouter from "./routes/profile.router";
|
||||||
import { logger, logInfo } from './utils/logger';
|
import { logger, logInfo } from "./utils/logger";
|
||||||
|
|
||||||
declare module 'fastify' {
|
declare module "fastify" {
|
||||||
interface FastifyRequest {
|
interface FastifyRequest {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const port = parseInt(process.env.API_PORT || '3000', 10);
|
const port = parseInt(process.env.API_PORT || "3000", 10);
|
||||||
|
|
||||||
const startServer = async () => {
|
const startServer = async () => {
|
||||||
logInfo('Starting server');
|
logInfo("Starting server");
|
||||||
try {
|
try {
|
||||||
const fastify = Fastify({
|
const fastify = Fastify({
|
||||||
logger: logger,
|
logger: logger,
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.register(cors, {
|
fastify.register(cors, {
|
||||||
origin: '*',
|
origin: "*",
|
||||||
});
|
});
|
||||||
|
|
||||||
fastify.decorateRequest('projectId', '');
|
fastify.decorateRequest("projectId", "");
|
||||||
fastify.register(eventRouter, { prefix: '/event' });
|
fastify.register(eventRouter, { prefix: "/event" });
|
||||||
fastify.register(profileRouter, { prefix: '/profile' });
|
fastify.register(profileRouter, { prefix: "/profile" });
|
||||||
fastify.register(liveRouter, { prefix: '/live' });
|
fastify.register(liveRouter, { prefix: "/live" });
|
||||||
fastify.register(miscRouter, { prefix: '/misc' });
|
fastify.register(miscRouter, { prefix: "/misc" });
|
||||||
fastify.setErrorHandler((error, request, reply) => {
|
fastify.setErrorHandler((error, request, reply) => {
|
||||||
fastify.log.error(error);
|
fastify.log.error(error);
|
||||||
});
|
});
|
||||||
fastify.get('/', (request, reply) => {
|
fastify.get("/", (request, reply) => {
|
||||||
reply.send({ name: 'openpanel sdk api' });
|
reply.send({ name: "openpanel sdk api" });
|
||||||
});
|
});
|
||||||
// fastify.get('/health-check', async (request, reply) => {
|
// fastify.get('/health-check', async (request, reply) => {
|
||||||
// try {
|
// try {
|
||||||
@@ -47,8 +47,8 @@ const startServer = async () => {
|
|||||||
// reply.status(500).send()
|
// reply.status(500).send()
|
||||||
// }
|
// }
|
||||||
// })
|
// })
|
||||||
if (process.env.NODE_ENV === 'production') {
|
if (process.env.NODE_ENV === "production") {
|
||||||
for (const signal of ['SIGINT', 'SIGTERM']) {
|
for (const signal of ["SIGINT", "SIGTERM"]) {
|
||||||
process.on(signal, (err) => {
|
process.on(signal, (err) => {
|
||||||
logger.fatal(err, `uncaught exception detected ${signal}`);
|
logger.fatal(err, `uncaught exception detected ${signal}`);
|
||||||
fastify.close().then((err) => {
|
fastify.close().then((err) => {
|
||||||
@@ -59,12 +59,12 @@ const startServer = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
await fastify.listen({
|
await fastify.listen({
|
||||||
host: process.env.NODE_ENV === 'production' ? '0.0.0.0' : 'localhost',
|
host: process.env.NODE_ENV === "production" ? "0.0.0.0" : "localhost",
|
||||||
port,
|
port,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Notify when keys expires
|
// Notify when keys expires
|
||||||
redisPub.config('SET', 'notify-keyspace-events', 'Ex');
|
redisPub.config("SET", "notify-keyspace-events", "Ex");
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,56 +1,56 @@
|
|||||||
import { isBot } from '@/bots';
|
import { isBot } from "@/bots";
|
||||||
import * as controller from '@/controllers/event.controller';
|
import * as controller from "@/controllers/event.controller";
|
||||||
import { validateSdkRequest } from '@/utils/auth';
|
import { validateSdkRequest } from "@/utils/auth";
|
||||||
import type { FastifyPluginCallback, FastifyRequest } from 'fastify';
|
import type { FastifyPluginCallback, FastifyRequest } from "fastify";
|
||||||
|
|
||||||
import { createBotEvent } from '@mixan/db';
|
import { createBotEvent } from "@openpanel/db";
|
||||||
import type { PostEventPayload } from '@mixan/sdk';
|
import type { PostEventPayload } from "@openpanel/sdk";
|
||||||
|
|
||||||
const eventRouter: FastifyPluginCallback = (fastify, opts, done) => {
|
const eventRouter: FastifyPluginCallback = (fastify, opts, done) => {
|
||||||
fastify.addHook(
|
fastify.addHook(
|
||||||
'preHandler',
|
"preHandler",
|
||||||
async (
|
async (
|
||||||
req: FastifyRequest<{
|
req: FastifyRequest<{
|
||||||
Body: PostEventPayload;
|
Body: PostEventPayload;
|
||||||
}>,
|
}>,
|
||||||
reply
|
reply,
|
||||||
) => {
|
) => {
|
||||||
try {
|
try {
|
||||||
const projectId = await validateSdkRequest(req.headers);
|
const projectId = await validateSdkRequest(req.headers);
|
||||||
req.projectId = projectId;
|
req.projectId = projectId;
|
||||||
|
|
||||||
const bot = req.headers['user-agent']
|
const bot = req.headers["user-agent"]
|
||||||
? isBot(req.headers['user-agent'])
|
? isBot(req.headers["user-agent"])
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
if (bot) {
|
if (bot) {
|
||||||
const path = (req.body?.properties?.__path ||
|
const path = (req.body?.properties?.__path ||
|
||||||
req.body?.properties?.path) as string | undefined;
|
req.body?.properties?.path) as string | undefined;
|
||||||
req.log.warn({ ...req.headers, bot }, 'Bot detected (event)');
|
req.log.warn({ ...req.headers, bot }, "Bot detected (event)");
|
||||||
await createBotEvent({
|
await createBotEvent({
|
||||||
...bot,
|
...bot,
|
||||||
projectId,
|
projectId,
|
||||||
path: path ?? '',
|
path: path ?? "",
|
||||||
createdAt: new Date(req.body?.timestamp),
|
createdAt: new Date(req.body?.timestamp),
|
||||||
});
|
});
|
||||||
reply.status(202).send('OK');
|
reply.status(202).send("OK");
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
req.log.error(e, 'Failed to create bot event');
|
req.log.error(e, "Failed to create bot event");
|
||||||
reply.status(401).send();
|
reply.status(401).send();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
fastify.route({
|
fastify.route({
|
||||||
method: 'POST',
|
method: "POST",
|
||||||
url: '/',
|
url: "/",
|
||||||
handler: controller.postEvent,
|
handler: controller.postEvent,
|
||||||
});
|
});
|
||||||
fastify.route({
|
fastify.route({
|
||||||
method: 'GET',
|
method: "GET",
|
||||||
url: '/',
|
url: "/",
|
||||||
handler: controller.postEvent,
|
handler: controller.postEvent,
|
||||||
});
|
});
|
||||||
done();
|
done();
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import type { RawRequestDefaultExpression } from 'fastify';
|
import type { RawRequestDefaultExpression } from 'fastify';
|
||||||
|
|
||||||
import { verifyPassword } from '@mixan/common';
|
import { verifyPassword } from '@openpanel/common';
|
||||||
import { db } from '@mixan/db';
|
import { db } from '@openpanel/db';
|
||||||
|
|
||||||
export async function validateSdkRequest(
|
export async function validateSdkRequest(
|
||||||
headers: RawRequestDefaultExpression['headers']
|
headers: RawRequestDefaultExpression['headers']
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const clientId = headers['mixan-client-id'] as string;
|
const clientId = headers['openpanel-client-id'] as string;
|
||||||
const clientSecret = headers['mixan-client-secret'] as string;
|
const clientSecret = headers['openpanel-client-secret'] as string;
|
||||||
const origin = headers.origin;
|
const origin = headers.origin;
|
||||||
if (!clientId) {
|
if (!clientId) {
|
||||||
throw new Error('Misisng client id');
|
throw new Error('Misisng client id');
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
import { stripTrailingSlash } from '@mixan/common';
|
import { stripTrailingSlash } from "@openpanel/common";
|
||||||
|
|
||||||
import referrers from '../referrers';
|
import referrers from "../referrers";
|
||||||
|
|
||||||
function getHostname(url: string | undefined) {
|
function getHostname(url: string | undefined) {
|
||||||
if (!url) {
|
if (!url) {
|
||||||
return '';
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
return new URL(url).hostname;
|
return new URL(url).hostname;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return '';
|
return "";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseReferrer(url: string | undefined) {
|
export function parseReferrer(url: string | undefined) {
|
||||||
const hostname = getHostname(url);
|
const hostname = getHostname(url);
|
||||||
const match = referrers[hostname] ?? referrers[hostname.replace('www.', '')];
|
const match = referrers[hostname] ?? referrers[hostname.replace("www.", "")];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: match?.name ?? '',
|
name: match?.name ?? "",
|
||||||
type: match?.type ?? 'unknown',
|
type: match?.type ?? "unknown",
|
||||||
url: stripTrailingSlash(url ?? ''),
|
url: stripTrailingSlash(url ?? ""),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getReferrerWithQuery(
|
export function getReferrerWithQuery(
|
||||||
query: Record<string, string> | undefined
|
query: Record<string, string> | undefined,
|
||||||
) {
|
) {
|
||||||
if (!query) {
|
if (!query) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const source = query.utm_source ?? query.ref ?? query.utm_referrer ?? '';
|
const source = query.utm_source ?? query.ref ?? query.utm_referrer ?? "";
|
||||||
|
|
||||||
const match = Object.values(referrers).find(
|
const match = Object.values(referrers).find(
|
||||||
(referrer) => referrer.name.toLowerCase() === source?.toLowerCase()
|
(referrer) => referrer.name.toLowerCase() === source?.toLowerCase(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!match) {
|
if (!match) {
|
||||||
@@ -45,6 +45,6 @@ export function getReferrerWithQuery(
|
|||||||
return {
|
return {
|
||||||
name: match.name,
|
name: match.name,
|
||||||
type: match.type,
|
type: match.type,
|
||||||
url: '',
|
url: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"extends": "@mixan/tsconfig/base.json",
|
"extends": "@openpanel/tsconfig/base.json",
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { Options } from 'tsup';
|
|||||||
const options: Options = {
|
const options: Options = {
|
||||||
clean: true,
|
clean: true,
|
||||||
entry: ['src/index.ts'],
|
entry: ['src/index.ts'],
|
||||||
noExternal: [/^@mixan\/.*$/u, /^@\/.*$/u],
|
noExternal: [/^@openpanel\/.*$/u, /^@\/.*$/u],
|
||||||
sourcemap: true,
|
sourcemap: true,
|
||||||
splitting: false,
|
splitting: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ await import('./src/env.mjs');
|
|||||||
/** @type {import("next").NextConfig} */
|
/** @type {import("next").NextConfig} */
|
||||||
const config = {
|
const config = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
transpilePackages: ['@mixan/queue'],
|
transpilePackages: ['@openpanel/queue'],
|
||||||
eslint: { ignoreDuringBuilds: true },
|
eslint: { ignoreDuringBuilds: true },
|
||||||
typescript: { ignoreBuildErrors: true },
|
typescript: { ignoreBuildErrors: true },
|
||||||
experimental: {
|
experimental: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"name": "@mixan/dashboard",
|
"name": "@openpanel/dashboard",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
@@ -16,11 +16,11 @@
|
|||||||
"@clerk/nextjs": "^4.29.7",
|
"@clerk/nextjs": "^4.29.7",
|
||||||
"@clickhouse/client": "^0.2.9",
|
"@clickhouse/client": "^0.2.9",
|
||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@mixan/common": "workspace:^",
|
"@openpanel/common": "workspace:^",
|
||||||
"@mixan/constants": "workspace:^",
|
"@openpanel/constants": "workspace:^",
|
||||||
"@mixan/db": "workspace:^",
|
"@openpanel/db": "workspace:^",
|
||||||
"@mixan/queue": "workspace:^",
|
"@openpanel/queue": "workspace:^",
|
||||||
"@mixan/validation": "workspace:^",
|
"@openpanel/validation": "workspace:^",
|
||||||
"@radix-ui/react-accordion": "^1.1.2",
|
"@radix-ui/react-accordion": "^1.1.2",
|
||||||
"@radix-ui/react-alert-dialog": "^1.0.5",
|
"@radix-ui/react-alert-dialog": "^1.0.5",
|
||||||
"@radix-ui/react-aspect-ratio": "^1.0.3",
|
"@radix-ui/react-aspect-ratio": "^1.0.3",
|
||||||
@@ -93,9 +93,9 @@
|
|||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@mixan/eslint-config": "workspace:*",
|
"@openpanel/eslint-config": "workspace:*",
|
||||||
"@mixan/prettier-config": "workspace:*",
|
"@openpanel/prettier-config": "workspace:*",
|
||||||
"@mixan/tsconfig": "workspace:*",
|
"@openpanel/tsconfig": "workspace:*",
|
||||||
"@types/bcrypt": "^5.0.2",
|
"@types/bcrypt": "^5.0.2",
|
||||||
"@types/lodash.debounce": "^4.0.9",
|
"@types/lodash.debounce": "^4.0.9",
|
||||||
"@types/lodash.throttle": "^4.1.9",
|
"@types/lodash.throttle": "^4.1.9",
|
||||||
@@ -121,10 +121,10 @@
|
|||||||
"eslintConfig": {
|
"eslintConfig": {
|
||||||
"root": true,
|
"root": true,
|
||||||
"extends": [
|
"extends": [
|
||||||
"@mixan/eslint-config/base",
|
"@openpanel/eslint-config/base",
|
||||||
"@mixan/eslint-config/react",
|
"@openpanel/eslint-config/react",
|
||||||
"@mixan/eslint-config/nextjs"
|
"@openpanel/eslint-config/nextjs"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"prettier": "@mixan/prettier-config"
|
"prettier": "@openpanel/prettier-config"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,15 +13,14 @@ import {
|
|||||||
} from '@/components/ui/dropdown-menu';
|
} from '@/components/ui/dropdown-menu';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { ChevronRight, MoreHorizontal, PlusIcon, Trash } from 'lucide-react';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getDefaultIntervalByDates,
|
getDefaultIntervalByDates,
|
||||||
getDefaultIntervalByRange,
|
getDefaultIntervalByRange,
|
||||||
} from '@mixan/constants';
|
} from '@openpanel/constants';
|
||||||
import type { getReportsByDashboardId } from '@mixan/db';
|
import type { getReportsByDashboardId } from '@openpanel/db';
|
||||||
|
import { ChevronRight, MoreHorizontal, PlusIcon, Trash } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import { OverviewReportRange } from '../../overview-sticky-header';
|
import { OverviewReportRange } from '../../overview-sticky-header';
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
|
import { getDashboardById, getReportsByDashboardId } from '@openpanel/db';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
import { getDashboardById, getReportsByDashboardId } from '@mixan/db';
|
|
||||||
|
|
||||||
import { ListReports } from './list-reports';
|
import { ListReports } from './list-reports';
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { ToastAction } from '@/components/ui/toast';
|
import { ToastAction } from '@/components/ui/toast';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { pushModal } from '@/modals';
|
import { pushModal } from '@/modals';
|
||||||
|
import type { IServiceDashboards } from '@openpanel/db';
|
||||||
import { LayoutPanelTopIcon, Pencil, PlusIcon, Trash } from 'lucide-react';
|
import { LayoutPanelTopIcon, Pencil, PlusIcon, Trash } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import type { IServiceDashboards } from '@mixan/db';
|
|
||||||
|
|
||||||
interface ListDashboardsProps {
|
interface ListDashboardsProps {
|
||||||
dashboards: IServiceDashboards;
|
dashboards: IServiceDashboards;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
|
import { getDashboardsByProjectId } from '@openpanel/db';
|
||||||
import { getDashboardsByProjectId } from '@mixan/db';
|
|
||||||
|
|
||||||
import { HeaderDashboards } from './header-dashboards';
|
import { HeaderDashboards } from './header-dashboards';
|
||||||
import { ListDashboards } from './list-dashboards';
|
import { ListDashboards } from './list-dashboards';
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { ChartSwitchShortcut } from '@/components/report/chart';
|
import { ChartSwitchShortcut } from '@/components/report/chart';
|
||||||
|
import type { IChartEvent } from '@openpanel/validation';
|
||||||
import type { IChartEvent } from '@mixan/validation';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|||||||
@@ -13,10 +13,9 @@ import {
|
|||||||
useEventQueryFilters,
|
useEventQueryFilters,
|
||||||
useEventQueryNamesFilter,
|
useEventQueryNamesFilter,
|
||||||
} from '@/hooks/useEventQueryFilters';
|
} from '@/hooks/useEventQueryFilters';
|
||||||
|
import type { IServiceCreateEventPayload } from '@openpanel/db';
|
||||||
import { round } from 'mathjs';
|
import { round } from 'mathjs';
|
||||||
|
|
||||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
event: IServiceCreateEventPayload;
|
event: IServiceCreateEventPayload;
|
||||||
open: boolean;
|
open: boolean;
|
||||||
|
|||||||
@@ -12,11 +12,10 @@ import {
|
|||||||
SheetTitle,
|
SheetTitle,
|
||||||
} from '@/components/ui/sheet';
|
} from '@/components/ui/sheet';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
import type { IServiceCreateEventPayload } from '@openpanel/db';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
EventIconColors,
|
EventIconColors,
|
||||||
EventIconMapper,
|
EventIconMapper,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
SheetTrigger,
|
SheetTrigger,
|
||||||
} from '@/components/ui/sheet';
|
} from '@/components/ui/sheet';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
import type { EventMeta } from '@openpanel/db';
|
||||||
import type { VariantProps } from 'class-variance-authority';
|
import type { VariantProps } from 'class-variance-authority';
|
||||||
import { cva } from 'class-variance-authority';
|
import { cva } from 'class-variance-authority';
|
||||||
import type { LucideIcon } from 'lucide-react';
|
import type { LucideIcon } from 'lucide-react';
|
||||||
@@ -19,8 +20,6 @@ import * as Icons from 'lucide-react';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import type { EventMeta } from '@mixan/db';
|
|
||||||
|
|
||||||
const variants = cva('flex items-center justify-center shrink-0 rounded-full', {
|
const variants = cva('flex items-center justify-center shrink-0 rounded-full', {
|
||||||
variants: {
|
variants: {
|
||||||
size: {
|
size: {
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
|||||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { getProfileName } from '@/utils/getters';
|
import { getProfileName } from '@/utils/getters';
|
||||||
|
import type { IServiceCreateEventPayload } from '@openpanel/db';
|
||||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
|
||||||
|
|
||||||
import { EventDetails } from './event-details';
|
import { EventDetails } from './event-details';
|
||||||
import { EventEdit } from './event-edit';
|
import { EventEdit } from './event-edit';
|
||||||
|
|||||||
@@ -8,11 +8,10 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useCursor } from '@/hooks/useCursor';
|
import { useCursor } from '@/hooks/useCursor';
|
||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
|
import type { IServiceCreateEventPayload } from '@openpanel/db';
|
||||||
import { isSameDay } from 'date-fns';
|
import { isSameDay } from 'date-fns';
|
||||||
import { GanttChartIcon } from 'lucide-react';
|
import { GanttChartIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
|
||||||
|
|
||||||
import { EventListItem } from './event-list-item';
|
import { EventListItem } from './event-list-item';
|
||||||
import EventListener from './event-listener';
|
import EventListener from './event-listener';
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ import {
|
|||||||
} from '@/components/ui/tooltip';
|
} from '@/components/ui/tooltip';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
import type { IServiceCreateEventPayload } from '@openpanel/db';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import useWebSocket from 'react-use-websocket';
|
import useWebSocket from 'react-use-websocket';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import type { IServiceCreateEventPayload } from '@mixan/db';
|
|
||||||
|
|
||||||
const AnimatedNumbers = dynamic(() => import('react-animated-numbers'), {
|
const AnimatedNumbers = dynamic(() => import('react-animated-numbers'), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
loading: () => <div>0</div>,
|
loading: () => <div>0</div>,
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import {
|
|||||||
eventQueryNamesFilter,
|
eventQueryNamesFilter,
|
||||||
} from '@/hooks/useEventQueryFilters';
|
} from '@/hooks/useEventQueryFilters';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
|
import { getEventList, getEventsCount } from '@openpanel/db';
|
||||||
import { parseAsInteger } from 'nuqs';
|
import { parseAsInteger } from 'nuqs';
|
||||||
|
|
||||||
import { getEventList, getEventsCount } from '@mixan/db';
|
|
||||||
|
|
||||||
import { StickyBelowHeader } from '../layout-sticky-below-header';
|
import { StickyBelowHeader } from '../layout-sticky-below-header';
|
||||||
import { EventChart } from './event-chart';
|
import { EventChart } from './event-chart';
|
||||||
import { EventList } from './event-list';
|
import { EventList } from './event-list';
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useEffect } from 'react';
|
|||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { useUser } from '@clerk/nextjs';
|
import { useUser } from '@clerk/nextjs';
|
||||||
|
import type { IServiceDashboards } from '@openpanel/db';
|
||||||
import {
|
import {
|
||||||
BuildingIcon,
|
BuildingIcon,
|
||||||
CogIcon,
|
CogIcon,
|
||||||
@@ -20,8 +21,6 @@ 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 type { IServiceDashboards } from '@mixan/db';
|
|
||||||
|
|
||||||
function LinkWithIcon({
|
function LinkWithIcon({
|
||||||
href,
|
href,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
|
|||||||
@@ -2,11 +2,10 @@
|
|||||||
|
|
||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
|
import type { IServiceOrganization } from '@openpanel/db';
|
||||||
import { Building } from 'lucide-react';
|
import { Building } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import type { IServiceOrganization } from '@mixan/db';
|
|
||||||
|
|
||||||
interface LayoutOrganizationSelectorProps {
|
interface LayoutOrganizationSelectorProps {
|
||||||
organizations: IServiceOrganization[];
|
organizations: IServiceOrganization[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,9 @@
|
|||||||
|
|
||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
|
import type { getProjectsByOrganizationSlug } from '@openpanel/db';
|
||||||
import { usePathname, useRouter } from 'next/navigation';
|
import { usePathname, useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import type { getProjectsByOrganizationSlug } from '@mixan/db';
|
|
||||||
|
|
||||||
interface LayoutProjectSelectorProps {
|
interface LayoutProjectSelectorProps {
|
||||||
projects: Awaited<ReturnType<typeof getProjectsByOrganizationSlug>>;
|
projects: Awaited<ReturnType<typeof getProjectsByOrganizationSlug>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ import { useEffect, useState } from 'react';
|
|||||||
import { Logo } from '@/components/Logo';
|
import { Logo } from '@/components/Logo';
|
||||||
import { buttonVariants } from '@/components/ui/button';
|
import { buttonVariants } from '@/components/ui/button';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
import type { IServiceDashboards, IServiceOrganization } from '@openpanel/db';
|
||||||
import { Rotate as Hamburger } from 'hamburger-react';
|
import { Rotate as Hamburger } from 'hamburger-react';
|
||||||
import { PlusIcon } from 'lucide-react';
|
import { PlusIcon } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
import type { IServiceDashboards, IServiceOrganization } from '@mixan/db';
|
|
||||||
|
|
||||||
import LayoutMenu from './layout-menu';
|
import LayoutMenu from './layout-menu';
|
||||||
import LayoutOrganizationSelector from './layout-organization-selector';
|
import LayoutOrganizationSelector from './layout-organization-selector';
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {
|
|||||||
getCurrentOrganizations,
|
getCurrentOrganizations,
|
||||||
getDashboardsByOrganization,
|
getDashboardsByOrganization,
|
||||||
getDashboardsByProjectId,
|
getDashboardsByProjectId,
|
||||||
} from '@mixan/db';
|
} from '@openpanel/db';
|
||||||
|
|
||||||
import { LayoutSidebar } from './layout-sidebar';
|
import { LayoutSidebar } from './layout-sidebar';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getProjectsByOrganizationSlug } from '@mixan/db';
|
import { getProjectsByOrganizationSlug } from '@openpanel/db';
|
||||||
|
|
||||||
import LayoutProjectSelector from './layout-project-selector';
|
import LayoutProjectSelector from './layout-project-selector';
|
||||||
|
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import OverviewTopGeo from '@/components/overview/overview-top-geo';
|
|||||||
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
||||||
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
|
import { db } from '@openpanel/db';
|
||||||
import { db } from '@mixan/db';
|
|
||||||
|
|
||||||
import OverviewMetrics from '../../../../components/overview/overview-metrics';
|
import OverviewMetrics from '../../../../components/overview/overview-metrics';
|
||||||
import { CreateClient } from './create-client';
|
import { CreateClient } from './create-client';
|
||||||
|
|||||||
@@ -12,17 +12,16 @@ import {
|
|||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { getProfileName } from '@/utils/getters';
|
import { getProfileName } from '@/utils/getters';
|
||||||
import { notFound } from 'next/navigation';
|
import type { GetEventListOptions } from '@openpanel/db';
|
||||||
import { parseAsInteger, parseAsString } from 'nuqs';
|
|
||||||
|
|
||||||
import type { GetEventListOptions } from '@mixan/db';
|
|
||||||
import {
|
import {
|
||||||
getConversionEventNames,
|
getConversionEventNames,
|
||||||
getEventList,
|
getEventList,
|
||||||
getEventsCount,
|
getEventsCount,
|
||||||
getProfileById,
|
getProfileById,
|
||||||
} from '@mixan/db';
|
} from '@openpanel/db';
|
||||||
import type { IChartEvent, IChartInput } from '@mixan/validation';
|
import type { IChartEvent, IChartInput } from '@openpanel/validation';
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
import { parseAsInteger, parseAsString } from 'nuqs';
|
||||||
|
|
||||||
import { EventList } from '../../events/event-list';
|
import { EventList } from '../../events/event-list';
|
||||||
import { StickyBelowHeader } from '../../layout-sticky-below-header';
|
import { StickyBelowHeader } from '../../layout-sticky-below-header';
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import { OverviewFiltersButtons } from '@/components/overview/filters/overview-f
|
|||||||
import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer';
|
import { OverviewFiltersDrawer } from '@/components/overview/filters/overview-filters-drawer';
|
||||||
import { eventQueryFiltersParser } from '@/hooks/useEventQueryFilters';
|
import { eventQueryFiltersParser } from '@/hooks/useEventQueryFilters';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
|
import { getProfileList, getProfileListCount } from '@openpanel/db';
|
||||||
import { parseAsInteger } from 'nuqs';
|
import { parseAsInteger } from 'nuqs';
|
||||||
|
|
||||||
import { getProfileList, getProfileListCount } from '@mixan/db';
|
|
||||||
|
|
||||||
import { StickyBelowHeader } from '../layout-sticky-below-header';
|
import { StickyBelowHeader } from '../layout-sticky-below-header';
|
||||||
import { ProfileList } from './profile-list';
|
import { ProfileList } from './profile-list';
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import { KeyValue, KeyValueSubtle } from '@/components/ui/key-value';
|
|||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import { getProfileName } from '@/utils/getters';
|
import { getProfileName } from '@/utils/getters';
|
||||||
|
import type { IServiceProfile } from '@openpanel/db';
|
||||||
import type { IServiceProfile } from '@mixan/db';
|
|
||||||
|
|
||||||
type ProfileListItemProps = IServiceProfile;
|
type ProfileListItemProps = IServiceProfile;
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import { Pagination } from '@/components/Pagination';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useCursor } from '@/hooks/useCursor';
|
import { useCursor } from '@/hooks/useCursor';
|
||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
|
import type { IServiceProfile } from '@openpanel/db';
|
||||||
import { UsersIcon } from 'lucide-react';
|
import { UsersIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { IServiceProfile } from '@mixan/db';
|
|
||||||
|
|
||||||
import { ProfileListItem } from './profile-list-item';
|
import { ProfileListItem } from './profile-list-item';
|
||||||
|
|
||||||
interface ProfileListProps {
|
interface ProfileListProps {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
|
import { getOrganizationBySlug, getReportById } from '@openpanel/db';
|
||||||
import { Pencil } from 'lucide-react';
|
import { Pencil } from 'lucide-react';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
import { getOrganizationBySlug, getReportById } from '@mixan/db';
|
|
||||||
|
|
||||||
import ReportEditor from '../report-editor';
|
import ReportEditor from '../report-editor';
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
|
import { getOrganizationBySlug } from '@openpanel/db';
|
||||||
import { Pencil } from 'lucide-react';
|
import { Pencil } from 'lucide-react';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
import { getOrganizationBySlug } from '@mixan/db';
|
|
||||||
|
|
||||||
import ReportEditor from './report-editor';
|
import ReportEditor from './report-editor';
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
|
|||||||
@@ -22,11 +22,10 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
import { Sheet, SheetContent, SheetTrigger } from '@/components/ui/sheet';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
|
import type { IServiceReport } from '@openpanel/db';
|
||||||
import { endOfDay, startOfDay } from 'date-fns';
|
import { endOfDay, startOfDay } from 'date-fns';
|
||||||
import { GanttChartSquareIcon } from 'lucide-react';
|
import { GanttChartSquareIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { IServiceReport } from '@mixan/db';
|
|
||||||
|
|
||||||
interface ReportEditorProps {
|
interface ReportEditorProps {
|
||||||
report: IServiceReport | null;
|
report: IServiceReport | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import { DataTable } from '@/components/DataTable';
|
|||||||
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';
|
||||||
|
import type { getClientsByOrganizationId } from '@openpanel/db';
|
||||||
import { PlusIcon } from 'lucide-react';
|
import { PlusIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { getClientsByOrganizationId } from '@mixan/db';
|
|
||||||
|
|
||||||
interface ListClientsProps {
|
interface ListClientsProps {
|
||||||
clients: Awaited<ReturnType<typeof getClientsByOrganizationId>>;
|
clients: Awaited<ReturnType<typeof getClientsByOrganizationId>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
|
import { getClientsByOrganizationId } from '@openpanel/db';
|
||||||
import { getClientsByOrganizationId } from '@mixan/db';
|
|
||||||
|
|
||||||
import ListClients from './list-clients';
|
import ListClients from './list-clients';
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,12 @@ import { api, handleError } from '@/app/_trpc/client';
|
|||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||||
|
import type { getOrganizationBySlug } from '@openpanel/db';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { getOrganizationBySlug } from '@mixan/db';
|
|
||||||
|
|
||||||
const validator = z.object({
|
const validator = z.object({
|
||||||
id: z.string().min(2),
|
id: z.string().min(2),
|
||||||
name: z.string().min(2),
|
name: z.string().min(2),
|
||||||
|
|||||||
@@ -3,14 +3,13 @@ import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { zInviteUser } from '@openpanel/validation';
|
||||||
import { SendIcon } from 'lucide-react';
|
import { SendIcon } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { zInviteUser } from '@mixan/validation';
|
|
||||||
|
|
||||||
type IForm = z.infer<typeof zInviteUser>;
|
type IForm = z.infer<typeof zInviteUser>;
|
||||||
|
|
||||||
export function InviteUser() {
|
export function InviteUser() {
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||||
|
import type { IServiceInvites } from '@openpanel/db';
|
||||||
import type { IServiceInvites } from '@mixan/db';
|
|
||||||
|
|
||||||
import { InviteUser } from './invite-user';
|
import { InviteUser } from './invite-user';
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { clerkClient } from '@clerk/nextjs';
|
import { clerkClient } from '@clerk/nextjs';
|
||||||
|
import { getInvites, getOrganizationBySlug } from '@openpanel/db';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
import { getInvites, getOrganizationBySlug } from '@mixan/db';
|
|
||||||
|
|
||||||
import EditOrganization from './edit-organization';
|
import EditOrganization from './edit-organization';
|
||||||
import InvitedUsers from './invited-users';
|
import InvitedUsers from './invited-users';
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
|||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import type { getUserById } from '@openpanel/db';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { getUserById } from '@mixan/db';
|
|
||||||
|
|
||||||
const validator = z.object({
|
const validator = z.object({
|
||||||
firstName: z.string().min(2),
|
firstName: z.string().min(2),
|
||||||
lastName: z.string().min(2),
|
lastName: z.string().min(2),
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
import { auth } from '@clerk/nextjs';
|
import { auth } from '@clerk/nextjs';
|
||||||
|
import { getUserById } from '@openpanel/db';
|
||||||
import { getUserById } from '@mixan/db';
|
|
||||||
|
|
||||||
import EditProfile from './edit-profile';
|
import EditProfile from './edit-profile';
|
||||||
import { Logout } from './logout';
|
import { Logout } from './logout';
|
||||||
|
|||||||
@@ -6,10 +6,9 @@ import { columns } from '@/components/projects/table';
|
|||||||
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';
|
||||||
|
import type { getProjectsByOrganizationSlug } from '@openpanel/db';
|
||||||
import { PlusIcon } from 'lucide-react';
|
import { PlusIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { getProjectsByOrganizationSlug } from '@mixan/db';
|
|
||||||
|
|
||||||
interface ListProjectsProps {
|
interface ListProjectsProps {
|
||||||
projects: Awaited<ReturnType<typeof getProjectsByOrganizationSlug>>;
|
projects: Awaited<ReturnType<typeof getProjectsByOrganizationSlug>>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
|
import { getProjectsByOrganizationSlug } from '@openpanel/db';
|
||||||
import { getProjectsByOrganizationSlug } from '@mixan/db';
|
|
||||||
|
|
||||||
import ListProjects from './list-projects';
|
import ListProjects from './list-projects';
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import { DataTable } from '@/components/DataTable';
|
|||||||
import { columns } from '@/components/references/table';
|
import { columns } from '@/components/references/table';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { pushModal } from '@/modals';
|
import { pushModal } from '@/modals';
|
||||||
|
import type { IServiceReference } from '@openpanel/db';
|
||||||
import { PlusIcon } from 'lucide-react';
|
import { PlusIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { IServiceReference } from '@mixan/db';
|
|
||||||
|
|
||||||
interface ListProjectsProps {
|
interface ListProjectsProps {
|
||||||
data: IServiceReference[];
|
data: IServiceReference[];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationId]/[projectId]/page-layout';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
|
import { getReferences } from '@openpanel/db';
|
||||||
import { getReferences } from '@mixan/db';
|
|
||||||
|
|
||||||
import ListReferences from './list-references';
|
import ListReferences from './list-references';
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { LogoSquare } from '@/components/Logo';
|
import { LogoSquare } from '@/components/Logo';
|
||||||
import { ProjectCard } from '@/components/projects/project-card';
|
import { ProjectCard } from '@/components/projects/project-card';
|
||||||
import { notFound, redirect } from 'next/navigation';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getOrganizationBySlug,
|
getOrganizationBySlug,
|
||||||
getProjectsByOrganizationSlug,
|
getProjectsByOrganizationSlug,
|
||||||
} from '@mixan/db';
|
} from '@openpanel/db';
|
||||||
|
import { notFound, redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { CreateProject } from './create-project';
|
import { CreateProject } from './create-project';
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
// import { CreateOrganization } from '@clerk/nextjs';
|
// import { CreateOrganization } from '@clerk/nextjs';
|
||||||
|
|
||||||
import { LogoSquare } from '@/components/Logo';
|
import { LogoSquare } from '@/components/Logo';
|
||||||
|
import { getCurrentOrganizations } from '@openpanel/db';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import { getCurrentOrganizations } from '@mixan/db';
|
|
||||||
|
|
||||||
import { CreateOrganization } from './create-organization';
|
import { CreateOrganization } from './create-organization';
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
|
|||||||
@@ -11,10 +11,9 @@ import OverviewTopEvents from '@/components/overview/overview-top-events';
|
|||||||
import OverviewTopGeo from '@/components/overview/overview-top-geo';
|
import OverviewTopGeo from '@/components/overview/overview-top-geo';
|
||||||
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
||||||
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
||||||
|
import { getOrganizationBySlug, getShareOverviewById } from '@openpanel/db';
|
||||||
import { notFound } from 'next/navigation';
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
import { getOrganizationBySlug, getShareOverviewById } from '@mixan/db';
|
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
params: {
|
params: {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -3,12 +3,11 @@
|
|||||||
import { api } from '@/app/_trpc/client';
|
import { api } from '@/app/_trpc/client';
|
||||||
import { pushModal, showConfirm } from '@/modals';
|
import { pushModal, showConfirm } from '@/modals';
|
||||||
import { clipboard } from '@/utils/clipboard';
|
import { clipboard } from '@/utils/clipboard';
|
||||||
|
import type { IServiceClientWithProject } from '@openpanel/db';
|
||||||
import { MoreHorizontal } from 'lucide-react';
|
import { MoreHorizontal } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import type { IServiceClientWithProject } from '@mixan/db';
|
|
||||||
|
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { formatDate } from '@/utils/date';
|
import { formatDate } from '@/utils/date';
|
||||||
|
import type { IServiceClientWithProject } from '@openpanel/db';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
import type { IServiceClientWithProject } from '@mixan/db';
|
|
||||||
|
|
||||||
import { ClientActions } from './ClientActions';
|
import { ClientActions } from './ClientActions';
|
||||||
|
|
||||||
export const columns: ColumnDef<IServiceClientWithProject>[] = [
|
export const columns: ColumnDef<IServiceClientWithProject>[] = [
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { toDots } from '@mixan/common';
|
import { toDots } from '@openpanel/common';
|
||||||
|
|
||||||
import { Table, TableBody, TableCell, TableRow } from '../ui/table';
|
import { Table, TableBody, TableCell, TableRow } from '../ui/table';
|
||||||
|
|
||||||
|
|||||||
@@ -11,14 +11,13 @@ import {
|
|||||||
import { useEventValues } from '@/hooks/useEventValues';
|
import { useEventValues } from '@/hooks/useEventValues';
|
||||||
import { useProfileProperties } from '@/hooks/useProfileProperties';
|
import { useProfileProperties } from '@/hooks/useProfileProperties';
|
||||||
import { useProfileValues } from '@/hooks/useProfileValues';
|
import { useProfileValues } from '@/hooks/useProfileValues';
|
||||||
import { XIcon } from 'lucide-react';
|
|
||||||
import type { Options as NuqsOptions } from 'nuqs';
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
IChartEventFilter,
|
IChartEventFilter,
|
||||||
IChartEventFilterOperator,
|
IChartEventFilterOperator,
|
||||||
IChartEventFilterValue,
|
IChartEventFilterValue,
|
||||||
} from '@mixan/validation';
|
} from '@openpanel/validation';
|
||||||
|
import { XIcon } from 'lucide-react';
|
||||||
|
import type { Options as NuqsOptions } from 'nuqs';
|
||||||
|
|
||||||
export interface OverviewFiltersDrawerContentProps {
|
export interface OverviewFiltersDrawerContentProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getLiveVisitors } from '@mixan/db';
|
import { getLiveVisitors } from '@openpanel/db';
|
||||||
|
|
||||||
import type { LiveCounterProps } from './live-counter';
|
import type { LiveCounterProps } from './live-counter';
|
||||||
import LiveCounter from './live-counter';
|
import LiveCounter from './live-counter';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getConversionEventNames } from '@mixan/db';
|
import { getConversionEventNames } from '@openpanel/db';
|
||||||
|
|
||||||
import type { OverviewLatestEventsProps } from './overview-latest-events';
|
import type { OverviewLatestEventsProps } from './overview-latest-events';
|
||||||
import OverviewLatestEvents from './overview-latest-events';
|
import OverviewLatestEvents from './overview-latest-events';
|
||||||
|
|||||||
@@ -3,11 +3,10 @@
|
|||||||
import { Fragment } from 'react';
|
import { Fragment } from 'react';
|
||||||
import { api } from '@/app/_trpc/client';
|
import { api } from '@/app/_trpc/client';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
import type { IChartInput } from '@openpanel/validation';
|
||||||
import { ChevronsUpDownIcon } from 'lucide-react';
|
import { ChevronsUpDownIcon } from 'lucide-react';
|
||||||
import AnimateHeight from 'react-animate-height';
|
import AnimateHeight from 'react-animate-height';
|
||||||
|
|
||||||
import type { IChartInput } from '@mixan/validation';
|
|
||||||
|
|
||||||
import { redisSub } from '../../../../../packages/redis';
|
import { redisSub } from '../../../../../packages/redis';
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||||
import { useOverviewOptions } from './useOverviewOptions';
|
import { useOverviewOptions } from './useOverviewOptions';
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import { ChartSwitch } from '@/components/report/chart';
|
|||||||
import { Widget, WidgetBody } from '@/components/Widget';
|
import { Widget, WidgetBody } from '@/components/Widget';
|
||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
import type { IChartInput } from '@openpanel/validation';
|
||||||
import type { IChartInput } from '@mixan/validation';
|
|
||||||
|
|
||||||
interface OverviewMetricsProps {
|
interface OverviewMetricsProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
|
|||||||
@@ -2,12 +2,11 @@
|
|||||||
|
|
||||||
import { api } from '@/app/_trpc/client';
|
import { api } from '@/app/_trpc/client';
|
||||||
import { pushModal } from '@/modals';
|
import { pushModal } from '@/modals';
|
||||||
|
import type { ShareOverview } from '@openpanel/db';
|
||||||
import { EyeIcon, Globe2Icon, LockIcon } from 'lucide-react';
|
import { EyeIcon, Globe2Icon, LockIcon } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
import type { ShareOverview } from '@mixan/db';
|
|
||||||
|
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getConversionEventNames } from '@mixan/db';
|
import { getConversionEventNames } from '@openpanel/db';
|
||||||
|
|
||||||
import type { OverviewTopEventsProps } from './overview-top-events';
|
import type { OverviewTopEventsProps } from './overview-top-events';
|
||||||
import OverviewTopEvents from './overview-top-events';
|
import OverviewTopEvents from './overview-top-events';
|
||||||
|
|||||||
@@ -1,3 +1,9 @@
|
|||||||
|
import {
|
||||||
|
getDefaultIntervalByDates,
|
||||||
|
getDefaultIntervalByRange,
|
||||||
|
timeRanges,
|
||||||
|
} from '@openpanel/constants';
|
||||||
|
import { mapKeys } from '@openpanel/validation';
|
||||||
import {
|
import {
|
||||||
parseAsBoolean,
|
parseAsBoolean,
|
||||||
parseAsInteger,
|
parseAsInteger,
|
||||||
@@ -6,13 +12,6 @@ import {
|
|||||||
useQueryState,
|
useQueryState,
|
||||||
} from 'nuqs';
|
} from 'nuqs';
|
||||||
|
|
||||||
import {
|
|
||||||
getDefaultIntervalByDates,
|
|
||||||
getDefaultIntervalByRange,
|
|
||||||
timeRanges,
|
|
||||||
} from '@mixan/constants';
|
|
||||||
import { mapKeys } from '@mixan/validation';
|
|
||||||
|
|
||||||
const nuqsOptions = { history: 'push' } as const;
|
const nuqsOptions = { history: 'push' } as const;
|
||||||
|
|
||||||
export function useOverviewOptions() {
|
export function useOverviewOptions() {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
|
import { mapKeys } from '@openpanel/validation';
|
||||||
|
import type { IChartInput } from '@openpanel/validation';
|
||||||
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
||||||
|
|
||||||
import { mapKeys } from '@mixan/validation';
|
|
||||||
import type { IChartInput } from '@mixan/validation';
|
|
||||||
|
|
||||||
export function useOverviewWidget<T extends string>(
|
export function useOverviewWidget<T extends string>(
|
||||||
key: string,
|
key: string,
|
||||||
widgets: Record<
|
widgets: Record<
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
import type { IServiceProfile } from '@openpanel/db';
|
||||||
import { AvatarImage } from '@radix-ui/react-avatar';
|
import { AvatarImage } from '@radix-ui/react-avatar';
|
||||||
import type { VariantProps } from 'class-variance-authority';
|
import type { VariantProps } from 'class-variance-authority';
|
||||||
import { cva } from 'class-variance-authority';
|
import { cva } from 'class-variance-authority';
|
||||||
|
|
||||||
import type { IServiceProfile } from '@mixan/db';
|
|
||||||
|
|
||||||
import { Avatar, AvatarFallback } from '../ui/avatar';
|
import { Avatar, AvatarFallback } from '../ui/avatar';
|
||||||
|
|
||||||
interface ProfileAvatarProps
|
interface ProfileAvatarProps
|
||||||
|
|||||||
@@ -3,12 +3,11 @@
|
|||||||
import { api } from '@/app/_trpc/client';
|
import { api } from '@/app/_trpc/client';
|
||||||
import { pushModal, showConfirm } from '@/modals';
|
import { pushModal, showConfirm } from '@/modals';
|
||||||
import { clipboard } from '@/utils/clipboard';
|
import { clipboard } from '@/utils/clipboard';
|
||||||
|
import type { IServiceProject } from '@openpanel/db';
|
||||||
import { MoreHorizontal } from 'lucide-react';
|
import { MoreHorizontal } from 'lucide-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
import type { IServiceProject } from '@mixan/db';
|
|
||||||
|
|
||||||
import { Button } from '../ui/button';
|
import { Button } from '../ui/button';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { shortNumber } from '@/hooks/useNumerFormatter';
|
import { shortNumber } from '@/hooks/useNumerFormatter';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
import type { IServiceProject } from '@mixan/db';
|
import type { IServiceProject } from '@openpanel/db';
|
||||||
import { chQuery } from '@mixan/db';
|
import { chQuery } from '@openpanel/db';
|
||||||
|
|
||||||
import { ChartSSR } from '../chart-ssr';
|
import { ChartSSR } from '../chart-ssr';
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { formatDate } from '@/utils/date';
|
import { formatDate } from '@/utils/date';
|
||||||
|
import { IServiceProject } from '@openpanel/db';
|
||||||
|
import type { Project as IProject } from '@openpanel/db';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
import { IServiceProject } from '@mixan/db';
|
|
||||||
import type { Project as IProject } from '@mixan/db';
|
|
||||||
|
|
||||||
import { ProjectActions } from './ProjectActions';
|
import { ProjectActions } from './ProjectActions';
|
||||||
|
|
||||||
export type Project = IProject;
|
export type Project = IProject;
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { formatDate, formatDateTime } from '@/utils/date';
|
import { formatDate, formatDateTime } from '@/utils/date';
|
||||||
|
import type { IServiceReference } from '@openpanel/db';
|
||||||
import type { ColumnDef } from '@tanstack/react-table';
|
import type { ColumnDef } from '@tanstack/react-table';
|
||||||
|
|
||||||
import type { IServiceReference } from '@mixan/db';
|
|
||||||
|
|
||||||
export const columns: ColumnDef<IServiceReference>[] = [
|
export const columns: ColumnDef<IServiceReference>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: 'title',
|
accessorKey: 'title',
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
|
import { chartTypes } from '@openpanel/constants';
|
||||||
|
import { objectToZodEnums } from '@openpanel/validation';
|
||||||
import { LineChartIcon } from 'lucide-react';
|
import { LineChartIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { chartTypes } from '@mixan/constants';
|
|
||||||
import { objectToZodEnums } from '@mixan/validation';
|
|
||||||
|
|
||||||
import { Combobox } from '../ui/combobox';
|
import { Combobox } from '../ui/combobox';
|
||||||
import { changeChartType } from './reportSlice';
|
import { changeChartType } from './reportSlice';
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
import { ClockIcon } from 'lucide-react';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
isHourIntervalEnabledByRange,
|
isHourIntervalEnabledByRange,
|
||||||
isMinuteIntervalEnabledByRange,
|
isMinuteIntervalEnabledByRange,
|
||||||
} from '@mixan/constants';
|
} from '@openpanel/constants';
|
||||||
import type { IInterval } from '@mixan/validation';
|
import type { IInterval } from '@openpanel/validation';
|
||||||
|
import { ClockIcon } from 'lucide-react';
|
||||||
|
|
||||||
import { Combobox } from '../ui/combobox';
|
import { Combobox } from '../ui/combobox';
|
||||||
import { changeInterval } from './reportSlice';
|
import { changeInterval } from './reportSlice';
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
|
import { lineTypes } from '@openpanel/constants';
|
||||||
|
import { objectToZodEnums } from '@openpanel/validation';
|
||||||
import { Tv2Icon } from 'lucide-react';
|
import { Tv2Icon } from 'lucide-react';
|
||||||
|
|
||||||
import { lineTypes } from '@mixan/constants';
|
|
||||||
import { objectToZodEnums } from '@mixan/validation';
|
|
||||||
|
|
||||||
import { Combobox } from '../ui/combobox';
|
import { Combobox } from '../ui/combobox';
|
||||||
import { changeLineType } from './reportSlice';
|
import { changeLineType } from './reportSlice';
|
||||||
|
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ import {
|
|||||||
import { useBreakpoint } from '@/hooks/useBreakpoint';
|
import { useBreakpoint } from '@/hooks/useBreakpoint';
|
||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
import { timeRanges } from '@openpanel/constants';
|
||||||
|
import type { IChartRange } from '@openpanel/validation';
|
||||||
import { endOfDay, format, startOfDay } from 'date-fns';
|
import { endOfDay, format, startOfDay } from 'date-fns';
|
||||||
import { CalendarIcon, ChevronsUpDownIcon } from 'lucide-react';
|
import { CalendarIcon, ChevronsUpDownIcon } from 'lucide-react';
|
||||||
import type { SelectRangeEventHandler } from 'react-day-picker';
|
import type { SelectRangeEventHandler } from 'react-day-picker';
|
||||||
|
|
||||||
import { timeRanges } from '@mixan/constants';
|
|
||||||
import type { IChartRange } from '@mixan/validation';
|
|
||||||
|
|
||||||
import type { ExtendedComboboxProps } from '../ui/combobox';
|
import type { ExtendedComboboxProps } from '../ui/combobox';
|
||||||
import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group';
|
import { ToggleGroup, ToggleGroupItem } from '../ui/toggle-group';
|
||||||
import { changeDates, changeEndDate, changeStartDate } from './reportSlice';
|
import { changeDates, changeEndDate, changeStartDate } from './reportSlice';
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { api } from '@/app/_trpc/client';
|
import { api } from '@/app/_trpc/client';
|
||||||
|
import type { IChartInput } from '@openpanel/validation';
|
||||||
import type { IChartInput } from '@mixan/validation';
|
|
||||||
|
|
||||||
import { ChartEmpty } from './ChartEmpty';
|
import { ChartEmpty } from './ChartEmpty';
|
||||||
import { ReportAreaChart } from './ReportAreaChart';
|
import { ReportAreaChart } from './ReportAreaChart';
|
||||||
|
|||||||
@@ -10,8 +10,7 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
import type { IChartSerie } from '@/server/api/routers/chart';
|
import type { IChartSerie } from '@/server/api/routers/chart';
|
||||||
|
import type { IChartInput } from '@openpanel/validation';
|
||||||
import type { IChartInput } from '@mixan/validation';
|
|
||||||
|
|
||||||
import { ChartLoading } from './ChartLoading';
|
import { ChartLoading } from './ChartLoading';
|
||||||
import { MetricCardLoading } from './MetricCard';
|
import { MetricCardLoading } from './MetricCard';
|
||||||
|
|||||||
@@ -4,11 +4,10 @@ import type { IChartData } from '@/app/_trpc/client';
|
|||||||
import { ColorSquare } from '@/components/ColorSquare';
|
import { ColorSquare } from '@/components/ColorSquare';
|
||||||
import { fancyMinutes, useNumber } from '@/hooks/useNumerFormatter';
|
import { fancyMinutes, useNumber } from '@/hooks/useNumerFormatter';
|
||||||
import { theme } from '@/utils/theme';
|
import { theme } from '@/utils/theme';
|
||||||
|
import type { IChartMetric } from '@openpanel/validation';
|
||||||
import AutoSizer from 'react-virtualized-auto-sizer';
|
import AutoSizer from 'react-virtualized-auto-sizer';
|
||||||
import { Area, AreaChart } from 'recharts';
|
import { Area, AreaChart } from 'recharts';
|
||||||
|
|
||||||
import type { IChartMetric } from '@mixan/validation';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
getDiffIndicator,
|
getDiffIndicator,
|
||||||
PreviousDiffIndicatorText,
|
PreviousDiffIndicatorText,
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useNumber } from '@/hooks/useNumerFormatter';
|
|||||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||||
import { getChartColor } from '@/utils/theme';
|
import { getChartColor } from '@/utils/theme';
|
||||||
|
import type { IChartLineType, IInterval } from '@openpanel/validation';
|
||||||
import {
|
import {
|
||||||
Area,
|
Area,
|
||||||
AreaChart,
|
AreaChart,
|
||||||
@@ -14,8 +15,6 @@ import {
|
|||||||
YAxis,
|
YAxis,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
|
|
||||||
import type { IChartLineType, IInterval } from '@mixan/validation';
|
|
||||||
|
|
||||||
import { getYAxisWidth } from './chart-utils';
|
import { getYAxisWidth } from './chart-utils';
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||||
|
|||||||
@@ -6,8 +6,7 @@ import { Progress } from '@/components/ui/progress';
|
|||||||
import { useNumber } from '@/hooks/useNumerFormatter';
|
import { useNumber } from '@/hooks/useNumerFormatter';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { getChartColor } from '@/utils/theme';
|
import { getChartColor } from '@/utils/theme';
|
||||||
|
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||||
import { NOT_SET_VALUE } from '@mixan/constants';
|
|
||||||
|
|
||||||
import { PreviousDiffIndicatorText } from '../PreviousDiffIndicator';
|
import { PreviousDiffIndicatorText } from '../PreviousDiffIndicator';
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import { useNumber } from '@/hooks/useNumerFormatter';
|
|||||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||||
import { getChartColor, theme } from '@/utils/theme';
|
import { getChartColor, theme } from '@/utils/theme';
|
||||||
|
import type { IInterval } from '@openpanel/validation';
|
||||||
import { Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts';
|
import { Bar, BarChart, CartesianGrid, Tooltip, XAxis, YAxis } from 'recharts';
|
||||||
|
|
||||||
import type { IInterval } from '@mixan/validation';
|
|
||||||
|
|
||||||
import { getYAxisWidth } from './chart-utils';
|
import { getYAxisWidth } from './chart-utils';
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { useNumber } from '@/hooks/useNumerFormatter';
|
|||||||
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
import { useRechartDataModel } from '@/hooks/useRechartDataModel';
|
||||||
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
import { useVisibleSeries } from '@/hooks/useVisibleSeries';
|
||||||
import { getChartColor } from '@/utils/theme';
|
import { getChartColor } from '@/utils/theme';
|
||||||
|
import type { IServiceReference } from '@openpanel/db';
|
||||||
|
import type { IChartLineType, IInterval } from '@openpanel/validation';
|
||||||
import {
|
import {
|
||||||
CartesianGrid,
|
CartesianGrid,
|
||||||
Line,
|
Line,
|
||||||
@@ -17,9 +19,6 @@ import {
|
|||||||
YAxis,
|
YAxis,
|
||||||
} from 'recharts';
|
} from 'recharts';
|
||||||
|
|
||||||
import type { IServiceReference } from '@mixan/db';
|
|
||||||
import type { IChartLineType, IInterval } from '@mixan/validation';
|
|
||||||
|
|
||||||
import { getYAxisWidth } from './chart-utils';
|
import { getYAxisWidth } from './chart-utils';
|
||||||
import { useChartContext } from './ChartProvider';
|
import { useChartContext } from './ChartProvider';
|
||||||
import { ReportChartTooltip } from './ReportChartTooltip';
|
import { ReportChartTooltip } from './ReportChartTooltip';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
|
import { NOT_SET_VALUE } from '@openpanel/constants';
|
||||||
import type { LucideIcon, LucideProps } from 'lucide-react';
|
import type { LucideIcon, LucideProps } from 'lucide-react';
|
||||||
import {
|
import {
|
||||||
ActivityIcon,
|
ActivityIcon,
|
||||||
@@ -14,8 +15,6 @@ import {
|
|||||||
TabletIcon,
|
TabletIcon,
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
import { NOT_SET_VALUE } from '@mixan/constants';
|
|
||||||
|
|
||||||
interface SerieIconProps extends LucideProps {
|
interface SerieIconProps extends LucideProps {
|
||||||
name: string;
|
name: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { IChartInput } from '@mixan/validation';
|
import type { IChartInput } from '@openpanel/validation';
|
||||||
|
|
||||||
import { Funnel } from '../funnel';
|
import { Funnel } from '../funnel';
|
||||||
import { Chart } from './Chart';
|
import { Chart } from './Chart';
|
||||||
|
|||||||
@@ -2,8 +2,7 @@
|
|||||||
|
|
||||||
import type { RouterOutputs } from '@/app/_trpc/client';
|
import type { RouterOutputs } from '@/app/_trpc/client';
|
||||||
import { api } from '@/app/_trpc/client';
|
import { api } from '@/app/_trpc/client';
|
||||||
|
import type { IChartInput } from '@openpanel/validation';
|
||||||
import type { IChartInput } from '@mixan/validation';
|
|
||||||
|
|
||||||
import { ChartEmpty } from '../chart/ChartEmpty';
|
import { ChartEmpty } from '../chart/ChartEmpty';
|
||||||
import { withChartProivder } from '../chart/ChartProvider';
|
import { withChartProivder } from '../chart/ChartProvider';
|
||||||
|
|||||||
@@ -1,15 +1,11 @@
|
|||||||
import { start } from 'repl';
|
import { start } from 'repl';
|
||||||
import { createSlice } from '@reduxjs/toolkit';
|
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
|
||||||
import { isSameDay, isSameMonth } from 'date-fns';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
alphabetIds,
|
alphabetIds,
|
||||||
getDefaultIntervalByDates,
|
getDefaultIntervalByDates,
|
||||||
getDefaultIntervalByRange,
|
getDefaultIntervalByRange,
|
||||||
isHourIntervalEnabledByRange,
|
isHourIntervalEnabledByRange,
|
||||||
isMinuteIntervalEnabledByRange,
|
isMinuteIntervalEnabledByRange,
|
||||||
} from '@mixan/constants';
|
} from '@openpanel/constants';
|
||||||
import type {
|
import type {
|
||||||
IChartBreakdown,
|
IChartBreakdown,
|
||||||
IChartEvent,
|
IChartEvent,
|
||||||
@@ -18,7 +14,10 @@ import type {
|
|||||||
IChartRange,
|
IChartRange,
|
||||||
IChartType,
|
IChartType,
|
||||||
IInterval,
|
IInterval,
|
||||||
} from '@mixan/validation';
|
} from '@openpanel/validation';
|
||||||
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
|
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||||
|
import { isSameDay, isSameMonth } from 'date-fns';
|
||||||
|
|
||||||
type InitialState = IChartInput & {
|
type InitialState = IChartInput & {
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
|
|||||||
@@ -3,10 +3,9 @@ import { Combobox } from '@/components/ui/combobox';
|
|||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useDispatch } from '@/redux';
|
import { useDispatch } from '@/redux';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
import type { IChartEvent } from '@openpanel/validation';
|
||||||
import { DatabaseIcon } from 'lucide-react';
|
import { DatabaseIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { IChartEvent } from '@mixan/validation';
|
|
||||||
|
|
||||||
import { changeEvent } from '../reportSlice';
|
import { changeEvent } from '../reportSlice';
|
||||||
|
|
||||||
interface EventPropertiesComboboxProps {
|
interface EventPropertiesComboboxProps {
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import { ColorSquare } from '@/components/ColorSquare';
|
|||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
|
import type { IChartBreakdown } from '@openpanel/validation';
|
||||||
import { SplitIcon } from 'lucide-react';
|
import { SplitIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { IChartBreakdown } from '@mixan/validation';
|
|
||||||
|
|
||||||
import { addBreakdown, changeBreakdown, removeBreakdown } from '../reportSlice';
|
import { addBreakdown, changeBreakdown, removeBreakdown } from '../reportSlice';
|
||||||
import { ReportBreakdownMore } from './ReportBreakdownMore';
|
import { ReportBreakdownMore } from './ReportBreakdownMore';
|
||||||
import type { ReportEventMoreProps } from './ReportEventMore';
|
import type { ReportEventMoreProps } from './ReportEventMore';
|
||||||
|
|||||||
@@ -9,10 +9,9 @@ import { useAppParams } from '@/hooks/useAppParams';
|
|||||||
import { useDebounceFn } from '@/hooks/useDebounceFn';
|
import { useDebounceFn } from '@/hooks/useDebounceFn';
|
||||||
import { useEventNames } from '@/hooks/useEventNames';
|
import { useEventNames } from '@/hooks/useEventNames';
|
||||||
import { useDispatch, useSelector } from '@/redux';
|
import { useDispatch, useSelector } from '@/redux';
|
||||||
|
import type { IChartEvent } from '@openpanel/validation';
|
||||||
import { GanttChart, GanttChartIcon, Users } from 'lucide-react';
|
import { GanttChart, GanttChartIcon, Users } from 'lucide-react';
|
||||||
|
|
||||||
import type { IChartEvent } from '@mixan/validation';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
addEvent,
|
addEvent,
|
||||||
changeEvent,
|
changeEvent,
|
||||||
|
|||||||
@@ -7,15 +7,14 @@ import { RenderDots } from '@/components/ui/RenderDots';
|
|||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useMappings } from '@/hooks/useMappings';
|
import { useMappings } from '@/hooks/useMappings';
|
||||||
import { useDispatch } from '@/redux';
|
import { useDispatch } from '@/redux';
|
||||||
import { SlidersHorizontal, Trash } from 'lucide-react';
|
import { operators } from '@openpanel/constants';
|
||||||
|
|
||||||
import { operators } from '@mixan/constants';
|
|
||||||
import type {
|
import type {
|
||||||
IChartEvent,
|
IChartEvent,
|
||||||
IChartEventFilterOperator,
|
IChartEventFilterOperator,
|
||||||
IChartEventFilterValue,
|
IChartEventFilterValue,
|
||||||
} from '@mixan/validation';
|
} from '@openpanel/validation';
|
||||||
import { mapKeys } from '@mixan/validation';
|
import { mapKeys } from '@openpanel/validation';
|
||||||
|
import { SlidersHorizontal, Trash } from 'lucide-react';
|
||||||
|
|
||||||
import { changeEvent } from '../../reportSlice';
|
import { changeEvent } from '../../reportSlice';
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import { api } from '@/app/_trpc/client';
|
|||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { useDispatch } from '@/redux';
|
import { useDispatch } from '@/redux';
|
||||||
|
import type { IChartEvent } from '@openpanel/validation';
|
||||||
import { FilterIcon } from 'lucide-react';
|
import { FilterIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { IChartEvent } from '@mixan/validation';
|
|
||||||
|
|
||||||
import { changeEvent } from '../../reportSlice';
|
import { changeEvent } from '../../reportSlice';
|
||||||
|
|
||||||
interface FiltersComboboxProps {
|
interface FiltersComboboxProps {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { IChartEvent } from '@mixan/validation';
|
import type { IChartEvent } from '@openpanel/validation';
|
||||||
|
|
||||||
import { FilterItem } from './FilterItem';
|
import { FilterItem } from './FilterItem';
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { IInterval } from '@mixan/validation';
|
import type { IInterval } from '@openpanel/validation';
|
||||||
|
|
||||||
export function formatDateInterval(interval: IInterval, date: Date): string {
|
export function formatDateInterval(interval: IInterval, date: Date): string {
|
||||||
if (interval === 'hour' || interval === 'minute') {
|
if (interval === 'hour' || interval === 'minute') {
|
||||||
|
|||||||
@@ -7,13 +7,12 @@ import { Button } from '@/components/ui/button';
|
|||||||
import { Calendar } from '@/components/ui/calendar';
|
import { Calendar } from '@/components/ui/calendar';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { zCreateReference } from '@openpanel/validation';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import type { z } from 'zod';
|
import type { z } from 'zod';
|
||||||
|
|
||||||
import { zCreateReference } from '@mixan/validation';
|
|
||||||
|
|
||||||
import { popModal } from '.';
|
import { popModal } from '.';
|
||||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import { ButtonContainer } from '@/components/ButtonContainer';
|
|||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import type { IServiceClient } from '@openpanel/db';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { IServiceClient } from '@mixan/db';
|
|
||||||
|
|
||||||
import { popModal } from '.';
|
import { popModal } from '.';
|
||||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import { ButtonContainer } from '@/components/ButtonContainer';
|
|||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import type { IServiceDashboard } from '@openpanel/db';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { IServiceDashboard } from '@mixan/db';
|
|
||||||
|
|
||||||
import { popModal } from '.';
|
import { popModal } from '.';
|
||||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import { ButtonContainer } from '@/components/ButtonContainer';
|
|||||||
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
import { InputWithLabel } from '@/components/forms/InputWithLabel';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import type { IServiceProject } from '@openpanel/db';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { IServiceProject } from '@mixan/db';
|
|
||||||
|
|
||||||
import { popModal } from '.';
|
import { popModal } from '.';
|
||||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,12 @@ import { Combobox } from '@/components/ui/combobox';
|
|||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import type { IChartInput } from '@openpanel/validation';
|
||||||
import { useRouter, useSearchParams } from 'next/navigation';
|
import { useRouter, useSearchParams } from 'next/navigation';
|
||||||
import { Controller, useForm } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { IChartInput } from '@mixan/validation';
|
|
||||||
|
|
||||||
import { popModal } from '.';
|
import { popModal } from '.';
|
||||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user