forked from baron/baron-sso
devfront UI 일관성 및 정렬 정책 적용
This commit is contained in:
@@ -197,7 +197,20 @@ function AppLayout() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</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("msg.dev.sidebar.notice", "개발자 전용 콘솔입니다.")}</p>
|
||||||
<p>
|
<p>
|
||||||
{t(
|
{t(
|
||||||
@@ -207,17 +220,6 @@ function AppLayout() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</aside>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
import type { AxiosError } from "axios";
|
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 { useEffect, useState } from "react";
|
||||||
import { Link, useNavigate, useParams } from "react-router-dom";
|
import { Link, useNavigate, useParams } from "react-router-dom";
|
||||||
import { Badge } from "../../components/ui/badge";
|
import { Badge } from "../../components/ui/badge";
|
||||||
@@ -16,6 +16,7 @@ import { Input } from "../../components/ui/input";
|
|||||||
import { Label } from "../../components/ui/label";
|
import { Label } from "../../components/ui/label";
|
||||||
import { Switch } from "../../components/ui/switch";
|
import { Switch } from "../../components/ui/switch";
|
||||||
import { Textarea } from "../../components/ui/textarea";
|
import { Textarea } from "../../components/ui/textarea";
|
||||||
|
import { toast } from "../../components/ui/use-toast";
|
||||||
import {
|
import {
|
||||||
createClient,
|
createClient,
|
||||||
deleteClient,
|
deleteClient,
|
||||||
@@ -128,6 +129,21 @@ function ClientGeneralPage() {
|
|||||||
setScopes(scopes.filter((s) => s.id !== id));
|
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({
|
const mutation = useMutation({
|
||||||
mutationFn: async () => {
|
mutationFn: async () => {
|
||||||
const scopeNames = scopes.map((scope) => scope.name).filter(Boolean);
|
const scopeNames = scopes.map((scope) => scope.name).filter(Boolean);
|
||||||
@@ -204,7 +220,7 @@ function ClientGeneralPage() {
|
|||||||
window.confirm(
|
window.confirm(
|
||||||
t(
|
t(
|
||||||
"msg.dev.clients.delete_confirm",
|
"msg.dev.clients.delete_confirm",
|
||||||
"정말로 이 앱을 삭제하시겠습니까? 이 작업은 되돌릴 수 없습니다.",
|
"정말로 이 앱을 삭제하시습니까? 이 작업은 되돌릴 수 없습니다.",
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
@@ -309,33 +325,6 @@ function ClientGeneralPage() {
|
|||||||
)}
|
)}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</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>
|
||||||
<div className="grid gap-8 md:grid-cols-2">
|
<div className="grid gap-8 md:grid-cols-2">
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
@@ -371,40 +360,66 @@ function ClientGeneralPage() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-5">
|
||||||
<Label className="text-sm font-semibold">
|
<div className="space-y-2">
|
||||||
{t("ui.dev.clients.general.identity.logo", "App Logo URL")}
|
<Label className="text-sm font-semibold">
|
||||||
</Label>
|
{t("ui.dev.clients.general.identity.logo", "App Logo URL")}
|
||||||
<div className="flex gap-4">
|
</Label>
|
||||||
<div className="flex-1 space-y-2">
|
<div className="flex gap-4">
|
||||||
<Input
|
<div className="flex-1 space-y-2">
|
||||||
value={logoUrl}
|
<Input
|
||||||
onChange={(e) => setLogoUrl(e.target.value)}
|
value={logoUrl}
|
||||||
placeholder={t(
|
onChange={(e) => setLogoUrl(e.target.value)}
|
||||||
"ui.dev.clients.general.identity.logo_placeholder",
|
placeholder={t(
|
||||||
"https://example.com/logo.png",
|
"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",
|
|
||||||
)}
|
)}
|
||||||
className="h-full w-full object-contain"
|
|
||||||
/>
|
/>
|
||||||
) : (
|
<p className="text-xs text-muted-foreground">
|
||||||
<Upload className="h-5 w-5 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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -658,20 +673,30 @@ function ClientGeneralPage() {
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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")}>
|
<Button variant="outline" onClick={() => navigate("/clients")}>
|
||||||
{t("ui.common.cancel", "취소")}
|
{t("ui.common.cancel", "취소")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => mutation.mutate()}
|
onClick={() => mutation.mutate()}
|
||||||
disabled={mutation.isPending}
|
disabled={
|
||||||
className="px-8 shadow-lg shadow-primary/20"
|
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
|
{mutation.isPending
|
||||||
? t("msg.common.saving", "저장 중...")
|
? t("msg.common.saving", "저장 중...")
|
||||||
: isCreate
|
: isCreate
|
||||||
? t("ui.dev.clients.general.create", "클라이언트 생성")
|
? t("ui.dev.clients.general.create", "클라이언트 생성")
|
||||||
: t("ui.dev.clients.general.save", "설정 저장")}
|
: t("ui.common.save", "저장")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,11 +1,30 @@
|
|||||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { Plus, Trash2, Edit, Globe, Save } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
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 {
|
import {
|
||||||
createIdpConfigForClient,
|
createIdpConfigForClient,
|
||||||
listIdpConfigsForClient,
|
listIdpConfigsForClient,
|
||||||
} from "../../../lib/devApi";
|
} from "../../../lib/devApi";
|
||||||
import type { IdpConfig, IdpConfigCreateRequest } from "../../../lib/devApi";
|
import type { IdpConfig, IdpConfigCreateRequest } from "../../../lib/devApi";
|
||||||
|
import { t } from "../../../lib/i18n";
|
||||||
|
|
||||||
// Proper Modal Component with Form
|
// Proper Modal Component with Form
|
||||||
const CreateIdpModal = ({
|
const CreateIdpModal = ({
|
||||||
@@ -37,12 +56,10 @@ const CreateIdpModal = ({
|
|||||||
onClose();
|
onClose();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
// Basic error handling
|
|
||||||
alert(`Failed to create configuration: ${error.message}`);
|
alert(`Failed to create configuration: ${error.message}`);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// 이 내용으로 교체해주세요
|
|
||||||
const handleChange = (
|
const handleChange = (
|
||||||
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
||||||
) => {
|
) => {
|
||||||
@@ -61,104 +78,89 @@ const CreateIdpModal = ({
|
|||||||
if (!isOpen) return null;
|
if (!isOpen) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50 backdrop-blur-sm">
|
||||||
<div className="bg-white p-6 rounded-lg shadow-xl w-full max-w-lg">
|
<Card className="w-full max-w-lg shadow-2xl animate-in zoom-in-95 duration-200">
|
||||||
<h2 className="text-xl font-bold mb-4">Add New IdP Configuration</h2>
|
<CardHeader>
|
||||||
<form onSubmit={handleSubmit}>
|
<CardTitle>{t("ui.dev.clients.federation.add_title", "Add Identity Provider")}</CardTitle>
|
||||||
{/* Display Name */}
|
<CardDescription>
|
||||||
<div className="mb-4">
|
{t("msg.dev.clients.federation.add_subtitle", "Connect an external OIDC provider.")}
|
||||||
<label className="block text-gray-700 text-sm font-bold mb-2">
|
</CardDescription>
|
||||||
Display Name
|
</CardHeader>
|
||||||
</label>
|
<CardContent>
|
||||||
<input
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
type="text"
|
<div className="space-y-2">
|
||||||
name="display_name"
|
<label className="text-sm font-semibold">Display Name</label>
|
||||||
value={formData.display_name}
|
<Input
|
||||||
onChange={handleChange}
|
name="display_name"
|
||||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
value={formData.display_name}
|
||||||
required
|
onChange={handleChange}
|
||||||
/>
|
placeholder="e.g. Google Workspace"
|
||||||
</div>
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Issuer URL */}
|
<div className="space-y-2">
|
||||||
<div className="mb-4">
|
<label className="text-sm font-semibold">Issuer URL</label>
|
||||||
<label className="block text-gray-700 text-sm font-bold mb-2">
|
<Input
|
||||||
Issuer URL
|
type="url"
|
||||||
</label>
|
name="issuer_url"
|
||||||
<input
|
value={formData.issuer_url}
|
||||||
type="url"
|
onChange={handleChange}
|
||||||
name="issuer_url"
|
placeholder="https://accounts.google.com"
|
||||||
value={formData.issuer_url}
|
required
|
||||||
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>
|
||||||
placeholder="https://accounts.google.com"
|
|
||||||
required
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Client ID */}
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div className="mb-4">
|
<div className="space-y-2">
|
||||||
<label className="block text-gray-700 text-sm font-bold mb-2">
|
<label className="text-sm font-semibold">Client ID</label>
|
||||||
Client ID
|
<Input
|
||||||
</label>
|
name="oidc_client_id"
|
||||||
<input
|
value={formData.oidc_client_id}
|
||||||
type="text"
|
onChange={handleChange}
|
||||||
name="oidc_client_id"
|
required
|
||||||
value={formData.oidc_client_id}
|
/>
|
||||||
onChange={handleChange}
|
</div>
|
||||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
<div className="space-y-2">
|
||||||
required
|
<label className="text-sm font-semibold">Client Secret</label>
|
||||||
/>
|
<Input
|
||||||
</div>
|
type="password"
|
||||||
|
name="oidc_client_secret"
|
||||||
|
value={formData.oidc_client_secret}
|
||||||
|
onChange={handleChange}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Client Secret */}
|
<div className="space-y-2">
|
||||||
<div className="mb-4">
|
<label className="text-sm font-semibold">Scopes</label>
|
||||||
<label className="block text-gray-700 text-sm font-bold mb-2">
|
<Input
|
||||||
Client Secret
|
name="scopes"
|
||||||
</label>
|
value={formData.scopes}
|
||||||
<input
|
onChange={handleChange}
|
||||||
type="password"
|
/>
|
||||||
name="oidc_client_secret"
|
</div>
|
||||||
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>
|
|
||||||
|
|
||||||
{/* Scopes */}
|
<div className="flex flex-col-reverse sm:flex-row sm:justify-end gap-2 pt-4">
|
||||||
<div className="mb-4">
|
<Button type="button" variant="outline" onClick={onClose}>
|
||||||
<label className="block text-gray-700 text-sm font-bold mb-2">
|
{t("ui.common.cancel", "Cancel")}
|
||||||
Scopes
|
</Button>
|
||||||
</label>
|
<Button
|
||||||
<input
|
type="submit"
|
||||||
type="text"
|
disabled={mutation.isPending || formData.display_name.trim() === "" || formData.issuer_url.trim() === ""}
|
||||||
name="scopes"
|
>
|
||||||
value={formData.scopes}
|
{mutation.isPending ? (
|
||||||
onChange={handleChange}
|
<div className="h-4 w-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2" />
|
||||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
) : (
|
||||||
/>
|
<Save size={16} className="mr-2" />
|
||||||
</div>
|
)}
|
||||||
|
{mutation.isPending ? t("msg.common.saving", "Saving...") : t("ui.common.save", "Save Configuration")}
|
||||||
{/* Action Buttons */}
|
</Button>
|
||||||
<div className="flex items-center justify-end">
|
</div>
|
||||||
<button
|
</form>
|
||||||
type="button"
|
</CardContent>
|
||||||
onClick={onClose}
|
</Card>
|
||||||
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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -168,7 +170,7 @@ export function ClientFederationPage() {
|
|||||||
const [isCreateModalOpen, setCreateModalOpen] = useState(false);
|
const [isCreateModalOpen, setCreateModalOpen] = useState(false);
|
||||||
|
|
||||||
if (!clientId) {
|
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({
|
const { data, isLoading, error } = useQuery({
|
||||||
@@ -177,94 +179,92 @@ export function ClientFederationPage() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="space-y-6 p-1">
|
||||||
<h1 className="text-2xl font-bold mb-4">Identity Federation Settings</h1>
|
<header className="flex flex-wrap items-center justify-between gap-4">
|
||||||
<p className="mb-4 text-gray-600">
|
<div>
|
||||||
Manage external identity providers for this application.
|
<h1 className="text-2xl font-bold flex items-center gap-2">
|
||||||
</p>
|
<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">
|
<Card className="glass-panel">
|
||||||
<button
|
<CardContent className="p-0">
|
||||||
type="button"
|
<Table>
|
||||||
onClick={() => setCreateModalOpen(true)}
|
<TableHeader>
|
||||||
className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
|
<TableRow>
|
||||||
>
|
<TableHead>Display Name</TableHead>
|
||||||
+ Add IdP Configuration
|
<TableHead>Provider Type</TableHead>
|
||||||
</button>
|
<TableHead>Status</TableHead>
|
||||||
</div>
|
<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
|
<CreateIdpModal
|
||||||
isOpen={isCreateModalOpen}
|
isOpen={isCreateModalOpen}
|
||||||
onClose={() => setCreateModalOpen(false)}
|
onClose={() => setCreateModalOpen(false)}
|
||||||
clientId={clientId}
|
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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ load_error = "Error loading client: {{error}}"
|
|||||||
loading = "Loading client..."
|
loading = "Loading client..."
|
||||||
saved = "Saved"
|
saved = "Saved"
|
||||||
save_error = "Failed to save: {{error}}"
|
save_error = "Failed to save: {{error}}"
|
||||||
|
status_changed = "Status changed to {{status}}."
|
||||||
|
|
||||||
[msg.dev.clients.general.identity]
|
[msg.dev.clients.general.identity]
|
||||||
logo_help = "Logo Help"
|
logo_help = "Logo Help"
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ load_error = "Error loading client: {{error}}"
|
|||||||
loading = "Loading client..."
|
loading = "Loading client..."
|
||||||
saved = "설정이 저장되었습니다."
|
saved = "설정이 저장되었습니다."
|
||||||
save_error = "저장 실패: {{error}}"
|
save_error = "저장 실패: {{error}}"
|
||||||
|
status_changed = "상태가 {{status}}로 변경되었습니다."
|
||||||
|
|
||||||
[msg.dev.clients.general.identity]
|
[msg.dev.clients.general.identity]
|
||||||
logo_help = "인증 화면에 표시될 PNG/SVG URL입니다."
|
logo_help = "인증 화면에 표시될 PNG/SVG URL입니다."
|
||||||
|
|||||||
@@ -252,6 +252,7 @@ load_error = ""
|
|||||||
loading = ""
|
loading = ""
|
||||||
saved = ""
|
saved = ""
|
||||||
save_error = ""
|
save_error = ""
|
||||||
|
status_changed = ""
|
||||||
|
|
||||||
[msg.dev.clients.general.identity]
|
[msg.dev.clients.general.identity]
|
||||||
logo_help = ""
|
logo_help = ""
|
||||||
|
|||||||
@@ -1168,7 +1168,7 @@ settings = "Settings"
|
|||||||
[ui.dev.clients.general]
|
[ui.dev.clients.general]
|
||||||
create = "Create Application"
|
create = "Create Application"
|
||||||
display_new = "Add Connected Application"
|
display_new = "Add Connected Application"
|
||||||
save = "Settings Save"
|
save = "Save"
|
||||||
title_create = "Create Client"
|
title_create = "Create Client"
|
||||||
title_edit = "Client Settings"
|
title_edit = "Client Settings"
|
||||||
|
|
||||||
|
|||||||
@@ -1168,7 +1168,7 @@ settings = "Settings"
|
|||||||
[ui.dev.clients.general]
|
[ui.dev.clients.general]
|
||||||
create = "앱 생성"
|
create = "앱 생성"
|
||||||
display_new = "연동 앱 추가"
|
display_new = "연동 앱 추가"
|
||||||
save = "설정 저장"
|
save = "저장"
|
||||||
title_create = "Create Client"
|
title_create = "Create Client"
|
||||||
title_edit = "Client Settings"
|
title_edit = "Client Settings"
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user