Compare commits
No commits in common. "fa58d6eec471563a246f1bf06838549bead04cb1" and "b79faf3ea96dc8832de0d3a3a57ba8088026ec9a" have entirely different histories.
fa58d6eec4
...
b79faf3ea9
175
deno.lock
175
deno.lock
@ -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
|
await knex.schema.createTable("users", (b) => {
|
||||||
.createTable('users')
|
b.string("username").primary().comment("user's login id");
|
||||||
.addColumn('username', 'varchar(256)', col => col.primaryKey())
|
b.string("password_hash", 64).notNullable();
|
||||||
.addColumn('password_hash', 'varchar(64)', col => col.notNull())
|
b.string("password_salt", 64).notNullable();
|
||||||
.addColumn('password_salt', 'varchar(64)', col => col.notNull())
|
});
|
||||||
.execute();
|
await knex.schema.createTable("document", (b) => {
|
||||||
|
b.increments("id").primary();
|
||||||
await db.schema
|
b.string("title").notNullable();
|
||||||
.createTable('document')
|
b.string("content_type", 16).notNullable();
|
||||||
.addColumn('id', 'serial', col => col.primaryKey())
|
b.string("basepath", 256).notNullable().comment("directory path for resource");
|
||||||
.addColumn('title', 'varchar(512)', col => col.notNull())
|
b.string("filename", 256).notNullable().comment("filename");
|
||||||
.addColumn('content_type', 'varchar(16)', col => col.notNull())
|
b.string("content_hash").nullable();
|
||||||
.addColumn('basepath', 'varchar(256)', col => col.notNull())
|
b.json("additional").nullable();
|
||||||
.addColumn('filename', 'varchar(512)', col => col.notNull())
|
b.integer("created_at").notNullable();
|
||||||
.addColumn('content_hash', 'varchar')
|
b.integer("modified_at").notNullable();
|
||||||
.addColumn('additional', 'json')
|
b.integer("deleted_at");
|
||||||
.addColumn("pagenum", "integer", col => col.notNull())
|
b.index("content_type", "content_type_index");
|
||||||
.addColumn('created_at', 'integer', col => col.notNull())
|
});
|
||||||
.addColumn('modified_at', 'integer', col => col.notNull())
|
await knex.schema.createTable("tags", (b) => {
|
||||||
.addColumn('deleted_at', 'integer')
|
b.string("name").primary();
|
||||||
.execute();
|
b.text("description");
|
||||||
|
});
|
||||||
await db.schema
|
await knex.schema.createTable("doc_tag_relation", (b) => {
|
||||||
.createTable('tags')
|
b.integer("doc_id").unsigned().notNullable();
|
||||||
.addColumn('name', 'varchar', col => col.primaryKey())
|
b.string("tag_name").notNullable();
|
||||||
.addColumn('description', 'text')
|
b.foreign("doc_id").references("document.id");
|
||||||
.execute();
|
b.foreign("tag_name").references("tags.name");
|
||||||
|
b.primary(["doc_id", "tag_name"]);
|
||||||
await db.schema
|
});
|
||||||
.createTable('doc_tag_relation')
|
await knex.schema.createTable("permissions", (b) => {
|
||||||
.addColumn('doc_id', 'integer', col => col.notNull())
|
b.string("username").notNullable();
|
||||||
.addColumn('tag_name', 'varchar', col => col.notNull())
|
b.string("name").notNullable();
|
||||||
.addForeignKeyConstraint('doc_id_fk', ['doc_id'], 'document', ['id'])
|
b.primary(["username", "name"]);
|
||||||
.addForeignKeyConstraint('tag_name_fk', ['tag_name'], 'tags', ['name'])
|
b.foreign("username").references("users.username");
|
||||||
.addPrimaryKeyConstraint('doc_tag_relation_pk', ['doc_id', 'tag_name'])
|
});
|
||||||
.execute();
|
// create admin account.
|
||||||
|
await knex
|
||||||
await db.schema
|
.insert({
|
||||||
.createTable('permissions')
|
username: "admin",
|
||||||
.addColumn('username', 'varchar', col => col.notNull())
|
password_hash: "unchecked",
|
||||||
.addColumn('name', 'varchar', col => col.notNull())
|
password_salt: "unchecked",
|
||||||
.addPrimaryKeyConstraint('permissions_pk', ['username', 'name'])
|
|
||||||
.addForeignKeyConstraint('username_fk', ['username'], 'users', ['username'])
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
// create admin account.
|
|
||||||
await db
|
|
||||||
.insertInto('users')
|
|
||||||
.values({
|
|
||||||
username: 'admin',
|
|
||||||
password_hash: 'unchecked',
|
|
||||||
password_salt: 'unchecked',
|
|
||||||
})
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
await db
|
|
||||||
.insertInto('schema_migration')
|
|
||||||
.values({
|
|
||||||
version: '0.0.1',
|
|
||||||
dirty: false,
|
|
||||||
})
|
})
|
||||||
.execute();
|
.into("users");
|
||||||
}
|
}
|
||||||
|
|
||||||
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
700
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user