From 12e63cf4b6d4e5e372bc31930b41d967831ed45e Mon Sep 17 00:00:00 2001 From: Matthias Date: Thu, 26 Sep 2024 20:19:58 +0200 Subject: [PATCH] feat: Storage explorer in dashboard (#28) --- config.env.example | 14 +- docker-compose.yml | 3 - package.json | 2 +- packages/api/package.json | 1 + packages/api/src/client/index.ts | 2 +- packages/api/src/contract.ts | 32 +- packages/api/src/env.ts | 10 +- packages/api/src/index.ts | 7 + packages/api/src/jobs.ts | 4 +- packages/api/src/openapi.ts | 7 + packages/api/src/s3.ts | 57 + packages/api/src/types.ts | 20 + packages/artisan/src/connection.ts | 16 +- packages/artisan/src/consumer/env.ts | 4 +- .../artisan/src/consumer/workers/package.ts | 7 +- packages/artisan/src/producer/index.ts | 27 +- packages/dashboard/.env.example | 1 - packages/dashboard/Dockerfile | 5 +- packages/dashboard/index.html | 4 +- packages/dashboard/package.json | 5 +- packages/dashboard/public/scalar.html | 30 + packages/dashboard/src/App.tsx | 7 +- .../dashboard/src/components/Container.tsx | 14 - packages/dashboard/src/components/Header.tsx | 44 - .../dashboard/src/components/JobTreeItem.tsx | 8 +- .../dashboard/src/components/JobsStats.tsx | 14 +- packages/dashboard/src/components/Loader.tsx | 14 + .../src/components/OpenApiReference.tsx | 35 - packages/dashboard/src/components/Scalar.tsx | 12 + packages/dashboard/src/components/Sidebar.tsx | 76 + .../src/components/StorageExplorer.tsx | 153 + .../src/components/StretchLoader.tsx | 16 - .../src/components/editor/Editor.tsx | 90 +- .../dashboard/src/components/editor/worker.ts | 14 - .../src/components/ui/breadcrumb.tsx | 115 + .../dashboard/src/components/ui/table.tsx | 117 + packages/dashboard/src/pages/ApiPage.tsx | 5 +- packages/dashboard/src/pages/JobPage.tsx | 40 +- packages/dashboard/src/pages/JobsPage.tsx | 41 +- packages/dashboard/src/pages/PlayerPage.tsx | 17 +- packages/dashboard/src/pages/RootLayout.tsx | 20 +- packages/dashboard/src/pages/StoragePage.tsx | 40 + packages/dashboard/src/tsr.ts | 4 +- packages/dashboard/src/vite-env.d.ts | 4 +- packages/dashboard/vite.config.ts | 8 + packages/stitcher/src/env.ts | 8 +- packages/stitcher/src/helpers.ts | 2 +- packages/stitcher/src/index.ts | 6 +- packages/stitcher/src/playlist.ts | 6 +- packages/stitcher/test/mocks/mock-env.ts | 2 +- packages/stitcher/test/playlist.test.ts | 58 +- packages/stitcher/test/vast.test.ts | 10 +- pnpm-lock.yaml | 3767 ++--------------- turbo.json | 1 - 54 files changed, 1224 insertions(+), 3802 deletions(-) create mode 100644 packages/api/src/s3.ts delete mode 100644 packages/dashboard/.env.example create mode 100644 packages/dashboard/public/scalar.html delete mode 100644 packages/dashboard/src/components/Container.tsx delete mode 100644 packages/dashboard/src/components/Header.tsx create mode 100644 packages/dashboard/src/components/Loader.tsx delete mode 100644 packages/dashboard/src/components/OpenApiReference.tsx create mode 100644 packages/dashboard/src/components/Scalar.tsx create mode 100644 packages/dashboard/src/components/Sidebar.tsx create mode 100644 packages/dashboard/src/components/StorageExplorer.tsx delete mode 100644 packages/dashboard/src/components/StretchLoader.tsx delete mode 100644 packages/dashboard/src/components/editor/worker.ts create mode 100644 packages/dashboard/src/components/ui/breadcrumb.tsx create mode 100644 packages/dashboard/src/components/ui/table.tsx create mode 100644 packages/dashboard/src/pages/StoragePage.tsx diff --git a/config.env.example b/config.env.example index 9ffb7818..65253d88 100644 --- a/config.env.example +++ b/config.env.example @@ -3,9 +3,13 @@ S3_REGION= S3_ACCESS_KEY= S3_SECRET_KEY= S3_BUCKET= -S3_PUBLIC_URL= -# When you run your redis elsewhere, or locally, uncomment these -# and point them to your redis. -# REDIS_HOST= -# REDIS_PORT= \ No newline at end of file +# Uncomment when using docker. +# REDIS_HOST=redis +REDIS_PORT=6379 + +# These are public, they'll end up in client JS. +PUBLIC_API_ENDPOINT=http://localhost:52001 +PUBLIC_STITCHER_ENDPOINT=http://localhost:52002 +PUBLIC_S3_ENDPOINT= + diff --git a/docker-compose.yml b/docker-compose.yml index 5b3719dd..ff3e98c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,9 +6,6 @@ services: build: context: . dockerfile: ./packages/dashboard/Dockerfile - args: - - API_URL=http://127.0.0.1:52001 - - STITCHER_URL=http://127.0.0.1:52002 restart: always ports: - 127.0.0.1:52000:8080 diff --git a/package.json b/package.json index 4eca4e31..9839570f 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "mixwave", "scripts": { - "dev": "turbo dev", + "dev": "turbo watch dev", "build": "turbo build", "start": "turbo start" }, diff --git a/packages/api/package.json b/packages/api/package.json index b4adb1cd..480ad86a 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -23,6 +23,7 @@ }, "dependencies": { "@anatine/zod-openapi": "^2.2.6", + "@aws-sdk/client-s3": "^3.623.0", "@bull-board/api": "^5.21.3", "@bull-board/fastify": "^5.21.3", "@fastify/cors": "^9.0.1", diff --git a/packages/api/src/client/index.ts b/packages/api/src/client/index.ts index 3f7a19e4..cb6c9606 100644 --- a/packages/api/src/client/index.ts +++ b/packages/api/src/client/index.ts @@ -1,3 +1,3 @@ export * from "../contract"; -export type { JobDto } from "../types"; +export type { JobDto, FolderDto } from "../types"; diff --git a/packages/api/src/contract.ts b/packages/api/src/contract.ts index 1964c7e4..05856b78 100644 --- a/packages/api/src/contract.ts +++ b/packages/api/src/contract.ts @@ -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 } from "./types.js"; +import { jobDtoSchema, folderDtoSchema } from "./types.js"; extendZodWithOpenApi(z); @@ -15,7 +15,8 @@ export const postTranscodeBodySchema = z.object({ streams: z.array(streamSchema).openapi({ description: "Streams that need to be produced.", }), - segmentSize: z.number().default(4).openapi({ + segmentSize: z.number().optional().openapi({ + default: 4, description: "Inserts a keyframe at the start of each segment. When packaging, you can vary segmentSize as the same or a multiple of this.", }), @@ -23,7 +24,8 @@ export const postTranscodeBodySchema = z.object({ description: "Will override when specified but it is advised to leave this blank and have it auto generate a UUID.", }), - packageAfter: z.boolean().default(false).openapi({ + packageAfter: z.boolean().optional().openapi({ + default: false, description: "When transcode is finished, package it immediately with all default settings.", }), @@ -34,10 +36,18 @@ export const postTranscodeBodySchema = z.object({ export const postPackageBodySchema = z.object({ assetId: z.string(), - segmentSize: z.number().default(4), + segmentSize: z.number().optional().openapi({ + default: 4, + description: + "Segment size, must be equal or a multiple of the segmentSize defined in transcode.", + }), tag: z.string().optional().openapi({ description: "An arbitrary tag, used to group jobs.", }), + name: z.string().optional().openapi({ + default: "hls", + description: "The name of the package, will be used in storage.", + }), }); export const contract = c.router({ @@ -77,7 +87,19 @@ export const contract = c.router({ 200: jobDtoSchema, }, query: z.object({ - fromRoot: z.coerce.boolean().default(false), + fromRoot: z.coerce.boolean().optional(), + }), + }, + getStorage: { + method: "GET", + path: "/storage", + responses: { + 200: folderDtoSchema, + }, + query: z.object({ + path: z.string(), + skip: z.string().optional(), + take: z.coerce.number().optional(), }), }, getJobLogs: { diff --git a/packages/api/src/env.ts b/packages/api/src/env.ts index 311de344..96b992f6 100644 --- a/packages/api/src/env.ts +++ b/packages/api/src/env.ts @@ -10,8 +10,14 @@ if (configPath) { const envSchema = z.object({ PORT: z.coerce.number().default(52001), HOST: z.string().default("0.0.0.0"), - REDIS_HOST: z.string().default("redis"), - REDIS_PORT: z.coerce.number().default(6379), + REDIS_HOST: z.string(), + REDIS_PORT: z.coerce.number(), + PUBLIC_API_ENDPOINT: z.string(), + S3_ENDPOINT: z.string(), + S3_REGION: z.string(), + S3_ACCESS_KEY: z.string(), + S3_SECRET_KEY: z.string(), + S3_BUCKET: z.string(), }); export const env = envSchema.parse(process.env); diff --git a/packages/api/src/index.ts b/packages/api/src/index.ts index 976dbac0..9d588f8b 100644 --- a/packages/api/src/index.ts +++ b/packages/api/src/index.ts @@ -7,6 +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"; async function buildServer() { const app = Fastify(); @@ -48,6 +49,12 @@ async function buildServer() { body: await getJobLogs(params.id), }; }, + getStorage: async ({ query }) => { + return { + status: 200, + body: await getStorage(query.path, query.take, query.skip), + }; + }, getSpec: async () => { return { status: 200, diff --git a/packages/api/src/jobs.ts b/packages/api/src/jobs.ts index abde5596..1da5a4c2 100644 --- a/packages/api/src/jobs.ts +++ b/packages/api/src/jobs.ts @@ -35,7 +35,7 @@ export async function getJobs(): Promise { return result; } -export async function getJob(id: string, fromRoot: boolean) { +export async function getJob(id: string, fromRoot?: boolean) { const node = await getJobNode(id, fromRoot); return await formatJobNode(node); } @@ -48,7 +48,7 @@ export async function getJobLogs(id: string) { return logs; } -async function getJobNode(id: string, fromRoot: boolean) { +async function getJobNode(id: string, fromRoot?: boolean) { const [queue, jobId] = formatIdPair(id); let job = await Job.fromId(queue, jobId); diff --git a/packages/api/src/openapi.ts b/packages/api/src/openapi.ts index 3782c856..6768d1b2 100644 --- a/packages/api/src/openapi.ts +++ b/packages/api/src/openapi.ts @@ -1,11 +1,18 @@ import { generateOpenApi } from "@ts-rest/open-api"; import { contract } from "./contract.js"; +import { env } from "./env.js"; export const openApiSpec = generateOpenApi(contract, { info: { title: "API", version: "1.0.0", }, + servers: [ + { + url: env.PUBLIC_API_ENDPOINT, + description: "Public", + }, + ], }); delete openApiSpec.paths["/spec.json"]; diff --git a/packages/api/src/s3.ts b/packages/api/src/s3.ts new file mode 100644 index 00000000..adf81543 --- /dev/null +++ b/packages/api/src/s3.ts @@ -0,0 +1,57 @@ +import { S3, ListObjectsCommand } from "@aws-sdk/client-s3"; +import { env } from "./env.js"; +import type { FolderDto } from "./types.js"; + +const client = new S3({ + endpoint: env.S3_ENDPOINT, + region: env.S3_REGION, + credentials: { + accessKeyId: env.S3_ACCESS_KEY, + secretAccessKey: env.S3_SECRET_KEY, + }, +}); + +export async function getStorage( + path: string, + take: number = 10, + skip?: string, +): Promise { + const response = await client.send( + new ListObjectsCommand({ + Bucket: env.S3_BUCKET, + Delimiter: "/", + Prefix: path, + MaxKeys: take, + Marker: skip, + }), + ); + + const folder: FolderDto = { + path, + contents: [], + skip: response.IsTruncated ? response.NextMarker : undefined, + }; + + response.CommonPrefixes?.forEach((prefix) => { + if (!prefix.Prefix) { + return; + } + folder.contents.push({ + type: "folder", + path: prefix.Prefix, + }); + }); + + response.Contents?.forEach((content) => { + if (!content.Key) { + return; + } + folder.contents.push({ + type: "file", + path: content.Key, + size: content.Size ?? 0, + }); + }); + + return folder; +} diff --git a/packages/api/src/types.ts b/packages/api/src/types.ts index 51654517..9261f7c7 100644 --- a/packages/api/src/types.ts +++ b/packages/api/src/types.ts @@ -22,3 +22,23 @@ export type JobDto = z.infer & { export const jobDtoSchema: z.ZodType = baseJobDtoSchema.extend({ children: z.lazy(() => jobDtoSchema.array()), }); + +export const folderDtoSchema = z.object({ + path: z.string(), + skip: z.string().optional(), + contents: z.array( + z.discriminatedUnion("type", [ + z.object({ + type: z.literal("file"), + path: z.string(), + size: z.number(), + }), + z.object({ + type: z.literal("folder"), + path: z.string(), + }), + ]), + ), +}); + +export type FolderDto = z.infer; diff --git a/packages/artisan/src/connection.ts b/packages/artisan/src/connection.ts index 30cedfde..efb4cb6a 100644 --- a/packages/artisan/src/connection.ts +++ b/packages/artisan/src/connection.ts @@ -1,9 +1,13 @@ -let port = 6379; -if (process.env.REDIS_PORT) { - port = +process.env.REDIS_PORT; -} +import { z } from "zod"; + +const envSchema = z.object({ + REDIS_HOST: z.string(), + REDIS_PORT: z.coerce.number(), +}); + +const env = envSchema.parse(process.env); export const connection = { - host: process.env.REDIS_HOST ?? "redis", - port, + host: env.REDIS_HOST, + port: env.REDIS_PORT, }; diff --git a/packages/artisan/src/consumer/env.ts b/packages/artisan/src/consumer/env.ts index f87e6d83..67c82398 100644 --- a/packages/artisan/src/consumer/env.ts +++ b/packages/artisan/src/consumer/env.ts @@ -13,8 +13,8 @@ const envSchema = z.object({ S3_ACCESS_KEY: z.string(), S3_SECRET_KEY: z.string(), S3_BUCKET: z.string(), - REDIS_HOST: z.string().default("redis"), - REDIS_PORT: z.coerce.number().default(6379), + REDIS_HOST: z.string(), + REDIS_PORT: z.coerce.number(), }); export const env = envSchema.parse(process.env); diff --git a/packages/artisan/src/consumer/workers/package.ts b/packages/artisan/src/consumer/workers/package.ts index 4a691c05..aaec692e 100644 --- a/packages/artisan/src/consumer/workers/package.ts +++ b/packages/artisan/src/consumer/workers/package.ts @@ -18,6 +18,7 @@ export type PackageData = { params: { assetId: string; segmentSize: number; + name: string; }; metadata: { tag?: string; @@ -109,7 +110,7 @@ export default async function (job: Job) { await once(packagerProcess, "close"); - await uploadFolder(outDir.name, `package/${params.assetId}/hls`, { + await uploadFolder(outDir.name, `package/${params.assetId}/${params.name}`, { del: true, commandInput: (input) => ({ ContentType: lookup(input.Key) || "binary/octet-stream", @@ -121,8 +122,8 @@ export default async function (job: Job) { // becomes available on CDN. // This way we ensure we have all the segments on S3 before we make the manifest available. await copyFile( - `package/${params.assetId}/hls/master_tmp.m3u8`, - `package/${params.assetId}/hls/master.m3u8`, + `package/${params.assetId}/${params.name}/master_tmp.m3u8`, + `package/${params.assetId}/${params.name}/master.m3u8`, "public-read", ); diff --git a/packages/artisan/src/producer/index.ts b/packages/artisan/src/producer/index.ts index 3f5c9e10..dd131383 100644 --- a/packages/artisan/src/producer/index.ts +++ b/packages/artisan/src/producer/index.ts @@ -33,8 +33,8 @@ type AddTranscodeJobData = { assetId?: string; inputs: Input[]; streams: Stream[]; - segmentSize: number; - packageAfter: boolean; + segmentSize?: number; + packageAfter?: boolean; tag?: string; }; @@ -42,8 +42,8 @@ export async function addTranscodeJob({ assetId = randomUUID(), inputs, streams, - segmentSize, - packageAfter, + segmentSize = 4, + packageAfter = false, tag, }: AddTranscodeJobData) { const jobId = `transcode_${assetId}`; @@ -130,24 +130,31 @@ export async function addTranscodeJob({ type AddPackageJobData = { assetId: string; - segmentSize: number; + segmentSize?: number; + name?: string; tag?: string; }; -export async function addPackageJob(data: AddPackageJobData) { +export async function addPackageJob({ + assetId, + segmentSize = 4, + name = "hls", + tag, +}: AddPackageJobData) { return await packageQueue.add( "package", { params: { - assetId: data.assetId, - segmentSize: data.segmentSize, + assetId, + segmentSize, + name, }, metadata: { - tag: data.tag, + tag, }, } satisfies PackageData, { - jobId: `package_${data.assetId}`, + jobId: `package_${randomUUID()}`, }, ); } diff --git a/packages/dashboard/.env.example b/packages/dashboard/.env.example deleted file mode 100644 index 3452b5c8..00000000 --- a/packages/dashboard/.env.example +++ /dev/null @@ -1 +0,0 @@ -VITE_API_URL= \ No newline at end of file diff --git a/packages/dashboard/Dockerfile b/packages/dashboard/Dockerfile index 4dfc61fb..36df0239 100644 --- a/packages/dashboard/Dockerfile +++ b/packages/dashboard/Dockerfile @@ -14,10 +14,7 @@ WORKDIR /app COPY --from=builder /app/out/json/ . RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install COPY --from=builder /app/out/full/ . -ARG API_URL -ARG STITCHER_URL -ENV VITE_API_URL=$API_URL -ENV VITE_STITCHER_URL=$STITCHER_URL +COPY --from=builder /app/config.env . RUN turbo run build --filter=@mixwave/dashboard FROM devforth/spa-to-http as runner diff --git a/packages/dashboard/index.html b/packages/dashboard/index.html index af192873..4712a68f 100644 --- a/packages/dashboard/index.html +++ b/packages/dashboard/index.html @@ -1,9 +1,9 @@ - + - mixwave + Mixwave
diff --git a/packages/dashboard/package.json b/packages/dashboard/package.json index 93991c5b..a6818f86 100644 --- a/packages/dashboard/package.json +++ b/packages/dashboard/package.json @@ -11,6 +11,7 @@ "@hookform/resolvers": "^3.9.0", "@mixwave/api": "workspace:*", "@mixwave/player": "workspace:*", + "@monaco-editor/react": "^4.6.0", "@radix-ui/react-accordion": "^1.2.0", "@radix-ui/react-checkbox": "^1.1.1", "@radix-ui/react-collapsible": "^1.1.0", @@ -23,7 +24,6 @@ "@radix-ui/react-switch": "^1.1.0", "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", - "@scalar/api-reference-react": "^0.3.71", "@tanstack/react-query": "^5.51.21", "@ts-rest/core": "^3.49.3", "@ts-rest/react-query": "^3.51.0", @@ -47,12 +47,15 @@ "zod": "^3.23.8" }, "devDependencies": { + "@types/find-config": "^1.0.4", "@types/node": "^22.1.0", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/react-syntax-highlighter": "^15.5.13", "@vitejs/plugin-react": "^4.3.1", "autoprefixer": "^10.4.20", + "dotenv": "^16.4.5", + "find-config": "^1.0.0", "postcss": "^8.4.41", "tailwindcss": "^3.4.7", "typescript": "^5.2.2", diff --git a/packages/dashboard/public/scalar.html b/packages/dashboard/public/scalar.html new file mode 100644 index 00000000..33769c1a --- /dev/null +++ b/packages/dashboard/public/scalar.html @@ -0,0 +1,30 @@ + + + + Scalar API Reference + + + + + + + + + + diff --git a/packages/dashboard/src/App.tsx b/packages/dashboard/src/App.tsx index 87910a51..43fa0ecf 100644 --- a/packages/dashboard/src/App.tsx +++ b/packages/dashboard/src/App.tsx @@ -11,6 +11,7 @@ import { RootLayout } from "@/pages/RootLayout"; import { Suspense } from "react"; import { tsr } from "./tsr"; import { PlayerPage } from "./pages/PlayerPage"; +import { StoragePage } from "./pages/StoragePage"; const queryClient = new QueryClient(); @@ -21,7 +22,7 @@ const router = createBrowserRouter([ children: [ { index: true, - element: , + element: , }, { path: "/jobs", @@ -39,6 +40,10 @@ const router = createBrowserRouter([ path: "/player", element: , }, + { + path: "/storage", + element: , + }, ], }, ]); diff --git a/packages/dashboard/src/components/Container.tsx b/packages/dashboard/src/components/Container.tsx deleted file mode 100644 index bf3ab8a7..00000000 --- a/packages/dashboard/src/components/Container.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { cn } from "@/lib/utils"; - -type ContainerProps = { - children: React.ReactNode; - className?: string; -}; - -export function Container({ children, className }: ContainerProps) { - return ( -
- {children} -
- ); -} diff --git a/packages/dashboard/src/components/Header.tsx b/packages/dashboard/src/components/Header.tsx deleted file mode 100644 index 7fb3cc94..00000000 --- a/packages/dashboard/src/components/Header.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import logo from "../assets/logo.svg"; -import { cn } from "@/lib/utils"; -import { Link, useLocation } from "react-router-dom"; - -export function Header() { - const { pathname } = useLocation(); - - return ( -
- - - -
- - API - - - Jobs - - - Player - -
-
- ); -} diff --git a/packages/dashboard/src/components/JobTreeItem.tsx b/packages/dashboard/src/components/JobTreeItem.tsx index 29592e69..c1cd6b9d 100644 --- a/packages/dashboard/src/components/JobTreeItem.tsx +++ b/packages/dashboard/src/components/JobTreeItem.tsx @@ -16,15 +16,13 @@ export function JobTreeItem({ job, activeId }: JobTreeItemProps) { {job.name} - {durationStr ? ( - {durationStr} - ) : null} + {durationStr ? {durationStr} : null} ); } diff --git a/packages/dashboard/src/components/JobsStats.tsx b/packages/dashboard/src/components/JobsStats.tsx index 518f675d..6a29ec47 100644 --- a/packages/dashboard/src/components/JobsStats.tsx +++ b/packages/dashboard/src/components/JobsStats.tsx @@ -44,11 +44,10 @@ export function JobsStats({ jobs, filter, onChange }: JobsStatsProps) { return ( -
+
filterJobState("completed")} active={filter.state === "completed"} tooltip="Completed" @@ -56,7 +55,6 @@ export function JobsStats({ jobs, filter, onChange }: JobsStatsProps) { filterJobState("failed")} active={filter.state === "failed"} tooltip="Failed" @@ -64,7 +62,6 @@ export function JobsStats({ jobs, filter, onChange }: JobsStatsProps) { filterJobState("running")} active={filter.state === "running"} tooltip="Running" @@ -84,14 +81,12 @@ export function JobsStats({ jobs, filter, onChange }: JobsStatsProps) { function Tile({ value, className, - outerClassName, onClick, active, tooltip, }: { value: number; className: string; - outerClassName?: string; onClick: () => void; active: boolean; tooltip: string; @@ -102,13 +97,12 @@ function Tile({
  • {value} -
    +
  • diff --git a/packages/dashboard/src/components/Loader.tsx b/packages/dashboard/src/components/Loader.tsx new file mode 100644 index 00000000..bea6fd52 --- /dev/null +++ b/packages/dashboard/src/components/Loader.tsx @@ -0,0 +1,14 @@ +import { cn } from "@/lib/utils"; +import LoaderIcon from "lucide-react/icons/loader"; + +type LoaderProps = { + className?: string; +}; + +export function Loader({ className }: LoaderProps) { + return ( +
    + +
    + ); +} diff --git a/packages/dashboard/src/components/OpenApiReference.tsx b/packages/dashboard/src/components/OpenApiReference.tsx deleted file mode 100644 index 0e0099cb..00000000 --- a/packages/dashboard/src/components/OpenApiReference.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { ApiReferenceReact } from "@scalar/api-reference-react"; -import { tsr } from "@/tsr"; -import "@scalar/api-reference-react/style.css"; - -type OpenApiReferenceProps = { - url: string; -}; - -export function OpenApiReference({ url }: OpenApiReferenceProps) { - const { data } = tsr.getSpec.useSuspenseQuery({ - queryKey: ["spec"], - }); - - if (!data) { - return null; - } - - return ( - - ); -} diff --git a/packages/dashboard/src/components/Scalar.tsx b/packages/dashboard/src/components/Scalar.tsx new file mode 100644 index 00000000..2aeadcd7 --- /dev/null +++ b/packages/dashboard/src/components/Scalar.tsx @@ -0,0 +1,12 @@ +type ScalarProps = { + url: string; +}; + +export function Scalar({ url }: ScalarProps) { + return ( +