From 88c7a8bc5313ba769c4ea9c5c422774908e40bd1 Mon Sep 17 00:00:00 2001 From: monoid Date: Wed, 9 Oct 2024 00:18:56 +0900 Subject: [PATCH] =?UTF-8?q?[BREAKING!]:=20=EC=84=9C=EB=B2=84=20=EC=9E=AC?= =?UTF-8?q?=EC=9E=91=EC=97=85=20(#16)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 서버 실행 바꾸고 여기 저기 path 바꿈. Reviewed-on: https://git.monoid.top/monoid/ionian/pulls/16 --- deno.lock | 175 +++++ packages/client/package.json | 9 +- packages/client/src/App.tsx | 4 + .../src/components/gallery/WorkQueue.tsx | 206 ++++++ packages/client/src/components/layout/nav.tsx | 11 +- packages/client/src/components/ui/chart.tsx | 363 +++++++++ .../client/src/components/ui/progress.tsx | 26 + .../client/src/components/ui/scroll-area.tsx | 46 ++ packages/client/src/components/ui/select.tsx | 162 ++++ packages/client/src/components/ui/tabs.tsx | 53 ++ packages/client/src/hook/useSearchGallery.ts | 2 +- packages/client/src/hook/useTags.ts | 3 +- packages/client/src/page/reader/comicPage.tsx | 2 +- packages/client/src/page/tagsPage.tsx | 93 +++ packages/client/src/page/taskQueuePage.tsx | 9 + packages/dbtype/src/api.ts | 9 - packages/server/migrations/initial.ts | 122 +-- packages/server/pt.ts | 60 ++ packages/server/src/SettingConfig.ts | 2 +- packages/server/src/content/file.ts | 11 +- packages/server/src/content/video.ts | 6 - packages/server/src/database.ts | 4 +- packages/server/src/db/tag.ts | 8 +- packages/server/src/diff/content_list.ts | 2 +- packages/server/src/diff/diff.ts | 6 +- packages/server/src/diff/router.ts | 8 +- packages/server/src/diff/watcher.ts | 2 +- packages/server/src/model/tag.ts | 3 +- packages/server/src/permission/permission.ts | 4 +- packages/server/src/util/oshash.ts | 10 +- pnpm-lock.yaml | 700 ++++++++++++++++++ 31 files changed, 2017 insertions(+), 104 deletions(-) create mode 100644 deno.lock create mode 100644 packages/client/src/components/gallery/WorkQueue.tsx create mode 100644 packages/client/src/components/ui/chart.tsx create mode 100644 packages/client/src/components/ui/progress.tsx create mode 100644 packages/client/src/components/ui/scroll-area.tsx create mode 100644 packages/client/src/components/ui/select.tsx create mode 100644 packages/client/src/components/ui/tabs.tsx create mode 100644 packages/client/src/page/tagsPage.tsx create mode 100644 packages/client/src/page/taskQueuePage.tsx create mode 100644 packages/server/pt.ts delete mode 100644 packages/server/src/content/video.ts diff --git a/deno.lock b/deno.lock new file mode 100644 index 0000000..77efb12 --- /dev/null +++ b/deno.lock @@ -0,0 +1,175 @@ +{ + "version": "3", + "packages": { + "specifiers": { + "jsr:@db/sqlite": "jsr:@db/sqlite@0.11.1", + "jsr:@denosaurs/plug@1": "jsr:@denosaurs/plug@1.0.5", + "jsr:@std/assert@^0.214.0": "jsr:@std/assert@0.214.0", + "jsr:@std/assert@^0.217.0": "jsr:@std/assert@0.217.0", + "jsr:@std/encoding@0.214": "jsr:@std/encoding@0.214.0", + "jsr:@std/fmt@0.214": "jsr:@std/fmt@0.214.0", + "jsr:@std/fs@0.214": "jsr:@std/fs@0.214.0", + "jsr:@std/path": "jsr:@std/path@0.217.0", + "jsr:@std/path@0.214": "jsr:@std/path@0.214.0", + "jsr:@std/path@0.217": "jsr:@std/path@0.217.0", + "jsr:@std/path@^0.214.0": "jsr:@std/path@0.214.0" + }, + "jsr": { + "@db/sqlite@0.11.1": { + "integrity": "546434e7ed762db07e6ade0f963540dd5e06723b802937bf260ff855b21ef9c5", + "dependencies": [ + "jsr:@denosaurs/plug@1", + "jsr:@std/path@0.217" + ] + }, + "@denosaurs/plug@1.0.5": { + "integrity": "04cd988da558adc226202d88c3a434d5fcc08146eaf4baf0cea0c2284b16d2bf", + "dependencies": [ + "jsr:@std/encoding@0.214", + "jsr:@std/fmt@0.214", + "jsr:@std/fs@0.214", + "jsr:@std/path@0.214" + ] + }, + "@std/assert@0.214.0": { + "integrity": "55d398de76a9828fd3b1aa653f4dba3eee4c6985d90c514865d2be9bd082b140" + }, + "@std/assert@0.217.0": { + "integrity": "c98e279362ca6982d5285c3b89517b757c1e3477ee9f14eb2fdf80a45aaa9642" + }, + "@std/encoding@0.214.0": { + "integrity": "30a8713e1db22986c7e780555ffd2fefd1d4f9374d734bb41f5970f6c3352af5" + }, + "@std/fmt@0.214.0": { + "integrity": "40382cff88a0783b347b4d69b94cf931ab8e549a733916718cb866c08efac4d4" + }, + "@std/fs@0.214.0": { + "integrity": "bc880fea0be120cb1550b1ed7faf92fe071003d83f2456a1e129b39193d85bea", + "dependencies": [ + "jsr:@std/assert@^0.214.0", + "jsr:@std/path@^0.214.0" + ] + }, + "@std/path@0.214.0": { + "integrity": "d5577c0b8d66f7e8e3586d864ebdf178bb326145a3611da5a51c961740300285", + "dependencies": [ + "jsr:@std/assert@^0.214.0" + ] + }, + "@std/path@0.217.0": { + "integrity": "1217cc25534bca9a2f672d7fe7c6f356e4027df400c0e85c0ef3e4343bc67d11", + "dependencies": [ + "jsr:@std/assert@^0.217.0" + ] + } + } + }, + "remote": { + "https://deno.land/x/sqlite@v3.9.1/build/sqlite.js": "2afc7875c7b9c85d89730c4a311ab3a304e5d1bf761fbadd8c07bbdf130f5f9b", + "https://deno.land/x/sqlite@v3.9.1/build/vfs.js": "7f7778a9fe499cd10738d6e43867340b50b67d3e39142b0065acd51a84cd2e03", + "https://deno.land/x/sqlite@v3.9.1/mod.ts": "e09fc79d8065fe222578114b109b1fd60077bff1bb75448532077f784f4d6a83", + "https://deno.land/x/sqlite@v3.9.1/src/constants.ts": "90f3be047ec0a89bcb5d6fc30db121685fc82cb00b1c476124ff47a4b0472aa9", + "https://deno.land/x/sqlite@v3.9.1/src/db.ts": "03d0c860957496eadedd86e51a6e650670764630e64f56df0092e86c90752401", + "https://deno.land/x/sqlite@v3.9.1/src/error.ts": "f7a15cb00d7c3797da1aefee3cf86d23e0ae92e73f0ba3165496c3816ab9503a", + "https://deno.land/x/sqlite@v3.9.1/src/function.ts": "bc778cab7a6d771f690afa27264c524d22fcb96f1bb61959ade7922c15a4ab8d", + "https://deno.land/x/sqlite@v3.9.1/src/query.ts": "d58abda928f6582d77bad685ecf551b1be8a15e8e38403e293ec38522e030cad", + "https://deno.land/x/sqlite@v3.9.1/src/wasm.ts": "e79d0baa6e42423257fb3c7cc98091c54399254867e0f34a09b5bdef37bd9487" + }, + "workspace": { + "packageJson": { + "dependencies": [ + "npm:@biomejs/biome@1.6.3" + ] + }, + "members": { + "packages/client": { + "packageJson": { + "dependencies": [ + "npm:@radix-ui/react-icons@^1.3.0", + "npm:@radix-ui/react-label@^2.1.0", + "npm:@radix-ui/react-popover@^1.1.2", + "npm:@radix-ui/react-progress@^1.1.0", + "npm:@radix-ui/react-radio-group@^1.2.1", + "npm:@radix-ui/react-scroll-area@^1.2.0", + "npm:@radix-ui/react-select@^2.1.2", + "npm:@radix-ui/react-separator@^1.1.0", + "npm:@radix-ui/react-slot@^1.1.0", + "npm:@radix-ui/react-tabs@^1.1.1", + "npm:@radix-ui/react-tooltip@^1.1.3", + "npm:@tanstack/react-virtual@^3.10.8", + "npm:@types/node@^22.7.4", + "npm:@types/react-dom@^18.3.0", + "npm:@types/react@^18.3.11", + "npm:@typescript-eslint/eslint-plugin@^7.18.0", + "npm:@typescript-eslint/parser@^7.18.0", + "npm:@vitejs/plugin-react-swc@^3.7.1", + "npm:autoprefixer@^10.4.20", + "npm:class-variance-authority@^0.7.0", + "npm:clsx@^2.1.1", + "npm:eslint-plugin-react-hooks@^4.6.2", + "npm:eslint-plugin-react-refresh@^0.4.12", + "npm:eslint@^8.57.1", + "npm:jotai@^2.10.0", + "npm:lucide-react@^0.451.0", + "npm:postcss@^8.4.47", + "npm:react-dom@^18.3.1", + "npm:react-resizable-panels@^2.1.4", + "npm:react@^18.3.1", + "npm:recharts@^2.12.7", + "npm:shadcn-ui@^0.8.0", + "npm:swr@^2.2.5", + "npm:tailwind-merge@^2.5.3", + "npm:tailwindcss-animate@^1.0.7", + "npm:tailwindcss@^3.4.13", + "npm:typescript@^5.6.2", + "npm:usehooks-ts@^3.1.0", + "npm:vite@^5.4.8", + "npm:vitest@^2.1.2", + "npm:wouter@^3.3.5" + ] + } + }, + "packages/dbtype": { + "packageJson": { + "dependencies": [ + "npm:@types/better-sqlite3@^7.6.9", + "npm:better-sqlite3@^9.4.3", + "npm:kysely-codegen@^0.14.1", + "npm:kysely@^0.27.3", + "npm:typescript@^5.4.3", + "npm:zod@^3.23.8" + ] + } + }, + "packages/server": { + "packageJson": { + "dependencies": [ + "npm:@types/better-sqlite3@^7.6.11", + "npm:@types/jsonwebtoken@^8.5.9", + "npm:@types/koa-bodyparser@^4.3.12", + "npm:@types/koa-compose@^3.2.8", + "npm:@types/koa-router@^7.4.8", + "npm:@types/koa@^2.15.0", + "npm:@types/node@^22.7.4", + "npm:@types/tiny-async-pool@^1.0.5", + "npm:@zip.js/zip.js@^2.7.52", + "npm:better-sqlite3@^9.6.0", + "npm:chokidar@^3.6.0", + "npm:dotenv@^16.4.5", + "npm:jose@^5.9.3", + "npm:koa-bodyparser@^4.4.1", + "npm:koa-compose@^4.1.0", + "npm:koa-router@^12.0.1", + "npm:koa@^2.15.3", + "npm:kysely@^0.27.4", + "npm:natural-orderby@^2.0.3", + "npm:nodemon@^3.1.7", + "npm:tiny-async-pool@^1.3.0", + "npm:tsx@^4.19.1", + "npm:typescript@^5.6.2" + ] + } + } + } + } +} diff --git a/packages/client/package.json b/packages/client/package.json index e10751f..0f2b728 100644 --- a/packages/client/package.json +++ b/packages/client/package.json @@ -14,18 +14,24 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.2", + "@radix-ui/react-progress": "^1.1.0", "@radix-ui/react-radio-group": "^1.2.1", + "@radix-ui/react-scroll-area": "^1.2.0", + "@radix-ui/react-select": "^2.1.2", "@radix-ui/react-separator": "^1.1.0", "@radix-ui/react-slot": "^1.1.0", + "@radix-ui/react-tabs": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.3", "@tanstack/react-virtual": "^3.10.8", "class-variance-authority": "^0.7.0", "clsx": "^2.1.1", "dbtype": "workspace:dbtype", "jotai": "^2.10.0", + "lucide-react": "^0.451.0", "react": "^18.3.1", "react-dom": "^18.3.1", "react-resizable-panels": "^2.1.4", + "recharts": "^2.12.7", "swr": "^2.2.5", "tailwind-merge": "^2.5.3", "tailwindcss-animate": "^1.0.7", @@ -47,6 +53,7 @@ "shadcn-ui": "^0.8.0", "tailwindcss": "^3.4.13", "typescript": "^5.6.2", - "vite": "^5.4.8" + "vite": "^5.4.8", + "vitest": "^2.1.2" } } diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index 45a6fe5..9c500c1 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -15,6 +15,8 @@ import ContentInfoPage from "@/page/contentInfoPage.tsx"; import SettingPage from "@/page/settingPage.tsx"; import ComicPage from "@/page/reader/comicPage.tsx"; import DifferencePage from "./page/differencePage.tsx"; +import TagsPage from "./page/tagsPage.tsx"; +import TaskQueuePage from "./page/taskQueuePage.tsx"; const App = () => { const { isDarkMode } = useTernaryDarkMode(); @@ -40,6 +42,8 @@ const App = () => { + + diff --git a/packages/client/src/components/gallery/WorkQueue.tsx b/packages/client/src/components/gallery/WorkQueue.tsx new file mode 100644 index 0000000..bb89fb8 --- /dev/null +++ b/packages/client/src/components/gallery/WorkQueue.tsx @@ -0,0 +1,206 @@ +import { useState, useEffect } from 'react' +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { Progress } from "@/components/ui/progress" +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" +import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts' + +interface Task { + id: string; + status: "Processed" | "Processing" | "Queued" | "Exception"; + createdAt: Date; + title: string; + description: string; + expectedProgress: number; + currentJobDescription: string; +} + +const generateMockTasks = (): Task[] => { + const statuses: Task['status'][] = ["Processed", "Processing", "Queued", "Exception"]; + return Array.from({ length: 50 }, (_, i) => ({ + id: `task-${i + 1}`, + status: statuses[Math.floor(Math.random() * statuses.length)], + createdAt: new Date(Date.now() - Math.random() * 10000000000), + title: `Task ${i + 1}`, + description: `This is a description for Task ${i + 1}`, + expectedProgress: Math.random(), + currentJobDescription: `Current job for Task ${i + 1}` + })); +}; + +export default function DynamicWorkQueue() { + const [tasks, setTasks] = useState([]) + + useEffect(() => { + setTasks(generateMockTasks()); + + const intervalId = setInterval(() => { + setTasks(prevTasks => { + let newTasks: Task[] = prevTasks; + // if processed tasks are more than 10, remove the oldest one + if (newTasks.filter(task => task.status === "Processed").length > 10) { + newTasks = newTasks.filter(task => task.status !== "Processed"); + } + // update the progress of each task + newTasks = newTasks.map(task => ({ + ...task, + expectedProgress: Math.min(1, task.expectedProgress + Math.random() * 0.2), + status: task.expectedProgress >= 1 ? "Processed" : task.status + })); + // if there are no queued tasks, add a new one + if (newTasks.filter(task => task.status === "Queued").length === 0) { + newTasks.push({ + id: `task-${newTasks.length + 1}`, + status: "Queued", + createdAt: new Date(), + title: `Task ${newTasks.length + 1}`, + description: `This is a description for Task ${newTasks.length + 1}`, + expectedProgress: 0, + currentJobDescription: `Current job for Task ${newTasks.length + 1}` + }); + } + return newTasks; + }); + }, 5000); + + return () => clearInterval(intervalId); + }, []) + + const updateTaskStatus = (taskId: string, newStatus: Task['status']) => { + setTasks(prevTasks => + prevTasks.map(task => + task.id === taskId ? { ...task, status: newStatus } : task + ) + ); + } + + const renderTaskList = (status: Task['status']) => { + const filteredTasks = tasks.filter(task => task.status === status) + + return ( + <> + {filteredTasks.length === 0 ? ( +

No tasks

+ ) : ( +
    + {filteredTasks.map(task => ( +
  • +

    {task.title}

    +

    {task.description}

    +

    Created: {task.createdAt.toLocaleString()}

    +
    + +

    {Math.round(task.expectedProgress * 100)}%

    +
    +

    {task.currentJobDescription}

    + {status === "Queued" && ( + + )} + {status === "Processing" && ( + + )} +
  • + ))} +
+ )} + + ) + } + + const renderProcessedTaskSummary = () => { + const processedTasks = tasks.filter(task => task.status === "Processed"); + const totalProcessed = processedTasks.length; + const averageProgress = processedTasks.reduce((sum, task) => sum + task.expectedProgress, 0) / totalProcessed || 0; + + const chartData = [ + { name: 'Processed', value: totalProcessed }, + { name: 'Remaining', value: tasks.length - totalProcessed }, + ]; + + return ( +
+
+ + + Total Processed + + +

{totalProcessed}

+
+
+ + + Average Progress + + +

{(averageProgress * 100).toFixed(2)}%

+
+
+
+ + + Processed vs Remaining Tasks + + + + + + + + + + + + +
+ ) + } + + return ( +
+ + + Dynamic Work Queue + + + + + Queued ({tasks.filter(t => t.status === "Queued").length}) + Processing ({tasks.filter(t => t.status === "Processing").length}) + Processed ({tasks.filter(t => t.status === "Processed").length}) + Exception ({tasks.filter(t => t.status === "Exception").length}) + Summary + + + {renderTaskList("Queued")} + + + {renderTaskList("Processing")} + + + {renderTaskList("Processed")} + + + {renderTaskList("Exception")} + + + {renderProcessedTaskSummary()} + + + + +
+ ) +} diff --git a/packages/client/src/components/layout/nav.tsx b/packages/client/src/components/layout/nav.tsx index 88f1c37..5ea962f 100644 --- a/packages/client/src/components/layout/nav.tsx +++ b/packages/client/src/components/layout/nav.tsx @@ -1,5 +1,5 @@ import { Link } from "wouter" -import { MagnifyingGlassIcon, GearIcon, ActivityLogIcon, ArchiveIcon, PersonIcon } from "@radix-ui/react-icons" +import { SearchIcon, SettingsIcon, TagsIcon, ArchiveIcon, UserIcon, LayoutListIcon } from "lucide-react" import { Button, buttonVariants } from "@/components/ui/button.tsx" import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip.tsx" import { useLogin } from "@/state/user.ts"; @@ -66,13 +66,14 @@ export function NavList() { return } \ No newline at end of file diff --git a/packages/client/src/components/ui/chart.tsx b/packages/client/src/components/ui/chart.tsx new file mode 100644 index 0000000..a21d77e --- /dev/null +++ b/packages/client/src/components/ui/chart.tsx @@ -0,0 +1,363 @@ +import * as React from "react" +import * as RechartsPrimitive from "recharts" + +import { cn } from "@/lib/utils" + +// Format: { THEME_NAME: CSS_SELECTOR } +const THEMES = { light: "", dark: ".dark" } as const + +export type ChartConfig = { + [k in string]: { + label?: React.ReactNode + icon?: React.ComponentType + } & ( + | { color?: string; theme?: never } + | { color?: never; theme: Record } + ) +} + +type ChartContextProps = { + config: ChartConfig +} + +const ChartContext = React.createContext(null) + +function useChart() { + const context = React.useContext(ChartContext) + + if (!context) { + throw new Error("useChart must be used within a ") + } + + return context +} + +const ChartContainer = React.forwardRef< + HTMLDivElement, + React.ComponentProps<"div"> & { + config: ChartConfig + children: React.ComponentProps< + typeof RechartsPrimitive.ResponsiveContainer + >["children"] + } +>(({ id, className, children, config, ...props }, ref) => { + const uniqueId = React.useId() + const chartId = `chart-${id || uniqueId.replace(/:/g, "")}` + + return ( + +
+ + + {children} + +
+
+ ) +}) +ChartContainer.displayName = "Chart" + +const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { + const colorConfig = Object.entries(config).filter( + ([_, config]) => config.theme || config.color + ) + + if (!colorConfig.length) { + return null + } + + return ( +