Skip to content

Commit

Permalink
refactor: replace usage of prisma queries with custom queries in seve…
Browse files Browse the repository at this point in the history
…ral components to improve performance and maintainability

feat: add icons to project edit page tabs to improve UX and make it easier to identify each tab

fix: add type import for SortAttribute and SortDirection in project page component to avoid type errors

feat: add support for sending test exceptions to Airbrake JS and Airbrake Node

fix: change error message in Airbrake JS test exception to match convention

refactor: change occurrences table to use getOccurrences query function

refactor: change projects table to remove unused import

refactor: change sidebar desktop to use selectedProjectId instead of selectedProject

feat: add getProjectById query function to fetch a single project by ID

refactor: change overview component to use getProjectById query function

fix: remove unused import in airbrakeActions

refactor: remove unused server-only import in notices query file

feat(occurrences.ts): add function to fetch occurrences based on provided search parameters

feat(occurrences.ts): add cached function to fetch a single occurrence by ID

refactor(projects.ts): remove unnecessary import statement
  • Loading branch information
masterkain committed May 27, 2023
1 parent 8216b8d commit 78adf7a
Show file tree
Hide file tree
Showing 13 changed files with 229 additions and 123 deletions.
81 changes: 41 additions & 40 deletions app/notices/[notice_id]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,49 @@
import Breadcrumbs from '@/components/Breadcrumbs';
import NoData from '@/components/NoData';
import OccurrencesTable from '@/components/OccurrencesTable';
import Search from '@/components/Search';
import SidebarDesktop from '@/components/SidebarDesktop';
import SidebarMobile from '@/components/SidebarMobile';
import ProjectActionsMenu from '@/components/project/ActionsMenu';
import { prisma } from '@/lib/db';
import { getNoticeById } from '@/lib/queries/notices';
import type { SortAttribute, SortDirection } from '@/lib/queries/occurrences';
import { getProjectById } from '@/lib/queries/projects';
import type { Route } from 'next';
import { Metadata } from 'next';

export const revalidate = 60;

export default async function Notice({
params,
searchParams,
}: {
type ComponentProps = {
params: { notice_id: string };
searchParams: Record<string, string>;
}) {
const notice = await prisma.notice.findFirst({
where: { id: params.notice_id },
include: {
project: true,
},
});
searchParams: { [key: string]: string | undefined };
};

export async function generateMetadata({ params }: ComponentProps): Promise<Metadata> {
try {
const notice = await getNoticeById(params.notice_id);
const project = notice && (await getProjectById(notice.project_id));
return { title: `(${project?.name}) ${notice?.kind}` };
} catch (error) {
console.error(error);
return { title: '' };
}
}

// /notices/:notice_id
export default async function Notice({ params, searchParams }: ComponentProps) {
const notice = await getNoticeById(params.notice_id);
if (!notice) {
throw new Error('Notice not found');
}
const search = searchParams.q;
const whereObject: any = {
notice_id: notice.id,
...(search && { message: { contains: search, mode: 'insensitive' } }),
};

const occurrences = await prisma.occurrence.findMany({
where: whereObject,
orderBy: { updated_at: 'desc' },
take: 100,
select: { id: true },
});
const occurrencesIds = occurrences.map((occurrence) => occurrence.id);
const project = await getProjectById(notice.project_id);
if (!project) {
throw new Error('Project not found');
}

const { sortDir, sortAttr, searchQuery } = searchParams;

const breadcrumbs = [
{
name: `${notice.project.organization.toLowerCase()} / ${notice.project.name.toLowerCase()}`,
href: `/projects/${notice.project.id}` as Route,
name: `${project.organization.toLowerCase()} / ${project.name.toLowerCase()}`,
href: `/projects/${project.id}` as Route,
current: false,
},
{ name: notice.kind, href: `/notices/${notice.id}` as Route, current: true },
Expand All @@ -54,36 +54,37 @@ export default async function Notice({
<div>
<SidebarMobile>
{/* @ts-expect-error Server Component */}
<SidebarDesktop selectedProject={notice.project} />
<SidebarDesktop selectedProjectId={project.id} />
</SidebarMobile>

<div className="hidden xl:fixed xl:inset-y-0 xl:z-50 xl:flex xl:w-72 xl:flex-col">
{/* @ts-expect-error Server Component */}
<SidebarDesktop selectedProject={notice.project} />
<SidebarDesktop selectedProjectId={project.id} />
</div>

<main className="xl:pl-72">
<div className="sticky top-0 z-40 bg-airbroke-900">
<nav className="border-b border-white border-opacity-10 bg-gradient-to-r from-airbroke-800 to-airbroke-900">
<div className="flex justify-between pr-4 sm:pr-6 lg:pr-6">
<Breadcrumbs breadcrumbs={breadcrumbs} />
<ProjectActionsMenu project={notice.project} />
<ProjectActionsMenu project={project} />
</div>
</nav>

<div className="flex h-16 shrink-0 items-center gap-x-6 border-b border-white/5 px-4 shadow-sm sm:px-6 lg:px-8">
<div className="flex flex-1 gap-x-4 self-stretch lg:gap-x-6">
<Search currentSearchTerm={search} />
<Search currentSearchTerm={searchQuery} />
</div>
</div>
</div>

{occurrencesIds.length === 0 ? (
<NoData project={notice.project} />
) : (
/* @ts-expect-error Server Component */
<OccurrencesTable occurrencesIds={occurrencesIds} />
)}
{/* @ts-expect-error Server Component */}
<OccurrencesTable
noticeId={notice.id}
sortDir={sortDir as SortDirection}
sortAttr={sortAttr as SortAttribute}
searchQuery={searchQuery}
/>
</main>
</div>
</>
Expand Down
4 changes: 2 additions & 2 deletions app/occurrences/[occurrence_id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,12 +84,12 @@ export default async function Occurrence({
<div>
<SidebarMobile>
{/* @ts-expect-error Server Component */}
<SidebarDesktop selectedProject={project} />
<SidebarDesktop selectedProjectId={project.id} />
</SidebarMobile>

<div className="hidden xl:fixed xl:inset-y-0 xl:z-50 xl:flex xl:w-72 xl:flex-col">
{/* @ts-expect-error Server Component */}
<SidebarDesktop selectedProject={project} />
<SidebarDesktop selectedProjectId={project.id} />
</div>
<main className="xl:pl-72">
<div className="sticky top-0 z-40 bg-airbroke-900">
Expand Down
88 changes: 61 additions & 27 deletions app/projects/[project_id]/edit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import SidebarDesktop from '@/components/SidebarDesktop';
import SidebarMobile from '@/components/SidebarMobile';
import ProjectActionsMenu from '@/components/project/ActionsMenu';
import Overview from '@/components/project/Overview';
import classNames from '@/lib/classNames';
import { jsclientTemplate, rubyTemplate } from '@/lib/configTemplates';
import { getProjectById } from '@/lib/queries/projects';
import type { Route } from 'next';
import Link from 'next/link';
import { SlSettings, SlWrench } from 'react-icons/sl';

export default async function Project({
params,
Expand All @@ -16,7 +18,7 @@ export default async function Project({
params: { project_id: string };
searchParams: Record<string, string>;
}) {
const tab = searchParams.tab ?? 'overview';
const currentTab = searchParams.tab ?? 'overview';

const project = await getProjectById(params.project_id);
if (!project) {
Expand All @@ -27,8 +29,8 @@ export default async function Project({
};

const tabs = [
{ id: 'overview', name: 'Overview', current: tab === 'overview' },
{ id: 'integrations', name: 'Integrations', current: tab === 'integrations' },
{ id: 'overview', name: 'Overview', current: currentTab === 'overview', icon: SlSettings },
{ id: 'integrations', name: 'Integrations', current: currentTab === 'integrations', icon: SlWrench },
].map((tab) => ({
...tab,
href: `/projects/${project.id}/edit?tab=${tab.id}` as Route,
Expand All @@ -52,12 +54,12 @@ export default async function Project({
<div>
<SidebarMobile>
{/* @ts-expect-error Server Component */}
<SidebarDesktop selectedProject={project} />
<SidebarDesktop selectedProjectId={project.id} />
</SidebarMobile>

<div className="hidden xl:fixed xl:inset-y-0 xl:z-50 xl:flex xl:w-72 xl:flex-col">
{/* @ts-expect-error Server Component */}
<SidebarDesktop selectedProject={project} />
<SidebarDesktop selectedProjectId={project.id} />
</div>

<main className="xl:pl-72">
Expand All @@ -68,30 +70,62 @@ export default async function Project({
<ProjectActionsMenu project={project} />
</div>
</nav>
<nav className="flex overflow-x-auto border-b border-white/10 py-4">
<ul
role="list"
className="flex min-w-full flex-none gap-x-6 px-4 text-sm font-semibold leading-6 text-gray-400 sm:px-6 lg:px-8"
>
{tabs.map((item) => (
<li key={item.name}>
<Link href={item.href} className={item.current ? 'text-indigo-400' : ''}>
{item.name}
</Link>
</li>
))}
</ul>
</nav>
</div>

<div className="mt-2 block max-w-full overflow-x-auto rounded-md p-4 text-xs">
{tab === 'overview' && <Overview project={project} />}
{tab === 'integrations' && (
<>
<CodeTemplate code={rubyTemplate} replacements={replacements} name="Ruby / Rails" />
<CodeTemplate code={jsclientTemplate} replacements={replacements} name="JavaScript (Browser)" />
</>
)}
<div className="pb-8">
<div className="px-4 pb-4 sm:px-6 lg:px-8">
<div className="sm:hidden">
<label htmlFor="tabs" className="sr-only">
Select a tab
</label>
<select
name="tabs"
className="block w-full rounded-md border-gray-300 focus:border-indigo-500 focus:ring-indigo-500"
defaultValue={currentTab}
>
{tabs.map((tab) => (
<option key={tab.name}>{tab.name}</option>
))}
</select>
</div>
<div className="hidden sm:block">
<div className="border-b border-white/10">
<nav className="-mb-px flex space-x-8" aria-label="Tabs">
{tabs.map((tab) => (
<Link
key={tab.id}
href={tab.href}
className={classNames(
tab.current
? 'border-indigo-500 text-indigo-400 '
: 'border-transparent text-indigo-200 hover:border-indigo-500 hover:text-indigo-400',
'group inline-flex items-center border-b-2 px-1 py-4 text-sm font-medium'
)}
aria-current={tab.current ? 'page' : undefined}
>
<tab.icon
className={classNames(
tab.current ? 'text-indigo-400' : 'text-indigo-200 group-hover:text-indigo-400',
'-ml-0.5 mr-2 h-5 w-5'
)}
aria-hidden="true"
/>
<span>{tab.name}</span>
</Link>
))}
</nav>
</div>
</div>
</div>

{/* @ts-expect-error Server Component */}
{currentTab === 'overview' && <Overview projectId={project.id} />}
{currentTab === 'integrations' && (
<>
<CodeTemplate code={rubyTemplate} replacements={replacements} name="Ruby / Rails" />
<CodeTemplate code={jsclientTemplate} replacements={replacements} name="JavaScript (Browser)" />
</>
)}
</div>
</main>
</div>
Expand Down
6 changes: 3 additions & 3 deletions app/projects/[project_id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import Search from '@/components/Search';
import SidebarDesktop from '@/components/SidebarDesktop';
import SidebarMobile from '@/components/SidebarMobile';
import ProjectActionsMenu from '@/components/project/ActionsMenu';
import { SortAttribute, SortDirection } from '@/lib/queries/notices';
import type { SortAttribute, SortDirection } from '@/lib/queries/notices';
import { getProjectById } from '@/lib/queries/projects';
import type { Route } from 'next';
import { Metadata } from 'next';
Expand Down Expand Up @@ -43,12 +43,12 @@ export default async function ProjectNotices({ params, searchParams }: Component
<div>
<SidebarMobile>
{/* @ts-expect-error Server Component */}
<SidebarDesktop selectedProject={project} />
<SidebarDesktop selectedProjectId={project.id} />
</SidebarMobile>

<div className="hidden xl:fixed xl:inset-y-0 xl:z-50 xl:flex xl:w-72 xl:flex-col">
{/* @ts-expect-error Server Component */}
<SidebarDesktop selectedProject={project} />
<SidebarDesktop selectedProjectId={project.id} />
</div>

<main className="xl:pl-72">
Expand Down
60 changes: 32 additions & 28 deletions components/NoData.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export default function NoData({ project, showHeader = true }: { project: Projec
queryStats: false,
});

await airbrake.notify(new Error('This is a test exception from Airbroke'));
await airbrake.notify(new Error('[AirbrakeJs] This is a test exception from Airbroke'));
refresh();
};

Expand All @@ -35,33 +35,37 @@ export default function NoData({ project, showHeader = true }: { project: Projec
</>
)}

<div className="flex flex-col items-center">
<button
onClick={() => startTransition(() => sendAirbrakeJsException())}
disabled={isPending}
className="mb-4 inline-flex items-center rounded-md bg-indigo-400/10 px-3 py-2 text-sm font-semibold text-indigo-400 shadow-sm ring-indigo-400/30 transition-colors duration-200 hover:bg-indigo-500 hover:text-indigo-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:text-white"
>
{isPending ? (
<SlDisc className="-ml-0.5 mr-1.5 h-5 w-5 animate-spin" aria-hidden="true" />
) : (
<SlEnergy className="-ml-0.5 mr-1.5 h-5 w-5" aria-hidden="true" />
)}

<span>{isPending ? 'Sending...' : 'Send a test exception (Airbrake JS)'}</span>
</button>
<button
onClick={() => startTransition(() => sendAirbrakeNodeException(project.id, window.location.origin))}
disabled={isPending}
className="inline-flex items-center rounded-md bg-indigo-400/10 px-3 py-2 text-sm font-semibold text-indigo-400 shadow-sm ring-indigo-400/30 transition-colors duration-200 hover:bg-indigo-500 hover:text-indigo-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:text-white"
>
{isPending ? (
<SlDisc className="-ml-0.5 mr-1.5 h-5 w-5 animate-spin" aria-hidden="true" />
) : (
<SlEnergy className="-ml-0.5 mr-1.5 h-5 w-5" aria-hidden="true" />
)}

<span>{isPending ? 'Sending...' : 'Send a test exception (Airbrake Node)'}</span>
</button>
<div className="grid grid-cols-2 justify-items-center gap-4">
<div className="rounded-lg bg-gray-900 p-6 shadow-lg">
<h2 className="mb-2 text-lg font-bold text-indigo-500">Airbrake JS</h2>
<button
onClick={() => startTransition(() => sendAirbrakeJsException())}
disabled={isPending}
className="inline-flex w-full items-center justify-center rounded-md bg-indigo-400/10 px-3 py-2 text-sm font-semibold text-indigo-400 shadow-sm ring-indigo-400/30 transition-colors duration-200 hover:bg-indigo-500 hover:text-indigo-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:text-white"
>
{isPending ? (
<SlDisc className="-ml-0.5 mr-1.5 h-5 w-5 animate-spin" aria-hidden="true" />
) : (
<SlEnergy className="-ml-0.5 mr-1.5 h-5 w-5" aria-hidden="true" />
)}
<span>{isPending ? 'Sending...' : 'Send a test exception'}</span>
</button>
</div>
<div className="rounded-lg bg-gray-900 p-6 shadow-lg">
<h2 className="mb-2 text-lg font-bold text-indigo-500">Airbrake Node</h2>
<button
onClick={() => startTransition(() => sendAirbrakeNodeException(project.id, window.location.origin))}
disabled={isPending}
className="inline-flex w-full items-center justify-center rounded-md bg-indigo-400/10 px-3 py-2 text-sm font-semibold text-indigo-400 shadow-sm ring-indigo-400/30 transition-colors duration-200 hover:bg-indigo-500 hover:text-indigo-200 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-500 disabled:text-white"
>
{isPending ? (
<SlDisc className="-ml-0.5 mr-1.5 h-5 w-5 animate-spin" aria-hidden="true" />
) : (
<SlEnergy className="-ml-0.5 mr-1.5 h-5 w-5" aria-hidden="true" />
)}
<span>{isPending ? 'Sending...' : 'Send a test exception'}</span>
</button>
</div>
</div>
</div>
);
Expand Down
Loading

0 comments on commit 78adf7a

Please sign in to comment.