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 type { AxiosError } from "axios";
import {
ArrowDown,
ArrowUp,
ArrowUpDown,
Building2,
ChevronDown,
ChevronRight,
@@ -22,6 +19,7 @@ import {
} from "lucide-react";
import * as React from "react";
import { Link, useNavigate } from "react-router-dom";
import { SortableTableHead } from "../../../../../common/core/components/sort";
import {
type SortConfig,
type SortResolverMap,
@@ -513,17 +511,6 @@ function TenantListPage() {
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(
() => tenants.filter((tenant) => !isSeedTenant(tenant)),
[tenants],
@@ -977,69 +964,55 @@ function TenantListPage() {
}
/>
</TableHead>
<TableHead
className="min-w-[220px] cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap"
onClick={() => requestSort("id")}
>
<div className="flex items-center">
{t("ui.admin.tenants.table.id", "ID")}
{getSortIcon("id")}
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap"
onClick={() => requestSort("name")}
>
<div className="flex items-center">
{t("ui.admin.tenants.table.name", "NAME")}
{getSortIcon("name")}
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap"
onClick={() => requestSort("type")}
>
<div className="flex items-center">
{t("ui.admin.tenants.table.type", "TYPE")}
{getSortIcon("type")}
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap"
onClick={() => requestSort("slug")}
>
<div className="flex items-center">
{t("ui.admin.tenants.table.slug", "SLUG")}
{getSortIcon("slug")}
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap"
onClick={() => requestSort("status")}
>
<div className="flex items-center">
{t("ui.admin.tenants.table.status", "STATUS")}
{getSortIcon("status")}
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 transition-colors whitespace-nowrap"
onClick={() => requestSort("recursiveMemberCount")}
>
<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>
<SortableTableHead
className="min-w-[220px] whitespace-nowrap text-foreground sticky top-0 bg-inherit"
label={t("ui.admin.tenants.table.id", "ID")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="id"
/>
<SortableTableHead
className="whitespace-nowrap text-foreground sticky top-0 bg-inherit"
label={t("ui.admin.tenants.table.name", "NAME")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="name"
/>
<SortableTableHead
className="whitespace-nowrap text-foreground sticky top-0 bg-inherit"
label={t("ui.admin.tenants.table.type", "TYPE")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="type"
/>
<SortableTableHead
className="whitespace-nowrap text-foreground sticky top-0 bg-inherit"
label={t("ui.admin.tenants.table.slug", "SLUG")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="slug"
/>
<SortableTableHead
className="whitespace-nowrap text-foreground sticky top-0 bg-inherit"
label={t("ui.admin.tenants.table.status", "STATUS")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="status"
/>
<SortableTableHead
className="whitespace-nowrap text-foreground sticky top-0 bg-inherit"
label={t("ui.admin.tenants.table.members", "MEMBERS")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="recursiveMemberCount"
/>
<SortableTableHead
className="whitespace-nowrap text-foreground sticky top-0 bg-inherit"
label={t("ui.admin.tenants.table.updated", "UPDATED")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="updatedAt"
/>
<TableHead className="whitespace-nowrap">
{t("ui.common.actions", "액션")}
</TableHead>

View File

@@ -15,6 +15,13 @@
"moduleDetection": "force",
"noEmit": true,
"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 */
"strict": true,
@@ -24,6 +31,6 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"],
"include": ["src", "../common/**/*.ts", "../common/**/*.tsx"],
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
}

View File

@@ -1,4 +1,5 @@
import react from "@vitejs/plugin-react";
import path from "node:path";
import { defineConfig } from "vite";
const buildOutDir =
@@ -6,6 +7,20 @@ const buildOutDir =
export default defineConfig({
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_"],
cacheDir:
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 type { AxiosError } from "axios";
import {
ArrowDown,
ArrowUp,
ArrowUpDown,
BookOpenText,
Filter,
Plus,
@@ -13,6 +10,7 @@ import {
import { useEffect, useMemo, useState } from "react";
import { useAuth } from "react-oidc-context";
import { Link, useNavigate } from "react-router-dom";
import { SortableTableHead } from "../../../../common/core/components/sort";
import {
type SortConfig,
type SortResolverMap,
@@ -230,18 +228,6 @@ function ClientsPage() {
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) {
return (
<div className="p-8 text-center">
@@ -452,51 +438,41 @@ function ClientsPage() {
<Table>
<TableHeader>
<TableRow>
<TableHead
className="cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => requestSort("application")}
>
<div className="flex items-center">
{t("ui.dev.clients.table.application", "애플리케이션")}
{getSortIcon("application")}
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => requestSort("id")}
>
<div className="flex items-center">
{t("ui.dev.clients.table.client_id", "Client ID")}
{getSortIcon("id")}
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => requestSort("type")}
>
<div className="flex items-center">
{t("ui.dev.clients.table.type", "유형")}
{getSortIcon("type")}
</div>
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted/50 transition-colors"
onClick={() => requestSort("status")}
>
<div className="flex items-center">
{t("ui.dev.clients.table.status", "상태")}
{getSortIcon("status")}
</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>
<SortableTableHead
className="text-muted-foreground"
label={t("ui.dev.clients.table.application", "애플리케이션")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="application"
/>
<SortableTableHead
className="text-muted-foreground"
label={t("ui.dev.clients.table.client_id", "Client ID")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="id"
/>
<SortableTableHead
className="text-muted-foreground"
label={t("ui.dev.clients.table.type", "유형")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="type"
/>
<SortableTableHead
className="text-muted-foreground"
label={t("ui.dev.clients.table.status", "상태")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="status"
/>
<SortableTableHead
className="text-muted-foreground"
label={t("ui.dev.clients.table.created_at", "생성일")}
onSort={requestSort}
sortConfig={sortConfig}
sortKey="createdAt"
/>
<TableHead className="text-right">
{t("ui.dev.clients.table.actions", "액션")}
</TableHead>

View File

@@ -15,6 +15,13 @@
"moduleDetection": "force",
"noEmit": true,
"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 */
"strict": true,
@@ -24,6 +31,6 @@
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"],
"include": ["src", "../common/**/*.ts", "../common/**/*.tsx"],
"exclude": ["src/**/*.test.ts", "src/**/*.test.tsx"]
}

View File

@@ -1,4 +1,5 @@
import react from "@vitejs/plugin-react";
import path from "node:path";
import { defineConfig } from "vite";
const buildOutDir =
@@ -35,6 +36,20 @@ const allowedHosts = Array.from(
export default defineConfig({
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:
process.env.DEVFRONT_VITE_CACHE_DIR ?? "/tmp/baron-sso-devfront-vite-cache",
build: {