feat: add lighthouse reporter generate in website
This commit is contained in:
parent
fb75a8b654
commit
d29785a311
178
src/client/components/website/WebsiteLighthouseBtn.tsx
Normal file
178
src/client/components/website/WebsiteLighthouseBtn.tsx
Normal 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';
|
@ -7,6 +7,7 @@ import { NotFoundTip } from '@/components/NotFoundTip';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area';
|
||||
import { WebsiteCodeBtn } from '@/components/website/WebsiteCodeBtn';
|
||||
import { WebsiteLighthouseBtn } from '@/components/website/WebsiteLighthouseBtn';
|
||||
import { WebsiteMetricsTable } from '@/components/website/WebsiteMetricsTable';
|
||||
import { WebsiteOverview } from '@/components/website/WebsiteOverview';
|
||||
import { WebsiteVisitorMapBtn } from '@/components/website/WebsiteVisitorMapBtn';
|
||||
@ -70,6 +71,9 @@ function WebsiteDetailComponent() {
|
||||
>
|
||||
<LuSettings />
|
||||
</Button>
|
||||
|
||||
<WebsiteLighthouseBtn websiteId={website.id} />
|
||||
|
||||
<WebsiteCodeBtn websiteId={website.id} />
|
||||
</div>
|
||||
}
|
||||
|
@ -34,6 +34,9 @@ export default defineConfig({
|
||||
'/trpc': {
|
||||
target: 'http://localhost:12345',
|
||||
},
|
||||
'/lh': {
|
||||
target: 'http://localhost:12345',
|
||||
},
|
||||
'/api/auth/': {
|
||||
target: 'http://localhost:12345',
|
||||
},
|
||||
|
@ -337,8 +337,7 @@ export const surveyRouter = router({
|
||||
)
|
||||
.output(buildCursorResponseSchema(SurveyResultModelSchema))
|
||||
.query(async ({ input }) => {
|
||||
const limit = input.limit;
|
||||
const { cursor, surveyId } = input;
|
||||
const { cursor, surveyId, limit } = input;
|
||||
|
||||
const where: Prisma.SurveyResultWhereInput = {
|
||||
surveyId,
|
||||
|
@ -32,11 +32,11 @@ import {
|
||||
websiteStatsSchema,
|
||||
} from '../../model/_schema/filter.js';
|
||||
import dayjs from 'dayjs';
|
||||
import { WebsiteQueryFilters } from '../../utils/prisma.js';
|
||||
import { fetchDataByCursor, WebsiteQueryFilters } from '../../utils/prisma.js';
|
||||
import { WebsiteLighthouseReportStatus } from '@prisma/client';
|
||||
import { generateLighthouse } from '../../utils/screenshot/lighthouse.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 websiteDomainSchema = z.union([
|
||||
@ -644,36 +644,44 @@ export const websiteRouter = router({
|
||||
.input(
|
||||
z.object({
|
||||
websiteId: z.string().cuid2(),
|
||||
limit: z.number().min(1).max(100).default(10),
|
||||
cursor: z.string().optional(),
|
||||
})
|
||||
)
|
||||
.output(
|
||||
z.array(
|
||||
buildCursorResponseSchema(
|
||||
WebsiteLighthouseReportModelSchema.pick({
|
||||
id: true,
|
||||
status: true,
|
||||
url: true,
|
||||
createdAt: true,
|
||||
})
|
||||
)
|
||||
)
|
||||
.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: {
|
||||
websiteId,
|
||||
},
|
||||
take: 10,
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
status: true,
|
||||
url: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
limit,
|
||||
cursor,
|
||||
}
|
||||
);
|
||||
|
||||
return list;
|
||||
return {
|
||||
items,
|
||||
nextCursor,
|
||||
};
|
||||
}),
|
||||
getLighthouseJSON: publicProcedure
|
||||
.meta({
|
||||
|
@ -226,6 +226,12 @@ type ExtractFindManyWhereType<
|
||||
},
|
||||
> = NonNullable<Parameters<T['findMany']>[0]>['where'];
|
||||
|
||||
type ExtractFindManySelectType<
|
||||
T extends {
|
||||
findMany: (args?: any) => Prisma.PrismaPromise<any>;
|
||||
},
|
||||
> = NonNullable<Parameters<T['findMany']>[0]>['select'];
|
||||
|
||||
/**
|
||||
* @example
|
||||
* const { items, nextCursor } = await fetchDataByCursor(
|
||||
@ -249,16 +255,25 @@ export async function fetchDataByCursor<
|
||||
options: {
|
||||
// where: Record<string, any>;
|
||||
where: ExtractFindManyWhereType<Model>;
|
||||
select?: ExtractFindManySelectType<Model>;
|
||||
limit: number;
|
||||
cursor: CursorType;
|
||||
cursorName?: string;
|
||||
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']> =
|
||||
await fetchModel.findMany({
|
||||
where,
|
||||
select,
|
||||
take: limit + 1,
|
||||
cursor: cursor
|
||||
? {
|
||||
|
Loading…
Reference in New Issue
Block a user