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 { 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>
|
||||||
}
|
}
|
||||||
|
@ -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',
|
||||||
},
|
},
|
||||||
|
@ -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,
|
||||||
|
@ -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({
|
||||||
|
@ -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
|
||||||
? {
|
? {
|
||||||
|
Loading…
Reference in New Issue
Block a user