diff --git a/adminfront/public/LINE_WORKS_Appicon.svg b/adminfront/public/LINE_WORKS_Appicon.svg
new file mode 100644
index 00000000..7cf048cd
--- /dev/null
+++ b/adminfront/public/LINE_WORKS_Appicon.svg
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/adminfront/seed-tenant.csv b/adminfront/seed-tenant.csv
index f774259a..5ff8391f 100644
--- a/adminfront/seed-tenant.csv
+++ b/adminfront/seed-tenant.csv
@@ -1,11 +1,12 @@
-id,name,type,parent_tenant_slug,slug,memo,email_domain
-038326b6-954a-48a7-a85f-efd83f62b82a,한맥가족,COMPANY_GROUP,,hanmac-family,한맥가족 기본 루트 테넌트,
-9caf62e1-297d-4e8f-870b-61780998bbeb,삼안,COMPANY,hanmac-family,saman,네이버웍스 삼안 SAMAN_DOMAIN_ID, samaneng.com
-369c1843-56af-4344-9c21-0e01197ab861,한맥기술,COMPANY,hanmac-family,hanmac,네이버웍스 한맥 HANMAC_DOMAIN_ID, hanmaceng.co.kr
-5530ca6e-c5e6-4bf0-84d6-76c6a8fb70ee,총괄기획&기술개발센터,COMPANY,hanmac-family,gpdtdc,네이버웍스 총괄기획&기술개발센터 GPDTDC_DOMAIN_ID, baroncs.co.kr
-96369f12-6b66-4b2a-a916-d1c99d326f02,바론그룹,COMPANY_GROUP,hanmac-family,baron-group,네이버웍스 바론그룹 BARONGROUP_DOMAIN_ID,
-c18a8284-0008-48aa-9cdf-9f47ab79a2a9,(주)장헌,COMPANY,baron-group,jangheon,,jangheon.com
-b2fcf17f-7085-4bfe-9663-d8a2f2f4b2d6,장헌산업,COMPANY,baron-group,jangheon-sanup,,jangheon.co.kr
-5a03efd2-e62f-4243-800d-58334bf48b2f,한라산업개발,COMPANY,baron-group,hanlla,,hanllasanup.co.kr
-e57cb22c-383e-4489-8c2f-0c5431917e86,(주)피티씨,COMPANY,baron-group,ptc,,pre-cast.co.kr
-9607eb7b-04d2-42ab-80fe-780fe21c7e8f,Personal,PERSONAL,,personal,개인 사용자 기본 루트 테넌트,
+id,name,type,parent_tenant_slug,slug,memo,email_domain,visibility,org_unit_type,worksmobile_sync
+038326b6-954a-48a7-a85f-efd83f62b82a,한맥가족,COMPANY_GROUP,,hanmac-family,한맥가족 기본 루트 테넌트,,,,
+5530ca6e-c5e6-4bf0-84d6-76c6a8fb70ee,총괄기획&기술개발센터,COMPANY,hanmac-family,gpdtdc,네이버웍스 총괄기획&기술개발센터 GPDTDC_DOMAIN_ID, baroncs.co.kr,,,
+9caf62e1-297d-4e8f-870b-61780998bbeb,삼안,COMPANY,hanmac-family,saman,네이버웍스 삼안 SAMAN_DOMAIN_ID, samaneng.com,,,
+369c1843-56af-4344-9c21-0e01197ab861,한맥기술,COMPANY,hanmac-family,hanmac,네이버웍스 한맥 HANMAC_DOMAIN_ID, hanmaceng.co.kr,,,
+96369f12-6b66-4b2a-a916-d1c99d326f02,바론그룹,COMPANY_GROUP,hanmac-family,baron-group,네이버웍스 바론그룹 BARONGROUP_DOMAIN_ID,brsw.kr,,,
+5a03efd2-e62f-4243-800d-58334bf48b2f,한라산업개발,COMPANY,hanmac-family,halla,네이버웍스 한라 HALLA_DOMAIN_ID,hallasanup.com,,,
+c18a8284-0008-48aa-9cdf-9f47ab79a2a9,(주)장헌,COMPANY,baron-group,jangheon,,jangheon.com,,,
+b2fcf17f-7085-4bfe-9663-d8a2f2f4b2d6,장헌산업,COMPANY,baron-group,jangheon-sanup,,jangheon.co.kr,,,
+e57cb22c-383e-4489-8c2f-0c5431917e86,(주)피티씨,COMPANY,baron-group,ptc,,pre-cast.co.kr,,,
+4d0f26b9-702c-4bc6-8996-46e9eedfdeb7,MH_manager,USER_GROUP,hanmac-family,mhd,맨아워 대시보드 권한 보유자그룹,,private,,no
+9607eb7b-04d2-42ab-80fe-780fe21c7e8f,Personal,PERSONAL,,personal,개인 사용자 기본 루트 테넌트,,,,
diff --git a/adminfront/src/app/routes.tsx b/adminfront/src/app/routes.tsx
index 47029154..0d44cda1 100644
--- a/adminfront/src/app/routes.tsx
+++ b/adminfront/src/app/routes.tsx
@@ -48,6 +48,7 @@ export const adminRoutes: RouteObject[] = [
{ path: "users/:id", element: },
{ path: "tenants", element: },
{ path: "tenants/new", element: },
+ { path: "worksmobile", element: },
{
path: "tenants/:tenantId",
element: ,
@@ -56,7 +57,6 @@ export const adminRoutes: RouteObject[] = [
{ path: "permissions", element: },
{ path: "organization", element: },
{ path: "schema", element: },
- { path: "worksmobile", element: },
],
},
{
diff --git a/adminfront/src/components/layout/AppLayout.test.tsx b/adminfront/src/components/layout/AppLayout.test.tsx
index 39fa215b..51a75576 100644
--- a/adminfront/src/components/layout/AppLayout.test.tsx
+++ b/adminfront/src/components/layout/AppLayout.test.tsx
@@ -70,6 +70,7 @@ function renderLayout(entry = "/users") {
path="tenants/:tenantId"
element={
Tenant outlet
}
/>
+ Worksmobile outlet} />
Login outlet} />
@@ -99,7 +100,28 @@ describe("admin AppLayout", () => {
expect(screen.getByText("Admin Control")).toBeInTheDocument();
expect(screen.getByText("Users outlet")).toBeInTheDocument();
expect(screen.getByText("Tenants")).toBeInTheDocument();
+ expect(screen.getByText("Worksmobile")).toBeInTheDocument();
expect(screen.getByText("Data Integrity")).toBeInTheDocument();
+ expect(screen.queryByText("User Projection")).not.toBeInTheDocument();
+ const navigation = screen.getByRole("navigation");
+ const navLabels = Array.from(navigation.querySelectorAll("a")).map((link) =>
+ link.textContent?.trim(),
+ );
+ expect(navLabels).toEqual([
+ "Overview",
+ "Tenants",
+ "Worksmobile",
+ "Users",
+ "Data Integrity",
+ "Auth Guard",
+ "API Keys",
+ "Audit Logs",
+ ]);
+ const worksmobileIcon = screen.getByTestId("worksmobile-nav-icon");
+ expect(worksmobileIcon.tagName.toLowerCase()).toBe("svg");
+ expect(worksmobileIcon).toHaveAttribute("fill", "none");
+ expect(worksmobileIcon.querySelectorAll("path")).toHaveLength(4);
+ expect(worksmobileIcon.querySelector('path[fill="white"]')).toBeNull();
});
it("opens profile menu, navigates, toggles theme/session, and logs out", async () => {
diff --git a/adminfront/src/components/layout/AppLayout.tsx b/adminfront/src/components/layout/AppLayout.tsx
index 0c46e736..e6593aee 100644
--- a/adminfront/src/components/layout/AppLayout.tsx
+++ b/adminfront/src/components/layout/AppLayout.tsx
@@ -2,13 +2,11 @@ import { useQuery } from "@tanstack/react-query";
import {
Building2,
ChevronDown,
- Database,
Key,
KeyRound,
LayoutDashboard,
LogOut,
Moon,
- Network,
NotebookTabs,
ShieldCheck,
ShieldHalf,
@@ -32,7 +30,7 @@ import {
shellLayoutClasses,
writeShellSessionExpiryEnabled,
} from "../../../../common/shell";
-import { buildAuthenticatedOrgChartUrl } from "../../features/users/orgChartPicker";
+import { canAccessWorksmobile } from "../../features/tenants/routes/worksmobileAccess";
import { fetchMe } from "../../lib/adminApi";
import { debugLog } from "../../lib/debugLog";
import { t } from "../../lib/i18n";
@@ -61,6 +59,12 @@ const staticNavItems: ShellSidebarNavItem[] = [
to: "/users",
icon: Users,
},
+ {
+ labelKey: "ui.admin.nav.auth_guard",
+ labelFallback: "Auth Guard",
+ to: "/auth",
+ icon: KeyRound,
+ },
{
labelKey: "ui.admin.nav.api_keys",
labelFallback: "API Keys",
@@ -73,12 +77,6 @@ const staticNavItems: ShellSidebarNavItem[] = [
to: "/audit-logs",
icon: NotebookTabs,
},
- {
- labelKey: "ui.admin.nav.auth_guard",
- labelFallback: "Auth Guard",
- to: "/auth",
- icon: KeyRound,
- },
];
type SessionStatusProps = {
@@ -123,6 +121,38 @@ function SessionStatusText(props: SessionStatusProps) {
return <>{sessionStatus.text}>;
}
+function LineWorksNavIcon({ size = 18 }: { size?: number | string }) {
+ const iconSize = typeof size === "number" ? size : Number.parseFloat(size);
+ return (
+
+
+
+
+
+
+ );
+}
+
function AppLayout() {
const auth = useAuth();
const location = useLocation();
@@ -178,11 +208,10 @@ function AppLayout() {
const isSuperAdmin = isTest || isSuperAdminRole(effectiveRole);
const isTenantAdmin = effectiveRole === "tenant_admin";
const manageableCount = profile?.manageableTenants?.length ?? 0;
- const orgfrontUrl = buildAuthenticatedOrgChartUrl(
- import.meta.env.ORGFRONT_URL || "http://localhost:5175",
- { includeInternal: true },
- );
-
+ const showWorksmobile = canAccessWorksmobile({
+ ...profile,
+ role: effectiveRole ?? profile?.role,
+ });
const filteredItems = items.filter((item) => {
if (isTest) return true;
if (item.to === "/api-keys") return isSuperAdmin;
@@ -196,20 +225,15 @@ function AppLayout() {
to: "/tenants",
icon: Building2,
});
- filteredItems.splice(2, 0, {
- labelKey: "ui.admin.nav.org_chart",
- labelFallback: "Org Chart",
- to: orgfrontUrl,
- icon: Network,
- isExternal: true,
- });
+ if (showWorksmobile) {
+ filteredItems.splice(2, 0, {
+ labelKey: "ui.admin.nav.worksmobile",
+ labelFallback: "Worksmobile",
+ to: "/worksmobile",
+ icon: LineWorksNavIcon,
+ });
+ }
filteredItems.splice(4, 0, {
- labelKey: "ui.admin.nav.user_projection",
- labelFallback: "User Projection",
- to: "/system/projections/users",
- icon: Database,
- });
- filteredItems.splice(5, 0, {
labelKey: "ui.admin.nav.data_integrity",
labelFallback: "Data Integrity",
to: "/system/data-integrity",
@@ -231,26 +255,14 @@ function AppLayout() {
icon: Building2,
});
}
- filteredItems.splice(
- manageableCount <= 1 && profile?.tenantId ? 2 : 2,
- 0,
- {
- labelKey: "ui.admin.nav.org_chart",
- labelFallback: "Org Chart",
- to: orgfrontUrl,
- icon: Network,
- isExternal: true,
- },
- );
- } else {
- // 일반 사용자(Tenant Member)도 조직도 메뉴를 볼 수 있도록 추가합니다.
- filteredItems.splice(1, 0, {
- labelKey: "ui.admin.nav.org_chart",
- labelFallback: "Org Chart",
- to: orgfrontUrl,
- icon: Network,
- isExternal: true,
- });
+ if (showWorksmobile) {
+ filteredItems.splice(2, 0, {
+ labelKey: "ui.admin.nav.worksmobile",
+ labelFallback: "Worksmobile",
+ to: "/worksmobile",
+ icon: LineWorksNavIcon,
+ });
+ }
}
return filteredItems;
diff --git a/adminfront/src/features/integrity/DataIntegrityPage.test.tsx b/adminfront/src/features/integrity/DataIntegrityPage.test.tsx
index 06a138d4..973a4ca4 100644
--- a/adminfront/src/features/integrity/DataIntegrityPage.test.tsx
+++ b/adminfront/src/features/integrity/DataIntegrityPage.test.tsx
@@ -6,6 +6,9 @@ import {
fetchDataIntegrityReport,
fetchMe,
fetchOrphanUserLoginIDs,
+ fetchUserProjectionStatus,
+ reconcileUserProjection,
+ resetUserProjection,
} from "../../lib/adminApi";
import { createI18nMock } from "../../test/i18nMock";
import DataIntegrityPage from "./DataIntegrityPage";
@@ -60,6 +63,24 @@ vi.mock("../../lib/adminApi", () => ({
],
total: 1,
})),
+ fetchUserProjectionStatus: vi.fn(async () => ({
+ name: "kratos_users",
+ status: "ready",
+ ready: true,
+ lastSyncedAt: "2026-05-11T03:00:00Z",
+ updatedAt: "2026-05-11T03:00:10Z",
+ projectedUsers: 152,
+ })),
+ reconcileUserProjection: vi.fn(async () => ({
+ status: "success",
+ syncedUsers: 152,
+ updatedAt: "2026-05-11T03:01:00Z",
+ })),
+ resetUserProjection: vi.fn(async () => ({
+ status: "success",
+ syncedUsers: 152,
+ updatedAt: "2026-05-11T03:02:00Z",
+ })),
deleteOrphanUserLoginIDs: vi.fn(async () => ({
deletedCount: 1,
deleted: [
@@ -95,6 +116,7 @@ describe("DataIntegrityPage", () => {
beforeEach(() => {
currentRole = "super_admin";
vi.clearAllMocks();
+ vi.spyOn(window, "confirm").mockReturnValue(true);
window.localStorage.setItem("locale", "ko");
});
@@ -102,6 +124,12 @@ describe("DataIntegrityPage", () => {
renderPage();
expect(await screen.findByText("데이터 정합성 검증")).toBeInTheDocument();
+ expect(
+ screen.getByRole("tab", { name: "정합성 검사" }),
+ ).toBeInTheDocument();
+ expect(
+ screen.getByRole("tab", { name: "사용자 동기화" }),
+ ).toBeInTheDocument();
expect(
await screen.findByText(
"정합성 상태를 확인하고 데이터 모델 전반의 검증 결과를 살펴봅니다.",
@@ -113,6 +141,28 @@ describe("DataIntegrityPage", () => {
expect(fetchDataIntegrityReport).toHaveBeenCalledTimes(1);
});
+ it("renders user projection sync inside data integrity", async () => {
+ renderPage();
+
+ fireEvent.click(await screen.findByRole("tab", { name: "사용자 동기화" }));
+
+ expect(await screen.findByText("사용자 동기화 관리")).toBeInTheDocument();
+ expect(await screen.findByText("Kratos 사용자 동기화")).toBeInTheDocument();
+ expect(screen.getByText("준비됨")).toBeInTheDocument();
+ expect(screen.getByText("152")).toBeInTheDocument();
+
+ fireEvent.click(screen.getByRole("button", { name: /재동기화/ }));
+ await waitFor(() => {
+ expect(reconcileUserProjection).toHaveBeenCalledTimes(1);
+ });
+
+ fireEvent.click(screen.getByRole("button", { name: /초기화 후 재구축/ }));
+ await waitFor(() => {
+ expect(resetUserProjection).toHaveBeenCalledTimes(1);
+ });
+ expect(fetchUserProjectionStatus).toHaveBeenCalled();
+ });
+
it("shows orphan login ID targets and deletes selected rows", async () => {
vi.spyOn(window, "confirm").mockReturnValue(true);
renderPage();
diff --git a/adminfront/src/features/integrity/DataIntegrityPage.tsx b/adminfront/src/features/integrity/DataIntegrityPage.tsx
index 1d4ca4ef..5dc81c20 100644
--- a/adminfront/src/features/integrity/DataIntegrityPage.tsx
+++ b/adminfront/src/features/integrity/DataIntegrityPage.tsx
@@ -19,6 +19,7 @@ import {
} from "../../lib/adminApi";
import { t } from "../../lib/i18n";
import { getAdminDateLocale } from "../../lib/locale";
+import { UserProjectionContent } from "../projections/UserProjectionPage";
function statusLabel(status: DataIntegrityStatus) {
switch (status) {
@@ -187,6 +188,14 @@ function recheckStatusText(status: "idle" | "running" | "success" | "error") {
}
}
+function pageTabClassName(active: boolean) {
+ return `relative px-6 py-3 text-sm font-medium transition-colors ${
+ active
+ ? "border-b-2 border-primary text-primary"
+ : "text-muted-foreground hover:text-foreground"
+ }`;
+}
+
function OrphanLoginIDTable({
items,
selectedIds,
@@ -284,6 +293,9 @@ function OrphanLoginIDTable({
function DataIntegrityContent() {
const queryClient = useQueryClient();
+ const [activeTab, setActiveTab] = useState<"integrity" | "projection">(
+ "integrity",
+ );
const [selectedOrphanIds, setSelectedOrphanIds] = useState([]);
const [recheckStatus, setRecheckStatus] = useState<
"idle" | "running" | "success" | "error"
@@ -360,210 +372,243 @@ function DataIntegrityContent() {
-
-
-
- {isManualRechecking
- ? t("ui.admin.integrity.recheck.running", "검사 중")
- : t("ui.admin.integrity.recheck.run", "다시 검사")}
-
- {recheckMessage ? (
-
- {recheckMessage}
-
- ) : null}
-
-
-
-
- {isError ? (
-
- {(error as Error)?.message ||
- t(
- "msg.admin.integrity.report.load_error",
- "정합성 리포트를 불러오지 못했습니다.",
- )}
-
- ) : null}
-
-
-
-
-
- {t(
- "ui.admin.integrity.read_model.title",
- "Read model integrity",
- )}
-
-
- {t(
- "msg.admin.integrity.read_model.description",
- "Ory SoT를 덮어쓰지 않고 backend DB read model의 이상 징후만 확인합니다.",
- )}
-
-
- {data ? (
-
- {statusLabel(data.status)}
-
- ) : null}
-
-
- {isLoading ? (
-
- {t("ui.admin.integrity.loading", "불러오는 중")}
-
- ) : (
-
-
-
- {t("ui.admin.integrity.summary.total_checks", "검사 항목")}
-
-
- {data?.summary.totalChecks ?? 0}
-
-
-
-
- {t("ui.admin.integrity.summary.passed", "정상")}
-
-
- {data?.summary.passed ?? 0}
-
-
-
-
- {t("ui.admin.integrity.summary.failures", "실패 건수")}
-
-
- {data?.summary.failures ?? 0}
-
-
-
-
- {t("ui.admin.integrity.summary.checked_at", "검사 시각")}
-
-
- {formatDateTime(data?.checkedAt)}
-
-
-
- )}
-
-
-
- {(data?.sections ?? []).map((section) => (
-
-
-
-
- {integritySectionLabel(section.key, section.label)}
-
-
- {integritySectionDescription(section.key)}
-
-
-
- {statusLabel(section.status)}
-
-
-
- {section.checks.map((check) => (
-
-
-
-
-
- {integrityCheckLabel(check.key, check.label)}
-
-
- {integrityCheckDescription(
- check.key,
- check.description,
- )}
-
-
-
-
-
- {statusLabel(check.status)}
-
-
- {check.count}
-
-
-
- ))}
-
-
- ))}
-
-
-
-
-
-
- {t(
- "ui.admin.integrity.orphan_login_ids.title",
- "유령 로그인 ID 정리",
- )}
-
-
- {t(
- "msg.admin.integrity.orphan_login_ids.description",
- "삭제되었거나 존재하지 않는 사용자/테넌트를 참조하는 로그인 ID를 확인한 뒤 선택 삭제합니다.",
- )}
-
-
+ {activeTab === "integrity" ? (
+
- {t("ui.admin.integrity.orphan_login_ids.delete", "선택 삭제")}
+
+ {isManualRechecking
+ ? t("ui.admin.integrity.recheck.running", "검사 중")
+ : t("ui.admin.integrity.recheck.run", "다시 검사")}
+ {recheckMessage ? (
+
+ {recheckMessage}
+
+ ) : null}
- {orphanLoginIDsQuery.isError ? (
-
- {t(
- "msg.admin.integrity.orphan_login_ids.load_error",
- "유령 로그인 ID 대상을 불러오지 못했습니다.",
- )}
-
- ) : null}
- {deleteMutation.data ? (
-
- {t(
- "msg.admin.integrity.orphan_login_ids.delete_success",
- "{{count}}개의 유령 로그인 ID를 삭제했습니다.",
- { count: deleteMutation.data.deletedCount },
- )}
-
- ) : null}
-
-
+ ) : null}
+
+
+
+ setActiveTab("integrity")}
+ >
+ {t("ui.admin.integrity.tab_checks", "정합성 검사")}
+
+ setActiveTab("projection")}
+ >
+ {t("ui.admin.integrity.tab_user_projection", "사용자 동기화")}
+
+
+ {activeTab === "integrity" ? (
+
+ {isError ? (
+
+ {(error as Error)?.message ||
+ t(
+ "msg.admin.integrity.report.load_error",
+ "정합성 리포트를 불러오지 못했습니다.",
+ )}
+
+ ) : null}
+
+
+
+
+
+ {t(
+ "ui.admin.integrity.read_model.title",
+ "Read model integrity",
+ )}
+
+
+ {t(
+ "msg.admin.integrity.read_model.description",
+ "Ory SoT를 덮어쓰지 않고 backend DB read model의 이상 징후만 확인합니다.",
+ )}
+
+
+ {data ? (
+
+ {statusLabel(data.status)}
+
+ ) : null}
+
+
+ {isLoading ? (
+
+ {t("ui.admin.integrity.loading", "불러오는 중")}
+
+ ) : (
+
+
+
+ {t("ui.admin.integrity.summary.total_checks", "검사 항목")}
+
+
+ {data?.summary.totalChecks ?? 0}
+
+
+
+
+ {t("ui.admin.integrity.summary.passed", "정상")}
+
+
+ {data?.summary.passed ?? 0}
+
+
+
+
+ {t("ui.admin.integrity.summary.failures", "실패 건수")}
+
+
+ {data?.summary.failures ?? 0}
+
+
+
+
+ {t("ui.admin.integrity.summary.checked_at", "검사 시각")}
+
+
+ {formatDateTime(data?.checkedAt)}
+
+
+
+ )}
+
+
+
+ {(data?.sections ?? []).map((section) => (
+
+
+
+
+ {integritySectionLabel(section.key, section.label)}
+
+
+ {integritySectionDescription(section.key)}
+
+
+
+ {statusLabel(section.status)}
+
+
+
+ {section.checks.map((check) => (
+
+
+
+
+
+ {integrityCheckLabel(check.key, check.label)}
+
+
+ {integrityCheckDescription(
+ check.key,
+ check.description,
+ )}
+
+
+
+
+
+ {statusLabel(check.status)}
+
+
+ {check.count}
+
+
+
+ ))}
+
+
+ ))}
+
+
+
+
+
+
+ {t(
+ "ui.admin.integrity.orphan_login_ids.title",
+ "유령 로그인 ID 정리",
+ )}
+
+
+ {t(
+ "msg.admin.integrity.orphan_login_ids.description",
+ "삭제되었거나 존재하지 않는 사용자/테넌트를 참조하는 로그인 ID를 확인한 뒤 선택 삭제합니다.",
+ )}
+
+
+
+ {t("ui.admin.integrity.orphan_login_ids.delete", "선택 삭제")}
+
+
+ {orphanLoginIDsQuery.isError ? (
+
+ {t(
+ "msg.admin.integrity.orphan_login_ids.load_error",
+ "유령 로그인 ID 대상을 불러오지 못했습니다.",
+ )}
+
+ ) : null}
+ {deleteMutation.data ? (
+
+ {t(
+ "msg.admin.integrity.orphan_login_ids.delete_success",
+ "{{count}}개의 유령 로그인 ID를 삭제했습니다.",
+ { count: deleteMutation.data.deletedCount },
+ )}
+
+ ) : null}
+
+
+
+ ) : (
+
+
+
+ )}
);
}
diff --git a/adminfront/src/features/projections/UserProjectionPage.tsx b/adminfront/src/features/projections/UserProjectionPage.tsx
index 9ec98255..d1f27743 100644
--- a/adminfront/src/features/projections/UserProjectionPage.tsx
+++ b/adminfront/src/features/projections/UserProjectionPage.tsx
@@ -55,7 +55,11 @@ function ProjectionStatusBadge({
);
}
-function UserProjectionContent() {
+export function UserProjectionContent({
+ embedded = false,
+}: {
+ embedded?: boolean;
+}) {
const queryClient = useQueryClient();
const { data, isLoading, isError, error } = useQuery({
queryKey: ["user-projection-status"],
@@ -94,50 +98,55 @@ function UserProjectionContent() {
const actionResult = reconcileMutation.data ?? resetMutation.data;
const actionError = reconcileMutation.error ?? resetMutation.error;
- return (
-
-
-
-
-
-
-
-
- {t(
- "ui.admin.user_projection.title",
- "User Projection Management",
- )}
-
-
- {t(
- "msg.admin.user_projection.subtitle",
- "Review and sync the Kratos user read model.",
- )}
-
-
+ const header = (
+