Skip to content

Commit

Permalink
feat: add option to use S3 for reports and results (#21)
Browse files Browse the repository at this point in the history
* feat: add option to use S3 for reports and results

* chore: apply prettier

* fix: apply sorting list to api route to work with s3 as well

* chore: resolve package lock conflict

* fix: report trend sorting after merge

* chore: move pw out of storage

* fix: build issues due to incorrect package import for trends components

* chore: lockfile missing next/swc deps

* fix: imports in report page components after merge

* feat: clarify readme, describe storage options

* fix: prettier ci check
  • Loading branch information
Oleksandr Shevtsov authored Sep 26, 2024
1 parent deee4e4 commit ac5772a
Show file tree
Hide file tree
Showing 38 changed files with 2,478 additions and 2,447 deletions.
13 changes: 12 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,15 @@ AUTH_SECRET=
AUTH_URL=http://localhost:3000/api/auth

API_TOKEN='my-api-token'
UI_AUTH_EXPIRE_HOURS='2'
UI_AUTH_EXPIRE_HOURS='2'

DATA_STORAGE=fs # could be s3

# S3 related configuration if DATA_STORAGE is "s3"
S3_ENDPOINT="s3.endpoint",
S3_ACCESS_KEY="some_access_key"
S3_SECRET_KEY="some_secret_key"
S3_PORT=9000 # optional
S3_REGION="us-east-1"
S3_BUCKET="bucket_name" # by default "playwright-reports-server"
S3_BATCH_SIZE=10 # by default 10
11 changes: 9 additions & 2 deletions app/api/info/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { getServerDataInfo } from '@/app/lib/data';
import { storage } from '@/app/lib/storage';
import { withError } from '@/app/lib/withError';

export const dynamic = 'force-dynamic'; // defaults to auto

export async function GET() {
return Response.json(await getServerDataInfo());
const { result, error } = await withError(storage.getServerDataInfo());

if (error) {
return Response.json({ error: error.message }, { status: 500 });
}

return Response.json(result);
}
6 changes: 3 additions & 3 deletions app/api/report/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import path from 'node:path';

import { type NextRequest } from 'next/server';

import { readFile, readReports } from '@/app/lib/data';
import { storage } from '@/app/lib/storage';
import { parse } from '@/app/lib/parser';
import { withError } from '@/app/lib/withError';

Expand All @@ -24,7 +24,7 @@ export async function GET(
return new Response('report ID is required', { status: 400 });
}

const { result: html, error } = await withError(readFile(path.join(id, 'index.html'), 'text/html'));
const { result: html, error } = await withError(storage.readFile(path.join(id, 'index.html'), 'text/html'));

if (error || !html) {
return new Response(`failed to read report html file: ${error?.message ?? 'unknown error'}`, { status: 404 });
Expand All @@ -36,7 +36,7 @@ export async function GET(
return new Response(`failed to parse report html file: ${parseError?.message ?? 'unknown error'}`, { status: 400 });
}

const { result: stats, error: statsError } = await withError(readReports());
const { result: stats, error: statsError } = await withError(storage.readReports());

if (statsError || !stats) {
return new Response(`failed to read reports: ${statsError?.message ?? 'unknown error'}`, { status: 500 });
Expand Down
25 changes: 15 additions & 10 deletions app/api/report/delete/route.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { deleteReports } from '@/app/lib/data';
import { storage } from '@/app/lib/storage';
import { withError } from '@/app/lib/withError';

export const dynamic = 'force-dynamic'; // defaults to auto
export async function DELETE(request: Request) {
const reqData = await request.json();
const { result: reqData, error: reqError } = await withError(request.json());

try {
await deleteReports(reqData.reportsIds);
if (reqError) {
return new Response(reqError.message, { status: 400 });
}

const { error } = await withError(storage.deleteReports(reqData.reportsIds));

return Response.json({
message: `Reports deleted successfully`,
reportsIds: reqData.reportsIds,
});
} catch (err) {
return new Response((err as Error).message, { status: 404 });
if (error) {
return new Response(error.message, { status: 404 });
}

return Response.json({
message: `Reports deleted successfully`,
reportsIds: reqData.reportsIds,
});
}
16 changes: 13 additions & 3 deletions app/api/report/generate/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
import { generateReport } from '@/app/lib/data';
import { storage } from '@/app/lib/storage';
import { withError } from '@/app/lib/withError';

export const dynamic = 'force-dynamic'; // defaults to auto
export async function POST(request: Request) {
const reqBody = await request.json();
const reportId = await generateReport(reqBody.resultsIds);
const { result: reqBody, error: reqError } = await withError(request.json());

if (reqError) {
return new Response(reqError.message, { status: 400 });
}

const { result: reportId, error } = await withError(storage.generateReport(reqBody.resultsIds));

if (error) {
return new Response(error.message, { status: 404 });
}

return Response.json({
reportId,
Expand Down
12 changes: 9 additions & 3 deletions app/api/report/list/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import { readReports } from '@/app/lib/data';
import { sortReportsByCreatedDate } from '@/app/lib/sort';
import { storage } from '@/app/lib/storage';
import { withError } from '@/app/lib/withError';

export const dynamic = 'force-dynamic'; // defaults to auto

export async function GET() {
const reports = await readReports();
const { result: reports, error } = await withError(storage.readReports());

return Response.json(reports);
if (error) {
return new Response(error.message, { status: 400 });
}

return Response.json(sortReportsByCreatedDate(reports!));
}
9 changes: 5 additions & 4 deletions app/api/report/trend/route.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import path from 'node:path';

import { readFile, readReports } from '@/app/lib/data';
import { storage } from '@/app/lib/storage';
import { parse } from '@/app/lib/parser';
import { sortReportsByCreatedDate } from '@/app/lib/sort';

export const dynamic = 'force-dynamic'; // defaults to auto

export async function GET() {
const reports = await readReports();
const reports = await storage.readReports();

const latestReports = reports.slice(0, 20);
const latestReports = sortReportsByCreatedDate(reports).slice(0, 20);

const latestReportsInfo = await Promise.all(
latestReports.map(async (report) => {
const html = await readFile(path.join(report.reportID, 'index.html'), 'text/html');
const html = await storage.readFile(path.join(report.reportID, 'index.html'), 'text/html');
const info = await parse(html as string);

return {
Expand Down
26 changes: 15 additions & 11 deletions app/api/result/delete/route.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import { deleteResults } from '@/app/lib/data';
import { storage } from '@/app/lib/storage';
import { withError } from '@/app/lib/withError';

export const dynamic = 'force-dynamic'; // defaults to auto

export async function DELETE(request: Request) {
const reqData = await request.json();
const { result: reqData, error: reqError } = await withError(request.json());

reqData.resultsIds;
try {
await deleteResults(reqData.resultsIds);
if (reqError) {
return new Response(reqError.message, { status: 400 });
}

const { error } = await withError(storage.deleteResults(reqData.resultsIds));

return Response.json({
message: `Results files deleted successfully`,
resultsIds: reqData.resultsIds,
});
} catch (err) {
return new Response((err as Error).message, { status: 404 });
if (error) {
return new Response(error.message, { status: 404 });
}

return Response.json({
message: `Results files deleted successfully`,
resultsIds: reqData.resultsIds,
});
}
11 changes: 8 additions & 3 deletions app/api/result/list/route.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
import { readResults } from '@/app/lib/data';
import { storage } from '@/app/lib/storage';
import { withError } from '@/app/lib/withError';

export const dynamic = 'force-dynamic'; // defaults to auto

export async function GET() {
const results = await readResults();
const { result: results, error } = await withError(storage.readResults());

return Response.json(results);
if (error) {
return new Response(error.message, { status: 400 });
}

return Response.json(results?.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()));
}
76 changes: 44 additions & 32 deletions app/api/result/upload/route.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,51 @@
import { saveResult } from '@/app/lib/data';
import { type ResultDetails, storage } from '@/app/lib/storage';
import { withError } from '@/app/lib/withError';

export const dynamic = 'force-dynamic'; // defaults to auto
export async function PUT(request: Request) {
try {
const formData = await request.formData();
const { result: formData, error: formParseError } = await withError(request.formData());

if (!formData.has('file')) {
return Response.json({ error: 'Field "file" with result is missing' }, { status: 400 });
}
const file = formData.get('file') as File;
const buffer = Buffer.from(await file.arrayBuffer());
const resultDetails: { [key: string]: string } = {};

for (const [key, value] of formData.entries()) {
if (key === 'file') {
// already processed
continue;
}
// String values for now
resultDetails[key] = value.toString();
if (formParseError) {
return Response.json({ error: formParseError.message }, { status: 400 });
}

if (!formData) {
return Response.json({ error: 'Form data is missing' }, { status: 400 });
}

if (!formData.has('file')) {
return Response.json({ error: 'Field "file" with result is missing' }, { status: 400 });
}

const file = formData.get('file') as File;

const { result: arrayBuffer, error: arrayBufferError } = await withError(file.arrayBuffer());

if (arrayBufferError) {
return Response.json({ error: `failed to get array buffer: ${arrayBufferError.message}` }, { status: 400 });
}

const buffer = Buffer.from(arrayBuffer!);
const resultDetails: ResultDetails = {};

for (const [key, value] of formData.entries()) {
if (key === 'file') {
// already processed
continue;
}
const savedResult = await saveResult(buffer, resultDetails);

return Response.json({
message: 'Success',
data: savedResult,
status: 201,
});
} catch (error) {
return Response.json({
message: 'Failed',
data: {
error: (error as Error).message,
},
status: 500,
});
// String values for now
resultDetails[key] = value.toString();
}

const { result: savedResult, error } = await withError(storage.saveResult(buffer, resultDetails));

if (error) {
return Response.json({ error: `failed to save results: ${error.message}` }, { status: 500 });
}

return Response.json({
message: 'Success',
data: savedResult,
status: 201,
});
}
34 changes: 15 additions & 19 deletions app/api/serve/[reportId]/[[...filePath]]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import mime from 'mime';
import { type NextRequest, NextResponse } from 'next/server';

import { withError } from '@/app/lib/withError';
import { readFile } from '@/app/lib/data';
import { storage } from '@/app/lib/storage';

interface ReportParams {
reportId: string;
Expand All @@ -25,27 +25,23 @@ export async function GET(

const targetPath = path.join(reportId, file);

try {
const contentType = mime.getType(path.basename(targetPath));
const contentType = mime.getType(path.basename(targetPath));

if (!contentType && !path.extname(targetPath)) {
return NextResponse.next();
}
if (!contentType && !path.extname(targetPath)) {
return NextResponse.next();
}

const { result: content, error } = await withError(readFile(targetPath, contentType));
const { result: content, error } = await withError(storage.readFile(targetPath, contentType));

if (error ?? !content) {
return NextResponse.json({ error: `Could not read file ${error?.message ?? ''}` }, { status: 404 });
}
if (error ?? !content) {
return NextResponse.json({ error: `Could not read file ${error?.message ?? ''}` }, { status: 404 });
}

const headers = {
headers: {
'Content-Type': contentType ?? 'application/octet-stream',
},
};
const headers = {
headers: {
'Content-Type': contentType ?? 'application/octet-stream',
},
};

return new Response(content, headers);
} catch (error) {
return NextResponse.json({ error: `Page not found: ${error}` }, { status: 404 });
}
return new Response(content, headers);
}
2 changes: 1 addition & 1 deletion app/components/delete-report-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export default function DeleteReportButton({ reportId, onDeleted }: DeleteProjec
!!reportId && (
<>
<Tooltip color="danger" content="Delete Report" placement="top">
<Button color="danger" size="md" onPress={onOpen}>
<Button color="danger" isLoading={isLoading} size="md" onPress={onOpen}>
<DeleteIcon />
</Button>
</Tooltip>
Expand Down
2 changes: 1 addition & 1 deletion app/components/delete-results-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export default function DeleteResultsButton({ resultIds, onDeletedResult }: Dele
return (
<>
<Tooltip color="danger" content="Delete Result" placement="top">
<Button color="danger" isDisabled={!resultIds?.length} size="md" onPress={onOpen}>
<Button color="danger" isDisabled={!resultIds?.length} isLoading={isLoading} size="md" onPress={onOpen}>
<DeleteIcon />
</Button>
</Tooltip>
Expand Down
2 changes: 1 addition & 1 deletion app/components/fs-stat-tabs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import { Badge, Tab, Tabs } from '@nextui-org/react';

import { type ServerDataInfo } from '@/app/lib/data';
import { type ServerDataInfo } from '@/app/lib/storage';
import { ReportIcon, ResultIcon, TrendIcon } from '@/app/components/icons';
import Reports from '@/app/components/reports';
import Results from '@/app/components/results';
Expand Down
2 changes: 1 addition & 1 deletion app/components/report-details/file-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { StatChart } from '../stat-chart';

import renderFileSuitesTree from './suite-tree';

import { ReportHistory } from '@/app/lib/data';
import { type ReportHistory } from '@/app/lib/storage';

interface FileListProps {
report?: ReportHistory | null;
Expand Down
4 changes: 2 additions & 2 deletions app/components/report-details/suite-tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { Accordion, AccordionItem, Chip } from '@nextui-org/react';

import TestInfo from './test-info';

import { ReportFile, ReportTest } from '@/app/lib/parser';
import { ReportHistory } from '@/app/lib/data';
import { type ReportFile, type ReportTest } from '@/app/lib/parser';
import { type ReportHistory } from '@/app/lib/storage';
import { testStatusToColor } from '@/app/lib/tailwind';

interface SuiteNode {
Expand Down
Loading

0 comments on commit ac5772a

Please sign in to comment.