|
|
|
@@ -46,10 +46,11 @@ export function AssetsPage({ token, assets, monitors, onChanged }: AssetsPagePro
|
|
|
|
|
const [discovering, setDiscovering] = useState(false);
|
|
|
|
|
const [submitting, setSubmitting] = useState(false);
|
|
|
|
|
const [deletingAssetId, setDeletingAssetId] = useState<number | null>(null);
|
|
|
|
|
const [deletingMonitorId, setDeletingMonitorId] = useState<number | null>(null);
|
|
|
|
|
const [message, setMessage] = useState<string | null>(null);
|
|
|
|
|
|
|
|
|
|
const setupName = selectedAsset?.name ?? name.trim();
|
|
|
|
|
const setupAddress = selectedAsset?.address?.trim() || address.trim();
|
|
|
|
|
const setupName = name.trim();
|
|
|
|
|
const setupAddress = address.trim();
|
|
|
|
|
const selectedItems = useMemo(
|
|
|
|
|
() => (discoveryResult?.monitorable_items ?? []).filter((item) => selectedItemIds.has(item.item_id)),
|
|
|
|
|
[discoveryResult, selectedItemIds]
|
|
|
|
@@ -76,12 +77,39 @@ export function AssetsPage({ token, assets, monitors, onChanged }: AssetsPagePro
|
|
|
|
|
setSelectedItemIds(new Set());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleAssetChoice(value: string) {
|
|
|
|
|
setSetupAssetId(value === "new" ? "new" : Number(value));
|
|
|
|
|
function loadAssetIntoForm(asset: Asset) {
|
|
|
|
|
const assetSnmpMonitors = monitors.filter((monitor) => monitor.asset_id === asset.id && monitor.monitor_type === "snmp");
|
|
|
|
|
const firstSnmpMonitor = assetSnmpMonitors[0];
|
|
|
|
|
const firstProfileId = firstSnmpMonitor ? snmpCredentialProfileId(firstSnmpMonitor) : null;
|
|
|
|
|
|
|
|
|
|
setSetupAssetId(asset.id);
|
|
|
|
|
setName(asset.name);
|
|
|
|
|
setAssetType(asset.asset_type);
|
|
|
|
|
setAddress(asset.address ?? "");
|
|
|
|
|
setIntervalSeconds(firstSnmpMonitor?.interval_seconds ?? 60);
|
|
|
|
|
setPingEnabled(false);
|
|
|
|
|
setTcpEnabled(false);
|
|
|
|
|
setWebsiteEnabled(false);
|
|
|
|
|
setSnmpEnabled(assetSnmpMonitors.length > 0);
|
|
|
|
|
setTcpPort(443);
|
|
|
|
|
setWebsiteUrl("https://");
|
|
|
|
|
setProfileId(firstProfileId ?? profiles[0]?.id ?? "");
|
|
|
|
|
resetDiscovery();
|
|
|
|
|
setMessage(null);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleAssetChoice(value: string) {
|
|
|
|
|
if (value === "new") {
|
|
|
|
|
resetForm();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const asset = assets.find((candidate) => candidate.id === Number(value));
|
|
|
|
|
if (asset) {
|
|
|
|
|
loadAssetIntoForm(asset);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function runSnmpDiscovery() {
|
|
|
|
|
if (!profileId || !setupAddress) return;
|
|
|
|
|
setDiscovering(true);
|
|
|
|
@@ -91,9 +119,13 @@ export function AssetsPage({ token, assets, monitors, onChanged }: AssetsPagePro
|
|
|
|
|
const discovered = await api.discoverSnmpDevice(token, {
|
|
|
|
|
host: setupAddress,
|
|
|
|
|
credential_profile_id: profileId,
|
|
|
|
|
asset_type: selectedAsset?.asset_type ?? assetType,
|
|
|
|
|
asset_type: assetType,
|
|
|
|
|
});
|
|
|
|
|
const existingItemIds = new Set(selectedAssetMonitors.map(snmpItemId).filter((itemId): itemId is string => Boolean(itemId)));
|
|
|
|
|
const discoveredItemIds = new Set(discovered.monitorable_items.map((item) => item.item_id));
|
|
|
|
|
const nextSelectedItemIds = new Set(Array.from(existingItemIds).filter((itemId) => discoveredItemIds.has(itemId)));
|
|
|
|
|
setDiscoveryResult(discovered);
|
|
|
|
|
setSelectedItemIds(nextSelectedItemIds);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setMessage(err instanceof Error ? err.message : "SNMP discovery failed");
|
|
|
|
|
} finally {
|
|
|
|
@@ -119,18 +151,24 @@ export function AssetsPage({ token, assets, monitors, onChanged }: AssetsPagePro
|
|
|
|
|
if (snmpEnabled && !profileId) {
|
|
|
|
|
throw new Error("SNMP profile is required");
|
|
|
|
|
}
|
|
|
|
|
if (snmpEnabled && selectedItems.length === 0) {
|
|
|
|
|
const hasSelectedAssetSnmpMonitors = selectedAssetMonitors.some((monitor) => monitor.monitor_type === "snmp");
|
|
|
|
|
if (snmpEnabled && selectedItems.length === 0 && (!selectedAsset || !hasSelectedAssetSnmpMonitors)) {
|
|
|
|
|
throw new Error("Select at least one SNMP item");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const asset =
|
|
|
|
|
selectedAsset ??
|
|
|
|
|
(await api.createAsset(token, {
|
|
|
|
|
name: setupName,
|
|
|
|
|
asset_type: assetType,
|
|
|
|
|
address: setupAddress || null,
|
|
|
|
|
metadata: {},
|
|
|
|
|
}));
|
|
|
|
|
selectedAsset
|
|
|
|
|
? await api.updateAsset(token, selectedAsset.id, {
|
|
|
|
|
name: setupName,
|
|
|
|
|
asset_type: assetType,
|
|
|
|
|
address: setupAddress || null,
|
|
|
|
|
})
|
|
|
|
|
: await api.createAsset(token, {
|
|
|
|
|
name: setupName,
|
|
|
|
|
asset_type: assetType,
|
|
|
|
|
address: setupAddress || null,
|
|
|
|
|
metadata: {},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const monitorCreates: Promise<unknown>[] = [];
|
|
|
|
|
if (pingEnabled) {
|
|
|
|
@@ -185,22 +223,30 @@ export function AssetsPage({ token, assets, monitors, onChanged }: AssetsPagePro
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
if (snmpEnabled && profileId) {
|
|
|
|
|
monitorCreates.push(
|
|
|
|
|
api.createSnmpMonitorsFromDiscovery(token, {
|
|
|
|
|
host: setupAddress,
|
|
|
|
|
asset_id: asset.id,
|
|
|
|
|
credential_profile_id: profileId,
|
|
|
|
|
selected_items: selectedItems,
|
|
|
|
|
interval_seconds: intervalSeconds,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
if (snmpEnabled && profileId && discoveryResult) {
|
|
|
|
|
if (selectedAsset) {
|
|
|
|
|
monitorCreates.push(reconcileSnmpMonitors(asset, profileId));
|
|
|
|
|
} else {
|
|
|
|
|
monitorCreates.push(
|
|
|
|
|
api.createSnmpMonitorsFromDiscovery(token, {
|
|
|
|
|
host: setupAddress,
|
|
|
|
|
asset_id: asset.id,
|
|
|
|
|
credential_profile_id: profileId,
|
|
|
|
|
selected_items: selectedItems,
|
|
|
|
|
interval_seconds: intervalSeconds,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Promise.all(monitorCreates);
|
|
|
|
|
resetForm(asset.id);
|
|
|
|
|
await onChanged();
|
|
|
|
|
setMessage("Asset setup saved");
|
|
|
|
|
setSetupAssetId(asset.id);
|
|
|
|
|
setName(asset.name);
|
|
|
|
|
setAssetType(asset.asset_type);
|
|
|
|
|
setAddress(asset.address ?? "");
|
|
|
|
|
resetDiscovery();
|
|
|
|
|
setMessage(selectedAsset ? "Asset setup updated" : "Asset setup saved");
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setMessage(err instanceof Error ? err.message : "Could not save asset setup");
|
|
|
|
|
} finally {
|
|
|
|
@@ -208,12 +254,8 @@ export function AssetsPage({ token, assets, monitors, onChanged }: AssetsPagePro
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resetForm(nextAssetId?: number) {
|
|
|
|
|
if (nextAssetId) {
|
|
|
|
|
setSetupAssetId(nextAssetId);
|
|
|
|
|
} else {
|
|
|
|
|
setSetupAssetId("new");
|
|
|
|
|
}
|
|
|
|
|
function resetForm() {
|
|
|
|
|
setSetupAssetId("new");
|
|
|
|
|
setName("");
|
|
|
|
|
setAssetType("server");
|
|
|
|
|
setAddress("");
|
|
|
|
@@ -254,6 +296,60 @@ export function AssetsPage({ token, assets, monitors, onChanged }: AssetsPagePro
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function reconcileSnmpMonitors(asset: Asset, credentialProfileId: number) {
|
|
|
|
|
const currentSnmpMonitors = monitors.filter((monitor) => monitor.asset_id === asset.id && monitor.monitor_type === "snmp");
|
|
|
|
|
const discoveredItemIds = new Set(discoveryResult?.monitorable_items.map((item) => item.item_id) ?? []);
|
|
|
|
|
const selectedItemsById = new Map(selectedItems.map((item) => [item.item_id, item]));
|
|
|
|
|
const existingItemIds = new Set<string>();
|
|
|
|
|
const changes: Promise<unknown>[] = [];
|
|
|
|
|
|
|
|
|
|
for (const monitor of currentSnmpMonitors) {
|
|
|
|
|
const itemId = snmpItemId(monitor);
|
|
|
|
|
if (!itemId || !discoveredItemIds.has(itemId)) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
existingItemIds.add(itemId);
|
|
|
|
|
const item = selectedItemsById.get(itemId);
|
|
|
|
|
if (!item) {
|
|
|
|
|
changes.push(api.deleteMonitor(token, monitor.id));
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
changes.push(
|
|
|
|
|
api.updateMonitor(token, monitor.id, {
|
|
|
|
|
name: `${asset.name} ${item.label}`,
|
|
|
|
|
target: setupAddress,
|
|
|
|
|
interval_seconds: intervalSeconds,
|
|
|
|
|
config: {
|
|
|
|
|
...(monitor.config ?? {}),
|
|
|
|
|
credential_profile_id: credentialProfileId,
|
|
|
|
|
item_id: item.item_id,
|
|
|
|
|
item_type: item.item_type,
|
|
|
|
|
group: item.group,
|
|
|
|
|
label: item.label,
|
|
|
|
|
unit: item.unit ?? null,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newItems = selectedItems.filter((item) => !existingItemIds.has(item.item_id));
|
|
|
|
|
if (newItems.length) {
|
|
|
|
|
changes.push(
|
|
|
|
|
api.createSnmpMonitorsFromDiscovery(token, {
|
|
|
|
|
host: setupAddress,
|
|
|
|
|
asset_id: asset.id,
|
|
|
|
|
credential_profile_id: credentialProfileId,
|
|
|
|
|
selected_items: newItems,
|
|
|
|
|
interval_seconds: intervalSeconds,
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Promise.all(changes);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deleteAsset(asset: Asset, monitorCount: number) {
|
|
|
|
|
const confirmed = window.confirm(
|
|
|
|
|
monitorCount
|
|
|
|
@@ -278,6 +374,31 @@ export function AssetsPage({ token, assets, monitors, onChanged }: AssetsPagePro
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function deleteAttachedMonitor(monitor: Monitor) {
|
|
|
|
|
const confirmed = window.confirm(`Delete monitor ${monitor.name}?`);
|
|
|
|
|
if (!confirmed) return;
|
|
|
|
|
|
|
|
|
|
setDeletingMonitorId(monitor.id);
|
|
|
|
|
setMessage(null);
|
|
|
|
|
try {
|
|
|
|
|
await api.deleteMonitor(token, monitor.id);
|
|
|
|
|
const itemId = snmpItemId(monitor);
|
|
|
|
|
if (itemId) {
|
|
|
|
|
setSelectedItemIds((current) => {
|
|
|
|
|
const next = new Set(current);
|
|
|
|
|
next.delete(itemId);
|
|
|
|
|
return next;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
await onChanged();
|
|
|
|
|
setMessage("Monitor deleted");
|
|
|
|
|
} catch (err) {
|
|
|
|
|
setMessage(err instanceof Error ? err.message : "Could not delete monitor");
|
|
|
|
|
} finally {
|
|
|
|
|
setDeletingMonitorId(null);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="space-y-6">
|
|
|
|
|
<div className="flex flex-col justify-between gap-4 md:flex-row md:items-end">
|
|
|
|
@@ -310,36 +431,27 @@ export function AssetsPage({ token, assets, monitors, onChanged }: AssetsPagePro
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
{selectedAsset ? (
|
|
|
|
|
<div className="rounded-md border border-line bg-slate-950 p-3 text-sm text-slate-300">
|
|
|
|
|
<div className="font-medium">{selectedAsset.name}</div>
|
|
|
|
|
<div className="mt-1 text-slate-400">{friendlyAssetType(selectedAsset.asset_type)} - {selectedAsset.address || "No address"}</div>
|
|
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<>
|
|
|
|
|
<label className="block space-y-2">
|
|
|
|
|
<span className="text-sm text-slate-300">Name</span>
|
|
|
|
|
<input className="h-10 w-full rounded-md border border-line bg-slate-950 px-3 text-sm outline-none ring-pulse/40 focus:ring-2" value={name} onChange={(event) => setName(event.target.value)} required />
|
|
|
|
|
</label>
|
|
|
|
|
<label className="block space-y-2">
|
|
|
|
|
<span className="text-sm text-slate-300">Name</span>
|
|
|
|
|
<input className="h-10 w-full rounded-md border border-line bg-slate-950 px-3 text-sm outline-none ring-pulse/40 focus:ring-2" value={name} onChange={(event) => setName(event.target.value)} required />
|
|
|
|
|
</label>
|
|
|
|
|
|
|
|
|
|
<div className="grid gap-3 sm:grid-cols-[1fr_1.3fr]">
|
|
|
|
|
<label className="block space-y-2">
|
|
|
|
|
<span className="text-sm text-slate-300">Type</span>
|
|
|
|
|
<select className="h-10 w-full rounded-md border border-line bg-slate-950 px-3 text-sm outline-none ring-pulse/40 focus:ring-2" value={assetType} onChange={(event) => setAssetType(event.target.value)}>
|
|
|
|
|
{assetTypes.map((type) => (
|
|
|
|
|
<option key={type.value} value={type.value}>
|
|
|
|
|
{type.label}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
<label className="block space-y-2">
|
|
|
|
|
<span className="text-sm text-slate-300">Address</span>
|
|
|
|
|
<input className="h-10 w-full rounded-md border border-line bg-slate-950 px-3 text-sm outline-none ring-pulse/40 focus:ring-2" value={address} onChange={(event) => { setAddress(event.target.value); resetDiscovery(); }} placeholder="192.168.1.1 or app.example.com" />
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
<div className="grid gap-3 sm:grid-cols-[1fr_1.3fr]">
|
|
|
|
|
<label className="block space-y-2">
|
|
|
|
|
<span className="text-sm text-slate-300">Type</span>
|
|
|
|
|
<select className="h-10 w-full rounded-md border border-line bg-slate-950 px-3 text-sm outline-none ring-pulse/40 focus:ring-2" value={assetType} onChange={(event) => { setAssetType(event.target.value); resetDiscovery(); }}>
|
|
|
|
|
{assetTypes.map((type) => (
|
|
|
|
|
<option key={type.value} value={type.value}>
|
|
|
|
|
{type.label}
|
|
|
|
|
</option>
|
|
|
|
|
))}
|
|
|
|
|
</select>
|
|
|
|
|
</label>
|
|
|
|
|
<label className="block space-y-2">
|
|
|
|
|
<span className="text-sm text-slate-300">Address</span>
|
|
|
|
|
<input className="h-10 w-full rounded-md border border-line bg-slate-950 px-3 text-sm outline-none ring-pulse/40 focus:ring-2" value={address} onChange={(event) => { setAddress(event.target.value); resetDiscovery(); }} placeholder="192.168.1.1 or app.example.com" />
|
|
|
|
|
</label>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div className="grid gap-2 sm:grid-cols-2">
|
|
|
|
|
<MethodToggle active={pingEnabled} icon={<Activity size={16} />} label="Ping" onClick={() => setPingEnabled((value) => !value)} />
|
|
|
|
@@ -373,7 +485,7 @@ export function AssetsPage({ token, assets, monitors, onChanged }: AssetsPagePro
|
|
|
|
|
<div className="grid gap-3 sm:grid-cols-[1fr_auto]">
|
|
|
|
|
<label className="block space-y-2">
|
|
|
|
|
<span className="text-sm text-slate-300">SNMP Profile</span>
|
|
|
|
|
<select className="h-10 w-full rounded-md border border-line bg-[#0d131c] px-3 text-sm outline-none ring-pulse/40 focus:ring-2" value={profileId} onChange={(event) => { setProfileId(Number(event.target.value)); resetDiscovery(); }} required={snmpEnabled}>
|
|
|
|
|
<select className="h-10 w-full rounded-md border border-line bg-[#0d131c] px-3 text-sm outline-none ring-pulse/40 focus:ring-2" value={profileId} onChange={(event) => { setProfileId(event.target.value ? Number(event.target.value) : ""); resetDiscovery(); }} required={snmpEnabled}>
|
|
|
|
|
<option value="" disabled>
|
|
|
|
|
Select a profile
|
|
|
|
|
</option>
|
|
|
|
@@ -419,7 +531,7 @@ export function AssetsPage({ token, assets, monitors, onChanged }: AssetsPagePro
|
|
|
|
|
{selectedItemIds.has(item.item_id) ? <CheckSquare className="shrink-0 text-pulse" size={18} /> : <Square className="shrink-0 text-slate-500" size={18} />}
|
|
|
|
|
<span className="min-w-0">
|
|
|
|
|
<span className="block text-sm font-medium text-slate-200">{item.label}</span>
|
|
|
|
|
<span className="block text-xs text-slate-500">{friendlyItemType(item.item_type)}{item.unit ? `, ${item.unit}` : ""}</span>
|
|
|
|
|
<span className="block text-xs text-slate-500">{itemSubtitle(item)}</span>
|
|
|
|
|
</span>
|
|
|
|
|
</button>
|
|
|
|
|
))}
|
|
|
|
@@ -432,7 +544,7 @@ export function AssetsPage({ token, assets, monitors, onChanged }: AssetsPagePro
|
|
|
|
|
</div>
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
{message ? <div className={`rounded-md border p-3 text-sm ${message.includes("saved") || message.includes("deleted") ? "border-teal-500/40 bg-teal-950/40 text-teal-200" : "border-red-500/40 bg-red-950/40 text-red-200"}`}>{message}</div> : null}
|
|
|
|
|
{message ? <div className={`rounded-md border p-3 text-sm ${isSuccessMessage(message) ? "border-teal-500/40 bg-teal-950/40 text-teal-200" : "border-red-500/40 bg-red-950/40 text-red-200"}`}>{message}</div> : null}
|
|
|
|
|
|
|
|
|
|
<div className="flex gap-2">
|
|
|
|
|
<Button className="flex-1" onClick={() => resetForm()} type="button" variant="ghost">
|
|
|
|
@@ -441,7 +553,7 @@ export function AssetsPage({ token, assets, monitors, onChanged }: AssetsPagePro
|
|
|
|
|
</Button>
|
|
|
|
|
<Button className="flex-1" disabled={submitting} type="submit">
|
|
|
|
|
<Save size={16} />
|
|
|
|
|
{submitting ? "Saving..." : "Save Setup"}
|
|
|
|
|
{submitting ? "Saving..." : selectedAsset ? "Save Asset" : "Save Setup"}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</form>
|
|
|
|
@@ -486,14 +598,17 @@ export function AssetsPage({ token, assets, monitors, onChanged }: AssetsPagePro
|
|
|
|
|
<div className="divide-y divide-line">
|
|
|
|
|
{selectedAsset && selectedAssetMonitors.length ? (
|
|
|
|
|
selectedAssetMonitors.map((monitor) => (
|
|
|
|
|
<div key={monitor.id} className="grid gap-2 p-4 md:grid-cols-[1fr_110px_110px_120px] md:items-center">
|
|
|
|
|
<div key={monitor.id} className="grid gap-2 p-4 md:grid-cols-[1fr_90px_110px_90px_auto] md:items-center">
|
|
|
|
|
<div className="min-w-0">
|
|
|
|
|
<div className="truncate font-medium">{monitor.name}</div>
|
|
|
|
|
<div className="truncate text-sm text-slate-400">{monitor.target}</div>
|
|
|
|
|
</div>
|
|
|
|
|
<span className="text-sm uppercase text-slate-400">{monitor.monitor_type}</span>
|
|
|
|
|
<Status status={monitor.status} />
|
|
|
|
|
<div className="text-sm text-slate-400 md:text-right">{monitor.interval_seconds}s</div>
|
|
|
|
|
<div className="text-sm text-slate-400">{monitor.interval_seconds}s</div>
|
|
|
|
|
<Button aria-label={`Delete ${monitor.name}`} className="h-8 px-3 text-red-100" disabled={deletingMonitorId === monitor.id} onClick={() => deleteAttachedMonitor(monitor)} title="Delete monitor" type="button" variant="ghost">
|
|
|
|
|
<Trash2 className="text-red-200" size={15} />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
))
|
|
|
|
|
) : (
|
|
|
|
@@ -560,6 +675,30 @@ function friendlyItemType(value: string) {
|
|
|
|
|
return value.replaceAll("_", " ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function itemSubtitle(item: SnmpDiscoveryItem) {
|
|
|
|
|
const details = [friendlyItemType(item.item_type), item.unit, item.current_value ? `now ${item.current_value}` : null];
|
|
|
|
|
return details.filter(Boolean).join(", ");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function snmpItemId(monitor: Monitor) {
|
|
|
|
|
if (monitor.monitor_type !== "snmp") return null;
|
|
|
|
|
const itemId = monitor.config?.item_id;
|
|
|
|
|
return typeof itemId === "string" ? itemId : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function snmpCredentialProfileId(monitor: Monitor) {
|
|
|
|
|
const profileId = monitor.config?.credential_profile_id;
|
|
|
|
|
if (typeof profileId === "string") {
|
|
|
|
|
const parsed = Number(profileId);
|
|
|
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
|
|
|
}
|
|
|
|
|
return typeof profileId === "number" ? profileId : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function isSuccessMessage(message: string) {
|
|
|
|
|
return message.includes("saved") || message.includes("updated") || message.includes("deleted");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatCapabilities(capabilities: Record<string, boolean>) {
|
|
|
|
|
const labels: Record<string, string> = {
|
|
|
|
|
interfaces: "interfaces",
|
|
|
|
|