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:
@@ -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,100 @@ 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>
|
||||||
{/* Display Name */}
|
{t("ui.dev.clients.federation.add_title", "Add Identity Provider")}
|
||||||
<div className="mb-4">
|
</CardTitle>
|
||||||
<label className="block text-gray-700 text-sm font-bold mb-2">
|
<CardDescription>
|
||||||
Display Name
|
{t(
|
||||||
</label>
|
"msg.dev.clients.federation.add_subtitle",
|
||||||
<input
|
"Connect an external OIDC provider.",
|
||||||
type="text"
|
)}
|
||||||
name="display_name"
|
</CardDescription>
|
||||||
value={formData.display_name}
|
</CardHeader>
|
||||||
onChange={handleChange}
|
<CardContent>
|
||||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
required
|
<div className="space-y-2">
|
||||||
/>
|
<label className="text-sm font-semibold">Display Name</label>
|
||||||
</div>
|
<Input
|
||||||
|
name="display_name"
|
||||||
|
value={formData.display_name}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="e.g. Google Workspace"
|
||||||
|
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={
|
||||||
name="scopes"
|
mutation.isPending ||
|
||||||
value={formData.scopes}
|
formData.display_name.trim() === "" ||
|
||||||
onChange={handleChange}
|
formData.issuer_url.trim() === ""
|
||||||
className="shadow appearance-none border rounded w-full py-2 px-3 text-gray-700 leading-tight focus:outline-none focus:shadow-outline"
|
}
|
||||||
/>
|
>
|
||||||
</div>
|
{mutation.isPending ? (
|
||||||
|
<div className="h-4 w-4 border-2 border-white/30 border-t-white rounded-full animate-spin mr-2" />
|
||||||
{/* Action Buttons */}
|
) : (
|
||||||
<div className="flex items-center justify-end">
|
<Save size={16} className="mr-2" />
|
||||||
<button
|
)}
|
||||||
type="button"
|
{mutation.isPending
|
||||||
onClick={onClose}
|
? t("msg.common.saving", "Saving...")
|
||||||
className="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded mr-2"
|
: t("ui.common.save", "Save Configuration")}
|
||||||
>
|
</Button>
|
||||||
Cancel
|
</div>
|
||||||
</button>
|
</form>
|
||||||
<button
|
</CardContent>
|
||||||
type="submit"
|
</Card>
|
||||||
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 +181,11 @@ 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 +194,113 @@ 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,12 @@ 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.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]
|
[msg.dev.clients.general.identity]
|
||||||
logo_help = "Logo Help"
|
logo_help = "Logo Help"
|
||||||
@@ -1019,10 +1025,14 @@ 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"
|
|
||||||
title_create = "Create Client"
|
title_create = "Create Client"
|
||||||
title_edit = "Client Settings"
|
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]
|
[ui.dev.clients.general.breadcrumb]
|
||||||
section = "Applications"
|
section = "Applications"
|
||||||
|
|
||||||
|
|||||||
@@ -252,6 +252,12 @@ 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.federation]
|
||||||
|
subtitle = "이 애플리케이션의 외부 IdP 설정을 관리합니다."
|
||||||
|
add_subtitle = "외부 OIDC 제공자를 연결합니다."
|
||||||
|
empty = "등록된 IdP 설정이 없습니다."
|
||||||
|
|
||||||
[msg.dev.clients.general.identity]
|
[msg.dev.clients.general.identity]
|
||||||
logo_help = "인증 화면에 표시될 PNG/SVG URL입니다."
|
logo_help = "인증 화면에 표시될 PNG/SVG URL입니다."
|
||||||
@@ -1019,10 +1025,14 @@ settings = "Settings"
|
|||||||
[ui.dev.clients.general]
|
[ui.dev.clients.general]
|
||||||
create = "앱 생성"
|
create = "앱 생성"
|
||||||
display_new = "연동 앱 추가"
|
display_new = "연동 앱 추가"
|
||||||
save = "설정 저장"
|
|
||||||
title_create = "Create Client"
|
title_create = "Create Client"
|
||||||
title_edit = "Client Settings"
|
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]
|
[ui.dev.clients.general.breadcrumb]
|
||||||
section = "Applications"
|
section = "Applications"
|
||||||
|
|
||||||
|
|||||||
@@ -252,6 +252,12 @@ load_error = ""
|
|||||||
loading = ""
|
loading = ""
|
||||||
saved = ""
|
saved = ""
|
||||||
save_error = ""
|
save_error = ""
|
||||||
|
status_changed = ""
|
||||||
|
|
||||||
|
[msg.dev.clients.federation]
|
||||||
|
subtitle = ""
|
||||||
|
add_subtitle = ""
|
||||||
|
empty = ""
|
||||||
|
|
||||||
[msg.dev.clients.general.identity]
|
[msg.dev.clients.general.identity]
|
||||||
logo_help = ""
|
logo_help = ""
|
||||||
@@ -1031,10 +1037,14 @@ settings = ""
|
|||||||
[ui.dev.clients.general]
|
[ui.dev.clients.general]
|
||||||
create = ""
|
create = ""
|
||||||
display_new = ""
|
display_new = ""
|
||||||
save = ""
|
|
||||||
title_create = ""
|
title_create = ""
|
||||||
title_edit = ""
|
title_edit = ""
|
||||||
|
|
||||||
|
[ui.dev.clients.federation]
|
||||||
|
title = ""
|
||||||
|
add_title = ""
|
||||||
|
add_btn = ""
|
||||||
|
|
||||||
[ui.dev.clients.general.breadcrumb]
|
[ui.dev.clients.general.breadcrumb]
|
||||||
section = ""
|
section = ""
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -250,6 +250,12 @@ load_error = ""
|
|||||||
loading = ""
|
loading = ""
|
||||||
saved = ""
|
saved = ""
|
||||||
save_error = ""
|
save_error = ""
|
||||||
|
status_changed = ""
|
||||||
|
|
||||||
|
[msg.dev.clients.federation]
|
||||||
|
subtitle = ""
|
||||||
|
add_subtitle = ""
|
||||||
|
empty = ""
|
||||||
|
|
||||||
[msg.dev.clients.general.identity]
|
[msg.dev.clients.general.identity]
|
||||||
logo_help = ""
|
logo_help = ""
|
||||||
@@ -1030,10 +1036,14 @@ settings = ""
|
|||||||
[ui.dev.clients.general]
|
[ui.dev.clients.general]
|
||||||
create = ""
|
create = ""
|
||||||
display_new = ""
|
display_new = ""
|
||||||
save = ""
|
|
||||||
title_create = ""
|
title_create = ""
|
||||||
title_edit = ""
|
title_edit = ""
|
||||||
|
|
||||||
|
[ui.dev.clients.federation]
|
||||||
|
title = ""
|
||||||
|
add_title = ""
|
||||||
|
add_btn = ""
|
||||||
|
|
||||||
[ui.dev.clients.general.breadcrumb]
|
[ui.dev.clients.general.breadcrumb]
|
||||||
section = ""
|
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
Reference in New Issue
Block a user