Merge remote-tracking branch 'origin/dev' into feat/static-board-ssr
This commit is contained in:
295 files changed
+4142
-6042
No files matched your search
@@ -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];
|
||||
@@ -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={
|
||||
|
||||
@@ -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" ? (
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user