Skip to content

Commit

Permalink
feat(ConfirmationDialog.tsx): add support for deleting all errors ass…
Browse files Browse the repository at this point in the history
…ociated with a project

refactor(ConfirmationDialog.tsx): rename projectConfirmationAction prop to btnId for better semantics

refactor(ConfirmationDialog.tsx): simplify handleConfirmAction function to handle both deleteProject and deleteAllErrors

refactor(Backtrace.tsx): remove commented out BookmarkButton component

feat(OccurrencesChartWrapper.tsx): create new component to display hourly occurrence chart for a list of occurrence ids

feat: add revalidate constant to pages and components to improve Next.js ISR performance

feat(Overview.tsx): add statistics section to display project statistics

feat(Overview.tsx): add chart section to display hourly occurrences in the past 14 days

feat(queries/notices.ts): add function to get all notice IDs for a given projectId

refactor(occurrenceBookmarks.ts, occurrences.ts): remove unused orderByObject parameter from fetchOccurrenceBookmarks function and add new function to get occurrence IDs by notice IDs
  • Loading branch information
masterkain committed May 28, 2023
1 parent bc0739a commit b0d1b5a
Show file tree
Hide file tree
Showing 12 changed files with 263 additions and 150 deletions.
2 changes: 2 additions & 0 deletions app/notices/[notice_id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ type ComponentProps = {
searchParams: { [key: string]: string | undefined };
};

export const revalidate = 10;

export async function generateMetadata({ params }: ComponentProps): Promise<Metadata> {
const notice = await getNoticeById(params.notice_id);
return { title: `(${notice?.project?.name}) ${notice?.kind}` };
Expand Down
2 changes: 2 additions & 0 deletions app/occurrences/[occurrence_id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { SlCompass, SlGlobe, SlGraph, SlLink, SlList, SlUser, SlWrench } from 'r

type OccurrenceTabKeys = 'backtrace' | 'context' | 'environment' | 'session' | 'params' | 'chart' | 'toolbox';

export const revalidate = 10;

export default async function Occurrence({
params,
searchParams,
Expand Down
2 changes: 2 additions & 0 deletions app/projects/[project_id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ import type { Route } from 'next';
import { Metadata } from 'next';
import Sort from './Sort';

export const revalidate = 10;

type ComponentProps = {
params: { project_id: string };
searchParams: { [key: string]: string | undefined };
Expand Down
90 changes: 56 additions & 34 deletions components/BookmarksTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,45 +14,67 @@ async function BookmarksTable({ searchQuery }: BookmarksTableProps) {
const session = await getServerSession(authOptions);

const occurrenceBookmarks = await getOccurrenceBookmarks(session?.user?.id, searchQuery);
// Group by project name
const groupedBookmarks = occurrenceBookmarks.reduce((acc, bookmark) => {
const projectName = bookmark.occurrence.notice.project.name;
if (!acc[projectName]) {
acc[projectName] = [];
}
acc[projectName].push(bookmark);
return acc;
}, {} as Record<string, (typeof occurrenceBookmarks)[0][]>);

// Convert the grouped object into an array that can be mapped over
const groupedBookmarksArray = Object.entries(groupedBookmarks);
groupedBookmarksArray.sort(([projectNameA], [projectNameB]) => projectNameA.localeCompare(projectNameB));

return (
<ul role="list" className="divide-y divide-white/5">
{occurrenceBookmarks.map((bookmark) => (
<li
key={bookmark.occurrence.id}
className="relative flex items-center space-x-4 px-4 py-4 transition-colors duration-200 hover:bg-airbroke-800 sm:px-6 lg:px-8"
>
<div className="min-w-0 flex-auto">
<div className="flex items-center gap-x-3">
<h2 className="min-w-0 text-sm font-semibold leading-6 text-white">
<Link href={`/occurrences/${bookmark.occurrence.id}`} className="flex gap-x-2">
<span className="truncate">{bookmark.occurrence.message}</span>
<span className="absolute inset-0" />
</Link>
</h2>
</div>
<div className="mt-3 flex items-center gap-x-2.5 text-xs leading-5 text-gray-400">
<div className="flex-none rounded-md bg-gray-900 px-2 py-1 text-xs font-medium text-white ring-1 ring-inset ring-gray-700">
{bookmark.occurrence.notice.kind}
</div>
<EnvironmentLabel env={bookmark.occurrence.notice.env} />

<p className="truncate">
First seen: <CustomTimeAgo datetime={bookmark.occurrence.created_at} locale="en_US" />
</p>
</div>
<div>
{groupedBookmarksArray.map(([projectName, bookmarks]) => (
<div key={projectName}>
<div className="bg-airbroke-800 px-4 py-2 text-sm font-semibold text-white transition-colors duration-200 hover:bg-airbroke-700">
{projectName}
</div>
<ul role="list" className="divide-y divide-white/5">
{bookmarks.map((bookmark) => (
<li
key={bookmark.occurrence.id}
className="relative flex items-center space-x-4 px-4 py-4 transition-colors duration-200 hover:bg-airbroke-800 sm:px-6 lg:px-8"
>
<div className="min-w-0 flex-auto">
<div className="flex items-center gap-x-3">
<h2 className="min-w-0 text-sm font-semibold leading-6 text-white">
<Link href={`/occurrences/${bookmark.occurrence.id}`} className="flex gap-x-2">
<span className="truncate">{bookmark.occurrence.message}</span>
<span className="absolute inset-0" />
</Link>
</h2>
</div>
<div className="mt-3 flex items-center gap-x-2.5 text-xs leading-5 text-gray-400">
<div className="flex-none rounded-md bg-gray-900 px-2 py-1 text-xs font-medium text-white ring-1 ring-inset ring-gray-700">
{bookmark.occurrence.notice.kind}
</div>
<EnvironmentLabel env={bookmark.occurrence.notice.env} />

<div className="text-right text-xs">
<p className="text-white">
<CustomTimeAgo datetime={bookmark.occurrence.updated_at} locale="en_US" />
</p>
<p className="text-xs text-gray-400">{bookmark.occurrence.updated_at.toUTCString()}</p>
</div>
<OccurrenceCounterLabel counter={bookmark.occurrence.seen_count} />
</li>
<p className="truncate">
First seen: <CustomTimeAgo datetime={bookmark.occurrence.created_at} locale="en_US" />
</p>
</div>
</div>

<div className="text-right text-xs">
<p className="text-white">
<CustomTimeAgo datetime={bookmark.occurrence.updated_at} locale="en_US" />
</p>
<p className="text-xs text-gray-400">{bookmark.occurrence.updated_at.toUTCString()}</p>
</div>
<OccurrenceCounterLabel counter={bookmark.occurrence.seen_count} />
</li>
))}
</ul>
</div>
))}
</ul>
</div>
);
}

Expand Down
72 changes: 46 additions & 26 deletions components/ConfirmationDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client';

import { deleteProject, deleteProjectNotices } from '@/app/_actions';
import { Dialog, Transition } from '@headlessui/react';
import { Project } from '@prisma/client';
import { Fragment, useRef, useState, useTransition } from 'react';
Expand All @@ -10,38 +11,54 @@ export default function ConfirmationDialog({
project,
title,
body,
btnTitle,
projectConfirmationAction, // server action imported from _actions and passed down as prop
btnId,
}: {
project: Project;
title?: string;
body?: string;
btnTitle?: string;
projectConfirmationAction: (projectId: string) => Promise<void>;
btnId?: string;
}) {
const [open, setOpen] = useState(false);
const [isPending, startTransition] = useTransition();
const cancelButtonRef = useRef(null);

async function handleDeleteProjectConfirm(project_id: string) {
const handleDeleteProjectConfirm = async (projectId: string) => {
startTransition(async () => {
await projectConfirmationAction(project_id);
await deleteProject(projectId);
setOpen(false);
});
}
};

const handleDeleteProjectNoticesConfirm = async (projectId: string) => {
startTransition(async () => {
await deleteProjectNotices(projectId);
setOpen(false);
});
};

const handleConfirmAction = () => {
if (btnId === 'deleteProject') {
handleDeleteProjectConfirm(project.id);
} else if (btnId === 'deleteAllErrors') {
handleDeleteProjectNoticesConfirm(project.id);
}
};

return (
<>
<button
onClick={() => setOpen(true)}
className={`inline-flex items-center rounded-md bg-red-400/10 px-3 py-2 text-sm font-semibold text-red-400 shadow-sm ring-1 ring-red-400/30 transition-colors duration-200 hover:bg-red-500 hover:text-red-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-red-500`}
>
<VscTrash className="-ml-0.5 mr-1.5 h-5 w-5" aria-hidden="true" />
{btnTitle || 'Delete All Errs'}
<button type="button" onClick={() => setOpen(true)} className="text-indigo-500 hover:text-indigo-700">
{btnId === 'deleteProject' ? 'Delete Project' : 'Delete All Errors'}
</button>

<Transition.Root show={open} as={Fragment}>
<Dialog as="div" className="relative z-50" initialFocus={cancelButtonRef} onClose={setOpen}>
<Dialog
as="div"
static
className="fixed inset-0 overflow-y-auto"
initialFocus={cancelButtonRef}
open={open}
onClose={setOpen}
>
<div className="fixed inset-0 z-10 overflow-y-auto">
<div className="flex min-h-full items-end justify-center p-4 text-center sm:items-center sm:p-0">
<Transition.Child
Expand All @@ -56,36 +73,39 @@ export default function ConfirmationDialog({
<Dialog.Panel className="relative transform overflow-hidden rounded-lg bg-gray-900 px-4 pb-4 pt-5 text-left shadow-xl transition-all sm:my-8 sm:w-full sm:max-w-lg sm:p-6">
<div className="sm:flex sm:items-start">
<div className="mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-rose-600 sm:mx-0 sm:h-10 sm:w-10">
<SlFire className="h-6 w-6 text-red-200" aria-hidden="true" />
<SlFire className="h-6 w-6 text-rose-200" aria-hidden="true" />
</div>
<div className="mt-3 text-center sm:ml-4 sm:mt-0 sm:text-left">
<Dialog.Title as="h3" className="text-base font-semibold leading-6 text-white">
{title || 'Are you sure?'}
{title}
</Dialog.Title>
<div className="mt-2">
<p className="text-sm text-gray-300">
{body || 'All data will be erased, are you sure you want to continue?'}
</p>
<p className="text-sm text-gray-300">{body}</p>
</div>
</div>
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<button
type="button"
className="inline-flex w-full justify-center rounded-md border border-transparent bg-rose-600 px-4 py-2 text-base font-medium text-white shadow-sm hover:bg-rose-700 focus:outline-none focus:ring-2 focus:ring-rose-500 focus:ring-offset-2 sm:ml-3 sm:w-auto sm:text-sm"
onClick={handleConfirmAction}
disabled={isPending}
className="inline-flex w-full justify-center rounded-md bg-rose-600 px-3 py-2 text-sm font-semibold text-white shadow-sm hover:bg-rose-500 sm:ml-3 sm:w-auto"
onClick={() => handleDeleteProjectConfirm(project.id)}
>
{isPending ? (
<SlDisc className="-ml-0.5 mr-1.5 h-5 w-5 animate-spin" aria-hidden="true" />
{btnId === 'deleteProject' ? (
<>
<VscTrash className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
Delete Project
</>
) : (
<VscTrash className="-ml-0.5 mr-1.5 h-5 w-5" aria-hidden="true" />
<>
<SlDisc className="-ml-1 mr-2 h-5 w-5" aria-hidden="true" />
Delete All Errors
</>
)}
<span>{isPending ? 'Deleting...' : 'Proceed'}</span>
</button>
<button
type="button"
className="mt-3 inline-flex w-full justify-center rounded-md bg-gray-800 px-3 py-2 text-sm font-semibold text-white shadow-sm ring-1 ring-inset ring-gray-700 hover:bg-gray-700 sm:mt-0 sm:w-auto"
className="mt-3 inline-flex w-full justify-center rounded-md border border-gray-300 bg-white px-4 py-2 text-base font-medium text-gray-700 shadow-sm hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 sm:mt-0 sm:w-auto sm:text-sm"
onClick={() => setOpen(false)}
ref={cancelButtonRef}
>
Expand Down
2 changes: 1 addition & 1 deletion components/OccurrenceChartWrapper.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import OccurrenceChart from '@/components/OccurrenceChart';
import { prisma } from '@/lib/db';
import OccurrenceChart from './OccurrenceChart';

interface OccurrenceChartWrapperProps {
occurrenceId: string;
Expand Down
4 changes: 1 addition & 3 deletions components/occurrence/Backtrace.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import classNames from '@/lib/classNames';
import { getOccurrenceById } from '@/lib/queries/occurrences';
import { Prisma } from '@prisma/client';
import LinkedBacktraceLine from './BacktraceLine';
// import BookmarkButton from './BookmarkButton';
import { getOccurrenceById } from '@/lib/queries/occurrences';
import ClipboardButton from './ClipboardButton';

interface BacktraceItem {
Expand Down Expand Up @@ -30,7 +29,6 @@ async function Backtrace({ occurrenceId }: BacktraceProps) {
<div className="flex items-center justify-between">
<div className="mb-4 flex items-center gap-x-4">
<ClipboardButton json={occurrence.backtrace} />
{/* <BookmarkButton projectId={project.id} noticeId={occurrence.notice_id} occurrenceId={occurrence.id} /> */}
</div>
</div>
{occurrence.backtrace && typeof occurrence.backtrace === 'object' && Array.isArray(occurrence.backtrace) && (
Expand Down
55 changes: 55 additions & 0 deletions components/project/OccurrencesChartWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import OccurrenceChart from '@/components/OccurrenceChart';
import { prisma } from '@/lib/db';

interface OccurrencesChartWrapperProps {
occurrenceIds: string[];
}

async function OccurrencesChartWrapper({ occurrenceIds }: OccurrencesChartWrapperProps) {
// Calculate the start and end date for the past two weeks
const endDate = new Date();
const startDate = new Date();
startDate.setDate(endDate.getDate() - 14);

// Query the database for the occurrence summaries for the past two weeks
const occurrenceSummaries = await prisma.hourlyOccurrence.groupBy({
by: ['interval_start'],
where: {
occurrence_id: {
in: occurrenceIds,
},
interval_start: {
gte: startDate.toISOString(),
},
interval_end: {
lte: endDate.toISOString(),
},
},
_sum: {
count: true,
},
orderBy: {
interval_start: 'asc',
},
});

// Map the occurrence summaries to the format expected by the chart component
const chartData = occurrenceSummaries.map((summary) => {
return {
date: summary.interval_start.toISOString().slice(0, 13), // Get date and hour only
count: Number(summary._sum.count),
};
});

return (
<div className="px-4 sm:px-6 lg:px-8">
<h2 className="mb-6 min-w-0 text-sm font-semibold leading-6 text-white">
Hourly Occurrences in the past 14 days
</h2>

<OccurrenceChart data={chartData} />
</div>
);
}

export default OccurrencesChartWrapper as unknown as (props: OccurrencesChartWrapperProps) => JSX.Element;
Loading

0 comments on commit b0d1b5a

Please sign in to comment.