Popovers and sidebar

This commit is contained in:
Marc Brooks 2025-10-07 18:45:10 -05:00
parent 474cb70e80
commit 985b53c02b
No known key found for this signature in database
GPG Key ID: 583A6AF2D6AE1DC6
8 changed files with 170 additions and 92 deletions

View File

@ -6,6 +6,10 @@
"jetkvm_logo": "JetKVM Logo",
"load": "Load",
"unknown_error": "Unknown error",
"close": "Close",
"cancel": "Cancel",
"action_bar_virtual_media": "Virtual Media",
"action_bar_paste_text": "Paste text",
"action_bar_web_terminal": "Web Terminal",
@ -16,6 +20,7 @@
"action_bar_settings": "Settings",
"action_bar_fullscreen": "Fullscreen",
"action_bar_exit_fullscreen": "Exit Fullscreen",
"extensions_popover_extensions": "Extensions",
"extension_popover_set_error_notification": "Failed to set active extension: {error}",
"extension_popover_unload_extension": "Unload Extension",
@ -26,6 +31,7 @@
"extensions_dc_power_control_description": "Control your DC Power extension",
"extension_serial_console": "Serial Console",
"extension_serial_console_description": "Access your serial console extension",
"atx_power_control_get_state_error": "Failed to get ATX power state: {error}",
"atx_power_control_send_action_error": "Failed to send ATX power action {action}: {error}",
"atx_power_control_power_button": "Power",
@ -34,6 +40,7 @@
"atx_power_control_reset_button": "Reset",
"atx_power_control_power_led": "Power LED",
"atx_power_control_hdd_led": "HDD LED",
"dc_power_control_get_state_error": "Failed to get DC power state: {error}",
"dc_power_control_set_power_state_error": "Failed to send DC power state to {enabled}: {error}",
"dc_power_control_set_restore_state_error": "Failed to send DC power restore state to {state}: {error}",
@ -49,6 +56,7 @@
"dc_power_control_current_unit": "A",
"dc_power_control_power": "Power",
"dc_power_control_power_unit": "W",
"serial_console_get_settings_error": "Failed to get serial console settings: {error}",
"serial_console_set_settings_error": "Failed to set serial console settings to {settings}: {error}",
"serial_console_configure_description": "Configure your serial console settings",
@ -61,5 +69,76 @@
"serial_console_parity_odd": "Odd Parity",
"serial_console_parity_none": "No Parity",
"serial_console_parity_mark": "Mark Parity",
"serial_console_parity_space": "Space Parity"
"serial_console_parity_space": "Space Parity",
"wake_on_lan_add_device_device_name": "Device Name",
"wake_on_lan_add_device_example_device_name": "Plex Media Server",
"wake_on_lan_add_device_mac_address": "MAC Address",
"wake_on_lan_add_device_back": "Back",
"wake_on_lan_add_device_save_device": "Save Device",
"paste_modal_paste_text": "Paste text",
"paste_modal_paste_text_description": "Paste text from your client to the remote host",
"paste_modal_paste_from_host": "Paste from host",
"paste_modal_invalid_chars_intro": "The following characters won't be pasted:",
"paste_modal_delay_between_keys": "Delay between keys",
"paste_modal_delay_out_of_range": "Delay must be between {min} and {max}",
"paste_modal_sending_using_layout": "Sending text using keyboard layout: {iso}-{name}",
"paste_modal_confirm_paste": "Confirm Paste",
"mount_virtual_media": "Virtual Media",
"mount_virtual_media_description": "Mount an image to boot from or install an operating system.",
"mount_no_mounted_media": "No mounted media",
"mount_add_file_to_get_started": "Add a file to get started",
"mount_streaming_from_url": "Streaming from URL",
"mount_mounted_from_storage": "Mounted from JetKVM Storage",
"mount_unmount": "Unmount",
"mount_add_new_media": "Add New Media",
"mount_get_state_error": "Failed to get virtual media state: {error}",
"mount_unmount_error": "Failed to unmount image: {error}",
"mount_mounted_as": "Mounted as",
"mount_mode_disk": "Disk",
"mount_mode_cdrom": "CD-ROM",
"wake_on_lan": "Wake On LAN",
"wake_on_lan_description": "Send a Magic Packet to wake up a remote device.",
"wake_on_lan_invalid_mac": "Invalid MAC address",
"wake_on_lan_failed_send_magic": "Failed to send Magic Packet",
"wake_on_lan_magic_sent_success": "Magic Packet sent successfully",
"wake_on_lan_failed_add_device": "Failed to add device",
"wake_on_lan_empty_no_devices_added": "No devices added",
"wake_on_lan_empty_add_device_to_start": "Add a device to start using Wake-on-LAN",
"wake_on_lan_empty_add_new_device": "Add New Device",
"wake_on_lan_device_list_wake": "Wake",
"wake_on_lan_device_list_delete_device": "Delete device",
"wake_on_lan_device_list_add_new_device": "Add New Device",
"connection_stats_sidebar": "Connection Stats",
"connection_stats_connection": "Connection",
"connection_stats_connection_description": "The connection between the client and the JetKVM.",
"connection_stats_round_trip_time": "Round-Trip Time",
"connection_stats_round_trip_time_description": "Round-trip time for the active ICE candidate pair between peers.",
"connection_stats_video": "Video",
"connection_stats_video_description": "The video stream from the JetKVM to the client.",
"connection_stats_network_stability": "Network Stability",
"connection_stats_network_stability_description": "How steady the flow of inbound video packets is across the network.",
"connection_stats_badge_jitter": "Jitter",
"connection_stats_playback_delay": "Playback Delay",
"connection_stats_playback_delay_description": "Delay added by the jitter buffer to smooth playback when frames arrive unevenly.",
"connection_stats_badge_jitter_buffer_avg_delay": "Jitter Buffer Avg. Delay",
"connection_stats_packets_lost": "Packets Lost",
"connection_stats_packets_lost_description": "Count of lost inbound video RTP packets.",
"connection_stats_frames_per_second": "Frames per second",
"connection_stats_frames_per_second_description": "Number of inbound video frames displayed per second."
}

View File

@ -1,20 +1,17 @@
import { PlusCircleIcon } from "@heroicons/react/20/solid";
import { forwardRef, useEffect, useCallback } from "react";
import {
LuLink,
LuPlus,
LuRadioReceiver,
} from "react-icons/lu";
import { LuLink, LuPlus, LuRadioReceiver } from "react-icons/lu";
import { useClose } from "@headlessui/react";
import { useLocation } from "react-router";
import { m } from "@localizations/messages.js";
import { Button } from "@components/Button";
import Card, { GridCard } from "@components/Card";
import { formatters } from "@/utils";
import { RemoteVirtualMediaState, useMountMediaStore } from "@/hooks/stores";
import { RemoteVirtualMediaState, useMountMediaStore } from "@hooks/stores";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useDeviceUiNavigation } from "@/hooks/useAppNavigation";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { useDeviceUiNavigation } from "@hooks/useAppNavigation";
import notifications from "@/notifications";
const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
@ -25,9 +22,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
const syncRemoteVirtualMediaState = useCallback(() => {
send("getVirtualMediaState", {}, (response: JsonRpcResponse) => {
if ("error" in response) {
notifications.error(
`Failed to get virtual media state: ${response.error.message}`,
);
notifications.error(m.mount_get_state_error({ error: response.error.message }));
} else {
setRemoteVirtualMediaState(response.result as unknown as RemoteVirtualMediaState);
}
@ -37,7 +32,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
const handleUnmount = () => {
send("unmountImage", {}, (response: JsonRpcResponse) => {
if ("error" in response) {
notifications.error(`Failed to unmount image: ${response.error.message}`);
notifications.error(m.mount_unmount_error({ error: response.error.message }));
} else {
syncRemoteVirtualMediaState();
}
@ -57,10 +52,10 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
</div>
<div className="space-y-1">
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
No mounted media
{m.mount_no_mounted_media()}
</h3>
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
Add a file to get started
{m.mount_add_file_to_get_started()}
</p>
</div>
</div>
@ -81,7 +76,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
</Card>
</div>
<h3 className="text-base font-semibold text-black dark:text-white">
Streaming from URL
{m.mount_streaming_from_url()}
</h3>
<p className="truncate text-sm text-slate-900 dark:text-slate-100">
{formatters.truncateMiddle(url, 55)}
@ -105,7 +100,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
</Card>
</div>
<h3 className="text-base font-semibold text-black dark:text-white">
Mounted from JetKVM Storage
{m.mount_mounted_from_storage()}
</h3>
<p className="text-sm text-slate-900 dark:text-slate-100">
{formatters.truncateMiddle(path, 50)}
@ -138,8 +133,8 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<div className="h-full space-y-4">
<div className="space-y-4">
<SettingsPageHeader
title="Virtual Media"
description="Mount an image to boot from or install an operating system."
title={m.mount_virtual_media()}
description={m.mount_virtual_media_description()}
/>
<div
@ -162,10 +157,10 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
</div>
{remoteVirtualMediaState ? (
<div className="flex select-none items-center justify-between text-xs">
<div className="select-none text-white dark:text-slate-300">
<span>Mounted as</span>{" "}
<div className="select-none text-white dark:text-slate-300">
<span>{m.mount_mounted_as()}</span>{" "}
<span className="font-semibold">
{remoteVirtualMediaState.mode === "Disk" ? "Disk" : "CD-ROM"}
{remoteVirtualMediaState.mode === "Disk" ? m.mount_mode_disk() : m.mount_mode_cdrom()}
</span>
</div>
@ -173,7 +168,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<Button
size="SM"
theme="blank"
text="Close"
text={m.close()}
onClick={() => {
close();
}}
@ -181,7 +176,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<Button
size="SM"
theme="light"
text="Unmount"
text={m.mount_unmount()}
LeadingIcon={({ className }) => (
<svg
className={`${className} h-2.5 w-2.5 shrink-0`}
@ -227,7 +222,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<Button
size="SM"
theme="blank"
text="Close"
text={m.close()}
onClick={() => {
close();
}}
@ -235,7 +230,7 @@ const MountPopopover = forwardRef<HTMLDivElement, object>((_props, ref) => {
<Button
size="SM"
theme="primary"
text="Add New Media"
text={m.mount_add_new_media()}
onClick={() => {
setModalView("mode");
navigateTo("/mount");

View File

@ -1,13 +1,14 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useClose } from "@headlessui/react";
import { ExclamationCircleIcon } from "@heroicons/react/16/solid";
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { LuCornerDownLeft } from "react-icons/lu";
import { cx } from "@/cva.config";
import { useHidStore, useSettingsStore, useUiStore } from "@/hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import useKeyboard, { type MacroStep } from "@/hooks/useKeyboard";
import useKeyboardLayout from "@/hooks/useKeyboardLayout";
import { m } from "@localizations/messages.js";
import { useHidStore, useSettingsStore, useUiStore } from "@hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import useKeyboard, { type MacroStep } from "@hooks/useKeyboard";
import useKeyboardLayout from "@hooks/useKeyboardLayout";
import notifications from "@/notifications";
import { Button } from "@components/Button";
import { GridCard } from "@components/Card";
@ -122,8 +123,8 @@ export default function PasteModal() {
<div className="h-full space-y-4">
<div className="space-y-4">
<SettingsPageHeader
title="Paste text"
description="Paste text from your client to the remote host"
title={m.paste_modal_paste_text()}
description={m.paste_modal_paste_text_description()}
/>
<div
@ -143,7 +144,7 @@ export default function PasteModal() {
>
<TextAreaWithLabel
ref={TextAreaRef}
label="Paste from host"
label={m.paste_modal_paste_from_host()}
rows={4}
onKeyUp={e => e.stopPropagation()}
maxLength={pasteMaxLength}
@ -176,7 +177,7 @@ export default function PasteModal() {
<div className="mt-2 flex items-center gap-x-2">
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
<span className="text-xs text-red-500 dark:text-red-400">
The following characters won&apos;t be pasted:{" "}
{m.paste_modal_invalid_chars_intro()}{" "}
{invalidChars.join(", ")}
</span>
</div>
@ -186,8 +187,8 @@ export default function PasteModal() {
<div className={cx("text-xs text-slate-600 dark:text-slate-400", delayClassName)}>
<InputFieldWithLabel
type="number"
label="Delay between keys"
placeholder="Delay between keys"
label={m.paste_modal_delay_between_keys()}
placeholder={m.paste_modal_delay_between_keys()}
min={50}
max={65534}
value={delayValue}
@ -199,15 +200,14 @@ export default function PasteModal() {
<div className="mt-2 flex items-center gap-x-2">
<ExclamationCircleIcon className="h-4 w-4 text-red-500 dark:text-red-400" />
<span className="text-xs text-red-500 dark:text-red-400">
Delay must be between 50 and 65534
{m.paste_modal_delay_out_of_range({ min: 50, max: 65534 })}
</span>
</div>
)}
</div>
<div className="space-y-4">
<p className="text-xs text-slate-600 dark:text-slate-400">
Sending text using keyboard layout: {selectedKeyboard.isoCode}-
{selectedKeyboard.name}
{m.paste_modal_sending_using_layout({ iso: selectedKeyboard.isoCode, name: selectedKeyboard.name })}
</p>
</div>
</div>
@ -224,7 +224,7 @@ export default function PasteModal() {
<Button
size="SM"
theme="blank"
text="Cancel"
text={m.cancel()}
onClick={() => {
onCancelPasteMode();
close();
@ -233,7 +233,7 @@ export default function PasteModal() {
<Button
size="SM"
theme="primary"
text="Confirm Paste"
text={m.paste_modal_confirm_paste()}
disabled={isPasteInProgress}
onClick={onConfirmPaste}
LeadingIcon={LuCornerDownLeft}

View File

@ -1,8 +1,9 @@
import { useState, useRef } from "react";
import { LuPlus, LuArrowLeft } from "react-icons/lu";
import { InputFieldWithLabel } from "@/components/InputField";
import { Button } from "@/components/Button";
import { m } from "@localizations/messages.js";
import { InputFieldWithLabel } from "@components/InputField";
import { Button } from "@components/Button";
interface AddDeviceFormProps {
onAddDevice: (name: string, macAddress: string) => void;
@ -34,8 +35,8 @@ export default function AddDeviceForm({
>
<InputFieldWithLabel
ref={nameInputRef}
placeholder="Plex Media Server"
label="Device Name"
placeholder={m.wake_on_lan_add_device_example_device_name()}
label={m.wake_on_lan_add_device_device_name()}
required
onChange={e => {
setIsDeviceNameValid(e.target.validity.valid);
@ -46,7 +47,7 @@ export default function AddDeviceForm({
<InputFieldWithLabel
ref={macInputRef}
placeholder="00:b0:d0:63:c2:26"
label="MAC Address"
label={m.wake_on_lan_add_device_mac_address()}
onKeyUp={e => e.stopPropagation()}
required
pattern="^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$"
@ -82,14 +83,14 @@ export default function AddDeviceForm({
<Button
size="SM"
theme="light"
text="Back"
text={m.wake_on_lan_add_device_back()}
LeadingIcon={LuArrowLeft}
onClick={() => setShowAddForm(false)}
/>
<Button
size="SM"
theme="primary"
text="Save Device"
text={m.wake_on_lan_add_device_save_device()}
disabled={!isDeviceNameValid || !isMacAddressValid}
onClick={() => {
const deviceName = nameInputRef.current?.value || "";

View File

@ -1,8 +1,9 @@
import { LuPlus, LuSend, LuTrash2 } from "react-icons/lu";
import { Button } from "@/components/Button";
import Card from "@/components/Card";
import { FieldError } from "@/components/InputField";
import { m } from "@localizations/messages.js";
import { Button } from "@components/Button";
import Card from "@components/Card";
import { FieldError } from "@components/InputField";
export interface StoredDevice {
name: string;
@ -46,7 +47,7 @@ export default function DeviceList({
<Button
size="XS"
theme="light"
text="Wake"
text={m.wake_on_lan_device_list_wake()}
LeadingIcon={LuSend}
onClick={() => onSendMagicPacket(device.macAddress)}
/>
@ -55,7 +56,7 @@ export default function DeviceList({
theme="danger"
LeadingIcon={LuTrash2}
onClick={() => onDeleteDevice(index)}
aria-label="Delete device"
aria-label={m.wake_on_lan_device_list_delete_device()}
/>
</div>
</div>
@ -69,11 +70,11 @@ export default function DeviceList({
animationDelay: "0.2s",
}}
>
<Button size="SM" theme="blank" text="Close" onClick={onCancelWakeOnLanModal} />
<Button size="SM" theme="blank" text={m.close()} onClick={onCancelWakeOnLanModal} />
<Button
size="SM"
theme="primary"
text="Add New Device"
text={m.wake_on_lan_device_list_add_new_device()}
onClick={() => setShowAddForm(true)}
LeadingIcon={LuPlus}
/>

View File

@ -1,8 +1,9 @@
import { PlusCircleIcon } from "@heroicons/react/16/solid";
import { LuPlus } from "react-icons/lu";
import Card from "@/components/Card";
import { Button } from "@/components/Button";
import { m } from "@localizations/messages.js";
import Card from "@components/Card";
import { Button } from "@components/Button";
export default function EmptyStateCard({
onCancelWakeOnLanModal,
@ -25,10 +26,10 @@ export default function EmptyStateCard({
</Card>
</div>
<h3 className="text-sm font-semibold leading-none text-black dark:text-white">
No devices added
{m.wake_on_lan_empty_no_devices_added()}
</h3>
<p className="text-xs leading-none text-slate-700 dark:text-slate-300">
Add a device to start using Wake-on-LAN
{m.wake_on_lan_empty_add_device_to_start()}
</p>
</div>
</div>
@ -41,11 +42,11 @@ export default function EmptyStateCard({
animationDelay: "0.2s",
}}
>
<Button size="SM" theme="blank" text="Close" onClick={onCancelWakeOnLanModal} />
<Button size="SM" theme="blank" text={m.close()} onClick={onCancelWakeOnLanModal} />
<Button
size="SM"
theme="primary"
text="Add New Device"
text={m.wake_on_lan_empty_add_new_device()}
onClick={() => setShowAddForm(true)}
LeadingIcon={LuPlus}
/>

View File

@ -1,10 +1,11 @@
import { useCallback, useEffect, useState } from "react";
import { useClose } from "@headlessui/react";
import { m } from "@localizations/messages.js";
import { GridCard } from "@components/Card";
import { SettingsPageHeader } from "@components/SettingsPageheader";
import { JsonRpcResponse, useJsonRpc } from "@/hooks/useJsonRpc";
import { useRTCStore, useUiStore } from "@/hooks/stores";
import { JsonRpcResponse, useJsonRpc } from "@hooks/useJsonRpc";
import { useRTCStore, useUiStore } from "@hooks/stores";
import notifications from "@/notifications";
import EmptyStateCard from "./EmptyStateCard";
@ -35,12 +36,12 @@ export default function WakeOnLanModal() {
if ("error" in resp) {
const isInvalid = resp.error.data?.includes("invalid MAC address");
if (isInvalid) {
setErrorMessage("Invalid MAC address");
setErrorMessage(m.wake_on_lan_invalid_mac());
} else {
setErrorMessage("Failed to send Magic Packet");
setErrorMessage(m.wake_on_lan_failed_send_magic());
}
} else {
notifications.success("Magic Packet sent successfully");
notifications.success(m.wake_on_lan_magic_sent_success());
setDisableVideoFocusTrap(false);
close();
}
@ -87,7 +88,7 @@ export default function WakeOnLanModal() {
send("setWakeOnLanDevices", { params: { devices: updatedDevices } }, (resp: JsonRpcResponse) => {
if ("error" in resp) {
console.error("Failed to add Wake-on-LAN device:", resp.error);
setAddDeviceErrorMessage("Failed to add device");
setAddDeviceErrorMessage(m.wake_on_lan_failed_add_device());
} else {
setShowAddForm(false);
syncStoredDevices();
@ -103,8 +104,8 @@ export default function WakeOnLanModal() {
<div className="grid h-full grid-rows-(--grid-headerBody)">
<div className="space-y-4">
<SettingsPageHeader
title="Wake On LAN"
description="Send a Magic Packet to wake up a remote device."
title={m.wake_on_lan()}
description={m.wake_on_lan_description()}
/>
{showAddForm ? (

View File

@ -1,12 +1,12 @@
import { useInterval } from "usehooks-ts";
import SidebarHeader from "@/components/SidebarHeader";
import { useRTCStore, useUiStore } from "@/hooks/stores";
import { m } from "@localizations/messages.js";
import { createChartArray, Metric } from "@components/Metric";
import { SettingsSectionHeader } from "@components/SettingsSectionHeader";
import SidebarHeader from "@components/SidebarHeader";
import { useRTCStore, useUiStore } from "@hooks/stores";
import { someIterable } from "@/utils";
import { createChartArray, Metric } from "../Metric";
import { SettingsSectionHeader } from "../SettingsSectionHeader";
export default function ConnectionStatsSidebar() {
const { sidebarView, setSidebarView } = useUiStore();
const {
@ -95,7 +95,7 @@ export default function ConnectionStatsSidebar() {
return (
<div className="grid h-full grid-rows-(--grid-headerBody) shadow-xs">
<SidebarHeader title="Connection Stats" setSidebarView={setSidebarView} />
<SidebarHeader title={m.connection_stats_sidebar()} 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">
{sidebarView === "connection-stats" && (
@ -103,12 +103,12 @@ export default function ConnectionStatsSidebar() {
{/* Connection Group */}
<div className="space-y-3">
<SettingsSectionHeader
title="Connection"
description="The connection between the client and the JetKVM."
title={m.connection_stats_connection()}
description={m.connection_stats_connection_description()}
/>
<Metric
title="Round-Trip Time"
description="Round-trip time for the active ICE candidate pair between peers."
title={m.connection_stats_round_trip_time()}
description={m.connection_stats_round_trip_time_description()}
stream={iceCandidatePairStats}
metric="currentRoundTripTime"
map={x => ({
@ -123,16 +123,16 @@ export default function ConnectionStatsSidebar() {
{/* Video Group */}
<div className="space-y-3">
<SettingsSectionHeader
title="Video"
description="The video stream from the JetKVM to the client."
title={m.connection_stats_video()}
description={m.connection_stats_video_description()}
/>
{/* RTP Jitter */}
<Metric
title="Network Stability"
badge="Jitter"
title={m.connection_stats_network_stability()}
badge={m.connection_stats_badge_jitter()}
badgeTheme="light"
description="How steady the flow of inbound video packets is across the network."
description={m.connection_stats_network_stability_description()}
stream={inboundVideoRtpStats}
metric="jitter"
map={x => ({
@ -145,9 +145,9 @@ export default function ConnectionStatsSidebar() {
{/* Playback Delay */}
<Metric
title="Playback Delay"
description="Delay added by the jitter buffer to smooth playback when frames arrive unevenly."
badge="Jitter Buffer Avg. Delay"
title={m.connection_stats_playback_delay()}
description={m.connection_stats_playback_delay_description()}
badge={m.connection_stats_badge_jitter_buffer_avg_delay()}
badgeTheme="light"
data={jitterBufferAvgDelayData}
gate={inboundVideoRtpStats}
@ -167,8 +167,8 @@ export default function ConnectionStatsSidebar() {
{/* Packets Lost */}
<Metric
title="Packets Lost"
description="Count of lost inbound video RTP packets."
title={m.connection_stats_packets_lost()}
description={m.connection_stats_packets_lost_description()}
stream={inboundVideoRtpStats}
metric="packetsLost"
domain={[0, 100]}
@ -177,8 +177,8 @@ export default function ConnectionStatsSidebar() {
{/* Frames Per Second */}
<Metric
title="Frames per second"
description="Number of inbound video frames displayed per second."
title={m.connection_stats_frames_per_second()}
description={m.connection_stats_frames_per_second_description()}
stream={inboundVideoRtpStats}
metric="framesPerSecond"
domain={[0, 80]}