Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion frontend/src/components/Dashboard/DashboardGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { useMosaicWebRTCConnection } from "@/hooks/useMosaicWebRTCConnection.ts"
import { useRobotInfo } from "@/hooks/useRobotInfo.ts";
import { RobotConnector, type TabConfig, type WidgetConfig } from "@/mosaic";
import { DASHBOARD_STORAGE_KEYS } from "@/utils";
import { getWidgetDescriptor } from "@/widgets/_utils/widgetRegistry.ts";

const ResponsiveGridLayout = WidthProvider(Responsive);

Expand Down Expand Up @@ -157,7 +158,10 @@ export default function DashboardGrid({ tabId }: DashboardGridProps) {
connectors: widget.connectors.map((connector) => {
return new RobotConnector(connector.robotId, connector.connectorId);
}),
params: widget.params,
params: {
...getWidgetDescriptor(widget.type)?.getDefaultParams(),
...widget.params,
},
onUpdateWidgetParams: (params?: any) => {
onUpdateWidgetParams(widget.id, params);
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { WidgetHeader } from "./WidgetHeader.tsx";
interface WidgetFrameProps {
children?: ReactNode;
widgetConfig: WidgetConfig;
error?: string;
error?: string | null;
}

export function WidgetRoot({ widgetConfig, children, error }: WidgetFrameProps) {
Expand Down
8 changes: 6 additions & 2 deletions frontend/src/components/Dashboard/Widgets/WidgetDescriptor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { StoreType } from "@/mosaic/store/interface/mosaic-store.ts";

export abstract class WidgetDescriptor {
export abstract class WidgetDescriptor<T = Record<string, any>> {
public abstract getName(): string;

public getRequiredStoreType(): StoreType | null {
Expand All @@ -27,7 +27,11 @@ export abstract class WidgetDescriptor {
return false;
}

public validateParams(_params: Record<string, any>): string | null {
public getDefaultParams(): T {
return {} as T;
}

public validateParams(_params: T): string | null {
return null;
}

Expand Down
35 changes: 25 additions & 10 deletions frontend/src/components/WidgetCustomize/StoreDataPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,16 @@ interface ReceivableDataPanelProps {
defaultData: string;
}

function tryFormatJson(value: string): string {
try {
return JSON.stringify(JSON.parse(value), null, 2);
} catch {
return value;
}
}

function ReceivableDataPanel({ onInject, defaultData }: ReceivableDataPanelProps) {
const [input, setInput] = useState(defaultData);
const [input, setInput] = useState(() => tryFormatJson(defaultData));

return (
<VStack gap={2} align="stretch">
Expand All @@ -18,9 +26,10 @@ function ReceivableDataPanel({ onInject, defaultData }: ReceivableDataPanelProps
size="sm"
value={input}
onChange={(e) => setInput(e.target.value)}
onBlur={(e) => setInput(tryFormatJson(e.target.value))}
placeholder="Enter raw data string..."
fontFamily="mono"
rows={4}
rows={6}
/>
<HStack justify="flex-end">
<Button size="sm" colorPalette="green" onClick={() => onInject(input)}>
Expand All @@ -32,7 +41,7 @@ function ReceivableDataPanel({ onInject, defaultData }: ReceivableDataPanelProps
}

interface SendableDataPanelProps {
sentDataLog: string[];
sentDataLog: { time: string; data: string }[];
onClearLog: () => void;
}

Expand All @@ -52,20 +61,26 @@ function SendableDataPanel({ sentDataLog, onClearLog }: SendableDataPanelProps)
borderColor="gray.200"
borderRadius="md"
p={2}
h="150px"
overflowY="auto"
h="200px"
overflow="auto"
resize="vertical"
bg="gray.50"
>
{sentDataLog.length === 0 ? (
<Text fontSize="xs" color="gray.400">
No data sent yet...
</Text>
) : (
<VStack gap={1} align="stretch">
<VStack gap={2} align="stretch">
{sentDataLog.map((entry, i) => (
<Code key={i} fontSize="xs" whiteSpace="pre-wrap" wordBreak="break-all">
{entry}
</Code>
<Box key={i}>
<Text fontSize="xs" color="gray.400" mb="1px">
{entry.time}
</Text>
<Code fontSize="xs" whiteSpace="pre-wrap" wordBreak="break-all" display="block">
{tryFormatJson(entry.data)}
</Code>
</Box>
))}
</VStack>
)}
Expand Down Expand Up @@ -111,7 +126,7 @@ interface StoreDataPanelProps {
defaultInjectData: string;
onInject: (rawData: string) => void;
onInjectMedia: (url: string) => void;
sentDataLog: string[];
sentDataLog: { time: string; data: string }[];
onClearLog: () => void;
}

Expand Down
43 changes: 25 additions & 18 deletions frontend/src/components/WidgetCustomize/WidgetCustomizePage.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Box, Heading, Separator, Text, VStack } from "@chakra-ui/react";
import { useContext, useEffect, useRef, useState } from "react";
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "react";

import type { RobotConfig, WidgetConfig } from "@/mosaic";
import type { MosaicStore } from "@/mosaic/store/interface/mosaic-store.ts";
Expand Down Expand Up @@ -95,7 +95,7 @@ export function WidgetCustomizePage() {

// Store interaction
const [storeType, setStoreType] = useState<"receivable" | "sendable" | "media" | null>(null);
const [sentDataLog, setSentDataLog] = useState<string[]>([]);
const [sentDataLog, setSentDataLog] = useState<{ time: string; data: string }[]>([]);

// Keep refs for cleanup
const currentStoreRef = useRef<MosaicStore | null>(null);
Expand Down Expand Up @@ -126,16 +126,16 @@ export function WidgetCustomizePage() {
const mockChannel = {
readyState: "open" as RTCDataChannelState,
send: (data: string) => {
setSentDataLog((prev) =>
[`[${new Date().toLocaleTimeString()}] ${data}`, ...prev].slice(0, 100),
);
const now = new Date();
const time = `${String(now.getHours()).padStart(2, "0")}:${String(now.getMinutes()).padStart(2, "0")}:${String(now.getSeconds()).padStart(2, "0")}`;
setSentDataLog((prev) => [{ time, data }, ...prev].slice(0, 100));
},
} as unknown as RTCDataChannel;
store.setDataChannel(mockChannel);
}

setStoreType(store.getStoreType());
setWidgetParams({});
setWidgetParams(getWidgetDescriptor(widgetType)?.getDefaultParams() ?? {});
setWidgetPosition({ x: 0, y: 0, w: 8, h: 6 });

updateRobotInfo(new RobotInfo(TEST_ROBOT_ID, "Test Robot", 6, fakeRobotConfig));
Expand All @@ -153,9 +153,10 @@ export function WidgetCustomizePage() {
const handleApply = () => {
if (!widgetType || !connectorId || !connectorType) return;

// Release this component's own store ref (not the widget's ref)
// Force-delete the store for this connector so Phase 2 always creates a fresh store
// of the newly selected type. The widget's own cleanup will release its ref gracefully.
if (currentConnectorRef.current) {
storeManager.releaseStore(currentConnectorRef.current);
storeManager.forceDeleteStore(currentConnectorRef.current);
currentStoreRef.current = null;
currentConnectorRef.current = null;
}
Expand Down Expand Up @@ -207,16 +208,22 @@ export function WidgetCustomizePage() {
}
};

const widgetConfig: WidgetConfig | null = appliedConfig
? {
id: "test-widget",
type: appliedConfig.widgetType,
position: widgetPosition,
connectors: [new RobotConnector(TEST_ROBOT_ID, appliedConfig.connectorId)],
params: widgetParams,
onUpdateWidgetParams: (params) => setWidgetParams(params ?? {}),
}
: null;
const handleUpdateWidgetParams = useCallback((params?: any) => setWidgetParams(params ?? {}), []);

const widgetConfig: WidgetConfig | null = useMemo(
() =>
appliedConfig
? {
id: "test-widget",
type: appliedConfig.widgetType,
position: widgetPosition,
connectors: [new RobotConnector(TEST_ROBOT_ID, appliedConfig.connectorId)],
params: widgetParams,
onUpdateWidgetParams: handleUpdateWidgetParams,
}
: null,
[appliedConfig, widgetPosition, widgetParams, handleUpdateWidgetParams],
);

const isApplyDisabled = !widgetType || !connectorId || !connectorType;

Expand Down
4 changes: 4 additions & 0 deletions frontend/src/mosaic/store/store-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,10 @@ export class StoreManager {
return store;
}

public forceDeleteStore(robotConnector: RobotConnector): void {
this.mosaicStores.delete(robotConnector);
}

public releaseStore(robotConnector: RobotConnector): boolean {
const store = this.mosaicStores.get(robotConnector);
if (store === undefined) return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { MdBatteryFull, MdPower } from "react-icons/md";
import type { JsonReceivableStore } from "@/stores/JsonReceivableStore/JsonReceivableStore.ts";
import type { WidgetProps } from "@/widgets/index.ts";

import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx";
import { useMosaicStore } from "@/hooks/useMosaicStore.ts";

interface PowerData {
Expand Down Expand Up @@ -113,80 +113,90 @@ export default function ClearpathPlatformPowerViewerWidget({ widgetConfig }: Wid
};
}, [widgetConfig]);

if (!data) {
return (
<WidgetRoot widgetConfig={widgetConfig}>
<Box display="flex" alignItems="center" justifyContent="center" h="100%">
<Text color="fg.muted" fontSize="sm">
Waiting for data...
</Text>
</Box>
</WidgetRoot>
);
}

return (
<WidgetRoot widgetConfig={widgetConfig}>
<VStack align="stretch" gap={3} p={3} h="100%" overflowY="auto">
{/* Connection Status */}
<VStack align="stretch" gap={2}>
<StatusRow
icon={<MdBatteryFull size={18} />}
label="Battery"
connected={data.battery_connected === 1}
/>
<StatusRow
icon={<MdPower size={18} />}
label="Charger"
connected={data.charger_connected === 1}
/>
</VStack>

<Separator />

{/* Voltages | Currents */}
<Grid templateColumns="repeat(2, 1fr)" gap={4} flex={1}>
<VStack align="stretch" gap={2}>
<Text
fontSize="xs"
fontWeight="bold"
color="fg.muted"
textTransform="uppercase"
letterSpacing="wider"
>
Voltages
<MosaicWidget.Root widgetConfig={widgetConfig}>
<MosaicWidget.Body>
{!data ? (
<Box display="flex" alignItems="center" justifyContent="center" h="100%">
<Text color="fg.muted" fontSize="sm">
Waiting for data...
</Text>
<StatCard label="Battery" rawValue={data.measured_voltages.battery_voltage} />
<StatCard label="Left Driver" rawValue={data.measured_voltages.left_driver_voltage} />
<StatCard label="Right Driver" rawValue={data.measured_voltages.right_driver_voltage} />
</VStack>

<VStack align="stretch" gap={2}>
<Text
fontSize="xs"
fontWeight="bold"
color="fg.muted"
textTransform="uppercase"
letterSpacing="wider"
>
Currents
</Box>
) : (
<VStack align="stretch" gap={3} p={3} h="100%" overflowY="auto">
{/* Connection Status */}
<VStack align="stretch" gap={2}>
<StatusRow
icon={<MdBatteryFull size={18} />}
label="Battery"
connected={data.battery_connected === 1}
/>
<StatusRow
icon={<MdPower size={18} />}
label="Charger"
connected={data.charger_connected === 1}
/>
</VStack>

<Separator />

{/* Voltages | Currents */}
<Grid templateColumns="repeat(2, 1fr)" gap={4} flex={1}>
<VStack align="stretch" gap={2}>
<Text
fontSize="xs"
fontWeight="bold"
color="fg.muted"
textTransform="uppercase"
letterSpacing="wider"
>
Voltages
</Text>
<StatCard label="Battery" rawValue={data.measured_voltages.battery_voltage} />
<StatCard
label="Left Driver"
rawValue={data.measured_voltages.left_driver_voltage}
/>
<StatCard
label="Right Driver"
rawValue={data.measured_voltages.right_driver_voltage}
/>
</VStack>

<VStack align="stretch" gap={2}>
<Text
fontSize="xs"
fontWeight="bold"
color="fg.muted"
textTransform="uppercase"
letterSpacing="wider"
>
Currents
</Text>
<StatCard
label="Left Driver"
rawValue={data.measured_currents.left_driver_current}
/>
<StatCard
label="MCU & Port"
rawValue={data.measured_currents.mcu_and_user_port_current}
/>
<StatCard
label="Right Driver"
rawValue={data.measured_currents.right_driver_current}
/>
</VStack>
</Grid>

<Separator />

{/* Timestamp */}
<Text fontSize="xs" color="fg.subtle" fontFamily="mono">
{formatTimestamp(data.timestamp)}
</Text>
<StatCard label="Left Driver" rawValue={data.measured_currents.left_driver_current} />
<StatCard
label="MCU & Port"
rawValue={data.measured_currents.mcu_and_user_port_current}
/>
<StatCard label="Right Driver" rawValue={data.measured_currents.right_driver_current} />
</VStack>
</Grid>

<Separator />

{/* Timestamp */}
<Text fontSize="xs" color="fg.subtle" fontFamily="mono">
{formatTimestamp(data.timestamp)}
</Text>
</VStack>
</WidgetRoot>
)}
</MosaicWidget.Body>
</MosaicWidget.Root>
);
}
Loading
Loading