diff --git a/core/components/StatsManager/playerDrop/classifyDropReason.test.ts b/core/components/StatsManager/playerDrop/classifyDropReason.test.ts index 49ca29261..f00f34810 100644 --- a/core/components/StatsManager/playerDrop/classifyDropReason.test.ts +++ b/core/components/StatsManager/playerDrop/classifyDropReason.test.ts @@ -19,7 +19,7 @@ const serverInitiatedExamples = [ `Server shutting down: %s`, `[txAdmin] Server restarting (scheduled restart at 03:00).`, //not so sure about this ]; -const networkingExamples = [ +const timeoutExamples = [ `Server->client connection timed out. Pending commands: %d.\nCommand list:\n%s`, `Server->client connection timed out. Last seen %d msec ago.`, `Fetching info timed out.`, @@ -64,9 +64,9 @@ suite('classifyDropReason', () => { expect(fnc(reason).category).toBe('server-initiated'); } }); - it('should classify networking reasons', () => { - for (const reason of networkingExamples) { - expect(fnc(reason).category).toBe('networking'); + it('should classify timeout reasons', () => { + for (const reason of timeoutExamples) { + expect(fnc(reason).category).toBe('timeout'); } }); it('should classify security reasons', () => { diff --git a/core/components/StatsManager/playerDrop/classifyDropReason.ts b/core/components/StatsManager/playerDrop/classifyDropReason.ts index a8c87b219..4bdd223c2 100644 --- a/core/components/StatsManager/playerDrop/classifyDropReason.ts +++ b/core/components/StatsManager/playerDrop/classifyDropReason.ts @@ -15,7 +15,7 @@ const serverInitiatedRules = [ `server shutting down:`, `[txadmin]`, ]; -const networkingRules = [ +const timeoutRules = [ `server->client connection timed out`, //basically only see this one `connection timed out`, `fetching info timed out`, @@ -120,8 +120,8 @@ export const classifyDropReason = (reason: string) => { return { category: 'user-initiated' }; } else if (serverInitiatedRules.some((rule) => reasonToMatch.startsWith(rule))) { return { category: 'server-initiated' }; - } else if (networkingRules.some((rule) => reasonToMatch.includes(rule))) { - return { category: 'networking' }; + } else if (timeoutRules.some((rule) => reasonToMatch.includes(rule))) { + return { category: 'timeout' }; } else if (securityRules.some((rule) => reasonToMatch.includes(rule))) { return { category: 'security' }; } else if (crashRulesIntl.some((rule) => reasonToMatch.includes(rule))) { diff --git a/package-lock.json b/package-lock.json index 8033d8729..dd89e6adb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13939,15 +13939,6 @@ "react-dom": ">=16.6.0" } }, - "node_modules/react-virtualized-auto-sizer": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/react-virtualized-auto-sizer/-/react-virtualized-auto-sizer-1.0.24.tgz", - "integrity": "sha512-3kCn7N9NEb3FlvJrSHWGQ4iVl+ydQObq2fHMn12i5wbtm74zHOPhz/i64OL3c1S1vi9i2GXtZqNqUJTQ+BnNfg==", - "peerDependencies": { - "react": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0", - "react-dom": "^15.3.0 || ^16.0.0-alpha || ^17.0.0 || ^18.0.0" - } - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", @@ -17962,7 +17953,6 @@ "react-hot-toast": "^2.4.1", "react-icons": "^5.1.0", "react-markdown": "^9.0.1", - "react-virtualized-auto-sizer": "^1.0.24", "socket.io-client": "^4.7.5", "swr": "^2.2.5", "tailwind-merge": "^2.2.2", diff --git a/panel/package.json b/panel/package.json index 9a19ee772..1a89d37ba 100644 --- a/panel/package.json +++ b/panel/package.json @@ -59,7 +59,6 @@ "react-hot-toast": "^2.4.1", "react-icons": "^5.1.0", "react-markdown": "^9.0.1", - "react-virtualized-auto-sizer": "^1.0.24", "socket.io-client": "^4.7.5", "swr": "^2.2.5", "tailwind-merge": "^2.2.2", diff --git a/panel/src/components/DebouncedResizeContainer.tsx b/panel/src/components/DebouncedResizeContainer.tsx new file mode 100644 index 000000000..feac49327 --- /dev/null +++ b/panel/src/components/DebouncedResizeContainer.tsx @@ -0,0 +1,92 @@ +import { debounce } from 'throttle-debounce'; +import { useRef, useEffect, useCallback, memo } from 'react'; +import { Loader2Icon } from 'lucide-react'; + +type SizeType = { + width: number; + height: number; +}; + +type DebouncedResizeContainerProps = { + delay?: number; + onDebouncedResize: ({ width, height }: SizeType) => void; + children: React.ReactNode; +}; + +/** + * A container that will call onDebouncedResize with the width and height of the container after a resize event. + */ +function DebouncedResizeContainerInner({ + delay, + onDebouncedResize, + children, +}: DebouncedResizeContainerProps) { + const containerRef = useRef(null); + const loaderRef = useRef(null); + const childRef = useRef(null); + const lastMeasure = useRef({ width: 0, height: 0 }); + if (delay === undefined) delay = 250; + + const updateSizeState = () => { + if (!containerRef.current || !containerRef.current.parentNode) return; + const measures = { + width: containerRef.current.clientWidth, + height: containerRef.current.clientHeight + } + lastMeasure.current = measures; + onDebouncedResize(measures); + childRef.current!.style.visibility = 'visible'; + loaderRef.current!.style.visibility = 'hidden'; + } + + const debouncedResizer = useCallback( + debounce(delay, updateSizeState, { atBegin: false }), + [containerRef] + ); + + useEffect(() => { + if (!containerRef.current) return; + const resizeObserver = new ResizeObserver(() => { + const currHeight = containerRef.current!.clientHeight; + const currWidth = containerRef.current!.clientWidth; + if (currHeight === 0 || currWidth === 0) return; + if (lastMeasure.current.width === currWidth && lastMeasure.current.height === currHeight) return; + if (lastMeasure.current.width === 0 || lastMeasure.current.height === 0) { + updateSizeState(); + } else { + debouncedResizer(); + childRef.current!.style.visibility = 'hidden'; + loaderRef.current!.style.visibility = 'visible'; + } + }); + resizeObserver.observe(containerRef.current); + updateSizeState(); + return () => resizeObserver.disconnect(); + }, [containerRef]); + + return ( +
+
+ +
+
+ {children} +
+
+ ); +} + +export default memo(DebouncedResizeContainerInner); diff --git a/panel/src/pages/Dashboard/DashboardPage.tsx b/panel/src/pages/Dashboard/DashboardPage.tsx index 9249ca019..f515b6fd7 100644 --- a/panel/src/pages/Dashboard/DashboardPage.tsx +++ b/panel/src/pages/Dashboard/DashboardPage.tsx @@ -1,8 +1,8 @@ import { useEffect, useMemo, useState } from 'react'; import { GaugeIcon } from 'lucide-react'; import { Button } from '@/components/ui/button'; -import PlayerDropCard from './PlayerDropCard'; import ThreadPerfCard from './ThreadPerfCard'; +import PlayerDropCard from './PlayerDropCard'; import * as d3ScaleChromatic from 'd3-scale-chromatic'; import { getMinTickIntervalMarker } from './chartingUtils'; import FullPerfCard from './FullPerfCard'; @@ -22,7 +22,7 @@ export type PlayerDropChartDatum = { } export default function DashboardPage() { - const [isRunning, setIsRunning] = useState(true); + const [isRunning, setIsRunning] = useState(false); const [rndCounter, setRndCounter] = useState(491); const [ztoCounter, setZtoCounter] = useState(0); @@ -48,7 +48,7 @@ export default function DashboardPage() { data.push({ bucket: boundaries[i], count: Math.round(Math.random() * 1000), - value: Math.max(0, (Math.sin((indexPct + ztoCounter) * 2 * Math.PI) -1 ) + 1), //rnd + value: Math.max(0, (Math.sin((indexPct + ztoCounter) * 2 * Math.PI) - 1) + 1), //rnd // value: Math.max(0, Math.sin(i * 0.24 + tmpMultiplier)) / 2.8, //rnd // value: Math.max(0, Math.sin(i * 0.295 + 0.7)) / 2.8, //good // value: Math.max(0, Math.sin(i * 0.295 + -0.6)) / 2.8, //bad @@ -92,8 +92,8 @@ export default function DashboardPage() { value: 180 / tmpTotal, }, { - "id": "networking", - "label": "Networking", + "id": "timeout", + "label": "Timeout", count: 169, value: 169 / tmpTotal, }, @@ -113,12 +113,11 @@ export default function DashboardPage() { return data; }, [rndCounter]); - return ( -
-
+
+
-
+

Host stats (last minute)

diff --git a/panel/src/pages/Dashboard/FullPerfCard.tsx b/panel/src/pages/Dashboard/FullPerfCard.tsx index 93640e067..90c6cae30 100644 --- a/panel/src/pages/Dashboard/FullPerfCard.tsx +++ b/panel/src/pages/Dashboard/FullPerfCard.tsx @@ -1,8 +1,10 @@ -import { BarChartHorizontalIcon } from 'lucide-react'; -import { memo, useEffect, useRef, useState } from 'react'; -import AutoSizer from "react-virtualized-auto-sizer"; +import { BarChartHorizontalIcon, LineChartIcon } from 'lucide-react'; +import { memo, useCallback, useEffect, useRef, useState } from 'react'; +import AutoSizer, { Size, VerticalSize } from "react-virtualized-auto-sizer"; import * as d3 from 'd3'; import { createRandomHslColor } from '@/lib/utils'; +import { debounce } from 'throttle-debounce'; +import DebouncedResizeContainer from '@/components/DebouncedResizeContainer'; type FullPerfChartProps = any; @@ -23,7 +25,7 @@ const FullPerfChart = memo(({ useEffect(() => void d3.select(gx.current).call(d3.axisBottom(x)), [gx, x]); useEffect(() => void d3.select(gy.current).call(d3.axisLeft(y)), [gy, y]); return ( - + d3.ticks(-2, 2, 200).map(Math.sin)); - function onMouseMove(event) { - const [x, y] = d3.pointer(event); - setData(data.slice(-200).concat(Math.atan2(x, y))); - } return (

Thread performance (last minute)

-
-
-
- - {({ height, width }) => ( - - )} - +
+ + +
); } diff --git a/panel/src/pages/Dashboard/PlayerDropCard.tsx b/panel/src/pages/Dashboard/PlayerDropCard.tsx index 5cd0f6d22..bb51fdcd2 100644 --- a/panel/src/pages/Dashboard/PlayerDropCard.tsx +++ b/panel/src/pages/Dashboard/PlayerDropCard.tsx @@ -1,10 +1,10 @@ -import AutoSizer from "react-virtualized-auto-sizer"; import { memo, useCallback, useMemo, useState } from 'react'; import { LegendDatum, Pie, DatumId, PieCustomLayerProps, ComputedDatum } from '@nivo/pie'; import { numberToLocaleString } from '@/lib/utils'; import { PlayerDropChartDatum } from './DashboardPage'; import { DoorOpenIcon } from 'lucide-react'; import { useIsDarkMode } from '@/hooks/theme'; +import DebouncedResizeContainer from "@/components/DebouncedResizeContainer"; type PieCenterTextProps = PieCustomLayerProps & { active?: ComputedDatum; @@ -71,9 +71,11 @@ type PlayerDropChartProps = { setCustomLegends: (data: LegendDatum[]) => void; activeId: DatumId | null; setActiveId: (id: DatumId | null) => void; + width: number; + height: number; }; -const PlayerDropChart = memo(({ data, setCustomLegends, activeId, setActiveId }: PlayerDropChartProps) => { +const PlayerDropChart = memo(({ data, setCustomLegends, activeId, setActiveId, width, height }: PlayerDropChartProps) => { const isDarkMode = useIsDarkMode(); const [hasClicked, setHasClicked] = useState(false); const CenterLayer = useCallback((allArgs: PieCustomLayerProps) => { @@ -82,75 +84,88 @@ const PlayerDropChart = memo(({ data, setCustomLegends, activeId, setActiveId }: return PieCenterText({ ...allArgs, active }); }, [activeId]); - return - {({ height, width }) => ( - setHasClicked(curr => !curr)} - onMouseEnter={(datum, event) => setHasClicked(false)} //resets behavior - onMouseLeave={(datum, event) => { - hasClicked && setActiveId(datum.id) - event.preventDefault() - }} - colors={{ scheme: 'nivo' }} - tooltip={() => null} - forwardLegendData={setCustomLegends} - /> - )} - + if (!width || !height) return null; + return ( + setHasClicked(curr => !curr)} + onMouseEnter={(datum, event) => setHasClicked(false)} //resets behavior + onMouseLeave={(datum, event) => { + hasClicked && setActiveId(datum.id) + event.preventDefault() + }} + colors={{ scheme: 'nivo' }} + tooltip={() => null} + forwardLegendData={(data) => { + console.log('forwardLegendData', data); + setCustomLegends(data); + }} + /> + ) }); +const getInitialLegendsData = (data: PlayerDropChartDatum[]) => { + return data.map(d => ({ + id: d.id, + label: d.label, + color: 'transparent', + } as any)); +} + type PlayerDropCardProps = { data: PlayerDropChartDatum[]; }; export default function PlayerDropCard({ data }: PlayerDropCardProps) { - const [customLegends, setCustomLegends] = useState[]>([]) + const [customLegends, setCustomLegends] = useState[]>(getInitialLegendsData(data)) const [activeId, setActiveId] = useState(null) + const [chartSize, setChartSize] = useState({ width: 0, height: 0 }); return ( -
+

Player drop reasons (last 6h)

-
+ -
+
{customLegends.map(legend => { return ( diff --git a/panel/src/pages/Dashboard/ThreadPerfCard.tsx b/panel/src/pages/Dashboard/ThreadPerfCard.tsx index fc62d08ea..2e34dcad4 100644 --- a/panel/src/pages/Dashboard/ThreadPerfCard.tsx +++ b/panel/src/pages/Dashboard/ThreadPerfCard.tsx @@ -1,19 +1,21 @@ -import AutoSizer from "react-virtualized-auto-sizer"; import { Bar, BarTooltipProps } from '@nivo/bar'; import { BarChartHorizontalIcon } from 'lucide-react'; -import { memo } from 'react'; +import { memo, useState } from 'react'; import { useIsDarkMode } from '@/hooks/theme'; import { ThreadPerfChartDatum } from './DashboardPage'; import { formatTickBoundary } from './chartingUtils'; +import DebouncedResizeContainer from "@/components/DebouncedResizeContainer"; type ThreadPerfChartProps = { data: ThreadPerfChartDatum[]; boundaries: (number | string)[]; minTickIntervalMarker: number | undefined; + width: number; + height: number; }; -const ThreadPerfChart = memo(({ data, minTickIntervalMarker }: ThreadPerfChartProps) => { +const ThreadPerfChart = memo(({ data, minTickIntervalMarker, width, height }: ThreadPerfChartProps) => { const isDarkMode = useIsDarkMode(); const CustomToolbar = (datum: BarTooltipProps) => { @@ -35,100 +37,99 @@ const ThreadPerfChart = memo(({ data, minTickIntervalMarker }: ThreadPerfChartPr ); } + if (!width || !height) return null; return ( - - {({ height, width }) => ( - - )} - + ); }); type ThreadPerfCardProps = { - data: ThreadPerfChartProps; + data: Omit; }; export default function ThreadPerfCard({ data }: ThreadPerfCardProps) { + const [chartSize, setChartSize] = useState({ width: 0, height: 0 }); + return ( -
+

Thread performance (last minute)

-
- -
+ + +
); }