Compare commits
No commits in common. "fa58d6eec471563a246f1bf06838549bead04cb1" and "b79faf3ea96dc8832de0d3a3a57ba8088026ec9a" have entirely different histories.
fa58d6eec4
...
b79faf3ea9
31 changed files with 104 additions and 2017 deletions
175
deno.lock
generated
175
deno.lock
generated
|
@ -1,175 +0,0 @@
|
||||||
{
|
|
||||||
"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"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -14,24 +14,18 @@
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.1.0",
|
"@radix-ui/react-label": "^2.1.0",
|
||||||
"@radix-ui/react-popover": "^1.1.2",
|
"@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-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-separator": "^1.1.0",
|
||||||
"@radix-ui/react-slot": "^1.1.0",
|
"@radix-ui/react-slot": "^1.1.0",
|
||||||
"@radix-ui/react-tabs": "^1.1.1",
|
|
||||||
"@radix-ui/react-tooltip": "^1.1.3",
|
"@radix-ui/react-tooltip": "^1.1.3",
|
||||||
"@tanstack/react-virtual": "^3.10.8",
|
"@tanstack/react-virtual": "^3.10.8",
|
||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"dbtype": "workspace:dbtype",
|
"dbtype": "workspace:dbtype",
|
||||||
"jotai": "^2.10.0",
|
"jotai": "^2.10.0",
|
||||||
"lucide-react": "^0.451.0",
|
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-resizable-panels": "^2.1.4",
|
"react-resizable-panels": "^2.1.4",
|
||||||
"recharts": "^2.12.7",
|
|
||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
"tailwind-merge": "^2.5.3",
|
"tailwind-merge": "^2.5.3",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
|
@ -53,7 +47,6 @@
|
||||||
"shadcn-ui": "^0.8.0",
|
"shadcn-ui": "^0.8.0",
|
||||||
"tailwindcss": "^3.4.13",
|
"tailwindcss": "^3.4.13",
|
||||||
"typescript": "^5.6.2",
|
"typescript": "^5.6.2",
|
||||||
"vite": "^5.4.8",
|
"vite": "^5.4.8"
|
||||||
"vitest": "^2.1.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,6 @@ import ContentInfoPage from "@/page/contentInfoPage.tsx";
|
||||||
import SettingPage from "@/page/settingPage.tsx";
|
import SettingPage from "@/page/settingPage.tsx";
|
||||||
import ComicPage from "@/page/reader/comicPage.tsx";
|
import ComicPage from "@/page/reader/comicPage.tsx";
|
||||||
import DifferencePage from "./page/differencePage.tsx";
|
import DifferencePage from "./page/differencePage.tsx";
|
||||||
import TagsPage from "./page/tagsPage.tsx";
|
|
||||||
import TaskQueuePage from "./page/taskQueuePage.tsx";
|
|
||||||
|
|
||||||
const App = () => {
|
const App = () => {
|
||||||
const { isDarkMode } = useTernaryDarkMode();
|
const { isDarkMode } = useTernaryDarkMode();
|
||||||
|
@ -42,8 +40,6 @@ const App = () => {
|
||||||
<Route path="/setting" component={SettingPage} />
|
<Route path="/setting" component={SettingPage} />
|
||||||
<Route path="/doc/:id/reader" component={ComicPage}/>
|
<Route path="/doc/:id/reader" component={ComicPage}/>
|
||||||
<Route path="/difference" component={DifferencePage}/>
|
<Route path="/difference" component={DifferencePage}/>
|
||||||
<Route path="/tags" component={TagsPage}/>
|
|
||||||
<Route path="/queue" component={TaskQueuePage} />
|
|
||||||
<Route component={NotFoundPage} />
|
<Route component={NotFoundPage} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|
|
@ -1,206 +0,0 @@
|
||||||
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<Task[]>([])
|
|
||||||
|
|
||||||
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 ? (
|
|
||||||
<p className="text-gray-500 p-4">No tasks</p>
|
|
||||||
) : (
|
|
||||||
<ul className="space-y-4 p-4">
|
|
||||||
{filteredTasks.map(task => (
|
|
||||||
<li key={task.id} className="border p-4 rounded-md shadow-sm">
|
|
||||||
<h4 className="font-medium text-primary">{task.title}</h4>
|
|
||||||
<p className="text-sm text-gray-600">{task.description}</p>
|
|
||||||
<p className="text-sm text-gray-500 mt-1">Created: {task.createdAt.toLocaleString()}</p>
|
|
||||||
<div className="mt-2">
|
|
||||||
<Progress value={task.expectedProgress * 100} className="h-2" />
|
|
||||||
<p className="text-xs text-right mt-1">{Math.round(task.expectedProgress * 100)}%</p>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm mt-1 text-gray-700">{task.currentJobDescription}</p>
|
|
||||||
{status === "Queued" && (
|
|
||||||
<Button
|
|
||||||
onClick={() => updateTaskStatus(task.id, "Processing")}
|
|
||||||
className="mt-2"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
Start Processing
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{status === "Processing" && (
|
|
||||||
<Button
|
|
||||||
onClick={() => updateTaskStatus(task.id, "Processed")}
|
|
||||||
className="mt-2"
|
|
||||||
size="sm"
|
|
||||||
>
|
|
||||||
Mark as Processed
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Total Processed</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<p className="text-4xl font-bold">{totalProcessed}</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Average Progress</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<p className="text-4xl font-bold">{(averageProgress * 100).toFixed(2)}%</p>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Processed vs Remaining Tasks</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<ResponsiveContainer width="100%" height={300}>
|
|
||||||
<BarChart data={chartData}>
|
|
||||||
<XAxis dataKey="name" />
|
|
||||||
<YAxis />
|
|
||||||
<Tooltip />
|
|
||||||
<Bar dataKey="value" fill="#8884d8" />
|
|
||||||
</BarChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="container mx-auto p-4 max-w-4xl">
|
|
||||||
<Card>
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle>Dynamic Work Queue</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
<Tabs defaultValue="Queued" className="w-full">
|
|
||||||
<TabsList className="grid w-full grid-cols-5">
|
|
||||||
<TabsTrigger value="Queued">Queued ({tasks.filter(t => t.status === "Queued").length})</TabsTrigger>
|
|
||||||
<TabsTrigger value="Processing">Processing ({tasks.filter(t => t.status === "Processing").length})</TabsTrigger>
|
|
||||||
<TabsTrigger value="Processed">Processed ({tasks.filter(t => t.status === "Processed").length})</TabsTrigger>
|
|
||||||
<TabsTrigger value="Exception">Exception ({tasks.filter(t => t.status === "Exception").length})</TabsTrigger>
|
|
||||||
<TabsTrigger value="Summary">Summary</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<TabsContent value="Queued">
|
|
||||||
{renderTaskList("Queued")}
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="Processing">
|
|
||||||
{renderTaskList("Processing")}
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="Processed">
|
|
||||||
{renderTaskList("Processed")}
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="Exception">
|
|
||||||
{renderTaskList("Exception")}
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="Summary">
|
|
||||||
{renderProcessedTaskSummary()}
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { Link } from "wouter"
|
import { Link } from "wouter"
|
||||||
import { SearchIcon, SettingsIcon, TagsIcon, ArchiveIcon, UserIcon, LayoutListIcon } from "lucide-react"
|
import { MagnifyingGlassIcon, GearIcon, ActivityLogIcon, ArchiveIcon, PersonIcon } from "@radix-ui/react-icons"
|
||||||
import { Button, buttonVariants } from "@/components/ui/button.tsx"
|
import { Button, buttonVariants } from "@/components/ui/button.tsx"
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip.tsx"
|
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip.tsx"
|
||||||
import { useLogin } from "@/state/user.ts";
|
import { useLogin } from "@/state/user.ts";
|
||||||
|
@ -66,14 +66,13 @@ export function NavList() {
|
||||||
return <aside className="h-dvh flex flex-col">
|
return <aside className="h-dvh flex flex-col">
|
||||||
<nav className="flex flex-col items-center gap-4 px-2 sm:py-5 flex-1">
|
<nav className="flex flex-col items-center gap-4 px-2 sm:py-5 flex-1">
|
||||||
{navItems && <>{navItems} <Separator/> </>}
|
{navItems && <>{navItems} <Separator/> </>}
|
||||||
<NavItem icon={<SearchIcon className="h-5 w-5" />} to="/search" name="Search" />
|
<NavItem icon={<MagnifyingGlassIcon className="h-5 w-5" />} to="/search" name="Search" />
|
||||||
<NavItem icon={<TagsIcon className="h-5 w-5" />} to="/tags" name="Tags" />
|
<NavItem icon={<ActivityLogIcon className="h-5 w-5" />} to="/tags" name="Tags" />
|
||||||
<NavItem icon={<ArchiveIcon className="h-5 w-5" />} to="/difference" name="Difference" />
|
<NavItem icon={<ArchiveIcon className="h-5 w-5" />} to="/difference" name="Difference" />
|
||||||
<NavItem icon={<LayoutListIcon className="h-5 w-5" />} to="/queue" name="Task Queue" />
|
|
||||||
</nav>
|
</nav>
|
||||||
<nav className="mt-auto flex flex-col items-center gap-4 px-2 sm:py-5 flex-grow-0">
|
<nav className="mt-auto flex flex-col items-center gap-4 px-2 sm:py-5 flex-grow-0">
|
||||||
<NavItem icon={<UserIcon className="h-5 w-5" />} to={ loginInfo ? "/profile" : "/login"} name={ loginInfo ? "Profiles" : "Login"} />
|
<NavItem icon={<PersonIcon className="h-5 w-5" />} to={ loginInfo ? "/profile" : "/login"} name={ loginInfo ? "Profiles" : "Login"} />
|
||||||
<NavItem icon={<SettingsIcon className="h-5 w-5" />} to="/setting" name="Settings" />
|
<NavItem icon={<GearIcon className="h-5 w-5" />} to="/setting" name="Settings" />
|
||||||
</nav>
|
</nav>
|
||||||
</aside>
|
</aside>
|
||||||
}
|
}
|
|
@ -1,363 +0,0 @@
|
||||||
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<keyof typeof THEMES, string> }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ChartContextProps = {
|
|
||||||
config: ChartConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartContext = React.createContext<ChartContextProps | null>(null)
|
|
||||||
|
|
||||||
function useChart() {
|
|
||||||
const context = React.useContext(ChartContext)
|
|
||||||
|
|
||||||
if (!context) {
|
|
||||||
throw new Error("useChart must be used within a <ChartContainer />")
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<ChartContext.Provider value={{ config }}>
|
|
||||||
<div
|
|
||||||
data-chart={chartId}
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex aspect-video justify-center text-xs [&_.recharts-cartesian-axis-tick_text]:fill-muted-foreground [&_.recharts-cartesian-grid_line[stroke='#ccc']]:stroke-border/50 [&_.recharts-curve.recharts-tooltip-cursor]:stroke-border [&_.recharts-dot[stroke='#fff']]:stroke-transparent [&_.recharts-layer]:outline-none [&_.recharts-polar-grid_[stroke='#ccc']]:stroke-border [&_.recharts-radial-bar-background-sector]:fill-muted [&_.recharts-rectangle.recharts-tooltip-cursor]:fill-muted [&_.recharts-reference-line_[stroke='#ccc']]:stroke-border [&_.recharts-sector[stroke='#fff']]:stroke-transparent [&_.recharts-sector]:outline-none [&_.recharts-surface]:outline-none",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChartStyle id={chartId} config={config} />
|
|
||||||
<RechartsPrimitive.ResponsiveContainer>
|
|
||||||
{children}
|
|
||||||
</RechartsPrimitive.ResponsiveContainer>
|
|
||||||
</div>
|
|
||||||
</ChartContext.Provider>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
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 (
|
|
||||||
<style
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: Object.entries(THEMES)
|
|
||||||
.map(
|
|
||||||
([theme, prefix]) => `
|
|
||||||
${prefix} [data-chart=${id}] {
|
|
||||||
${colorConfig
|
|
||||||
.map(([key, itemConfig]) => {
|
|
||||||
const color =
|
|
||||||
itemConfig.theme?.[theme as keyof typeof itemConfig.theme] ||
|
|
||||||
itemConfig.color
|
|
||||||
return color ? ` --color-${key}: ${color};` : null
|
|
||||||
})
|
|
||||||
.join("\n")}
|
|
||||||
}
|
|
||||||
`
|
|
||||||
)
|
|
||||||
.join("\n"),
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ChartTooltip = RechartsPrimitive.Tooltip
|
|
||||||
|
|
||||||
const ChartTooltipContent = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentProps<typeof RechartsPrimitive.Tooltip> &
|
|
||||||
React.ComponentProps<"div"> & {
|
|
||||||
hideLabel?: boolean
|
|
||||||
hideIndicator?: boolean
|
|
||||||
indicator?: "line" | "dot" | "dashed"
|
|
||||||
nameKey?: string
|
|
||||||
labelKey?: string
|
|
||||||
}
|
|
||||||
>(
|
|
||||||
(
|
|
||||||
{
|
|
||||||
active,
|
|
||||||
payload,
|
|
||||||
className,
|
|
||||||
indicator = "dot",
|
|
||||||
hideLabel = false,
|
|
||||||
hideIndicator = false,
|
|
||||||
label,
|
|
||||||
labelFormatter,
|
|
||||||
labelClassName,
|
|
||||||
formatter,
|
|
||||||
color,
|
|
||||||
nameKey,
|
|
||||||
labelKey,
|
|
||||||
},
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const { config } = useChart()
|
|
||||||
|
|
||||||
const tooltipLabel = React.useMemo(() => {
|
|
||||||
if (hideLabel || !payload?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const [item] = payload
|
|
||||||
const key = `${labelKey || item.dataKey || item.name || "value"}`
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
||||||
const value =
|
|
||||||
!labelKey && typeof label === "string"
|
|
||||||
? config[label as keyof typeof config]?.label || label
|
|
||||||
: itemConfig?.label
|
|
||||||
|
|
||||||
if (labelFormatter) {
|
|
||||||
return (
|
|
||||||
<div className={cn("font-medium", labelClassName)}>
|
|
||||||
{labelFormatter(value, payload)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={cn("font-medium", labelClassName)}>{value}</div>
|
|
||||||
}, [
|
|
||||||
label,
|
|
||||||
labelFormatter,
|
|
||||||
payload,
|
|
||||||
hideLabel,
|
|
||||||
labelClassName,
|
|
||||||
config,
|
|
||||||
labelKey,
|
|
||||||
])
|
|
||||||
|
|
||||||
if (!active || !payload?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const nestLabel = payload.length === 1 && indicator !== "dot"
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"grid min-w-[8rem] items-start gap-1.5 rounded-lg border border-border/50 bg-background px-2.5 py-1.5 text-xs shadow-xl",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{!nestLabel ? tooltipLabel : null}
|
|
||||||
<div className="grid gap-1.5">
|
|
||||||
{payload.map((item, index) => {
|
|
||||||
const key = `${nameKey || item.name || item.dataKey || "value"}`
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
||||||
const indicatorColor = color || item.payload.fill || item.color
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.dataKey}
|
|
||||||
className={cn(
|
|
||||||
"flex w-full flex-wrap items-stretch gap-2 [&>svg]:h-2.5 [&>svg]:w-2.5 [&>svg]:text-muted-foreground",
|
|
||||||
indicator === "dot" && "items-center"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{formatter && item?.value !== undefined && item.name ? (
|
|
||||||
formatter(item.value, item.name, item, index, item.payload)
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{itemConfig?.icon ? (
|
|
||||||
<itemConfig.icon />
|
|
||||||
) : (
|
|
||||||
!hideIndicator && (
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"shrink-0 rounded-[2px] border-[--color-border] bg-[--color-bg]",
|
|
||||||
{
|
|
||||||
"h-2.5 w-2.5": indicator === "dot",
|
|
||||||
"w-1": indicator === "line",
|
|
||||||
"w-0 border-[1.5px] border-dashed bg-transparent":
|
|
||||||
indicator === "dashed",
|
|
||||||
"my-0.5": nestLabel && indicator === "dashed",
|
|
||||||
}
|
|
||||||
)}
|
|
||||||
style={
|
|
||||||
{
|
|
||||||
"--color-bg": indicatorColor,
|
|
||||||
"--color-border": indicatorColor,
|
|
||||||
} as React.CSSProperties
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
<div
|
|
||||||
className={cn(
|
|
||||||
"flex flex-1 justify-between leading-none",
|
|
||||||
nestLabel ? "items-end" : "items-center"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="grid gap-1.5">
|
|
||||||
{nestLabel ? tooltipLabel : null}
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
{itemConfig?.label || item.name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
{item.value && (
|
|
||||||
<span className="font-mono font-medium tabular-nums text-foreground">
|
|
||||||
{item.value.toLocaleString()}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
ChartTooltipContent.displayName = "ChartTooltip"
|
|
||||||
|
|
||||||
const ChartLegend = RechartsPrimitive.Legend
|
|
||||||
|
|
||||||
const ChartLegendContent = React.forwardRef<
|
|
||||||
HTMLDivElement,
|
|
||||||
React.ComponentProps<"div"> &
|
|
||||||
Pick<RechartsPrimitive.LegendProps, "payload" | "verticalAlign"> & {
|
|
||||||
hideIcon?: boolean
|
|
||||||
nameKey?: string
|
|
||||||
}
|
|
||||||
>(
|
|
||||||
(
|
|
||||||
{ className, hideIcon = false, payload, verticalAlign = "bottom", nameKey },
|
|
||||||
ref
|
|
||||||
) => {
|
|
||||||
const { config } = useChart()
|
|
||||||
|
|
||||||
if (!payload?.length) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center justify-center gap-4",
|
|
||||||
verticalAlign === "top" ? "pb-3" : "pt-3",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{payload.map((item) => {
|
|
||||||
const key = `${nameKey || item.dataKey || "value"}`
|
|
||||||
const itemConfig = getPayloadConfigFromPayload(config, item, key)
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
key={item.value}
|
|
||||||
className={cn(
|
|
||||||
"flex items-center gap-1.5 [&>svg]:h-3 [&>svg]:w-3 [&>svg]:text-muted-foreground"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{itemConfig?.icon && !hideIcon ? (
|
|
||||||
<itemConfig.icon />
|
|
||||||
) : (
|
|
||||||
<div
|
|
||||||
className="h-2 w-2 shrink-0 rounded-[2px]"
|
|
||||||
style={{
|
|
||||||
backgroundColor: item.color,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{itemConfig?.label}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
ChartLegendContent.displayName = "ChartLegend"
|
|
||||||
|
|
||||||
// Helper to extract item config from a payload.
|
|
||||||
function getPayloadConfigFromPayload(
|
|
||||||
config: ChartConfig,
|
|
||||||
payload: unknown,
|
|
||||||
key: string
|
|
||||||
) {
|
|
||||||
if (typeof payload !== "object" || payload === null) {
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
const payloadPayload =
|
|
||||||
"payload" in payload &&
|
|
||||||
typeof payload.payload === "object" &&
|
|
||||||
payload.payload !== null
|
|
||||||
? payload.payload
|
|
||||||
: undefined
|
|
||||||
|
|
||||||
let configLabelKey: string = key
|
|
||||||
|
|
||||||
if (
|
|
||||||
key in payload &&
|
|
||||||
typeof payload[key as keyof typeof payload] === "string"
|
|
||||||
) {
|
|
||||||
configLabelKey = payload[key as keyof typeof payload] as string
|
|
||||||
} else if (
|
|
||||||
payloadPayload &&
|
|
||||||
key in payloadPayload &&
|
|
||||||
typeof payloadPayload[key as keyof typeof payloadPayload] === "string"
|
|
||||||
) {
|
|
||||||
configLabelKey = payloadPayload[
|
|
||||||
key as keyof typeof payloadPayload
|
|
||||||
] as string
|
|
||||||
}
|
|
||||||
|
|
||||||
return configLabelKey in config
|
|
||||||
? config[configLabelKey]
|
|
||||||
: config[key as keyof typeof config]
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
|
||||||
ChartContainer,
|
|
||||||
ChartTooltip,
|
|
||||||
ChartTooltipContent,
|
|
||||||
ChartLegend,
|
|
||||||
ChartLegendContent,
|
|
||||||
ChartStyle,
|
|
||||||
}
|
|
|
@ -1,26 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import * as ProgressPrimitive from "@radix-ui/react-progress"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Progress = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ProgressPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
|
|
||||||
>(({ className, value, ...props }, ref) => (
|
|
||||||
<ProgressPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ProgressPrimitive.Indicator
|
|
||||||
className="h-full w-full flex-1 bg-primary transition-all"
|
|
||||||
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
|
|
||||||
/>
|
|
||||||
</ProgressPrimitive.Root>
|
|
||||||
))
|
|
||||||
Progress.displayName = ProgressPrimitive.Root.displayName
|
|
||||||
|
|
||||||
export { Progress }
|
|
|
@ -1,46 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const ScrollArea = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<ScrollAreaPrimitive.Root
|
|
||||||
ref={ref}
|
|
||||||
className={cn("relative overflow-hidden", className)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
|
||||||
{children}
|
|
||||||
</ScrollAreaPrimitive.Viewport>
|
|
||||||
<ScrollBar />
|
|
||||||
<ScrollAreaPrimitive.Corner />
|
|
||||||
</ScrollAreaPrimitive.Root>
|
|
||||||
))
|
|
||||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
|
||||||
|
|
||||||
const ScrollBar = React.forwardRef<
|
|
||||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
||||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
|
||||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
||||||
ref={ref}
|
|
||||||
orientation={orientation}
|
|
||||||
className={cn(
|
|
||||||
"flex touch-none select-none transition-colors",
|
|
||||||
orientation === "vertical" &&
|
|
||||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
|
||||||
orientation === "horizontal" &&
|
|
||||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
|
||||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
||||||
))
|
|
||||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
|
||||||
|
|
||||||
export { ScrollArea, ScrollBar }
|
|
|
@ -1,162 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import {
|
|
||||||
CaretSortIcon,
|
|
||||||
CheckIcon,
|
|
||||||
ChevronDownIcon,
|
|
||||||
ChevronUpIcon,
|
|
||||||
} from "@radix-ui/react-icons"
|
|
||||||
import * as SelectPrimitive from "@radix-ui/react-select"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Select = SelectPrimitive.Root
|
|
||||||
|
|
||||||
const SelectGroup = SelectPrimitive.Group
|
|
||||||
|
|
||||||
const SelectValue = SelectPrimitive.Value
|
|
||||||
|
|
||||||
const SelectTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
<SelectPrimitive.Icon asChild>
|
|
||||||
<CaretSortIcon className="h-4 w-4 opacity-50" />
|
|
||||||
</SelectPrimitive.Icon>
|
|
||||||
</SelectPrimitive.Trigger>
|
|
||||||
))
|
|
||||||
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const SelectScrollUpButton = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.ScrollUpButton
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronUpIcon />
|
|
||||||
</SelectPrimitive.ScrollUpButton>
|
|
||||||
))
|
|
||||||
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
|
|
||||||
|
|
||||||
const SelectScrollDownButton = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.ScrollDownButton
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"flex cursor-default items-center justify-center py-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<ChevronDownIcon />
|
|
||||||
</SelectPrimitive.ScrollDownButton>
|
|
||||||
))
|
|
||||||
SelectScrollDownButton.displayName =
|
|
||||||
SelectPrimitive.ScrollDownButton.displayName
|
|
||||||
|
|
||||||
const SelectContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
|
||||||
>(({ className, children, position = "popper", ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Portal>
|
|
||||||
<SelectPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
|
||||||
position === "popper" &&
|
|
||||||
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
position={position}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<SelectScrollUpButton />
|
|
||||||
<SelectPrimitive.Viewport
|
|
||||||
className={cn(
|
|
||||||
"p-1",
|
|
||||||
position === "popper" &&
|
|
||||||
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</SelectPrimitive.Viewport>
|
|
||||||
<SelectScrollDownButton />
|
|
||||||
</SelectPrimitive.Content>
|
|
||||||
</SelectPrimitive.Portal>
|
|
||||||
))
|
|
||||||
SelectContent.displayName = SelectPrimitive.Content.displayName
|
|
||||||
|
|
||||||
const SelectLabel = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Label>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Label
|
|
||||||
ref={ref}
|
|
||||||
className={cn("px-2 py-1.5 text-sm font-semibold", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
|
||||||
|
|
||||||
const SelectItem = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
|
||||||
>(({ className, children, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Item
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
<span className="absolute right-2 flex h-3.5 w-3.5 items-center justify-center">
|
|
||||||
<SelectPrimitive.ItemIndicator>
|
|
||||||
<CheckIcon className="h-4 w-4" />
|
|
||||||
</SelectPrimitive.ItemIndicator>
|
|
||||||
</span>
|
|
||||||
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
|
||||||
</SelectPrimitive.Item>
|
|
||||||
))
|
|
||||||
SelectItem.displayName = SelectPrimitive.Item.displayName
|
|
||||||
|
|
||||||
const SelectSeparator = React.forwardRef<
|
|
||||||
React.ElementRef<typeof SelectPrimitive.Separator>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<SelectPrimitive.Separator
|
|
||||||
ref={ref}
|
|
||||||
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
|
||||||
|
|
||||||
export {
|
|
||||||
Select,
|
|
||||||
SelectGroup,
|
|
||||||
SelectValue,
|
|
||||||
SelectTrigger,
|
|
||||||
SelectContent,
|
|
||||||
SelectLabel,
|
|
||||||
SelectItem,
|
|
||||||
SelectSeparator,
|
|
||||||
SelectScrollUpButton,
|
|
||||||
SelectScrollDownButton,
|
|
||||||
}
|
|
|
@ -1,53 +0,0 @@
|
||||||
import * as React from "react"
|
|
||||||
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
|
||||||
|
|
||||||
import { cn } from "@/lib/utils"
|
|
||||||
|
|
||||||
const Tabs = TabsPrimitive.Root
|
|
||||||
|
|
||||||
const TabsList = React.forwardRef<
|
|
||||||
React.ElementRef<typeof TabsPrimitive.List>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<TabsPrimitive.List
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
TabsList.displayName = TabsPrimitive.List.displayName
|
|
||||||
|
|
||||||
const TabsTrigger = React.forwardRef<
|
|
||||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<TabsPrimitive.Trigger
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
|
||||||
|
|
||||||
const TabsContent = React.forwardRef<
|
|
||||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
|
||||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
|
||||||
>(({ className, ...props }, ref) => (
|
|
||||||
<TabsPrimitive.Content
|
|
||||||
ref={ref}
|
|
||||||
className={cn(
|
|
||||||
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
||||||
className
|
|
||||||
)}
|
|
||||||
{...props}
|
|
||||||
/>
|
|
||||||
))
|
|
||||||
TabsContent.displayName = TabsPrimitive.Content.displayName
|
|
||||||
|
|
||||||
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
|
|
@ -1,5 +1,5 @@
|
||||||
import useSWRInifinite from "swr/infinite";
|
import useSWRInifinite from "swr/infinite";
|
||||||
import type { Document } from "dbtype";
|
import type { Document } from "dbtype/api";
|
||||||
import { fetcher } from "./fetcher";
|
import { fetcher } from "./fetcher";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,5 @@ export function useTags() {
|
||||||
return useSWR<{
|
return useSWR<{
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
occurs: number;
|
}[]>("/api/tags", fetcher);
|
||||||
}[]>("/api/tags?withCount=true", fetcher);
|
|
||||||
}
|
}
|
|
@ -6,7 +6,7 @@ import { useGalleryDoc } from "@/hook/useGalleryDoc.ts";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { EnterFullScreenIcon, ExitFullScreenIcon, ExitIcon } from "@radix-ui/react-icons";
|
import { EnterFullScreenIcon, ExitFullScreenIcon, ExitIcon } from "@radix-ui/react-icons";
|
||||||
import { useEventListener } from "usehooks-ts";
|
import { useEventListener } from "usehooks-ts";
|
||||||
import type { Document } from "dbtype";
|
import type { Document } from "dbtype/api";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
|
||||||
interface ComicPageProps {
|
interface ComicPageProps {
|
||||||
|
|
|
@ -1,93 +0,0 @@
|
||||||
import { toPrettyTagname } from "@/components/gallery/TagBadge";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
|
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
|
||||||
import { useTags } from "@/hook/useTags";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
export function TagsPage() {
|
|
||||||
const { data, error, isLoading } = useTags();
|
|
||||||
const [searchInput, setSearchInput] = useState<string>("");
|
|
||||||
const [orderby, setOrderby] = useState<string>("name");
|
|
||||||
const [page, setPage] = useState<number>(1);
|
|
||||||
const pageSize = 10;
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <div>Loading...</div>
|
|
||||||
}
|
|
||||||
if (error) {
|
|
||||||
return <div>Error: {error.message}</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredTags = data?.filter(tag =>
|
|
||||||
tag.name.toLowerCase().includes(searchInput.toLowerCase())
|
|
||||||
).sort((a, b) => {
|
|
||||||
if (orderby === "name") {
|
|
||||||
return a.name.localeCompare(b.name);
|
|
||||||
} else if (orderby === "occurs") {
|
|
||||||
return b.occurs - a.occurs;
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
});
|
|
||||||
const paginatedTags = filteredTags?.slice((page - 1) * pageSize, page * pageSize);
|
|
||||||
const totalPages = Math.ceil((filteredTags?.length || 0) / pageSize);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="p-4">
|
|
||||||
<div className="flex space-x-2">
|
|
||||||
<Select value={orderby} onValueChange={setOrderby}>
|
|
||||||
<SelectTrigger className="w-[180px]">
|
|
||||||
<SelectValue placeholder="order by" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
<SelectItem value="name">Name</SelectItem>
|
|
||||||
<SelectItem value="occurs">Occurs</SelectItem>
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
<Input
|
|
||||||
className="mb-4"
|
|
||||||
placeholder="Search tags..."
|
|
||||||
value={searchInput}
|
|
||||||
onChange={(e) => {
|
|
||||||
setSearchInput(e.target.value);
|
|
||||||
setPage(1);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-2 2xl:grid-cols-4 xl:grid-cols-3 gap-2 ">
|
|
||||||
{paginatedTags?.map(tag => (
|
|
||||||
<Card key={tag.name} className="mb-4">
|
|
||||||
<CardHeader>
|
|
||||||
<CardTitle className="text-2xl">{toPrettyTagname(tag.name)}({tag.occurs})</CardTitle>
|
|
||||||
</CardHeader>
|
|
||||||
<CardContent>
|
|
||||||
{tag.description}
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-between mt-4">
|
|
||||||
<Button onClick={() => setPage(page - 1)} disabled={page <= 1}>
|
|
||||||
이전
|
|
||||||
</Button>
|
|
||||||
<Popover>
|
|
||||||
<PopoverTrigger asChild>
|
|
||||||
<span>{page} / {totalPages}</span>
|
|
||||||
</PopoverTrigger>
|
|
||||||
<PopoverContent>
|
|
||||||
<Input type="number" value={page}
|
|
||||||
max={totalPages} min={1}
|
|
||||||
onChange={(e) => setPage(Number(e.target.value))} />
|
|
||||||
</PopoverContent>
|
|
||||||
</Popover>
|
|
||||||
<Button onClick={() => setPage(page + 1)} disabled={page >= totalPages}>
|
|
||||||
다음
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default TagsPage;
|
|
|
@ -1,9 +0,0 @@
|
||||||
import DynamicWorkQueue from "@/components/gallery/WorkQueue";
|
|
||||||
|
|
||||||
export default function TaskQueuePage(){
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<DynamicWorkQueue />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
|
@ -59,7 +59,16 @@ export const SchemaMigrationSchema = z.object({
|
||||||
|
|
||||||
export type SchemaMigration = z.infer<typeof SchemaMigrationSchema>;
|
export type SchemaMigration = z.infer<typeof SchemaMigrationSchema>;
|
||||||
|
|
||||||
|
const WorkStatusEnum = z.enum(["pending", "done", "error"]);
|
||||||
|
|
||||||
|
export const WorkSchema = z.object({
|
||||||
|
uuid: z.string(),
|
||||||
|
type: z.literal("rehash"),
|
||||||
|
status: WorkStatusEnum,
|
||||||
|
detail: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Work = z.infer<typeof WorkSchema>;
|
||||||
|
|
||||||
export const QueryListOptionSchema = z.object({
|
export const QueryListOptionSchema = z.object({
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,76 +1,56 @@
|
||||||
import { Kysely, sql } from 'kysely';
|
import { Knex } from "knex";
|
||||||
|
|
||||||
export async function up(db: Kysely<any>) {
|
export async function up(knex: Knex) {
|
||||||
await db.schema
|
await knex.schema.createTable("schema_migration", (b) => {
|
||||||
.createTable('schema_migration')
|
b.string("version");
|
||||||
.addColumn('version', 'char(16)')
|
b.boolean("dirty");
|
||||||
.addColumn('dirty', 'boolean')
|
});
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.createTable('users')
|
|
||||||
.addColumn('username', 'varchar(256)', col => col.primaryKey())
|
|
||||||
.addColumn('password_hash', 'varchar(64)', col => col.notNull())
|
|
||||||
.addColumn('password_salt', 'varchar(64)', col => col.notNull())
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.createTable('document')
|
|
||||||
.addColumn('id', 'serial', col => col.primaryKey())
|
|
||||||
.addColumn('title', 'varchar(512)', col => col.notNull())
|
|
||||||
.addColumn('content_type', 'varchar(16)', col => col.notNull())
|
|
||||||
.addColumn('basepath', 'varchar(256)', col => col.notNull())
|
|
||||||
.addColumn('filename', 'varchar(512)', col => col.notNull())
|
|
||||||
.addColumn('content_hash', 'varchar')
|
|
||||||
.addColumn('additional', 'json')
|
|
||||||
.addColumn("pagenum", "integer", col => col.notNull())
|
|
||||||
.addColumn('created_at', 'integer', col => col.notNull())
|
|
||||||
.addColumn('modified_at', 'integer', col => col.notNull())
|
|
||||||
.addColumn('deleted_at', 'integer')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.createTable('tags')
|
|
||||||
.addColumn('name', 'varchar', col => col.primaryKey())
|
|
||||||
.addColumn('description', 'text')
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.createTable('doc_tag_relation')
|
|
||||||
.addColumn('doc_id', 'integer', col => col.notNull())
|
|
||||||
.addColumn('tag_name', 'varchar', col => col.notNull())
|
|
||||||
.addForeignKeyConstraint('doc_id_fk', ['doc_id'], 'document', ['id'])
|
|
||||||
.addForeignKeyConstraint('tag_name_fk', ['tag_name'], 'tags', ['name'])
|
|
||||||
.addPrimaryKeyConstraint('doc_tag_relation_pk', ['doc_id', 'tag_name'])
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db.schema
|
|
||||||
.createTable('permissions')
|
|
||||||
.addColumn('username', 'varchar', col => col.notNull())
|
|
||||||
.addColumn('name', 'varchar', col => col.notNull())
|
|
||||||
.addPrimaryKeyConstraint('permissions_pk', ['username', 'name'])
|
|
||||||
.addForeignKeyConstraint('username_fk', ['username'], 'users', ['username'])
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
|
await knex.schema.createTable("users", (b) => {
|
||||||
|
b.string("username").primary().comment("user's login id");
|
||||||
|
b.string("password_hash", 64).notNullable();
|
||||||
|
b.string("password_salt", 64).notNullable();
|
||||||
|
});
|
||||||
|
await knex.schema.createTable("document", (b) => {
|
||||||
|
b.increments("id").primary();
|
||||||
|
b.string("title").notNullable();
|
||||||
|
b.string("content_type", 16).notNullable();
|
||||||
|
b.string("basepath", 256).notNullable().comment("directory path for resource");
|
||||||
|
b.string("filename", 256).notNullable().comment("filename");
|
||||||
|
b.string("content_hash").nullable();
|
||||||
|
b.json("additional").nullable();
|
||||||
|
b.integer("created_at").notNullable();
|
||||||
|
b.integer("modified_at").notNullable();
|
||||||
|
b.integer("deleted_at");
|
||||||
|
b.index("content_type", "content_type_index");
|
||||||
|
});
|
||||||
|
await knex.schema.createTable("tags", (b) => {
|
||||||
|
b.string("name").primary();
|
||||||
|
b.text("description");
|
||||||
|
});
|
||||||
|
await knex.schema.createTable("doc_tag_relation", (b) => {
|
||||||
|
b.integer("doc_id").unsigned().notNullable();
|
||||||
|
b.string("tag_name").notNullable();
|
||||||
|
b.foreign("doc_id").references("document.id");
|
||||||
|
b.foreign("tag_name").references("tags.name");
|
||||||
|
b.primary(["doc_id", "tag_name"]);
|
||||||
|
});
|
||||||
|
await knex.schema.createTable("permissions", (b) => {
|
||||||
|
b.string("username").notNullable();
|
||||||
|
b.string("name").notNullable();
|
||||||
|
b.primary(["username", "name"]);
|
||||||
|
b.foreign("username").references("users.username");
|
||||||
|
});
|
||||||
// create admin account.
|
// create admin account.
|
||||||
await db
|
await knex
|
||||||
.insertInto('users')
|
.insert({
|
||||||
.values({
|
username: "admin",
|
||||||
username: 'admin',
|
password_hash: "unchecked",
|
||||||
password_hash: 'unchecked',
|
password_salt: "unchecked",
|
||||||
password_salt: 'unchecked',
|
|
||||||
})
|
})
|
||||||
.execute();
|
.into("users");
|
||||||
|
|
||||||
await db
|
|
||||||
.insertInto('schema_migration')
|
|
||||||
.values({
|
|
||||||
version: '0.0.1',
|
|
||||||
dirty: false,
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function down(db: Kysely<any>) {
|
export async function down(knex: Knex) {
|
||||||
throw new Error('Downward migrations are not supported. Restore from backup.');
|
throw new Error("Downward migrations are not supported. Restore from backup.");
|
||||||
}
|
}
|
|
@ -1,60 +0,0 @@
|
||||||
import { Database } from "jsr:@db/sqlite";
|
|
||||||
import {join} from "jsr:@std/path";
|
|
||||||
let db = new Database("./db.sqlite3")
|
|
||||||
const stmt = db.prepare("SELECT id, basepath, filename from document");
|
|
||||||
let ds = [...stmt.all()];
|
|
||||||
|
|
||||||
async function oshash(
|
|
||||||
path: string
|
|
||||||
){
|
|
||||||
const chunkSize = 4096;
|
|
||||||
const minFileSize = chunkSize * 2;
|
|
||||||
|
|
||||||
const fd = await Deno.open(path);
|
|
||||||
const st = await fd.stat();
|
|
||||||
let hash = BigInt(st.size);
|
|
||||||
|
|
||||||
if (st.size < minFileSize){
|
|
||||||
throw new Error("File is too small to hash");
|
|
||||||
}
|
|
||||||
|
|
||||||
// read first and last chunk
|
|
||||||
const firstChunk = new Uint8Array(chunkSize);
|
|
||||||
await fd.read(firstChunk, 0, chunkSize, 0);
|
|
||||||
const lastChunk = new Uint8Array(chunkSize);
|
|
||||||
await fd.read(lastChunk, 0, chunkSize, st.size - chunkSize);
|
|
||||||
// iterate over first and last chunk.
|
|
||||||
// for each uint64_t, add it to the hash.
|
|
||||||
const firstChunkView = new DataView(firstChunk.buffer);
|
|
||||||
for (let i = 0; i < chunkSize; i += 8){
|
|
||||||
hash += firstChunkView.getBigUint64(i, true);
|
|
||||||
// prevent overflow
|
|
||||||
hash = (hash & 0xFFFFFFFFFFFFFFFFn);
|
|
||||||
}
|
|
||||||
const lastChunkView = new DataView(lastChunk.buffer);
|
|
||||||
for (let i = 0; i < chunkSize; i += 8){
|
|
||||||
hash += lastChunkView.getBigUint64(i, true);
|
|
||||||
// prevent overflow
|
|
||||||
hash = (hash & 0xFFFFFFFFFFFFFFFFn);
|
|
||||||
}
|
|
||||||
return hash;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateHash(ds: {id: number, basepath: string, filename: string}[]) {
|
|
||||||
const content_hashs = await Promise.all(ds.map(async (d) => {
|
|
||||||
const p = join(d.basepath, d.filename);
|
|
||||||
return await oshash(p);
|
|
||||||
}));
|
|
||||||
db.transaction(() => {
|
|
||||||
for (let i = 0; i < ds.length; i++) {
|
|
||||||
db.run(`UPDATE document SET content_hash = ? where id = ?`, content_hashs[i].toString(), ds[i].id)
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = 0; i < ds.length; i += 32) {
|
|
||||||
const d = ds.slice(i, i + 32);
|
|
||||||
console.log(d.map(x => x.id));
|
|
||||||
await updateHash(d);
|
|
||||||
}
|
|
||||||
db.close();
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { randomBytes } from "node:crypto";
|
import { randomBytes } from "node:crypto";
|
||||||
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
import type { Permission } from "./permission/permission.ts";
|
import type { Permission } from "./permission/permission";
|
||||||
|
|
||||||
export interface SettingConfig {
|
export interface SettingConfig {
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -2,7 +2,6 @@ import { createHash } from "node:crypto";
|
||||||
import { promises, type Stats } from "node:fs";
|
import { promises, type Stats } from "node:fs";
|
||||||
import path, { extname } from "node:path";
|
import path, { extname } from "node:path";
|
||||||
import type { DocumentBody } from "dbtype";
|
import type { DocumentBody } from "dbtype";
|
||||||
import { oshash } from "src/util/oshash.ts";
|
|
||||||
/**
|
/**
|
||||||
* content file or directory referrer
|
* content file or directory referrer
|
||||||
*/
|
*/
|
||||||
|
@ -61,8 +60,14 @@ export const createDefaultClass = (type: string): ContentFileConstructor => {
|
||||||
}
|
}
|
||||||
async getHash(): Promise<string> {
|
async getHash(): Promise<string> {
|
||||||
if (this.hash !== undefined) return this.hash;
|
if (this.hash !== undefined) return this.hash;
|
||||||
const hash = await oshash(this.path);
|
this.stat = await promises.stat(this.path);
|
||||||
this.hash = hash.toString();
|
const hash = createHash("sha512");
|
||||||
|
hash.update(extname(this.path));
|
||||||
|
hash.update(this.stat.mode.toString());
|
||||||
|
// if(this.desc !== undefined)
|
||||||
|
// hash.update(JSON.stringify(this.desc));
|
||||||
|
hash.update(this.stat.size.toString());
|
||||||
|
this.hash = hash.digest("base64");
|
||||||
return this.hash;
|
return this.hash;
|
||||||
}
|
}
|
||||||
async getMtime(): Promise<number> {
|
async getMtime(): Promise<number> {
|
||||||
|
|
6
packages/server/src/content/video.ts
Normal file
6
packages/server/src/content/video.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import { registerContentReferrer } from "./file.ts";
|
||||||
|
import { createDefaultClass } from "./file.ts";
|
||||||
|
|
||||||
|
export class VideoReferrer extends createDefaultClass("video") {
|
||||||
|
}
|
||||||
|
registerContentReferrer(VideoReferrer);
|
|
@ -1,4 +1,6 @@
|
||||||
import { getKysely } from "./db/kysely.ts";
|
import { existsSync } from "node:fs";
|
||||||
|
import { get_setting } from "./SettingConfig";
|
||||||
|
import { getKysely } from "./db/kysely";
|
||||||
|
|
||||||
export async function connectDB() {
|
export async function connectDB() {
|
||||||
const kysely = getKysely();
|
const kysely = getKysely();
|
||||||
|
|
|
@ -12,14 +12,8 @@ class SqliteTagAccessor implements TagAccessor {
|
||||||
.select("tag_name")
|
.select("tag_name")
|
||||||
.select(qb => qb.fn.count<number>("doc_id").as("occurs"))
|
.select(qb => qb.fn.count<number>("doc_id").as("occurs"))
|
||||||
.groupBy("tag_name")
|
.groupBy("tag_name")
|
||||||
.innerJoin("tags", "tags.name", "doc_tag_relation.tag_name")
|
|
||||||
.select("tags.description")
|
|
||||||
.execute();
|
.execute();
|
||||||
return result.map((x) => ({
|
return result;
|
||||||
name: x.tag_name,
|
|
||||||
description: x.description ?? undefined,
|
|
||||||
occurs: x.occurs,
|
|
||||||
}));
|
|
||||||
}
|
}
|
||||||
async getAllTagList(): Promise<Tag[]> {
|
async getAllTagList(): Promise<Tag[]> {
|
||||||
return (await this.kysely.selectFrom("tags")
|
return (await this.kysely.selectFrom("tags")
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import type { ContentFile } from "../content/mod.ts";
|
import type { ContentFile } from "../content/mod";
|
||||||
|
|
||||||
export class ContentList {
|
export class ContentList {
|
||||||
/** path map */
|
/** path map */
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import asyncPool from "tiny-async-pool";
|
import asyncPool from "tiny-async-pool";
|
||||||
import type { DocumentAccessor } from "../model/doc.ts";
|
import type { DocumentAccessor } from "../model/doc";
|
||||||
import { ContentDiffHandler } from "./content_handler.ts";
|
import { ContentDiffHandler } from "./content_handler";
|
||||||
import type { IDiffWatcher } from "./watcher.ts";
|
import type { IDiffWatcher } from "./watcher";
|
||||||
|
|
||||||
export class DiffManager {
|
export class DiffManager {
|
||||||
watching: { [content_type: string]: ContentDiffHandler };
|
watching: { [content_type: string]: ContentDiffHandler };
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import type Koa from "koa";
|
import type Koa from "koa";
|
||||||
import Router from "koa-router";
|
import Router from "koa-router";
|
||||||
import type { ContentFile } from "../content/mod.ts";
|
import type { ContentFile } from "../content/mod";
|
||||||
import { AdminOnlyMiddleware } from "../permission/permission.ts";
|
import { AdminOnlyMiddleware } from "../permission/permission";
|
||||||
import { sendError } from "../route/error_handler.ts";
|
import { sendError } from "../route/error_handler";
|
||||||
import type { DiffManager } from "./diff.ts";
|
import type { DiffManager } from "./diff";
|
||||||
|
|
||||||
function content_file_to_return(x: ContentFile) {
|
function content_file_to_return(x: ContentFile) {
|
||||||
return { path: x.path, type: x.type };
|
return { path: x.path, type: x.type };
|
||||||
|
|
|
@ -2,7 +2,7 @@ import type event from "node:events";
|
||||||
import { FSWatcher, watch } from "node:fs";
|
import { FSWatcher, watch } from "node:fs";
|
||||||
import { promises } from "node:fs";
|
import { promises } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import type { DocumentAccessor } from "../model/doc.ts";
|
import type { DocumentAccessor } from "../model/doc";
|
||||||
|
|
||||||
const readdir = promises.readdir;
|
const readdir = promises.readdir;
|
||||||
|
|
||||||
|
|
|
@ -4,8 +4,7 @@ export interface Tag {
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TagCount {
|
export interface TagCount {
|
||||||
name: string;
|
tag_name: string;
|
||||||
description?: string;
|
|
||||||
occurs: number;
|
occurs: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import type Koa from "koa";
|
import type Koa from "koa";
|
||||||
import type { UserState } from "../login.ts";
|
import type { UserState } from "../login";
|
||||||
import { sendError } from "../route/error_handler.ts";
|
import { sendError } from "../route/error_handler";
|
||||||
|
|
||||||
export enum Permission {
|
export enum Permission {
|
||||||
// ========
|
// ========
|
||||||
|
|
|
@ -24,21 +24,19 @@ export async function oshash(
|
||||||
}
|
}
|
||||||
|
|
||||||
// read first and last chunk
|
// read first and last chunk
|
||||||
const firstChunk = new Uint8Array(chunkSize);
|
const firstChunk = Buffer.alloc(chunkSize);
|
||||||
await fd.read(firstChunk, 0, chunkSize, 0);
|
await fd.read(firstChunk, 0, chunkSize, 0);
|
||||||
const lastChunk = new Uint8Array(chunkSize);
|
const lastChunk = Buffer.alloc(chunkSize);
|
||||||
await fd.read(lastChunk, 0, chunkSize, st.size - chunkSize);
|
await fd.read(lastChunk, 0, chunkSize, st.size - chunkSize);
|
||||||
// iterate over first and last chunk.
|
// iterate over first and last chunk.
|
||||||
// for each uint64_t, add it to the hash.
|
// for each uint64_t, add it to the hash.
|
||||||
const firstChunkView = new DataView(firstChunk.buffer);
|
|
||||||
for (let i = 0; i < chunkSize; i += 8){
|
for (let i = 0; i < chunkSize; i += 8){
|
||||||
hash += firstChunkView.getBigUint64(i, true);
|
hash += firstChunk.readBigUInt64LE(i);
|
||||||
// prevent overflow
|
// prevent overflow
|
||||||
hash = (hash & 0xFFFFFFFFFFFFFFFFn);
|
hash = (hash & 0xFFFFFFFFFFFFFFFFn);
|
||||||
}
|
}
|
||||||
const lastChunkView = new DataView(lastChunk.buffer);
|
|
||||||
for (let i = 0; i < chunkSize; i += 8){
|
for (let i = 0; i < chunkSize; i += 8){
|
||||||
hash += lastChunkView.getBigUint64(i, true);
|
hash += lastChunk.readBigUInt64LE(i);
|
||||||
// prevent overflow
|
// prevent overflow
|
||||||
hash = (hash & 0xFFFFFFFFFFFFFFFFn);
|
hash = (hash & 0xFFFFFFFFFFFFFFFFn);
|
||||||
}
|
}
|
||||||
|
|
700
pnpm-lock.yaml
generated
700
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue