diff --git a/apps/web/src/components/navbar/NavbarMenu.tsx b/apps/web/src/components/navbar/NavbarMenu.tsx index 060ea009..08760372 100644 --- a/apps/web/src/components/navbar/NavbarMenu.tsx +++ b/apps/web/src/components/navbar/NavbarMenu.tsx @@ -27,7 +27,9 @@ export function NavbarMenu() { return (
{params.project && ( - Home + + Dashboards + )} {params.project && ( diff --git a/apps/web/src/components/report/chart/index.tsx b/apps/web/src/components/report/chart/index.tsx index 107c0d62..95234ff9 100644 --- a/apps/web/src/components/report/chart/index.tsx +++ b/apps/web/src/components/report/chart/index.tsx @@ -47,7 +47,7 @@ export const Chart = memo( if (!enabled) { return ( - +

Please select at least one event to see the chart.

@@ -58,7 +58,7 @@ export const Chart = memo( if (chart.isFetching) { return ( - +

Loading...

); @@ -67,20 +67,22 @@ export const Chart = memo( if (chart.isError) { return ( - +

Something went wrong...

); } if (!chart.isSuccess) { - return ; + return ( + + ); } if (!anyData) { return ( - +

No data

); @@ -96,7 +98,7 @@ export const Chart = memo( return ( - +

Chart type "{chartType}" is not supported yet.

diff --git a/apps/web/src/components/report/reportSlice.ts b/apps/web/src/components/report/reportSlice.ts index 5f66fc65..ccf8f0de 100644 --- a/apps/web/src/components/report/reportSlice.ts +++ b/apps/web/src/components/report/reportSlice.ts @@ -19,7 +19,7 @@ type InitialState = IChartInput & { // First approach: define the initial state using that type const initialState: InitialState = { dirty: false, - name: 'screen_view', + name: 'Untitled', chartType: 'linear', interval: 'day', breakdowns: [], @@ -30,7 +30,7 @@ const initialState: InitialState = { }; export const reportSlice = createSlice({ - name: 'counter', + name: 'report', initialState, reducers: { resetDirty(state) { @@ -50,6 +50,10 @@ export const reportSlice = createSlice({ dirty: false, }; }, + setName(state, action: PayloadAction) { + state.dirty = true; + state.name = action.payload; + }, // Events addEvent: (state, action: PayloadAction>) => { state.dirty = true; @@ -162,6 +166,7 @@ export const reportSlice = createSlice({ export const { reset, setReport, + setName, addEvent, removeEvent, changeEvent, diff --git a/apps/web/src/modals/EditDashboard.tsx b/apps/web/src/modals/EditDashboard.tsx new file mode 100644 index 00000000..dca3ac8e --- /dev/null +++ b/apps/web/src/modals/EditDashboard.tsx @@ -0,0 +1,75 @@ +import { useEffect } from 'react'; +import { ButtonContainer } from '@/components/ButtonContainer'; +import { InputWithLabel } from '@/components/forms/InputWithLabel'; +import { Button } from '@/components/ui/button'; +import { toast } from '@/components/ui/use-toast'; +import { useRefetchActive } from '@/hooks/useRefetchActive'; +import { api, handleError } from '@/utils/api'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { popModal } from '.'; +import { ModalContent, ModalHeader } from './Modal/Container'; + +interface EditDashboardProps { + id: string; +} + +const validator = z.object({ + id: z.string().min(1), + name: z.string().min(1), +}); + +type IForm = z.infer; + +export default function EditDashboard({ id }: EditDashboardProps) { + const refetch = useRefetchActive(); + const mutation = api.dashboard.update.useMutation({ + onError: handleError, + onSuccess() { + toast({ + title: 'Success', + description: 'Dashboard updated.', + }); + popModal(); + refetch(); + }, + }); + const query = api.dashboard.get.useQuery({ id }); + const data = query.data; + const { register, handleSubmit, reset, formState } = useForm({ + resolver: zodResolver(validator), + defaultValues: { + id: '', + name: '', + }, + }); + + useEffect(() => { + if (data) { + reset(data); + } + }, [data, reset]); + + return ( + + +
{ + mutation.mutate(values); + })} + > + + + + + + +
+ ); +} diff --git a/apps/web/src/modals/EditReport.tsx b/apps/web/src/modals/EditReport.tsx new file mode 100644 index 00000000..c64755f5 --- /dev/null +++ b/apps/web/src/modals/EditReport.tsx @@ -0,0 +1,49 @@ +import { useEffect } from 'react'; +import { ButtonContainer } from '@/components/ButtonContainer'; +import { InputWithLabel } from '@/components/forms/InputWithLabel'; +import { Button } from '@/components/ui/button'; +import { toast } from '@/components/ui/use-toast'; +import { useRefetchActive } from '@/hooks/useRefetchActive'; +import { api, handleError } from '@/utils/api'; +import { zodResolver } from '@hookform/resolvers/zod'; +import type { SubmitHandler } from 'react-hook-form'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; + +import { popModal } from '.'; +import { ModalContent, ModalHeader } from './Modal/Container'; + +const validator = z.object({ + name: z.string().min(1), +}); + +type IForm = z.infer; + +interface EditReportProps { + form: IForm; + onSubmit: SubmitHandler; +} + +export default function EditReport({ form, onSubmit }: EditReportProps) { + const { register, handleSubmit, reset, formState } = useForm({ + resolver: zodResolver(validator), + defaultValues: form, + }); + + return ( + + +
+ + + + + + +
+ ); +} diff --git a/apps/web/src/modals/SaveReport.tsx b/apps/web/src/modals/SaveReport.tsx index 985c15ff..2327bc7e 100644 --- a/apps/web/src/modals/SaveReport.tsx +++ b/apps/web/src/modals/SaveReport.tsx @@ -54,7 +54,7 @@ export default function SaveReport({ report }: SaveReportProps) { useForm({ resolver: zodResolver(validator), defaultValues: { - name: '', + name: report.name, dashboardId: '', }, }); @@ -136,7 +136,7 @@ export default function SaveReport({ report }: SaveReportProps) { - diff --git a/apps/web/src/modals/index.tsx b/apps/web/src/modals/index.tsx index 8be3ad8c..3ac0fe48 100644 --- a/apps/web/src/modals/index.tsx +++ b/apps/web/src/modals/index.tsx @@ -34,6 +34,12 @@ const modals = { AddDashboard: dynamic(() => import('./AddDashboard'), { loading: Loading, }), + EditDashboard: dynamic(() => import('./EditDashboard'), { + loading: Loading, + }), + EditReport: dynamic(() => import('./EditReport'), { + loading: Loading, + }), }; const emitter = mitt<{ diff --git a/apps/web/src/pages/[organization]/[project]/[dashboard].tsx b/apps/web/src/pages/[organization]/[project]/[dashboard].tsx index 60e79b3b..f60be8c2 100644 --- a/apps/web/src/pages/[organization]/[project]/[dashboard].tsx +++ b/apps/web/src/pages/[organization]/[project]/[dashboard].tsx @@ -1,17 +1,25 @@ import { useMemo, useState } from 'react'; +import { CardActions, CardActionsItem } from '@/components/Card'; import { Container } from '@/components/Container'; import { MainLayout } from '@/components/layouts/MainLayout'; import { PageTitle } from '@/components/PageTitle'; import { LazyChart } from '@/components/report/chart/LazyChart'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuGroup, + DropdownMenuItem, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; import { useOrganizationParams } from '@/hooks/useOrganizationParams'; import { createServerSideProps } from '@/server/getServerSideProps'; import type { IChartRange } from '@/types'; -import { api } from '@/utils/api'; +import { api, handleError } from '@/utils/api'; import { cn } from '@/utils/cn'; import { timeRanges } from '@/utils/constants'; import { getRangeLabel } from '@/utils/getRangeLabel'; -import { ChevronRight } from 'lucide-react'; +import { ChevronRight, MoreHorizontal, Trash } from 'lucide-react'; import Link from 'next/link'; export const getServerSideProps = createServerSideProps(); @@ -29,6 +37,13 @@ export default function Dashboard() { return query.data?.reports ?? []; }, [query]); + const deletion = api.report.delete.useMutation({ + onError: handleError, + onSuccess() { + query.refetch(); + }, + }); + const [range, setRange] = useState(null); return ( @@ -62,7 +77,7 @@ export default function Dashboard() { >
@@ -76,7 +91,33 @@ export default function Dashboard() {
)}
- +
+ + + + + + + { + event.stopPropagation(); + deletion.mutate({ + reportId: report.id, + }); + }} + > + + Delete + + + + + +
+ + + + +
diff --git a/apps/web/src/server/api/routers/dashboard.ts b/apps/web/src/server/api/routers/dashboard.ts index 5dee72b8..32b5788b 100644 --- a/apps/web/src/server/api/routers/dashboard.ts +++ b/apps/web/src/server/api/routers/dashboard.ts @@ -10,12 +10,22 @@ import { z } from 'zod'; export const dashboardRouter = createTRPCRouter({ get: protectedProcedure .input( - z.object({ - slug: z.string(), - }) + z + .object({ + slug: z.string(), + }) + .or(z.object({ id: z.string() })) ) - .query(async ({ input: { slug } }) => { - return getDashboardBySlug(slug); + .query(async ({ input }) => { + if ('id' in input) { + return db.dashboard.findUnique({ + where: { + id: input.id, + }, + }); + } else { + return getDashboardBySlug(input.slug); + } }), list: protectedProcedure .input( @@ -41,6 +51,9 @@ export const dashboardRouter = createTRPCRouter({ where: { project_id: projectId, }, + orderBy: { + createdAt: 'desc', + }, }); }), create: protectedProcedure @@ -60,6 +73,23 @@ export const dashboardRouter = createTRPCRouter({ }, }); }), + update: protectedProcedure + .input( + z.object({ + id: z.string(), + name: z.string(), + }) + ) + .mutation(({ input }) => { + return db.dashboard.update({ + where: { + id: input.id, + }, + data: { + name: input.name, + }, + }); + }), delete: protectedProcedure .input( z.object({ diff --git a/apps/web/src/server/api/routers/report.ts b/apps/web/src/server/api/routers/report.ts index c8cedb35..8a68c87f 100644 --- a/apps/web/src/server/api/routers/report.ts +++ b/apps/web/src/server/api/routers/report.ts @@ -83,6 +83,9 @@ export const reportRouter = createTRPCRouter({ project_id: project.id, dashboard_id: dashboard.id, }, + orderBy: { + createdAt: 'desc', + }, }); return { @@ -138,4 +141,17 @@ export const reportRouter = createTRPCRouter({ }, }); }), + delete: protectedProcedure + .input( + z.object({ + reportId: z.string(), + }) + ) + .mutation(({ input: { reportId } }) => { + return db.report.delete({ + where: { + id: reportId, + }, + }); + }), });