Add OMV integration / widget (#1879)

feat: Add health monitoring widget (OMV)
OpenMediaVault as first supported integration.
This commit is contained in:
Yossi Hillali
2024-02-27 21:44:52 +02:00
committed by GitHub
parent db2501633d
commit b51fcdb342
12 changed files with 523 additions and 2 deletions

View File

@@ -0,0 +1,111 @@
import { Center, Flex, Group, HoverCard, RingProgress, Text } from '@mantine/core';
import { IconCpu } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
const HealthMonitoringCpu = ({ info, cpuTemp, fahrenheit }: any) => {
const { t } = useTranslation('modules/health-monitoring');
const toFahrenheit = (value: number) => {
return Math.round(value * 1.8 + 32);
};
interface LoadDataItem {
label: string;
stats: number;
progress: number;
color: string;
}
const loadData = [
{
label: `${t('cpu.minute', { minute: 1 })}`,
stats: info.loadAverage['1min'],
progress: info.loadAverage['1min'],
color: 'teal',
},
{
label: `${t('cpu.minute', { minute: 5 })}`,
stats: info.loadAverage['5min'],
progress: info.loadAverage['5min'],
color: 'blue',
},
{
label: `${t('cpu.minute', { minute: 15 })}`,
stats: info.loadAverage['15min'],
progress: info.loadAverage['15min'],
color: 'red',
},
] as const;
return (
<Group position="center">
<RingProgress
roundCaps
size={140}
thickness={12}
label={
<Center style={{ flexDirection: 'column' }}>
{info.cpuUtilization.toFixed(2)}%
<HoverCard width={280} shadow="md" position="top">
<HoverCard.Target>
<IconCpu size={40} />
</HoverCard.Target>
<HoverCard.Dropdown>
<Text fz="lg" tt="uppercase" fw={700} c="dimmed" align="center">
{t('cpu.load')}
</Text>
<Flex
direction={{ base: 'column', sm: 'row' }}
gap={{ base: 'sm', sm: 'lg' }}
justify={{ sm: 'center' }}
>
{loadData.map((load: LoadDataItem) => (
<RingProgress
size={80}
roundCaps
thickness={8}
label={
<Text color={load.color} weight={700} align="center" size="xl">
{load.progress}
</Text>
}
sections={[{ value: load.progress, color: load.color, tooltip: load.label }]}
/>
))}
</Flex>
</HoverCard.Dropdown>
</HoverCard>
</Center>
}
sections={[
{
value: info.cpuUtilization.toFixed(2),
color: info.cpuUtilization.toFixed(2) > 70 ? 'red' : 'green',
},
]}
/>
<RingProgress
roundCaps
size={140}
thickness={12}
label={
<Center
style={{
flexDirection: 'column',
}}
>
{fahrenheit ? `${toFahrenheit(cpuTemp.cputemp)}°F` : `${cpuTemp.cputemp}°C`}
<IconCpu size={40} />
</Center>
}
sections={[
{
value: cpuTemp.cputemp,
color: cpuTemp.cputemp < 60 ? 'green' : 'red',
},
]}
/>
</Group>
);
};
export default HealthMonitoringCpu;

View File

@@ -0,0 +1,62 @@
import { Center, Flex, Group, HoverCard, RingProgress, Text } from '@mantine/core';
import { IconServer } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
import { humanFileSize } from '~/tools/humanFileSize';
import { ringColor } from './HealthMonitoringTile';
const HealthMonitoringFileSystem = ({ fileSystem }: any) => {
const { t } = useTranslation('modules/health-monitoring');
interface FileSystemDisk {
devicename: string;
used: string;
percentage: number;
available: number;
}
return (
<Group position="center">
<Flex
direction={{ base: 'column', sm: 'row' }}
gap={{ base: 'sm', sm: 'lg' }}
justify={{ sm: 'center' }}
>
{fileSystem.map((disk: FileSystemDisk) => (
<RingProgress
size={140}
roundCaps
thickness={12}
label={
<Center style={{ flexDirection: 'column' }}>
{disk.devicename}
<HoverCard width={280} shadow="md" position="top">
<HoverCard.Target>
<IconServer size={40} />
</HoverCard.Target>
<HoverCard.Dropdown>
<Text fz="lg" tt="uppercase" fw={700} c="dimmed" align="center">
{t('fileSystem.available', {
available: humanFileSize(disk.available),
percentage: 100 - disk.percentage,
})}
</Text>
</HoverCard.Dropdown>
</HoverCard>
</Center>
}
sections={[
{
value: disk.percentage,
color: ringColor(disk.percentage),
tooltip: disk.used,
},
]}
/>
))}
</Flex>
</Group>
);
};
export default HealthMonitoringFileSystem;

View File

@@ -0,0 +1,50 @@
import { Center, Group, HoverCard, RingProgress, Text } from '@mantine/core';
import { IconBrain } from '@tabler/icons-react';
import { useTranslation } from 'react-i18next';
import { ringColor } from './HealthMonitoringTile';
const HealthMonitoringMemory = ({ info }: any) => {
const { t } = useTranslation('modules/health-monitoring');
const totalMemoryGB: any = (info.memTotal / 1024 ** 3).toFixed(2);
const freeMemoryGB: any = (info.memAvailable / 1024 ** 3).toFixed(2);
const usedMemoryGB: any = ((info.memTotal - info.memAvailable) / 1024 ** 3).toFixed(2);
const percentageUsed: any = ((usedMemoryGB / totalMemoryGB) * 100).toFixed(2);
const percentageFree: any = (100 - percentageUsed).toFixed(2);
return (
<Group position="center">
<RingProgress
roundCaps
size={140}
thickness={12}
label={
<Center style={{ flexDirection: 'column' }}>
{usedMemoryGB}GiB
<HoverCard width={280} shadow="md" position="top">
<HoverCard.Target>
<IconBrain size={40} />
</HoverCard.Target>
<HoverCard.Dropdown>
<Text fz="lg" tt="uppercase" fw={700} c="dimmed" align="center">
{t('memory.totalMem', { total: totalMemoryGB })}
</Text>
<Text fz="lg" fw={500} align="center">
{t('memory.available', { available: freeMemoryGB, percentage: percentageFree })}
</Text>
</HoverCard.Dropdown>
</HoverCard>
</Center>
}
sections={[
{
value: percentageUsed,
color: ringColor(percentageUsed),
},
]}
/>
</Group>
);
};
export default HealthMonitoringMemory;

View File

@@ -0,0 +1,130 @@
import { Card, Divider, Flex, Group, ScrollArea, Text } from '@mantine/core';
import {
IconCloudDownload,
IconHeartRateMonitor,
IconInfoSquare,
IconStatusChange,
} from '@tabler/icons-react';
import { useTranslation } from 'next-i18next';
import { useConfigContext } from '~/config/provider';
import { api } from '~/utils/api';
import { defineWidget } from '../helper';
import { WidgetLoading } from '../loading';
import { IWidget } from '../widgets';
import HealthMonitoringCpu from './HealthMonitoringCpu';
import HealthMonitoringFileSystem from './HealthMonitoringFileSystem';
import HealthMonitoringMemory from './HealthMonitoringMemory';
const definition = defineWidget({
id: 'health-monitoring',
icon: IconHeartRateMonitor,
options: {
fahrenheit: {
type: 'switch',
defaultValue: false,
},
cpu: {
type: 'switch',
defaultValue: true,
},
memory: {
type: 'switch',
defaultValue: true,
},
fileSystem: {
type: 'switch',
defaultValue: true,
},
},
gridstack: {
minWidth: 1,
minHeight: 1,
maxWidth: 6,
maxHeight: 6,
},
component: HealthMonitoringWidgetTile,
});
export type IHealthMonitoringWidget = IWidget<(typeof definition)['id'], typeof definition>;
interface HealthMonitoringWidgetProps {
widget: IHealthMonitoringWidget;
}
function HealthMonitoringWidgetTile({ widget }: HealthMonitoringWidgetProps) {
const { t } = useTranslation('modules/health-monitoring');
const { isInitialLoading, data } = useOpenmediavaultQuery();
if (isInitialLoading || !data) {
return <WidgetLoading />;
}
const formatUptime = (uptime: number) => {
const days = Math.floor(uptime / (60 * 60 * 24));
const remainingHours = Math.floor((uptime % (60 * 60 * 24)) / 3600);
return `${days} days, ${remainingHours} hours`;
};
return (
<Flex h="100%" w="100%" direction="column">
<ScrollArea>
<Card>
<Group position="center">
<IconInfoSquare size={40} />
<Text fz="lg" tt="uppercase" fw={700} c="dimmed" align="center">
{t('info.uptime')}:
<br />
{formatUptime(data.systemInfo.uptime)}
</Text>
<Group position="center">
{data.systemInfo.availablePkgUpdates === 0 ? (
''
) : (
<IconCloudDownload size={40} color="red" />
)}
{data.systemInfo.rebootRequired ? <IconStatusChange size={40} color="red" /> : ''}
</Group>
</Group>
</Card>
<Divider my="sm" />
<Group position="center">
{widget?.properties.cpu && (
<HealthMonitoringCpu
info={data.systemInfo}
cpuTemp={data.cpuTemp}
fahrenheit={widget?.properties.fahrenheit}
/>
)}
{widget?.properties.memory && <HealthMonitoringMemory info={data.systemInfo} />}
</Group>
{widget?.properties.fileSystem && (
<>
<Divider my="sm" />
<HealthMonitoringFileSystem fileSystem={data.fileSystem} />
</>
)}
</ScrollArea>
</Flex>
);
}
export const ringColor = (percentage: number) => {
if (percentage < 30) return 'green';
else if (percentage < 60) return 'yellow';
else if (percentage < 90) return 'orange';
else return 'red';
};
export const useOpenmediavaultQuery = () => {
const { name: configName } = useConfigContext();
return api.openmediavault.fetchData.useQuery(
{
configName: configName!,
},
{
staleTime: 1000 * 10,
}
);
};
export default definition;

View File

@@ -5,6 +5,7 @@ import date from './date/DateTile';
import dnsHoleControls from './dnshole/DnsHoleControls';
import dnsHoleSummary from './dnshole/DnsHoleSummary';
import torrentNetworkTraffic from './download-speed/TorrentNetworkTrafficTile';
import healthMonitoring from './health-monitoring/HealthMonitoringTile';
import iframe from './iframe/IFrameTile';
import indexerManager from './indexer-manager/IndexerManagerTile';
import mediaRequestsList from './media-requests/MediaRequestListTile';
@@ -40,4 +41,5 @@ export default {
notebook,
'smart-home/entity-state': smartHomeEntityState,
'smart-home/trigger-automation': smartHomeTriggerAutomation,
'health-monitoring': healthMonitoring,
};