feat: add lighthouse reporter generate in website

This commit is contained in:
moonrailgun 2024-09-22 00:14:33 +08:00
parent fb75a8b654
commit d29785a311
6 changed files with 229 additions and 22 deletions

View File

@ -0,0 +1,178 @@
import React, { useMemo, useState } from 'react';
import { Button } from '../ui/button';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from '../ui/dialog';
import { useTranslation } from '@i18next-toolkit/react';
import { TbBuildingLighthouse } from 'react-icons/tb';
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
SheetTrigger,
} from '../ui/sheet';
import { defaultErrorHandler, defaultSuccessHandler, trpc } from '@/api/trpc';
import { useCurrentWorkspaceId } from '@/store/user';
import { formatDate } from '@/utils/date';
import { Input } from '../ui/input';
import { toast } from 'sonner';
import { useEvent } from '@/hooks/useEvent';
import { Badge } from '../ui/badge';
import { LuArrowRight, LuPlus } from 'react-icons/lu';
interface WebsiteLighthouseBtnProps {
websiteId: string;
}
export const WebsiteLighthouseBtn: React.FC<WebsiteLighthouseBtnProps> =
React.memo((props) => {
const workspaceId = useCurrentWorkspaceId();
const { t } = useTranslation();
const [url, setUrl] = useState('');
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
const { data, hasNextPage, fetchNextPage, isFetchingNextPage, refetch } =
trpc.website.getLighthouseReport.useInfiniteQuery(
{
workspaceId,
websiteId: props.websiteId,
},
{
getNextPageParam: (lastPage) => lastPage.nextCursor,
}
);
const createMutation = trpc.website.generateLighthouseReport.useMutation({
onSuccess: defaultSuccessHandler,
onError: defaultErrorHandler,
});
const allData = useMemo(() => {
if (!data) {
return [];
}
return [...data.pages.flatMap((p) => p.items)];
}, [data]);
const handleGenerateReport = useEvent(async () => {
if (!url) {
toast.error(t('Url is required'));
return;
}
await createMutation.mutateAsync({
workspaceId,
websiteId: props.websiteId,
url,
});
setIsCreateDialogOpen(false);
refetch();
});
return (
<Sheet>
<SheetTrigger>
<Button variant="outline" size="icon" Icon={TbBuildingLighthouse} />
</SheetTrigger>
<SheetContent>
<SheetHeader>
<SheetTitle>{t('Website Lighthouse Reports')}</SheetTitle>
<SheetDescription>
{t(
'Lighthouse is an open-source, automated tool developed by Google, designed to evaluate the quality of web applications.'
)}
</SheetDescription>
</SheetHeader>
<div className="mt-2 flex flex-col gap-2">
<div>
<Dialog
open={isCreateDialogOpen}
onOpenChange={setIsCreateDialogOpen}
>
<DialogTrigger>
<Button
variant="outline"
loading={createMutation.isLoading}
Icon={LuPlus}
>
{t('Create Report')}
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>{t('Generate Lighthouse Report')}</DialogTitle>
<DialogDescription>
{t('Its will take a while to generate the report.')}
</DialogDescription>
</DialogHeader>
<div>
<Input
value={url}
onChange={(e) => setUrl(e.target.value)}
placeholder="https://google.com"
/>
</div>
<DialogFooter>
<Button
loading={createMutation.isLoading}
onClick={handleGenerateReport}
>
{t('Create')}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
<div className="flex flex-col gap-2">
{allData.map((report) => {
return (
<div className="border-border flex items-start gap-2 rounded-lg border p-2">
<Badge>{report.status}</Badge>
<div className="flex-1 overflow-hidden">
<div className="text-base">{report.url}</div>
<div className="text-sm opacity-50">
{formatDate(report.createdAt)}
</div>
</div>
{report.status === 'Success' && (
<Button
variant="outline"
size="icon"
Icon={LuArrowRight}
onClick={() => window.open(`/lh/${report.id}/html`)}
/>
)}
</div>
);
})}
</div>
{hasNextPage && (
<Button
variant="outline"
size="icon"
loading={isFetchingNextPage}
onClick={() => fetchNextPage()}
>
{t('Load More')}
</Button>
)}
</div>
</SheetContent>
</Sheet>
);
});
WebsiteLighthouseBtn.displayName = 'WebsiteLighthouseBtn';

View File

@ -7,6 +7,7 @@ import { NotFoundTip } from '@/components/NotFoundTip';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'; import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
import { WebsiteCodeBtn } from '@/components/website/WebsiteCodeBtn'; import { WebsiteCodeBtn } from '@/components/website/WebsiteCodeBtn';
import { WebsiteLighthouseBtn } from '@/components/website/WebsiteLighthouseBtn';
import { WebsiteMetricsTable } from '@/components/website/WebsiteMetricsTable'; import { WebsiteMetricsTable } from '@/components/website/WebsiteMetricsTable';
import { WebsiteOverview } from '@/components/website/WebsiteOverview'; import { WebsiteOverview } from '@/components/website/WebsiteOverview';
import { WebsiteVisitorMapBtn } from '@/components/website/WebsiteVisitorMapBtn'; import { WebsiteVisitorMapBtn } from '@/components/website/WebsiteVisitorMapBtn';
@ -70,6 +71,9 @@ function WebsiteDetailComponent() {
> >
<LuSettings /> <LuSettings />
</Button> </Button>
<WebsiteLighthouseBtn websiteId={website.id} />
<WebsiteCodeBtn websiteId={website.id} /> <WebsiteCodeBtn websiteId={website.id} />
</div> </div>
} }

View File

@ -34,6 +34,9 @@ export default defineConfig({
'/trpc': { '/trpc': {
target: 'http://localhost:12345', target: 'http://localhost:12345',
}, },
'/lh': {
target: 'http://localhost:12345',
},
'/api/auth/': { '/api/auth/': {
target: 'http://localhost:12345', target: 'http://localhost:12345',
}, },

View File

@ -337,8 +337,7 @@ export const surveyRouter = router({
) )
.output(buildCursorResponseSchema(SurveyResultModelSchema)) .output(buildCursorResponseSchema(SurveyResultModelSchema))
.query(async ({ input }) => { .query(async ({ input }) => {
const limit = input.limit; const { cursor, surveyId, limit } = input;
const { cursor, surveyId } = input;
const where: Prisma.SurveyResultWhereInput = { const where: Prisma.SurveyResultWhereInput = {
surveyId, surveyId,

View File

@ -32,11 +32,11 @@ import {
websiteStatsSchema, websiteStatsSchema,
} from '../../model/_schema/filter.js'; } from '../../model/_schema/filter.js';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import { WebsiteQueryFilters } from '../../utils/prisma.js'; import { fetchDataByCursor, WebsiteQueryFilters } from '../../utils/prisma.js';
import { WebsiteLighthouseReportStatus } from '@prisma/client'; import { WebsiteLighthouseReportStatus } from '@prisma/client';
import { generateLighthouse } from '../../utils/screenshot/lighthouse.js'; import { generateLighthouse } from '../../utils/screenshot/lighthouse.js';
import { WebsiteLighthouseReportModelSchema } from '../../prisma/zod/websitelighthousereport.js'; import { WebsiteLighthouseReportModelSchema } from '../../prisma/zod/websitelighthousereport.js';
import { method } from 'lodash-es'; import { buildCursorResponseSchema } from '../../utils/schema.js';
const websiteNameSchema = z.string().max(100); const websiteNameSchema = z.string().max(100);
const websiteDomainSchema = z.union([ const websiteDomainSchema = z.union([
@ -644,36 +644,44 @@ export const websiteRouter = router({
.input( .input(
z.object({ z.object({
websiteId: z.string().cuid2(), websiteId: z.string().cuid2(),
limit: z.number().min(1).max(100).default(10),
cursor: z.string().optional(),
}) })
) )
.output( .output(
z.array( buildCursorResponseSchema(
WebsiteLighthouseReportModelSchema.pick({ WebsiteLighthouseReportModelSchema.pick({
id: true, id: true,
status: true, status: true,
url: true,
createdAt: true, createdAt: true,
}) })
) )
) )
.query(async ({ input }) => { .query(async ({ input }) => {
const { websiteId } = input; const { websiteId, limit, cursor } = input;
const list = await prisma.websiteLighthouseReport.findMany({ const { items, nextCursor } = await fetchDataByCursor(
prisma.websiteLighthouseReport,
{
where: { where: {
websiteId, websiteId,
}, },
take: 10,
orderBy: {
createdAt: 'desc',
},
select: { select: {
id: true, id: true,
status: true, status: true,
url: true,
createdAt: true, createdAt: true,
}, },
}); limit,
cursor,
}
);
return list; return {
items,
nextCursor,
};
}), }),
getLighthouseJSON: publicProcedure getLighthouseJSON: publicProcedure
.meta({ .meta({

View File

@ -226,6 +226,12 @@ type ExtractFindManyWhereType<
}, },
> = NonNullable<Parameters<T['findMany']>[0]>['where']; > = NonNullable<Parameters<T['findMany']>[0]>['where'];
type ExtractFindManySelectType<
T extends {
findMany: (args?: any) => Prisma.PrismaPromise<any>;
},
> = NonNullable<Parameters<T['findMany']>[0]>['select'];
/** /**
* @example * @example
* const { items, nextCursor } = await fetchDataByCursor( * const { items, nextCursor } = await fetchDataByCursor(
@ -249,16 +255,25 @@ export async function fetchDataByCursor<
options: { options: {
// where: Record<string, any>; // where: Record<string, any>;
where: ExtractFindManyWhereType<Model>; where: ExtractFindManyWhereType<Model>;
select?: ExtractFindManySelectType<Model>;
limit: number; limit: number;
cursor: CursorType; cursor: CursorType;
cursorName?: string; cursorName?: string;
order?: 'asc' | 'desc'; order?: 'asc' | 'desc';
} }
) { ) {
const { where, limit, cursor, cursorName = 'id', order = 'desc' } = options; const {
where,
limit,
cursor,
select,
cursorName = 'id',
order = 'desc',
} = options;
const items: ExtractFindManyReturnType<Model['findMany']> = const items: ExtractFindManyReturnType<Model['findMany']> =
await fetchModel.findMany({ await fetchModel.findMany({
where, where,
select,
take: limit + 1, take: limit + 1,
cursor: cursor cursor: cursor
? { ? {