1
0
forked from baron/baron-sso

정렬 헤더 UI 공통화 및 devfront secret 표시 수정

This commit is contained in:
2026-05-13 14:15:30 +09:00
parent 498fdd802c
commit 187f0da29b
8 changed files with 291 additions and 139 deletions

View File

@@ -2,9 +2,6 @@ import { useInfiniteQuery, useMutation, useQuery } from "@tanstack/react-query";
import { useVirtualizer } from "@tanstack/react-virtual"; import { useVirtualizer } from "@tanstack/react-virtual";
import type { AxiosError } from "axios"; import type { AxiosError } from "axios";
import { import {
ArrowDown,
ArrowUp,
ArrowUpDown,
Building2, Building2,
ChevronDown, ChevronDown,
ChevronRight, ChevronRight,
@@ -22,6 +19,7 @@ import {
} from "lucide-react"; } from "lucide-react";
import * as React from "react"; import * as React from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { SortableTableHead } from "../../../../../common/core/components/sort";
import { import {
type SortConfig, type SortConfig,
type SortResolverMap, type SortResolverMap,
@@ -513,17 +511,6 @@ function TenantListPage() {
setSortConfig((current) => toggleSort(current, key)); setSortConfig((current) => toggleSort(current, key));
}; };
const getSortIcon = (key: TenantSortKey) => {
if (!sortConfig || sortConfig.key !== key) {
return <ArrowUpDown size={14} className="ml-1 opacity-50" />;
}
return sortConfig.direction === "asc" ? (
<ArrowUp size={14} className="ml-1" />
) : (
<ArrowDown size={14} className="ml-1" />
);
};
const deletableTenants = React.useMemo( const deletableTenants = React.useMemo(
() => tenants.filter((tenant) => !isSeedTenant(tenant)), () => tenants.filter((tenant) => !isSeedTenant(tenant)),
[tenants], [tenants],
@@ -977,69 +964,55 @@ function TenantListPage() {
} }
/> />
</TableHead> </TableHead>
<TableHead <SortableTableHead
className="min-w-[220px] cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap" className="min-w-[220px] whitespace-nowrap text-foreground sticky top-0 bg-inherit"
onClick={() => requestSort("id")} label={t("ui.admin.tenants.table.id", "ID")}
> onSort={requestSort}
<div className="flex items-center"> sortConfig={sortConfig}
{t("ui.admin.tenants.table.id", "ID")} sortKey="id"
{getSortIcon("id")} />
</div> <SortableTableHead
</TableHead> className="whitespace-nowrap text-foreground sticky top-0 bg-inherit"
<TableHead label={t("ui.admin.tenants.table.name", "NAME")}
className="cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap" onSort={requestSort}
onClick={() => requestSort("name")} sortConfig={sortConfig}
> sortKey="name"
<div className="flex items-center"> />
{t("ui.admin.tenants.table.name", "NAME")} <SortableTableHead
{getSortIcon("name")} className="whitespace-nowrap text-foreground sticky top-0 bg-inherit"
</div> label={t("ui.admin.tenants.table.type", "TYPE")}
</TableHead> onSort={requestSort}
<TableHead sortConfig={sortConfig}
className="cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap" sortKey="type"
onClick={() => requestSort("type")} />
> <SortableTableHead
<div className="flex items-center"> className="whitespace-nowrap text-foreground sticky top-0 bg-inherit"
{t("ui.admin.tenants.table.type", "TYPE")} label={t("ui.admin.tenants.table.slug", "SLUG")}
{getSortIcon("type")} onSort={requestSort}
</div> sortConfig={sortConfig}
</TableHead> sortKey="slug"
<TableHead />
className="cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap" <SortableTableHead
onClick={() => requestSort("slug")} className="whitespace-nowrap text-foreground sticky top-0 bg-inherit"
> label={t("ui.admin.tenants.table.status", "STATUS")}
<div className="flex items-center"> onSort={requestSort}
{t("ui.admin.tenants.table.slug", "SLUG")} sortConfig={sortConfig}
{getSortIcon("slug")} sortKey="status"
</div> />
</TableHead> <SortableTableHead
<TableHead className="whitespace-nowrap text-foreground sticky top-0 bg-inherit"
className="cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap" label={t("ui.admin.tenants.table.members", "MEMBERS")}
onClick={() => requestSort("status")} onSort={requestSort}
> sortConfig={sortConfig}
<div className="flex items-center"> sortKey="recursiveMemberCount"
{t("ui.admin.tenants.table.status", "STATUS")} />
{getSortIcon("status")} <SortableTableHead
</div> className="whitespace-nowrap text-foreground sticky top-0 bg-inherit"
</TableHead> label={t("ui.admin.tenants.table.updated", "UPDATED")}
<TableHead onSort={requestSort}
className="cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap" sortConfig={sortConfig}
onClick={() => requestSort("recursiveMemberCount")} sortKey="updatedAt"
> />
<div className="flex items-center">
{t("ui.admin.tenants.table.members", "MEMBERS")}
{getSortIcon("recursiveMemberCount")}
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap"
onClick={() => requestSort("updatedAt")}
>
<div className="flex items-center">
{t("ui.admin.tenants.table.updated", "UPDATED")}
{getSortIcon("updatedAt")}
</div>
</TableHead>
<TableHead className="whitespace-nowrap"> <TableHead className="whitespace-nowrap">
{t("ui.common.actions", "액션")} {t("ui.common.actions", "액션")}
</TableHead> </TableHead>

View File

@@ -15,6 +15,13 @@
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"lucide-react": ["./node_modules/lucide-react"],
"react": ["./node_modules/@types/react/index.d.ts"],
"react/jsx-dev-runtime": ["./node_modules/@types/react/jsx-dev-runtime.d.ts"],
"react/jsx-runtime": ["./node_modules/@types/react/jsx-runtime.d.ts"]
},
/* Linting */ /* Linting */
"strict": true, "strict": true,
@@ -24,6 +31,6 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["src"], "include": ["src", "../common/**/*.ts", "../common/**/*.tsx"],
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"] "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
} }

View File

@@ -1,4 +1,5 @@
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import path from "node:path";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
const buildOutDir = const buildOutDir =
@@ -6,6 +7,20 @@ const buildOutDir =
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
resolve: {
alias: {
"lucide-react": path.resolve(__dirname, "node_modules/lucide-react"),
react: path.resolve(__dirname, "node_modules/react"),
"react/jsx-dev-runtime": path.resolve(
__dirname,
"node_modules/react/jsx-dev-runtime.js",
),
"react/jsx-runtime": path.resolve(
__dirname,
"node_modules/react/jsx-runtime.js",
),
},
},
envPrefix: ["VITE_", "USERFRONT_", "ORGFRONT_"], envPrefix: ["VITE_", "USERFRONT_", "ORGFRONT_"],
cacheDir: cacheDir:
process.env.ADMINFRONT_VITE_CACHE_DIR ?? process.env.ADMINFRONT_VITE_CACHE_DIR ??

View File

@@ -0,0 +1,158 @@
import type { ReactNode, ThHTMLAttributes } from "react";
import type { SortConfig } from "../../utils";
function SortAscendingIcon() {
return (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m12 5-5 5" />
<path d="m12 5 5 5" />
<path d="M12 19V5" />
</svg>
);
}
function SortDescendingIcon() {
return (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="h-4 w-4"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m12 19-5-5" />
<path d="m12 19 5-5" />
<path d="M12 5v14" />
</svg>
);
}
function SortIdleIcon() {
return (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
className="h-4 w-4 opacity-50"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="m7 15 5 5 5-5" />
<path d="m7 9 5-5 5 5" />
</svg>
);
}
type SortableTableHeadAlign = "left" | "center" | "right";
function alignClassName(align: SortableTableHeadAlign) {
switch (align) {
case "center":
return "text-center";
case "right":
return "text-right";
default:
return "text-left";
}
}
function buttonAlignClassName(align: SortableTableHeadAlign) {
switch (align) {
case "center":
return "justify-center";
case "right":
return "justify-end";
default:
return "justify-start";
}
}
function sortAriaValue(
isActive: boolean,
direction: "asc" | "desc" | null,
): ThHTMLAttributes<HTMLTableCellElement>["aria-sort"] {
if (!isActive || direction === null) {
return "none";
}
return direction === "asc" ? "ascending" : "descending";
}
type SortableTableHeadProps<Key extends string> = Omit<
ThHTMLAttributes<HTMLTableCellElement>,
"children"
> & {
align?: SortableTableHeadAlign;
contentClassName?: string;
disabled?: boolean;
label: ReactNode;
onSort: (key: Key) => void;
sortConfig: SortConfig<Key> | null;
sortKey: Key;
};
export function SortableTableHead<Key extends string>({
align = "left",
className = "",
contentClassName = "",
disabled = false,
label,
onSort,
sortConfig,
sortKey,
...props
}: SortableTableHeadProps<Key>) {
const isActive = sortConfig?.key === sortKey;
const direction = isActive ? sortConfig?.direction ?? null : null;
return (
<th
aria-sort={sortAriaValue(isActive, direction)}
className={[
"h-12 px-6 text-xs font-bold uppercase tracking-[0.08em] align-middle",
alignClassName(align),
disabled ? "" : "transition-colors hover:bg-muted/50",
className,
]
.filter(Boolean)
.join(" ")}
{...props}
>
<button
type="button"
onClick={() => onSort(sortKey)}
disabled={disabled}
className={[
"flex w-full items-center gap-1",
buttonAlignClassName(align),
disabled ? "cursor-default opacity-70" : "cursor-pointer",
contentClassName,
]
.filter(Boolean)
.join(" ")}
>
<span>{label}</span>
{direction === "asc" ? (
<SortAscendingIcon />
) : direction === "desc" ? (
<SortDescendingIcon />
) : (
<SortIdleIcon />
)}
</button>
</th>
);
}

View File

@@ -0,0 +1 @@
export * from "./SortableTableHead";

View File

@@ -1,9 +1,6 @@
import { useMutation, useQuery } from "@tanstack/react-query"; import { useMutation, useQuery } from "@tanstack/react-query";
import type { AxiosError } from "axios"; import type { AxiosError } from "axios";
import { import {
ArrowDown,
ArrowUp,
ArrowUpDown,
BookOpenText, BookOpenText,
Filter, Filter,
Plus, Plus,
@@ -13,6 +10,7 @@ import {
import { useEffect, useMemo, useState } from "react"; import { useEffect, useMemo, useState } from "react";
import { useAuth } from "react-oidc-context"; import { useAuth } from "react-oidc-context";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { SortableTableHead } from "../../../../common/core/components/sort";
import { import {
type SortConfig, type SortConfig,
type SortResolverMap, type SortResolverMap,
@@ -230,18 +228,6 @@ function ClientsPage() {
setSortConfig((current) => toggleSort(current, key)); setSortConfig((current) => toggleSort(current, key));
}; };
const getSortIcon = (key: ClientSortKey) => {
if (!sortConfig || sortConfig.key !== key) {
return <ArrowUpDown className="ml-1 h-4 w-4 opacity-50" />;
}
return sortConfig.direction === "asc" ? (
<ArrowUp className="ml-1 h-4 w-4" />
) : (
<ArrowDown className="ml-1 h-4 w-4" />
);
};
if (auth.isLoading || !hasAccessToken || isLoading) { if (auth.isLoading || !hasAccessToken || isLoading) {
return ( return (
<div className="p-8 text-center"> <div className="p-8 text-center">
@@ -452,51 +438,41 @@ function ClientsPage() {
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead <SortableTableHead
className="cursor-pointer hover:bg-muted/50 transition-colors" className="text-muted-foreground"
onClick={() => requestSort("application")} label={t("ui.dev.clients.table.application", "애플리케이션")}
> onSort={requestSort}
<div className="flex items-center"> sortConfig={sortConfig}
{t("ui.dev.clients.table.application", "애플리케이션")} sortKey="application"
{getSortIcon("application")} />
</div> <SortableTableHead
</TableHead> className="text-muted-foreground"
<TableHead label={t("ui.dev.clients.table.client_id", "Client ID")}
className="cursor-pointer hover:bg-muted/50 transition-colors" onSort={requestSort}
onClick={() => requestSort("id")} sortConfig={sortConfig}
> sortKey="id"
<div className="flex items-center"> />
{t("ui.dev.clients.table.client_id", "Client ID")} <SortableTableHead
{getSortIcon("id")} className="text-muted-foreground"
</div> label={t("ui.dev.clients.table.type", "유형")}
</TableHead> onSort={requestSort}
<TableHead sortConfig={sortConfig}
className="cursor-pointer hover:bg-muted/50 transition-colors" sortKey="type"
onClick={() => requestSort("type")} />
> <SortableTableHead
<div className="flex items-center"> className="text-muted-foreground"
{t("ui.dev.clients.table.type", "유형")} label={t("ui.dev.clients.table.status", "상태")}
{getSortIcon("type")} onSort={requestSort}
</div> sortConfig={sortConfig}
</TableHead> sortKey="status"
<TableHead />
className="cursor-pointer hover:bg-muted/50 transition-colors" <SortableTableHead
onClick={() => requestSort("status")} className="text-muted-foreground"
> label={t("ui.dev.clients.table.created_at", "생성일")}
<div className="flex items-center"> onSort={requestSort}
{t("ui.dev.clients.table.status", "상태")} sortConfig={sortConfig}
{getSortIcon("status")} sortKey="createdAt"
</div> />
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => requestSort("createdAt")}
>
<div className="flex items-center">
{t("ui.dev.clients.table.created_at", "생성일")}
{getSortIcon("createdAt")}
</div>
</TableHead>
<TableHead className="text-right"> <TableHead className="text-right">
{t("ui.dev.clients.table.actions", "액션")} {t("ui.dev.clients.table.actions", "액션")}
</TableHead> </TableHead>

View File

@@ -15,6 +15,13 @@
"moduleDetection": "force", "moduleDetection": "force",
"noEmit": true, "noEmit": true,
"jsx": "react-jsx", "jsx": "react-jsx",
"baseUrl": ".",
"paths": {
"lucide-react": ["./node_modules/lucide-react"],
"react": ["./node_modules/@types/react/index.d.ts"],
"react/jsx-dev-runtime": ["./node_modules/@types/react/jsx-dev-runtime.d.ts"],
"react/jsx-runtime": ["./node_modules/@types/react/jsx-runtime.d.ts"]
},
/* Linting */ /* Linting */
"strict": true, "strict": true,
@@ -24,6 +31,6 @@
"noFallthroughCasesInSwitch": true, "noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true "noUncheckedSideEffectImports": true
}, },
"include": ["src"], "include": ["src", "../common/**/*.ts", "../common/**/*.tsx"],
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"] "exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
} }

View File

@@ -1,4 +1,5 @@
import react from "@vitejs/plugin-react"; import react from "@vitejs/plugin-react";
import path from "node:path";
import { defineConfig } from "vite"; import { defineConfig } from "vite";
const buildOutDir = const buildOutDir =
@@ -35,6 +36,20 @@ const allowedHosts = Array.from(
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
resolve: {
alias: {
"lucide-react": path.resolve(__dirname, "node_modules/lucide-react"),
react: path.resolve(__dirname, "node_modules/react"),
"react/jsx-dev-runtime": path.resolve(
__dirname,
"node_modules/react/jsx-dev-runtime.js",
),
"react/jsx-runtime": path.resolve(
__dirname,
"node_modules/react/jsx-runtime.js",
),
},
},
cacheDir: cacheDir:
process.env.DEVFRONT_VITE_CACHE_DIR ?? "/tmp/baron-sso-devfront-vite-cache", process.env.DEVFRONT_VITE_CACHE_DIR ?? "/tmp/baron-sso-devfront-vite-cache",
build: { build: {