Merge remote-tracking branch 'origin/dev' into feat/static-board-ssr

This commit is contained in:
Thomas Camlong committed 2026-06-04 23:09:38 +02:00
commit 73550ce998
295 files changed
+4142 -6042

No files matched your search

-5
View File
@@ -1,5 +0,0 @@
import baseConfig from "@homarr/eslint-config/base";
import reactConfig from "@homarr/eslint-config/react";
/** @type {import('typescript-eslint').Config} */
export default [...baseConfig, ...reactConfig];
+9 -13
View File
@@ -4,12 +4,6 @@
"private": true,
"license": "Apache-2.0",
"type": "module",
"exports": {
".": "./index.ts",
"./errors": "./src/errors/component.tsx",
"./modals": "./src/modals/index.ts",
"./prefetch": "./src/prefetch.ts"
},
"typesVersions": {
"*": {
"*": [
@@ -17,13 +11,18 @@
]
}
},
"exports": {
".": "./index.ts",
"./errors": "./src/errors/component.tsx",
"./modals": "./src/modals/index.ts",
"./prefetch": "./src/prefetch.ts"
},
"scripts": {
"clean": "rm -rf .turbo node_modules",
"format": "prettier --check . --ignore-path ../../.gitignore",
"lint": "eslint",
"format": "oxfmt .",
"lint": "oxlint",
"typecheck": "tsc --noEmit"
},
"prettier": "@homarr/prettier-config",
"dependencies": {
"@dnd-kit/core": "catalog:",
"@dnd-kit/sortable": "catalog:",
@@ -50,6 +49,7 @@
"@homarr/validation": "workspace:^0.1.0",
"@mantine/charts": "catalog:",
"@mantine/core": "catalog:",
"@mantine/form": "catalog:",
"@mantine/hooks": "catalog:",
"@tabler/icons-react": "catalog:",
"@tiptap/extension-color": "catalog:",
@@ -70,7 +70,6 @@
"@tiptap/starter-kit": "catalog:",
"clsx": "catalog:",
"dayjs": "catalog:",
"mantine-form-zod-resolver": "catalog:",
"mantine-react-table": "catalog:",
"next": "catalog:",
"react": "catalog:",
@@ -82,11 +81,8 @@
"zod": "catalog:"
},
"devDependencies": {
"@homarr/eslint-config": "workspace:^0.2.0",
"@homarr/prettier-config": "workspace:^0.1.0",
"@homarr/tsconfig": "workspace:^0.1.0",
"@types/video.js": "catalog:",
"eslint": "catalog:",
"typescript": "catalog:"
}
}
@@ -25,9 +25,22 @@ export const WidgetDynamicSelectInput = ({ property, kind, options }: CommonWidg
const [search, setSearch] = useState("");
const [debouncedSearch] = useDebouncedValue(search, 300);
const { isPending, options: selectOptions } = options.useOptions(debouncedSearch, form.values.integrationIds);
const value = inputProps.value as DynamicSelectOption | null;
const currentOption = inputProps.value as DynamicSelectOption | null;
const onChange = inputProps.onChange as (value: DynamicSelectOption | null) => void;
const getLabelForValue = (selectedValue: string) => {
const matched = selectOptions.find((option) =>
typeof option === "string" ? option === selectedValue : option.value === selectedValue,
);
if (!matched) {
return selectedValue;
}
if (typeof matched === "string") {
return matched;
}
return translateIfNecessary(t, matched.label) ?? selectedValue;
};
return (
<Select
label={tWidget("label")}
@@ -41,14 +54,13 @@ export const WidgetDynamicSelectInput = ({ property, kind, options }: CommonWidg
},
)
.concat(
typeof value === "object" &&
value !== null &&
currentOption !== null &&
!selectOptions.some((option) =>
typeof option === "string" ? option === value.value : option.value === value.value,
typeof option === "string" ? option === currentOption.value : option.value === currentOption.value,
)
? {
value: value.value,
label: value.label,
value: currentOption.value,
label: currentOption.label,
}
: [],
)}
@@ -74,19 +86,17 @@ export const WidgetDynamicSelectInput = ({ property, kind, options }: CommonWidg
description={options.withDescription ? tWidget("description") : undefined}
searchable
{...inputProps}
value={value?.value ?? null}
onChange={(value) =>
onChange(
value
? {
value,
label:
selectOptions.filter((option) => typeof option === "object").find((option) => option.value === value)
?.label ?? value,
}
: null,
)
}
value={currentOption === null ? null : currentOption.value}
onChange={(selectedValue: string | null) => {
if (selectedValue === null) {
onChange(null);
return;
}
onChange({
value: selectedValue,
label: getLabelForValue(selectedValue),
});
}}
/>
);
};
@@ -53,7 +53,7 @@ export const WidgetSelectInput = ({ property, kind, options }: CommonWidgetInput
)}
leftSection={CurrentIcon && <CurrentIcon size={16} stroke={1.5} />}
renderOption={({ option, checked }) => {
const Icon = getIconFor(options.options, option.value);
const Icon = getIconFor(options.options, String(option.value));
return (
<Group flex="1" gap="xs">
@@ -194,8 +194,6 @@ const Item = <TItem, TOptionValue extends UniqueIdentifier>({
return (
<Card
withBorder
shadow="xs"
padding="sm"
radius="md"
style={
+17 -2
View File
@@ -16,7 +16,7 @@ export default function BookmarksWidget({ options, itemId }: WidgetComponentProp
const board = useRequiredBoard();
const [data] = clientApi.app.byIds.useSuspenseQuery(options.items, {
select(data) {
return data.sort((appA, appB) => options.items.indexOf(appA.id) - options.items.indexOf(appB.id));
return data.toSorted((appA, appB) => options.items.indexOf(appA.id) - options.items.indexOf(appB.id));
},
});
@@ -56,6 +56,7 @@ export default function BookmarksWidget({ options, itemId }: WidgetComponentProp
hideIcon={options.hideIcon}
hideHostname={options.hideHostname}
openNewTab={options.openNewTab}
withBorder={options.withBorder}
hasIconColor={board.iconColor !== null}
/>
)}
@@ -67,6 +68,7 @@ export default function BookmarksWidget({ options, itemId }: WidgetComponentProp
hideIcon={options.hideIcon}
hideHostname={options.hideHostname}
openNewTab={options.openNewTab}
withBorder={options.withBorder}
hasIconColor={board.iconColor !== null}
/>
)}
@@ -81,6 +83,7 @@ interface FlexLayoutProps {
hideIcon: boolean;
hideHostname: boolean;
openNewTab: boolean;
withBorder: boolean;
hasIconColor: boolean;
}
@@ -91,6 +94,7 @@ const FlexLayout = ({
hideIcon,
hideHostname,
openNewTab,
withBorder,
hasIconColor,
}: FlexLayoutProps) => {
const board = useRequiredBoard();
@@ -106,7 +110,15 @@ const FlexLayout = ({
key={app.id}
w="100%"
>
<Card radius={board.itemRadius} className={classes.card} w="100%" display="flex" p={4} h="100%">
<Card
radius={board.itemRadius}
className={classes.card}
w="100%"
display="flex"
p={4}
h="100%"
withBorder={withBorder}
>
{direction === "row" ? (
<VerticalItem
app={app}
@@ -138,6 +150,7 @@ interface GridLayoutProps {
hideIcon: boolean;
hideHostname: boolean;
openNewTab: boolean;
withBorder: boolean;
itemDirection: "horizontal" | "vertical";
hasIconColor: boolean;
}
@@ -148,6 +161,7 @@ const GridLayout = ({
hideIcon,
hideHostname,
openNewTab,
withBorder,
itemDirection,
hasIconColor,
}: GridLayoutProps) => {
@@ -168,6 +182,7 @@ const GridLayout = ({
h="100%"
className={combineClasses(classes.card, classes["card-grid"])}
radius={board.itemRadius}
withBorder={withBorder}
p="xs"
>
{itemDirection === "horizontal" ? (
+1
View File
@@ -24,6 +24,7 @@ export const { definition, componentLoader } = createWidgetDefinition("bookmarks
hideIcon: factory.switch({ defaultValue: false }),
hideHostname: factory.switch({ defaultValue: false }),
openNewTab: factory.switch({ defaultValue: true }),
withBorder: factory.switch({ defaultValue: false }),
items: factory.sortableItemList<RouterOutputs["app"]["all"][number], string>({
ItemComponent: ({ item, handle: Handle, removeItem, rootAttributes }) => {
return (
@@ -50,7 +50,7 @@ export function InstanceCard({ instance, options, isTiny, widgetKey }: InstanceC
).length;
return (
<Card p={0} radius="sm" withBorder>
<Card p={0} radius="sm">
<Group
p="xs"
justify="space-between"
@@ -11,7 +11,7 @@ import { buildServerResourceCounts } from "./coolify-utils";
import { ServersSection } from "./servers-section";
import { ServicesSection } from "./services-section";
import type { CoolifyOptions, InstanceData } from "./types";
import { COOLIFY_ICON_URL } from "./types";
import { COOLIFY_BRAND_COLOR, COOLIFY_ICON_URL } from "./types";
interface SingleInstanceLayoutProps {
instance: InstanceData;
@@ -44,10 +44,10 @@ export function SingleInstanceLayout({ instance, options, isTiny, widgetKey }: S
return (
<ScrollArea h="100%">
<Stack gap={0}>
<Group p="xs" justify="center" gap="xs" style={{ borderBottom: "2px solid #8B5CF6" }}>
<Group p="xs" justify="center" gap="xs" style={{ borderBottom: `2px solid ${COOLIFY_BRAND_COLOR}` }}>
<Group gap={2}>
<Image src={COOLIFY_ICON_URL} alt="Coolify" w={isTiny ? 18 : 24} h={isTiny ? 18 : 24} />
<Text fz={isTiny ? "xs" : "sm"} fw={700} style={{ color: "#8B5CF6" }}>
<Text fz={isTiny ? "xs" : "sm"} fw={700} style={{ color: COOLIFY_BRAND_COLOR }}>
oolify
</Text>
</Group>
@@ -75,7 +75,11 @@ export function SingleInstanceLayout({ instance, options, isTiny, widgetKey }: S
)}
</Accordion>
<Group justify="space-between" p={4} style={{ borderTop: "1px solid var(--mantine-color-dark-4)" }}>
<Group
justify="space-between"
p={4}
style={{ borderTop: "1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4))" }}
>
<Group gap={2}>
<Image src={COOLIFY_ICON_URL} alt="Coolify" w={16} h={16} />
<Text size="xs" c="dimmed">
+1
View File
@@ -3,6 +3,7 @@ import type { CoolifyInstanceInfo } from "@homarr/integrations/types";
import type { WidgetComponentProps } from "../definition";
export const COOLIFY_ICON_URL = "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/coolify.svg";
export const COOLIFY_BRAND_COLOR = "#8B5CF6";
export interface InstanceData {
integrationId: string;
+1 -1
View File
@@ -62,7 +62,7 @@ const createColumns = (
Cell({ renderedCellValue, row }) {
return (
<Group gap="xs" wrap="nowrap">
<Avatar variant="outline" radius="md" size={20} src={row.original.iconUrl} />
<Avatar variant="outline" size={20} src={row.original.iconUrl} />
<Text p="0.5" size="sm" style={{ overflow: "hidden", textOverflow: "ellipsis" }}>
{renderedCellValue}
</Text>
+4 -4
View File
@@ -65,8 +65,8 @@ const columnsRatios: Record<keyof ExtendedDownloadClientItem, number> = {
id: 1,
index: 1,
integration: 1,
name: 8,
progress: 4,
name: 16,
progress: 6,
ratio: 2,
received: 3,
sent: 3,
@@ -805,7 +805,7 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses }: Cli
<Chip.Group
multiple
value={filters.statuses}
onChange={(statuses) => setFilters({ ...filters, statuses: statuses as typeof filters.statuses })}
onChange={(statuses) => setFilters({ ...filters, statuses: statuses })}
>
{availableStatuses.map((status) => (
<Chip key={status} value={status}>
@@ -864,7 +864,7 @@ const ClientsControl = ({ clients, filters, setFilters, availableStatuses }: Cli
<Stack key={client.integration.id} gap="10px">
<Divider />
<Group wrap="nowrap" w="100%">
<Paper withBorder radius={999}>
<Paper radius={999}>
<Group gap={5} pl={10} pr={15} fz={16} w={275} justify="space-between" wrap="nowrap">
<Avatar radius={0} src={getIconUrl(client.integration.kind)} />
{client.status ? (
+2 -2
View File
@@ -153,7 +153,7 @@ export default function FirewallWidget({ integrationIds, width, itemId }: Widget
style={{ justifyContent: "flex-start" }}
>
<IconArrowBarUp size={isTiny ? "8" : "12"} color="lightgreen" />
<Text size={isTiny ? "8px" : "xs"} color="lightgreen" style={{ textAlign: "left" }}>
<Text size={isTiny ? "8px" : "xs"} c="lightgreen" style={{ textAlign: "left" }}>
{formatBitsPerSec(transmit, 2)}
</Text>
</Flex>
@@ -164,7 +164,7 @@ export default function FirewallWidget({ integrationIds, width, itemId }: Widget
style={{ justifyContent: "flex-start" }}
>
<IconArrowBarDown size={isTiny ? "8" : "12"} color="yellow" />
<Text size={isTiny ? "8px" : "xs"} color="yellow" style={{ textAlign: "left" }}>
<Text size={isTiny ? "8px" : "xs"} c="yellow" style={{ textAlign: "left" }}>
{formatBitsPerSec(receive, 2)}
</Text>
</Flex>
@@ -1,4 +1,4 @@
import { Group, Indicator, Popover, Table, TableTbody, TableThead, TableTr, Text } from "@mantine/core";
import { Group, Indicator, Popover, Table, TableTbody, TableTh, TableThead, TableTr, Text } from "@mantine/core";
import type { Resource } from "@homarr/integrations/types";
import { useI18n } from "@homarr/translation/client";
@@ -17,23 +17,23 @@ export const ResourceTable = ({ type, data, isTiny }: ResourceTableProps) => {
<Table highlightOnHover>
<TableThead>
<TableTr fz={isTiny ? "8px" : "xs"}>
<Table.Th ta="start" p={0}>
<TableTh ta="start" p={0}>
{t("widget.healthMonitoring.cluster.table.header.name")}
</Table.Th>
</TableTh>
{type !== "storage" ? (
<Table.Th ta="start" p={0}>
<TableTh ta="start" p={0}>
{t("widget.healthMonitoring.cluster.table.header.cpu")}
</Table.Th>
</TableTh>
) : null}
{type !== "storage" ? (
<Table.Th ta="start" p={0}>
<TableTh ta="start" p={0}>
{t("widget.healthMonitoring.cluster.table.header.memory")}
</Table.Th>
</TableTh>
) : null}
{type === "storage" ? (
<Table.Th ta="start" p={0}>
<TableTh ta="start" p={0}>
{t("widget.healthMonitoring.cluster.table.header.node")}
</Table.Th>
</TableTh>
) : null}
</TableTr>
</TableThead>
@@ -1,20 +1,10 @@
.statItem {
padding: 12px;
border-radius: var(--mantine-radius-md);
background-color: var(--mantine-color-gray-0);
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
transition: all 0.2s ease;
&:hover {
background-color: var(--mantine-color-gray-1);
}
}
@media (prefers-color-scheme: dark) {
.statItem {
background-color: var(--mantine-color-dark-6);
&:hover {
background-color: var(--mantine-color-dark-5);
}
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
}
}
@@ -95,7 +95,6 @@ const MediaRequestCard = ({ request, isTiny, options }: MediaRequestCardProps) =
className={`mediaRequests-list-item-wrapper mediaRequests-list-item-${request.type} mediaRequests-list-item-${request.status}`}
radius={board.itemRadius}
p="xs"
withBorder
>
<Image
className="mediaRequests-list-item-background"
@@ -214,7 +213,6 @@ const DecisionButtons = ({ requestId, integrationId }: DecisionButtonsProps) =>
variant="light"
color="green"
size="xs"
radius="md"
onClick={() => {
handleDecision("approve");
}}
@@ -228,7 +226,6 @@ const DecisionButtons = ({ requestId, integrationId }: DecisionButtonsProps) =>
variant="light"
color="red"
size="xs"
radius="md"
onClick={() => {
handleDecision("decline");
}}
@@ -23,6 +23,9 @@ import type { WidgetComponentProps } from "../../definition";
import { NoIntegrationDataError } from "../../errors/no-data-integration";
import classes from "./component.module.css";
const OVERSEERR_COLOR = "#ECB000";
const JELLYSEERR_COLOR = "#6677CC";
export default function MediaServerWidget({
integrationIds,
isEditMode,
@@ -103,7 +106,7 @@ export default function MediaServerWidget({
<Text className="mediaRequests-stats-stats-title" fw="bold" ta="center" size={isTiny ? "xs" : "sm"}>
{t("titles.stats.main")}
</Text>
<Grid className="mediaRequests-stats-stats-grid" gutter={4} w="100%">
<Grid className="mediaRequests-stats-stats-grid" gap={4} w="100%">
{data.map((stat) => (
<Grid.Col
className={combineClasses("mediaRequests-stats-stat-wrapper", `mediaRequests-stats-stat-${stat.name}`)}
@@ -151,7 +154,7 @@ export default function MediaServerWidget({
className="mediaRequests-stats-users-user-avatar"
size={20}
src={user.avatar}
bd={`2px solid ${user.integration.kind === "overseerr" ? "#ECB000" : "#6677CC"}`}
bd={`2px solid ${user.integration.kind === "overseerr" ? OVERSEERR_COLOR : JELLYSEERR_COLOR}`}
/>
</Tooltip>
<Text className="mediaRequests-stats-users-user-userName" size="sm">
@@ -17,6 +17,8 @@ import { WorkersPanel } from "./panels/workers.panel";
type View = (typeof views)[number];
const viewBySegmentValue = Object.fromEntries(views.map((view) => [view, view])) as Record<string, View>;
const viewIcons = {
workers: IconCpu2,
queue: IconClipboardList,
@@ -83,7 +85,12 @@ export default function MediaTranscodingWidget({
};
})}
value={view}
onChange={(value) => setView(value as View)}
onChange={(value) => {
const nextView = viewBySegmentValue[value];
if (nextView) {
setView(nextView);
}
}}
size="xs"
/>
@@ -27,7 +27,11 @@ export function HealthCheckStatus(props: HealthCheckStatusProps) {
<IconHeartbeat size={16} />
</Indicator>
</HoverCard.Target>
<HoverCard.Dropdown bg={colorScheme === "light" ? "gray.2" : "dark.8"}>
<HoverCard.Dropdown
style={{
backgroundColor: "light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-8))",
}}
>
<Stack gap="sm" align="center">
<Group gap="xs">
<IconHeartbeat size={18} />
@@ -1,4 +1,17 @@
import { Center, Group, ScrollArea, Table, TableTd, TableTh, TableTr, Text, Title, Tooltip } from "@mantine/core";
import {
Center,
Group,
ScrollArea,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
Tooltip,
} from "@mantine/core";
import { IconHeartbeat, IconTransform } from "@tabler/icons-react";
import { humanFileSize } from "@homarr/common";
@@ -25,7 +38,7 @@ export function QueuePanel(props: QueuePanelProps) {
return (
<ScrollArea style={{ flex: "1" }}>
<Table style={{ tableLayout: "fixed" }}>
<Table.Thead>
<TableThead>
<TableTr>
<TableTh ta="start" py={4}>
<Text size="xs" fw="bold">
@@ -38,8 +51,8 @@ export function QueuePanel(props: QueuePanelProps) {
</Text>
</TableTh>
</TableTr>
</Table.Thead>
<Table.Tbody>
</TableThead>
<TableTbody>
{queue.array.map((item) => (
<TableTr key={item.id}>
<TableTd py={2}>
@@ -63,7 +76,7 @@ export function QueuePanel(props: QueuePanelProps) {
</TableTd>
</TableTr>
))}
</Table.Tbody>
</TableTbody>
</Table>
</ScrollArea>
);
@@ -91,7 +91,7 @@ function StatisticItem(props: StatisticItemProps) {
const board = useRequiredBoard();
return (
<Tooltip label={props.label}>
<Card p={0} withBorder radius={board.itemRadius} miw={48} flex={1}>
<Card p={0} radius={board.itemRadius} miw={48} flex={1}>
<Group justify="center" align="center" gap="xs" w="100%" wrap="nowrap">
<props.icon size={16} style={{ minWidth: 16 }} />
<Text size="md">{props.value}</Text>
@@ -4,8 +4,10 @@ import {
Progress,
ScrollArea,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Text,
Title,
@@ -35,7 +37,7 @@ export function WorkersPanel(props: WorkersPanelProps) {
return (
<ScrollArea style={{ flex: "1" }}>
<Table style={{ tableLayout: "fixed" }}>
<Table.Thead>
<TableThead>
<TableTr>
<TableTh ta="start" py={4}>
<Text size="xs" fw="bold">
@@ -53,8 +55,8 @@ export function WorkersPanel(props: WorkersPanelProps) {
</Text>
</TableTh>
</TableTr>
</Table.Thead>
<Table.Tbody>
</TableThead>
<TableTbody>
{props.workers.map((worker) => {
const fileName = worker.filePath.split("\\").pop()?.split("/").pop() ?? worker.filePath;
return (
@@ -101,7 +103,7 @@ export function WorkersPanel(props: WorkersPanelProps) {
</TableTr>
);
})}
</Table.Tbody>
</TableTbody>
</Table>
</ScrollArea>
);
@@ -2,7 +2,7 @@
import { useState } from "react";
import { Button, Group, Stack } from "@mantine/core";
import { zod4Resolver } from "mantine-form-zod-resolver";
import { schemaResolver } from "@mantine/form";
import { z } from "zod/v4";
import { objectEntries } from "@homarr/common";
@@ -50,7 +50,7 @@ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, i
const form = useForm({
mode: "controlled",
initialValues: innerProps.value,
validate: zod4Resolver(
validate: schemaResolver(
z.object({
options: z.object(
objectEntries(options).reduce(
@@ -70,6 +70,7 @@ export const WidgetEditModal = createModal<ModalProps<WidgetKind>>(({ actions, i
borderColor: z.string(),
}),
}),
{ sync: true },
),
validateInputOnBlur: true,
validateInputOnChange: true,
@@ -421,7 +421,6 @@ export function Notebook({ options, setOptions, isEditMode, boardId, itemId }: W
color={primaryColor}
variant="light"
size={30}
radius={"md"}
onClick={handleEditToggle}
>
{isEditing ? <IconDeviceFloppy {...iconProps} /> : <IconEdit {...iconProps} />}
@@ -438,7 +437,6 @@ export function Notebook({ options, setOptions, isEditMode, boardId, itemId }: W
color={primaryColor}
variant="light"
size={30}
radius={"md"}
onClick={handleEditCancel}
>
<IconX {...iconProps} />
@@ -67,7 +67,7 @@ export default function NotificationsWidget({ options, integrationIds }: WidgetC
<Stack w={"100%"} gap="sm">
{sortedNotifications.length > 0 ? (
sortedNotifications.map((notification) => (
<Card key={notification.id} withBorder radius={board.itemRadius} w="100%" p="sm">
<Card key={notification.id} radius={board.itemRadius} w="100%" p="sm">
<Flex gap="sm" direction="column" w="100%">
{notification.title && (
<Text fz="sm" lh="sm" lineClamp={2}>
@@ -1,10 +1,8 @@
.releasesRepository {
border-left: 2px solid transparent;
&:has(.releasesRepositoryHeader:hover),
&:has(.releasesRepositoryHeader.active),
&:has(.releasesRepositoryDetails:hover) {
border-left-color: var(--mantine-color-secondaryColor-text);
background-color: var(--mantine-color-secondaryColor-light);
}
}
@@ -57,7 +57,6 @@ export default function RssFeed({ options, width }: WidgetComponentProps<"rssFee
{feedEntries.map((feedEntry) => (
<Card
key={feedEntry.id}
withBorder
component={"a"}
href={feedEntry.link}
radius={board.itemRadius}
@@ -78,7 +78,7 @@ export default function SmartHomeTriggerAutomationWidget({
</Center>
</Overlay>
)}
<LoadingOverlay visible={isPending} zIndex={1000} overlayProps={{ radius: "sm", blur: 2 }} />
<LoadingOverlay visible={isPending} />
<Center w="100%" h="100%">
<Stack align="center" gap="md">
<IconAutomation size={isTiny ? 16 : undefined} />
@@ -38,7 +38,7 @@ export const CombinedNetworkTrafficChart = ({
tooltipProps={{
content: ({ payload }) => {
return (
<Paper px={3} py={2} withBorder shadow="md" radius="md">
<Paper px={3} py={2} shadow="md">
<Stack gap={0}>
{payload.map((payloadData) => (
<Group key={payloadData.key} gap={4}>
@@ -36,7 +36,7 @@ export const SystemResourceCPUChart = ({
content: ({ payload }) => {
const value = payload[0] ? Number(payload[0].value) : 0;
return (
<Paper px={3} py={2} withBorder shadow="md" radius="md">
<Paper px={3} py={2} shadow="md">
<Text c="dimmed" size="xs">
{value.toFixed(0)}%
</Text>
@@ -36,7 +36,7 @@ export const SystemResourceGPUChart = ({
content: ({ payload }) => {
const value = payload[0] ? Number(payload[0].value) : 0;
return (
<Paper px={3} py={2} withBorder shadow="md" radius="md">
<Paper px={3} py={2} shadow="md">
<Text c="dimmed" size="xs">
{value.toFixed(0)}%
</Text>
@@ -42,7 +42,7 @@ export const SystemResourceMemoryChart = ({
content: ({ payload }) => {
const value = payload[0] ? Number(payload[0].value) : 0;
return (
<Paper px={3} py={2} withBorder shadow="md" radius="md">
<Paper px={3} py={2} shadow="md">
<Text c="dimmed" size="xs">
{humanFileSize(Math.round(value))} / {humanFileSize(totalCapacityInBytes)} (
{Math.round((value / totalCapacityInBytes) * 100)}%)
@@ -39,7 +39,7 @@ export const NetworkTrafficChart = ({
content: ({ payload }) => {
const value = payload[0] ? Number(payload[0].value) : 0;
return (
<Paper px={3} py={2} withBorder shadow="md" radius="md">
<Paper px={3} py={2} shadow="md">
<Text c="dimmed" size="xs">
{humanFileSize(Math.round(value))}/s
</Text>
@@ -24,7 +24,7 @@ export function RecentActivityList({ sessions }: { sessions: TracearrHistorySess
: session.mediaTitle;
return (
<Paper key={session.id} p="sm" radius="lg" withBorder>
<Paper key={session.id} p="sm" radius="lg">
<Group justify="space-between" wrap="nowrap">
<Group gap="xs" wrap="nowrap" style={{ overflow: "hidden" }}>
<Avatar src={session.user.avatarUrl} alt={session.user.username} radius="xl" size="sm" />
@@ -32,7 +32,7 @@ export function StatsBar({
function StatCard({ icon, label, value }: { icon: React.ReactNode; label: string; value: string | number }) {
return (
<Paper p="xs" radius="lg" withBorder>
<Paper p="xs" radius="lg">
<Group gap={4} wrap="nowrap">
{icon}
<Stack gap={0} style={{ overflow: "hidden" }}>
@@ -45,7 +45,7 @@ function StreamCard({ stream, compact }: { stream: TracearrStream; compact: bool
: null;
return (
<Paper p="xs" radius="lg" withBorder style={{ position: "relative", overflow: "hidden" }}>
<Paper p="xs" radius="lg" style={{ position: "relative", overflow: "hidden" }}>
{stream.posterUrl && (
<Box
style={{
@@ -100,7 +100,7 @@ function StreamCard({ stream, compact }: { stream: TracearrStream; compact: bool
</Group>
</Group>
<Tooltip label={mediaLabel} openDelay={300}>
<Tooltip label={mediaLabel}>
<Text size="xs" c="dimmed" lineClamp={1}>
{mediaLabel}
</Text>
@@ -19,7 +19,7 @@ export function ViolationsList({ violations }: { violations: TracearrViolation[]
) : (
<Stack gap="xs">
{violations.map((violation) => (
<Paper key={violation.id} p="xs" radius="lg" withBorder>
<Paper key={violation.id} p="xs" radius="lg">
<Group justify="space-between" wrap="nowrap">
<Group gap="xs" wrap="nowrap" style={{ overflow: "hidden" }}>
<IconAlertTriangle