perf: reduce widget render overhead
This commit is contained in:
1 parent
63dca7d1b6
commit
b2fbd10965
10 files changed
+133
-113
No files matched your search
@@ -39,12 +39,21 @@ interface ItemProps {
|
||||
}
|
||||
|
||||
export const SectionContentItem = ({ item, integrations }: ItemProps) => {
|
||||
const minWidth = useMinSize(item, "x");
|
||||
const minHeight = useMinSize(item, "y");
|
||||
if (item.type === "section") return <ContainerSectionContentItem item={item} />;
|
||||
|
||||
return (
|
||||
<FixedGridItem key={item.id} item={item} minWidth={minWidth} minHeight={minHeight}>
|
||||
{item.type === "item" && item.kind !== "app" && <WidgetHoverOverlay item={item} integrations={integrations} />}
|
||||
{item.type === "item" ? <BoardItemContent item={item} /> : <BoardContainerSection section={item} />}
|
||||
<FixedGridItem item={item}>
|
||||
{item.kind !== "app" && <WidgetHoverOverlay item={item} integrations={integrations} />}
|
||||
<BoardItemContent item={item} />
|
||||
</FixedGridItem>
|
||||
);
|
||||
};
|
||||
|
||||
const ContainerSectionContentItem = ({ item }: { item: ContainerSectionItem }) => {
|
||||
const { minWidth, minHeight } = useMinSize(item);
|
||||
return (
|
||||
<FixedGridItem item={item} minWidth={minWidth} minHeight={minHeight}>
|
||||
<BoardContainerSection section={item} />
|
||||
</FixedGridItem>
|
||||
);
|
||||
};
|
||||
@@ -52,14 +61,18 @@ export const SectionContentItem = ({ item, integrations }: ItemProps) => {
|
||||
/**
|
||||
* Calculates a container's minimum width or height from its direct content.
|
||||
*/
|
||||
export const useMinSize = (item: ContainerSectionItem | SectionItem, direction: "x" | "y") => {
|
||||
export const useMinSize = (item: ContainerSectionItem) => {
|
||||
const { items, innerSections } = useSectionItems(item.id);
|
||||
if (item.type === "item") return undefined;
|
||||
|
||||
const size = direction === "x" ? "width" : "height";
|
||||
return Math.max(
|
||||
1,
|
||||
...items.map((childItem) => childItem[`${direction}Offset`] + childItem[size]),
|
||||
...innerSections.map((childSection) => childSection[`${direction}Offset`] + childSection[size]),
|
||||
);
|
||||
return {
|
||||
minWidth: Math.max(
|
||||
1,
|
||||
...items.map((child) => child.xOffset + child.width),
|
||||
...innerSections.map((child) => child.xOffset + child.width),
|
||||
),
|
||||
minHeight: Math.max(
|
||||
1,
|
||||
...items.map((child) => child.yOffset + child.height),
|
||||
...innerSections.map((child) => child.yOffset + child.height),
|
||||
),
|
||||
};
|
||||
};
|
||||
@@ -34,8 +34,6 @@ import {
|
||||
IconTriangleFilled,
|
||||
IconZoomScan,
|
||||
} from "@tabler/icons-react";
|
||||
import { escapeForRegEx } from "@tiptap/react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useSession } from "@homarr/auth/client";
|
||||
import { createId } from "@homarr/common";
|
||||
@@ -63,6 +61,8 @@ interface FormValidation {
|
||||
errors: FormErrors;
|
||||
}
|
||||
|
||||
const escapeForRegEx = (value: string) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
|
||||
export const WidgetMultiReleasesRepositoriesInput = ({
|
||||
property,
|
||||
kind,
|
||||
|
||||
@@ -59,17 +59,9 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
|
||||
return (
|
||||
<Box h="100%" w="100%" pos="relative">
|
||||
<AppLink href={href} openInNewTab={options.openInNewTab} enabled={Boolean(href) && !isEditMode}>
|
||||
<Tooltip.Floating
|
||||
label={app.description?.split("\n").map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
{line}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
position="right-start"
|
||||
multiline
|
||||
disabled={options.descriptionDisplayMode !== "tooltip" || !app.description || isEditMode}
|
||||
styles={{ tooltip: { maxWidth: 300 } }}
|
||||
<AppDescriptionTooltip
|
||||
description={app.description}
|
||||
enabled={options.descriptionDisplayMode === "tooltip" && Boolean(app.description) && !isEditMode}
|
||||
>
|
||||
<Flex
|
||||
p={isTiny ? 4 : "sm"}
|
||||
@@ -123,7 +115,7 @@ export default function AppWidget({ options, isEditMode, height, width }: Widget
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Tooltip.Floating>
|
||||
</AppDescriptionTooltip>
|
||||
{options.pingEnabled &&
|
||||
!settings.forceDisableStatus &&
|
||||
!board.disableStatus &&
|
||||
@@ -148,6 +140,29 @@ interface AppLinkProps {
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
const AppDescriptionTooltip = ({
|
||||
description,
|
||||
enabled,
|
||||
children,
|
||||
}: PropsWithChildren<{ description?: string | null; enabled: boolean }>) =>
|
||||
enabled ? (
|
||||
<Tooltip.Floating
|
||||
label={description?.split("\n").map((line, index) => (
|
||||
<Fragment key={index}>
|
||||
{line}
|
||||
<br />
|
||||
</Fragment>
|
||||
))}
|
||||
position="right-start"
|
||||
multiline
|
||||
styles={{ tooltip: { maxWidth: 300 } }}
|
||||
>
|
||||
{children}
|
||||
</Tooltip.Floating>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
|
||||
const AppLink = ({ href, openInNewTab, enabled, children }: PropsWithChildren<AppLinkProps>) =>
|
||||
enabled ? (
|
||||
<UnstyledButton
|
||||
|
||||
@@ -45,8 +45,6 @@ export function InstanceCard({ instance, options, isTiny, isAdvanced, widgetKey,
|
||||
|
||||
const baseUrl = getSafeApplicationUrl(instance.integrationUrl)?.replace(/\/+$/, "") ?? "";
|
||||
const displayUrl = baseUrl ? baseUrl.replace(/^https?:\/\//, "") : "—";
|
||||
const relativeTime = useTimeAgo(instance.updatedAt);
|
||||
|
||||
const onlineServers = instance.instanceInfo.servers.filter(isCoolifyServerOnline).length;
|
||||
const runningApps = instance.instanceInfo.applications.filter(
|
||||
(a) => parseStatus(a.status ?? "") === "running",
|
||||
@@ -140,6 +138,7 @@ export function InstanceCard({ instance, options, isTiny, isAdvanced, widgetKey,
|
||||
variant="filled"
|
||||
chevronPosition="right"
|
||||
multiple
|
||||
keepMounted={false}
|
||||
value={isAdvanced ? advancedOpenSections : openSections}
|
||||
onChange={isAdvanced ? setAdvancedOpenSections : setOpenSections}
|
||||
>
|
||||
@@ -168,19 +167,27 @@ export function InstanceCard({ instance, options, isTiny, isAdvanced, widgetKey,
|
||||
</Accordion>
|
||||
|
||||
{!hideFooter && (
|
||||
<Group
|
||||
justify="space-between"
|
||||
p={4}
|
||||
style={{ borderTop: "1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4))" }}
|
||||
>
|
||||
<Text size="10px" c="dimmed">
|
||||
v{instance.instanceInfo.version}
|
||||
</Text>
|
||||
<Text size="10px" c="dimmed">
|
||||
{t("footer.updated", { when: relativeTime })}
|
||||
</Text>
|
||||
</Group>
|
||||
<InstanceFooter version={instance.instanceInfo.version} updatedAt={instance.updatedAt} />
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const InstanceFooter = ({ version, updatedAt }: { version: string; updatedAt: Date }) => {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const relativeTime = useTimeAgo(updatedAt, 60_000);
|
||||
return (
|
||||
<Group
|
||||
justify="space-between"
|
||||
p={4}
|
||||
style={{ borderTop: "1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4))" }}
|
||||
>
|
||||
<Text size="10px" c="dimmed">
|
||||
v{version}
|
||||
</Text>
|
||||
<Text size="10px" c="dimmed">
|
||||
{t("footer.updated", { when: relativeTime })}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
@@ -50,8 +50,6 @@ export function SingleInstanceLayout({
|
||||
|
||||
const baseUrl = getSafeApplicationUrl(instance.integrationUrl)?.replace(/\/+$/, "") ?? "";
|
||||
const displayUrl = baseUrl ? baseUrl.replace(/^https?:\/\//, "") : "—";
|
||||
const relativeTime = useTimeAgo(instance.updatedAt);
|
||||
|
||||
return (
|
||||
<ScrollArea h="100%">
|
||||
<Stack gap={0}>
|
||||
@@ -87,6 +85,7 @@ export function SingleInstanceLayout({
|
||||
variant="contained"
|
||||
chevronPosition="right"
|
||||
multiple
|
||||
keepMounted={false}
|
||||
value={isAdvanced ? advancedOpenSections : openSections}
|
||||
onChange={isAdvanced ? setAdvancedOpenSections : setOpenSections}
|
||||
>
|
||||
@@ -115,23 +114,31 @@ export function SingleInstanceLayout({
|
||||
</Accordion>
|
||||
|
||||
{!hideFooter && (
|
||||
<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">
|
||||
v{instance.instanceInfo.version}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("footer.updated", { when: relativeTime })}
|
||||
</Text>
|
||||
</Group>
|
||||
<InstanceFooter version={instance.instanceInfo.version} updatedAt={instance.updatedAt} />
|
||||
)}
|
||||
</Stack>
|
||||
</ScrollArea>
|
||||
);
|
||||
}
|
||||
|
||||
const InstanceFooter = ({ version, updatedAt }: { version: string; updatedAt: Date }) => {
|
||||
const t = useScopedI18n("widget.coolify");
|
||||
const relativeTime = useTimeAgo(updatedAt, 60_000);
|
||||
return (
|
||||
<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">
|
||||
v{version}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="xs" c="dimmed">
|
||||
{t("footer.updated", { when: relativeTime })}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
};
|
||||
@@ -168,6 +168,7 @@ export default function MediaMissingWidget({
|
||||
return (
|
||||
<Tabs
|
||||
value={activeTab}
|
||||
keepMounted={false}
|
||||
onChange={(value) => {
|
||||
if (value === "missing" || value === "queued") setSelectedTab(value);
|
||||
}}
|
||||
@@ -193,12 +194,12 @@ export default function MediaMissingWidget({
|
||||
|
||||
{showMissing && (
|
||||
<Tabs.Panel value="missing" flex={1} style={{ overflow: "hidden" }}>
|
||||
{renderPanel(missing, t("empty.missing"))}
|
||||
{activeTab === "missing" && renderPanel(missing, t("empty.missing"))}
|
||||
</Tabs.Panel>
|
||||
)}
|
||||
{showQueued && (
|
||||
<Tabs.Panel value="queued" flex={1} style={{ overflow: "hidden" }}>
|
||||
{renderPanel(queued, t("empty.queued"))}
|
||||
{activeTab === "queued" && renderPanel(queued, t("empty.queued"))}
|
||||
</Tabs.Panel>
|
||||
)}
|
||||
</Tabs>
|
||||
|
||||
@@ -874,27 +874,30 @@ function TaskListToggle() {
|
||||
);
|
||||
}
|
||||
|
||||
function ListIndentIncrease() {
|
||||
function useActiveListItemType() {
|
||||
const { editor } = useRichTextEditorContext();
|
||||
const [itemType, setItemType] = useState("listItem");
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
const updateItemType = () => setItemType(editor.isActive("taskItem") ? "taskItem" : "listItem");
|
||||
updateItemType();
|
||||
editor.on("selectionUpdate", updateItemType);
|
||||
return () => {
|
||||
editor.off("selectionUpdate", updateItemType);
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
return { editor, itemType };
|
||||
}
|
||||
|
||||
function ListIndentIncrease() {
|
||||
const { editor, itemType } = useActiveListItemType();
|
||||
const tControls = useScopedI18n("widget.notebook.controls");
|
||||
const handleIncreaseIndent = useCallback(() => {
|
||||
editor?.chain().focus().sinkListItem(itemType).run();
|
||||
}, [editor, itemType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
|
||||
const handleSelectionUpdate = ({ editor: updatedEditor }: { editor: Editor }) => {
|
||||
setItemType(updatedEditor.isActive("taskItem") ? "taskItem" : "listItem");
|
||||
};
|
||||
|
||||
editor.on("selectionUpdate", handleSelectionUpdate);
|
||||
return () => {
|
||||
editor.off("selectionUpdate", handleSelectionUpdate);
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<RichTextEditor.Control
|
||||
title={tControls("increaseIndent")}
|
||||
@@ -907,27 +910,13 @@ function ListIndentIncrease() {
|
||||
}
|
||||
|
||||
function ListIndentDecrease() {
|
||||
const { editor } = useRichTextEditorContext();
|
||||
const [itemType, setItemType] = useState("listItem");
|
||||
const { editor, itemType } = useActiveListItemType();
|
||||
const tControls = useScopedI18n("widget.notebook.controls");
|
||||
|
||||
const handleDecreaseIndent = useCallback(() => {
|
||||
editor?.chain().focus().liftListItem(itemType).run();
|
||||
}, [editor, itemType]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) return;
|
||||
|
||||
const handleSelectionUpdate = ({ editor: updatedEditor }: { editor: Editor }) => {
|
||||
setItemType(updatedEditor.isActive("taskItem") ? "taskItem" : "listItem");
|
||||
};
|
||||
|
||||
editor.on("selectionUpdate", handleSelectionUpdate);
|
||||
return () => {
|
||||
editor.off("selectionUpdate", handleSelectionUpdate);
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
return (
|
||||
<RichTextEditor.Control
|
||||
title={tControls("decreaseIndent")}
|
||||
|
||||
@@ -131,11 +131,12 @@ export default function ReleasesWidget({ options, itemId, width, displayMode }:
|
||||
const isInitialLoading = queryResults.some(isInitialWidgetQueryPending);
|
||||
|
||||
const repositories = useMemo(() => {
|
||||
const resultById = new Map(results.flat().map((result) => [result.id, result]));
|
||||
const formattedResults = normalizedRepositories
|
||||
.map((repository) => {
|
||||
if (!repository.provider) return { ...repository, error: { code: "noProviderSelected" } };
|
||||
|
||||
const repositoryResult = results.flat().find(({ id }) => id === repository.id);
|
||||
const repositoryResult = resultById.get(repository.id);
|
||||
if (!repositoryResult) return { ...repository, error: { code: "noProviderResponse" } };
|
||||
if (!repositoryResult.success) return { ...repository, error: repositoryResult.error };
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ export function UmamiContent({
|
||||
{ integrationId: integrationIds[0] ?? "", websiteId, timeFrame, eventNames: [...eventNames].toSorted() },
|
||||
{ enabled: viewMode === "events" && eventNames.length > 0, ...umamiQueryOptions },
|
||||
);
|
||||
const multiEventSeries = multiEventQuery.data;
|
||||
const multiEventSeries = getUsableWidgetQueryData(multiEventQuery);
|
||||
|
||||
const multiEventTotal = multiEventSeries
|
||||
? multiEventSeries.flatMap(({ dataPoints }) => dataPoints).reduce((sum, { y }) => sum + y, 0)
|
||||
@@ -106,10 +106,9 @@ export function UmamiContent({
|
||||
const selectedView =
|
||||
viewMode === "events" ? (
|
||||
<UmamiEventsContent
|
||||
integrationIds={integrationIds}
|
||||
websiteId={websiteId}
|
||||
series={multiEventSeries ?? []}
|
||||
timeFrame={timeFrame}
|
||||
eventNames={eventNames}
|
||||
hasSelectedEvents={eventNames.length > 0}
|
||||
chartType={chartType}
|
||||
showXAxis={showXAxis}
|
||||
/>
|
||||
|
||||
@@ -3,27 +3,23 @@
|
||||
import { BarChart, LineChart } from "@mantine/charts";
|
||||
import { Box, Group, Stack, Text } from "@mantine/core";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { UmamiEventSeries } from "@homarr/integrations/types";
|
||||
import { useCurrentIntlLocale, useScopedI18n } from "@homarr/translation/client";
|
||||
|
||||
import { EVENT_COLORS, formatXLabel, umamiQueryOptions } from "./umami-utils";
|
||||
import { getUsableWidgetQueryData } from "../common/query-state";
|
||||
import { EVENT_COLORS, formatXLabel } from "./umami-utils";
|
||||
|
||||
interface UmamiEventsContentProps {
|
||||
integrationIds: string[];
|
||||
websiteId: string;
|
||||
series: UmamiEventSeries[];
|
||||
timeFrame: string;
|
||||
eventNames: string[];
|
||||
hasSelectedEvents: boolean;
|
||||
chartType: string;
|
||||
showXAxis: boolean;
|
||||
}
|
||||
|
||||
export function UmamiEventsContent({
|
||||
integrationIds,
|
||||
websiteId,
|
||||
series,
|
||||
timeFrame,
|
||||
eventNames,
|
||||
hasSelectedEvents,
|
||||
chartType,
|
||||
showXAxis,
|
||||
}: UmamiEventsContentProps) {
|
||||
@@ -31,15 +27,7 @@ export function UmamiEventsContent({
|
||||
const locale = useCurrentIntlLocale();
|
||||
const tickColor = "var(--mantine-color-dimmed)";
|
||||
|
||||
const series =
|
||||
getUsableWidgetQueryData(
|
||||
clientApi.widget.umami.getMultiEventTimeSeries.useQuery(
|
||||
{ integrationId: integrationIds[0] ?? "", websiteId, timeFrame, eventNames: [...eventNames].toSorted() },
|
||||
umamiQueryOptions,
|
||||
),
|
||||
) ?? [];
|
||||
|
||||
if (eventNames.length === 0) {
|
||||
if (!hasSelectedEvents) {
|
||||
return (
|
||||
<Stack align="center" justify="center" h="100%">
|
||||
<Text c="dimmed" size="sm">
|
||||
|
||||
Reference in new issue
Block a user