add domain filter on dashboard (#161)
This commit is contained in:
@@ -7,8 +7,6 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
ResponsiveContainer,
|
ResponsiveContainer,
|
||||||
CartesianGrid,
|
CartesianGrid,
|
||||||
AreaChart,
|
|
||||||
Area,
|
|
||||||
} from "recharts";
|
} from "recharts";
|
||||||
import { EmailStatusIcon } from "../emails/email-status-badge";
|
import { EmailStatusIcon } from "../emails/email-status-badge";
|
||||||
import { EmailStatus } from "@prisma/client";
|
import { EmailStatus } from "@prisma/client";
|
||||||
@@ -17,12 +15,29 @@ import Spinner from "@unsend/ui/src/spinner";
|
|||||||
import { Tabs, TabsList, TabsTrigger } from "@unsend/ui/src/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "@unsend/ui/src/tabs";
|
||||||
import { useUrlState } from "~/hooks/useUrlState";
|
import { useUrlState } from "~/hooks/useUrlState";
|
||||||
import { useTheme } from "@unsend/ui";
|
import { useTheme } from "@unsend/ui";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
} from "@unsend/ui/src/select";
|
||||||
|
|
||||||
export default function DashboardChart() {
|
export default function DashboardChart() {
|
||||||
const [days, setDays] = useUrlState("days", "7");
|
const [days, setDays] = useUrlState("days", "7");
|
||||||
const statusQuery = api.email.dashboard.useQuery({ days: Number(days) });
|
const [domain, setDomain] = useUrlState("domain");
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
|
const domainId = domain ? Number(domain) : undefined;
|
||||||
|
const statusQuery = api.email.dashboard.useQuery({
|
||||||
|
days: Number(days),
|
||||||
|
domain: domainId,
|
||||||
|
});
|
||||||
|
const { data: domainsQuery } = api.domain.domains.useQuery();
|
||||||
|
|
||||||
|
const handleDomain = (val: string) => {
|
||||||
|
setDomain(val === "All Domain" ? null : val);
|
||||||
|
};
|
||||||
|
|
||||||
const lightColors = {
|
const lightColors = {
|
||||||
delivered: "#40a02bcc",
|
delivered: "#40a02bcc",
|
||||||
bounced: "#d20f39cc",
|
bounced: "#d20f39cc",
|
||||||
@@ -45,17 +60,36 @@ export default function DashboardChart() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<h1 className="font-semibold text-xl">Dashboard</h1>
|
<h1 className="font-semibold text-xl">Dashboard</h1>
|
||||||
<Tabs
|
<div className="flex gap-3">
|
||||||
value={days || "7"}
|
<Select
|
||||||
onValueChange={(value) => setDays(value)}
|
value={domain ?? "All Domain"}
|
||||||
className=""
|
onValueChange={(val) => handleDomain(val)}
|
||||||
>
|
>
|
||||||
|
<SelectTrigger className="w-[180px]">
|
||||||
|
{domain
|
||||||
|
? domainsQuery?.find((d) => d.id === Number(domain))?.name
|
||||||
|
: "All Domain"}
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="All Domain" className="capitalize">
|
||||||
|
All Domain
|
||||||
|
</SelectItem>
|
||||||
|
{domainsQuery &&
|
||||||
|
domainsQuery.map((domain) => (
|
||||||
|
<SelectItem key={domain.id} value={domain.id.toString()}>
|
||||||
|
{domain.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<Tabs value={days || "7"} onValueChange={(value) => setDays(value)}>
|
||||||
<TabsList>
|
<TabsList>
|
||||||
<TabsTrigger value="7">7 Days</TabsTrigger>
|
<TabsTrigger value="7">7 Days</TabsTrigger>
|
||||||
<TabsTrigger value="30">30 Days</TabsTrigger>
|
<TabsTrigger value="30">30 Days</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-16 mt-10">
|
<div className="flex flex-col gap-16 mt-10">
|
||||||
<div className="flex flex-wrap gap-2">
|
<div className="flex flex-wrap gap-2">
|
||||||
@@ -159,14 +193,14 @@ export default function DashboardChart() {
|
|||||||
if (!data || data.sent === 0) return null;
|
if (!data || data.sent === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className=" bg-background border shadow-lg p-2 rounded flex flex-col gap-2 px-4">
|
<div className=" bg-background border shadow-lg p-2 rounded-xl flex flex-col gap-2 px-4">
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
{data.date}
|
{data.date}
|
||||||
</p>
|
</p>
|
||||||
{data.delivered ? (
|
{data.delivered ? (
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="w-2.5 h-2.5 bg-[#40a02bcc] dark:bg-[#a6e3a1] rounded-[2px]"></div>
|
<div className="w-2.5 h-2.5 bg-[#40a02bcc] dark:bg-[#a6e3a1] rounded-[2px]"></div>
|
||||||
<p className="text-xs text-muted-foreground w-[60px]">
|
<p className="text-xs text-muted-foreground w-[70px]">
|
||||||
Delivered
|
Delivered
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs font-mono">
|
<p className="text-xs font-mono">
|
||||||
@@ -177,7 +211,7 @@ export default function DashboardChart() {
|
|||||||
{data.bounced ? (
|
{data.bounced ? (
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="w-2.5 h-2.5 bg-[#d20f39cc] dark:bg-[#f38ba8] rounded-[2px]"></div>
|
<div className="w-2.5 h-2.5 bg-[#d20f39cc] dark:bg-[#f38ba8] rounded-[2px]"></div>
|
||||||
<p className="text-xs text-muted-foreground w-[60px]">
|
<p className="text-xs text-muted-foreground w-[70px]">
|
||||||
Bounced
|
Bounced
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs font-mono">{data.bounced}</p>
|
<p className="text-xs font-mono">{data.bounced}</p>
|
||||||
@@ -186,7 +220,7 @@ export default function DashboardChart() {
|
|||||||
{data.complained ? (
|
{data.complained ? (
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="w-2.5 h-2.5 bg-[#df8e1dcc] dark:bg-[#F9E2AF] rounded-[2px]"></div>
|
<div className="w-2.5 h-2.5 bg-[#df8e1dcc] dark:bg-[#F9E2AF] rounded-[2px]"></div>
|
||||||
<p className="text-xs text-muted-foreground w-[60px]">
|
<p className="text-xs text-muted-foreground w-[70px]">
|
||||||
Complained
|
Complained
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs font-mono">
|
<p className="text-xs font-mono">
|
||||||
@@ -197,7 +231,7 @@ export default function DashboardChart() {
|
|||||||
{data.opened ? (
|
{data.opened ? (
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="w-2.5 h-2.5 bg-[#8839efcc] dark:bg-[#cba6f7] rounded-[2px]"></div>
|
<div className="w-2.5 h-2.5 bg-[#8839efcc] dark:bg-[#cba6f7] rounded-[2px]"></div>
|
||||||
<p className="text-xs text-muted-foreground w-[60px]">
|
<p className="text-xs text-muted-foreground w-[70px]">
|
||||||
Opened
|
Opened
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs font-mono">{data.opened}</p>
|
<p className="text-xs font-mono">{data.opened}</p>
|
||||||
@@ -206,7 +240,7 @@ export default function DashboardChart() {
|
|||||||
{data.clicked ? (
|
{data.clicked ? (
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<div className="w-2.5 h-2.5 bg-[#04a5e5cc] dark:bg-[#93c5fd] rounded-[2px]"></div>
|
<div className="w-2.5 h-2.5 bg-[#04a5e5cc] dark:bg-[#93c5fd] rounded-[2px]"></div>
|
||||||
<p className="text-xs text-muted-foreground w-[60px]">
|
<p className="text-xs text-muted-foreground w-[70px]">
|
||||||
Clicked
|
Clicked
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs font-mono">{data.clicked}</p>
|
<p className="text-xs font-mono">{data.clicked}</p>
|
||||||
|
@@ -22,7 +22,7 @@ export const emailRouter = createTRPCRouter({
|
|||||||
domain: z.number().optional(),
|
domain: z.number().optional(),
|
||||||
search: z.string().optional().nullable(),
|
search: z.string().optional().nullable(),
|
||||||
apiId: z.number().optional(),
|
apiId: z.number().optional(),
|
||||||
}),
|
})
|
||||||
)
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const page = input.page || 1;
|
const page = input.page || 1;
|
||||||
@@ -65,7 +65,8 @@ export const emailRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
days: z.number().optional(),
|
days: z.number().optional(),
|
||||||
}),
|
domain: z.number().optional(),
|
||||||
|
})
|
||||||
)
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const { team } = ctx;
|
const { team } = ctx;
|
||||||
@@ -97,6 +98,7 @@ export const emailRouter = createTRPCRouter({
|
|||||||
FROM "DailyEmailUsage"
|
FROM "DailyEmailUsage"
|
||||||
WHERE "teamId" = ${team.id}
|
WHERE "teamId" = ${team.id}
|
||||||
AND "date" >= ${isoStartDate}
|
AND "date" >= ${isoStartDate}
|
||||||
|
${input.domain ? Prisma.sql`AND "domainId" = ${input.domain}` : Prisma.sql``}
|
||||||
GROUP BY "date"
|
GROUP BY "date"
|
||||||
ORDER BY "date" ASC
|
ORDER BY "date" ASC
|
||||||
`;
|
`;
|
||||||
@@ -146,7 +148,7 @@ export const emailRouter = createTRPCRouter({
|
|||||||
clicked: 0,
|
clicked: 0,
|
||||||
bounced: 0,
|
bounced: 0,
|
||||||
complained: 0,
|
complained: 0,
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
return { result: filledResult, totalCounts };
|
return { result: filledResult, totalCounts };
|
||||||
|
@@ -65,7 +65,7 @@ const DropdownMenuContent = React.forwardRef<
|
|||||||
ref={ref}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
sideOffset={sideOffset}
|
||||||
className={cn(
|
className={cn(
|
||||||
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
"z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] overflow-y-auto overflow-x-hidden rounded-xl border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 origin-[--radix-dropdown-menu-content-transform-origin]",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -83,7 +83,7 @@ const DropdownMenuItem = React.forwardRef<
|
|||||||
<DropdownMenuPrimitive.Item
|
<DropdownMenuPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex cursor-default select-none items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
"relative flex cursor-default select-none items-center gap-2 rounded-lg px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:size-4 [&_svg]:shrink-0",
|
||||||
inset && "pl-8",
|
inset && "pl-8",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
@@ -75,7 +75,7 @@ const SelectContent = React.forwardRef<
|
|||||||
<SelectPrimitive.Content
|
<SelectPrimitive.Content
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-lg border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-xl border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
position === "popper" &&
|
position === "popper" &&
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
className
|
className
|
||||||
@@ -118,7 +118,7 @@ const SelectItem = React.forwardRef<
|
|||||||
<SelectPrimitive.Item
|
<SelectPrimitive.Item
|
||||||
ref={ref}
|
ref={ref}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
"relative flex w-full cursor-default select-none items-center rounded-lg py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
|
Reference in New Issue
Block a user