improve profile page
This commit is contained in:
@@ -30,7 +30,7 @@ interface EventListProps {
|
|||||||
count: number;
|
count: number;
|
||||||
}
|
}
|
||||||
export function EventList({ data, count }: EventListProps) {
|
export function EventList({ data, count }: EventListProps) {
|
||||||
const { cursor, setCursor } = useCursor();
|
const { cursor, setCursor, loading } = useCursor();
|
||||||
const [filters] = useEventQueryFilters();
|
const [filters] = useEventQueryFilters();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -77,6 +77,7 @@ export function EventList({ data, count }: EventListProps) {
|
|||||||
setCursor={setCursor}
|
setCursor={setCursor}
|
||||||
count={count}
|
count={count}
|
||||||
take={50}
|
take={50}
|
||||||
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -92,6 +93,7 @@ export function EventList({ data, count }: EventListProps) {
|
|||||||
setCursor={setCursor}
|
setCursor={setCursor}
|
||||||
count={count}
|
count={count}
|
||||||
take={50}
|
take={50}
|
||||||
|
loading={loading}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import FullPageLoadingState from '@/components/full-page-loading-state';
|
|
||||||
|
|
||||||
export default FullPageLoadingState;
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo } from 'react';
|
import { Suspense, useMemo } from 'react';
|
||||||
import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout';
|
import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout';
|
||||||
import { ListPropertiesIcon } from '@/components/events/list-properties-icon';
|
import { ListPropertiesIcon } from '@/components/events/list-properties-icon';
|
||||||
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
|
import { OverviewFiltersButtons } from '@/components/overview/filters/overview-filters-buttons';
|
||||||
@@ -64,12 +64,7 @@ export default async function Page({
|
|||||||
};
|
};
|
||||||
const startDate = parseAsString.parseServerSide(searchParams.startDate);
|
const startDate = parseAsString.parseServerSide(searchParams.startDate);
|
||||||
const endDate = parseAsString.parseServerSide(searchParams.endDate);
|
const endDate = parseAsString.parseServerSide(searchParams.endDate);
|
||||||
const [profile, events, count, metrics] = await Promise.all([
|
const profile = await getProfileById(profileId, projectId);
|
||||||
getProfileById(profileId, projectId),
|
|
||||||
getEventList(eventListOptions),
|
|
||||||
getEventsCount(eventListOptions),
|
|
||||||
getProfileMetrics(profileId, projectId),
|
|
||||||
]);
|
|
||||||
|
|
||||||
const pageViewsChart: IChartInput = {
|
const pageViewsChart: IChartInput = {
|
||||||
projectId,
|
projectId,
|
||||||
@@ -148,11 +143,11 @@ export default async function Page({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageLayout organizationSlug={organizationSlug} title={<div />} />
|
<PageLayout organizationSlug={organizationSlug} title={<div />} />
|
||||||
<StickyBelowHeader className="!relative !top-auto !z-0 flex items-center gap-8 p-8">
|
<StickyBelowHeader className="!relative !top-auto !z-0 flex flex-col gap-8 p-4 md:flex-row md:items-center md:p-8">
|
||||||
<div className="flex flex-1 gap-4">
|
<div className="flex flex-1 gap-4">
|
||||||
<ProfileAvatar {...profile} size={'lg'} />
|
<ProfileAvatar {...profile} size={'lg'} />
|
||||||
<div className="">
|
<div className="min-w-0">
|
||||||
<h1 className="text-2xl font-semibold">
|
<h1 className="max-w-full overflow-hidden text-ellipsis break-words text-lg font-semibold md:max-w-sm md:whitespace-nowrap md:text-2xl">
|
||||||
{getProfileName(profile)}
|
{getProfileName(profile)}
|
||||||
</h1>
|
</h1>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
@@ -194,31 +189,19 @@ export default async function Page({
|
|||||||
</Widget>
|
</Widget>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<EventList data={events} count={count} />
|
<Suspense fallback={<div />}>
|
||||||
|
<EventListServer {...eventListOptions} />
|
||||||
|
</Suspense>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ValueRow({ name, value }: { name: string; value?: unknown }) {
|
async function EventListServer(props: GetEventListOptions) {
|
||||||
if (!value) {
|
const [events, count] = await Promise.all([
|
||||||
return null;
|
getEventList(props),
|
||||||
}
|
getEventsCount(props),
|
||||||
return (
|
]);
|
||||||
<div className="flex flex-row justify-between">
|
return <EventList data={events} count={count} />;
|
||||||
<div className="font-medium capitalize text-muted-foreground">
|
|
||||||
{name.replace('_', ' ')}
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center gap-2 text-right">
|
|
||||||
{typeof value === 'string' ? (
|
|
||||||
<>
|
|
||||||
<SerieIcon name={value} /> {value}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>{value}</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import withLoadingWidget from '@/hocs/with-loading-widget';
|
import withSuspense from '@/hocs/with-suspense';
|
||||||
|
|
||||||
import { getProfileMetrics } from '@openpanel/db';
|
import { getProfileMetrics } from '@openpanel/db';
|
||||||
|
|
||||||
@@ -14,4 +14,4 @@ const ProfileMetricsServer = async ({ projectId, profileId }: Props) => {
|
|||||||
return <ProfileMetrics data={data} />;
|
return <ProfileMetrics data={data} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default withLoadingWidget(ProfileMetricsServer);
|
export default withSuspense(ProfileMetricsServer, () => null);
|
||||||
|
|||||||
@@ -12,52 +12,52 @@ type Props = {
|
|||||||
const ProfileMetrics = ({ data }: Props) => {
|
const ProfileMetrics = ({ data }: Props) => {
|
||||||
const number = useNumber();
|
const number = useNumber();
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-6">
|
<div className="flex flex-wrap gap-6 whitespace-nowrap md:justify-end md:text-right">
|
||||||
<div className="rounded-xl text-right">
|
<div>
|
||||||
<div className="text-xs font-medium text-muted-foreground">
|
<div className="text-xs font-medium text-muted-foreground">
|
||||||
First seen
|
First seen
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-medium">
|
<div className="text-lg font-semibold">
|
||||||
{formatDistanceToNow(data.firstSeen)}
|
{formatDistanceToNow(data.firstSeen)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl text-right">
|
<div>
|
||||||
<div className="text-xs font-medium text-muted-foreground">
|
<div className="text-xs font-medium text-muted-foreground">
|
||||||
Last seen
|
Last seen
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-medium">
|
<div className="text-lg font-semibold">
|
||||||
{formatDistanceToNow(data.lastSeen)}
|
{formatDistanceToNow(data.lastSeen)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl text-right">
|
<div>
|
||||||
<div className="text-xs font-medium text-muted-foreground">
|
<div className="text-xs font-medium text-muted-foreground">
|
||||||
Sessions
|
Sessions
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-medium">
|
<div className="text-lg font-semibold">
|
||||||
{number.format(data.sessions)}
|
{number.format(data.sessions)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl text-right">
|
<div>
|
||||||
<div className="text-xs font-medium text-muted-foreground">
|
<div className="text-xs font-medium text-muted-foreground">
|
||||||
Avg. Session
|
Avg. Session
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-medium">
|
<div className="text-lg font-semibold">
|
||||||
{number.formatWithUnit(data.durationAvg / 1000, 'min')}
|
{number.formatWithUnit(data.durationAvg / 1000, 'min')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl text-right">
|
<div>
|
||||||
<div className="text-xs font-medium text-muted-foreground">
|
<div className="text-xs font-medium text-muted-foreground">
|
||||||
P90. Session
|
P90. Session
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-medium">
|
<div className="text-lg font-semibold">
|
||||||
{number.formatWithUnit(data.durationP90 / 1000, 'min')}
|
{number.formatWithUnit(data.durationP90 / 1000, 'min')}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-xl text-right">
|
<div>
|
||||||
<div className="text-xs font-medium text-muted-foreground">
|
<div className="text-xs font-medium text-muted-foreground">
|
||||||
Page views
|
Page views
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xl font-medium">
|
<div className="text-lg font-semibold">
|
||||||
{number.format(data.screenViews)}
|
{number.format(data.screenViews)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
import FullPageLoadingState from '@/components/full-page-loading-state';
|
|
||||||
|
|
||||||
export default FullPageLoadingState;
|
|
||||||
@@ -11,7 +11,7 @@ interface Props {
|
|||||||
filters?: IChartEventFilter[];
|
filters?: IChartEventFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const limit = 50;
|
const limit = 40;
|
||||||
|
|
||||||
async function ProfileListServer({ projectId, cursor, filters }: Props) {
|
async function ProfileListServer({ projectId, cursor, filters }: Props) {
|
||||||
const [profiles, count] = await Promise.all([
|
const [profiles, count] = await Promise.all([
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ interface ProfileListProps {
|
|||||||
}
|
}
|
||||||
export function ProfileList({ data, count, limit = 50 }: ProfileListProps) {
|
export function ProfileList({ data, count, limit = 50 }: ProfileListProps) {
|
||||||
const { organizationSlug, projectId } = useAppParams();
|
const { organizationSlug, projectId } = useAppParams();
|
||||||
const { cursor, setCursor } = useCursor();
|
const { cursor, setCursor, loading } = useCursor();
|
||||||
return (
|
return (
|
||||||
<Widget>
|
<Widget>
|
||||||
<WidgetHead className="flex items-center justify-between">
|
<WidgetHead className="flex items-center justify-between">
|
||||||
@@ -32,6 +32,7 @@ export function ProfileList({ data, count, limit = 50 }: ProfileListProps) {
|
|||||||
size="sm"
|
size="sm"
|
||||||
cursor={cursor}
|
cursor={cursor}
|
||||||
setCursor={setCursor}
|
setCursor={setCursor}
|
||||||
|
loading={loading}
|
||||||
count={count}
|
count={count}
|
||||||
take={limit}
|
take={limit}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export function Pagination({
|
|||||||
setCursor,
|
setCursor,
|
||||||
className,
|
className,
|
||||||
size = 'base',
|
size = 'base',
|
||||||
|
loading,
|
||||||
}: {
|
}: {
|
||||||
take: number;
|
take: number;
|
||||||
count: number;
|
count: number;
|
||||||
@@ -36,6 +37,7 @@ export function Pagination({
|
|||||||
setCursor: Dispatch<SetStateAction<number>>;
|
setCursor: Dispatch<SetStateAction<number>>;
|
||||||
className?: string;
|
className?: string;
|
||||||
size?: 'sm' | 'base';
|
size?: 'sm' | 'base';
|
||||||
|
loading?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const lastCursor = Math.floor(count / take) - 1;
|
const lastCursor = Math.floor(count / take) - 1;
|
||||||
const isNextDisabled = count === 0 || lastCursor === cursor;
|
const isNextDisabled = count === 0 || lastCursor === cursor;
|
||||||
@@ -56,42 +58,43 @@ export function Pagination({
|
|||||||
)}
|
)}
|
||||||
{size === 'base' && (
|
{size === 'base' && (
|
||||||
<Button
|
<Button
|
||||||
|
loading={loading}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setCursor(0)}
|
onClick={() => setCursor(0)}
|
||||||
disabled={cursor === 0}
|
disabled={cursor === 0}
|
||||||
className="max-sm:hidden"
|
className="max-sm:hidden"
|
||||||
>
|
icon={ChevronsLeftIcon}
|
||||||
<ChevronsLeftIcon size={14} />
|
/>
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
|
loading={loading}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setCursor((p) => Math.max(0, p - 1))}
|
onClick={() => setCursor((p) => Math.max(0, p - 1))}
|
||||||
disabled={cursor === 0}
|
disabled={cursor === 0}
|
||||||
>
|
icon={ChevronLeftIcon}
|
||||||
<ChevronLeftIcon size={14} />
|
/>
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
|
loading={loading}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setCursor((p) => Math.min(lastCursor, p + 1))}
|
onClick={() => setCursor((p) => Math.min(lastCursor, p + 1))}
|
||||||
disabled={isNextDisabled}
|
disabled={isNextDisabled}
|
||||||
>
|
icon={ChevronRightIcon}
|
||||||
<ChevronRightIcon size={14} />
|
/>
|
||||||
</Button>
|
|
||||||
{size === 'base' && (
|
{size === 'base' && (
|
||||||
<Button
|
<Button
|
||||||
|
loading={loading}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setCursor(lastCursor)}
|
onClick={() => setCursor(lastCursor)}
|
||||||
disabled={isNextDisabled}
|
disabled={isNextDisabled}
|
||||||
className="max-sm:hidden"
|
className="max-sm:hidden"
|
||||||
>
|
icon={ChevronsRightIcon}
|
||||||
<ChevronsRightIcon size={14} />
|
/>
|
||||||
</Button>
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -76,9 +76,10 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
{Icon && (
|
{Icon && (
|
||||||
<Icon
|
<Icon
|
||||||
className={cn(
|
className={cn(
|
||||||
'mr-2 h-4 w-4 flex-shrink-0',
|
'h-4 w-4 flex-shrink-0',
|
||||||
responsive && 'mr-0 sm:mr-2',
|
loading && 'animate-spin',
|
||||||
loading && 'animate-spin'
|
size !== 'icon' && responsive && 'mr-0 sm:mr-2',
|
||||||
|
size !== 'icon' && 'mr-2'
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
|
import { useTransition } from 'react';
|
||||||
import { parseAsInteger, useQueryState } from 'nuqs';
|
import { parseAsInteger, useQueryState } from 'nuqs';
|
||||||
|
|
||||||
export function useCursor() {
|
export function useCursor() {
|
||||||
|
const [loading, startTransition] = useTransition();
|
||||||
const [cursor, setCursor] = useQueryState(
|
const [cursor, setCursor] = useQueryState(
|
||||||
'cursor',
|
'cursor',
|
||||||
parseAsInteger
|
parseAsInteger
|
||||||
.withOptions({ shallow: false, history: 'push' })
|
.withOptions({ shallow: false, history: 'push', startTransition })
|
||||||
.withDefault(0)
|
.withDefault(0)
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
cursor,
|
cursor,
|
||||||
setCursor,
|
setCursor,
|
||||||
|
loading,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,16 +15,15 @@ export const ch = createClient({
|
|||||||
export async function chQueryWithMeta<T extends Record<string, any>>(
|
export async function chQueryWithMeta<T extends Record<string, any>>(
|
||||||
query: string
|
query: string
|
||||||
): Promise<ResponseJSON<T>> {
|
): Promise<ResponseJSON<T>> {
|
||||||
console.log('Query:', query);
|
|
||||||
const start = Date.now();
|
const start = Date.now();
|
||||||
const res = await ch.query({
|
const res = await ch.query({
|
||||||
query,
|
query,
|
||||||
});
|
});
|
||||||
const json = await res.json<T>();
|
const json = await res.json<T>();
|
||||||
|
const keys = Object.keys(json.data[0] || {});
|
||||||
const response = {
|
const response = {
|
||||||
...json,
|
...json,
|
||||||
data: json.data.map((item) => {
|
data: json.data.map((item) => {
|
||||||
const keys = Object.keys(item);
|
|
||||||
return keys.reduce((acc, key) => {
|
return keys.reduce((acc, key) => {
|
||||||
const meta = json.meta?.find((m) => m.name === key);
|
const meta = json.meta?.find((m) => m.name === key);
|
||||||
return {
|
return {
|
||||||
@@ -38,8 +37,10 @@ export async function chQueryWithMeta<T extends Record<string, any>>(
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
console.log(`Clickhouse query took ${response.statistics?.elapsed}ms`);
|
console.log(
|
||||||
console.log(`chQuery took ${Date.now() - start}ms`);
|
`Query: (${Date.now() - start}ms, ${response.statistics?.elapsed}ms)`,
|
||||||
|
query
|
||||||
|
);
|
||||||
|
|
||||||
return response;
|
return response;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export async function getProfileById(id: string, projectId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const [profile] = await chQuery<IClickhouseProfile>(
|
const [profile] = await chQuery<IClickhouseProfile>(
|
||||||
`SELECT *, created_at as max_created_at FROM profiles WHERE id = ${escape(id)} AND project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT 1`
|
`SELECT * FROM profiles WHERE id = ${escape(id)} AND project_id = ${escape(projectId)} ORDER BY created_at DESC LIMIT 1`
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!profile) {
|
if (!profile) {
|
||||||
@@ -59,20 +59,6 @@ interface GetProfileListOptions {
|
|||||||
filters?: IChartEventFilter[];
|
filters?: IChartEventFilter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProfileSelectFields() {
|
|
||||||
return [
|
|
||||||
'id',
|
|
||||||
'argMax(first_name, created_at) as first_name',
|
|
||||||
'argMax(last_name, created_at) as last_name',
|
|
||||||
'argMax(email, created_at) as email',
|
|
||||||
'argMax(avatar, created_at) as avatar',
|
|
||||||
'argMax(properties, created_at) as properties',
|
|
||||||
'argMax(project_id, created_at) as project_id',
|
|
||||||
'argMax(is_external, created_at) as is_external',
|
|
||||||
'max(created_at) as max_created_at',
|
|
||||||
].join(', ');
|
|
||||||
}
|
|
||||||
|
|
||||||
interface GetProfilesOptions {
|
interface GetProfilesOptions {
|
||||||
ids: string[];
|
ids: string[];
|
||||||
}
|
}
|
||||||
@@ -82,25 +68,15 @@ export async function getProfiles({ ids }: GetProfilesOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const data = await chQuery<IClickhouseProfile>(
|
const data = await chQuery<IClickhouseProfile>(
|
||||||
`SELECT
|
`SELECT *
|
||||||
${getProfileSelectFields()}
|
FROM profiles FINAL
|
||||||
FROM profiles
|
|
||||||
WHERE id IN (${ids.map((id) => escape(id)).join(',')})
|
WHERE id IN (${ids.map((id) => escape(id)).join(',')})
|
||||||
GROUP BY id
|
|
||||||
`
|
`
|
||||||
);
|
);
|
||||||
|
|
||||||
return data.map(transformProfile);
|
return data.map(transformProfile);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getProfileInnerSelect(projectId: string) {
|
|
||||||
return `(SELECT
|
|
||||||
${getProfileSelectFields()}
|
|
||||||
FROM profiles
|
|
||||||
GROUP BY id
|
|
||||||
HAVING project_id = ${escape(projectId)})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function getProfileList({
|
export async function getProfileList({
|
||||||
take,
|
take,
|
||||||
cursor,
|
cursor,
|
||||||
@@ -108,16 +84,12 @@ export async function getProfileList({
|
|||||||
filters,
|
filters,
|
||||||
}: GetProfileListOptions) {
|
}: GetProfileListOptions) {
|
||||||
const { sb, getSql } = createSqlBuilder();
|
const { sb, getSql } = createSqlBuilder();
|
||||||
sb.from = getProfileInnerSelect(projectId);
|
sb.from = 'profiles FINAL';
|
||||||
if (filters) {
|
sb.select.all = '*';
|
||||||
sb.where = {
|
sb.where.project_id = `project_id = ${escape(projectId)}`;
|
||||||
...sb.where,
|
|
||||||
...getEventFiltersWhereClause(filters),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
sb.limit = take;
|
sb.limit = take;
|
||||||
sb.offset = Math.max(0, (cursor ?? 0) * take);
|
sb.offset = Math.max(0, (cursor ?? 0) * take);
|
||||||
sb.orderBy.created_at = 'max_created_at DESC';
|
sb.orderBy.created_at = 'created_at DESC';
|
||||||
const data = await chQuery<IClickhouseProfile>(getSql());
|
const data = await chQuery<IClickhouseProfile>(getSql());
|
||||||
return data.map(transformProfile);
|
return data.map(transformProfile);
|
||||||
}
|
}
|
||||||
@@ -127,21 +99,17 @@ export async function getProfileListCount({
|
|||||||
filters,
|
filters,
|
||||||
}: Omit<GetProfileListOptions, 'cursor' | 'take'>) {
|
}: Omit<GetProfileListOptions, 'cursor' | 'take'>) {
|
||||||
const { sb, getSql } = createSqlBuilder();
|
const { sb, getSql } = createSqlBuilder();
|
||||||
|
sb.from = 'profiles FINAL';
|
||||||
sb.select.count = 'count(id) as count';
|
sb.select.count = 'count(id) as count';
|
||||||
sb.from = getProfileInnerSelect(projectId);
|
sb.where.project_id = `project_id = ${escape(projectId)}`;
|
||||||
if (filters) {
|
sb.groupBy.project_id = 'project_id';
|
||||||
sb.where = {
|
const data = await chQuery<{ count: number }>(getSql());
|
||||||
...sb.where,
|
return data[0]?.count ?? 0;
|
||||||
...getEventFiltersWhereClause(filters),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
const [data] = await chQuery<{ count: number }>(getSql());
|
|
||||||
return data?.count ?? 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IServiceProfile = Omit<
|
export type IServiceProfile = Omit<
|
||||||
IClickhouseProfile,
|
IClickhouseProfile,
|
||||||
'max_created_at' | 'properties' | 'first_name' | 'last_name' | 'is_external'
|
'created_at' | 'properties' | 'first_name' | 'last_name' | 'is_external'
|
||||||
> & {
|
> & {
|
||||||
firstName: string;
|
firstName: string;
|
||||||
lastName: string;
|
lastName: string;
|
||||||
@@ -168,7 +136,7 @@ export interface IClickhouseProfile {
|
|||||||
properties: Record<string, string | undefined>;
|
properties: Record<string, string | undefined>;
|
||||||
project_id: string;
|
project_id: string;
|
||||||
is_external: boolean;
|
is_external: boolean;
|
||||||
max_created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IServiceUpsertProfile {
|
export interface IServiceUpsertProfile {
|
||||||
@@ -183,7 +151,7 @@ export interface IServiceUpsertProfile {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function transformProfile({
|
export function transformProfile({
|
||||||
max_created_at,
|
created_at,
|
||||||
first_name,
|
first_name,
|
||||||
last_name,
|
last_name,
|
||||||
...profile
|
...profile
|
||||||
@@ -194,7 +162,7 @@ export function transformProfile({
|
|||||||
lastName: last_name,
|
lastName: last_name,
|
||||||
isExternal: profile.is_external,
|
isExternal: profile.is_external,
|
||||||
properties: toObject(profile.properties),
|
properties: toObject(profile.properties),
|
||||||
createdAt: new Date(max_created_at),
|
createdAt: new Date(created_at),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
export interface SqlBuilderObject {
|
export interface SqlBuilderObject {
|
||||||
where: Record<string, string>;
|
where: Record<string, string>;
|
||||||
|
having: Record<string, string>;
|
||||||
select: Record<string, string>;
|
select: Record<string, string>;
|
||||||
groupBy: Record<string, string>;
|
groupBy: Record<string, string>;
|
||||||
orderBy: Record<string, string>;
|
orderBy: Record<string, string>;
|
||||||
@@ -18,12 +19,15 @@ export function createSqlBuilder() {
|
|||||||
select: {},
|
select: {},
|
||||||
groupBy: {},
|
groupBy: {},
|
||||||
orderBy: {},
|
orderBy: {},
|
||||||
|
having: {},
|
||||||
limit: undefined,
|
limit: undefined,
|
||||||
offset: undefined,
|
offset: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const getWhere = () =>
|
const getWhere = () =>
|
||||||
Object.keys(sb.where).length ? 'WHERE ' + join(sb.where, ' AND ') : '';
|
Object.keys(sb.where).length ? 'WHERE ' + join(sb.where, ' AND ') : '';
|
||||||
|
const getHaving = () =>
|
||||||
|
Object.keys(sb.having).length ? 'HAVING ' + join(sb.having, ' AND ') : '';
|
||||||
const getFrom = () => `FROM ${sb.from}`;
|
const getFrom = () => `FROM ${sb.from}`;
|
||||||
const getSelect = () =>
|
const getSelect = () =>
|
||||||
'SELECT ' + (Object.keys(sb.select).length ? join(sb.select, ', ') : '*');
|
'SELECT ' + (Object.keys(sb.select).length ? join(sb.select, ', ') : '*');
|
||||||
@@ -42,22 +46,20 @@ export function createSqlBuilder() {
|
|||||||
getSelect,
|
getSelect,
|
||||||
getGroupBy,
|
getGroupBy,
|
||||||
getOrderBy,
|
getOrderBy,
|
||||||
|
getHaving,
|
||||||
getSql: () => {
|
getSql: () => {
|
||||||
const sql = [
|
const sql = [
|
||||||
getSelect(),
|
getSelect(),
|
||||||
getFrom(),
|
getFrom(),
|
||||||
getWhere(),
|
getWhere(),
|
||||||
getGroupBy(),
|
getGroupBy(),
|
||||||
|
getHaving(),
|
||||||
getOrderBy(),
|
getOrderBy(),
|
||||||
getLimit(),
|
getLimit(),
|
||||||
getOffset(),
|
getOffset(),
|
||||||
]
|
]
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.join(' ');
|
.join(' ');
|
||||||
console.log('---');
|
|
||||||
console.log(sql);
|
|
||||||
console.log('---');
|
|
||||||
|
|
||||||
return sql;
|
return sql;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user