Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 69 additions & 0 deletions src/app/(dashboard)/projects/[projectId]/analytics/page.tsx
Original file line number Diff line number Diff line change
@@ -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<ProjectResponse>({
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 (
<div className="flex flex-col">
<Breadcrumb className="mb-4">
<BreadcrumbList>
<BreadcrumbItem>
<BreadcrumbLink href="/projects">Projects</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbLink href={`/projects/${projectId}`}>
{isLoading ? "****" : (data?.name ?? "Unknown")}
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
<BreadcrumbItem>
<BreadcrumbPage>Analytics</BreadcrumbPage>
</BreadcrumbItem>
</BreadcrumbList>
</Breadcrumb>
<Separator className="mb-8" />
<div className="flex flex-col gap-8">
<AnalyticsConfigTable projectId={projectId} />
</div>
</div>
);
};

export default ProjectAnalyticsPage;
8 changes: 4 additions & 4 deletions src/app/(dashboard)/projects/[projectId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -575,9 +575,9 @@ const DashboardPage = () => {
clicks, and custom events using Juno.
</p>
<Button asChild>
<Link href={`/projects/${projectId}/settings`}>
<Settings className="mr-2 h-4 w-4" />
Go to Settings
<Link href={`/projects/${projectId}/analytics`}>
<BarChart3 className="mr-2 h-4 w-4" />
Set up analytics
</Link>
</Button>
</CardContent>
Expand Down
2 changes: 0 additions & 2 deletions src/app/(dashboard)/projects/[projectId]/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -61,7 +60,6 @@ const ProjectSettingsPage = () => {
<div className="flex flex-col gap-8">
<FileConfigTable projectId={projectId} />
<EmailConfigTable projectId={projectId} />
<AnalyticsConfigTable projectId={projectId} />
</div>
</div>
);
Expand Down
148 changes: 88 additions & 60 deletions src/components/analyticsConfigTable/analyticsConfig-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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();
Expand All @@ -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.`,
Expand Down Expand Up @@ -135,11 +132,11 @@ export function AnalyticsConfigTable({ projectId }: AnalyticsConfigTableProps) {
<Dialog open={isDeleteDialogOpen} onOpenChange={setIsDeleteDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Delete Selected Analytics Config</DialogTitle>
<DialogTitle>Delete Analytics Configuration</DialogTitle>
<DialogDescription>
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.
</DialogDescription>
</DialogHeader>
<DialogFooter>
Expand All @@ -163,48 +160,79 @@ export function AnalyticsConfigTable({ projectId }: AnalyticsConfigTableProps) {
</DialogContent>
</Dialog>
<h1 className="text-lg font-bold">Analytics Configurations</h1>
<BaseTable
data={analyticsConfigRowData}
columns={analyticsConfigColumns(
Number(projectId),
addAnalyticsConfigHandler.isPending,
(
projectId: number,
keys: {
serverAnalyticsKey: string;
clientAnalyticsKey: string;
{isError && (
<Alert variant="destructive">
<CircleX className="h-4 w-4" />
<span className="text-sm">
Couldn&apos;t load analytics config: {error.message}
</span>
</Alert>
)}
{isError ? null : !isLoading && analyticsConfigRowData.length === 0 ? (
<Card className="max-w-full sm:max-w-md">
<CardHeader>
<div className="flex items-center gap-3">
<ChartColumn className="h-5 w-5 text-muted-foreground" />
<CardTitle>No Analytics Configuration</CardTitle>
</div>
</CardHeader>
<CardContent>
<p className="text-sm text-muted-foreground mb-4">
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.
</p>
<Button
onClick={() => setIsAddConfigDialogOpen(true)}
disabled={isReadOnly || addAnalyticsConfigHandler.isPending}
>
Set up analytics
</Button>
</CardContent>
</Card>
) : (
<BaseTable
data={analyticsConfigRowData}
columns={analyticsConfigColumns(
Number(projectId),
addAnalyticsConfigHandler.isPending,
(
projectId: number,
keys: {
serverAnalyticsKey: string;
clientAnalyticsKey: string;
},
) => {
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);
}}
/>
)}
</div>
);
}
6 changes: 6 additions & 0 deletions src/components/sidebar/project-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
Settings,
Shield,
User,
ChartColumn,
} from "lucide-react";

import {
Expand Down Expand Up @@ -135,6 +136,11 @@ export function ProjectSidebar({
url: `/projects/${projectId}/settings`,
icon: Settings,
},
{
title: "Analytics",
url: `/projects/${projectId}/analytics`,
icon: ChartColumn,
},
];

return (
Expand Down
1 change: 1 addition & 0 deletions src/components/top-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
9 changes: 7 additions & 2 deletions src/lib/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ export async function getEmailConfig(

export async function getAnalyticsConfig(
projectId: string,
): Promise<AnalyticsConfigResponse> {
): Promise<AnalyticsConfigResponse | null> {
const session = await getSession();
if (!session) {
throw new Error("Unauthorized");
Expand All @@ -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;
}
}
Expand Down
Loading