web: added interval minute, profile list and profile view
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
-- AlterEnum
|
||||||
|
ALTER TYPE "Interval" ADD VALUE 'minute';
|
||||||
@@ -109,6 +109,7 @@ enum Interval {
|
|||||||
hour
|
hour
|
||||||
day
|
day
|
||||||
month
|
month
|
||||||
|
minute
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ChartType {
|
enum ChartType {
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ export function usePagination(take = 100) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Pagination(props: ReturnType<typeof usePagination>) {
|
export type PaginationProps = ReturnType<typeof usePagination>
|
||||||
|
|
||||||
|
export function Pagination(props: PaginationProps) {
|
||||||
return (
|
return (
|
||||||
<div className="flex select-none items-center justify-end space-x-2 py-4">
|
<div className="flex select-none items-center justify-end space-x-2 py-4">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
99
apps/web/src/components/events/EventsTable.tsx
Normal file
99
apps/web/src/components/events/EventsTable.tsx
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
import { DataTable } from "@/components/DataTable";
|
||||||
|
import { type PaginationProps, Pagination } from "@/components/Pagination";
|
||||||
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
|
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
|
||||||
|
import { type RouterOutputs } from "@/utils/api";
|
||||||
|
import { formatDateTime } from "@/utils/date";
|
||||||
|
import { toDots } from "@/utils/object";
|
||||||
|
import { AvatarImage } from "@radix-ui/react-avatar";
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useOrganizationParams } from "@/hooks/useOrganizationParams";
|
||||||
|
|
||||||
|
const columnHelper =
|
||||||
|
createColumnHelper<RouterOutputs["event"]["list"][number]>();
|
||||||
|
|
||||||
|
type EventsTableProps = {
|
||||||
|
data: RouterOutputs["event"]["list"];
|
||||||
|
pagination: PaginationProps;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function EventsTable({ data, pagination }: EventsTableProps) {
|
||||||
|
const params = useOrganizationParams()
|
||||||
|
const columns = useMemo(() => {
|
||||||
|
return [
|
||||||
|
columnHelper.accessor((row) => row.createdAt, {
|
||||||
|
id: "createdAt",
|
||||||
|
header: () => "Created At",
|
||||||
|
cell(info) {
|
||||||
|
return formatDateTime(info.getValue());
|
||||||
|
},
|
||||||
|
footer: () => "Created At",
|
||||||
|
}),
|
||||||
|
columnHelper.accessor((row) => row.name, {
|
||||||
|
id: "event",
|
||||||
|
header: () => "Event",
|
||||||
|
cell(info) {
|
||||||
|
return <span className="font-medium">{info.getValue()}</span>;
|
||||||
|
},
|
||||||
|
footer: () => "Created At",
|
||||||
|
}),
|
||||||
|
columnHelper.accessor((row) => row.profile, {
|
||||||
|
id: "profile",
|
||||||
|
header: () => "Profile",
|
||||||
|
cell(info) {
|
||||||
|
const profile = info.getValue();
|
||||||
|
return (
|
||||||
|
<Link href={`/${params.organization}/${params.project}/profiles/${profile?.id}`} className="flex items-center gap-2">
|
||||||
|
<Avatar className="h-6 w-6">
|
||||||
|
{profile?.avatar && <AvatarImage src={profile.avatar} />}
|
||||||
|
<AvatarFallback className="text-xs">
|
||||||
|
{profile?.first_name?.at(0)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
{`${profile?.first_name} ${profile?.last_name ?? ""}`}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
footer: () => "Created At",
|
||||||
|
}),
|
||||||
|
columnHelper.accessor((row) => row.properties, {
|
||||||
|
id: "properties",
|
||||||
|
header: () => "Properties",
|
||||||
|
cell(info) {
|
||||||
|
const dots = toDots(info.getValue() as Record<string, any>);
|
||||||
|
return (
|
||||||
|
<Table className="mini">
|
||||||
|
<TableBody>
|
||||||
|
{Object.keys(dots).map((key) => {
|
||||||
|
return (
|
||||||
|
<TableRow key={key}>
|
||||||
|
<TableCell className="font-medium">{key}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{typeof dots[key] === "boolean"
|
||||||
|
? dots[key]
|
||||||
|
? "true"
|
||||||
|
: "false"
|
||||||
|
: dots[key]}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
footer: () => "Created At",
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Pagination {...pagination} />
|
||||||
|
<DataTable data={data} columns={columns} />
|
||||||
|
<Pagination {...pagination} />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ export function NavbarMenu() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex gap-6 items-center">
|
<div className="flex gap-6 items-center">
|
||||||
<Link href={`/${params.organization}`}>Home</Link>
|
<Link href={`/${params.organization}`}>Home</Link>
|
||||||
|
{params.project && <Link href={`/${params.organization}/${params.project}/events`}>Events</Link>}
|
||||||
|
{params.project && <Link href={`/${params.organization}/${params.project}/profiles`}>Profiles</Link>}
|
||||||
<NavbarCreate />
|
<NavbarCreate />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
|
|||||||
import { changeDateRanges, changeInterval } from "./reportSlice";
|
import { changeDateRanges, changeInterval } from "./reportSlice";
|
||||||
import { Combobox } from "../ui/combobox";
|
import { Combobox } from "../ui/combobox";
|
||||||
import { type IInterval } from "@/types";
|
import { type IInterval } from "@/types";
|
||||||
import { timeRanges } from "@/utils/constants";
|
import { intervals, timeRanges } from "@/utils/constants";
|
||||||
|
|
||||||
export function ReportDateRange() {
|
export function ReportDateRange() {
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
@@ -14,7 +14,7 @@ export function ReportDateRange() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RadioGroup>
|
<RadioGroup>
|
||||||
{timeRanges.map(item => {
|
{timeRanges.map((item) => {
|
||||||
return (
|
return (
|
||||||
<RadioGroupItem
|
<RadioGroupItem
|
||||||
key={item.range}
|
key={item.range}
|
||||||
@@ -25,7 +25,7 @@ export function ReportDateRange() {
|
|||||||
>
|
>
|
||||||
{item.title}
|
{item.title}
|
||||||
</RadioGroupItem>
|
</RadioGroupItem>
|
||||||
)
|
);
|
||||||
})}
|
})}
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
{chartType === "linear" && (
|
{chartType === "linear" && (
|
||||||
@@ -36,20 +36,10 @@ export function ReportDateRange() {
|
|||||||
dispatch(changeInterval(value as IInterval));
|
dispatch(changeInterval(value as IInterval));
|
||||||
}}
|
}}
|
||||||
value={interval}
|
value={interval}
|
||||||
items={[
|
items={Object.entries(intervals).map(([key, value]) => ({
|
||||||
{
|
label: value,
|
||||||
label: "Hour",
|
value: key,
|
||||||
value: "hour",
|
}))}
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Day",
|
|
||||||
value: "day",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Month",
|
|
||||||
value: "month",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { useFormatDateInterval } from "@/hooks/useFormatDateInterval";
|
||||||
import { useMappings } from "@/hooks/useMappings";
|
import { useMappings } from "@/hooks/useMappings";
|
||||||
|
import { useSelector } from "@/redux";
|
||||||
import { type IToolTipProps } from "@/types";
|
import { type IToolTipProps } from "@/types";
|
||||||
import { formatDate } from "@/utils/date";
|
|
||||||
|
|
||||||
type ReportLineChartTooltipProps = IToolTipProps<{
|
type ReportLineChartTooltipProps = IToolTipProps<{
|
||||||
color: string;
|
color: string;
|
||||||
@@ -17,6 +18,8 @@ export function ReportLineChartTooltip({
|
|||||||
payload,
|
payload,
|
||||||
}: ReportLineChartTooltipProps) {
|
}: ReportLineChartTooltipProps) {
|
||||||
const getLabel = useMappings();
|
const getLabel = useMappings();
|
||||||
|
const interval = useSelector((state) => state.report.interval);
|
||||||
|
const formatDate = useFormatDateInterval(interval);
|
||||||
|
|
||||||
if (!active || !payload) {
|
if (!active || !payload) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -117,7 +117,9 @@ export const reportSlice = createSlice({
|
|||||||
|
|
||||||
changeDateRanges: (state, action: PayloadAction<IChartRange>) => {
|
changeDateRanges: (state, action: PayloadAction<IChartRange>) => {
|
||||||
state.range = action.payload
|
state.range = action.payload
|
||||||
if (action.payload === 0 || action.payload === 1) {
|
if (action.payload === 0.3 || action.payload === 0.6) {
|
||||||
|
state.interval = "minute";
|
||||||
|
} else if (action.payload === 0 || action.payload === 1) {
|
||||||
state.interval = "hour";
|
state.interval = "hour";
|
||||||
} else if (action.payload <= 30) {
|
} else if (action.payload <= 30) {
|
||||||
state.interval = "day";
|
state.interval = "day";
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { type IInterval } from "@/types";
|
|||||||
|
|
||||||
|
|
||||||
export function formatDateInterval(interval: IInterval, date: Date): string {
|
export function formatDateInterval(interval: IInterval, date: Date): string {
|
||||||
if (interval === "hour") {
|
if (interval === "hour" || interval === "minute") {
|
||||||
return new Intl.DateTimeFormat("en-GB", {
|
return new Intl.DateTimeFormat("en-GB", {
|
||||||
hour: "2-digit",
|
hour: "2-digit",
|
||||||
minute: "2-digit",
|
minute: "2-digit",
|
||||||
@@ -14,11 +14,11 @@ export function formatDateInterval(interval: IInterval, date: Date): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (interval === "day") {
|
if (interval === "day") {
|
||||||
return new Intl.DateTimeFormat("en-GB", { weekday: "short" }).format(
|
return new Intl.DateTimeFormat("en-GB", { weekday: "short", day: '2-digit', month: '2-digit' }).format(
|
||||||
date,
|
date,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return date.toISOString();
|
return date.toISOString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ export function useOrganizationParams() {
|
|||||||
organization: z.string(),
|
organization: z.string(),
|
||||||
project: z.string(),
|
project: z.string(),
|
||||||
dashboard: z.string(),
|
dashboard: z.string(),
|
||||||
|
profileId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,12 @@
|
|||||||
import { Container } from "@/components/Container";
|
import { Container } from "@/components/Container";
|
||||||
import { DataTable } from "@/components/DataTable";
|
|
||||||
import { PageTitle } from "@/components/PageTitle";
|
import { PageTitle } from "@/components/PageTitle";
|
||||||
import { Pagination, usePagination } from "@/components/Pagination";
|
import { usePagination } from "@/components/Pagination";
|
||||||
|
import { EventsTable } from "@/components/events/EventsTable";
|
||||||
import { MainLayout } from "@/components/layouts/MainLayout";
|
import { MainLayout } from "@/components/layouts/MainLayout";
|
||||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
|
||||||
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
|
|
||||||
import { useOrganizationParams } from "@/hooks/useOrganizationParams";
|
import { useOrganizationParams } from "@/hooks/useOrganizationParams";
|
||||||
import { type RouterOutputs, api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { formatDateTime } from "@/utils/date";
|
|
||||||
import { toDots } from "@/utils/object";
|
|
||||||
import { AvatarImage } from "@radix-ui/react-avatar";
|
|
||||||
import { createColumnHelper } from "@tanstack/react-table";
|
|
||||||
import { useMemo } from "react";
|
|
||||||
|
|
||||||
const columnHelper =
|
import { useMemo } from "react";
|
||||||
createColumnHelper<RouterOutputs["event"]["list"][number]>();
|
|
||||||
|
|
||||||
export default function Events() {
|
export default function Events() {
|
||||||
const pagination = usePagination();
|
const pagination = usePagination();
|
||||||
@@ -29,80 +21,12 @@ export default function Events() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
const events = useMemo(() => eventsQuery.data ?? [], [eventsQuery]);
|
const events = useMemo(() => eventsQuery.data ?? [], [eventsQuery]);
|
||||||
const columns = useMemo(() => {
|
|
||||||
return [
|
|
||||||
columnHelper.accessor((row) => row.createdAt, {
|
|
||||||
id: "createdAt",
|
|
||||||
header: () => "Created At",
|
|
||||||
cell(info) {
|
|
||||||
return formatDateTime(info.getValue());
|
|
||||||
},
|
|
||||||
footer: () => "Created At",
|
|
||||||
}),
|
|
||||||
columnHelper.accessor((row) => row.name, {
|
|
||||||
id: "event",
|
|
||||||
header: () => "Event",
|
|
||||||
cell(info) {
|
|
||||||
return <span className="font-medium">{info.getValue()}</span>;
|
|
||||||
},
|
|
||||||
footer: () => "Created At",
|
|
||||||
}),
|
|
||||||
columnHelper.accessor((row) => row.profile, {
|
|
||||||
id: "profile",
|
|
||||||
header: () => "Profile",
|
|
||||||
cell(info) {
|
|
||||||
const profile = info.getValue();
|
|
||||||
return (
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<Avatar className="h-6 w-6">
|
|
||||||
{profile?.avatar && <AvatarImage src={profile.avatar} />}
|
|
||||||
<AvatarFallback className="text-xs">
|
|
||||||
{profile?.first_name?.at(0)}
|
|
||||||
</AvatarFallback>
|
|
||||||
</Avatar>
|
|
||||||
{`${profile?.first_name} ${profile?.last_name ?? ""}`}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
footer: () => "Created At",
|
|
||||||
}),
|
|
||||||
columnHelper.accessor((row) => row.properties, {
|
|
||||||
id: "properties",
|
|
||||||
header: () => "Properties",
|
|
||||||
cell(info) {
|
|
||||||
const dots = toDots(info.getValue() as Record<string, any>);
|
|
||||||
return (
|
|
||||||
<Table className="mini">
|
|
||||||
<TableBody>
|
|
||||||
{Object.keys(dots).map((key) => {
|
|
||||||
return (
|
|
||||||
<TableRow key={key}>
|
|
||||||
<TableCell className="font-medium">{key}</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
{typeof dots[key] === "boolean"
|
|
||||||
? dots[key]
|
|
||||||
? "true"
|
|
||||||
: "false"
|
|
||||||
: dots[key]}
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</TableBody>
|
|
||||||
</Table>
|
|
||||||
);
|
|
||||||
},
|
|
||||||
footer: () => "Created At",
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
}, []);
|
|
||||||
return (
|
return (
|
||||||
<MainLayout>
|
<MainLayout>
|
||||||
<Container>
|
<Container>
|
||||||
<PageTitle>Events</PageTitle>
|
<PageTitle>Events</PageTitle>
|
||||||
<Pagination {...pagination} />
|
<EventsTable data={events} pagination={pagination} />
|
||||||
<DataTable data={events} columns={columns}></DataTable>
|
|
||||||
<Pagination {...pagination} />
|
|
||||||
</Container>
|
</Container>
|
||||||
</MainLayout>
|
</MainLayout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { Container } from "@/components/Container";
|
||||||
|
import { PageTitle } from "@/components/PageTitle";
|
||||||
|
import { usePagination } from "@/components/Pagination";
|
||||||
|
import { EventsTable } from "@/components/events/EventsTable";
|
||||||
|
import { MainLayout } from "@/components/layouts/MainLayout";
|
||||||
|
import { useOrganizationParams } from "@/hooks/useOrganizationParams";
|
||||||
|
import { useQueryParams } from "@/hooks/useQueryParams";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export default function ProfileId() {
|
||||||
|
const pagination = usePagination();
|
||||||
|
const params = useOrganizationParams();
|
||||||
|
const { profileId } = useQueryParams(
|
||||||
|
z.object({
|
||||||
|
profileId: z.string(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const eventsQuery = api.event.list.useQuery(
|
||||||
|
{
|
||||||
|
projectSlug: params.project,
|
||||||
|
profileId,
|
||||||
|
...pagination,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const events = useMemo(() => eventsQuery.data ?? [], [eventsQuery]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<Container>
|
||||||
|
<PageTitle>Profile</PageTitle>
|
||||||
|
<EventsTable data={events} pagination={pagination} />
|
||||||
|
</Container>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
100
apps/web/src/pages/[organization]/[project]/profiles/index.tsx
Normal file
100
apps/web/src/pages/[organization]/[project]/profiles/index.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import { Container } from "@/components/Container";
|
||||||
|
import { DataTable } from "@/components/DataTable";
|
||||||
|
import { PageTitle } from "@/components/PageTitle";
|
||||||
|
import { Pagination, usePagination } from "@/components/Pagination";
|
||||||
|
import { MainLayout } from "@/components/layouts/MainLayout";
|
||||||
|
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
|
||||||
|
import { Table, TableBody, TableCell, TableRow } from "@/components/ui/table";
|
||||||
|
import { useOrganizationParams } from "@/hooks/useOrganizationParams";
|
||||||
|
import { type RouterOutputs, api } from "@/utils/api";
|
||||||
|
import { formatDateTime } from "@/utils/date";
|
||||||
|
import { toDots } from "@/utils/object";
|
||||||
|
import { AvatarImage } from "@radix-ui/react-avatar";
|
||||||
|
import { createColumnHelper } from "@tanstack/react-table";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
const columnHelper =
|
||||||
|
createColumnHelper<RouterOutputs["profile"]["list"][number]>();
|
||||||
|
|
||||||
|
export default function Events() {
|
||||||
|
const pagination = usePagination();
|
||||||
|
const params = useOrganizationParams();
|
||||||
|
const eventsQuery = api.profile.list.useQuery(
|
||||||
|
{
|
||||||
|
projectSlug: params.project,
|
||||||
|
...pagination,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
keepPreviousData: true,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
const profiles = useMemo(() => eventsQuery.data ?? [], [eventsQuery]);
|
||||||
|
const columns = useMemo(() => {
|
||||||
|
return [
|
||||||
|
columnHelper.accessor((row) => row.createdAt, {
|
||||||
|
id: "createdAt",
|
||||||
|
header: () => "Created At",
|
||||||
|
cell(info) {
|
||||||
|
return formatDateTime(info.getValue());
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor('first_name', {
|
||||||
|
id: "name",
|
||||||
|
header: () => "Name",
|
||||||
|
cell(info) {
|
||||||
|
const profile = info.row.original;
|
||||||
|
return (
|
||||||
|
<Link href={`/${params.organization}/${params.project}/profiles/${profile?.id}`} className="flex items-center gap-2">
|
||||||
|
<Avatar className="h-6 w-6">
|
||||||
|
{profile?.avatar && <AvatarImage src={profile.avatar} />}
|
||||||
|
<AvatarFallback className="text-xs">
|
||||||
|
{profile?.first_name?.at(0)}
|
||||||
|
</AvatarFallback>
|
||||||
|
</Avatar>
|
||||||
|
{`${profile?.first_name} ${profile?.last_name ?? ""}`}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
columnHelper.accessor((row) => row.properties, {
|
||||||
|
id: "properties",
|
||||||
|
header: () => "Properties",
|
||||||
|
cell(info) {
|
||||||
|
const dots = toDots(info.getValue() as Record<string, any>);
|
||||||
|
if(Object.keys(dots).length === 0) return 'No properties';
|
||||||
|
return (
|
||||||
|
<Table className="mini">
|
||||||
|
<TableBody>
|
||||||
|
{Object.keys(dots).map((key) => {
|
||||||
|
return (
|
||||||
|
<TableRow key={key}>
|
||||||
|
<TableCell className="font-medium">{key}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
{typeof dots[key] === "boolean"
|
||||||
|
? dots[key]
|
||||||
|
? "true"
|
||||||
|
: "false"
|
||||||
|
: dots[key]}
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<MainLayout>
|
||||||
|
<Container>
|
||||||
|
<PageTitle>Profiles</PageTitle>
|
||||||
|
<Pagination {...pagination} />
|
||||||
|
<DataTable data={profiles} columns={columns}></DataTable>
|
||||||
|
<Pagination {...pagination} />
|
||||||
|
</Container>
|
||||||
|
</MainLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { projectRouter } from "./routers/project";
|
|||||||
import { clientRouter } from "./routers/client";
|
import { clientRouter } from "./routers/client";
|
||||||
import { dashboardRouter } from "./routers/dashboard";
|
import { dashboardRouter } from "./routers/dashboard";
|
||||||
import { eventRouter } from "./routers/event";
|
import { eventRouter } from "./routers/event";
|
||||||
|
import { profileRouter } from "./routers/profile";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the primary router for your server.
|
* This is the primary router for your server.
|
||||||
@@ -22,6 +23,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
project: projectRouter,
|
project: projectRouter,
|
||||||
client: clientRouter,
|
client: clientRouter,
|
||||||
event: eventRouter,
|
event: eventRouter,
|
||||||
|
profile: profileRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -190,6 +190,10 @@ function getTotalCount(arr: ResultItem[]) {
|
|||||||
return arr.reduce((acc, item) => acc + item.count, 0);
|
return arr.reduce((acc, item) => acc + item.count, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isFloat(n: number) {
|
||||||
|
return n % 1 !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
function getDatesFromRange(range: IChartRange) {
|
function getDatesFromRange(range: IChartRange) {
|
||||||
if (range === 0) {
|
if (range === 0) {
|
||||||
const startDate = new Date();
|
const startDate = new Date();
|
||||||
@@ -202,6 +206,16 @@ function getDatesFromRange(range: IChartRange) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isFloat(range)) {
|
||||||
|
const startDate = new Date(Date.now() - 1000 * 60 * (range * 100));
|
||||||
|
const endDate = new Date().toISOString();
|
||||||
|
|
||||||
|
return {
|
||||||
|
startDate: startDate.toISOString(),
|
||||||
|
endDate: endDate,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
const startDate = getDaysOldDate(range).toISOString();
|
const startDate = getDaysOldDate(range).toISOString();
|
||||||
const endDate = new Date().toISOString();
|
const endDate = new Date().toISOString();
|
||||||
return {
|
return {
|
||||||
@@ -210,25 +224,7 @@ function getDatesFromRange(range: IChartRange) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getChartData({
|
function getChartSql({ event, chartType, breakdowns, interval, startDate, endDate }: Omit<IGetChartDataInput, 'range'>) {
|
||||||
chartType,
|
|
||||||
event,
|
|
||||||
breakdowns,
|
|
||||||
interval,
|
|
||||||
range,
|
|
||||||
startDate: _startDate,
|
|
||||||
endDate: _endDate,
|
|
||||||
}: {
|
|
||||||
event: IChartEvent;
|
|
||||||
} & Omit<IChartInputWithDates, "events">) {
|
|
||||||
const { startDate, endDate } =
|
|
||||||
_startDate && _endDate
|
|
||||||
? {
|
|
||||||
startDate: _startDate,
|
|
||||||
endDate: _endDate,
|
|
||||||
}
|
|
||||||
: getDatesFromRange(range);
|
|
||||||
|
|
||||||
const select = [];
|
const select = [];
|
||||||
const where = [];
|
const where = [];
|
||||||
const groupBy = [];
|
const groupBy = [];
|
||||||
@@ -338,7 +334,54 @@ async function getChartData({
|
|||||||
sql.push(`ORDER BY ${orderBy.join(", ")}`);
|
sql.push(`ORDER BY ${orderBy.join(", ")}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await db.$queryRawUnsafe<ResultItem[]>(sql.join("\n"));
|
return sql.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
type IGetChartDataInput = {
|
||||||
|
event: IChartEvent;
|
||||||
|
} & Omit<IChartInputWithDates, "events" | 'name'>
|
||||||
|
|
||||||
|
async function getChartData({
|
||||||
|
chartType,
|
||||||
|
event,
|
||||||
|
breakdowns,
|
||||||
|
interval,
|
||||||
|
range,
|
||||||
|
startDate: _startDate,
|
||||||
|
endDate: _endDate,
|
||||||
|
}: IGetChartDataInput) {
|
||||||
|
const { startDate, endDate } =
|
||||||
|
_startDate && _endDate
|
||||||
|
? {
|
||||||
|
startDate: _startDate,
|
||||||
|
endDate: _endDate,
|
||||||
|
}
|
||||||
|
: getDatesFromRange(range);
|
||||||
|
|
||||||
|
const sql = getChartSql({
|
||||||
|
chartType,
|
||||||
|
event,
|
||||||
|
breakdowns,
|
||||||
|
interval,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
})
|
||||||
|
|
||||||
|
let result = await db.$queryRawUnsafe<ResultItem[]>(sql);
|
||||||
|
|
||||||
|
if(result.length === 0 && breakdowns.length > 0) {
|
||||||
|
result = await db.$queryRawUnsafe<ResultItem[]>(getChartSql({
|
||||||
|
chartType,
|
||||||
|
event,
|
||||||
|
breakdowns: [],
|
||||||
|
interval,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(sql);
|
||||||
|
|
||||||
|
|
||||||
// group by sql label
|
// group by sql label
|
||||||
const series = result.reduce(
|
const series = result.reduce(
|
||||||
@@ -399,7 +442,10 @@ function fillEmptySpotsInTimeline(
|
|||||||
const clonedEndDate = new Date(endDate);
|
const clonedEndDate = new Date(endDate);
|
||||||
const today = new Date();
|
const today = new Date();
|
||||||
|
|
||||||
if (interval === "hour") {
|
if(interval === 'minute') {
|
||||||
|
clonedStartDate.setSeconds(0, 0)
|
||||||
|
clonedEndDate.setMinutes(clonedEndDate.getMinutes() + 1, 0, 0);
|
||||||
|
} else if (interval === "hour") {
|
||||||
clonedStartDate.setMinutes(0, 0, 0);
|
clonedStartDate.setMinutes(0, 0, 0);
|
||||||
clonedEndDate.setMinutes(0, 0, 0);
|
clonedEndDate.setMinutes(0, 0, 0);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ export const eventRouter = createTRPCRouter({
|
|||||||
list: protectedProcedure
|
list: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
cursor: z.string().optional(),
|
|
||||||
projectSlug: z.string(),
|
projectSlug: z.string(),
|
||||||
take: z.number().default(100),
|
take: z.number().default(100),
|
||||||
skip: z.number().default(0),
|
skip: z.number().default(0),
|
||||||
|
profileId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input: { take, skip, projectSlug } }) => {
|
.query(async ({ input: { take, skip, projectSlug, profileId } }) => {
|
||||||
const project = await db.project.findUniqueOrThrow({
|
const project = await db.project.findUniqueOrThrow({
|
||||||
where: {
|
where: {
|
||||||
slug: projectSlug,
|
slug: projectSlug,
|
||||||
@@ -29,6 +29,7 @@ export const eventRouter = createTRPCRouter({
|
|||||||
skip,
|
skip,
|
||||||
where: {
|
where: {
|
||||||
project_id: project.id,
|
project_id: project.id,
|
||||||
|
profile_id: profileId
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
createdAt: "desc",
|
createdAt: "desc",
|
||||||
|
|||||||
37
apps/web/src/server/api/routers/profile.ts
Normal file
37
apps/web/src/server/api/routers/profile.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
responseLimit: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const profileRouter = createTRPCRouter({
|
||||||
|
list: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
projectSlug: z.string(),
|
||||||
|
take: z.number().default(100),
|
||||||
|
skip: z.number().default(0),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.query(async ({ input: { take, skip, projectSlug } }) => {
|
||||||
|
const project = await db.project.findUniqueOrThrow({
|
||||||
|
where: {
|
||||||
|
slug: projectSlug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return db.profile.findMany({
|
||||||
|
take,
|
||||||
|
skip,
|
||||||
|
where: {
|
||||||
|
project_id: project.id,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
createdAt: "desc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -14,6 +14,7 @@ export const chartTypes = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const intervals = {
|
export const intervals = {
|
||||||
|
minute: "Minute",
|
||||||
day: "Day",
|
day: "Day",
|
||||||
hour: "Hour",
|
hour: "Hour",
|
||||||
month: "Month",
|
month: "Month",
|
||||||
@@ -33,12 +34,14 @@ export const alphabetIds = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export const timeRanges = [
|
export const timeRanges = [
|
||||||
|
{ range: 0.3, title: "30m" },
|
||||||
|
{ range: 0.6, title: "1h" },
|
||||||
{ range: 0, title: "Today" },
|
{ range: 0, title: "Today" },
|
||||||
{ range: 1, title: "24 hours" },
|
{ range: 1, title: "24h" },
|
||||||
{ range: 7, title: "7 days" },
|
{ range: 7, title: "7d" },
|
||||||
{ range: 14, title: "14 days" },
|
{ range: 14, title: "14d" },
|
||||||
{ range: 30, title: "30 days" },
|
{ range: 30, title: "30d" },
|
||||||
{ range: 90, title: "3 months" },
|
{ range: 90, title: "3mo" },
|
||||||
{ range: 180, title: "6 months" },
|
{ range: 180, title: "6mo" },
|
||||||
{ range: 365, title: "1 year" },
|
{ range: 365, title: "1y" },
|
||||||
] as const
|
] as const;
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ export const zChartInput = z.object({
|
|||||||
breakdowns: zChartBreakdowns,
|
breakdowns: zChartBreakdowns,
|
||||||
range: z
|
range: z
|
||||||
.literal(0)
|
.literal(0)
|
||||||
|
.or(z.literal(0.3))
|
||||||
|
.or(z.literal(0.6))
|
||||||
.or(z.literal(1))
|
.or(z.literal(1))
|
||||||
.or(z.literal(7))
|
.or(z.literal(7))
|
||||||
.or(z.literal(14))
|
.or(z.literal(14))
|
||||||
|
|||||||
Reference in New Issue
Block a user