save, create and view reports in dashboard

This commit is contained in:
Carl-Gerhard Lindesvärd
2023-10-19 11:56:52 +02:00
parent 2cb6bbfdd3
commit 4576453aef
28 changed files with 686 additions and 403 deletions

View File

@@ -0,0 +1,8 @@
import { type HtmlProps } from "@/types";
import { cn } from "@/utils/cn";
export function Container({className,...props}: HtmlProps<HTMLDivElement>) {
return (
<div className={cn("mx-auto w-full max-w-4xl", className)} {...props} />
);
}

View File

@@ -0,0 +1,10 @@
import Link from "next/link";
export function Navbar() {
return (
<div className="flex gap-4 text-sm uppercase">
<Link href="/">Dashboards</Link>
<Link href="/reports">Reports</Link>
</div>
);
}

View File

@@ -1,124 +0,0 @@
import {
Cloud,
CreditCard,
Github,
Keyboard,
LifeBuoy,
LogOut,
Mail,
MessageSquare,
Plus,
PlusCircle,
Settings,
User,
UserPlus,
Users,
} from "lucide-react"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
import { useState } from "react"
export function ReportFilterPicker() {
const [open, setOpen] = useState(false)
return (
<DropdownMenu open={open} onOpenChange={setOpen}>
<DropdownMenuTrigger asChild>
<Button variant="outline">Open</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<User className="mr-2 h-4 w-4" />
<span>Profile</span>
<DropdownMenuShortcut>P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<CreditCard className="mr-2 h-4 w-4" />
<span>Billing</span>
<DropdownMenuShortcut>B</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<Settings className="mr-2 h-4 w-4" />
<span>Settings</span>
<DropdownMenuShortcut>S</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<Keyboard className="mr-2 h-4 w-4" />
<span>Keyboard shortcuts</span>
<DropdownMenuShortcut>K</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<Users className="mr-2 h-4 w-4" />
<span>Team</span>
</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>
<UserPlus className="mr-2 h-4 w-4" />
<span>Invite users</span>
</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem>
<Mail className="mr-2 h-4 w-4" />
<span>Email</span>
</DropdownMenuItem>
<DropdownMenuItem>
<MessageSquare className="mr-2 h-4 w-4" />
<span>Message</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<PlusCircle className="mr-2 h-4 w-4" />
<span>More...</span>
</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuItem>
<Plus className="mr-2 h-4 w-4" />
<span>New Team</span>
<DropdownMenuShortcut>+T</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Github className="mr-2 h-4 w-4" />
<span>GitHub</span>
</DropdownMenuItem>
<DropdownMenuItem>
<LifeBuoy className="mr-2 h-4 w-4" />
<span>Support</span>
</DropdownMenuItem>
<DropdownMenuItem disabled>
<Cloud className="mr-2 h-4 w-4" />
<span>API</span>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
<LogOut className="mr-2 h-4 w-4" />
<span>Log out</span>
<DropdownMenuShortcut>Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@@ -0,0 +1,34 @@
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";
import { User } from "lucide-react";
export function UserDropdown() {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button>
<Avatar className="text-black">
<AvatarFallback>CL</AvatarFallback>
</Avatar>
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-[200px]">
<DropdownMenuGroup>
<DropdownMenuItem>
<User className="mr-2 h-4 w-4" />
Profile
</DropdownMenuItem>
<DropdownMenuItem>
<User className="mr-2 h-4 w-4" />
Organization
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem className="text-red-600">
<User className="mr-2 h-4 w-4" />
Logout
</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@@ -0,0 +1,35 @@
import Head from "next/head";
import { UserDropdown } from "../UserDropdown";
import { Navbar } from "../Navbar";
type MainLayoutProps = {
children: React.ReactNode;
className?: string;
}
export function MainLayout({ children, className }: MainLayoutProps) {
return (
<>
<Head>
<title>Create T3 App</title>
<meta name="description" content="Generated by create-t3-app" />
<link rel="icon" href="/favicon.ico" />
</Head>
<nav className="flex h-20 items-center justify-between border-b border-border bg-black px-8 text-white">
<div className="flex flex-col [&_*]:leading-tight">
<span className="text-2xl font-extrabold">MIXAN</span>
<span className="text-xs text-muted">v0.0.1</span>
</div>
<Navbar />
<div>
<UserDropdown />
</div>
</nav>
<main className={className}>
{children}
</main>
</>
);
}

View File

@@ -0,0 +1,89 @@
import { useDispatch, useSelector } from "@/redux";
import { RadioGroup, RadioGroupItem } from "../ui/radio-group";
import { changeDateRanges, changeInterval } from "./reportSlice";
import { Combobox } from "../ui/combobox";
import { type IInterval } from "@/types";
export function ReportDateRange() {
const dispatch = useDispatch();
const interval = useSelector((state) => state.report.interval);
return (
<>
<RadioGroup>
<RadioGroupItem
onClick={() => {
dispatch(changeDateRanges(1));
}}
>
Today
</RadioGroupItem>
<RadioGroupItem
onClick={() => {
dispatch(changeDateRanges(7));
}}
>
7 days
</RadioGroupItem>
<RadioGroupItem
onClick={() => {
dispatch(changeDateRanges(14));
}}
>
14 days
</RadioGroupItem>
<RadioGroupItem
onClick={() => {
dispatch(changeDateRanges(30));
}}
>
1 month
</RadioGroupItem>
<RadioGroupItem
onClick={() => {
dispatch(changeDateRanges(90));
}}
>
3 month
</RadioGroupItem>
<RadioGroupItem
onClick={() => {
dispatch(changeDateRanges(180));
}}
>
6 month
</RadioGroupItem>
<RadioGroupItem
onClick={() => {
dispatch(changeDateRanges(356));
}}
>
1 year
</RadioGroupItem>
</RadioGroup>
<div className="w-full max-w-[200px]">
<Combobox
placeholder="Interval"
onChange={(value) => {
dispatch(changeInterval(value as IInterval));
}}
value={interval}
items={[
{
label: "Hour",
value: "hour",
},
{
label: "Day",
value: "day",
},
{
label: "Month",
value: "month",
},
]}
></Combobox>
</div>
</>
);
}

View File

@@ -1,7 +1,6 @@
import { api } from "@/utils/api";
import {
CartesianGrid,
Legend,
Line,
LineChart,
Tooltip,
@@ -10,22 +9,13 @@ import {
} from "recharts";
import { ReportLineChartTooltip } from "./ReportLineChartTooltop";
import { useFormatDateInterval } from "@/hooks/useFormatDateInterval";
import {
type IChartBreakdown,
type IChartEvent,
type IInterval,
} from "@/types";
import { type IChartInput } from "@/types";
import { getChartColor } from "@/utils/theme";
import { ReportTable } from "./ReportTable";
import { useEffect, useRef, useState } from "react";
import { AutoSizer } from "@/components/AutoSizer";
type ReportLineChartProps = {
interval: IInterval;
startDate: Date;
endDate: Date;
events: IChartEvent[];
breakdowns: IChartBreakdown[];
type ReportLineChartProps = IChartInput & {
showTable?: boolean;
};
@@ -36,16 +26,20 @@ export function ReportLineChart({
events,
breakdowns,
showTable,
chartType,
name,
}: ReportLineChartProps) {
const [visibleSeries, setVisibleSeries] = useState<string[]>([]);
const chart = api.chartMeta.chart.useQuery(
{
interval,
chartType: "linear",
chartType,
startDate,
endDate,
events,
breakdowns,
name,
},
{
enabled: events.length > 0,
@@ -58,25 +52,23 @@ export function ReportLineChart({
useEffect(() => {
if (!ref.current && chart.data) {
const max = 20;
setVisibleSeries(
chart.data?.series?.slice(0, max).map((serie) => serie.name) ?? [],
);
// ref.current = true;
}
}, [chart.data]);
return (
<>
);
// ref.current = true;
}
}, [chart.data]);
return (
<>
{chart.isSuccess && chart.data?.series?.[0]?.data && (
<>
<AutoSizer disableHeight>
{({ width }) => (
<LineChart width={width} height={width * 0.5}>
{/* <Legend /> */}
<LineChart width={width} height={Math.min(width * 0.5, 400)}>
<YAxis dataKey={"count"}></YAxis>
<Tooltip content={<ReportLineChartTooltip />} />
{/* <Tooltip /> */}
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="date"

View File

@@ -0,0 +1,9 @@
import { useQueryParams } from "@/hooks/useQueryParams";
import { z } from "zod";
export const useReportId = () =>
useQueryParams(
z.object({
reportId: z.string().optional(),
}),
);

View File

@@ -1,4 +1,5 @@
import {
type IChartInput,
type IChartBreakdown,
type IChartEvent,
type IInterval,
@@ -6,32 +7,17 @@ import {
import { getDaysOldDate } from "@/utils/date";
import { type PayloadAction, createSlice } from "@reduxjs/toolkit";
type InitialState = {
events: IChartEvent[];
breakdowns: IChartBreakdown[];
interval: IInterval;
startDate: Date;
endDate: Date;
};
type InitialState = IChartInput;
// First approach: define the initial state using that type
const initialState: InitialState = {
name: "",
chartType: "linear",
startDate: getDaysOldDate(7),
endDate: new Date(),
interval: "day",
breakdowns: [
{
id: "A",
name: 'properties.id'
}
],
events: [
{
id: "A",
name: "sign_up",
filters: []
},
],
breakdowns: [],
events: [],
};
const IDS = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J"] as const;
@@ -40,6 +26,12 @@ export const reportSlice = createSlice({
name: "counter",
initialState,
reducers: {
reset() {
return initialState
},
setReport(state, action: PayloadAction<IChartInput>) {
return action.payload
},
// Events
addEvent: (state, action: PayloadAction<Omit<IChartEvent, "id">>) => {
state.events.push({
@@ -111,21 +103,23 @@ export const reportSlice = createSlice({
},
changeDateRanges: (state, action: PayloadAction<number>) => {
if(action.payload === 1) {
if (action.payload === 1) {
state.interval = "hour";
} else if(action.payload <= 30) {
} else if (action.payload <= 30) {
state.interval = "day";
} else {
state.interval = "month";
}
state.startDate = getDaysOldDate(action.payload);
state.endDate = new Date();
}
},
},
});
// Action creators are generated for each case reducer function
export const {
reset,
setReport,
addEvent,
removeEvent,
changeEvent,

View File

@@ -0,0 +1,36 @@
import { Button } from "@/components/ui/button";
import { useReportId } from "../hooks/useReportId";
import { api } from "@/utils/api";
import { useSelector } from "@/redux";
export function ReportSave() {
const { reportId } = useReportId();
const save = api.report.save.useMutation();
const update = api.report.update.useMutation();
const report = useSelector((state) => state.report);
if (reportId) {
return <Button onClick={() => {
update.mutate({
reportId,
report,
dashboardId: "9227feb4-ad59-40f3-b887-3501685733dd",
projectId: "f7eabf0c-e0b0-4ac0-940f-1589715b0c3d",
});
}}>Update</Button>;
} else {
return (
<Button
onClick={() => {
save.mutate({
report,
dashboardId: "9227feb4-ad59-40f3-b887-3501685733dd",
projectId: "f7eabf0c-e0b0-4ac0-940f-1589715b0c3d",
});
}}
>
Create
</Button>
);
}
}

View File

@@ -1,11 +1,13 @@
import { ReportEvents } from "./ReportEvents";
import { ReportBreakdowns } from "./ReportBreakdowns";
import { ReportSave } from "./ReportSave";
export function ReportSidebar() {
return (
<div className="flex flex-col gap-4 p-4">
<ReportEvents />
<ReportBreakdowns />
<ReportSave />
</div>
);
}