forked from baron/baron-sso
devfront RP 상세 탭 i18n 및 순서 일관화
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
|||||||
import { fetchClient, fetchConsents, revokeConsent } from "../../lib/devApi";
|
import { fetchClient, fetchConsents, revokeConsent } from "../../lib/devApi";
|
||||||
import { t } from "../../lib/i18n";
|
import { t } from "../../lib/i18n";
|
||||||
import { cn } from "../../lib/utils";
|
import { cn } from "../../lib/utils";
|
||||||
|
import { ClientDetailTabs } from "./ClientDetailTabs";
|
||||||
|
|
||||||
function ClientConsentsPage() {
|
function ClientConsentsPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -214,29 +215,7 @@ function ClientConsentsPage() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-6 overflow-x-auto border-b border-border pb-3 text-sm font-bold">
|
<ClientDetailTabs activeTab="consents" clientId={clientId} />
|
||||||
<Link
|
|
||||||
to={`/clients/${clientId}`}
|
|
||||||
className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
{t("ui.dev.clients.details.tab.connection", "Federation")}
|
|
||||||
</Link>
|
|
||||||
<span className="whitespace-nowrap border-b-2 border-primary pb-1 text-primary">
|
|
||||||
{t("ui.dev.clients.details.tab.consents", "Consent & Users")}
|
|
||||||
</span>
|
|
||||||
<Link
|
|
||||||
to={`/clients/${clientId}/settings`}
|
|
||||||
className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
{t("ui.dev.clients.details.tab.settings", "Settings")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to={`/clients/${clientId}/relationships`}
|
|
||||||
className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
{t("ui.dev.clients.details.tab.relationships", "Relationships")}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<Card className="glass-panel">
|
<Card className="glass-panel">
|
||||||
|
|||||||
54
devfront/src/features/clients/ClientDetailTabs.tsx
Normal file
54
devfront/src/features/clients/ClientDetailTabs.tsx
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { Link } from "react-router-dom";
|
||||||
|
import { t } from "../../lib/i18n";
|
||||||
|
import { cn } from "../../lib/utils";
|
||||||
|
|
||||||
|
type ClientDetailTab = "connection" | "consents" | "settings" | "relationships";
|
||||||
|
|
||||||
|
interface ClientDetailTabsProps {
|
||||||
|
activeTab: ClientDetailTab;
|
||||||
|
clientId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabOrder: Array<{
|
||||||
|
key: ClientDetailTab;
|
||||||
|
href: (clientId: string) => string;
|
||||||
|
}> = [
|
||||||
|
{ key: "connection", href: (clientId) => `/clients/${clientId}` },
|
||||||
|
{ key: "consents", href: (clientId) => `/clients/${clientId}/consents` },
|
||||||
|
{ key: "settings", href: (clientId) => `/clients/${clientId}/settings` },
|
||||||
|
{
|
||||||
|
key: "relationships",
|
||||||
|
href: (clientId) => `/clients/${clientId}/relationships`,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export function ClientDetailTabs({
|
||||||
|
activeTab,
|
||||||
|
clientId,
|
||||||
|
}: ClientDetailTabsProps) {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-6 overflow-x-auto border-b border-border pb-3 text-sm font-bold">
|
||||||
|
{tabOrder.map((tab) => {
|
||||||
|
const isActive = tab.key === activeTab;
|
||||||
|
return isActive ? (
|
||||||
|
<span
|
||||||
|
key={tab.key}
|
||||||
|
className="whitespace-nowrap border-b-2 border-primary pb-1 text-primary"
|
||||||
|
>
|
||||||
|
{t(`ui.dev.clients.details.tab.${tab.key}`)}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
key={tab.key}
|
||||||
|
to={tab.href(clientId)}
|
||||||
|
className={cn(
|
||||||
|
"whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{t(`ui.dev.clients.details.tab.${tab.key}`)}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -38,6 +38,7 @@ import {
|
|||||||
} from "../../lib/devApi";
|
} from "../../lib/devApi";
|
||||||
import { t } from "../../lib/i18n";
|
import { t } from "../../lib/i18n";
|
||||||
import { cn } from "../../lib/utils";
|
import { cn } from "../../lib/utils";
|
||||||
|
import { ClientDetailTabs } from "./ClientDetailTabs";
|
||||||
|
|
||||||
function ClientDetailsPage() {
|
function ClientDetailsPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
@@ -253,32 +254,7 @@ function ClientDetailsPage() {
|
|||||||
: t("msg.common.loading", "Loading...")}
|
: t("msg.common.loading", "Loading...")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-6 border-b border-border">
|
<ClientDetailTabs activeTab="connection" clientId={clientId} />
|
||||||
<Link
|
|
||||||
to={`/clients/${clientId}`}
|
|
||||||
className="border-b-2 border-primary pb-3 text-sm font-bold text-primary"
|
|
||||||
>
|
|
||||||
{t("ui.dev.clients.details.tab.connection", "Federation")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to={`/clients/${clientId}/consents`}
|
|
||||||
className="pb-3 text-sm font-bold text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
{t("ui.dev.clients.details.tab.consents", "Consent & Users")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to={`/clients/${clientId}/settings`}
|
|
||||||
className="pb-3 text-sm font-bold text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
{t("ui.dev.clients.details.tab.settings", "Settings")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to={`/clients/${clientId}/relationships`}
|
|
||||||
className="pb-3 text-sm font-bold text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
{t("ui.dev.clients.details.tab.relationships", "Relationships")}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid gap-8 lg:grid-cols-2">
|
<div className="grid gap-8 lg:grid-cols-2">
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ import type {
|
|||||||
} from "../../lib/devApi";
|
} from "../../lib/devApi";
|
||||||
import { t } from "../../lib/i18n";
|
import { t } from "../../lib/i18n";
|
||||||
import { cn } from "../../lib/utils";
|
import { cn } from "../../lib/utils";
|
||||||
|
import { ClientDetailTabs } from "./ClientDetailTabs";
|
||||||
|
|
||||||
interface ScopeItem {
|
interface ScopeItem {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -665,33 +666,9 @@ function ClientGeneralPage() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-6 overflow-x-auto border-b border-border pb-3 text-sm font-bold">
|
{!isCreate && (
|
||||||
{!isCreate && (
|
<ClientDetailTabs activeTab="settings" clientId={clientId} />
|
||||||
<>
|
)}
|
||||||
<Link
|
|
||||||
to={`/clients/${clientId}`}
|
|
||||||
className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
{t("ui.dev.clients.details.tab.connection", "Federation")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to={`/clients/${clientId}/consents`}
|
|
||||||
className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
{t("ui.dev.clients.details.tab.consents", "Consent & Users")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to={`/clients/${clientId}/relationships`}
|
|
||||||
className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
{t("ui.dev.clients.details.tab.relationships", "Relationships")}
|
|
||||||
</Link>
|
|
||||||
<span className="whitespace-nowrap border-b-2 border-primary pb-1 text-primary">
|
|
||||||
{t("ui.dev.clients.details.tab.settings", "Settings")}
|
|
||||||
</span>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
{/* 1. Application Identity */}
|
{/* 1. Application Identity */}
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import {
|
|||||||
removeClientRelation,
|
removeClientRelation,
|
||||||
} from "../../lib/devApi";
|
} from "../../lib/devApi";
|
||||||
import { t } from "../../lib/i18n";
|
import { t } from "../../lib/i18n";
|
||||||
|
import { ClientDetailTabs } from "./ClientDetailTabs";
|
||||||
|
|
||||||
const relationOptions = [
|
const relationOptions = [
|
||||||
"admins",
|
"admins",
|
||||||
@@ -48,9 +49,8 @@ function ClientRelationsPage() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const clientId = params.id ?? "";
|
const clientId = params.id ?? "";
|
||||||
const [relation, setRelation] = useState<(typeof relationOptions)[number]>(
|
const [relation, setRelation] =
|
||||||
"config_editor",
|
useState<(typeof relationOptions)[number]>("config_editor");
|
||||||
);
|
|
||||||
const [userId, setUserId] = useState("");
|
const [userId, setUserId] = useState("");
|
||||||
|
|
||||||
const { data: clientData } = useQuery({
|
const { data: clientData } = useQuery({
|
||||||
@@ -86,7 +86,9 @@ function ClientRelationsPage() {
|
|||||||
userId: userId.trim(),
|
userId: userId.trim(),
|
||||||
}),
|
}),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["client-relations", clientId] });
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["client-relations", clientId],
|
||||||
|
});
|
||||||
setUserId("");
|
setUserId("");
|
||||||
toast(
|
toast(
|
||||||
t(
|
t(
|
||||||
@@ -115,7 +117,9 @@ function ClientRelationsPage() {
|
|||||||
mutationFn: (payload: { relation: string; subject: string }) =>
|
mutationFn: (payload: { relation: string; subject: string }) =>
|
||||||
removeClientRelation(clientId, payload.relation, payload.subject),
|
removeClientRelation(clientId, payload.relation, payload.subject),
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
queryClient.invalidateQueries({ queryKey: ["client-relations", clientId] });
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["client-relations", clientId],
|
||||||
|
});
|
||||||
toast(
|
toast(
|
||||||
t(
|
t(
|
||||||
"msg.dev.clients.relationships.removed",
|
"msg.dev.clients.relationships.removed",
|
||||||
@@ -191,10 +195,7 @@ function ClientRelationsPage() {
|
|||||||
<span>{clientData?.client?.name || clientId}</span>
|
<span>{clientData?.client?.name || clientId}</span>
|
||||||
<span>/</span>
|
<span>/</span>
|
||||||
<span className="text-foreground font-semibold">
|
<span className="text-foreground font-semibold">
|
||||||
{t(
|
{t("ui.dev.clients.details.tab.relationships", "Relationships")}
|
||||||
"ui.dev.clients.details.tab.relationships",
|
|
||||||
"Relationships",
|
|
||||||
)}
|
|
||||||
</span>
|
</span>
|
||||||
</nav>
|
</nav>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -231,29 +232,7 @@ function ClientRelationsPage() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-6 overflow-x-auto border-b border-border pb-3 text-sm font-bold">
|
<ClientDetailTabs activeTab="relationships" clientId={clientId} />
|
||||||
<Link
|
|
||||||
to={`/clients/${clientId}`}
|
|
||||||
className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
{t("ui.dev.clients.details.tab.connection", "Federation")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to={`/clients/${clientId}/consents`}
|
|
||||||
className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
{t("ui.dev.clients.details.tab.consents", "Consent & Users")}
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
to={`/clients/${clientId}/settings`}
|
|
||||||
className="whitespace-nowrap border-b-2 border-transparent text-muted-foreground hover:text-foreground"
|
|
||||||
>
|
|
||||||
{t("ui.dev.clients.details.tab.settings", "Settings")}
|
|
||||||
</Link>
|
|
||||||
<span className="whitespace-nowrap border-b-2 border-primary pb-1 text-primary">
|
|
||||||
{t("ui.dev.clients.details.tab.relationships", "Relationships")}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<Card className="glass-panel">
|
<Card className="glass-panel">
|
||||||
|
|||||||
@@ -1363,6 +1363,7 @@ title = "Security Note"
|
|||||||
connection = "Federation"
|
connection = "Federation"
|
||||||
consents = "Consent & Users"
|
consents = "Consent & Users"
|
||||||
settings = "Settings"
|
settings = "Settings"
|
||||||
|
relationships = "Relationships"
|
||||||
|
|
||||||
[ui.dev.clients.general]
|
[ui.dev.clients.general]
|
||||||
create = "Create Application"
|
create = "Create Application"
|
||||||
|
|||||||
@@ -1362,6 +1362,7 @@ title = "보안 메모"
|
|||||||
connection = "연동 설정"
|
connection = "연동 설정"
|
||||||
consents = "동의 및 사용자"
|
consents = "동의 및 사용자"
|
||||||
settings = "설정"
|
settings = "설정"
|
||||||
|
relationships = "관계"
|
||||||
|
|
||||||
[ui.dev.clients.general]
|
[ui.dev.clients.general]
|
||||||
create = "앱 생성"
|
create = "앱 생성"
|
||||||
|
|||||||
@@ -1363,6 +1363,7 @@ title = ""
|
|||||||
connection = ""
|
connection = ""
|
||||||
consents = ""
|
consents = ""
|
||||||
settings = ""
|
settings = ""
|
||||||
|
relationships = ""
|
||||||
|
|
||||||
[ui.dev.clients.general]
|
[ui.dev.clients.general]
|
||||||
create = ""
|
create = ""
|
||||||
|
|||||||
68
devfront/tests/devfront-client-tabs.spec.ts
Normal file
68
devfront/tests/devfront-client-tabs.spec.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import { type Page, expect, test } from "@playwright/test";
|
||||||
|
import {
|
||||||
|
type ClientRelation,
|
||||||
|
type Consent,
|
||||||
|
installDevApiMock,
|
||||||
|
makeClient,
|
||||||
|
seedAuth,
|
||||||
|
} from "./helpers/devfront-fixtures";
|
||||||
|
|
||||||
|
function expectClientTabsOrder(pagePath: string, expectedActive: RegExp) {
|
||||||
|
return async ({ page }: { page: Page }) => {
|
||||||
|
const state = {
|
||||||
|
clients: [makeClient("client-tabs", { name: "탭 테스트 앱" })],
|
||||||
|
consents: [] as Consent[],
|
||||||
|
relations: {
|
||||||
|
"client-tabs": [
|
||||||
|
{
|
||||||
|
relation: "config_editor",
|
||||||
|
subject: "User:user-1",
|
||||||
|
subjectType: "User",
|
||||||
|
subjectId: "user-1",
|
||||||
|
},
|
||||||
|
] satisfies ClientRelation[],
|
||||||
|
},
|
||||||
|
auditLogsByCursor: undefined,
|
||||||
|
};
|
||||||
|
await installDevApiMock(page, state);
|
||||||
|
|
||||||
|
await page.goto(pagePath);
|
||||||
|
|
||||||
|
const header = page
|
||||||
|
.locator("header")
|
||||||
|
.filter({ hasText: "탭 테스트 앱" })
|
||||||
|
.first();
|
||||||
|
const tabs = header.locator(
|
||||||
|
"div.border-b.border-border .whitespace-nowrap",
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(tabs).toHaveText([
|
||||||
|
"연동 설정",
|
||||||
|
"동의 및 사용자",
|
||||||
|
"설정",
|
||||||
|
"관계",
|
||||||
|
]);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
header
|
||||||
|
.locator("div.border-b.border-border .text-primary")
|
||||||
|
.filter({ hasText: expectedActive }),
|
||||||
|
).toHaveCount(1);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test.describe("DevFront client detail tabs", () => {
|
||||||
|
test.beforeEach(async ({ page }) => {
|
||||||
|
await seedAuth(page, "rp_admin");
|
||||||
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
"settings page keeps tab order and uses localized relationships label",
|
||||||
|
expectClientTabsOrder("/clients/client-tabs/settings", /^설정$/),
|
||||||
|
);
|
||||||
|
|
||||||
|
test(
|
||||||
|
"relationships page keeps tab order and uses localized relationships label",
|
||||||
|
expectClientTabsOrder("/clients/client-tabs/relationships", /^관계$/),
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user