Merge remote-tracking branch 'origin/v2' into feat/custom-widgets-architecture-audit
# Conflicts: # pnpm-lock.yaml
This commit is contained in:
33 files changed
+1223
-793
No files matched your search
@@ -3,7 +3,7 @@
|
||||
import { Avatar, Badge, Card, Group, SimpleGrid, Stack, Text } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { humanFileSize } from "@homarr/common";
|
||||
import { formatByteRate } from "@homarr/common";
|
||||
import { getIconUrl } from "@homarr/definitions";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
@@ -84,4 +84,4 @@ const Metric = ({ label, value }: { label: string; value: number | string }) =>
|
||||
</Stack>
|
||||
);
|
||||
|
||||
const formatBandwidth = (value?: number) => humanFileSize(Math.round(value ?? 0), "/s");
|
||||
const formatBandwidth = (value?: number) => formatByteRate(Math.round(value ?? 0));
|
||||
@@ -3,13 +3,13 @@
|
||||
import { useMemo } from "react";
|
||||
import { ActionIcon, Avatar, Badge, Center, Group, Stack, Text, Tooltip } from "@mantine/core";
|
||||
import type { IconProps } from "@tabler/icons-react";
|
||||
import { IconBrandDocker, IconPlayerPlay, IconPlayerStop, IconRotateClockwise } from "@tabler/icons-react";
|
||||
import { IconBrandDocker, IconPlayerPlay, IconPlayerStop, IconRefresh, IconRotateClockwise } from "@tabler/icons-react";
|
||||
import type { MRT_ColumnDef, MRT_VisibilityState } from "mantine-react-table";
|
||||
import { MantineReactTable } from "mantine-react-table";
|
||||
|
||||
import type { RouterOutputs } from "@homarr/api";
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { humanFileSize, useTimeAgo } from "@homarr/common";
|
||||
import { formatBytes, useTimeAgo } from "@homarr/common";
|
||||
import type { ContainerState } from "@homarr/docker";
|
||||
import { containerStateColorMap, cpuUsageColor, memoryUsageColor, safeValue } from "@homarr/docker/shared";
|
||||
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
|
||||
@@ -110,7 +110,7 @@ const createColumns = (
|
||||
|
||||
return (
|
||||
<Text size="xs" c={memoryUsageColor(bytesUsage, row.original.state)}>
|
||||
{humanFileSize(bytesUsage)}
|
||||
{formatBytes(bytesUsage)}
|
||||
</Text>
|
||||
);
|
||||
},
|
||||
@@ -184,7 +184,11 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
|
||||
const t = useScopedI18n("docker");
|
||||
const isTiny = width <= 256;
|
||||
|
||||
const { data } = clientApi.docker.getContainers.useQuery(undefined, {
|
||||
const {
|
||||
data,
|
||||
refetch,
|
||||
isFetching,
|
||||
} = clientApi.docker.getContainers.useQuery(undefined, {
|
||||
refetchInterval: 30_000,
|
||||
});
|
||||
const containers = data?.containers ?? [];
|
||||
@@ -297,12 +301,23 @@ export default function DockerWidget({ options, width, isEditMode }: WidgetCompo
|
||||
</Text>
|
||||
|
||||
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
|
||||
{t("table.totalMemory", { memory: humanFileSize(totals.memory) })}
|
||||
{t("table.totalMemory", { memory: formatBytes(totals.memory) })}
|
||||
</Text>
|
||||
|
||||
<Text size="sm" style={{ whiteSpace: "nowrap" }}>
|
||||
{t("table.updated", { when: relativeTime })}
|
||||
</Text>
|
||||
<Tooltip
|
||||
label={t("table.refresh.lastUpdated", { when: relativeTime })}
|
||||
>
|
||||
<ActionIcon
|
||||
size="sm"
|
||||
variant="transparent"
|
||||
c="var(--mantine-color-text)"
|
||||
loading={isFetching}
|
||||
onClick={() => void refetch()}
|
||||
aria-label={t("table.refresh.lastUpdated", { when: relativeTime })}
|
||||
>
|
||||
<IconRefresh style={actionIconIconStyle} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Group>
|
||||
</Group>
|
||||
)}
|
||||
|
||||
@@ -25,6 +25,7 @@ const columnTranslationKeyMap = {
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("dockerContainers", {
|
||||
icon: IconBrandDocker,
|
||||
queryKey: [["docker", "getContainers"]],
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
columns: factory.multiSelect({
|
||||
|
||||
@@ -43,7 +43,7 @@ import { MantineReactTable, useMantineReactTable } from "mantine-react-table";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useIntegrationsWithInteractAccess } from "@homarr/auth/client";
|
||||
import { humanFileSize, useIntegrationConnected } from "@homarr/common";
|
||||
import { formatByteRate, formatBytes, useIntegrationConnected } from "@homarr/common";
|
||||
import { getIconUrl, getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
import type { ExtendedClientStatus, ExtendedDownloadClientItem } from "@homarr/integrations";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
@@ -386,7 +386,7 @@ export default function DownloadClientsWidget({
|
||||
sortUndefined: "last",
|
||||
Cell: ({ cell }) => {
|
||||
const downSpeed = cell.getValue<ExtendedDownloadClientItem["downSpeed"]>();
|
||||
return downSpeed ? <Text size="xs">{humanFileSize(downSpeed, "/s")}</Text> : null;
|
||||
return downSpeed ? <Text size="xs">{formatByteRate(downSpeed)}</Text> : null;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -466,7 +466,7 @@ export default function DownloadClientsWidget({
|
||||
...columnsDefBase({ key: "received", showHeader: true }),
|
||||
Cell: ({ cell }) => {
|
||||
const received = cell.getValue<ExtendedDownloadClientItem["received"]>();
|
||||
return <Text size="xs">{humanFileSize(received)}</Text>;
|
||||
return <Text size="xs">{formatBytes(received)}</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -474,14 +474,14 @@ export default function DownloadClientsWidget({
|
||||
sortUndefined: "last",
|
||||
Cell: ({ cell }) => {
|
||||
const sent = cell.getValue<ExtendedDownloadClientItem["sent"]>();
|
||||
return sent && <Text size="xs">{humanFileSize(sent)}</Text>;
|
||||
return sent && <Text size="xs">{formatBytes(sent)}</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
...columnsDefBase({ key: "size", showHeader: true }),
|
||||
Cell: ({ cell }) => {
|
||||
const size = cell.getValue<ExtendedDownloadClientItem["size"]>();
|
||||
return <Text size="xs">{humanFileSize(size)}</Text>;
|
||||
return <Text size="xs">{formatBytes(size)}</Text>;
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -521,7 +521,7 @@ export default function DownloadClientsWidget({
|
||||
sortUndefined: "last",
|
||||
Cell: ({ cell }) => {
|
||||
const upSpeed = cell.getValue<ExtendedDownloadClientItem["upSpeed"]>();
|
||||
return upSpeed && <Text size="xs">{humanFileSize(upSpeed, "/s")}</Text>;
|
||||
return upSpeed && <Text size="xs">{formatByteRate(upSpeed)}</Text>;
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -669,21 +669,21 @@ const ItemInfoModal = ({ items, currentIndex, opened, onClose }: ItemInfoModalPr
|
||||
{item.type !== "miscellaneous" && (
|
||||
<NormalizedLine
|
||||
itemKey="upSpeed"
|
||||
values={item.upSpeed === undefined ? undefined : humanFileSize(item.upSpeed, "/s")}
|
||||
values={item.upSpeed === undefined ? undefined : formatByteRate(item.upSpeed)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<NormalizedLine
|
||||
itemKey="downSpeed"
|
||||
values={item.downSpeed === undefined ? undefined : humanFileSize(item.downSpeed, "/s")}
|
||||
values={item.downSpeed === undefined ? undefined : formatByteRate(item.downSpeed)}
|
||||
/>
|
||||
|
||||
{item.type !== "miscellaneous" && (
|
||||
<NormalizedLine itemKey="sent" values={item.sent === undefined ? undefined : humanFileSize(item.sent)} />
|
||||
<NormalizedLine itemKey="sent" values={item.sent === undefined ? undefined : formatBytes(item.sent)} />
|
||||
)}
|
||||
|
||||
<NormalizedLine itemKey="received" values={humanFileSize(item.received)} />
|
||||
<NormalizedLine itemKey="size" values={humanFileSize(item.size)} />
|
||||
<NormalizedLine itemKey="received" values={formatBytes(item.received)} />
|
||||
<NormalizedLine itemKey="size" values={formatBytes(item.size)} />
|
||||
<NormalizedLine
|
||||
itemKey="progress"
|
||||
values={new Intl.NumberFormat("en", {
|
||||
@@ -744,10 +744,7 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses }: Cli
|
||||
{ paused: [] as string[], active: [] as string[] },
|
||||
);
|
||||
const someInteract = clients.some(({ interact }) => interact);
|
||||
const totalSpeed = humanFileSize(
|
||||
clients.reduce((count, { status }) => count + (status?.rates.down ?? 0), 0),
|
||||
"/s",
|
||||
);
|
||||
const totalSpeed = formatByteRate(clients.reduce((count, { status }) => count + (status?.rates.down ?? 0), 0));
|
||||
|
||||
const utils = clientApi.useUtils();
|
||||
const invalidateDownloads = { onSettled: () => void utils.widget.downloads.getJobsAndStatuses.invalidate() };
|
||||
@@ -849,21 +846,21 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses }: Cli
|
||||
{client.status.rates.up !== undefined ? (
|
||||
<Group display="flex" justify="center" c="green" w="100%" gap={5}>
|
||||
<Text flex={1} ta="right">
|
||||
{`↑ ${humanFileSize(client.status.rates.up, "/s")}`}
|
||||
{`↑ ${formatByteRate(client.status.rates.up)}`}
|
||||
</Text>
|
||||
<Text>{"-"}</Text>
|
||||
<Text flex={1} ta="left">
|
||||
{humanFileSize(client.status.totalUp ?? 0)}
|
||||
{formatBytes(client.status.totalUp ?? 0)}
|
||||
</Text>
|
||||
</Group>
|
||||
) : undefined}
|
||||
<Group display="flex" justify="center" c="blue" w="100%" gap={5}>
|
||||
<Text flex={1} ta="right">
|
||||
{`↓ ${humanFileSize(client.status.rates.down, "/s")}`}
|
||||
{`↓ ${formatByteRate(client.status.rates.down)}`}
|
||||
</Text>
|
||||
<Text>{"-"}</Text>
|
||||
<Text flex={1} ta="left">
|
||||
{humanFileSize(Math.floor(client.status.totalDown ?? 0))}
|
||||
{formatBytes(Math.floor(client.status.totalDown ?? 0))}
|
||||
</Text>
|
||||
</Group>
|
||||
</Stack>
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
import dayjs from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
|
||||
import { capitalize, humanFileSize } from "@homarr/common";
|
||||
import { capitalize, formatBytes, formatBytesPair } from "@homarr/common";
|
||||
import type { ComputeResource, Resource, StorageResource } from "@homarr/integrations/types";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
@@ -93,16 +93,18 @@ const RightSection = ({ label, value }: RightSectionProps) => {
|
||||
|
||||
const ComputeResourceDetails = ({ item }: { item: ComputeResource }) => {
|
||||
const t = useScopedI18n("widget.healthMonitoring.cluster.popover.detail");
|
||||
const memory = formatBytesPair(item.memory.used, item.memory.total);
|
||||
const storage = formatBytesPair(item.storage.used, item.storage.total);
|
||||
return (
|
||||
<List>
|
||||
<List.Item icon={<IconCpu size={16} />}>
|
||||
{t("cpu")} - {item.cpu.cores}
|
||||
</List.Item>
|
||||
<List.Item icon={<IconBrain size={16} />}>
|
||||
{t("memory")} - {humanFileSize(item.memory.used)} / {humanFileSize(item.memory.total)}
|
||||
{t("memory")} - {memory.used} / {memory.available}
|
||||
</List.Item>
|
||||
<List.Item icon={<IconDatabase size={16} />}>
|
||||
{t("storage")} - {humanFileSize(item.storage.used)} / {humanFileSize(item.storage.total)}
|
||||
{t("storage")} - {storage.used} / {storage.available}
|
||||
</List.Item>
|
||||
<List.Item icon={<IconClockHour3 size={16} />}>
|
||||
{t("uptime")} - {dayjs(dayjs().add(-item.uptime, "seconds")).fromNow(true)}
|
||||
@@ -121,6 +123,7 @@ const ComputeResourceDetails = ({ item }: { item: ComputeResource }) => {
|
||||
const StorageResourceDetails = ({ item }: { item: StorageResource }) => {
|
||||
const t = useScopedI18n("widget.healthMonitoring.cluster.popover.detail");
|
||||
const storagePercent = item.total ? (item.used / item.total) * 100 : 0;
|
||||
const storage = formatBytesPair(item.used, item.total);
|
||||
return (
|
||||
<Stack gap={0}>
|
||||
<Center>
|
||||
@@ -133,7 +136,7 @@ const StorageResourceDetails = ({ item }: { item: StorageResource }) => {
|
||||
/>
|
||||
<Group align="center" gap={0}>
|
||||
<Text>
|
||||
{t("storage")} - {humanFileSize(item.used)} / {humanFileSize(item.total)}
|
||||
{t("storage")} - {storage.used} / {storage.available}
|
||||
</Text>
|
||||
</Group>
|
||||
</Center>
|
||||
@@ -152,11 +155,11 @@ const DiskStats = ({ item }: { item: ComputeResource }) => {
|
||||
<List.Item icon={<IconDatabase size={16} />}>
|
||||
<Group gap="sm">
|
||||
<Group gap={0}>
|
||||
<Text>{humanFileSize(item.storage.write)}</Text>
|
||||
<Text>{formatBytes(item.storage.write)}</Text>
|
||||
<IconArrowNarrowDown size={14} />
|
||||
</Group>
|
||||
<Group gap={0}>
|
||||
<Text>{humanFileSize(item.storage.read)}</Text>
|
||||
<Text>{formatBytes(item.storage.read)}</Text>
|
||||
<IconArrowNarrowUp size={14} />
|
||||
</Group>
|
||||
</Group>
|
||||
@@ -172,11 +175,11 @@ const NetStats = ({ item }: { item: ComputeResource }) => {
|
||||
<List.Item icon={<IconNetwork size={16} />}>
|
||||
<Group gap="sm">
|
||||
<Group gap={0}>
|
||||
<Text>{humanFileSize(item.network.in)}</Text>
|
||||
<Text>{formatBytes(item.network.in)}</Text>
|
||||
<IconArrowNarrowDown size={14} />
|
||||
</Group>
|
||||
<Group gap={0}>
|
||||
<Text>{humanFileSize(item.network.out)}</Text>
|
||||
<Text>{formatBytes(item.network.out)}</Text>
|
||||
<IconArrowNarrowUp size={14} />
|
||||
</Group>
|
||||
</Group>
|
||||
|
||||
@@ -32,7 +32,7 @@ import duration from "dayjs/plugin/duration";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { humanFileSize } from "@homarr/common";
|
||||
import { formatBytes } from "@homarr/common";
|
||||
import type { TranslationFunction } from "@homarr/translation";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
@@ -277,7 +277,7 @@ export const progressColor = (percentage: number) => {
|
||||
// them (e.g. Unraid, dashdot). Format the former and pass the latter through untouched.
|
||||
const formatFileSize = (value: string) => {
|
||||
const bytes = Number(value);
|
||||
return Number.isFinite(bytes) ? humanFileSize(Math.round(bytes)) : value;
|
||||
return Number.isFinite(bytes) ? formatBytes(Math.round(bytes)) : value;
|
||||
};
|
||||
|
||||
interface FileSystem {
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Group, Stack, Text } from "@mantine/core";
|
||||
import { IconDatabase, IconPhoto, IconUsers, IconVideo } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { humanFileSize } from "@homarr/common";
|
||||
import { formatBytes } from "@homarr/common";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../../common/empty-state";
|
||||
@@ -46,7 +46,7 @@ export default function ImmichServerStatsWidget({
|
||||
<StatItem
|
||||
icon={<IconDatabase size={20} />}
|
||||
label={t("widget.immich-serverStats.storage")}
|
||||
value={humanFileSize(stats.totalLibraryUsageInBytes)}
|
||||
value={formatBytes(stats.totalLibraryUsageInBytes)}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
} from "@mantine/core";
|
||||
import { IconHeartbeat, IconTransform } from "@tabler/icons-react";
|
||||
|
||||
import { humanFileSize } from "@homarr/common";
|
||||
import { formatBytes } from "@homarr/common";
|
||||
import type { TdarrQueue } from "@homarr/integrations";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
@@ -72,7 +72,7 @@ export function QueuePanel(props: QueuePanelProps) {
|
||||
</Group>
|
||||
</TableTd>
|
||||
<TableTd py={2}>
|
||||
<Text size="xs">{humanFileSize(item.fileSize)}</Text>
|
||||
<Text size="xs">{formatBytes(item.fileSize)}</Text>
|
||||
</TableTd>
|
||||
</TableTr>
|
||||
))}
|
||||
|
||||
@@ -3,7 +3,7 @@ import { Card, Center, Group, RingProgress, ScrollArea, Stack, Text, Title, Tool
|
||||
import { IconDatabaseHeart, IconFileDescription, IconHeartbeat, IconTransform } from "@tabler/icons-react";
|
||||
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { humanFileSize } from "@homarr/common";
|
||||
import { formatBytes } from "@homarr/common";
|
||||
import type { TdarrPieSegment, TdarrStatistics } from "@homarr/integrations";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
@@ -41,7 +41,7 @@ export function StatisticsPanel(props: StatisticsPanelProps) {
|
||||
<StatisticItem
|
||||
icon={IconDatabaseHeart}
|
||||
label={t("savedSpace")}
|
||||
value={humanFileSize(Math.floor(allLibs.totalSavedSpace))}
|
||||
value={formatBytes(Math.floor(allLibs.totalSavedSpace))}
|
||||
/>
|
||||
</Group>
|
||||
<Group justify="center" wrap="wrap" grow>
|
||||
|
||||
@@ -39,6 +39,8 @@ import {
|
||||
IconRowRemove,
|
||||
IconTableOff,
|
||||
IconTablePlus,
|
||||
IconTextDirectionLtr,
|
||||
IconTextDirectionRtl,
|
||||
IconX,
|
||||
} from "@tabler/icons-react";
|
||||
import { Color } from "@tiptap/extension-color";
|
||||
@@ -66,6 +68,7 @@ import { useI18n, useScopedI18n } from "@homarr/translation/client";
|
||||
import type { TablerIcon } from "@homarr/ui";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import { NotebookTextDirection, setTextDirection } from "./text-direction";
|
||||
|
||||
import "@mantine/tiptap/styles.css";
|
||||
import "./notebook.css";
|
||||
@@ -171,6 +174,7 @@ export function Notebook({ options, setOptions, isEditMode, boardId, itemId }: W
|
||||
}),
|
||||
TaskList.configure({ itemTypeName: "taskItem" }),
|
||||
TextAlign.configure({ types: ["heading", "paragraph"] }),
|
||||
NotebookTextDirection,
|
||||
TextStyle,
|
||||
],
|
||||
shouldRerenderOnTransaction: true,
|
||||
@@ -347,6 +351,22 @@ export function Notebook({ options, setOptions, isEditMode, boardId, itemId }: W
|
||||
position: t("widget.notebook.align.right"),
|
||||
})}
|
||||
/>
|
||||
<RichTextEditor.Control
|
||||
title={tControls("directionLtr")}
|
||||
aria-label={tControls("directionLtr")}
|
||||
active={editor?.isActive({ dir: "ltr" })}
|
||||
onClick={() => editor && setTextDirection(editor, "ltr").run()}
|
||||
>
|
||||
<IconTextDirectionLtr {...controlIconProps} />
|
||||
</RichTextEditor.Control>
|
||||
<RichTextEditor.Control
|
||||
title={tControls("directionRtl")}
|
||||
aria-label={tControls("directionRtl")}
|
||||
active={editor?.isActive({ dir: "rtl" })}
|
||||
onClick={() => editor && setTextDirection(editor, "rtl").run()}
|
||||
>
|
||||
<IconTextDirectionRtl {...controlIconProps} />
|
||||
</RichTextEditor.Control>
|
||||
</RichTextEditor.ControlsGroup>
|
||||
|
||||
<RichTextEditor.ControlsGroup>
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Editor } from "@tiptap/react";
|
||||
import { StarterKit } from "@tiptap/starter-kit";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { NotebookTextDirection, setTextDirection } from "./text-direction";
|
||||
|
||||
describe("notebook text direction", () => {
|
||||
it("persists right-to-left direction in the notebook HTML", () => {
|
||||
const editor = new Editor({
|
||||
content: "<p>יש לי 16GB</p>",
|
||||
extensions: [StarterKit, NotebookTextDirection],
|
||||
});
|
||||
|
||||
setTextDirection(editor, "rtl").run();
|
||||
|
||||
expect(editor.getHTML()).toBe('<p dir="rtl">יש לי 16GB</p>');
|
||||
editor.destroy();
|
||||
});
|
||||
|
||||
it("reads and changes an existing direction attribute", () => {
|
||||
const editor = new Editor({
|
||||
content: '<h2 dir="rtl">Heading</h2>',
|
||||
extensions: [StarterKit, NotebookTextDirection],
|
||||
});
|
||||
|
||||
expect(editor.getHTML()).toBe('<h2 dir="rtl">Heading</h2>');
|
||||
|
||||
editor.commands.setTextSelection(1);
|
||||
setTextDirection(editor, "ltr").run();
|
||||
|
||||
expect(editor.getHTML()).toContain('<h2 dir="ltr">Heading</h2>');
|
||||
editor.destroy();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Extension } from "@tiptap/react";
|
||||
import type { Editor } from "@tiptap/react";
|
||||
|
||||
export type TextDirection = "ltr" | "rtl";
|
||||
|
||||
export const NotebookTextDirection = Extension.create({
|
||||
name: "notebookTextDirection",
|
||||
|
||||
addGlobalAttributes() {
|
||||
return [
|
||||
{
|
||||
types: ["heading", "paragraph"],
|
||||
attributes: {
|
||||
dir: {
|
||||
default: null,
|
||||
parseHTML: (element) => element.getAttribute("dir"),
|
||||
renderHTML: ({ dir }: { dir?: TextDirection }) => (dir ? { dir } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
export function setTextDirection(editor: Editor, direction: TextDirection) {
|
||||
const nodeType = editor.isActive("heading") ? "heading" : "paragraph";
|
||||
return editor.chain().focus().updateAttributes(nodeType, { dir: direction });
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import { Box, Card, Group, Stack, Tooltip, useMantineColorScheme } from "@mantin
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useRequiredBoard } from "@homarr/boards/context";
|
||||
import { humanFileSize } from "@homarr/common";
|
||||
import { formatBytesPair } from "@homarr/common";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
@@ -20,11 +20,13 @@ const getDisplayText = (item: { used: string; available: string; percentage: num
|
||||
case "percentage":
|
||||
return `${Math.round(item.percentage)}%`;
|
||||
case "absolute": {
|
||||
const usedInBytes = Number(item.used);
|
||||
const availableInBytes = Number(item.available);
|
||||
const availableText = Number.isFinite(availableInBytes)
|
||||
? humanFileSize(Math.round(availableInBytes))
|
||||
: item.available;
|
||||
return `${item.used} / ${availableText}`;
|
||||
if (Number.isFinite(usedInBytes) && Number.isFinite(availableInBytes)) {
|
||||
const { used, available } = formatBytesPair(usedInBytes, availableInBytes);
|
||||
return `${used} / ${available}`;
|
||||
}
|
||||
return `${item.used} / ${item.available}`;
|
||||
}
|
||||
case "free":
|
||||
return `${Math.round(100 - item.percentage)}% free`;
|
||||
@@ -130,16 +132,8 @@ export default function SystemResources({ integrationIds, options }: WidgetCompo
|
||||
throw new NoIntegrationDataError();
|
||||
}
|
||||
|
||||
const fileSystem = filterStorageVolumes(
|
||||
rawFileSystem,
|
||||
options.visibleStorageVolumes,
|
||||
lastItem.integrationId,
|
||||
);
|
||||
const smart = filterStorageVolumes(
|
||||
lastItem.healthInfo.smart,
|
||||
options.visibleStorageVolumes,
|
||||
lastItem.integrationId,
|
||||
);
|
||||
const fileSystem = filterStorageVolumes(rawFileSystem, options.visibleStorageVolumes, lastItem.integrationId);
|
||||
const smart = filterStorageVolumes(lastItem.healthInfo.smart, options.visibleStorageVolumes, lastItem.integrationId);
|
||||
|
||||
if (fileSystem.length === 0) return <WidgetEmptyState />;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Box, Group, Paper, Stack, Text } from "@mantine/core";
|
||||
import { IconNetwork } from "@tabler/icons-react";
|
||||
|
||||
import { humanFileSize } from "@homarr/common";
|
||||
import { formatByteRate } from "@homarr/common";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { LabelDisplayModeOption } from "..";
|
||||
@@ -47,7 +47,7 @@ export const CombinedNetworkTrafficChart = ({
|
||||
{payloadData.value === undefined ? (
|
||||
<>N/A</>
|
||||
) : (
|
||||
<>{humanFileSize(Math.round(Number(payloadData.value)))}/s</>
|
||||
formatByteRate(Math.round(Number(payloadData.value)))
|
||||
)}
|
||||
</Text>
|
||||
</Group>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Paper, Text } from "@mantine/core";
|
||||
import { IconBrain } from "@tabler/icons-react";
|
||||
|
||||
import { humanFileSize } from "@homarr/common";
|
||||
import { formatBytesPair } from "@homarr/common";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { LabelDisplayModeOption } from "..";
|
||||
@@ -41,11 +41,11 @@ export const SystemResourceMemoryChart = ({
|
||||
tooltipProps={{
|
||||
content: ({ payload }) => {
|
||||
const value = payload[0] ? Number(payload[0].value) : 0;
|
||||
const memory = formatBytesPair(Math.round(value), totalCapacityInBytes);
|
||||
return (
|
||||
<Paper px={3} py={2} shadow="md">
|
||||
<Text c="dimmed" size="xs">
|
||||
{humanFileSize(Math.round(value))} / {humanFileSize(totalCapacityInBytes)} (
|
||||
{Math.round((value / totalCapacityInBytes) * 100)}%)
|
||||
{memory.used} / {memory.available} ({Math.round((value / totalCapacityInBytes) * 100)}%)
|
||||
</Text>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Paper, Text } from "@mantine/core";
|
||||
import { IconArrowDown, IconArrowUp } from "@tabler/icons-react";
|
||||
|
||||
import { humanFileSize } from "@homarr/common";
|
||||
import { formatByteRate } from "@homarr/common";
|
||||
import { useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { LabelDisplayModeOption } from "..";
|
||||
@@ -32,7 +32,7 @@ export const NetworkTrafficChart = ({
|
||||
title={isUp ? t("up") : t("down")}
|
||||
icon={isUp ? IconArrowUp : IconArrowDown}
|
||||
yAxisProps={{ domain: [0, upperBound] }}
|
||||
lastValue={`${humanFileSize(Math.round(max))}/s`}
|
||||
lastValue={formatByteRate(Math.round(max))}
|
||||
chartType={hasShadow ? "area" : "line"}
|
||||
labelDisplayMode={labelDisplayMode}
|
||||
tooltipProps={{
|
||||
@@ -41,7 +41,7 @@ export const NetworkTrafficChart = ({
|
||||
return (
|
||||
<Paper px={3} py={2} shadow="md">
|
||||
<Text c="dimmed" size="xs">
|
||||
{humanFileSize(Math.round(value))}/s
|
||||
{formatByteRate(Math.round(value))}
|
||||
</Text>
|
||||
</Paper>
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user