Files
kvm/ui/src/components/sidebar/connectionStats.tsx
Adam Shiervani 4052b3d225 Move settings to modals & better modal handling (#194)
* feat(ui): Add other session handling route and modal

* feat(ui): Add dedicated update route and refactor update dialog state management

* feat(ui): Add local authentication route

* refactor(ui): Remove LocalAuthPasswordDialog component and clean up related code

* refactor(ui): Remove OtherSessionConnectedModal component

* feat(ui): Add dedicated mount route and refactor mount media dialog

* refactor(ui): Simplify Escape key navigation in device route

* refactor(ui): Add TODO comments for future URL-based state migration

* refactor(ui): Migrate settings and update routes to dedicated routes

This commit introduces a comprehensive refactoring of the UI routing and state management:
- Removed sidebar-based settings view
- Replaced global modal state with URL-based routing
- Added dedicated routes for settings, including general, security, and update sections
- Simplified modal and sidebar interactions
- Improved animation and transition handling using motion library
- Removed deprecated components and simplified route structure

* fix(ui): Add TODO comment for modal session interaction

* refactor(ui): Move USB configuration to new settings setup

This commit introduces several improvements to the USB configuration workflow:
- Refactored USB configuration dialog component
- Simplified USB config state management
- Moved USB configuration to hardware settings route
- Updated JSON-RPC type definitions
- Cleaned up unused imports and components
- Improved error handling and notifications

* refactor(ui): Replace react-router-dom navigation with custom navigation hook

This commit introduces a new custom navigation hook `useDeviceUiNavigation` to replace direct usage of `useNavigate` across multiple components:
- Removed direct `useNavigate` imports in various components
- Added `navigateTo` method from new navigation hook
- Updated navigation calls in ActionBar, MountPopover, UpdateInProgressStatusCard, and other routes
- Simplified navigation logic and prepared for potential future navigation enhancements
- Removed console logs and unnecessary comments

* refactor(ui): Remove unused react-router-dom import

Clean up unnecessary import of `useNavigate` from react-router-dom in device settings route

* feat(ui): Improve mobile navigation and scrolling in device settings

* refactor(ui): Reorganize device access and security settings

This commit introduces several changes to the device access and security settings:
- Renamed "Security" section to "Access" in settings navigation
- Moved local authentication routes from security to access
- Removed deprecated security settings route
- Added new route for device access settings with cloud and local authentication management
- Updated cloud URL and adoption logic to be part of the access settings
- Simplified routing and component structure for better user experience

* fix(ui): Update logout button hover state color

* fix(ui): Adjust device de-registration button size to small

* fix(ui): Update appearance settings section header and description

* refactor(ui): Replace SectionHeader with new SettingsPageHeader and SettingsSectionHeader components

This commit introduces two new header components for settings pages:
- Created SettingsPageHeader for main page headers
- Created SettingsSectionHeader for subsection headers
- Replaced all existing SectionHeader imports with new components
- Updated styling and type definitions to support more flexible header rendering

* feat(ui): Add dev channel toggle to advanced settings

Move dev channel update option from general settings to advanced settings
- Introduced new state and handler for dev channel toggle
- Removed dev channel option from general settings route
- Added dev channel toggle in advanced settings with error handling
2025-02-27 16:48:50 +01:00

246 lines
10 KiB
TypeScript

import SidebarHeader from "@components/SidebarHeader";
import { GridCard } from "@components/Card";
import { useRTCStore, useUiStore } from "@/hooks/stores";
import StatChart from "@components/StatChart";
import { useInterval } from "usehooks-ts";
function createChartArray<T, K extends keyof T>(
stream: Map<number, T>,
metric: K,
): { date: number; stat: T[K] | null }[] {
const stat = Array.from(stream).map(([key, stats]) => {
return { date: key, stat: stats[metric] };
});
// Sort the dates to ensure they are in chronological order
const sortedStat = stat.map(x => x.date).sort((a, b) => a - b);
// Determine the earliest statistic date
const earliestStat = sortedStat[0];
// Current time in seconds since the Unix epoch
const now = Math.floor(Date.now() / 1000);
// Determine the starting point for the chart data
const firstChartDate = earliestStat ? Math.min(earliestStat, now - 120) : now - 120;
// Generate the chart array for the range between 'firstChartDate' and 'now'
return Array.from({ length: now - firstChartDate }, (_, i) => {
const currentDate = firstChartDate + i;
return {
date: currentDate,
// Find the statistic for 'currentDate', or use the last known statistic if none exists for that date
stat: stat.find(x => x.date === currentDate)?.stat ?? null,
};
});
}
export default function ConnectionStatsSidebar() {
const inboundRtpStats = useRTCStore(state => state.inboundRtpStats);
const candidatePairStats = useRTCStore(state => state.candidatePairStats);
const setSidebarView = useUiStore(state => state.setSidebarView);
function isMetricSupported<T, K extends keyof T>(
stream: Map<number, T>,
metric: K,
): boolean {
return Array.from(stream).some(([, stat]) => stat[metric] !== undefined);
}
const appendInboundRtpStats = useRTCStore(state => state.appendInboundRtpStats);
const appendIceCandidatePair = useRTCStore(state => state.appendCandidatePairStats);
const appendDiskDataChannelStats = useRTCStore(
state => state.appendDiskDataChannelStats,
);
const appendLocalCandidateStats = useRTCStore(state => state.appendLocalCandidateStats);
const appendRemoteCandidateStats = useRTCStore(
state => state.appendRemoteCandidateStats,
);
const peerConnection = useRTCStore(state => state.peerConnection);
const mediaStream = useRTCStore(state => state.mediaStream);
const sidebarView = useUiStore(state => state.sidebarView);
useInterval(function collectWebRTCStats() {
(async () => {
if (!mediaStream) return;
const videoTrack = mediaStream.getVideoTracks()[0];
if (!videoTrack) return;
const stats = await peerConnection?.getStats();
let successfulLocalCandidateId: string | null = null;
let successfulRemoteCandidateId: string | null = null;
stats?.forEach(report => {
if (report.type === "inbound-rtp") {
appendInboundRtpStats(report);
} else if (report.type === "candidate-pair" && report.nominated) {
if (report.state === "succeeded") {
successfulLocalCandidateId = report.localCandidateId;
successfulRemoteCandidateId = report.remoteCandidateId;
}
appendIceCandidatePair(report);
} else if (report.type === "local-candidate") {
// We only want to append the local candidate stats that were used in nominated candidate pair
if (successfulLocalCandidateId === report.id) {
appendLocalCandidateStats(report);
}
} else if (report.type === "remote-candidate") {
if (successfulRemoteCandidateId === report.id) {
appendRemoteCandidateStats(report);
}
} else if (report.type === "data-channel" && report.label === "disk") {
appendDiskDataChannelStats(report);
}
});
})();
}, 500);
return (
<div className="grid h-full grid-rows-headerBody shadow-sm">
<SidebarHeader title="Connection Stats" setSidebarView={setSidebarView} />
<div className="h-full space-y-4 overflow-y-scroll bg-white px-4 py-2 pb-8 dark:bg-slate-900">
<div className="space-y-4">
{/*
The entire sidebar component is always rendered, with a display none when not visible
The charts below, need a height and width, otherwise they throw. So simply don't render them unless the thing is visible
*/}
{sidebarView === "connection-stats" && (
<div className="space-y-4">
<div className="space-y-2">
<div>
<h2 className="text-lg font-semibold text-black dark:text-white">
Packets Lost
</h2>
<p className="text-sm text-slate-700 dark:text-slate-300">
Number of data packets lost during transmission.
</p>
</div>
<GridCard>
<div className="flex h-[127px] w-full items-center justify-center text-sm text-slate-500">
{inboundRtpStats.size === 0 ? (
<div className="flex flex-col items-center space-y-1 ">
<p className="text-slate-700">Waiting for data...</p>
</div>
) : isMetricSupported(inboundRtpStats, "packetsLost") ? (
<StatChart
data={createChartArray(inboundRtpStats, "packetsLost")}
domain={[0, 100]}
unit=" packets"
/>
) : (
<div className="flex flex-col items-center space-y-1 ">
<p className="text-black">Metric not supported</p>
</div>
)}
</div>
</GridCard>
</div>
<div className="space-y-2">
<div>
<h2 className="text-lg font-semibold text-black dark:text-white">
Round-Trip Time
</h2>
<p className="text-sm text-slate-700 dark:text-slate-300">
Time taken for data to travel from source to destination and back
</p>
</div>
<GridCard>
<div className="flex h-[127px] w-full items-center justify-center text-sm text-slate-500">
{inboundRtpStats.size === 0 ? (
<div className="flex flex-col items-center space-y-1 ">
<p className="text-slate-700">Waiting for data...</p>
</div>
) : isMetricSupported(candidatePairStats, "currentRoundTripTime") ? (
<StatChart
data={createChartArray(
candidatePairStats,
"currentRoundTripTime",
).map(x => {
return {
date: x.date,
stat: x.stat ? Math.round(x.stat * 1000) : null,
};
})}
domain={[0, 600]}
unit=" ms"
/>
) : (
<div className="flex flex-col items-center space-y-1 ">
<p className="text-black">Metric not supported</p>
</div>
)}
</div>
</GridCard>
</div>
<div className="space-y-2">
<div>
<h2 className="text-lg font-semibold text-black dark:text-white">
Jitter
</h2>
<p className="text-sm text-slate-700 dark:text-slate-300">
Variation in packet delay, affecting video smoothness.{" "}
</p>
</div>
<GridCard>
<div className="flex h-[127px] w-full items-center justify-center text-sm text-slate-500">
{inboundRtpStats.size === 0 ? (
<div className="flex flex-col items-center space-y-1 ">
<p className="text-slate-700">Waiting for data...</p>
</div>
) : (
<StatChart
data={createChartArray(inboundRtpStats, "jitter").map(x => {
return {
date: x.date,
stat: x.stat ? Math.round(x.stat * 1000) : null,
};
})}
domain={[0, 300]}
unit=" ms"
/>
)}
</div>
</GridCard>
</div>
<div className="space-y-2">
<div>
<h2 className="text-lg font-semibold text-black dark:text-white">
Frames per second
</h2>
<p className="text-sm text-slate-700 dark:text-slate-300">
Number of video frames displayed per second.
</p>
</div>
<GridCard>
<div className="flex h-[127px] w-full items-center justify-center text-sm text-slate-500">
{inboundRtpStats.size === 0 ? (
<div className="flex flex-col items-center space-y-1 ">
<p className="text-slate-700">Waiting for data...</p>
</div>
) : (
<StatChart
data={createChartArray(inboundRtpStats, "framesPerSecond").map(
x => {
return {
date: x.date,
stat: x.stat ? x.stat : null,
};
},
)}
domain={[0, 80]}
unit=" fps"
/>
)}
</div>
</GridCard>
</div>
</div>
)}
</div>
</div>
</div>
);
}