Compare commits
3 Commits
6ed78e6459
...
f543ad1cf4
Author | SHA1 | Date | |
---|---|---|---|
f543ad1cf4 | |||
58adb46323 | |||
de1bb7dfde |
45
packages/client/src/components/BuildInfoCard.tsx
Normal file
45
packages/client/src/components/BuildInfoCard.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { getBuildInfo } from "../util/env";
|
||||||
|
import { Clock, GitBranch, GitCommit, Tag } from "lucide-react";
|
||||||
|
|
||||||
|
export default function BuildInfoCard() {
|
||||||
|
const buildInfo = getBuildInfo();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className="w-full">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="text-2xl font-bold">Build Information</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="grid gap-4">
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<GitBranch className="h-5 w-5 text-muted-foreground" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium leading-none">Git Branch</p>
|
||||||
|
<p className="text-sm text-muted-foreground">{buildInfo.gitBranch}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<GitCommit className="h-5 w-5 text-muted-foreground" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium leading-none">Git Hash</p>
|
||||||
|
<p className="text-sm text-muted-foreground">{buildInfo.gitHash}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<Clock className="h-5 w-5 text-muted-foreground" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium leading-none">Build Time</p>
|
||||||
|
<p className="text-sm text-muted-foreground">{buildInfo.buildTime}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-4">
|
||||||
|
<Tag className="h-5 w-5 text-muted-foreground" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium leading-none">Build Version</p>
|
||||||
|
<p className="text-sm text-muted-foreground">{buildInfo.buildVersion}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
@ -22,7 +22,7 @@ export function DescTagItem({
|
|||||||
{items.length === 0 ? "N/A" : items.map(
|
{items.length === 0 ? "N/A" : items.map(
|
||||||
(x, i) =>
|
(x, i) =>
|
||||||
<>
|
<>
|
||||||
<StyledLink key={x} to={`/search?allow_tag=${name}:${x}`}>{x}</StyledLink>
|
<StyledLink key={x} to={`/search?allow_tag=${name.toLowerCase()}:${x}`}>{x}</StyledLink>
|
||||||
{i + 1 < items.length && <span className="">, </span>}
|
{i + 1 < items.length && <span className="">, </span>}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
@ -1,7 +1,17 @@
|
|||||||
import useSWR from "swr";
|
import useSWR, { useSWRConfig } from "swr";
|
||||||
import type { Document } from "dbtype";
|
import type { Document } from "dbtype";
|
||||||
import { fetcher } from "./fetcher";
|
import { fetcher } from "./fetcher";
|
||||||
|
|
||||||
export function useGalleryDoc(id: string) {
|
export function useGalleryDoc(id: string) {
|
||||||
return useSWR<Document>(`/api/doc/${id}`, fetcher);
|
return useSWR<Document>(`/api/doc/${id}`, fetcher);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useRehashDoc() {
|
||||||
|
const { mutate } = useSWRConfig();
|
||||||
|
return async (id: string) => {
|
||||||
|
await fetch(`/api/doc/${id}/_rehash`, {
|
||||||
|
method: "POST",
|
||||||
|
});
|
||||||
|
mutate(`/api/doc/${id}`);
|
||||||
|
};
|
||||||
}
|
}
|
@ -1,10 +1,11 @@
|
|||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
import { useGalleryDoc } from "../hook/useGalleryDoc.ts";
|
import { useGalleryDoc, useRehashDoc } from "../hook/useGalleryDoc.ts";
|
||||||
import TagBadge from "@/components/gallery/TagBadge";
|
import TagBadge from "@/components/gallery/TagBadge";
|
||||||
import StyledLink from "@/components/gallery/StyledLink";
|
import StyledLink from "@/components/gallery/StyledLink";
|
||||||
import { Link } from "wouter";
|
import { Link } from "wouter";
|
||||||
import { classifyTags } from "../lib/classifyTags.tsx";
|
import { classifyTags } from "../lib/classifyTags.tsx";
|
||||||
import { DescTagItem, DescItem } from "../components/gallery/DescItem.tsx";
|
import { DescTagItem, DescItem } from "../components/gallery/DescItem.tsx";
|
||||||
|
import { Button } from "@/components/ui/button.tsx";
|
||||||
|
|
||||||
export interface ContentInfoPageProps {
|
export interface ContentInfoPageProps {
|
||||||
params: {
|
params: {
|
||||||
@ -14,6 +15,7 @@ export interface ContentInfoPageProps {
|
|||||||
|
|
||||||
export function ContentInfoPage({ params }: ContentInfoPageProps) {
|
export function ContentInfoPage({ params }: ContentInfoPageProps) {
|
||||||
const { data, error, isLoading } = useGalleryDoc(params.id);
|
const { data, error, isLoading } = useGalleryDoc(params.id);
|
||||||
|
const rehashDoc = useRehashDoc();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
return <div className="p-4">Loading...</div>
|
return <div className="p-4">Loading...</div>
|
||||||
@ -43,7 +45,13 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) {
|
|||||||
alt={data.title} />
|
alt={data.title} />
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<Card className="flex-1">
|
<Card className="flex-1 relative">
|
||||||
|
<div className="absolute top-0 right-0 p-2">
|
||||||
|
<Button variant="ghost" onClick={async () => {
|
||||||
|
// Rehash
|
||||||
|
await rehashDoc(params.id);
|
||||||
|
}}>Rehash</Button>
|
||||||
|
</div>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>
|
<CardTitle>
|
||||||
<StyledLink to={contentLocation}>
|
<StyledLink to={contentLocation}>
|
||||||
|
@ -2,6 +2,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|||||||
import { type TernaryDarkMode, useTernaryDarkMode, useMediaQuery } from "usehooks-ts";
|
import { type TernaryDarkMode, useTernaryDarkMode, useMediaQuery } from "usehooks-ts";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
|
import BuildInfoCard from "@/components/BuildInfoCard";
|
||||||
|
|
||||||
function LightModeView() {
|
function LightModeView() {
|
||||||
return <div className="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
|
return <div className="items-center rounded-md border-2 border-muted p-1 hover:border-accent">
|
||||||
@ -46,7 +47,7 @@ export function SettingPage() {
|
|||||||
const isSystemDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
|
const isSystemDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4 space-y-2">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle className="text-2xl">Settings</CardTitle>
|
<CardTitle className="text-2xl">Settings</CardTitle>
|
||||||
@ -85,6 +86,7 @@ export function SettingPage() {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
<BuildInfoCard />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
13
packages/client/src/util/env.ts
Normal file
13
packages/client/src/util/env.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
declare const __GIT_BRANCH__: string;
|
||||||
|
declare const __GIT_HASH__: string;
|
||||||
|
declare const __BUILD_TIME__: string;
|
||||||
|
declare const __BUILD_VERSION__: string;
|
||||||
|
|
||||||
|
export function getBuildInfo() {
|
||||||
|
return {
|
||||||
|
gitBranch: __GIT_BRANCH__,
|
||||||
|
gitHash: __GIT_HASH__,
|
||||||
|
buildTime: __BUILD_TIME__,
|
||||||
|
buildVersion: __BUILD_VERSION__,
|
||||||
|
};
|
||||||
|
}
|
@ -2,11 +2,41 @@ import { defineConfig, loadEnv } from 'vite'
|
|||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
import react from '@vitejs/plugin-react-swc'
|
import react from '@vitejs/plugin-react-swc'
|
||||||
|
|
||||||
|
import { execSync } from "child_process";
|
||||||
|
|
||||||
|
function getCurrentGitHash(): string {
|
||||||
|
const gitHash = execSync("git rev-parse HEAD")
|
||||||
|
.toString()
|
||||||
|
.trim();
|
||||||
|
return gitHash;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBuildTime(): string {
|
||||||
|
return new Date().toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getBuildVersion(): string {
|
||||||
|
return process.env.BUILD_VERSION ?? getCurrentGitHash();
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentGitBranch(): string {
|
||||||
|
const gitBranch = execSync("git rev-parse --abbrev-ref HEAD")
|
||||||
|
.toString()
|
||||||
|
.trim();
|
||||||
|
return gitBranch;
|
||||||
|
}
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig(({ mode }) => {
|
export default defineConfig(({ mode }) => {
|
||||||
const env = loadEnv(mode, process.cwd(), "");
|
const env = loadEnv(mode, process.cwd(), "");
|
||||||
return {
|
return {
|
||||||
plugins: [react()],
|
plugins: [react()],
|
||||||
|
define: {
|
||||||
|
__BUILD_TIME__: `"${getBuildTime()}"`,
|
||||||
|
__BUILD_VERSION__: `"${getBuildVersion()}"`,
|
||||||
|
__GIT_BRANCH__: `"${getCurrentGitBranch()}"`,
|
||||||
|
__GIT_HASH__: `"${getCurrentGitHash()}"`,
|
||||||
|
},
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
|
@ -15,6 +15,8 @@ import { AllContentRouter } from "./all.ts";
|
|||||||
import type { ContentLocation } from "./context.ts";
|
import type { ContentLocation } from "./context.ts";
|
||||||
import { sendError } from "./error_handler.ts";
|
import { sendError } from "./error_handler.ts";
|
||||||
import { ParseQueryArgString, ParseQueryArray, ParseQueryBoolean, ParseQueryNumber } from "./util.ts";
|
import { ParseQueryArgString, ParseQueryArray, ParseQueryBoolean, ParseQueryNumber } from "./util.ts";
|
||||||
|
import { oshash } from "src/util/oshash.ts";
|
||||||
|
|
||||||
|
|
||||||
const ContentIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
|
const ContentIDHandler = (controller: DocumentAccessor) => async (ctx: Context, next: Next) => {
|
||||||
const num = Number.parseInt(ctx.params.num);
|
const num = Number.parseInt(ctx.params.num);
|
||||||
@ -154,6 +156,34 @@ const ContentHandler = (controller: DocumentAccessor) => async (ctx: Context, ne
|
|||||||
await next();
|
await next();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function RehashContentHandler(controller: DocumentAccessor) {
|
||||||
|
return async (ctx: Context, next: Next) => {
|
||||||
|
const num = Number.parseInt(ctx.params.num);
|
||||||
|
const c = await controller.findById(num);
|
||||||
|
if (c === undefined || c.deleted_at !== null) {
|
||||||
|
return sendError(404);
|
||||||
|
}
|
||||||
|
const filepath = join(c.basepath, c.filename);
|
||||||
|
let new_hash: string;
|
||||||
|
try {
|
||||||
|
new_hash = (await oshash(filepath)).toString();
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
// if file is not found, return 404
|
||||||
|
if ( (e as NodeJS.ErrnoException).code === "ENOENT") {
|
||||||
|
return sendError(404, "file not found");
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
const r = await controller.update({
|
||||||
|
id: num,
|
||||||
|
content_hash: new_hash,
|
||||||
|
});
|
||||||
|
ctx.body = JSON.stringify(r);
|
||||||
|
ctx.type = "json";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const getContentRouter = (controller: DocumentAccessor) => {
|
export const getContentRouter = (controller: DocumentAccessor) => {
|
||||||
const ret = new Router();
|
const ret = new Router();
|
||||||
ret.get("/search", PerCheck(Per.QueryContent), ContentQueryHandler(controller));
|
ret.get("/search", PerCheck(Per.QueryContent), ContentQueryHandler(controller));
|
||||||
@ -167,6 +197,7 @@ export const getContentRouter = (controller: DocumentAccessor) => {
|
|||||||
ret.del("/:num(\\d+)", AdminOnly, DeleteContentHandler(controller));
|
ret.del("/:num(\\d+)", AdminOnly, DeleteContentHandler(controller));
|
||||||
ret.all("/:num(\\d+)/(.*)", PerCheck(Per.QueryContent), ContentHandler(controller));
|
ret.all("/:num(\\d+)/(.*)", PerCheck(Per.QueryContent), ContentHandler(controller));
|
||||||
ret.use("/:num(\\d+)", PerCheck(Per.QueryContent), new AllContentRouter().routes());
|
ret.use("/:num(\\d+)", PerCheck(Per.QueryContent), new AllContentRouter().routes());
|
||||||
|
ret.post("/:num(\\d+)/_rehash", AdminOnly, RehashContentHandler(controller));
|
||||||
return ret;
|
return ret;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user