From b317c6b219f68a532c878c623f81d867d4c7d1b3 Mon Sep 17 00:00:00 2001 From: YeonhyukKim Date: Wed, 18 Mar 2026 15:11:57 +0900 Subject: [PATCH 01/17] - refactor: refactor WidgetFrame to MosaicWidget compound components and add BaseTemplate --- .../components/Dashboard/DashboardGrid.tsx | 74 +++++++++----- .../components/Dashboard/WidgetFactory.tsx | 36 ------- .../src/components/Dashboard/WidgetFrame.tsx | 38 -------- .../src/components/Dashboard/WidgetHeader.tsx | 68 ------------- .../Dashboard/Widgets/ErrorWidget.tsx | 53 ++++++++++ .../Dashboard/{ => Widgets}/WidgetBody.tsx | 0 .../Dashboard/Widgets/WidgetComponents.tsx | 55 +++++++++++ .../Dashboard/Widgets/WidgetDescriptor.ts | 27 ++++++ .../Dashboard/Widgets/WidgetFactory.tsx | 66 +++++++++++++ .../Dashboard/{ => Widgets}/WidgetFooter.tsx | 0 .../Dashboard/Widgets/WidgetHeader.tsx | 57 +++++++++++ .../Dashboard/Widgets/WidgetSettingDialog.tsx | 97 +++++++++++++++++++ .../OccupancyMaps/OccupancyMapsTable.tsx | 2 +- frontend/src/mosaic/index.ts | 1 + .../ClearpathPlatformPowerViewerWidget.tsx | 10 +- .../ConnectionCheckWidget.tsx | 6 +- .../DelayCheckWidget/DelayCheckWidget.tsx | 6 +- .../ImuState3DViewerWidget.tsx | 6 +- .../JsonViewerWidget/JsonViewerWidget.tsx | 6 +- .../JsonViewerWidget/WidgetDescriptor.ts | 7 ++ .../LaserScanPolarViewerWidget.tsx | 6 +- .../MediaViewerWidget/MediaViewerWidget.tsx | 6 +- .../MediaViewerWithStatWidget.tsx | 6 +- .../widgets/NotFoundWidget/NotFoundWidget.tsx | 6 +- .../OpenStreetMapViewerWidget.tsx | 6 +- .../PointCloud2DViewerV2Widget.tsx | 6 +- .../PointCloud2DViewerWidget.tsx | 6 +- .../PointCloud3DViewerWidget.tsx | 6 +- .../ThumbstickSenderWidget.tsx | 6 +- .../WASDSenderWidget/WASDSenderWidget.tsx | 6 +- .../BaseTemplateSetting.tsx | 44 +++++++++ .../BaseTemplateWidget/BaseTemplateWidget.tsx | 50 ++++++++++ .../BaseTemplateWidget/WidgetDescriptor.ts | 27 ++++++ 33 files changed, 582 insertions(+), 214 deletions(-) delete mode 100644 frontend/src/components/Dashboard/WidgetFactory.tsx delete mode 100644 frontend/src/components/Dashboard/WidgetFrame.tsx delete mode 100644 frontend/src/components/Dashboard/WidgetHeader.tsx create mode 100644 frontend/src/components/Dashboard/Widgets/ErrorWidget.tsx rename frontend/src/components/Dashboard/{ => Widgets}/WidgetBody.tsx (100%) create mode 100644 frontend/src/components/Dashboard/Widgets/WidgetComponents.tsx create mode 100644 frontend/src/components/Dashboard/Widgets/WidgetDescriptor.ts create mode 100644 frontend/src/components/Dashboard/Widgets/WidgetFactory.tsx rename frontend/src/components/Dashboard/{ => Widgets}/WidgetFooter.tsx (100%) create mode 100644 frontend/src/components/Dashboard/Widgets/WidgetHeader.tsx create mode 100644 frontend/src/components/Dashboard/Widgets/WidgetSettingDialog.tsx create mode 100644 frontend/src/widgets/JsonViewerWidget/WidgetDescriptor.ts create mode 100644 frontend/src/widgets/_templates/BaseTemplateWidget/BaseTemplateSetting.tsx create mode 100644 frontend/src/widgets/_templates/BaseTemplateWidget/BaseTemplateWidget.tsx create mode 100644 frontend/src/widgets/_templates/BaseTemplateWidget/WidgetDescriptor.ts diff --git a/frontend/src/components/Dashboard/DashboardGrid.tsx b/frontend/src/components/Dashboard/DashboardGrid.tsx index 47c5176..317364d 100644 --- a/frontend/src/components/Dashboard/DashboardGrid.tsx +++ b/frontend/src/components/Dashboard/DashboardGrid.tsx @@ -16,7 +16,7 @@ import { type Layout, Responsive, WidthProvider } from "react-grid-layout"; import { getTabConfigApi, updateTabConfigApi } from "@/client/service/dashboard.api.ts"; import RobotConnectionPanel from "@/components/Dashboard/RobotConnectionPanel.tsx"; -import { WidgetFactory } from "@/components/Dashboard/WidgetFactory.tsx"; +import { WidgetFactory } from "@/components/Dashboard/Widgets/WidgetFactory.tsx"; import useAuth from "@/hooks/useAuth.ts"; import "react-grid-layout/css/styles.css"; import "react-resizable/css/styles.css"; @@ -83,29 +83,6 @@ export default function DashboardGrid({ tabId }: DashboardGridProps) { const editableWidgetsRef = useRef([]); const isLayoutDirtyRef = useRef(false); - const { data: tabConfig, isPending: isConfigLoading } = useQuery({ - queryKey: ["parsedDashboardTabConfig", tabId], - queryFn: async () => { - const tabConfigDto = await getTabConfigApi(tabId); - const widgets = JSON.parse(tabConfigDto.widgets).widgets as WidgetConfig[]; - return { - id: tabConfigDto.id, - name: tabConfigDto.name, - widgets: widgets.map((widget) => { - return { - id: widget.id, - type: widget.type, - position: widget.position, - connectors: widget.connectors.map((connector) => { - return new RobotConnector(connector.robotId, connector.connectorId); - }), - }; - }), - } as TabConfig; - }, - enabled: !!user, - }); - const saveLayoutMutation = useMutation({ mutationFn: async ({ targetTabId, @@ -142,6 +119,55 @@ export default function DashboardGrid({ tabId }: DashboardGridProps) { }, }); + const onUpdateWidgetParams = (widgetId: string, params?: any) => { + if (!tabConfig) { + return; + } + + const newWidgets = editableWidgetsRef.current.map((widget) => { + if (widget.id !== widgetId) { + return widget; + } + + return { + ...widget, + params: params, + }; + }); + + saveLayoutMutation.mutate({ + targetTabId: tabConfig.id, + widgets: newWidgets, + }); + }; + + const { data: tabConfig, isPending: isConfigLoading } = useQuery({ + queryKey: ["parsedDashboardTabConfig", tabId], + queryFn: async () => { + const tabConfigDto = await getTabConfigApi(tabId); + const widgets = JSON.parse(tabConfigDto.widgets).widgets as WidgetConfig[]; + return { + id: tabConfigDto.id, + name: tabConfigDto.name, + widgets: widgets.map((widget) => { + return { + id: widget.id, + type: widget.type, + position: widget.position, + connectors: widget.connectors.map((connector) => { + return new RobotConnector(connector.robotId, connector.connectorId); + }), + params: widget.params, + onUpdateWidgetParams: (params?: any) => { + onUpdateWidgetParams(widget.id, params); + }, + }; + }), + } as TabConfig; + }, + enabled: !!user, + }); + useEffect(() => { if (!tabConfig) { setEditableWidgets([]); diff --git a/frontend/src/components/Dashboard/WidgetFactory.tsx b/frontend/src/components/Dashboard/WidgetFactory.tsx deleted file mode 100644 index c824e83..0000000 --- a/frontend/src/components/Dashboard/WidgetFactory.tsx +++ /dev/null @@ -1,36 +0,0 @@ -import { type ComponentType, type LazyExoticComponent, Suspense, lazy } from "react"; - -import type { WidgetConfig } from "@/mosaic"; - -type WidgetComponent = ComponentType<{ widgetConfig: WidgetConfig }>; - -const modules = import.meta.glob("../../widgets/*/*.tsx"); -const registry = new Map>(); -const LazyNotFoundWidget = lazy( - () => import("../../widgets/NotFoundWidget/NotFoundWidget.tsx"), -) as LazyExoticComponent; - -function getLazyWidget(type: string) { - if (registry.has(type)) return registry.get(type)!; - - const entry = Object.entries(modules).find(([path]) => path.includes(`/${type}/${type}.tsx`)); - if (!entry) return null; - - const component = lazy(entry[1] as () => Promise<{ default: WidgetComponent }>); - registry.set(type, component); - return component; -} - -export interface WidgetFactoryProps { - widgetConfig: WidgetConfig; -} - -export function WidgetFactory({ widgetConfig }: WidgetFactoryProps) { - const LazyComponent = getLazyWidget(widgetConfig.type) ?? LazyNotFoundWidget; - - return ( - Loading...}> - - - ); -} diff --git a/frontend/src/components/Dashboard/WidgetFrame.tsx b/frontend/src/components/Dashboard/WidgetFrame.tsx deleted file mode 100644 index 47d918a..0000000 --- a/frontend/src/components/Dashboard/WidgetFrame.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import type { ReactNode } from "react"; - -import { VStack } from "@chakra-ui/react"; - -import type { WidgetConfig } from "@/mosaic"; - -import { WidgetBody } from "./WidgetBody"; -import { WidgetFooter } from "./WidgetFooter"; -import { WidgetHeader } from "./WidgetHeader"; - -export interface WidgetFrameProps { - widgetConfig: WidgetConfig; - children?: ReactNode; - useBody?: boolean; - showRobotInfo?: boolean; - footerInfo?: Array<{ - label: string; - value: string | ReactNode; - }>; - footerMessage?: string; -} - -export function WidgetFrame({ - widgetConfig, - children, - useBody = true, - showRobotInfo = true, - footerInfo = [], - footerMessage, -}: WidgetFrameProps) { - return ( - - - {useBody ? {children} : children} - - - ); -} diff --git a/frontend/src/components/Dashboard/WidgetHeader.tsx b/frontend/src/components/Dashboard/WidgetHeader.tsx deleted file mode 100644 index e3b627d..0000000 --- a/frontend/src/components/Dashboard/WidgetHeader.tsx +++ /dev/null @@ -1,68 +0,0 @@ -import { Flex, HStack, Text } from "@chakra-ui/react"; - -import type { WidgetConfig } from "@/mosaic"; - -import { useRobotInfo } from "@/hooks/useRobotInfo.ts"; - -export interface WidgetHeaderProps { - widgetConfig: WidgetConfig; - showRobotInfo?: boolean; -} - -export function WidgetHeader({ - widgetConfig: { type, connectors }, - showRobotInfo = true, -}: WidgetHeaderProps) { - const { robotInfos } = useRobotInfo(); - - return ( - - - - - {type} - - {showRobotInfo && ( - - Robot:{" "} - {connectors - .map((c) => { - const robotInfo = robotInfos.find((robotInfos) => robotInfos.id === c.robotId); - if (robotInfo) return robotInfo.name; - return c.robotId; - }) - .join(", ")} - - )} - - - - {/*{onRemove && (*/} - {/* {*/} - {/* e.stopPropagation()*/} - {/* onRemove()*/} - {/* }}*/} - {/* >*/} - {/* Remove*/} - {/* */} - {/*)}*/} - {/*{onSettingClick && (*/} - {/* {*/} - {/* e.stopPropagation()*/} - {/* onSettingClick()*/} - {/* }}*/} - {/* >*/} - {/* */} - {/* */} - {/*)}*/} - - ); -} diff --git a/frontend/src/components/Dashboard/Widgets/ErrorWidget.tsx b/frontend/src/components/Dashboard/Widgets/ErrorWidget.tsx new file mode 100644 index 0000000..1a840c6 --- /dev/null +++ b/frontend/src/components/Dashboard/Widgets/ErrorWidget.tsx @@ -0,0 +1,53 @@ +import { Box, Code, Flex, HStack, Icon, Text, VStack } from "@chakra-ui/react"; +import { LuTriangleAlert } from "react-icons/lu"; + +export interface ErrorWidgetProps { + error: string; +} + +export function ErrorWidget({ error }: ErrorWidgetProps) { + return ( + + + + + + + + + + Widget Error + + + This widget could not be loaded due to an error. + + + + + + Error + + + {error} + + + + + + ); +} \ No newline at end of file diff --git a/frontend/src/components/Dashboard/WidgetBody.tsx b/frontend/src/components/Dashboard/Widgets/WidgetBody.tsx similarity index 100% rename from frontend/src/components/Dashboard/WidgetBody.tsx rename to frontend/src/components/Dashboard/Widgets/WidgetBody.tsx diff --git a/frontend/src/components/Dashboard/Widgets/WidgetComponents.tsx b/frontend/src/components/Dashboard/Widgets/WidgetComponents.tsx new file mode 100644 index 0000000..a9ce6da --- /dev/null +++ b/frontend/src/components/Dashboard/Widgets/WidgetComponents.tsx @@ -0,0 +1,55 @@ +import { VStack } from "@chakra-ui/react"; +import { Children, isValidElement, ReactNode } from "react"; + +import type { WidgetConfig } from "@/mosaic"; + +import { WidgetSettingDialog } from "@/components/Dashboard/Widgets/WidgetSettingDialog.tsx"; + +import { ErrorWidget } from "./ErrorWidget.tsx"; +import { WidgetBody } from "./WidgetBody.tsx"; +import { WidgetFooter } from "./WidgetFooter.tsx"; +import { WidgetHeader } from "./WidgetHeader.tsx"; + +interface WidgetFrameProps { + children?: ReactNode; + widgetConfig: WidgetConfig; + error?: string; +} + +function WidgetRoot({ widgetConfig, children, error }: WidgetFrameProps) { + const foundHeader = Children.toArray(children).find( + (child) => isValidElement(child) && child.type === WidgetHeader, + ); + const foundBody = Children.toArray(children).find( + (child) => isValidElement(child) && child.type === WidgetBody, + ); + const foundFooter = Children.toArray(children).find( + (child) => isValidElement(child) && child.type === WidgetFooter, + ); + + return ( + + {foundHeader || } + {error ? ( + + + + ) : ( + foundBody || ( + + + + ) + )} + {foundFooter} + + ); +} + +export const MosaicWidget = { + Root: WidgetRoot, + Header: WidgetHeader, + SettingDialog: WidgetSettingDialog, + Body: WidgetBody, + Footer: WidgetFooter, +}; diff --git a/frontend/src/components/Dashboard/Widgets/WidgetDescriptor.ts b/frontend/src/components/Dashboard/Widgets/WidgetDescriptor.ts new file mode 100644 index 0000000..21e7ae0 --- /dev/null +++ b/frontend/src/components/Dashboard/Widgets/WidgetDescriptor.ts @@ -0,0 +1,27 @@ +export abstract class WidgetDescriptor { + public abstract getName(): string; + + public getMinStoreNumber(): number { + return 1; + } + + public getMaxStoreNumber(): number { + return 1; + } + + public getMinRobotConnectorNumber(): number { + return 1; + } + + public getMaxRobotConnectorNumber(): number { + return 1; + } + + public supportCustomParams(): boolean { + return false; + } + + public validateParams(_params: Record): string | null { + return null; + } +} diff --git a/frontend/src/components/Dashboard/Widgets/WidgetFactory.tsx b/frontend/src/components/Dashboard/Widgets/WidgetFactory.tsx new file mode 100644 index 0000000..c3cf7f3 --- /dev/null +++ b/frontend/src/components/Dashboard/Widgets/WidgetFactory.tsx @@ -0,0 +1,66 @@ +import { type ComponentType, type LazyExoticComponent, Suspense, lazy } from "react"; + +import type { WidgetDescriptor } from "@/components/Dashboard/Widgets/WidgetDescriptor.ts"; +import type { WidgetConfig } from "@/mosaic"; + +import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; + +type WidgetComponent = ComponentType<{ widgetConfig: WidgetConfig }>; +type WidgetDescriptorClass = new () => WidgetDescriptor; + +const descriptorModules = import.meta.glob("../../../widgets/*/WidgetDescriptor.ts", { + eager: true, +}) as Record; + +const componentModules: Record Promise> = import.meta.glob( + "../../../widgets/*/*.tsx", +); + +const registry = new Map(); +const componentCache = new Map>(); + +for (const module of Object.values(descriptorModules)) { + const descriptor = new module.default(); + registry.set(descriptor.getName(), descriptor); +} + +const LazyNotFoundWidget = lazy( + () => import("../../../widgets/NotFoundWidget/NotFoundWidget.tsx"), +) as LazyExoticComponent; + +function getLazyWidget(type: string) { + const descriptor = registry.get(type); + if (!descriptor) return null; + + if (componentCache.has(type)) return componentCache.get(type)!; + + const name = descriptor.getName(); + const moduleLoader = componentModules[`../../../widgets/${name}/${name}.tsx`]; + if (!moduleLoader) return null; + + const component = lazy(moduleLoader as () => Promise<{ default: WidgetComponent }>); + componentCache.set(type, component); + return component; +} + +export interface WidgetFactoryProps { + widgetConfig: WidgetConfig; +} + +export function WidgetFactory({ widgetConfig }: WidgetFactoryProps) { + const descriptor = registry.get(widgetConfig.type); + if (descriptor) { + const validationError = descriptor.validateParams(widgetConfig.params ?? {}); + if (validationError) { + return ; + } + } + + const LazyComponent = getLazyWidget(widgetConfig.type) ?? LazyNotFoundWidget; + + return ( + Loading...}> + + + ); +} diff --git a/frontend/src/components/Dashboard/WidgetFooter.tsx b/frontend/src/components/Dashboard/Widgets/WidgetFooter.tsx similarity index 100% rename from frontend/src/components/Dashboard/WidgetFooter.tsx rename to frontend/src/components/Dashboard/Widgets/WidgetFooter.tsx diff --git a/frontend/src/components/Dashboard/Widgets/WidgetHeader.tsx b/frontend/src/components/Dashboard/Widgets/WidgetHeader.tsx new file mode 100644 index 0000000..cde654e --- /dev/null +++ b/frontend/src/components/Dashboard/Widgets/WidgetHeader.tsx @@ -0,0 +1,57 @@ +import { Flex, HStack, Text } from "@chakra-ui/react"; +import { Children, isValidElement, ReactNode } from "react"; + +import type { WidgetConfig } from "@/mosaic"; + +import { WidgetSettingDialog } from "@/components/Dashboard/Widgets/WidgetSettingDialog.tsx"; +import { useRobotInfo } from "@/hooks/useRobotInfo.ts"; + +export interface WidgetHeaderProps { + children?: ReactNode; + widgetConfig: WidgetConfig; +} + +export function WidgetHeader({ children, widgetConfig: { type, connectors } }: WidgetHeaderProps) { + const { robotInfos } = useRobotInfo(); + + const foundSettingDialog = Children.toArray(children).find( + (child) => isValidElement(child) && child.type === WidgetSettingDialog, + ); + + return ( + + + + + {type} + + + Robot:{" "} + {connectors + .map((c) => { + const robotInfo = robotInfos.find((robotInfos) => robotInfos.id === c.robotId); + if (robotInfo) return robotInfo.name; + return c.robotId; + }) + .join(", ")} + + {foundSettingDialog} + + + + {/*{onRemove && (*/} + {/* {*/} + {/* e.stopPropagation()*/} + {/* onRemove()*/} + {/* }}*/} + {/* >*/} + {/* Remove*/} + {/* */} + {/*)}*/} + + ); +} diff --git a/frontend/src/components/Dashboard/Widgets/WidgetSettingDialog.tsx b/frontend/src/components/Dashboard/Widgets/WidgetSettingDialog.tsx new file mode 100644 index 0000000..2de8e05 --- /dev/null +++ b/frontend/src/components/Dashboard/Widgets/WidgetSettingDialog.tsx @@ -0,0 +1,97 @@ +import { Button, DialogActionTrigger, DialogTitle } from "@chakra-ui/react"; +import { useMutation, useQueryClient } from "@tanstack/react-query"; +import { ReactNode, useRef, useState } from "react"; +import { FieldValues, type SubmitHandler, UseFormReturn } from "react-hook-form"; +import { IoSettings } from "react-icons/io5"; + +import type { ApiError } from "@/client"; +import type { WidgetConfig } from "@/mosaic"; + +import { + DialogBody, + DialogCloseTrigger, + DialogContent, + DialogFooter, + DialogHeader, + DialogRoot, + DialogTrigger, +} from "@/components/ui/dialog.tsx"; +import useCustomToast from "@/hooks/useCustomToast.ts"; +import { handleError } from "@/utils"; + +export interface WidgetSettingDialogProps { + children?: ReactNode; + useFormReturn: UseFormReturn; + widgetConfig: WidgetConfig; +} + +export function WidgetSettingDialog({ + children, + useFormReturn: { + handleSubmit, + reset, + formState: { isValid, isSubmitting }, + }, + widgetConfig, +}: WidgetSettingDialogProps) { + const { showSuccessToast } = useCustomToast(); + const [isOpen, setIsOpen] = useState(false); + const dialogContentRef = useRef(null); + const queryClient = useQueryClient(); + + const mutation = useMutation({ + mutationFn: async (data: T) => { + widgetConfig.onUpdateWidgetParams(data); + }, + onSuccess: () => { + showSuccessToast("Robot created successfully."); + reset(); + setIsOpen(false); + }, + onError: (err: ApiError) => { + handleError(err); + }, + onSettled: async () => { + await queryClient.invalidateQueries({ queryKey: ["robots"] }); + }, + }); + + const onSubmit: SubmitHandler = (data: T) => { + mutation.mutate(data); + }; + + return ( + setIsOpen(open)} + > + + + + +
+ + Change Setting + + {children} + + + + + + + +
+ +
+
+ ); +} diff --git a/frontend/src/components/OccupancyMaps/OccupancyMapsTable.tsx b/frontend/src/components/OccupancyMaps/OccupancyMapsTable.tsx index 17cc8ca..b6bd8d1 100644 --- a/frontend/src/components/OccupancyMaps/OccupancyMapsTable.tsx +++ b/frontend/src/components/OccupancyMaps/OccupancyMapsTable.tsx @@ -41,7 +41,7 @@ export function OccupancyMapsTable() { const setPage = (page: number) => navigate({ - search: (prev: { [key: string]: string }) => ({ ...prev, page }), + search: (prev) => ({ ...prev, page }), }); const occupancyMaps = data?.data.slice(0, PER_PAGE) ?? []; diff --git a/frontend/src/mosaic/index.ts b/frontend/src/mosaic/index.ts index c4809d1..0dcf06d 100644 --- a/frontend/src/mosaic/index.ts +++ b/frontend/src/mosaic/index.ts @@ -32,6 +32,7 @@ export interface WidgetConfig { position: WidgetPositionConfig; connectors: RobotConnector[]; params?: any; + onUpdateWidgetParams: (params?: any) => void; } export interface TabConfig { diff --git a/frontend/src/widgets/ClearpathPlatformPowerViewerWidget/ClearpathPlatformPowerViewerWidget.tsx b/frontend/src/widgets/ClearpathPlatformPowerViewerWidget/ClearpathPlatformPowerViewerWidget.tsx index 1a300ce..4e8e0a3 100644 --- a/frontend/src/widgets/ClearpathPlatformPowerViewerWidget/ClearpathPlatformPowerViewerWidget.tsx +++ b/frontend/src/widgets/ClearpathPlatformPowerViewerWidget/ClearpathPlatformPowerViewerWidget.tsx @@ -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 { WidgetFrame } from "@/components/Dashboard/WidgetFrame.tsx"; +import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; import { useMosaicStore } from "@/hooks/useMosaicStore.ts"; interface PowerData { @@ -115,18 +115,18 @@ export default function ClearpathPlatformPowerViewerWidget({ widgetConfig }: Wid if (!data) { return ( - + Waiting for data... - + ); } return ( - + {/* Connection Status */} @@ -187,6 +187,6 @@ export default function ClearpathPlatformPowerViewerWidget({ widgetConfig }: Wid {formatTimestamp(data.timestamp)} - + ); } diff --git a/frontend/src/widgets/ConnectionCheckWidget/ConnectionCheckWidget.tsx b/frontend/src/widgets/ConnectionCheckWidget/ConnectionCheckWidget.tsx index b5b3e6d..beceb12 100644 --- a/frontend/src/widgets/ConnectionCheckWidget/ConnectionCheckWidget.tsx +++ b/frontend/src/widgets/ConnectionCheckWidget/ConnectionCheckWidget.tsx @@ -6,7 +6,7 @@ import type { ConnectionCheckReceiverStore } from "@/stores/ConnectionCheckRecei import type { ConnectionCheckSenderStore } from "@/stores/ConnectionCheckSenderStore/ConnectionCheckSenderStore.ts"; import type { WidgetProps } from "@/widgets/index.ts"; -import { WidgetFrame } from "@/components/Dashboard/WidgetFrame.tsx"; +import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; import { useMosaicStore } from "@/hooks/useMosaicStore.ts"; const MAX_MESSAGES = 200; @@ -99,7 +99,7 @@ export default function ConnectionCheckWidget({ widgetConfig }: WidgetProps) { }, [widgetConfig]); return ( - + {connectionCheckingMessages.map((msg, i) => { @@ -114,6 +114,6 @@ export default function ConnectionCheckWidget({ widgetConfig }: WidgetProps) { })} - + ); } diff --git a/frontend/src/widgets/DelayCheckWidget/DelayCheckWidget.tsx b/frontend/src/widgets/DelayCheckWidget/DelayCheckWidget.tsx index dca86f1..919bf48 100644 --- a/frontend/src/widgets/DelayCheckWidget/DelayCheckWidget.tsx +++ b/frontend/src/widgets/DelayCheckWidget/DelayCheckWidget.tsx @@ -6,7 +6,7 @@ import type { ConnectionCheckReceiverStore } from "@/stores/ConnectionCheckRecei import type { ConnectionCheckSenderStore } from "@/stores/ConnectionCheckSenderStore/ConnectionCheckSenderStore.ts"; import type { WidgetProps } from "@/widgets/index.ts"; -import { WidgetFrame } from "@/components/Dashboard/WidgetFrame.tsx"; +import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; import { useMosaicStore } from "@/hooks/useMosaicStore.ts"; import { useRobotInfo } from "@/hooks/useRobotInfo.ts"; @@ -153,7 +153,7 @@ export default function DelayCheckWidget({ widgetConfig }: WidgetProps) { }, [widgetConfig]); return ( - + {/* Stats + controls */} @@ -265,6 +265,6 @@ export default function DelayCheckWidget({ widgetConfig }: WidgetProps) { - + ); } diff --git a/frontend/src/widgets/ImuState3DViewerWidget/ImuState3DViewerWidget.tsx b/frontend/src/widgets/ImuState3DViewerWidget/ImuState3DViewerWidget.tsx index b30d6ac..5acb355 100644 --- a/frontend/src/widgets/ImuState3DViewerWidget/ImuState3DViewerWidget.tsx +++ b/frontend/src/widgets/ImuState3DViewerWidget/ImuState3DViewerWidget.tsx @@ -7,7 +7,7 @@ import * as THREE from "three"; import type { JsonReceivableStore } from "@/stores/JsonReceivableStore/JsonReceivableStore.ts"; import type { WidgetProps } from "@/widgets/index.ts"; -import { WidgetFrame } from "@/components/Dashboard/WidgetFrame.tsx"; +import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; import { useMosaicStore } from "@/hooks/useMosaicStore.ts"; interface ImuData { @@ -117,7 +117,7 @@ export default function ImuState3DViewerWidget({ widgetConfig }: WidgetProps) { }, [widgetConfig]); return ( - + {/* 3D Orientation Viewer */} @@ -189,6 +189,6 @@ export default function ImuState3DViewerWidget({ widgetConfig }: WidgetProps) { - + ); } diff --git a/frontend/src/widgets/JsonViewerWidget/JsonViewerWidget.tsx b/frontend/src/widgets/JsonViewerWidget/JsonViewerWidget.tsx index 7227a3b..9aa838b 100644 --- a/frontend/src/widgets/JsonViewerWidget/JsonViewerWidget.tsx +++ b/frontend/src/widgets/JsonViewerWidget/JsonViewerWidget.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from "react"; import type { JsonReceivableStore } from "@/stores/JsonReceivableStore/JsonReceivableStore.ts"; import type { WidgetProps } from "@/widgets/index.ts"; -import { WidgetFrame } from "@/components/Dashboard/WidgetFrame.tsx"; +import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; import { useMosaicStore } from "@/hooks/useMosaicStore.ts"; export default function JsonViewerWidget({ widgetConfig }: WidgetProps) { @@ -35,10 +35,10 @@ export default function JsonViewerWidget({ widgetConfig }: WidgetProps) { const formattedData = data ? JSON.stringify(data, null, 2) : ""; return ( - + {formattedData} - + ); } diff --git a/frontend/src/widgets/JsonViewerWidget/WidgetDescriptor.ts b/frontend/src/widgets/JsonViewerWidget/WidgetDescriptor.ts new file mode 100644 index 0000000..644684e --- /dev/null +++ b/frontend/src/widgets/JsonViewerWidget/WidgetDescriptor.ts @@ -0,0 +1,7 @@ +import { WidgetDescriptor } from "@/components/Dashboard/Widgets/WidgetDescriptor.ts"; + +export default class JsonViewerWidgetDescriptor extends WidgetDescriptor { + public getName(): string { + return "BaseTemplateWidget"; + } +} diff --git a/frontend/src/widgets/LaserScanPolarViewerWidget/LaserScanPolarViewerWidget.tsx b/frontend/src/widgets/LaserScanPolarViewerWidget/LaserScanPolarViewerWidget.tsx index b4643bf..c4f5762 100644 --- a/frontend/src/widgets/LaserScanPolarViewerWidget/LaserScanPolarViewerWidget.tsx +++ b/frontend/src/widgets/LaserScanPolarViewerWidget/LaserScanPolarViewerWidget.tsx @@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from "react"; import type { JsonReceivableStore } from "@/stores/JsonReceivableStore/JsonReceivableStore.ts"; import type { WidgetProps } from "@/widgets/index.ts"; -import { WidgetFrame } from "@/components/Dashboard/WidgetFrame.tsx"; +import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; import { useMosaicStore } from "@/hooks/useMosaicStore.ts"; interface LaserScanData { @@ -153,10 +153,10 @@ export default function LaserScanPolarViewerWidget({ widgetConfig }: WidgetProps }, [data]); return ( - + - + ); } diff --git a/frontend/src/widgets/MediaViewerWidget/MediaViewerWidget.tsx b/frontend/src/widgets/MediaViewerWidget/MediaViewerWidget.tsx index afc891b..c345af1 100644 --- a/frontend/src/widgets/MediaViewerWidget/MediaViewerWidget.tsx +++ b/frontend/src/widgets/MediaViewerWidget/MediaViewerWidget.tsx @@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from "react"; import type { MediaStreamStore } from "@/stores/MediaStreamStore/MediaStreamStore.ts"; import type { WidgetProps } from "@/widgets/index.ts"; -import { WidgetFrame } from "@/components/Dashboard/WidgetFrame.tsx"; +import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; import { useMosaicStore } from "@/hooks/useMosaicStore.ts"; export default function MediaViewerWidget({ widgetConfig }: WidgetProps) { @@ -137,7 +137,7 @@ export default function MediaViewerWidget({ widgetConfig }: WidgetProps) { }, []); return ( - + {error ? ( )} - + ); } diff --git a/frontend/src/widgets/MediaViewerWithStatWidget/MediaViewerWithStatWidget.tsx b/frontend/src/widgets/MediaViewerWithStatWidget/MediaViewerWithStatWidget.tsx index 48b1756..b20803b 100644 --- a/frontend/src/widgets/MediaViewerWithStatWidget/MediaViewerWithStatWidget.tsx +++ b/frontend/src/widgets/MediaViewerWithStatWidget/MediaViewerWithStatWidget.tsx @@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import type { MediaStreamStore, StreamStats } from "@/stores/MediaStreamStore/MediaStreamStore.ts"; import type { WidgetProps } from "@/widgets/index.ts"; -import { WidgetFrame } from "@/components/Dashboard/WidgetFrame.tsx"; +import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; import { useMosaicStore } from "@/hooks/useMosaicStore.ts"; import { useRobotInfo } from "@/hooks/useRobotInfo.ts"; @@ -163,7 +163,7 @@ export default function MediaViewerWithStatWidget({ widgetConfig }: WidgetProps) }, []); return ( - + {error ? ( )} - + ); } diff --git a/frontend/src/widgets/NotFoundWidget/NotFoundWidget.tsx b/frontend/src/widgets/NotFoundWidget/NotFoundWidget.tsx index c369558..f12c42a 100644 --- a/frontend/src/widgets/NotFoundWidget/NotFoundWidget.tsx +++ b/frontend/src/widgets/NotFoundWidget/NotFoundWidget.tsx @@ -3,11 +3,11 @@ import { LuPuzzle, LuWrench } from "react-icons/lu"; import type { WidgetProps } from "@/widgets/index.ts"; -import { WidgetFrame } from "@/components/Dashboard/WidgetFrame.tsx"; +import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; export default function NotFoundWidget({ widgetConfig }: WidgetProps) { return ( - + @@ -60,6 +60,6 @@ export default function NotFoundWidget({ widgetConfig }: WidgetProps) { - + ); } diff --git a/frontend/src/widgets/OpenStreetMapViewerWidget/OpenStreetMapViewerWidget.tsx b/frontend/src/widgets/OpenStreetMapViewerWidget/OpenStreetMapViewerWidget.tsx index b8c7cad..9c48d37 100644 --- a/frontend/src/widgets/OpenStreetMapViewerWidget/OpenStreetMapViewerWidget.tsx +++ b/frontend/src/widgets/OpenStreetMapViewerWidget/OpenStreetMapViewerWidget.tsx @@ -8,7 +8,7 @@ import { MapContainer, Marker, Popup, TileLayer } from "react-leaflet"; import type { JsonReceivableStore } from "@/stores/JsonReceivableStore/JsonReceivableStore.ts"; import type { WidgetProps } from "@/widgets/index.ts"; -import { WidgetFrame } from "@/components/Dashboard/WidgetFrame.tsx"; +import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; import { useMosaicStore } from "@/hooks/useMosaicStore.ts"; import { useRobotInfo } from "@/hooks/useRobotInfo.ts"; import "leaflet/dist/leaflet.css"; @@ -230,7 +230,7 @@ export default function OpenStreetMapViewerWidget({ widgetConfig }: WidgetProps) }; return ( - + - + ); } diff --git a/frontend/src/widgets/PointCloud2DViewerV2Widget/PointCloud2DViewerV2Widget.tsx b/frontend/src/widgets/PointCloud2DViewerV2Widget/PointCloud2DViewerV2Widget.tsx index eb88779..a941120 100644 --- a/frontend/src/widgets/PointCloud2DViewerV2Widget/PointCloud2DViewerV2Widget.tsx +++ b/frontend/src/widgets/PointCloud2DViewerV2Widget/PointCloud2DViewerV2Widget.tsx @@ -9,7 +9,7 @@ import type { } from "@/stores/@types/pointcloud.ts"; import type { WidgetProps } from "@/widgets/index.ts"; -import { WidgetFrame } from "@/components/Dashboard/WidgetFrame.tsx"; +import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; import { useMosaicStore } from "@/hooks/useMosaicStore.ts"; import AngleIndicator from "./AngleIndicator.tsx"; @@ -225,7 +225,7 @@ export default function PointCloud2DViewerV2Widget({ widgetConfig }: WidgetProps ]; return ( - + {error ? ( )} - + ); } diff --git a/frontend/src/widgets/PointCloud2DViewerWidget/PointCloud2DViewerWidget.tsx b/frontend/src/widgets/PointCloud2DViewerWidget/PointCloud2DViewerWidget.tsx index 6f23144..a216f03 100644 --- a/frontend/src/widgets/PointCloud2DViewerWidget/PointCloud2DViewerWidget.tsx +++ b/frontend/src/widgets/PointCloud2DViewerWidget/PointCloud2DViewerWidget.tsx @@ -9,7 +9,7 @@ import type { } from "@/stores/@types/pointcloud.ts"; import type { WidgetProps } from "@/widgets/index.ts"; -import { WidgetFrame } from "@/components/Dashboard/WidgetFrame.tsx"; +import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; import { useMosaicStore } from "@/hooks/useMosaicStore.ts"; import AngleIndicator from "@/widgets/PointCloud2DViewerWidget/AngleIndicator.tsx"; @@ -194,7 +194,7 @@ export default function PointCloud2DViewerWidget({ widgetConfig }: WidgetProps) ]; return ( - + {error ? ( )} - + ); } diff --git a/frontend/src/widgets/PointCloud3DViewerWidget/PointCloud3DViewerWidget.tsx b/frontend/src/widgets/PointCloud3DViewerWidget/PointCloud3DViewerWidget.tsx index bf71424..4f11206 100644 --- a/frontend/src/widgets/PointCloud3DViewerWidget/PointCloud3DViewerWidget.tsx +++ b/frontend/src/widgets/PointCloud3DViewerWidget/PointCloud3DViewerWidget.tsx @@ -9,7 +9,7 @@ import type { } from "@/stores/@types/pointcloud.ts"; import type { WidgetProps } from "@/widgets/index.ts"; -import { WidgetFrame } from "@/components/Dashboard/WidgetFrame.tsx"; +import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; import { useMosaicStore } from "@/hooks/useMosaicStore.ts"; import type { ColorMode } from "./colorMapping.ts"; @@ -203,7 +203,7 @@ export default function PointCloud3DViewerWidget({ widgetConfig }: WidgetProps) ]; return ( - + {error ? ( )} - + ); } diff --git a/frontend/src/widgets/ThumbstickSenderWidget/ThumbstickSenderWidget.tsx b/frontend/src/widgets/ThumbstickSenderWidget/ThumbstickSenderWidget.tsx index a687c24..1c7d44c 100644 --- a/frontend/src/widgets/ThumbstickSenderWidget/ThumbstickSenderWidget.tsx +++ b/frontend/src/widgets/ThumbstickSenderWidget/ThumbstickSenderWidget.tsx @@ -5,7 +5,7 @@ import type { SendableStore } from "@/mosaic/store/interface/sendable-store.ts"; import type { Thumbstick } from "@/stores/@types/thumbstick.ts"; import type { WidgetProps } from "@/widgets/index.ts"; -import { WidgetFrame } from "@/components/Dashboard/WidgetFrame.tsx"; +import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; import { Checkbox } from "@/components/ui/checkbox.tsx"; import { useMosaicStore } from "@/hooks/useMosaicStore.ts"; @@ -88,7 +88,7 @@ export default function ThumbstickSenderWidget({ widgetConfig }: WidgetProps) { }; return ( - + {/* Container that fills available space */} @@ -170,6 +170,6 @@ export default function ThumbstickSenderWidget({ widgetConfig }: WidgetProps) { - + ); } diff --git a/frontend/src/widgets/WASDSenderWidget/WASDSenderWidget.tsx b/frontend/src/widgets/WASDSenderWidget/WASDSenderWidget.tsx index 9003b3a..7802e09 100644 --- a/frontend/src/widgets/WASDSenderWidget/WASDSenderWidget.tsx +++ b/frontend/src/widgets/WASDSenderWidget/WASDSenderWidget.tsx @@ -5,7 +5,7 @@ import type { SendableStore } from "@/mosaic/store/interface/sendable-store.ts"; import type { SimpleDirection } from "@/stores/@types/simple-direction.ts"; import type { WidgetProps } from "@/widgets/index.ts"; -import { WidgetFrame } from "@/components/Dashboard/WidgetFrame.tsx"; +import { WidgetRoot } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; import { useMosaicStore } from "@/hooks/useMosaicStore.ts"; type Direction = "up" | "down" | "left" | "right" | "stop"; @@ -84,7 +84,7 @@ export default function WASDSenderWidget({ widgetConfig }: WidgetProps) { }; return ( - + {/* empty */} @@ -113,6 +113,6 @@ export default function WASDSenderWidget({ widgetConfig }: WidgetProps) { {/* empty */} - + ); } diff --git a/frontend/src/widgets/_templates/BaseTemplateWidget/BaseTemplateSetting.tsx b/frontend/src/widgets/_templates/BaseTemplateWidget/BaseTemplateSetting.tsx new file mode 100644 index 0000000..ea4a797 --- /dev/null +++ b/frontend/src/widgets/_templates/BaseTemplateWidget/BaseTemplateSetting.tsx @@ -0,0 +1,44 @@ +import { VStack, Text, Input } from "@chakra-ui/react"; +import { useForm } from "react-hook-form"; + +import type { WidgetConfig } from "@/mosaic"; + +import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; +import { Field } from "@/components/ui/field.tsx"; + +interface BaseTemplateSettingProps { + widgetConfig: WidgetConfig; +} + +export function BaseTemplateSetting({ widgetConfig }: BaseTemplateSettingProps) { + const useFormReturn = useForm<{ name: string }>({ + mode: "onBlur", + criteriaMode: "all", + defaultValues: { + name: "", + }, + }); + + const { + formState: { errors }, + register, + } = useFormReturn; + + return ( + + You can customize this dialog + + + + + + + ); +} diff --git a/frontend/src/widgets/_templates/BaseTemplateWidget/BaseTemplateWidget.tsx b/frontend/src/widgets/_templates/BaseTemplateWidget/BaseTemplateWidget.tsx new file mode 100644 index 0000000..6973035 --- /dev/null +++ b/frontend/src/widgets/_templates/BaseTemplateWidget/BaseTemplateWidget.tsx @@ -0,0 +1,50 @@ +import { useEffect, useState } from "react"; + +import type { JsonReceivableStore } from "@/stores/JsonReceivableStore/JsonReceivableStore.ts"; +import type { WidgetProps } from "@/widgets"; + +import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; +import { useMosaicStore } from "@/hooks/useMosaicStore.ts"; +import { BaseTemplateSetting } from "@/widgets/_templates/BaseTemplateWidget/BaseTemplateSetting.tsx"; + +export default function BaseTemplateWidget({ widgetConfig }: WidgetProps) { + const { getOrCreateStore, releaseStore } = useMosaicStore(); + const [data, setData] = useState(null); + + useEffect(() => { + const connector = widgetConfig.connectors[0]; + if (!connector) { + return; + } + + const store = getOrCreateStore(connector) as JsonReceivableStore; + if (store === null) { + return; + } + + const unsubscribe = store.subscribe((data) => { + setData(data); + }); + + return () => { + unsubscribe(); + releaseStore(connector); + }; + }, [widgetConfig]); + + return ( + + + + + + Customize this widget + {data} + + + + ); +} diff --git a/frontend/src/widgets/_templates/BaseTemplateWidget/WidgetDescriptor.ts b/frontend/src/widgets/_templates/BaseTemplateWidget/WidgetDescriptor.ts new file mode 100644 index 0000000..a9f0be4 --- /dev/null +++ b/frontend/src/widgets/_templates/BaseTemplateWidget/WidgetDescriptor.ts @@ -0,0 +1,27 @@ +import { WidgetDescriptor } from "@/components/Dashboard/Widgets/WidgetDescriptor.ts"; + +export default class BaseTemplateWidgetDescriptor extends WidgetDescriptor { + public getName(): string { + return "BaseTemplateWidget"; + } + + public getMinStoreNumber(): number { + return 1; + } + + public getMaxStoreNumber(): number { + return 1; + } + + public getMinRobotConnectorNumber(): number { + return 1; + } + + public getMaxRobotConnectorNumber(): number { + return 1; + } + + public validateParams(_params: Record): string | null { + return null; + } +} From f3073f9d1f2c97bbb719427ea1cea943d074aa92 Mon Sep 17 00:00:00 2001 From: YeonhyukKim Date: Wed, 18 Mar 2026 16:13:26 +0900 Subject: [PATCH 02/17] - feat: add JsonViewerSetting component --- .../JsonViewerWidget/JsonViewerSetting.tsx | 58 +++++++++++++++++++ .../JsonViewerWidget/JsonViewerWidget.tsx | 28 ++++++--- 2 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 frontend/src/widgets/JsonViewerWidget/JsonViewerSetting.tsx diff --git a/frontend/src/widgets/JsonViewerWidget/JsonViewerSetting.tsx b/frontend/src/widgets/JsonViewerWidget/JsonViewerSetting.tsx new file mode 100644 index 0000000..205c0fe --- /dev/null +++ b/frontend/src/widgets/JsonViewerWidget/JsonViewerSetting.tsx @@ -0,0 +1,58 @@ +import { VStack, Text } from "@chakra-ui/react"; +import { Controller, useForm } from "react-hook-form"; + +import type { WidgetConfig } from "@/mosaic"; + +import { MosaicWidget } from "@/components/Dashboard/Widgets/WidgetComponents.tsx"; +import { Checkbox } from "@/components/ui/checkbox.tsx"; +import { Field } from "@/components/ui/field.tsx"; + +interface JsonViewerSettingProps { + widgetConfig: WidgetConfig; +} + +export type JsonViewerParams = { + cumulative: boolean; +}; + +export function JsonViewerSetting({ widgetConfig }: JsonViewerSettingProps) { + const useFormReturn = useForm({ + mode: "onBlur", + criteriaMode: "all", + defaultValues: { + cumulative: widgetConfig.params.cumulative || false, + }, + }); + + const { + formState: { errors }, + control, + } = useFormReturn; + + return ( + + JSON Viewer Settings + + + ( + field.onChange(checked)} + > + Enable cumulative mode + + )} + /> + + + + ); +} diff --git a/frontend/src/widgets/JsonViewerWidget/JsonViewerWidget.tsx b/frontend/src/widgets/JsonViewerWidget/JsonViewerWidget.tsx index 9aa838b..90ab720 100644 --- a/frontend/src/widgets/JsonViewerWidget/JsonViewerWidget.tsx +++ b/frontend/src/widgets/JsonViewerWidget/JsonViewerWidget.tsx @@ -4,12 +4,13 @@ import { useEffect, useState } from "react"; 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"; +import { JsonViewerSetting } from "@/widgets/JsonViewerWidget/JsonViewerSetting.tsx"; export default function JsonViewerWidget({ widgetConfig }: WidgetProps) { const { getOrCreateStore, releaseStore } = useMosaicStore(); - const [data, setData] = useState(null); + const [data, setData] = useState([]); useEffect(() => { const connector = widgetConfig.connectors[0]; @@ -23,7 +24,11 @@ export default function JsonViewerWidget({ widgetConfig }: WidgetProps) { } const unsubscribe = store.subscribe((data) => { - setData(data); + if (widgetConfig.params.cumulative) { + setData((prevData) => [...prevData, data]); + } else { + setData([data]); + } }); return () => { @@ -32,13 +37,18 @@ export default function JsonViewerWidget({ widgetConfig }: WidgetProps) { }; }, [widgetConfig]); - const formattedData = data ? JSON.stringify(data, null, 2) : ""; + const formattedData = data.map((d) => JSON.stringify(d, null, 2)).join("\n"); return ( - - - {formattedData} - - + + + + + + + {formattedData} + + + ); } From 1d09978ee3852902694979281e81af341d867e78 Mon Sep 17 00:00:00 2001 From: YeonhyukKim Date: Wed, 18 Mar 2026 19:19:11 +0900 Subject: [PATCH 03/17] - feat: add WidgetCustomizePage --- .../src/components/Common/SidebarItems.tsx | 3 +- .../components/Dashboard/DashboardGrid.tsx | 13 +- .../Dashboard/Widgets/WidgetBody.tsx | 7 +- .../Dashboard/Widgets/WidgetComponents.tsx | 12 +- .../Dashboard/Widgets/WidgetDescriptor.ts | 4 + .../Dashboard/Widgets/WidgetFooter.tsx | 42 --- .../Dashboard/Widgets/WidgetHeader.tsx | 98 ++++--- .../Dashboard/Widgets/WidgetSettingDialog.tsx | 6 +- .../WidgetCustomize/StoreDataPanel.tsx | 128 +++++++++ .../WidgetCustomize/StoreSetupPanel.tsx | 76 ++++++ .../WidgetCustomize/WidgetConfigPanel.tsx | 38 +++ .../WidgetCustomize/WidgetCustomizePage.tsx | 243 ++++++++++++++++++ .../WidgetCustomize/WidgetPreviewPanel.tsx | 67 +++++ .../WidgetCustomize/WidgetSelector.tsx | 36 +++ .../WidgetCustomize/storeRegistry.ts | 10 + .../WidgetCustomize/widgetRegistry.ts | 20 ++ frontend/src/routeTree.gen.ts | 11 + .../src/routes/_layout/widget-customize.tsx | 7 + .../JsonViewerWidget/JsonViewerWidget.tsx | 10 +- .../JsonViewerWidget/WidgetDescriptor.ts | 6 +- 20 files changed, 730 insertions(+), 107 deletions(-) delete mode 100644 frontend/src/components/Dashboard/Widgets/WidgetFooter.tsx create mode 100644 frontend/src/components/WidgetCustomize/StoreDataPanel.tsx create mode 100644 frontend/src/components/WidgetCustomize/StoreSetupPanel.tsx create mode 100644 frontend/src/components/WidgetCustomize/WidgetConfigPanel.tsx create mode 100644 frontend/src/components/WidgetCustomize/WidgetCustomizePage.tsx create mode 100644 frontend/src/components/WidgetCustomize/WidgetPreviewPanel.tsx create mode 100644 frontend/src/components/WidgetCustomize/WidgetSelector.tsx create mode 100644 frontend/src/components/WidgetCustomize/storeRegistry.ts create mode 100644 frontend/src/components/WidgetCustomize/widgetRegistry.ts create mode 100644 frontend/src/routes/_layout/widget-customize.tsx diff --git a/frontend/src/components/Common/SidebarItems.tsx b/frontend/src/components/Common/SidebarItems.tsx index 1bbff20..ea43239 100644 --- a/frontend/src/components/Common/SidebarItems.tsx +++ b/frontend/src/components/Common/SidebarItems.tsx @@ -5,7 +5,7 @@ import { Link as RouterLink } from "@tanstack/react-router"; import { useState } from "react"; import { FaBars, FaRobot } from "react-icons/fa"; import { FaLink } from "react-icons/fa"; -import { FiMap, FiSettings, FiUsers } from "react-icons/fi"; +import { FiCode, FiMap, FiSettings, FiUsers } from "react-icons/fi"; import useAuth from "@/hooks/useAuth.ts"; @@ -13,6 +13,7 @@ const items = [ { icon: FaRobot, title: "Robots", path: "/robots" }, { icon: FiMap, title: "Occupancy Maps", path: "/occupancy-maps" }, { icon: FaLink, title: "Dashboard", path: "/dashboard" }, + { icon: FiCode, title: "Widget Test", path: "/widget-customize" }, { icon: FiSettings, title: "User Settings", path: "/settings" }, ]; diff --git a/frontend/src/components/Dashboard/DashboardGrid.tsx b/frontend/src/components/Dashboard/DashboardGrid.tsx index 317364d..6bb4fae 100644 --- a/frontend/src/components/Dashboard/DashboardGrid.tsx +++ b/frontend/src/components/Dashboard/DashboardGrid.tsx @@ -369,22 +369,21 @@ export default function DashboardGrid({ tabId }: DashboardGridProps) { onLayoutChange={handleLayoutChange} onDragStop={(layout) => handleLayoutCommit(layout)} onResizeStop={(layout) => handleLayoutCommit(layout)} - isDraggable={true} - isResizable={true} - margin={[16, 16]} + isDraggable + isResizable draggableHandle=".draggable-header" > {editableWidgets.map((widgetConfig) => ( diff --git a/frontend/src/components/Dashboard/Widgets/WidgetBody.tsx b/frontend/src/components/Dashboard/Widgets/WidgetBody.tsx index 88deb56..a959e48 100644 --- a/frontend/src/components/Dashboard/Widgets/WidgetBody.tsx +++ b/frontend/src/components/Dashboard/Widgets/WidgetBody.tsx @@ -9,12 +9,11 @@ export interface WidgetBodyProps { export function WidgetBody({ children }: WidgetBodyProps) { return ( isValidElement(child) && child.type === WidgetHeader, ); const foundBody = Children.toArray(children).find( (child) => isValidElement(child) && child.type === WidgetBody, ); - const foundFooter = Children.toArray(children).find( - (child) => isValidElement(child) && child.type === WidgetFooter, - ); return ( - + {foundHeader || } {error ? ( @@ -41,7 +37,6 @@ function WidgetRoot({ widgetConfig, children, error }: WidgetFrameProps) { ) )} - {foundFooter} ); } @@ -51,5 +46,4 @@ export const MosaicWidget = { Header: WidgetHeader, SettingDialog: WidgetSettingDialog, Body: WidgetBody, - Footer: WidgetFooter, }; diff --git a/frontend/src/components/Dashboard/Widgets/WidgetDescriptor.ts b/frontend/src/components/Dashboard/Widgets/WidgetDescriptor.ts index 21e7ae0..ac2b0bd 100644 --- a/frontend/src/components/Dashboard/Widgets/WidgetDescriptor.ts +++ b/frontend/src/components/Dashboard/Widgets/WidgetDescriptor.ts @@ -24,4 +24,8 @@ export abstract class WidgetDescriptor { public validateParams(_params: Record): string | null { return null; } + + public getDefaultInjectData(): string { + return ""; + } } diff --git a/frontend/src/components/Dashboard/Widgets/WidgetFooter.tsx b/frontend/src/components/Dashboard/Widgets/WidgetFooter.tsx deleted file mode 100644 index 40d396f..0000000 --- a/frontend/src/components/Dashboard/Widgets/WidgetFooter.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import type { ReactNode } from "react"; - -import { HStack, Text, VStack } from "@chakra-ui/react"; - -interface WidgetFooterProps { - footerInfo?: Array<{ - label: string; - value: string | ReactNode; - }>; - footerMessage?: string; -} - -export function WidgetFooter({ footerInfo, footerMessage }: WidgetFooterProps) { - return ( - <> - {/* Footer Info */} - {footerInfo && footerInfo.length > 0 && ( - - {footerInfo.map((info, index) => ( - - - {info.label} - - {typeof info.value === "string" ? ( - - {info.value} - - ) : ( - info.value - )} - - ))} - - )} - {footerMessage && ( - - {footerMessage} - - )} - - ); -} diff --git a/frontend/src/components/Dashboard/Widgets/WidgetHeader.tsx b/frontend/src/components/Dashboard/Widgets/WidgetHeader.tsx index cde654e..4c509c6 100644 --- a/frontend/src/components/Dashboard/Widgets/WidgetHeader.tsx +++ b/frontend/src/components/Dashboard/Widgets/WidgetHeader.tsx @@ -1,57 +1,77 @@ -import { Flex, HStack, Text } from "@chakra-ui/react"; -import { Children, isValidElement, ReactNode } from "react"; +import type { ReactNode } from "react"; + +import { Box, Flex, HStack, Icon, Text, VStack } from "@chakra-ui/react"; +import { FiInfo } from "react-icons/fi"; import type { WidgetConfig } from "@/mosaic"; -import { WidgetSettingDialog } from "@/components/Dashboard/Widgets/WidgetSettingDialog.tsx"; +import { Tooltip } from "@/components/ui/tooltip.tsx"; import { useRobotInfo } from "@/hooks/useRobotInfo.ts"; +export interface AdditionalInfo { + label: string; + value: string; +} + export interface WidgetHeaderProps { children?: ReactNode; widgetConfig: WidgetConfig; + additionalInfo?: AdditionalInfo[]; } -export function WidgetHeader({ children, widgetConfig: { type, connectors } }: WidgetHeaderProps) { +export function WidgetHeader({ + children, + widgetConfig: { type, connectors }, + additionalInfo, +}: WidgetHeaderProps) { const { robotInfos } = useRobotInfo(); - const foundSettingDialog = Children.toArray(children).find( - (child) => isValidElement(child) && child.type === WidgetSettingDialog, + const tooltipContent = ( + + + Connected Robots + + {connectors.map((c, i) => { + const robotInfo = robotInfos.find((r) => r.id === c.robotId); + return ( + + {robotInfo?.name ?? c.robotId} + + {c.connectorId} + + + ); + })} + {additionalInfo && additionalInfo.length > 0 && ( + + + {additionalInfo.map((info, i) => ( + + {info.label} + {info.value} + + ))} + + + )} + ); return ( - - - - - {type} - - - Robot:{" "} - {connectors - .map((c) => { - const robotInfo = robotInfos.find((robotInfos) => robotInfos.id === c.robotId); - if (robotInfo) return robotInfo.name; - return c.robotId; - }) - .join(", ")} - - {foundSettingDialog} - - - - {/*{onRemove && (*/} - {/* {*/} - {/* e.stopPropagation()*/} - {/* onRemove()*/} - {/* }}*/} - {/* >*/} - {/* Remove*/} - {/* */} - {/*)}*/} + + + + {type} + + + + + + {children && ( + e.stopPropagation()}> + {children} + + )} ); } diff --git a/frontend/src/components/Dashboard/Widgets/WidgetSettingDialog.tsx b/frontend/src/components/Dashboard/Widgets/WidgetSettingDialog.tsx index 2de8e05..d757730 100644 --- a/frontend/src/components/Dashboard/Widgets/WidgetSettingDialog.tsx +++ b/frontend/src/components/Dashboard/Widgets/WidgetSettingDialog.tsx @@ -43,9 +43,9 @@ export function WidgetSettingDialog({ mutationFn: async (data: T) => { widgetConfig.onUpdateWidgetParams(data); }, - onSuccess: () => { - showSuccessToast("Robot created successfully."); - reset(); + onSuccess: (_, variables) => { + showSuccessToast("Settings saved successfully."); + reset(variables); setIsOpen(false); }, onError: (err: ApiError) => { diff --git a/frontend/src/components/WidgetCustomize/StoreDataPanel.tsx b/frontend/src/components/WidgetCustomize/StoreDataPanel.tsx new file mode 100644 index 0000000..5274865 --- /dev/null +++ b/frontend/src/components/WidgetCustomize/StoreDataPanel.tsx @@ -0,0 +1,128 @@ +import { Badge, Box, Button, Code, HStack, Text, Textarea, VStack } from "@chakra-ui/react"; +import { useState } from "react"; + +interface ReceivableDataPanelProps { + onInject: (rawData: string) => void; + defaultData: string; +} + +function ReceivableDataPanel({ onInject, defaultData }: ReceivableDataPanelProps) { + const [input, setInput] = useState(defaultData); + + return ( + + + Inject raw data (simulates data arriving from the robot) + +