Skip to content

Commit a36be6c

Browse files
committed
- Add an Analytics entry to the project sidebar and a dedicated /projects/[projectId]/analytics page for CRUD operations on project's analytics config only.
- No Analytics Configuration card when none exist, matching the style of the file and email config pages. - getAnalyticsConfig returns null only on 404; throws 401 for auth failures and shows an error alert instead of looking like an empty state - Delete the config by the current projectId, not the table row id; only one config could be made which would always be row id = 0, passing 0 as the parameter in function that deletes configs by projectId (always deleted project 0 config id) - Move the analytics table out of Settings; point the project dashboard card at the new page; add the "Analytics" top bar title
1 parent 4f3ad41 commit a36be6c

7 files changed

Lines changed: 163 additions & 57 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
"use client";
2+
3+
import { AnalyticsConfigTable } from "@/components/analyticsConfigTable/analyticsConfig-table";
4+
import {
5+
Breadcrumb,
6+
BreadcrumbItem,
7+
BreadcrumbLink,
8+
BreadcrumbList,
9+
BreadcrumbPage,
10+
BreadcrumbSeparator,
11+
} from "@/components/ui/breadcrumb";
12+
import { Separator } from "@/components/ui/separator";
13+
import { getProjectById } from "@/lib/project";
14+
import { useQuery } from "@tanstack/react-query";
15+
import { ProjectResponse } from "juno-sdk/build/main/internal/index";
16+
import { useParams } from "next/navigation";
17+
import { toast } from "sonner";
18+
19+
const ProjectAnalyticsPage = () => {
20+
const { projectId } = useParams<{ projectId: string }>();
21+
22+
const { isLoading, isError, data, error } = useQuery<ProjectResponse>({
23+
queryKey: ["project", projectId],
24+
queryFn: async () => {
25+
const result = await getProjectById(Number(projectId));
26+
if (!result.success) {
27+
throw new Error(result.error);
28+
}
29+
return result.project;
30+
},
31+
});
32+
33+
if (isError) {
34+
toast.error("Error", {
35+
description: `Failed to fetch project: ${JSON.stringify(error)}`,
36+
});
37+
}
38+
39+
return (
40+
<div className="flex flex-col">
41+
<Breadcrumb className="mb-4">
42+
<BreadcrumbList>
43+
<BreadcrumbItem>
44+
<BreadcrumbLink href="/projects">Projects</BreadcrumbLink>
45+
</BreadcrumbItem>
46+
<BreadcrumbSeparator />
47+
<BreadcrumbItem>
48+
<BreadcrumbLink href={`/projects/${projectId}`}>
49+
{isLoading ? "****" : (data?.name ?? "Unknown")}
50+
</BreadcrumbLink>
51+
</BreadcrumbItem>
52+
<BreadcrumbSeparator />
53+
<BreadcrumbItem>
54+
<BreadcrumbPage>Analytics</BreadcrumbPage>
55+
</BreadcrumbItem>
56+
</BreadcrumbList>
57+
</Breadcrumb>
58+
<Separator className="mb-8" />
59+
<div className="flex flex-col gap-8">
60+
<AnalyticsConfigTable projectId={projectId} />
61+
</div>
62+
</div>
63+
);
64+
};
65+
66+
export default ProjectAnalyticsPage;

‎src/app/(dashboard)/projects/[projectId]/page.tsx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import {
3131
} from "@/lib/settings";
3232
import { useQuery } from "@tanstack/react-query";
3333
import type { ProjectResponse } from "juno-sdk/build/main/internal/index";
34-
import { BarChart3, Settings } from "lucide-react";
34+
import { BarChart3 } from "lucide-react";
3535
import Link from "next/link";
3636
import { useParams } from "next/navigation";
3737
import { useEffect, useMemo, useState } from "react";
@@ -575,9 +575,9 @@ const DashboardPage = () => {
575575
clicks, and custom events using Juno.
576576
</p>
577577
<Button asChild>
578-
<Link href={`/projects/${projectId}/settings`}>
579-
<Settings className="mr-2 h-4 w-4" />
580-
Go to Settings
578+
<Link href={`/projects/${projectId}/analytics`}>
579+
<BarChart3 className="mr-2 h-4 w-4" />
580+
Set up analytics
581581
</Link>
582582
</Button>
583583
</CardContent>

‎src/app/(dashboard)/projects/[projectId]/settings/page.tsx‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
"use client";
22

3-
import { AnalyticsConfigTable } from "@/components/analyticsConfigTable/analyticsConfig-table";
43
import { EmailConfigTable } from "@/components/emailConfigTable/emailConfig-table";
54
import { FileConfigTable } from "@/components/fileConfigTable/fileConfig-table";
65
import {
@@ -61,7 +60,6 @@ const ProjectSettingsPage = () => {
6160
<div className="flex flex-col gap-8">
6261
<FileConfigTable projectId={projectId} />
6362
<EmailConfigTable projectId={projectId} />
64-
<AnalyticsConfigTable projectId={projectId} />
6563
</div>
6664
</div>
6765
);

‎src/components/analyticsConfigTable/analyticsConfig-table.tsx‎

Lines changed: 79 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,15 @@ import {
1212
getAnalyticsConfig,
1313
updateAnalyticsConfig,
1414
} from "@/lib/settings";
15+
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
1516
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
17+
import { ChartColumn, CircleX } from "lucide-react";
1618
import { useState } from "react";
1719
import { toast } from "sonner";
1820
import { useReadOnlyMode } from "../providers/SessionProvider";
1921
import { BaseTable } from "../baseTable";
2022
import AddAnalyticsConfigForm from "../forms/AddAnalyticsConfigForm";
23+
import { Alert } from "../ui/alert";
2124
import { Button } from "../ui/button";
2225
import { DialogFooter, DialogHeader } from "../ui/dialog";
2326
import { analyticsConfigColumns } from "./columns";
@@ -48,14 +51,7 @@ export function AnalyticsConfigTable({ projectId }: AnalyticsConfigTableProps) {
4851
}
4952

5053
const deleteAnalyticsConfigHandler = useMutation({
51-
mutationFn: () => {
52-
const deletePromises = selectedRows.map(async (row) => {
53-
const projectId = row.id;
54-
return deleteAnalyticsConfig(projectId);
55-
});
56-
57-
return Promise.all(deletePromises);
58-
},
54+
mutationFn: async () => deleteAnalyticsConfig(projectId),
5955
onSuccess: () => {
6056
toast.success("Success", {
6157
description: `Successfully deleted analytics configs.`,
@@ -163,48 +159,82 @@ export function AnalyticsConfigTable({ projectId }: AnalyticsConfigTableProps) {
163159
</DialogContent>
164160
</Dialog>
165161
<h1 className="text-lg font-bold">Analytics Configurations</h1>
166-
<BaseTable
167-
data={analyticsConfigRowData}
168-
columns={analyticsConfigColumns(
169-
Number(projectId),
170-
addAnalyticsConfigHandler.isPending,
171-
(
172-
projectId: number,
173-
keys: {
174-
serverAnalyticsKey: string;
175-
clientAnalyticsKey: string;
162+
{isError && (
163+
<Alert className="border-red-500/20 bg-red-500/10">
164+
<div className="flex items-center gap-2 text-red-400">
165+
<CircleX className="h-4 w-4" />
166+
<span className="text-sm">
167+
Couldn&apos;t load analytics config: {error.message}
168+
</span>
169+
</div>
170+
</Alert>
171+
)}
172+
{isError ? null : !isLoading && analyticsConfigRowData.length === 0 ? (
173+
<Card className="max-w-[35%]">
174+
<CardHeader>
175+
<div className="flex items-center gap-3">
176+
<ChartColumn className="h-5 w-5 text-muted-foreground" />
177+
<CardTitle>No Analytics Configuration</CardTitle>
178+
</div>
179+
</CardHeader>
180+
<CardContent>
181+
<p className="text-sm text-muted-foreground mb-4">
182+
Project {projectId} has no analytics configuration yet. Add a
183+
server key and a client key to start tracking user interactions,
184+
visits, clicks, and custom events using Juno.
185+
</p>
186+
<Button
187+
onClick={() => setIsAddConfigDialogOpen(true)}
188+
disabled={isReadOnly || addAnalyticsConfigHandler.isPending}
189+
>
190+
Set up analytics
191+
</Button>
192+
</CardContent>
193+
</Card>
194+
) : (
195+
<BaseTable
196+
data={analyticsConfigRowData}
197+
columns={analyticsConfigColumns(
198+
Number(projectId),
199+
addAnalyticsConfigHandler.isPending,
200+
(
201+
projectId: number,
202+
keys: {
203+
serverAnalyticsKey: string;
204+
clientAnalyticsKey: string;
205+
},
206+
) => {
207+
const { serverAnalyticsKey, clientAnalyticsKey } = keys;
208+
updateAnalyticsConfigHandler.mutate({
209+
projectId,
210+
serverAnalyticsKey,
211+
clientAnalyticsKey,
212+
});
213+
setIsAddConfigDialogOpen(false);
176214
},
177-
) => {
178-
const { serverAnalyticsKey, clientAnalyticsKey } = keys;
179-
updateAnalyticsConfigHandler.mutate({
180-
projectId,
181-
serverAnalyticsKey,
182-
clientAnalyticsKey,
183-
});
184-
setIsAddConfigDialogOpen(false);
185-
},
186-
isReadOnly,
187-
)}
188-
isLoading={isLoading}
189-
filterParams={{
190-
placeholder: "Filter by environment...",
191-
filterColumn: "environment",
192-
}}
193-
onAddNewRow={() => {
194-
if (analyticsConfigRowData.length === 0) {
195-
setIsAddConfigDialogOpen(true);
196-
} else {
197-
toast.error("Error", {
198-
description:
199-
"Project can have at most 1 analytics configuration per environment",
200-
});
201-
}
202-
}}
203-
onDeleteRow={(rows) => {
204-
setSelectedRows(rows);
205-
setIsDeleteDialogOpen(true);
206-
}}
207-
/>
215+
isReadOnly,
216+
)}
217+
isLoading={isLoading}
218+
filterParams={{
219+
placeholder: "Filter by environment...",
220+
filterColumn: "environment",
221+
}}
222+
onAddNewRow={() => {
223+
if (analyticsConfigRowData.length === 0) {
224+
setIsAddConfigDialogOpen(true);
225+
} else {
226+
toast.error("Error", {
227+
description:
228+
"Project can have at most 1 analytics configuration per environment",
229+
});
230+
}
231+
}}
232+
onDeleteRow={(rows) => {
233+
setSelectedRows(rows);
234+
setIsDeleteDialogOpen(true);
235+
}}
236+
/>
237+
)}
208238
</div>
209239
);
210240
}

‎src/components/sidebar/project-sidebar.tsx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
Settings,
1010
Shield,
1111
User,
12+
ChartColumn,
1213
} from "lucide-react";
1314

1415
import {
@@ -125,6 +126,11 @@ export function ProjectSidebar({
125126
url: `/projects/${projectId}/settings`,
126127
icon: Settings,
127128
},
129+
{
130+
title: "Analytics",
131+
url: `/projects/${projectId}/analytics`,
132+
icon: ChartColumn,
133+
},
128134
];
129135

130136
return (

‎src/components/top-bar.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ function getPageTitle(pathname: string): string {
2222
if (sub === "/services/email") return "Email";
2323
if (sub === "/services/files") return "Files";
2424
if (sub === "/settings") return "Settings";
25+
if (sub === "/analytics") return "Analytics";
2526
}
2627

2728
return "Dashboard";

‎src/lib/settings.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export async function getEmailConfig(
126126

127127
export async function getAnalyticsConfig(
128128
projectId: string,
129-
): Promise<AnalyticsConfigResponse> {
129+
): Promise<AnalyticsConfigResponse | null> {
130130
const session = await getSession();
131131
if (!session) {
132132
throw new Error("Unauthorized");
@@ -147,9 +147,14 @@ export async function getAnalyticsConfig(
147147
);
148148
return JSON.parse(JSON.stringify(analyticsConfig));
149149
} catch (e) {
150-
if (e.response?.status === 404 || e.response?.status === 401) {
150+
if (e.response?.status === 404) {
151151
return null;
152152
}
153+
if (e.response?.status === 401) {
154+
throw new Error(
155+
`Unauthorized: Juno rejected the request for project ${projectId}. The API key may be missing, invalid, or not permitted for this project.`,
156+
);
157+
}
153158
throw e;
154159
}
155160
}

0 commit comments

Comments
 (0)