Compare commits
6 Commits
f1d155529a
...
4d1254753b
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d1254753b | |||
| f96c6e792f | |||
| c614354c09 | |||
| 6ec351a8e5 | |||
| 69998ac347 | |||
| ec754fbaa4 |
@ -23,7 +23,7 @@ import EditWidget from "./admin/EditWidget";
|
||||
import SlideView from "./dashboard/controls/SlideView";
|
||||
import DashboardFlex from "./dashboard/controls/DashboardFlex";
|
||||
import { useSearchParams } from "react-router-dom";
|
||||
import AnimatedCollapse from "./admin/controls/AnimatedCollapse";
|
||||
import AnimatedCollapse from "./dashboard/controls/AnimatedCollapse";
|
||||
import EntityDashboard from "./dashboard/controls/EntityDashboard";
|
||||
import { WidgetGroup } from "./dashboard/controls/WidgetGroup";
|
||||
|
||||
@ -37,15 +37,12 @@ function Dashboard({
|
||||
onChange,
|
||||
onSave,
|
||||
}) {
|
||||
const {
|
||||
data,
|
||||
subscribeToPanelUpdate,
|
||||
isConnected,
|
||||
connectionStatus,
|
||||
} = useWebSocket();
|
||||
const { data, subscribeToPanelUpdate, isConnected, connectionStatus } =
|
||||
useWebSocket();
|
||||
const deviceContext = useDeviceSafe();
|
||||
const panelSelectorVisible = deviceContext?.panelSelectorVisible ?? false;
|
||||
const setPanelSelectorVisible = deviceContext?.setPanelSelectorVisible ?? (() => {});
|
||||
const setPanelSelectorVisible =
|
||||
deviceContext?.setPanelSelectorVisible ?? (() => {});
|
||||
const isInDeviceContext = !!deviceContext;
|
||||
|
||||
const hpAppContext = useHPAppSafe();
|
||||
@ -71,7 +68,7 @@ function Dashboard({
|
||||
const [isEntityDashboardOpen, setIsEntityDashboardOpen] = useState(false);
|
||||
const [entityDashboardEntity, setEntityDashboardEntity] = useState(null);
|
||||
const [isPreview, setIsPreview] = useState(false);
|
||||
|
||||
|
||||
// Set device brightness via native app context instead of URL query
|
||||
useEffect(() => {
|
||||
if (editMode == true) {
|
||||
@ -154,12 +151,10 @@ function Dashboard({
|
||||
|
||||
const currentEntities = panel.entities || [];
|
||||
const exists = currentEntities.some(
|
||||
(e) => (typeof e === "string" ? e : e.entityId) === entity.entityId
|
||||
(e) => (typeof e === "string" ? e : e.entityId) === entity.entityId,
|
||||
);
|
||||
|
||||
if (!exists) {
|
||||
|
||||
|
||||
onChange({
|
||||
...panel,
|
||||
entities: [...currentEntities, entity],
|
||||
@ -178,7 +173,9 @@ function Dashboard({
|
||||
// Generate a unique ID for the widget if it doesn't have one
|
||||
const widgetWithId = {
|
||||
...widget,
|
||||
id: widget.id || `widget-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
id:
|
||||
widget.id ||
|
||||
`widget-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||
};
|
||||
onChange({
|
||||
...panel,
|
||||
@ -192,7 +189,7 @@ function Dashboard({
|
||||
|
||||
const currentEntities = panel.entities || [];
|
||||
const updatedEntities = currentEntities.filter(
|
||||
(e) => (typeof e === "string" ? e : e.entityId) !== entity.entityId
|
||||
(e) => (typeof e === "string" ? e : e.entityId) !== entity.entityId,
|
||||
);
|
||||
|
||||
onChange({
|
||||
@ -235,7 +232,7 @@ function Dashboard({
|
||||
setLocalEntities(reorderedEntities);
|
||||
setUnsavedChanges(true);
|
||||
},
|
||||
[panel, onChange]
|
||||
[panel, onChange],
|
||||
);
|
||||
|
||||
const handleDeleteWidget = useCallback(
|
||||
@ -255,7 +252,7 @@ function Dashboard({
|
||||
});
|
||||
setUnsavedChanges(true);
|
||||
},
|
||||
[panel, onChange]
|
||||
[panel, onChange],
|
||||
);
|
||||
|
||||
const handleWidgetEdit = useCallback((widget) => {
|
||||
@ -263,28 +260,31 @@ function Dashboard({
|
||||
setIsEditWidgetModalOpen(true);
|
||||
}, []);
|
||||
|
||||
const handleWidgetUpdate = useCallback((updatedWidget) => {
|
||||
if (!updatedWidget || !panel || !onChange) return;
|
||||
const handleWidgetUpdate = useCallback(
|
||||
(updatedWidget) => {
|
||||
if (!updatedWidget || !panel || !onChange) return;
|
||||
|
||||
const currentWidgets = panel.widgets || [];
|
||||
const widgetId = updatedWidget.id || updatedWidget.type;
|
||||
const updatedWidgets = currentWidgets.map((w) => {
|
||||
const wId = w.id || w.type;
|
||||
if (wId === widgetId) {
|
||||
return {
|
||||
...w,
|
||||
...updatedWidget,
|
||||
};
|
||||
}
|
||||
return w;
|
||||
});
|
||||
const currentWidgets = panel.widgets || [];
|
||||
const widgetId = updatedWidget.id || updatedWidget.type;
|
||||
const updatedWidgets = currentWidgets.map((w) => {
|
||||
const wId = w.id || w.type;
|
||||
if (wId === widgetId) {
|
||||
return {
|
||||
...w,
|
||||
...updatedWidget,
|
||||
};
|
||||
}
|
||||
return w;
|
||||
});
|
||||
|
||||
onChange({
|
||||
...panel,
|
||||
widgets: updatedWidgets,
|
||||
});
|
||||
setUnsavedChanges(true);
|
||||
}, [panel, onChange]);
|
||||
onChange({
|
||||
...panel,
|
||||
widgets: updatedWidgets,
|
||||
});
|
||||
setUnsavedChanges(true);
|
||||
},
|
||||
[panel, onChange],
|
||||
);
|
||||
|
||||
const handleReorderWidgets = useCallback(
|
||||
(reorderedWidgets) => {
|
||||
@ -296,7 +296,7 @@ function Dashboard({
|
||||
});
|
||||
setUnsavedChanges(true);
|
||||
},
|
||||
[panel, onChange]
|
||||
[panel, onChange],
|
||||
);
|
||||
|
||||
const handleCancel = () => {
|
||||
@ -351,136 +351,159 @@ function Dashboard({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div ref={dashboardRef} style={{ width, height }}>
|
||||
<DashboardContainer
|
||||
width="100%"
|
||||
height="100%"
|
||||
bordered={editMode && !isFullscreen}
|
||||
colors={colors}
|
||||
showColors={connectionStatus == "connected"}
|
||||
editTools={
|
||||
editMode && (
|
||||
<DashboardEditTools
|
||||
onAddWidget={() => setIsAddWidgetModalOpen(true)}
|
||||
onAddEntity={() => setIsAddEntityModalOpen(true)}
|
||||
onSave={() => {
|
||||
onSave?.(panel);
|
||||
setUnsavedChanges(false);
|
||||
}}
|
||||
onCancel={handleCancel}
|
||||
onFullscreen={toggleFullscreen}
|
||||
isFullscreen={isFullscreen}
|
||||
unsavedChanges={unsavedChanges}
|
||||
isPreview={isPreview}
|
||||
onPreview={() => setIsPreview(!isPreview)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`dashboard-content${
|
||||
!isContentVisible ? " dashboard-content-hidden" : ""
|
||||
} ${
|
||||
isEntityDashboardOpen ? " dashboard-content-entity-dashboard-open" : ""
|
||||
}`}
|
||||
>
|
||||
<div style={{ position: "relative" }}>
|
||||
<DashboardFlex style={{ marginBottom: "35px" }}>
|
||||
<DashboardTitle onPanelSelect={isInDeviceContext ? () => {
|
||||
setPanelSelectorVisible(!panelSelectorVisible);
|
||||
} : undefined}>
|
||||
{title
|
||||
}</DashboardTitle>
|
||||
<WidgetGroup
|
||||
widgets={panel?.widgets || []}
|
||||
panelName={title}
|
||||
containerId="widgets-container"
|
||||
editMode={editMode ? !isPreview : false}
|
||||
onDelete={handleDeleteWidget}
|
||||
onEdit={handleWidgetEdit}
|
||||
onUpdate={handleWidgetUpdate}
|
||||
onReorder={handleReorderWidgets}
|
||||
isContentVisible={isContentVisible}
|
||||
/>
|
||||
</DashboardFlex>
|
||||
{isInDeviceContext && (
|
||||
<DashboardPanelSelector
|
||||
isOpen={panelSelectorVisible}
|
||||
onClose={() => setPanelSelectorVisible(false)}
|
||||
onPanelSelect={(selectedPanel, index) => {
|
||||
// Panel selection is handled by DeviceContext via setCurrentPanel
|
||||
setPanelSelectorVisible(false);
|
||||
<div ref={dashboardRef} style={{ width, height }}>
|
||||
<DashboardContainer
|
||||
width="100%"
|
||||
height="100%"
|
||||
bordered={editMode && !isFullscreen}
|
||||
colors={colors}
|
||||
showColors={connectionStatus == "connected"}
|
||||
editTools={
|
||||
editMode && (
|
||||
<DashboardEditTools
|
||||
onAddWidget={() => setIsAddWidgetModalOpen(true)}
|
||||
onAddEntity={() => setIsAddEntityModalOpen(true)}
|
||||
onSave={() => {
|
||||
onSave?.(panel);
|
||||
setUnsavedChanges(false);
|
||||
}}
|
||||
onCancel={handleCancel}
|
||||
onFullscreen={toggleFullscreen}
|
||||
isFullscreen={isFullscreen}
|
||||
unsavedChanges={unsavedChanges}
|
||||
isPreview={isPreview}
|
||||
onPreview={() => setIsPreview(!isPreview)}
|
||||
/>
|
||||
)}
|
||||
)
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`dashboard-content${
|
||||
!isContentVisible ? " dashboard-content-hidden" : ""
|
||||
} ${
|
||||
isEntityDashboardOpen
|
||||
? " dashboard-content-entity-dashboard-open"
|
||||
: ""
|
||||
}`}
|
||||
>
|
||||
<div style={{ position: "relative" }}>
|
||||
<DashboardFlex style={{ marginBottom: "35px" }}>
|
||||
<DashboardTitle
|
||||
onPanelSelect={
|
||||
isInDeviceContext
|
||||
? () => {
|
||||
setPanelSelectorVisible(!panelSelectorVisible);
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{title}
|
||||
</DashboardTitle>
|
||||
<WidgetGroup
|
||||
widgets={panel?.widgets || []}
|
||||
panelName={title}
|
||||
containerId="widgets-container"
|
||||
editMode={editMode ? !isPreview : false}
|
||||
onDelete={handleDeleteWidget}
|
||||
onEdit={handleWidgetEdit}
|
||||
onUpdate={handleWidgetUpdate}
|
||||
onReorder={handleReorderWidgets}
|
||||
isContentVisible={isContentVisible}
|
||||
/>
|
||||
</DashboardFlex>
|
||||
{isInDeviceContext && (
|
||||
<DashboardPanelSelector
|
||||
isOpen={panelSelectorVisible}
|
||||
onClose={() => setPanelSelectorVisible(false)}
|
||||
onPanelSelect={(selectedPanel, index) => {
|
||||
// Panel selection is handled by DeviceContext via setCurrentPanel
|
||||
setPanelSelectorVisible(false);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<ConnectionStatus showIfConnected={false} />
|
||||
<AnimatedCollapse
|
||||
visible={connectionStatus == "connected" && !panelSelectorVisible}
|
||||
style={{ overflow: "visible" }}
|
||||
>
|
||||
<SlideView ref={slideViewRef} className="views-container">
|
||||
{/* Main View: All Entities */}
|
||||
<SlideView.Item>
|
||||
<EntityGroupContainer>
|
||||
<EntityGroup
|
||||
entities={entities}
|
||||
type="controls"
|
||||
containerId="controls-container"
|
||||
editMode={editMode ? !isPreview : false}
|
||||
panelName={title}
|
||||
onDelete={handleDeleteEntity}
|
||||
onUpdate={handleEntityUpdate}
|
||||
onReorder={handleReorderEntities}
|
||||
onShowDashboard={handleOpenEntityDashboard}
|
||||
isContentVisible={isContentVisible}
|
||||
/>
|
||||
|
||||
<EntityGroup
|
||||
entities={entities}
|
||||
type="sensors"
|
||||
containerId="sensors-container"
|
||||
onClick={() => {}}
|
||||
editMode={editMode ? !isPreview : false}
|
||||
panelName={title}
|
||||
onDelete={handleDeleteEntity}
|
||||
onUpdate={handleEntityUpdate}
|
||||
onReorder={handleReorderEntities}
|
||||
isContentVisible={isContentVisible}
|
||||
/>
|
||||
</EntityGroupContainer>
|
||||
</SlideView.Item>
|
||||
|
||||
{/* Media Player Detail Views */}
|
||||
{entities
|
||||
.filter((e) => {
|
||||
const entityId = e.entityId;
|
||||
return entityId?.startsWith("media_player.");
|
||||
})
|
||||
.map((entity) => {
|
||||
const entityId = entity.entityId;
|
||||
return (
|
||||
<SlideView.Item key={`slide-${entityId}`}>
|
||||
<MediaPlayerSlide entity={entity} />
|
||||
</SlideView.Item>
|
||||
);
|
||||
})}
|
||||
</SlideView>
|
||||
</AnimatedCollapse>
|
||||
</div>
|
||||
<ConnectionStatus showIfConnected={false} />
|
||||
<AnimatedCollapse visible={connectionStatus == "connected" && !panelSelectorVisible} style={{ overflow: "visible" }}>
|
||||
<SlideView ref={slideViewRef} className="views-container">
|
||||
{/* Main View: All Entities */}
|
||||
<SlideView.Item>
|
||||
<EntityGroupContainer>
|
||||
<EntityGroup
|
||||
entities={entities}
|
||||
type="controls"
|
||||
containerId="controls-container"
|
||||
editMode={editMode ? !isPreview : false}
|
||||
panelName={title}
|
||||
onDelete={handleDeleteEntity}
|
||||
onUpdate={handleEntityUpdate}
|
||||
onReorder={handleReorderEntities}
|
||||
onShowDashboard={handleOpenEntityDashboard}
|
||||
isContentVisible={isContentVisible}
|
||||
/>
|
||||
|
||||
<EntityGroup
|
||||
entities={entities}
|
||||
type="sensors"
|
||||
containerId="sensors-container"
|
||||
onClick={() => {}}
|
||||
editMode={editMode ? !isPreview : false}
|
||||
panelName={title}
|
||||
onDelete={handleDeleteEntity}
|
||||
onUpdate={handleEntityUpdate}
|
||||
onReorder={handleReorderEntities}
|
||||
isContentVisible={isContentVisible}
|
||||
/>
|
||||
</EntityGroupContainer>
|
||||
</SlideView.Item>
|
||||
|
||||
{/* Media Player Detail Views */}
|
||||
{entities
|
||||
.filter((e) => {
|
||||
const entityId = e.entityId;
|
||||
return entityId?.startsWith("media_player.");
|
||||
})
|
||||
.map((entity) => {
|
||||
const entityId = entity.entityId;
|
||||
return (
|
||||
<SlideView.Item key={`slide-${entityId}`}>
|
||||
<MediaPlayerSlide entity={entity} />
|
||||
</SlideView.Item>
|
||||
);
|
||||
})}
|
||||
</SlideView>
|
||||
</AnimatedCollapse>
|
||||
</div>
|
||||
</DashboardContainer>
|
||||
</div>
|
||||
<AddEntity isOpen={isAddEntityModalOpen} onClose={() => setIsAddEntityModalOpen(false)} onAdd={handleAddEntity} />
|
||||
<AddWidget isOpen={isAddWidgetModalOpen} onClose={() => setIsAddWidgetModalOpen(false)} onAdd={handleAddWidget} />
|
||||
<EditWidget
|
||||
isOpen={isEditWidgetModalOpen}
|
||||
onClose={() => {
|
||||
setIsEditWidgetModalOpen(false);
|
||||
setEditingWidget(null);
|
||||
}}
|
||||
widget={editingWidget}
|
||||
onUpdate={handleWidgetUpdate}
|
||||
/>
|
||||
<EntityDashboard isOpen={isEntityDashboardOpen} onClose={handleCloseEntityDashboard} entity={entityDashboardEntity} />
|
||||
</DashboardContainer>
|
||||
</div>
|
||||
<AddEntity
|
||||
isOpen={isAddEntityModalOpen}
|
||||
onClose={() => setIsAddEntityModalOpen(false)}
|
||||
onAdd={handleAddEntity}
|
||||
/>
|
||||
<AddWidget
|
||||
isOpen={isAddWidgetModalOpen}
|
||||
onClose={() => setIsAddWidgetModalOpen(false)}
|
||||
onAdd={handleAddWidget}
|
||||
/>
|
||||
<EditWidget
|
||||
isOpen={isEditWidgetModalOpen}
|
||||
onClose={() => {
|
||||
setIsEditWidgetModalOpen(false);
|
||||
setEditingWidget(null);
|
||||
}}
|
||||
widget={editingWidget}
|
||||
onUpdate={handleWidgetUpdate}
|
||||
/>
|
||||
<EntityDashboard
|
||||
isOpen={isEntityDashboardOpen}
|
||||
onClose={handleCloseEntityDashboard}
|
||||
entity={entityDashboardEntity}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
);
|
||||
}
|
||||
|
||||
export default Dashboard;
|
||||
|
||||
@ -117,7 +117,7 @@ function DashboardCarousel({
|
||||
console.log("transitioning", isTransitioning);
|
||||
setIsTransitioning(false);
|
||||
timeoutRef.current = null;
|
||||
}, transitionDuration);
|
||||
}, transitionDuration + 500);
|
||||
}, [currentPanelIndex, panels, activeSlot, slot1, slot2, transitionDuration]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -199,7 +199,7 @@ function DashboardCarousel({
|
||||
width,
|
||||
height,
|
||||
position: "relative",
|
||||
"--dashboard-carousel-duration": `${transitionDuration}ms`,
|
||||
"--dashboard-carousel-duration": `${transitionDuration - 500}ms`,
|
||||
}}
|
||||
>
|
||||
<div className="dashboard-carousel-item dashboard-carousel-visible">
|
||||
@ -225,7 +225,7 @@ function DashboardCarousel({
|
||||
width,
|
||||
height,
|
||||
position: "relative",
|
||||
"--dashboard-carousel-duration": `${transitionDuration}ms`,
|
||||
"--dashboard-carousel-duration": `${transitionDuration - 500}ms`,
|
||||
}}
|
||||
>
|
||||
{slot1 && (
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
.animated-collapse {
|
||||
transition: opacity 150ms ease-in-out, transform 150ms ease-in-out,
|
||||
transition:
|
||||
opacity 150ms ease-in-out,
|
||||
transform 150ms ease-in-out,
|
||||
max-height 150ms ease-in-out;
|
||||
transform: translateY(10px);
|
||||
opacity: 0;
|
||||
@ -10,6 +12,7 @@
|
||||
.animated-collapse.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.animated-collapse.hidden {
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
.dashboard-title-clickable {
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s ease;
|
||||
transform-origin: left center;
|
||||
transform-origin: center center;
|
||||
}
|
||||
|
||||
.dashboard-title-clickable:active {
|
||||
|
||||
@ -108,21 +108,21 @@ export const Entity = ({
|
||||
// onChange handler called when WebSocket updates the entity
|
||||
const onChange = useCallback(
|
||||
(updatedEntity) => {
|
||||
setEntityState((prev) => {
|
||||
const newState = {
|
||||
...prev,
|
||||
...updatedEntity,
|
||||
};
|
||||
// Call onUpdate callback when entity is updated via WebSocket
|
||||
if (onUpdate) {
|
||||
onUpdate(newState);
|
||||
}
|
||||
return newState;
|
||||
});
|
||||
setEntityState((prev) => ({
|
||||
...prev,
|
||||
...updatedEntity,
|
||||
}));
|
||||
},
|
||||
[onUpdate]
|
||||
[]
|
||||
);
|
||||
|
||||
// Call onUpdate callback after entityState changes (outside of render)
|
||||
useEffect(() => {
|
||||
if (onUpdate) {
|
||||
onUpdate(entityState);
|
||||
}
|
||||
}, [entityState, onUpdate]);
|
||||
|
||||
// Subscribe to entity updates
|
||||
// Including isConnected as a dependency ensures we resubscribe on reconnection
|
||||
useEffect(() => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user