diff --git a/src/app/(dashboard)/projects/[projectId]/analytics/page.tsx b/src/app/(dashboard)/projects/[projectId]/analytics/page.tsx new file mode 100644 index 0000000..76811f6 --- /dev/null +++ b/src/app/(dashboard)/projects/[projectId]/analytics/page.tsx @@ -0,0 +1,69 @@ +"use client"; + +import { AnalyticsConfigTable } from "@/components/analyticsConfigTable/analyticsConfig-table"; +import { + Breadcrumb, + BreadcrumbItem, + BreadcrumbLink, + BreadcrumbList, + BreadcrumbPage, + BreadcrumbSeparator, +} from "@/components/ui/breadcrumb"; +import { Separator } from "@/components/ui/separator"; +import { getProjectById } from "@/lib/project"; +import { useQuery } from "@tanstack/react-query"; +import { ProjectResponse } from "juno-sdk/build/main/internal/index"; +import { useParams } from "next/navigation"; +import { useEffect } from "react"; +import { toast } from "sonner"; + +const ProjectAnalyticsPage = () => { + const { projectId } = useParams<{ projectId: string }>(); + + const { isLoading, isError, data, error } = useQuery({ + queryKey: ["project", projectId], + queryFn: async () => { + const result = await getProjectById(Number(projectId)); + if (!result.success) { + throw new Error(result.error); + } + return result.project; + }, + }); + + useEffect(() => { + if (isError && error) { + toast.error("Error", { + description: `Failed to fetch project: ${error.message}`, + }); + } + }, [isError, error]); + + return ( +
+ + + + Projects + + + + + {isLoading ? "****" : (data?.name ?? "Unknown")} + + + + + Analytics + + + + +
+ +
+
+ ); +}; + +export default ProjectAnalyticsPage; diff --git a/src/app/(dashboard)/projects/[projectId]/page.tsx b/src/app/(dashboard)/projects/[projectId]/page.tsx index 84eff7e..65fd881 100644 --- a/src/app/(dashboard)/projects/[projectId]/page.tsx +++ b/src/app/(dashboard)/projects/[projectId]/page.tsx @@ -31,7 +31,7 @@ import { } from "@/lib/settings"; import { useQuery } from "@tanstack/react-query"; import type { ProjectResponse } from "juno-sdk/build/main/internal/index"; -import { BarChart3, Settings } from "lucide-react"; +import { BarChart3 } from "lucide-react"; import Link from "next/link"; import { useParams } from "next/navigation"; import { useEffect, useMemo, useState } from "react"; @@ -575,9 +575,9 @@ const DashboardPage = () => { clicks, and custom events using Juno.

diff --git a/src/app/(dashboard)/projects/[projectId]/settings/page.tsx b/src/app/(dashboard)/projects/[projectId]/settings/page.tsx index dd923f7..161140d 100644 --- a/src/app/(dashboard)/projects/[projectId]/settings/page.tsx +++ b/src/app/(dashboard)/projects/[projectId]/settings/page.tsx @@ -1,6 +1,5 @@ "use client"; -import { AnalyticsConfigTable } from "@/components/analyticsConfigTable/analyticsConfig-table"; import { EmailConfigTable } from "@/components/emailConfigTable/emailConfig-table"; import { FileConfigTable } from "@/components/fileConfigTable/fileConfig-table"; import { @@ -61,7 +60,6 @@ const ProjectSettingsPage = () => {
-
); diff --git a/src/components/analyticsConfigTable/analyticsConfig-table.tsx b/src/components/analyticsConfigTable/analyticsConfig-table.tsx index ad058c9..fbb2d6a 100644 --- a/src/components/analyticsConfigTable/analyticsConfig-table.tsx +++ b/src/components/analyticsConfigTable/analyticsConfig-table.tsx @@ -12,12 +12,15 @@ import { getAnalyticsConfig, updateAnalyticsConfig, } from "@/lib/settings"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { useState } from "react"; +import { ChartColumn, CircleX } from "lucide-react"; +import { useEffect, useState } from "react"; import { toast } from "sonner"; import { useReadOnlyMode } from "../providers/SessionProvider"; import { BaseTable } from "../baseTable"; import AddAnalyticsConfigForm from "../forms/AddAnalyticsConfigForm"; +import { Alert } from "../ui/alert"; import { Button } from "../ui/button"; import { DialogFooter, DialogHeader } from "../ui/dialog"; import { analyticsConfigColumns } from "./columns"; @@ -29,7 +32,6 @@ interface AnalyticsConfigTableProps { export function AnalyticsConfigTable({ projectId }: AnalyticsConfigTableProps) { const [isAddConfigDialogOpen, setIsAddConfigDialogOpen] = useState(false); const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false); - const [selectedRows, setSelectedRows] = useState([]); const isReadOnly = useReadOnlyMode(); const queryClient = useQueryClient(); @@ -41,21 +43,16 @@ export function AnalyticsConfigTable({ projectId }: AnalyticsConfigTableProps) { const analyticsConfigRowData = [data].filter((config) => config); - if (isError) { - toast.error("Error", { - description: `Failed to fetch analytics config: ${error.message}`, - }); - } - - const deleteAnalyticsConfigHandler = useMutation({ - mutationFn: () => { - const deletePromises = selectedRows.map(async (row) => { - const projectId = row.id; - return deleteAnalyticsConfig(projectId); + useEffect(() => { + if (isError && error) { + toast.error("Error", { + description: `Failed to fetch analytics config: ${error.message}`, }); + } + }, [isError, error]); - return Promise.all(deletePromises); - }, + const deleteAnalyticsConfigHandler = useMutation({ + mutationFn: async () => deleteAnalyticsConfig(projectId), onSuccess: () => { toast.success("Success", { description: `Successfully deleted analytics configs.`, @@ -135,11 +132,11 @@ export function AnalyticsConfigTable({ projectId }: AnalyticsConfigTableProps) { - Delete Selected Analytics Config + Delete Analytics Configuration - Are you sure you want to delete {selectedRows.length} selected - config{selectedRows.length > 1 ? "s" : ""}? This action cannot be - undone. + Are you sure you want to delete the analytics configuration for + this project? This will remove all analytics tracking settings and + cannot be undone. @@ -163,48 +160,79 @@ export function AnalyticsConfigTable({ projectId }: AnalyticsConfigTableProps) {

Analytics Configurations

- + + + Couldn't load analytics config: {error.message} + + + )} + {isError ? null : !isLoading && analyticsConfigRowData.length === 0 ? ( + + +
+ + No Analytics Configuration +
+
+ +

+ Project {projectId} has no analytics configuration yet. Add a + server key and a client key to start tracking user interactions, + visits, clicks, and custom events using Juno. +

+ +
+
+ ) : ( + { + const { serverAnalyticsKey, clientAnalyticsKey } = keys; + updateAnalyticsConfigHandler.mutate({ + projectId, + serverAnalyticsKey, + clientAnalyticsKey, + }); + setIsAddConfigDialogOpen(false); }, - ) => { - const { serverAnalyticsKey, clientAnalyticsKey } = keys; - updateAnalyticsConfigHandler.mutate({ - projectId, - serverAnalyticsKey, - clientAnalyticsKey, - }); - setIsAddConfigDialogOpen(false); - }, - isReadOnly, - )} - isLoading={isLoading} - filterParams={{ - placeholder: "Filter by environment...", - filterColumn: "environment", - }} - onAddNewRow={() => { - if (analyticsConfigRowData.length === 0) { - setIsAddConfigDialogOpen(true); - } else { - toast.error("Error", { - description: - "Project can have at most 1 analytics configuration per environment", - }); - } - }} - onDeleteRow={(rows) => { - setSelectedRows(rows); - setIsDeleteDialogOpen(true); - }} - /> + isReadOnly, + )} + isLoading={isLoading} + filterParams={{ + placeholder: "Filter by environment...", + filterColumn: "environment", + }} + onAddNewRow={() => { + if (analyticsConfigRowData.length === 0) { + setIsAddConfigDialogOpen(true); + } else { + toast.error("Error", { + description: + "Project can have at most 1 analytics configuration per environment", + }); + } + }} + onDeleteRow={() => { + setIsDeleteDialogOpen(true); + }} + /> + )} ); } diff --git a/src/components/sidebar/project-sidebar.tsx b/src/components/sidebar/project-sidebar.tsx index c8dd5d5..4f522a3 100644 --- a/src/components/sidebar/project-sidebar.tsx +++ b/src/components/sidebar/project-sidebar.tsx @@ -10,6 +10,7 @@ import { Settings, Shield, User, + ChartColumn, } from "lucide-react"; import { @@ -135,6 +136,11 @@ export function ProjectSidebar({ url: `/projects/${projectId}/settings`, icon: Settings, }, + { + title: "Analytics", + url: `/projects/${projectId}/analytics`, + icon: ChartColumn, + }, ]; return ( diff --git a/src/components/top-bar.tsx b/src/components/top-bar.tsx index 1facff1..04c34e5 100644 --- a/src/components/top-bar.tsx +++ b/src/components/top-bar.tsx @@ -22,6 +22,7 @@ function getPageTitle(pathname: string): string { if (sub === "/services/email") return "Email"; if (sub === "/services/files") return "Files"; if (sub === "/settings") return "Settings"; + if (sub === "/analytics") return "Analytics"; } return "Dashboard"; diff --git a/src/lib/settings.ts b/src/lib/settings.ts index 01c8775..d929a6f 100644 --- a/src/lib/settings.ts +++ b/src/lib/settings.ts @@ -126,7 +126,7 @@ export async function getEmailConfig( export async function getAnalyticsConfig( projectId: string, -): Promise { +): Promise { const session = await getSession(); if (!session) { throw new Error("Unauthorized"); @@ -147,9 +147,14 @@ export async function getAnalyticsConfig( ); return JSON.parse(JSON.stringify(analyticsConfig)); } catch (e) { - if (e.response?.status === 404 || e.response?.status === 401) { + if (e.response?.status === 404) { return null; } + if (e.response?.status === 401) { + throw new Error( + `Unauthorized: Juno rejected the request for project ${projectId}. The API key may be missing, invalid, or not permitted for this project.`, + ); + } throw e; } }