feat: remove cqmin system (#2407)
* feat: remove cqmin system * fix: improve weather widget --------- Co-authored-by: Meier Lukas <meierschlumpf@gmail.com>
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
import { Box, Center, RingProgress, Text } from "@mantine/core";
|
||||
import { useElementSize } from "@mantine/hooks";
|
||||
import { IconCpu } from "@tabler/icons-react";
|
||||
|
||||
import { progressColor } from "../system-health";
|
||||
|
||||
export const CpuRing = ({ cpuUtilization }: { cpuUtilization: number }) => {
|
||||
const { width, ref } = useElementSize();
|
||||
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
|
||||
|
||||
return (
|
||||
<Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu">
|
||||
<RingProgress
|
||||
className="health-monitoring-cpu-utilization"
|
||||
roundCaps
|
||||
size={fallbackWidth * 0.95}
|
||||
thickness={fallbackWidth / 10}
|
||||
label={
|
||||
<Center style={{ flexDirection: "column" }}>
|
||||
<Text className="health-monitoring-cpu-utilization-value" size="sm">{`${cpuUtilization.toFixed(2)}%`}</Text>
|
||||
<IconCpu className="health-monitoring-cpu-utilization-icon" size={30} />
|
||||
</Center>
|
||||
}
|
||||
sections={[
|
||||
{
|
||||
value: Number(cpuUtilization.toFixed(2)),
|
||||
color: progressColor(Number(cpuUtilization.toFixed(2))),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Box, Center, RingProgress, Text } from "@mantine/core";
|
||||
import { useElementSize } from "@mantine/hooks";
|
||||
import { IconCpu } from "@tabler/icons-react";
|
||||
|
||||
import { progressColor } from "../system-health";
|
||||
|
||||
export const CpuTempRing = ({ fahrenheit, cpuTemp }: { fahrenheit: boolean; cpuTemp: number | undefined }) => {
|
||||
const { width, ref } = useElementSize();
|
||||
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
|
||||
|
||||
if (!cpuTemp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu-temperature">
|
||||
<RingProgress
|
||||
className="health-monitoring-cpu-temp"
|
||||
roundCaps
|
||||
size={fallbackWidth * 0.95}
|
||||
thickness={fallbackWidth / 10}
|
||||
label={
|
||||
<Center style={{ flexDirection: "column" }}>
|
||||
<Text className="health-monitoring-cpu-temp-value" size="sm">
|
||||
{fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp.toFixed(1)}°C`}
|
||||
</Text>
|
||||
<IconCpu className="health-monitoring-cpu-temp-icon" size={30} />
|
||||
</Center>
|
||||
}
|
||||
sections={[
|
||||
{
|
||||
value: cpuTemp,
|
||||
color: progressColor(cpuTemp),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import { Box, Center, RingProgress, Text } from "@mantine/core";
|
||||
import { useElementSize } from "@mantine/hooks";
|
||||
import { IconBrain } from "@tabler/icons-react";
|
||||
|
||||
import { progressColor } from "../system-health";
|
||||
|
||||
export const MemoryRing = ({ available, used }: { available: string; used: string }) => {
|
||||
const { width, ref } = useElementSize();
|
||||
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
|
||||
const memoryUsage = formatMemoryUsage(available, used);
|
||||
|
||||
return (
|
||||
<Box ref={ref} w="100%" h="100%" className="health-monitoring-memory">
|
||||
<RingProgress
|
||||
className="health-monitoring-memory-use"
|
||||
roundCaps
|
||||
size={fallbackWidth * 0.95}
|
||||
thickness={fallbackWidth / 10}
|
||||
label={
|
||||
<Center style={{ flexDirection: "column" }}>
|
||||
<Text className="health-monitoring-memory-value" size="sm">
|
||||
{memoryUsage.memUsed.GB}GiB
|
||||
</Text>
|
||||
<IconBrain className="health-monitoring-memory-icon" size={30} />
|
||||
</Center>
|
||||
}
|
||||
sections={[
|
||||
{
|
||||
value: Number(memoryUsage.memUsed.percent),
|
||||
color: progressColor(Number(memoryUsage.memUsed.percent)),
|
||||
tooltip: `${memoryUsage.memUsed.percent}%`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const formatMemoryUsage = (memFree: string, memUsed: string) => {
|
||||
const memFreeBytes = Number(memFree);
|
||||
const memUsedBytes = Number(memUsed);
|
||||
const totalMemory = memFreeBytes + memUsedBytes;
|
||||
const memFreeGB = (memFreeBytes / 1024 ** 3).toFixed(2);
|
||||
const memUsedGB = (memUsedBytes / 1024 ** 3).toFixed(2);
|
||||
const memFreePercent = Math.round((memFreeBytes / totalMemory) * 100);
|
||||
const memUsedPercent = Math.round((memUsedBytes / totalMemory) * 100);
|
||||
const memTotalGB = (totalMemory / 1024 ** 3).toFixed(2);
|
||||
|
||||
return {
|
||||
memFree: { percent: memFreePercent, GB: memFreeGB },
|
||||
memUsed: { percent: memUsedPercent, GB: memUsedGB },
|
||||
memTotal: { GB: memTotalGB },
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,7 @@
|
||||
[data-mantine-color-scheme="light"] .card {
|
||||
background-color: var(--mantine-color-gray-1);
|
||||
}
|
||||
|
||||
[data-mantine-color-scheme="dark"] .card {
|
||||
background-color: var(--mantine-color-dark-7);
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
Avatar,
|
||||
ActionIcon,
|
||||
Box,
|
||||
Card,
|
||||
Center,
|
||||
Divider,
|
||||
Flex,
|
||||
Group,
|
||||
@@ -12,12 +11,11 @@ import {
|
||||
List,
|
||||
Modal,
|
||||
Progress,
|
||||
RingProgress,
|
||||
Stack,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useDisclosure, useElementSize } from "@mantine/hooks";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import {
|
||||
IconBrain,
|
||||
IconClock,
|
||||
@@ -29,14 +27,20 @@ import {
|
||||
IconTemperature,
|
||||
IconVersions,
|
||||
} from "@tabler/icons-react";
|
||||
import combineClasses from "clsx";
|
||||
import dayjs from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { CpuRing } from "./rings/cpu-ring";
|
||||
import { CpuTempRing } from "./rings/cpu-temp-ring";
|
||||
import { formatMemoryUsage, MemoryRing } from "./rings/memory-ring";
|
||||
import classes from "./system-health.module.css";
|
||||
|
||||
dayjs.extend(duration);
|
||||
|
||||
@@ -55,6 +59,7 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
|
||||
);
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
const utils = clientApi.useUtils();
|
||||
const board = useRequiredBoard();
|
||||
|
||||
clientApi.widget.healthMonitoring.subscribeSystemHealthStatus.useSubscription(
|
||||
{ integrationIds },
|
||||
@@ -75,23 +80,21 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack h="100%" gap="2.5cqmin" className="health-monitoring">
|
||||
<Stack h="100%" gap="sm" className="health-monitoring">
|
||||
{healthData.map(({ integrationId, integrationName, healthInfo, updatedAt }) => {
|
||||
const disksData = matchFileSystemAndSmart(healthInfo.fileSystem, healthInfo.smart);
|
||||
const memoryUsage = formatMemoryUsage(healthInfo.memAvailable, healthInfo.memUsed);
|
||||
return (
|
||||
<Stack
|
||||
gap="2.5cqmin"
|
||||
gap="sm"
|
||||
key={integrationId}
|
||||
h="100%"
|
||||
className={`health-monitoring-information health-monitoring-${integrationName}`}
|
||||
p="2.5cqmin"
|
||||
p="sm"
|
||||
>
|
||||
<Card className="health-monitoring-information-card" p="2.5cqmin" withBorder>
|
||||
<Box className="health-monitoring-information-card" p="sm">
|
||||
<Flex
|
||||
className="health-monitoring-information-card-elements"
|
||||
h="100%"
|
||||
w="100%"
|
||||
justify="space-between"
|
||||
align="center"
|
||||
key={integrationId}
|
||||
@@ -103,13 +106,19 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
|
||||
processing
|
||||
color={healthInfo.rebootRequired ? "red" : healthInfo.availablePkgUpdates > 0 ? "blue" : "gray"}
|
||||
position="top-end"
|
||||
size="4cqmin"
|
||||
size="md"
|
||||
label={healthInfo.availablePkgUpdates > 0 ? healthInfo.availablePkgUpdates : undefined}
|
||||
disabled={!healthInfo.rebootRequired && healthInfo.availablePkgUpdates === 0}
|
||||
>
|
||||
<Avatar className="health-monitoring-information-icon-avatar" size="10cqmin" radius="sm">
|
||||
<IconInfoCircle className="health-monitoring-information-icon" size="8cqmin" onClick={open} />
|
||||
</Avatar>
|
||||
<ActionIcon
|
||||
className="health-monitoring-information-icon-avatar"
|
||||
variant={"light"}
|
||||
color="var(--mantine-color-text)"
|
||||
size={40}
|
||||
radius={board.itemRadius}
|
||||
>
|
||||
<IconInfoCircle className="health-monitoring-information-icon" size={30} onClick={open} />
|
||||
</ActionIcon>
|
||||
</Indicator>
|
||||
<Modal
|
||||
opened={opened}
|
||||
@@ -120,49 +129,31 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
|
||||
>
|
||||
<Stack gap="10px" className="health-monitoring-modal-stack">
|
||||
<Divider />
|
||||
<List className="health-monitoring-information-list" center spacing="0.5cqmin">
|
||||
<List.Item
|
||||
className="health-monitoring-information-processor"
|
||||
icon={<IconCpu2 size="1.5cqmin" />}
|
||||
>
|
||||
<List className="health-monitoring-information-list" center spacing="xs">
|
||||
<List.Item className="health-monitoring-information-processor" icon={<IconCpu2 size={30} />}>
|
||||
{t("widget.healthMonitoring.popover.processor", { cpuModelName: healthInfo.cpuModelName })}
|
||||
</List.Item>
|
||||
<List.Item
|
||||
className="health-monitoring-information-memory"
|
||||
icon={<IconBrain size="1.5cqmin" />}
|
||||
>
|
||||
<List.Item className="health-monitoring-information-memory" icon={<IconBrain size={30} />}>
|
||||
{t("widget.healthMonitoring.popover.memory", { memory: memoryUsage.memTotal.GB })}
|
||||
</List.Item>
|
||||
<List.Item
|
||||
className="health-monitoring-information-memory"
|
||||
icon={<IconBrain size="1.5cqmin" />}
|
||||
>
|
||||
<List.Item className="health-monitoring-information-memory" icon={<IconBrain size={30} />}>
|
||||
{t("widget.healthMonitoring.popover.memoryAvailable", {
|
||||
memoryAvailable: memoryUsage.memFree.GB,
|
||||
percent: memoryUsage.memFree.percent,
|
||||
})}
|
||||
</List.Item>
|
||||
<List.Item
|
||||
className="health-monitoring-information-version"
|
||||
icon={<IconVersions size="1.5cqmin" />}
|
||||
>
|
||||
<List.Item className="health-monitoring-information-version" icon={<IconVersions size={30} />}>
|
||||
{t("widget.healthMonitoring.popover.version", {
|
||||
version: healthInfo.version,
|
||||
})}
|
||||
</List.Item>
|
||||
<List.Item
|
||||
className="health-monitoring-information-uptime"
|
||||
icon={<IconClock size="1.5cqmin" />}
|
||||
>
|
||||
<List.Item className="health-monitoring-information-uptime" icon={<IconClock size={30} />}>
|
||||
{formatUptime(healthInfo.uptime, t)}
|
||||
</List.Item>
|
||||
<List.Item
|
||||
className="health-monitoring-information-load-average"
|
||||
icon={<IconCpu size="1.5cqmin" />}
|
||||
>
|
||||
<List.Item className="health-monitoring-information-load-average" icon={<IconCpu size={30} />}>
|
||||
{t("widget.healthMonitoring.popover.loadAverage")}
|
||||
</List.Item>
|
||||
<List m="0.5cqmin" withPadding center spacing="0.5cqmin" icon={<IconCpu size="1cqmin" />}>
|
||||
<List m="xs" withPadding center spacing="xs" icon={<IconCpu size={30} />}>
|
||||
<List.Item className="health-monitoring-information-load-average-1min">
|
||||
{t("widget.healthMonitoring.popover.minute")} {healthInfo.loadAverage["1min"]}%
|
||||
</List.Item>
|
||||
@@ -184,56 +175,53 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
|
||||
{options.memory && <MemoryRing available={healthInfo.memAvailable} used={healthInfo.memUsed} />}
|
||||
</Flex>
|
||||
{
|
||||
<Text
|
||||
className="health-monitoring-status-update-time"
|
||||
c="dimmed"
|
||||
size="3.5cqmin"
|
||||
ta="center"
|
||||
mb="2.5cqmin"
|
||||
>
|
||||
<Text className="health-monitoring-status-update-time" c="dimmed" size="sm" ta="center">
|
||||
{t("widget.healthMonitoring.popover.lastSeen", { lastSeen: dayjs(updatedAt).fromNow() })}
|
||||
</Text>
|
||||
}
|
||||
</Card>
|
||||
</Box>
|
||||
{options.fileSystem &&
|
||||
disksData.map((disk) => {
|
||||
return (
|
||||
<Card
|
||||
className={`health-monitoring-disk-card health-monitoring-disk-card-${integrationName}`}
|
||||
className={combineClasses(
|
||||
`health-monitoring-disk-card health-monitoring-disk-card-${integrationName}`,
|
||||
classes.card,
|
||||
)}
|
||||
key={disk.deviceName}
|
||||
p="2.5cqmin"
|
||||
withBorder
|
||||
radius={board.itemRadius}
|
||||
p="sm"
|
||||
>
|
||||
<Flex className="health-monitoring-disk-status" justify="space-between" align="center" m="1.5cqmin">
|
||||
<Group gap="1cqmin">
|
||||
<IconServer className="health-monitoring-disk-icon" size="5cqmin" />
|
||||
<Text className="dihealth-monitoring-disk-name" size="4cqmin">
|
||||
<Flex className="health-monitoring-disk-status" justify="space-between" align="center" mb="sm">
|
||||
<Group gap="xs">
|
||||
<IconServer className="health-monitoring-disk-icon" size="1rem" />
|
||||
<Text className="dihealth-monitoring-disk-name" size={"md"}>
|
||||
{disk.deviceName}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap="1cqmin">
|
||||
<IconTemperature className="health-monitoring-disk-temperature-icon" size="5cqmin" />
|
||||
<Text className="health-monitoring-disk-temperature-value" size="4cqmin">
|
||||
<Group gap="xs">
|
||||
<IconTemperature className="health-monitoring-disk-temperature-icon" size="1rem" />
|
||||
<Text className="health-monitoring-disk-temperature-value" size="md">
|
||||
{options.fahrenheit
|
||||
? `${(disk.temperature * 1.8 + 32).toFixed(1)}°F`
|
||||
: `${disk.temperature}°C`}
|
||||
</Text>
|
||||
</Group>
|
||||
<Group gap="1cqmin">
|
||||
<IconFileReport className="health-monitoring-disk-status-icon" size="5cqmin" />
|
||||
<Text className="health-monitoring-disk-status-value" size="4cqmin">
|
||||
<Group gap="xs">
|
||||
<IconFileReport className="health-monitoring-disk-status-icon" size="1rem" />
|
||||
<Text className="health-monitoring-disk-status-value" size="md">
|
||||
{disk.overallStatus ? disk.overallStatus : "N/A"}
|
||||
</Text>
|
||||
</Group>
|
||||
</Flex>
|
||||
<Progress.Root className="health-monitoring-disk-use" h="6cqmin">
|
||||
<Progress.Root className="health-monitoring-disk-use" radius={board.itemRadius} h="md">
|
||||
<Tooltip label={disk.used}>
|
||||
<Progress.Section
|
||||
value={disk.percentage}
|
||||
color={progressColor(disk.percentage)}
|
||||
className="health-monitoring-disk-use-percentage"
|
||||
>
|
||||
<Progress.Label className="health-monitoring-disk-use-value" fz="2.5cqmin">
|
||||
<Progress.Label className="health-monitoring-disk-use-value" fz="xs">
|
||||
{t("widget.healthMonitoring.popover.used")}
|
||||
</Progress.Label>
|
||||
</Progress.Section>
|
||||
@@ -251,7 +239,7 @@ export const SystemHealthMonitoring = ({ options, integrationIds }: WidgetCompon
|
||||
value={100 - disk.percentage}
|
||||
color="default"
|
||||
>
|
||||
<Progress.Label className="health-monitoring-disk-available-value" fz="2.5cqmin">
|
||||
<Progress.Label className="health-monitoring-disk-available-value" fz="xs">
|
||||
{t("widget.healthMonitoring.popover.available")}
|
||||
</Progress.Label>
|
||||
</Progress.Section>
|
||||
@@ -314,117 +302,3 @@ export const matchFileSystemAndSmart = (fileSystems: FileSystem[], smartData: Sm
|
||||
})
|
||||
.sort((fileSystemA, fileSystemB) => fileSystemA.deviceName.localeCompare(fileSystemB.deviceName));
|
||||
};
|
||||
|
||||
const CpuRing = ({ cpuUtilization }: { cpuUtilization: number }) => {
|
||||
const { width, ref } = useElementSize();
|
||||
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
|
||||
|
||||
return (
|
||||
<Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu">
|
||||
<RingProgress
|
||||
className="health-monitoring-cpu-utilization"
|
||||
roundCaps
|
||||
size={fallbackWidth * 0.95}
|
||||
thickness={fallbackWidth / 10}
|
||||
label={
|
||||
<Center style={{ flexDirection: "column" }}>
|
||||
<Text
|
||||
className="health-monitoring-cpu-utilization-value"
|
||||
size="3cqmin"
|
||||
>{`${cpuUtilization.toFixed(2)}%`}</Text>
|
||||
<IconCpu className="health-monitoring-cpu-utilization-icon" size="7cqmin" />
|
||||
</Center>
|
||||
}
|
||||
sections={[
|
||||
{
|
||||
value: Number(cpuUtilization.toFixed(2)),
|
||||
color: progressColor(Number(cpuUtilization.toFixed(2))),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const CpuTempRing = ({ fahrenheit, cpuTemp }: { fahrenheit: boolean; cpuTemp: number | undefined }) => {
|
||||
const { width, ref } = useElementSize();
|
||||
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
|
||||
|
||||
if (!cpuTemp) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box ref={ref} w="100%" h="100%" className="health-monitoring-cpu-temperature">
|
||||
<RingProgress
|
||||
className="health-monitoring-cpu-temp"
|
||||
roundCaps
|
||||
size={fallbackWidth * 0.95}
|
||||
thickness={fallbackWidth / 10}
|
||||
label={
|
||||
<Center style={{ flexDirection: "column" }}>
|
||||
<Text className="health-monitoring-cpu-temp-value" size="3cqmin">
|
||||
{fahrenheit ? `${(cpuTemp * 1.8 + 32).toFixed(1)}°F` : `${cpuTemp.toFixed(1)}°C`}
|
||||
</Text>
|
||||
<IconCpu className="health-monitoring-cpu-temp-icon" size="7cqmin" />
|
||||
</Center>
|
||||
}
|
||||
sections={[
|
||||
{
|
||||
value: cpuTemp,
|
||||
color: progressColor(cpuTemp),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const MemoryRing = ({ available, used }: { available: string; used: string }) => {
|
||||
const { width, ref } = useElementSize();
|
||||
const fallbackWidth = width || 1; // See https://github.com/homarr-labs/homarr/issues/2196
|
||||
const memoryUsage = formatMemoryUsage(available, used);
|
||||
|
||||
return (
|
||||
<Box ref={ref} w="100%" h="100%" className="health-monitoring-memory">
|
||||
<RingProgress
|
||||
className="health-monitoring-memory-use"
|
||||
roundCaps
|
||||
size={fallbackWidth * 0.95}
|
||||
thickness={fallbackWidth / 10}
|
||||
label={
|
||||
<Center style={{ flexDirection: "column" }}>
|
||||
<Text className="health-monitoring-memory-value" size="3cqmin">
|
||||
{memoryUsage.memUsed.GB}GiB
|
||||
</Text>
|
||||
<IconBrain className="health-monitoring-memory-icon" size="7cqmin" />
|
||||
</Center>
|
||||
}
|
||||
sections={[
|
||||
{
|
||||
value: Number(memoryUsage.memUsed.percent),
|
||||
color: progressColor(Number(memoryUsage.memUsed.percent)),
|
||||
tooltip: `${memoryUsage.memUsed.percent}%`,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
export const formatMemoryUsage = (memFree: string, memUsed: string) => {
|
||||
const memFreeBytes = Number(memFree);
|
||||
const memUsedBytes = Number(memUsed);
|
||||
const totalMemory = memFreeBytes + memUsedBytes;
|
||||
const memFreeGB = (memFreeBytes / 1024 ** 3).toFixed(2);
|
||||
const memUsedGB = (memUsedBytes / 1024 ** 3).toFixed(2);
|
||||
const memFreePercent = Math.round((memFreeBytes / totalMemory) * 100);
|
||||
const memUsedPercent = Math.round((memUsedBytes / totalMemory) * 100);
|
||||
const memTotalGB = (totalMemory / 1024 ** 3).toFixed(2);
|
||||
|
||||
return {
|
||||
memFree: { percent: memFreePercent, GB: memFreeGB },
|
||||
memUsed: { percent: memUsedPercent, GB: memUsedGB },
|
||||
memTotal: { GB: memTotalGB },
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user