1
0
forked from baron/baron-sso

Merge pull request 'feature/ui-refactor' (#339) from feature/ui-refactor into dev

Reviewed-on: baron/baron-sso#339
This commit is contained in:
2026-02-25 17:12:59 +09:00
12 changed files with 887 additions and 380 deletions

View File

@@ -197,7 +197,20 @@ function AppLayout() {
))}
</div>
</nav>
<div className="hidden space-y-2 px-5 pb-6 text-xs text-[var(--color-muted)] md:block">
</div>
<div>
<div className="px-3 pt-4 border-t border-border/50">
<button
type="button"
onClick={handleLogout}
className="w-full flex items-center gap-3 rounded-xl px-3 py-3 text-sm transition text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
>
<LogOut size={18} />
<span>{t("ui.dev.nav.logout", "Logout")}</span>
</button>
</div>
<div className="hidden space-y-2 px-5 pb-6 pt-2 text-xs text-[var(--color-muted)] md:block">
<p>{t("msg.dev.sidebar.notice", "개발자 전용 콘솔입니다.")}</p>
<p>
{t(
@@ -207,17 +220,6 @@ function AppLayout() {
</p>
</div>
</div>
<div className="px-2 pb-6 md:px-3">
<button
type="button"
onClick={handleLogout}
className="flex w-full items-center gap-3 rounded-xl px-3 py-3 text-sm text-muted-foreground transition hover:bg-muted/10 hover:text-foreground"
>
<LogOut size={18} />
<span>{t("ui.dev.nav.logout", "Logout")}</span>
</button>
</div>
</aside>
<div className="relative">

View File

@@ -1,6 +1,6 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import type { AxiosError } from "axios";
import { Plus, Shield, Sparkles, Trash2, Upload } from "lucide-react";
import { Plus, Save, Shield, Sparkles, Trash2, Upload } from "lucide-react";
import { useEffect, useState } from "react";
import { Link, useNavigate, useParams } from "react-router-dom";
import { Badge } from "../../components/ui/badge";
@@ -16,6 +16,7 @@ import { Input } from "../../components/ui/input";
import { Label } from "../../components/ui/label";
import { Switch } from "../../components/ui/switch";
import { Textarea } from "../../components/ui/textarea";
import { toast } from "../../components/ui/use-toast";
import {
createClient,
deleteClient,
@@ -128,6 +129,21 @@ function ClientGeneralPage() {
setScopes(scopes.filter((s) => s.id !== id));
};
const handleStatusChange = (nextStatus: ClientStatus) => {
setStatus(nextStatus);
const statusLabel =
nextStatus === "active"
? t("ui.common.status.active", "Active")
: t("ui.common.status.inactive", "Inactive");
toast(
t(
"msg.dev.clients.general.status_changed",
"상태가 {{status}}로 변경되었습니다.",
{ status: statusLabel },
),
);
};
const mutation = useMutation({
mutationFn: async () => {
const scopeNames = scopes.map((scope) => scope.name).filter(Boolean);
@@ -204,7 +220,7 @@ function ClientGeneralPage() {
window.confirm(
t(
"msg.dev.clients.delete_confirm",
"정말로 이 앱을 삭제하시습니까? 이 작업은 되돌릴 수 없습니다.",
"정말로 이 앱을 삭제하시습니까? 이 작업은 되돌릴 수 없습니다.",
),
)
) {
@@ -309,33 +325,6 @@ function ClientGeneralPage() {
)}
</CardDescription>
</div>
{!isCreate && (
<div className="flex flex-col items-end gap-2">
<Label className="text-xs font-bold uppercase tracking-wider text-muted-foreground">
{t("ui.dev.clients.table.status", "상태")}
</Label>
<div className="flex items-center gap-3">
<Switch
checked={status === "active"}
onCheckedChange={(checked) =>
setStatus(checked ? "active" : "inactive")
}
/>
<span
className={cn(
"text-sm font-medium",
status === "active"
? "text-emerald-400"
: "text-muted-foreground",
)}
>
{status === "active"
? t("ui.common.status.active", "활성")
: t("ui.common.status.inactive", "비활성")}
</span>
</div>
</div>
)}
</div>
<div className="grid gap-8 md:grid-cols-2">
<div className="space-y-5">
@@ -371,40 +360,66 @@ function ClientGeneralPage() {
/>
</div>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">
{t("ui.dev.clients.general.identity.logo", "App Logo URL")}
</Label>
<div className="flex gap-4">
<div className="flex-1 space-y-2">
<Input
value={logoUrl}
onChange={(e) => setLogoUrl(e.target.value)}
placeholder={t(
"ui.dev.clients.general.identity.logo_placeholder",
"https://example.com/logo.png",
)}
/>
<p className="text-xs text-muted-foreground">
{t(
"msg.dev.clients.general.identity.logo_help",
"인증 화면에 표시될 PNG/SVG URL입니다.",
)}
</p>
</div>
<div className="flex h-20 w-20 items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted/40 shrink-0">
{logoUrl ? (
<img
src={logoUrl}
alt={t(
"ui.dev.clients.general.identity.logo_preview",
"Logo Preview",
<div className="space-y-5">
<div className="space-y-2">
<Label className="text-sm font-semibold">
{t("ui.dev.clients.general.identity.logo", "App Logo URL")}
</Label>
<div className="flex gap-4">
<div className="flex-1 space-y-2">
<Input
value={logoUrl}
onChange={(e) => setLogoUrl(e.target.value)}
placeholder={t(
"ui.dev.clients.general.identity.logo_placeholder",
"https://example.com/logo.png",
)}
className="h-full w-full object-contain"
/>
) : (
<Upload className="h-5 w-5 text-muted-foreground" />
)}
<p className="text-xs text-muted-foreground">
{t(
"msg.dev.clients.general.identity.logo_help",
"인증 화면에 표시될 PNG/SVG URL입니다.",
)}
</p>
</div>
<div className="flex h-20 w-20 items-center justify-center rounded-lg border-2 border-dashed border-border bg-muted/40 shrink-0">
{logoUrl ? (
<img
src={logoUrl}
alt={t(
"ui.dev.clients.general.identity.logo_preview",
"Logo Preview",
)}
className="h-full w-full object-contain"
/>
) : (
<Upload className="h-5 w-5 text-muted-foreground" />
)}
</div>
</div>
</div>
<div className="space-y-2">
<Label className="text-sm font-semibold">
{t("ui.dev.clients.table.status", "상태")}
</Label>
<div className="flex gap-2">
<Button
type="button"
size="sm"
variant={status === "active" ? "default" : "outline"}
onClick={() => handleStatusChange("active")}
>
{t("ui.common.status.active", "활성")}
</Button>
<Button
type="button"
size="sm"
variant={status === "inactive" ? "default" : "outline"}
onClick={() => handleStatusChange("inactive")}
>
{t("ui.common.status.inactive", "비활성")}
</Button>
</div>
</div>
</div>
@@ -658,20 +673,30 @@ function ClientGeneralPage() {
</Button>
)}
</div>
<div className="flex items-center gap-3">
<div className="flex flex-col-reverse sm:flex-row sm:justify-end gap-2">
<Button variant="outline" onClick={() => navigate("/clients")}>
{t("ui.common.cancel", "취소")}
</Button>
<Button
onClick={() => mutation.mutate()}
disabled={mutation.isPending}
className="px-8 shadow-lg shadow-primary/20"
disabled={
mutation.isPending ||
isLoading ||
name.trim() === "" ||
(isCreate && redirectUris.trim() === "")
}
className="shadow-lg shadow-primary/20"
>
{mutation.isPending ? (
<div className="h-4 w-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2" />
) : (
<Save size={16} className="mr-2" />
)}
{mutation.isPending
? t("msg.common.saving", "저장 중...")
: isCreate
? t("ui.dev.clients.general.create", "클라이언트 생성")
: t("ui.dev.clients.general.save", "설정 저장")}
: t("ui.common.save", "저장")}
</Button>
</div>
</div>

View File

@@ -1,11 +1,30 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Plus, Trash2, Edit, Globe, Save } from "lucide-react";
import { useState } from "react";
import { useParams } from "react-router-dom";
import { Button } from "../../../components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "../../../components/ui/card";
import { Input } from "../../../components/ui/input";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "../../../components/ui/table";
import {
createIdpConfigForClient,
listIdpConfigsForClient,
} from "../../../lib/devApi";
import type { IdpConfig, IdpConfigCreateRequest } from "../../../lib/devApi";
import { t } from "../../../lib/i18n";
// Proper Modal Component with Form
const CreateIdpModal = ({
@@ -37,12 +56,10 @@ const CreateIdpModal = ({
onClose();
},
onError: (error) => {
// Basic error handling
alert(`Failed to create configuration: ${error.message}`);
},
});
// 이 내용으로 교체해주세요
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
) => {
@@ -61,104 +78,100 @@ const CreateIdpModal = ({
if (!isOpen) return null;
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white p-6 rounded-lg shadow-xl w-full max-w-lg">
<h2 className="text-xl font-bold mb-4">Add New IdP Configuration</h2>
<form onSubmit={handleSubmit}>
{/* Display Name */}
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2">
Display Name
</label>
<input
type="text"
name="display_name"
value={formData.display_name}
onChange={handleChange}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
required
/>
</div>
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 backdrop-blur-sm">
<Card className="w-full max-w-lg shadow-2xl animate-in zoom-in-95 duration-200">
<CardHeader>
<CardTitle>
{t("ui.dev.clients.federation.add_title", "Add Identity Provider")}
</CardTitle>
<CardDescription>
{t(
"msg.dev.clients.federation.add_subtitle",
"Connect an external OIDC provider.",
)}
</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-semibold">Display Name</label>
<Input
name="display_name"
value={formData.display_name}
onChange={handleChange}
placeholder="e.g. Google Workspace"
required
/>
</div>
{/* Issuer URL */}
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2">
Issuer URL
</label>
<input
type="url"
name="issuer_url"
value={formData.issuer_url}
onChange={handleChange}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
placeholder="https://accounts.google.com"
required
/>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold">Issuer URL</label>
<Input
type="url"
name="issuer_url"
value={formData.issuer_url}
onChange={handleChange}
placeholder="https://accounts.google.com"
required
/>
</div>
{/* Client ID */}
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2">
Client ID
</label>
<input
type="text"
name="oidc_client_id"
value={formData.oidc_client_id}
onChange={handleChange}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-semibold">Client ID</label>
<Input
name="oidc_client_id"
value={formData.oidc_client_id}
onChange={handleChange}
required
/>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold">Client Secret</label>
<Input
type="password"
name="oidc_client_secret"
value={formData.oidc_client_secret}
onChange={handleChange}
required
/>
</div>
</div>
{/* Client Secret */}
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2">
Client Secret
</label>
<input
type="password"
name="oidc_client_secret"
value={formData.oidc_client_secret}
onChange={handleChange}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
required
/>
</div>
<div className="space-y-2">
<label className="text-sm font-semibold">Scopes</label>
<Input
name="scopes"
value={formData.scopes}
onChange={handleChange}
/>
</div>
{/* Scopes */}
<div className="mb-4">
<label className="block text-gray-700 text-sm font-bold mb-2">
Scopes
</label>
<input
type="text"
name="scopes"
value={formData.scopes}
onChange={handleChange}
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
/>
</div>
{/* Action Buttons */}
<div className="flex items-center justify-end">
<button
type="button"
onClick={onClose}
className="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mr-2"
>
Cancel
</button>
<button
type="submit"
disabled={mutation.isPending}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded disabled:opacity-50"
>
{mutation.isPending ? "Saving..." : "Save Configuration"}
</button>
</div>
</form>
</div>
<div className="flex flex-col-reverse sm:flex-row sm:justify-end gap-2 pt-4">
<Button type="button" variant="outline" onClick={onClose}>
{t("ui.common.cancel", "Cancel")}
</Button>
<Button
type="submit"
disabled={
mutation.isPending ||
formData.display_name.trim() === "" ||
formData.issuer_url.trim() === ""
}
>
{mutation.isPending ? (
<div className="h-4 w-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2" />
) : (
<Save size={16} className="mr-2" />
)}
{mutation.isPending
? t("msg.common.saving", "Saving...")
: t("ui.common.save", "Save Configuration")}
</Button>
</div>
</form>
</CardContent>
</Card>
</div>
);
};
@@ -168,7 +181,11 @@ export function ClientFederationPage() {
const [isCreateModalOpen, setCreateModalOpen] = useState(false);
if (!clientId) {
return <div>Client ID is missing</div>;
return (
<div className="p-8 text-center text-destructive">
Client ID is missing
</div>
);
}
const { data, isLoading, error } = useQuery({
@@ -177,94 +194,113 @@ export function ClientFederationPage() {
});
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4">Identity Federation Settings</h1>
<p className="mb-4 text-gray-600">
Manage external identity providers for this application.
</p>
<div className="space-y-6 p-1">
<header className="flex flex-wrap items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-bold flex items-center gap-2">
<Globe className="h-6 w-6 text-primary" />
{t("ui.dev.clients.federation.title", "Identity Federation")}
</h1>
<p className="text-muted-foreground">
{t(
"msg.dev.clients.federation.subtitle",
"Manage external identity providers for this application.",
)}
</p>
</div>
<Button onClick={() => setCreateModalOpen(true)} className="gap-2">
<Plus className="h-4 w-4" />
{t("ui.dev.clients.federation.add_btn", "Add Provider")}
</Button>
</header>
<div className="mb-4">
<button
type="button"
onClick={() => setCreateModalOpen(true)}
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
>
+ Add IdP Configuration
</button>
</div>
<Card className="glass-panel">
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Display Name</TableHead>
<TableHead>Provider Type</TableHead>
<TableHead>Status</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{isLoading ? (
<TableRow>
<TableCell colSpan={4} className="h-24 text-center">
{t("msg.common.loading", "Loading...")}
</TableCell>
</TableRow>
) : error ? (
<TableRow>
<TableCell
colSpan={4}
className="h-24 text-center text-destructive"
>
{(error as Error).message}
</TableCell>
</TableRow>
) : data?.length === 0 ? (
<TableRow>
<TableCell
colSpan={4}
className="h-24 text-center text-muted-foreground"
>
{t(
"msg.dev.clients.federation.empty",
"No IdP configurations found.",
)}
</TableCell>
</TableRow>
) : (
data?.map((config: IdpConfig) => (
<tr
key={config.id}
className="border-b transition-colors hover:bg-muted/50"
>
<TableCell className="font-medium">
{config.display_name}
</TableCell>
<TableCell>{config.provider_type.toUpperCase()}</TableCell>
<TableCell>
<span
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-semibold ${
config.status === "active"
? "bg-emerald-500/10 text-emerald-500"
: "bg-muted text-muted-foreground"
}`}
>
{config.status}
</span>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button variant="ghost" size="icon" className="h-8 w-8">
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-destructive"
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</tr>
))
)}
</TableBody>
</Table>
</CardContent>
</Card>
<CreateIdpModal
isOpen={isCreateModalOpen}
onClose={() => setCreateModalOpen(false)}
clientId={clientId}
/>
{isLoading && <div>Loading configurations...</div>}
{error && (
<div className="text-red-500">
Failed to load configurations: {error.message}
</div>
)}
{data && (
<div className="overflow-x-auto">
<table className="min-w-full bg-white">
<thead className="bg-gray-200">
<tr>
<th className="py-2 px-4 border-b">Display Name</th>
<th className="py-2 px-4 border-b">Provider Type</th>
<th className="py-2 px-4 border-b">Status</th>
<th className="py-2 px-4 border-b">Actions</th>
</tr>
</thead>
<tbody>
{data.length === 0 ? (
<tr>
<td colSpan={4} className="text-center py-4">
No IdP configurations found.
</td>
</tr>
) : (
data.map((config: IdpConfig) => (
<tr key={config.id}>
<td className="py-2 px-4 border-b">
{config.display_name}
</td>
<td className="py-2 px-4 border-b">
{config.provider_type.toUpperCase()}
</td>
<td className="py-2 px-4 border-b">
<span
className={`px-2 py-1 text-xs font-semibold rounded-full ${
config.status === "active"
? "bg-green-200 text-green-800"
: "bg-gray-200 text-gray-800"
}`}
>
{config.status}
</span>
</td>
<td className="py-2 px-4 border-b">
<button
type="button"
className="text-blue-500 hover:underline mr-2"
>
Edit
</button>
<button
type="button"
className="text-red-500 hover:underline"
>
Delete
</button>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
)}
</div>
);
}

View File

@@ -252,6 +252,12 @@ load_error = "Error loading client: {{error}}"
loading = "Loading client..."
saved = "Saved"
save_error = "Failed to save: {{error}}"
status_changed = "Status changed to {{status}}."
[msg.dev.clients.federation]
subtitle = "Manage external identity providers for this application."
add_subtitle = "Connect an external OIDC provider."
empty = "No IdP configurations found."
[msg.dev.clients.general.identity]
logo_help = "Logo Help"
@@ -1019,10 +1025,14 @@ settings = "Settings"
[ui.dev.clients.general]
create = "Create Application"
display_new = "Add Connected Application"
save = "Settings Save"
title_create = "Create Client"
title_edit = "Client Settings"
[ui.dev.clients.federation]
title = "Identity Federation"
add_title = "Add Identity Provider"
add_btn = "Add Provider"
[ui.dev.clients.general.breadcrumb]
section = "Applications"

View File

@@ -252,6 +252,12 @@ load_error = "Error loading client: {{error}}"
loading = "Loading client..."
saved = "설정이 저장되었습니다."
save_error = "저장 실패: {{error}}"
status_changed = "상태가 {{status}}로 변경되었습니다."
[msg.dev.clients.federation]
subtitle = "이 애플리케이션의 외부 IdP 설정을 관리합니다."
add_subtitle = "외부 OIDC 제공자를 연결합니다."
empty = "등록된 IdP 설정이 없습니다."
[msg.dev.clients.general.identity]
logo_help = "인증 화면에 표시될 PNG/SVG URL입니다."
@@ -1019,10 +1025,14 @@ settings = "Settings"
[ui.dev.clients.general]
create = "앱 생성"
display_new = "연동 앱 추가"
save = "설정 저장"
title_create = "Create Client"
title_edit = "Client Settings"
[ui.dev.clients.federation]
title = "Identity Federation"
add_title = "Add Identity Provider"
add_btn = "Add Provider"
[ui.dev.clients.general.breadcrumb]
section = "Applications"

View File

@@ -252,6 +252,12 @@ load_error = ""
loading = ""
saved = ""
save_error = ""
status_changed = ""
[msg.dev.clients.federation]
subtitle = ""
add_subtitle = ""
empty = ""
[msg.dev.clients.general.identity]
logo_help = ""
@@ -1031,10 +1037,14 @@ settings = ""
[ui.dev.clients.general]
create = ""
display_new = ""
save = ""
title_create = ""
title_edit = ""
[ui.dev.clients.federation]
title = ""
add_title = ""
add_btn = ""
[ui.dev.clients.general.breadcrumb]
section = ""

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -250,6 +250,12 @@ load_error = ""
loading = ""
saved = ""
save_error = ""
status_changed = ""
[msg.dev.clients.federation]
subtitle = ""
add_subtitle = ""
empty = ""
[msg.dev.clients.general.identity]
logo_help = ""
@@ -1030,10 +1036,14 @@ settings = ""
[ui.dev.clients.general]
create = ""
display_new = ""
save = ""
title_create = ""
title_edit = ""
[ui.dev.clients.federation]
title = ""
add_title = ""
add_btn = ""
[ui.dev.clients.general.breadcrumb]
section = ""

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long