Move people page to server components (#479)

* feat: person overview is now a server rendered page

* feat: loader component & fix: minor changes as suggested

* hide: session count

* getAttributeValue always returns string

* fix: remove createdAt & updatedAt fields from user for now

* fix: use select instead of include to specify retreival fields

* feat: suspense streaming

* feat: skeleton table for streaming and loading

* fix: use integrated loading and cleanup components

---------

Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
Shubham Palriwala
2023-07-05 16:36:37 +05:30
committed by GitHub
parent 419b9d0b90
commit dbec5426b2
4 changed files with 126 additions and 85 deletions

View File

@@ -1,78 +0,0 @@
"use client";
import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller";
import LoadingSpinner from "@/components/shared/LoadingSpinner";
import { usePeople } from "@/lib/people/people";
import { truncateMiddle } from "@/lib/utils";
import { ErrorComponent, PersonAvatar } from "@formbricks/ui";
import Link from "next/link";
export default function PeopleList({ environmentId }: { environmentId: string }) {
const { people, isLoadingPeople, isErrorPeople } = usePeople(environmentId);
if (isLoadingPeople) {
return <LoadingSpinner />;
}
if (isErrorPeople) {
return <ErrorComponent />;
}
const getAttributeValue = (person, attributeName) => {
return person.attributes.find((a) => a.attributeClass.name === attributeName)?.value;
};
return (
<>
{people.length === 0 ? (
<EmptySpaceFiller type="table" environmentId={environmentId} />
) : (
<div className="rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-7 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<div className="col-span-3 pl-6 ">User</div>
<div className="col-span-2 text-center">User ID</div>
<div className="text-center">Email</div>
<div className="text-center">Sessions</div>
</div>
<div className="grid-cols-7">
{people.map((person) => (
<Link
href={`/environments/${environmentId}/people/${person.id}`}
key={person.id}
className="w-full">
<div className="m-2 grid h-16 grid-cols-7 content-center rounded-lg hover:bg-slate-100">
<div className="col-span-3 flex items-center pl-6 text-sm">
<div className="flex items-center">
<div className="ph-no-capture h-10 w-10 flex-shrink-0">
<PersonAvatar personId={person.id} />
</div>
<div className="ml-4">
<div className="ph-no-capture font-medium text-slate-900">
{getAttributeValue(person, "email") ? (
<span>{getAttributeValue(person, "email")}</span>
) : (
<span>{person.id}</span>
)}
</div>
</div>
</div>
</div>
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="ph-no-capture text-slate-900">
{truncateMiddle(getAttributeValue(person, "userId"), 24)}
</div>
</div>
<div className="ph-no-capture my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="text-slate-900">{getAttributeValue(person, "email")}</div>
</div>
<div className="ph-no-capture my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="text-slate-900">{person._count?.sessions}</div>
</div>
</div>
</Link>
))}
</div>
</div>
)}
</>
);
}

View File

@@ -0,0 +1,34 @@
export default function Loading() {
return (
<div className="rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-7 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<div className="col-span-3 pl-6">User</div>
<div className="col-span-2 text-center">User ID</div>
<div className="col-span-2 text-center">Email</div>
</div>
<div className="w-full">
{[...Array(3)].map((_, index) => (
<div
key={index}
className="m-2 grid h-16 grid-cols-7 content-center rounded-lg hover:bg-slate-100"
>
<div className="col-span-3 flex items-center pl-6 text-sm">
<div className="flex items-center">
<div className="ph-no-capture h-10 w-10 flex-shrink-0 animate-pulse bg-gray-200 rounded-full"></div>
<div className="ml-4">
<div className="ph-no-capture font-medium text-slate-900 animate-pulse bg-gray-200 rounded-full w-28 h-4"></div>
</div>
</div>
</div>
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="ph-no-capture text-slate-900 animate-pulse bg-gray-200 rounded-full m-12 h-4"></div>
</div>
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="ph-no-capture text-slate-900 animate-pulse bg-gray-200 rounded-full m-12 h-4"></div>
</div>
</div>
))}
</div>
</div>
);
}

View File

@@ -1,13 +1,62 @@
import PeopleList from "./PeopleList";
export const revalidate = 0;
import EmptySpaceFiller from "@/components/shared/EmptySpaceFiller";
import { truncateMiddle } from "@/lib/utils";
import { TransformPersonOutput, getPeople } from "@formbricks/lib/services/person";
import { PersonAvatar } from "@formbricks/ui";
const getAttributeValue = (person: TransformPersonOutput, attributeName: string) =>
person.attributes[attributeName]?.toString();
export default async function PeoplePage({ params }) {
const people = await getPeople();
export default function PeoplePage({ params }) {
return (
<>
<h1 className="my-2 text-3xl font-bold text-slate-800">People</h1>
<p className="mb-6 text-slate-500">
A list of all people who used your application since embedding the Formbricks JS widget.
</p>
<PeopleList environmentId={params.environmentId} />
{people.length === 0 ? (
<EmptySpaceFiller type="table" environmentId={params.environmentId} />
) : (
<div className="rounded-lg border border-slate-200">
<div className="grid h-12 grid-cols-7 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
<div className="col-span-3 pl-6 ">User</div>
<div className="col-span-2 text-center">User ID</div>
<div className="col-span-2 text-center">Email</div>
</div>
{people.map((person) => (
<a
href={`/environments/${params.environmentId}/people/${person.id}`}
key={person.id}
className="w-full">
<div className="m-2 grid h-16 grid-cols-7 content-center rounded-lg hover:bg-slate-100">
<div className="col-span-3 flex items-center pl-6 text-sm">
<div className="flex items-center">
<div className="ph-no-capture h-10 w-10 flex-shrink-0">
<PersonAvatar personId={person.id} />
</div>
<div className="ml-4">
<div className="ph-no-capture font-medium text-slate-900">
{getAttributeValue(person, "email") ? (
<span>{getAttributeValue(person, "email")}</span>
) : (
<span>{person.id}</span>
)}
</div>
</div>
</div>
</div>
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="ph-no-capture text-slate-900">
{truncateMiddle(getAttributeValue(person, "userId"), 24)}
</div>
</div>
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="ph-no-capture text-slate-900">{getAttributeValue(person, "email")}</div>
</div>
</div>
</a>
))}
</div>
)}
</>
);
}

View File

@@ -2,6 +2,7 @@ import { prisma } from "@formbricks/database";
import { TPerson } from "@formbricks/types/v1/people";
import { Prisma } from "@prisma/client";
import { DatabaseError, ResourceNotFoundError } from "@formbricks/errors";
import { cache } from "react";
type TransformPersonInput = {
id: string;
@@ -68,3 +69,38 @@ export const getPerson = async (personId: string): Promise<TPerson | null> => {
throw error;
}
};
export const getPeople = cache(async (): Promise<TPerson[]> => {
try {
const personsPrisma = await prisma.person.findMany({
select: {
id: true,
attributes: {
select: {
value: true,
attributeClass: {
select: {
name: true,
},
},
},
},
},
});
if (!personsPrisma) {
throw new ResourceNotFoundError("Persons", "All Persons");
}
const transformedPersons: TransformPersonOutput[] = personsPrisma
.map(transformPrismaPerson)
.filter((person: TransformPersonOutput | null): person is TransformPersonOutput => person !== null);
return transformedPersons;
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError("Database operation failed");
}
throw error;
}
});