164 lines
5.2 KiB
TypeScript
164 lines
5.2 KiB
TypeScript
import { useEffect, useState } from "react";
|
|
import { useParams } from "react-router-dom";
|
|
import { apiClient } from "@shared/api/client";
|
|
|
|
type Farm = {
|
|
id: string;
|
|
name: string;
|
|
hub_site_id: string;
|
|
status: string;
|
|
last_seen: string | null;
|
|
};
|
|
|
|
type Member = {
|
|
user_id: string;
|
|
role: string;
|
|
email: string | null;
|
|
farm_ids: string[];
|
|
};
|
|
|
|
type SyncStatus = {
|
|
outbox_pending: number;
|
|
conflicts_pending: number;
|
|
hubs: Array<Farm & { sync_lag?: number }>;
|
|
};
|
|
|
|
export default function EnterpriseCabinetPage(): JSX.Element {
|
|
const { slug } = useParams();
|
|
const [farms, setFarms] = useState<Farm[]>([]);
|
|
const [members, setMembers] = useState<Member[]>([]);
|
|
const [enterpriseId, setEnterpriseId] = useState<string>("");
|
|
const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null);
|
|
const [pairCode, setPairCode] = useState<string>("");
|
|
const [pairingBusy, setPairingBusy] = useState(false);
|
|
const [inviteEmail, setInviteEmail] = useState("");
|
|
const [inviteRole, setInviteRole] = useState("viewer");
|
|
|
|
useEffect(() => {
|
|
void (async () => {
|
|
const ents = await apiClient.get<Array<{ id: string; slug: string }>>("/api/v1/enterprise/enterprises");
|
|
const ent = ents.find((e) => e.slug === slug);
|
|
if (!ent) return;
|
|
setEnterpriseId(ent.id);
|
|
const [list, status, mems] = await Promise.all([
|
|
apiClient.get<Farm[]>(`/api/v1/enterprise/enterprises/${ent.id}/farms`),
|
|
apiClient.get<SyncStatus>(`/api/v1/enterprise/enterprises/${ent.id}/sync-status`),
|
|
apiClient.get<Member[]>(`/api/v1/enterprise/enterprises/${ent.id}/members`),
|
|
]);
|
|
setFarms(list);
|
|
setSyncStatus(status);
|
|
setMembers(mems);
|
|
})();
|
|
}, [slug]);
|
|
|
|
async function startPairing(): Promise<void> {
|
|
if (!enterpriseId) return;
|
|
setPairingBusy(true);
|
|
try {
|
|
const resp = await apiClient.post<{ code: string }>(
|
|
`/api/v1/enterprise/pair/start`,
|
|
{ enterprise_id: enterpriseId, farm_name: "New hub" },
|
|
);
|
|
setPairCode(resp.code);
|
|
} finally {
|
|
setPairingBusy(false);
|
|
}
|
|
}
|
|
|
|
async function inviteMember(): Promise<void> {
|
|
if (!enterpriseId || !inviteEmail.trim()) return;
|
|
await apiClient.post(`/api/v1/enterprise/enterprises/${enterpriseId}/members`, {
|
|
email: inviteEmail.trim(),
|
|
role: inviteRole,
|
|
});
|
|
const mems = await apiClient.get<Member[]>(`/api/v1/enterprise/enterprises/${enterpriseId}/members`);
|
|
setMembers(mems);
|
|
setInviteEmail("");
|
|
}
|
|
|
|
return (
|
|
<div style={{ padding: 24, maxWidth: 960 }}>
|
|
<h1>Enterprise: {slug}</h1>
|
|
<p>Pairing, members, farms, sync status. Conflicts — в K-hub «Мультисервер».</p>
|
|
|
|
<section style={{ marginTop: 24 }}>
|
|
<h2>Pair hub</h2>
|
|
<button type="button" onClick={() => void startPairing()} disabled={pairingBusy || !enterpriseId}>
|
|
{pairingBusy ? "Generating…" : "Generate pairing code"}
|
|
</button>
|
|
{pairCode ? (
|
|
<p>
|
|
Pairing code: <strong>{pairCode}</strong> (enter on hub admin within 15 min)
|
|
</p>
|
|
) : null}
|
|
</section>
|
|
|
|
<section style={{ marginTop: 24 }}>
|
|
<h2>Members</h2>
|
|
<ul>
|
|
{members.map((m) => (
|
|
<li key={m.user_id}>
|
|
{m.email ?? m.user_id} — {m.role}
|
|
{m.farm_ids.length ? ` · farms: ${m.farm_ids.length}` : ""}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
<div style={{ display: "flex", gap: 8, marginTop: 8 }}>
|
|
<input
|
|
type="email"
|
|
placeholder="user@example.com"
|
|
value={inviteEmail}
|
|
onChange={(e) => setInviteEmail(e.target.value)}
|
|
/>
|
|
<select value={inviteRole} onChange={(e) => setInviteRole(e.target.value)}>
|
|
<option value="viewer">viewer</option>
|
|
<option value="zootech">zootech</option>
|
|
<option value="admin">admin</option>
|
|
</select>
|
|
<button type="button" onClick={() => void inviteMember()} disabled={!inviteEmail.trim()}>
|
|
Add member
|
|
</button>
|
|
</div>
|
|
</section>
|
|
|
|
<section style={{ marginTop: 24 }}>
|
|
<h2>Sync status</h2>
|
|
{syncStatus ? (
|
|
<ul>
|
|
<li>Outbox pending: {syncStatus.outbox_pending}</li>
|
|
<li>Conflicts pending: {syncStatus.conflicts_pending}</li>
|
|
{syncStatus.hubs.map((h) => (
|
|
<li key={h.id}>
|
|
{h.name}: lag {h.sync_lag ?? 0}
|
|
{h.last_seen ? ` · ${h.last_seen}` : " · offline"}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
) : (
|
|
<p>Loading…</p>
|
|
)}
|
|
</section>
|
|
|
|
<section style={{ marginTop: 24 }}>
|
|
<h2>Farms</h2>
|
|
<ul>
|
|
{farms.map((f) => (
|
|
<li key={f.id}>
|
|
{f.name} ({f.hub_site_id}) — {f.status}
|
|
{f.last_seen ? ` · last seen ${f.last_seen}` : " · offline"}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</section>
|
|
|
|
{enterpriseId ? (
|
|
<p style={{ marginTop: 24 }}>
|
|
<a href={`/recipes?enterprise_id=${encodeURIComponent(enterpriseId)}`}>
|
|
Zootech UI (WESP copy)
|
|
</a>
|
|
</p>
|
|
) : null}
|
|
</div>
|
|
);
|
|
}
|