mirror of
https://github.com/ajnart/homarr.git
synced 2025-11-13 00:45:47 +01:00
Add OMV integration / widget (#1879)
feat: Add health monitoring widget (OMV) OpenMediaVault as first supported integration.
This commit is contained in:
@@ -11,4 +11,4 @@ NEXTAUTH_SECRET="anything"
|
|||||||
# Disable analytics
|
# Disable analytics
|
||||||
NEXT_PUBLIC_DISABLE_ANALYTICS="true"
|
NEXT_PUBLIC_DISABLE_ANALYTICS="true"
|
||||||
|
|
||||||
DEFAULT_COLOR_SCHEME="light"
|
DEFAULT_COLOR_SCHEME="light"
|
||||||
37
public/locales/en/modules/health-monitoring.json
Normal file
37
public/locales/en/modules/health-monitoring.json
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
{
|
||||||
|
"descriptor": {
|
||||||
|
"name": "System Health Monitoring",
|
||||||
|
"description": "Information about your NAS",
|
||||||
|
"settings": {
|
||||||
|
"title": "System Health Monitoring",
|
||||||
|
"fahrenheit": {
|
||||||
|
"label": "Fahrenheit"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"cpu": {
|
||||||
|
"label": "CPU",
|
||||||
|
"load": "Load Average",
|
||||||
|
"minute": "{{minute}} minute"
|
||||||
|
},
|
||||||
|
"memory": {
|
||||||
|
"label": "Memory",
|
||||||
|
"totalMem": "Total memory: {{total}}GB",
|
||||||
|
"available": "Available: {{available}}GB - {{percentage}}%"
|
||||||
|
},
|
||||||
|
"fileSystem": {
|
||||||
|
"label": "File System",
|
||||||
|
"available": "Available: {{available}} - {{percentage}}%"
|
||||||
|
},
|
||||||
|
"info": {
|
||||||
|
"uptime": "Uptime",
|
||||||
|
"updates": "Updates",
|
||||||
|
"reboot": "Reboot"
|
||||||
|
},
|
||||||
|
"errors": {
|
||||||
|
"general": {
|
||||||
|
"title": "Unable to find your NAS",
|
||||||
|
"text": "There was a problem connecting to your NAS. Please verify your configuration/integration(s)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -193,4 +193,9 @@ export const availableIntegrations = [
|
|||||||
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/home-assistant.png',
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/home-assistant.png',
|
||||||
label: 'Home Assistant',
|
label: 'Home Assistant',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
value: 'openmediavault',
|
||||||
|
image: 'https://cdn.jsdelivr.net/gh/walkxcode/dashboard-icons@master/png/openmediavault.png',
|
||||||
|
label: 'OpenMediaVault',
|
||||||
|
},
|
||||||
] as const satisfies Readonly<SelectItem[]>;
|
] as const satisfies Readonly<SelectItem[]>;
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { inviteRouter } from './routers/invite/invite-router';
|
|||||||
import { mediaRequestsRouter } from './routers/media-request';
|
import { mediaRequestsRouter } from './routers/media-request';
|
||||||
import { mediaServerRouter } from './routers/media-server';
|
import { mediaServerRouter } from './routers/media-server';
|
||||||
import { notebookRouter } from './routers/notebook';
|
import { notebookRouter } from './routers/notebook';
|
||||||
|
import { openmediavaultRouter } from './routers/openmediavault';
|
||||||
import { overseerrRouter } from './routers/overseerr';
|
import { overseerrRouter } from './routers/overseerr';
|
||||||
import { passwordRouter } from './routers/password';
|
import { passwordRouter } from './routers/password';
|
||||||
import { rssRouter } from './routers/rss';
|
import { rssRouter } from './routers/rss';
|
||||||
@@ -49,6 +50,7 @@ export const rootRouter = createTRPCRouter({
|
|||||||
password: passwordRouter,
|
password: passwordRouter,
|
||||||
notebook: notebookRouter,
|
notebook: notebookRouter,
|
||||||
smartHomeEntityState: smartHomeEntityStateRouter,
|
smartHomeEntityState: smartHomeEntityStateRouter,
|
||||||
|
openmediavault: openmediavaultRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
119
src/server/api/routers/openmediavault.ts
Normal file
119
src/server/api/routers/openmediavault.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import axios from 'axios';
|
||||||
|
import Consola from 'consola';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { checkIntegrationsType, findAppProperty } from '~/tools/client/app-properties';
|
||||||
|
import { getConfig } from '~/tools/config/getConfig';
|
||||||
|
|
||||||
|
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||||
|
|
||||||
|
let sessionId: string | null = null;
|
||||||
|
let loginToken: string | null = null;
|
||||||
|
|
||||||
|
async function makeOpenMediaVaultRPCCall(
|
||||||
|
serviceName: string,
|
||||||
|
method: string,
|
||||||
|
params: Record<string, any>,
|
||||||
|
headers: Record<string, string>,
|
||||||
|
input: { configName: string }
|
||||||
|
) {
|
||||||
|
const config = getConfig(input.configName);
|
||||||
|
const app = config.apps.find((app) => checkIntegrationsType(app.integration, ['openmediavault']));
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
Consola.error(`App not found for configName '${input.configName}'`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const appUrl = new URL(app.url);
|
||||||
|
const response = await axios.post(
|
||||||
|
`${appUrl.origin}/rpc.php`,
|
||||||
|
{
|
||||||
|
service: serviceName,
|
||||||
|
method: method,
|
||||||
|
params: params,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...headers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const openmediavaultRouter = createTRPCRouter({
|
||||||
|
fetchData: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
configName: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.query(async ({ input }) => {
|
||||||
|
let authResponse: any = null;
|
||||||
|
let app: any;
|
||||||
|
|
||||||
|
if (!sessionId || !loginToken) {
|
||||||
|
app = getConfig(input.configName)?.apps.find((app) =>
|
||||||
|
checkIntegrationsType(app.integration, ['openmediavault'])
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!app) {
|
||||||
|
Consola.error(
|
||||||
|
`Failed to process request to app '${app.integration}' (${app.id}). Please check username & password`
|
||||||
|
);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
authResponse = await makeOpenMediaVaultRPCCall(
|
||||||
|
'session',
|
||||||
|
'login',
|
||||||
|
{
|
||||||
|
username: findAppProperty(app, 'username'),
|
||||||
|
password: findAppProperty(app, 'password'),
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
input
|
||||||
|
);
|
||||||
|
|
||||||
|
const cookies = authResponse.headers['set-cookie'] || [];
|
||||||
|
sessionId = cookies
|
||||||
|
.find((cookie: any) => cookie.includes('X-OPENMEDIAVAULT-SESSIONID'))
|
||||||
|
?.split(';')[0];
|
||||||
|
loginToken = cookies
|
||||||
|
.find((cookie: any) => cookie.includes('X-OPENMEDIAVAULT-LOGIN'))
|
||||||
|
?.split(';')[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
const [systemInfoResponse, fileSystemResponse, cpuTempResponse] = await Promise.all([
|
||||||
|
makeOpenMediaVaultRPCCall(
|
||||||
|
'system',
|
||||||
|
'getInformation',
|
||||||
|
{},
|
||||||
|
{ Cookie: `${loginToken};${sessionId}` },
|
||||||
|
input
|
||||||
|
),
|
||||||
|
makeOpenMediaVaultRPCCall(
|
||||||
|
'filesystemmgmt',
|
||||||
|
'enumerateMountedFilesystems',
|
||||||
|
{ includeroot: true },
|
||||||
|
{ Cookie: `${loginToken};${sessionId}` },
|
||||||
|
input
|
||||||
|
),
|
||||||
|
makeOpenMediaVaultRPCCall(
|
||||||
|
'cputemp',
|
||||||
|
'get',
|
||||||
|
{},
|
||||||
|
{ Cookie: `${loginToken};${sessionId}` },
|
||||||
|
input
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
authenticated: authResponse ? authResponse.data.response.authenticated : true,
|
||||||
|
systemInfo: systemInfoResponse?.data.response,
|
||||||
|
fileSystem: fileSystemResponse?.data.response,
|
||||||
|
cpuTemp: cpuTempResponse?.data.response,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -21,6 +21,7 @@ export const boardNamespaces = [
|
|||||||
'modules/docker',
|
'modules/docker',
|
||||||
'modules/dashdot',
|
'modules/dashdot',
|
||||||
'modules/overseerr',
|
'modules/overseerr',
|
||||||
|
'modules/health-monitoring',
|
||||||
'modules/media-server',
|
'modules/media-server',
|
||||||
'modules/indexer-manager',
|
'modules/indexer-manager',
|
||||||
'modules/common-media-cards',
|
'modules/common-media-cards',
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ export type IntegrationType =
|
|||||||
| 'nzbGet'
|
| 'nzbGet'
|
||||||
| 'pihole'
|
| 'pihole'
|
||||||
| 'adGuardHome'
|
| 'adGuardHome'
|
||||||
| 'homeAssistant';
|
| 'homeAssistant'
|
||||||
|
| 'openmediavault';
|
||||||
|
|
||||||
export type AppIntegrationType = {
|
export type AppIntegrationType = {
|
||||||
type: IntegrationType | null;
|
type: IntegrationType | null;
|
||||||
@@ -101,6 +102,7 @@ export const integrationFieldProperties: {
|
|||||||
pihole: ['apiKey'],
|
pihole: ['apiKey'],
|
||||||
adGuardHome: ['username', 'password'],
|
adGuardHome: ['username', 'password'],
|
||||||
homeAssistant: ['apiKey'],
|
homeAssistant: ['apiKey'],
|
||||||
|
openmediavault: ['username', 'password'],
|
||||||
};
|
};
|
||||||
|
|
||||||
export type IntegrationFieldDefinitionType = {
|
export type IntegrationFieldDefinitionType = {
|
||||||
|
|||||||
111
src/widgets/health-monitoring/HealthMonitoringCpu.tsx
Normal file
111
src/widgets/health-monitoring/HealthMonitoringCpu.tsx
Normal 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;
|
||||||
62
src/widgets/health-monitoring/HealthMonitoringFileSystem.tsx
Normal file
62
src/widgets/health-monitoring/HealthMonitoringFileSystem.tsx
Normal 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;
|
||||||
50
src/widgets/health-monitoring/HealthMonitoringMemory.tsx
Normal file
50
src/widgets/health-monitoring/HealthMonitoringMemory.tsx
Normal 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;
|
||||||
130
src/widgets/health-monitoring/HealthMonitoringTile.tsx
Normal file
130
src/widgets/health-monitoring/HealthMonitoringTile.tsx
Normal 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;
|
||||||
@@ -5,6 +5,7 @@ import date from './date/DateTile';
|
|||||||
import dnsHoleControls from './dnshole/DnsHoleControls';
|
import dnsHoleControls from './dnshole/DnsHoleControls';
|
||||||
import dnsHoleSummary from './dnshole/DnsHoleSummary';
|
import dnsHoleSummary from './dnshole/DnsHoleSummary';
|
||||||
import torrentNetworkTraffic from './download-speed/TorrentNetworkTrafficTile';
|
import torrentNetworkTraffic from './download-speed/TorrentNetworkTrafficTile';
|
||||||
|
import healthMonitoring from './health-monitoring/HealthMonitoringTile';
|
||||||
import iframe from './iframe/IFrameTile';
|
import iframe from './iframe/IFrameTile';
|
||||||
import indexerManager from './indexer-manager/IndexerManagerTile';
|
import indexerManager from './indexer-manager/IndexerManagerTile';
|
||||||
import mediaRequestsList from './media-requests/MediaRequestListTile';
|
import mediaRequestsList from './media-requests/MediaRequestListTile';
|
||||||
@@ -40,4 +41,5 @@ export default {
|
|||||||
notebook,
|
notebook,
|
||||||
'smart-home/entity-state': smartHomeEntityState,
|
'smart-home/entity-state': smartHomeEntityState,
|
||||||
'smart-home/trigger-automation': smartHomeTriggerAutomation,
|
'smart-home/trigger-automation': smartHomeTriggerAutomation,
|
||||||
|
'health-monitoring': healthMonitoring,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user