UI - МЕЖПРОЕКТНАЯ КОММУНИКАЦИЯ: единая шапка и OIDC вход launcher
This commit is contained in:
@@ -32,6 +32,7 @@ import {
|
||||
Save,
|
||||
SearchCheck,
|
||||
ShieldCheck,
|
||||
SlidersHorizontal,
|
||||
Trash2,
|
||||
UsersRound,
|
||||
Video,
|
||||
@@ -59,6 +60,7 @@ import {
|
||||
getUser,
|
||||
type AccessMatrixCell,
|
||||
type LauncherData,
|
||||
type LauncherSettings,
|
||||
type MeResponse,
|
||||
} from "../../shared/api/mockApi";
|
||||
import { uploadStorageFile } from "../../shared/api/storageApi";
|
||||
@@ -78,6 +80,7 @@ type AdminSection =
|
||||
| "invites"
|
||||
| "sync"
|
||||
| "audit"
|
||||
| "misc"
|
||||
| "company";
|
||||
|
||||
type AccessAssignmentRole = Exclude<ServiceAppRole, "owner">;
|
||||
@@ -109,6 +112,7 @@ const rootSections: Array<{ id: AdminSection; label: string; icon: React.ReactNo
|
||||
{ id: "invites", label: "Инвайты", icon: <MailPlus size={16} /> },
|
||||
{ id: "sync", label: "Синхронизация", icon: <RefreshCw size={16} /> },
|
||||
{ id: "audit", label: "Аудит", icon: <ClipboardList size={16} /> },
|
||||
{ id: "misc", label: "Разное", icon: <SlidersHorizontal size={16} /> },
|
||||
];
|
||||
|
||||
const clientSections: Array<{ id: AdminSection; label: string; icon: React.ReactNode }> = [
|
||||
@@ -146,6 +150,7 @@ export function AdminOverlay({
|
||||
onReorderServices,
|
||||
onCreateService,
|
||||
onDeleteService,
|
||||
onUpdateSettings,
|
||||
}: {
|
||||
data: LauncherData;
|
||||
me: MeResponse;
|
||||
@@ -171,6 +176,7 @@ export function AdminOverlay({
|
||||
onReorderServices: (orderedServiceIds: string[]) => void;
|
||||
onCreateService: () => void;
|
||||
onDeleteService: (serviceId: string) => void;
|
||||
onUpdateSettings: (patch: Partial<LauncherSettings>) => void;
|
||||
}) {
|
||||
const isRoot = me.launcherRole === "root_admin";
|
||||
const sections = isRoot ? rootSections : clientSections;
|
||||
@@ -335,6 +341,7 @@ export function AdminOverlay({
|
||||
) : null}
|
||||
{activeSection === "sync" ? <SyncSection data={data} clientId={scopedClientId} isRoot={isRoot} onRetrySync={onRetrySync} /> : null}
|
||||
{activeSection === "audit" && isRoot ? <AuditSection data={data} /> : null}
|
||||
{activeSection === "misc" && isRoot ? <MiscSection data={data} onUpdateSettings={onUpdateSettings} /> : null}
|
||||
{activeSection === "company" ? <CompanySection data={data} clientId={scopedClientId} /> : null}
|
||||
</div>
|
||||
</section>
|
||||
@@ -2327,6 +2334,58 @@ function AuditSection({ data }: { data: LauncherData }) {
|
||||
);
|
||||
}
|
||||
|
||||
function MiscSection({
|
||||
data,
|
||||
onUpdateSettings,
|
||||
}: {
|
||||
data: LauncherData;
|
||||
onUpdateSettings: (patch: Partial<LauncherSettings>) => void;
|
||||
}) {
|
||||
const [logoLinkUrl, setLogoLinkUrl] = useState(data.settings.brand.logoLinkUrl);
|
||||
|
||||
useEffect(() => {
|
||||
setLogoLinkUrl(data.settings.brand.logoLinkUrl);
|
||||
}, [data.settings.brand.logoLinkUrl]);
|
||||
|
||||
const normalizedLogoLinkUrl = logoLinkUrl.trim() || "/";
|
||||
const hasChanges = normalizedLogoLinkUrl !== data.settings.brand.logoLinkUrl;
|
||||
|
||||
return (
|
||||
<GlassSurface className="table-shell admin-settings-panel">
|
||||
<div className="table-toolbar">
|
||||
<div>
|
||||
<h3>Разное</h3>
|
||||
<p className="admin-helper-note">
|
||||
Общие настройки платформы, которые должны применяться в лаунчере, Task Manager и системных auth-экранах.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="accent"
|
||||
type="button"
|
||||
icon={<Save size={16} />}
|
||||
disabled={!hasChanges}
|
||||
onClick={() => onUpdateSettings({ brand: { logoLinkUrl: normalizedLogoLinkUrl } })}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="admin-settings-grid">
|
||||
<label className="admin-settings-field">
|
||||
<span>Ссылка логотипа</span>
|
||||
<input
|
||||
className="admin-table-input admin-settings-field__input"
|
||||
value={logoLinkUrl}
|
||||
placeholder="/"
|
||||
onChange={(event) => setLogoLinkUrl(event.target.value)}
|
||||
/>
|
||||
<small>Куда ведёт клик по логотипу NODE.DC. Можно указать относительный путь или полный URL.</small>
|
||||
</label>
|
||||
</div>
|
||||
</GlassSurface>
|
||||
);
|
||||
}
|
||||
|
||||
function CompanySection({ data, clientId }: { data: LauncherData; clientId: string }) {
|
||||
const client = getClient(data, clientId);
|
||||
|
||||
@@ -2386,6 +2445,7 @@ function sectionTitle(section: AdminSection): string {
|
||||
invites: "Инвайты",
|
||||
sync: "Синхронизация",
|
||||
audit: "Аудит",
|
||||
misc: "Разное",
|
||||
company: "Профиль компании",
|
||||
};
|
||||
return labels[section];
|
||||
|
||||
@@ -17,6 +17,7 @@ export function TopBar({
|
||||
onOpenShowcase,
|
||||
onOpenProfileSettings,
|
||||
onLogout,
|
||||
brandLinkUrl = "/",
|
||||
}: {
|
||||
me: MeResponse;
|
||||
clients: Client[];
|
||||
@@ -30,6 +31,7 @@ export function TopBar({
|
||||
onOpenShowcase: () => void;
|
||||
onOpenProfileSettings: () => void;
|
||||
onLogout?: () => void;
|
||||
brandLinkUrl?: string;
|
||||
}) {
|
||||
const availableClientIds = new Set(me.memberships.map((membership) => membership.clientId));
|
||||
const availableClients = clients.filter((client) => availableClientIds.has(client.id));
|
||||
@@ -51,7 +53,7 @@ export function TopBar({
|
||||
<div className="nodedc-expanded-toolbar">
|
||||
<div className="nodedc-expanded-toolbar-top">
|
||||
<div className="nodedc-expanded-toolbar-left">
|
||||
<a href="/" aria-label="NODE.DC">
|
||||
<a href={brandLinkUrl} className="nodedc-expanded-brand-link" aria-label="NODE.DC">
|
||||
<img src="/nodedc-logo.svg" alt="NODE DC" className="nodedc-expanded-brand-logo" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user