Compare commits

...

3 Commits

Author SHA1 Message Date
f543ad1cf4 feat: rehashdoc 2024-10-13 02:00:43 +09:00
58adb46323 feat: Add BuildInfoCard component
This commit adds a new component called BuildInfoCard. The BuildInfoCard component displays information about the build, including the Git branch, Git hash, build time, and build version. It is used in the SettingPage component.

The packages/client/vite.config.ts file is modified to define environment variables for the build information. The __BUILD_TIME__, __BUILD_VERSION__, __GIT_BRANCH__, and __GIT_HASH__ variables are defined using functions that retrieve the current build time, build version, Git branch, and Git hash respectively.

This commit introduces new functionality and improves the user experience by providing build information in the application.
2024-10-13 01:40:01 +09:00
de1bb7dfde fix: fix lowercase issue in DescItem component 2024-10-13 01:38:18 +09:00
8 changed files with 144 additions and 5 deletions

View 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>
);
}

View File

@ -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>}
</> </>
)} )}

View File

@ -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}`);
};
} }

View File

@ -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}>

View File

@ -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>
) )
} }

View 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__,
};
}

View File

@ -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'),

View File

@ -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;
}; };