Skip to content

Commit

Permalink
feat: Added preview for json, vtt and m3u8 files
Browse files Browse the repository at this point in the history
  • Loading branch information
matvp91 committed Sep 26, 2024
1 parent 8d534da commit 90744cb
Show file tree
Hide file tree
Showing 16 changed files with 343 additions and 29 deletions.
5 changes: 2 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
{
"name": "mixwave",
"scripts": {
"dev": "turbo watch dev",
"build": "turbo build",
"start": "turbo start"
"dev": "turbo dev",
"build": "turbo build"
},
"packageManager": "[email protected]",
"devDependencies": {
Expand Down
2 changes: 1 addition & 1 deletion packages/api/src/client/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export * from "../contract";

export type { JobDto, FolderDto } from "../types";
export type { JobDto, FolderDto, PreviewDto } from "../types";
12 changes: 11 additions & 1 deletion packages/api/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { initContract } from "@ts-rest/core";
import { streamSchema, inputSchema } from "@mixwave/shared/artisan";
import * as z from "zod";
import { extendZodWithOpenApi } from "@anatine/zod-openapi";
import { jobDtoSchema, folderDtoSchema } from "./types.js";
import { jobDtoSchema, folderDtoSchema, previewDtoSchema } from "./types.js";

extendZodWithOpenApi(z);

Expand Down Expand Up @@ -102,6 +102,16 @@ export const contract = c.router({
take: z.coerce.number().optional(),
}),
},
getStoragePreview: {
method: "GET",
path: "/storage/preview",
responses: {
200: previewDtoSchema,
},
query: z.object({
path: z.string(),
}),
},
getJobLogs: {
method: "GET",
path: "/jobs/:id/logs",
Expand Down
8 changes: 7 additions & 1 deletion packages/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { initServer } from "@ts-rest/fastify";
import { addTranscodeJob, addPackageJob } from "@mixwave/artisan/producer";
import { getJobs, getJob, getJobLogs } from "./jobs.js";
import { openApiSpec } from "./openapi.js";
import { getStorage } from "./s3.js";
import { getStorage, getStoragePreview } from "./s3.js";

async function buildServer() {
const app = Fastify();
Expand Down Expand Up @@ -55,6 +55,12 @@ async function buildServer() {
body: await getStorage(query.path, query.take, query.skip),
};
},
getStoragePreview: async ({ query }) => {
return {
status: 200,
body: await getStoragePreview(query.path),
};
},
getSpec: async () => {
return {
status: 200,
Expand Down
22 changes: 20 additions & 2 deletions packages/api/src/s3.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { S3, ListObjectsCommand } from "@aws-sdk/client-s3";
import { S3, ListObjectsCommand, GetObjectCommand } from "@aws-sdk/client-s3";
import { env } from "./env.js";
import type { FolderDto } from "./types.js";
import type { FolderDto, PreviewDto } from "./types.js";

const client = new S3({
endpoint: env.S3_ENDPOINT,
Expand Down Expand Up @@ -55,3 +55,21 @@ export async function getStorage(

return folder;
}

export async function getStoragePreview(path: string): Promise<PreviewDto> {
const response = await client.send(
new GetObjectCommand({
Bucket: env.S3_BUCKET,
Key: path,
}),
);

if (!response.Body) {
throw new Error("Missing body");
}

return {
path,
data: await response.Body.transformToString("utf-8"),
};
}
7 changes: 7 additions & 0 deletions packages/api/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,10 @@ export const folderDtoSchema = z.object({
});

export type FolderDto = z.infer<typeof folderDtoSchema>;

export const previewDtoSchema = z.object({
path: z.string(),
data: z.string(),
});

export type PreviewDto = z.infer<typeof previewDtoSchema>;
1 change: 1 addition & 0 deletions packages/dashboard/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@radix-ui/react-accordion": "^1.2.0",
"@radix-ui/react-checkbox": "^1.1.1",
"@radix-ui/react-collapsible": "^1.1.0",
"@radix-ui/react-dialog": "^1.1.1",
"@radix-ui/react-label": "^2.1.0",
"@radix-ui/react-popover": "^1.1.1",
"@radix-ui/react-radio-group": "^1.2.0",
Expand Down
12 changes: 6 additions & 6 deletions packages/dashboard/src/components/JobsFilter.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { JobTag } from "@/components/JobTag";
import { ObjSelect } from "./ObjSelect";
import { SelectObject } from "./SelectObject";
import type { JobDto } from "@/tsr";
import type { JobsFilterData } from "./types";
import type { ObjSelectItem } from "./ObjSelect";
import type { SelectObjectItem } from "./SelectObject";

type JobsFilterProps = {
allJobs: JobDto[];
Expand All @@ -11,7 +11,7 @@ type JobsFilterProps = {
};

export function JobsFilter({ allJobs, filter, onChange }: JobsFilterProps) {
const tags = getTags(allJobs).map<ObjSelectItem>((tag) => ({
const tags = getTags(allJobs).map<SelectObjectItem>((tag) => ({
value: tag,
label: <JobTag tag={tag} />,
}));
Expand All @@ -21,7 +21,7 @@ export function JobsFilter({ allJobs, filter, onChange }: JobsFilterProps) {
{ value: "none", label: "No tags" },
);

const names = getNames(allJobs).map<ObjSelectItem>((name) => ({
const names = getNames(allJobs).map<SelectObjectItem>((name) => ({
value: name,
label: name,
}));
Expand All @@ -30,12 +30,12 @@ export function JobsFilter({ allJobs, filter, onChange }: JobsFilterProps) {

return (
<div className="flex gap-2">
<ObjSelect
<SelectObject
items={names}
value={filter.name}
onChange={(name) => onChange({ name })}
/>
<ObjSelect
<SelectObject
items={tags}
value={filter.tag}
onChange={(tag) => onChange({ tag })}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,18 @@ import {
SelectValue,
} from "@/components/ui/select";

export type ObjSelectItem = {
export type SelectObjectItem = {
label: React.ReactNode;
value?: string;
};

type ObjSelectProps = {
items: ObjSelectItem[];
type SelectObjectProps = {
items: SelectObjectItem[];
value?: string;
onChange(value?: string): void;
};

export function ObjSelect({ items, value, onChange }: ObjSelectProps) {
export function SelectObject({ items, value, onChange }: SelectObjectProps) {
return (
<Select
value={toString(value)}
Expand Down
17 changes: 14 additions & 3 deletions packages/dashboard/src/components/StorageExplorer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,10 @@ import {
} from "@/components/ui/table";
import { StoragePathBreadcrumbs } from "./StoragePathBreadcrumbs";
import { StorageRow } from "./StorageRow";
import type { FolderDto } from "@/tsr";
import { useState } from "react";
import { StoragePreview } from "./StoragePreview";
import type { UIEventHandler } from "react";
import type { PreviewDto, FolderDto } from "@/tsr";

type StorageExplorerProps = {
path: string;
Expand All @@ -21,6 +23,8 @@ export function StorageExplorer({
contents,
onNext,
}: StorageExplorerProps) {
const [preview, setPreview] = useState<PreviewDto | null>(null);

const onScroll: UIEventHandler<HTMLDivElement> = (event) => {
const target = event.target as HTMLDivElement;
const totalHeight = target.scrollHeight - target.offsetHeight;
Expand All @@ -40,16 +44,23 @@ export function StorageExplorer({
<TableRow>
<TableHead className="w-[50px]"></TableHead>
<TableHead>Name</TableHead>
<TableHead>Size</TableHead>
<TableHead className="w-[200px]">Size</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{contents.map((content) => {
return <StorageRow key={content.path} content={content} />;
return (
<StorageRow
key={content.path}
content={content}
setPreview={setPreview}
/>
);
})}
</TableBody>
</Table>
</div>
<StoragePreview preview={preview} onClose={() => setPreview(null)} />
</div>
);
}
33 changes: 33 additions & 0 deletions packages/dashboard/src/components/StoragePreview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import type { PreviewDto } from "@/tsr";

type StoragePreviewProps = {
preview: PreviewDto | null;
onClose(): void;
};

export function StoragePreview({ preview, onClose }: StoragePreviewProps) {
return (
<Sheet open={preview !== null} onOpenChange={onClose}>
<SheetContent className="sm:max-w-none w-[640px]">
<SheetHeader>
<SheetTitle>Preview</SheetTitle>
{preview ? (
<>
<SheetDescription>
<p>{preview.path}</p>
</SheetDescription>
<pre className="text-xs overflow-auto">{preview.data}</pre>
</>
) : null}
</SheetHeader>
</SheetContent>
</Sheet>
);
}
12 changes: 5 additions & 7 deletions packages/dashboard/src/components/StorageRow.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import { Link } from "react-router-dom";
import { TableCell, TableRow } from "@/components/ui/table";
import Folder from "lucide-react/icons/folder";
import type { FolderDto } from "@/tsr";
import { StorageRowFile } from "./StorageRowFile";
import type { FolderDto, PreviewDto } from "@/tsr";

type StorageRowProps = {
content: FolderDto["contents"][0];
setPreview(preview: PreviewDto): void;
};

export function StorageRow({ content }: StorageRowProps) {
export function StorageRow({ content, setPreview }: StorageRowProps) {
const chunks = content.path.split("/");

if (content.type === "folder") {
Expand All @@ -33,11 +35,7 @@ export function StorageRow({ content }: StorageRowProps) {
if (content.type === "file") {
const name = chunks[chunks.length - 1];
return (
<TableRow>
<TableCell></TableCell>
<TableCell>{name}</TableCell>
<TableCell>{content.size} bytes</TableCell>
</TableRow>
<StorageRowFile name={name} content={content} setPreview={setPreview} />
);
}
return null;
Expand Down
57 changes: 57 additions & 0 deletions packages/dashboard/src/components/StorageRowFile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { TableCell, TableRow } from "@/components/ui/table";
import { tsr } from "@/tsr";
import { useState } from "react";
import { Loader } from "./Loader";
import type { FolderDto, PreviewDto } from "@/tsr";

type StorageRowFileProps = {
name: string;
content: Extract<FolderDto["contents"][0], { type: "file" }>;
setPreview(preview: PreviewDto): void;
};

export function StorageRowFile({
name,
content,
setPreview,
}: StorageRowFileProps) {
const [loading, setLoading] = useState(false);

const onClick = async () => {
setLoading(true);
try {
const response = await tsr.getStoragePreview.query({
query: { path: content.path },
});
if (response.status === 200) {
setPreview(response.body);
}
} finally {
setLoading(false);
}
};

const isPreview =
name.endsWith(".vtt") || name.endsWith(".m3u8") || name.endsWith(".json");

return (
<TableRow>
<TableCell></TableCell>
<TableCell>
{isPreview ? (
<button
disabled={loading}
className="flex items-center disabled:text-muted-foreground"
onClick={onClick}
>
{name}
{loading ? <Loader className="w-4 h-4 ml-2" /> : null}
</button>
) : (
name
)}
</TableCell>
<TableCell>{content.size} bytes</TableCell>
</TableRow>
);
}
Loading

0 comments on commit 90744cb

Please sign in to comment.