feat: Update dashboard canvas background color and refactor file handling in DashboardAPI

This commit is contained in:
Nawaz Dhandala
2026-03-26 21:41:22 +00:00
parent bec1c760ca
commit 72a796c03d
5 changed files with 362 additions and 353 deletions

View File

@@ -44,8 +44,7 @@ const BlankCanvasElement: FunctionComponent<ComponentProps> = (
No widgets yet
</h3>
<p className="text-sm text-gray-400 max-w-sm mx-auto">
Click <strong className="text-gray-500">Edit</strong> to start adding
charts, values, gauges, and more to this dashboard.
This dashboard does not have any widgets.
</p>
</div>
);

View File

@@ -1,7 +1,6 @@
import React, {
FunctionComponent,
ReactElement,
useCallback,
useEffect,
useRef,
useState,
@@ -59,286 +58,319 @@ export interface ComponentProps extends DashboardBaseComponentProps {
onClick: () => void;
}
type InteractionMode =
| "idle"
| "moving"
| "resizing-width"
| "resizing-height"
| "resizing-corner";
interface DragState {
mode: InteractionMode;
// ────────────────────────────────────────────────────────────
// All mutable drag/resize state lives here, outside React.
// Nothing in this struct triggers a re-render.
// ────────────────────────────────────────────────────────────
interface DragSession {
mode: "move" | "resize-w" | "resize-h" | "resize-corner";
startMouseX: number;
startMouseY: number;
startComponentTop: number;
startComponentLeft: number;
startComponentWidth: number;
startComponentHeight: number;
// Snapped values at the START of the gesture (dashboard units)
originTop: number;
originLeft: number;
originWidth: number;
originHeight: number;
// Live snapped values (updated every mousemove, used on commit)
liveTop: number;
liveLeft: number;
liveWidth: number;
liveHeight: number;
}
const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
// ── Derived data ──────────────────────────────────────────
const component: DashboardBaseComponent =
props.dashboardViewConfig.components.find(
(c: DashboardBaseComponent) => {
return c.componentId.toString() === props.componentId.toString();
},
(c: DashboardBaseComponent) =>
c.componentId.toString() === props.componentId.toString(),
) as DashboardBaseComponent;
const widthOfComponent: number = component.widthInDashboardUnits;
const heightOfComponent: number = component.heightInDashboardUnits;
const [interactionMode, setInteractionMode] =
useState<InteractionMode>("idle");
// ── Minimal React state (only for hover gating) ───────────
const [isHovered, setIsHovered] = useState<boolean>(false);
// We track "is dragging" in a ref so the mousemove handler never
// depends on React state. A *second* copy in useState lets the
// JSX read it for className changes on mount/unmount of the drag.
const [isDragging, setIsDragging] = useState<boolean>(false);
// Refs to hold mutable values the mouse handler reads.
// This avoids recreating the handler (and removing/re-adding listeners) on
// every render, which was causing the flicker.
const dragStateRef: React.MutableRefObject<DragState | null> =
useRef<DragState | null>(null);
const componentRef: React.MutableRefObject<DashboardBaseComponent> =
useRef<DashboardBaseComponent>(component);
const propsRef: React.MutableRefObject<ComponentProps> =
useRef<ComponentProps>(props);
const dashboardComponentRef: React.RefObject<HTMLDivElement> =
// ── Refs ──────────────────────────────────────────────────
const elRef: React.RefObject<HTMLDivElement> =
useRef<HTMLDivElement>(null);
const tooltipRef: React.RefObject<HTMLDivElement> =
useRef<HTMLDivElement>(null);
const sessionRef: React.MutableRefObject<DragSession | null> =
useRef<DragSession | null>(null);
// Keep latest props/component available for the imperative handlers.
const latestProps: React.MutableRefObject<ComponentProps> =
useRef<ComponentProps>(props);
const latestComponent: React.MutableRefObject<DashboardBaseComponent> =
useRef<DashboardBaseComponent>(component);
latestProps.current = props;
latestComponent.current = component;
// Keep refs in sync with latest values on every render.
componentRef.current = component;
propsRef.current = props;
const isDraggingOrResizing: boolean = interactionMode !== "idle";
// Stable handler — never recreated. Reads everything from refs.
const handleMouseMove: (event: MouseEvent) => void = useCallback(
(event: MouseEvent): void => {
const state: DragState | null = dragStateRef.current;
if (!state) {
return;
}
const currentComponent: DashboardBaseComponent = componentRef.current;
const currentProps: ComponentProps = propsRef.current;
const unitPx: number = GetDashboardUnitWidthInPx(
currentProps.totalCurrentDashboardWidthInPx,
);
const deltaXInPx: number = event.clientX - state.startMouseX;
const deltaYInPx: number = event.clientY - state.startMouseY;
if (state.mode === "moving") {
const deltaXUnits: number = Math.round(deltaXInPx / unitPx);
const deltaYUnits: number = Math.round(deltaYInPx / unitPx);
let newTop: number = state.startComponentTop + deltaYUnits;
let newLeft: number = state.startComponentLeft + deltaXUnits;
// Clamp to bounds
const maxLeft: number =
DefaultDashboardSize.widthInDashboardUnits -
currentComponent.widthInDashboardUnits;
const maxTop: number =
currentProps.dashboardViewConfig.heightInDashboardUnits -
currentComponent.heightInDashboardUnits;
if (newTop > maxTop) {
newTop = maxTop;
}
if (newLeft > maxLeft) {
newLeft = maxLeft;
}
if (newTop < 0) {
newTop = 0;
}
if (newLeft < 0) {
newLeft = 0;
}
// Only update if position actually changed
if (
newTop !== currentComponent.topInDashboardUnits ||
newLeft !== currentComponent.leftInDashboardUnits
) {
currentProps.onComponentUpdate({
...currentComponent,
topInDashboardUnits: newTop,
leftInDashboardUnits: newLeft,
});
}
} else if (state.mode === "resizing-width") {
if (!dashboardComponentRef.current) {
return;
}
const rect: DOMRect =
dashboardComponentRef.current.getBoundingClientRect();
const newWidthPx: number = event.pageX - (window.scrollX + rect.left);
let widthUnits: number = GetDashboardComponentWidthInDashboardUnits(
currentProps.totalCurrentDashboardWidthInPx,
Math.max(newWidthPx, unitPx),
);
if (widthUnits < currentComponent.minWidthInDashboardUnits) {
widthUnits = currentComponent.minWidthInDashboardUnits;
}
if (widthUnits > DefaultDashboardSize.widthInDashboardUnits) {
widthUnits = DefaultDashboardSize.widthInDashboardUnits;
}
if (widthUnits !== currentComponent.widthInDashboardUnits) {
currentProps.onComponentUpdate({
...currentComponent,
widthInDashboardUnits: widthUnits,
});
}
} else if (state.mode === "resizing-height") {
if (!dashboardComponentRef.current) {
return;
}
const rect: DOMRect =
dashboardComponentRef.current.getBoundingClientRect();
const newHeightPx: number = event.pageY - (window.scrollY + rect.top);
let heightUnits: number = GetDashboardComponentHeightInDashboardUnits(
currentProps.totalCurrentDashboardWidthInPx,
Math.max(newHeightPx, unitPx),
);
if (heightUnits < currentComponent.minHeightInDashboardUnits) {
heightUnits = currentComponent.minHeightInDashboardUnits;
}
if (heightUnits !== currentComponent.heightInDashboardUnits) {
currentProps.onComponentUpdate({
...currentComponent,
heightInDashboardUnits: heightUnits,
});
}
} else if (state.mode === "resizing-corner") {
if (!dashboardComponentRef.current) {
return;
}
const rect: DOMRect =
dashboardComponentRef.current.getBoundingClientRect();
const newWidthPx: number = event.pageX - (window.scrollX + rect.left);
const newHeightPx: number = event.pageY - (window.scrollY + rect.top);
let widthUnits: number = GetDashboardComponentWidthInDashboardUnits(
currentProps.totalCurrentDashboardWidthInPx,
Math.max(newWidthPx, unitPx),
);
let heightUnits: number = GetDashboardComponentHeightInDashboardUnits(
currentProps.totalCurrentDashboardWidthInPx,
Math.max(newHeightPx, unitPx),
);
if (widthUnits < currentComponent.minWidthInDashboardUnits) {
widthUnits = currentComponent.minWidthInDashboardUnits;
}
if (widthUnits > DefaultDashboardSize.widthInDashboardUnits) {
widthUnits = DefaultDashboardSize.widthInDashboardUnits;
}
if (heightUnits < currentComponent.minHeightInDashboardUnits) {
heightUnits = currentComponent.minHeightInDashboardUnits;
}
if (
widthUnits !== currentComponent.widthInDashboardUnits ||
heightUnits !== currentComponent.heightInDashboardUnits
) {
currentProps.onComponentUpdate({
...currentComponent,
widthInDashboardUnits: widthUnits,
heightInDashboardUnits: heightUnits,
});
}
}
},
[], // No dependencies — reads from refs
// ── Pixel helpers ─────────────────────────────────────────
const unitW: number = GetDashboardUnitWidthInPx(
props.totalCurrentDashboardWidthInPx,
);
const unitH: number = GetDashboardUnitHeightInPx(
props.totalCurrentDashboardWidthInPx,
);
// Stable handler — never recreated.
const handleMouseUp: () => void = useCallback((): void => {
dragStateRef.current = null;
setInteractionMode("idle");
document.body.style.cursor = "";
document.body.style.userSelect = "";
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
}, [handleMouseMove]);
// ── Core imperative handlers (stable — no deps) ──────────
// Clean up listeners if the component unmounts mid-drag.
useEffect(() => {
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};
}, [handleMouseMove, handleMouseUp]);
const startInteraction: (
event: React.MouseEvent,
mode: InteractionMode,
) => void = (event: React.MouseEvent, mode: InteractionMode): void => {
event.preventDefault();
event.stopPropagation();
const currentComponent: DashboardBaseComponent = componentRef.current;
dragStateRef.current = {
mode,
startMouseX: event.clientX,
startMouseY: event.clientY,
startComponentTop: currentComponent.topInDashboardUnits,
startComponentLeft: currentComponent.leftInDashboardUnits,
startComponentWidth: currentComponent.widthInDashboardUnits,
startComponentHeight: currentComponent.heightInDashboardUnits,
};
setInteractionMode(mode);
// Attach listeners directly — not via useEffect.
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
document.body.style.userSelect = "none";
if (mode === "moving") {
document.body.style.cursor = "grabbing";
} else if (mode === "resizing-width") {
document.body.style.cursor = "ew-resize";
} else if (mode === "resizing-height") {
document.body.style.cursor = "ns-resize";
} else if (mode === "resizing-corner") {
document.body.style.cursor = "nwse-resize";
function updateTooltip(session: DragSession): void {
if (!tooltipRef.current) {
return;
}
if (session.mode === "move") {
tooltipRef.current.textContent = `${session.liveLeft}, ${session.liveTop}`;
} else {
tooltipRef.current.textContent = `${session.liveWidth} \u00d7 ${session.liveHeight}`;
}
};
// Build class name
let className: string = `relative rounded-lg col-span-${widthOfComponent} row-span-${heightOfComponent} bg-white border overflow-hidden`;
if (isDraggingOrResizing) {
className += " z-50 shadow-2xl ring-2 ring-blue-400/40";
} else if (props.isSelected && props.isEditMode) {
className += " border-blue-400 ring-2 ring-blue-100 shadow-lg z-10";
} else if (props.isEditMode && isHovered) {
className +=
" border-blue-300 shadow-md z-10 cursor-pointer";
} else if (props.isEditMode) {
className +=
" border-gray-200 hover:border-blue-300 hover:shadow-md cursor-pointer transition-all duration-200";
} else {
className +=
" border-gray-200 hover:shadow-md transition-shadow duration-200";
}
function onMouseMove(e: MouseEvent): void {
const s: DragSession | null = sessionRef.current;
if (!s) {
return;
}
const p: ComponentProps = latestProps.current;
const c: DashboardBaseComponent = latestComponent.current;
const uW: number = GetDashboardUnitWidthInPx(
p.totalCurrentDashboardWidthInPx,
);
const uH: number = GetDashboardUnitHeightInPx(
p.totalCurrentDashboardWidthInPx,
);
const dxPx: number = e.clientX - s.startMouseX;
const dyPx: number = e.clientY - s.startMouseY;
const el: HTMLDivElement | null = elRef.current;
if (!el) {
return;
}
if (s.mode === "move") {
// Pure CSS transform — no React render
el.style.transform = `translate(${dxPx}px, ${dyPx}px) scale(1.01)`;
el.style.zIndex = "100";
// Compute snapped grid position for the tooltip & commit
const dxUnits: number = Math.round(dxPx / uW);
const dyUnits: number = Math.round(dyPx / uH);
let newLeft: number = s.originLeft + dxUnits;
let newTop: number = s.originTop + dyUnits;
const maxLeft: number =
DefaultDashboardSize.widthInDashboardUnits - c.widthInDashboardUnits;
const maxTop: number =
p.dashboardViewConfig.heightInDashboardUnits -
c.heightInDashboardUnits;
newLeft = Math.max(0, Math.min(newLeft, maxLeft));
newTop = Math.max(0, Math.min(newTop, maxTop));
s.liveLeft = newLeft;
s.liveTop = newTop;
updateTooltip(s);
} else {
// Resize modes — directly set width / height on the DOM element
const rect: DOMRect = el.getBoundingClientRect();
if (s.mode === "resize-w" || s.mode === "resize-corner") {
const wPx: number = Math.max(uW, e.pageX - (window.scrollX + rect.left));
let wUnits: number = GetDashboardComponentWidthInDashboardUnits(
p.totalCurrentDashboardWidthInPx,
wPx,
);
wUnits = Math.max(c.minWidthInDashboardUnits, wUnits);
wUnits = Math.min(DefaultDashboardSize.widthInDashboardUnits, wUnits);
s.liveWidth = wUnits;
const newWidthPx: number =
uW * wUnits + (SpaceBetweenUnitsInPx - 2) * (wUnits - 1);
el.style.width = `${newWidthPx}px`;
}
if (s.mode === "resize-h" || s.mode === "resize-corner") {
const hPx: number = Math.max(uH, e.pageY - (window.scrollY + rect.top));
let hUnits: number = GetDashboardComponentHeightInDashboardUnits(
p.totalCurrentDashboardWidthInPx,
hPx,
);
hUnits = Math.max(c.minHeightInDashboardUnits, hUnits);
s.liveHeight = hUnits;
const newHeightPx: number =
uH * hUnits + SpaceBetweenUnitsInPx * (hUnits - 1);
el.style.height = `${newHeightPx}px`;
}
updateTooltip(s);
}
}
function onMouseUp(): void {
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
document.body.style.cursor = "";
document.body.style.userSelect = "";
const s: DragSession | null = sessionRef.current;
const el: HTMLDivElement | null = elRef.current;
if (el) {
el.style.transform = "";
el.style.zIndex = "";
// Width/height are cleared so React's values take over after commit
el.style.width = "";
el.style.height = "";
}
sessionRef.current = null;
setIsDragging(false);
if (!s) {
return;
}
const c: DashboardBaseComponent = latestComponent.current;
const p: ComponentProps = latestProps.current;
// Build the final component — only the fields that changed
const updated: DashboardBaseComponent = { ...c };
let changed: boolean = false;
if (s.mode === "move") {
if (
s.liveTop !== c.topInDashboardUnits ||
s.liveLeft !== c.leftInDashboardUnits
) {
updated.topInDashboardUnits = s.liveTop;
updated.leftInDashboardUnits = s.liveLeft;
changed = true;
}
} else {
if (s.liveWidth !== c.widthInDashboardUnits) {
updated.widthInDashboardUnits = s.liveWidth;
changed = true;
}
if (s.liveHeight !== c.heightInDashboardUnits) {
updated.heightInDashboardUnits = s.liveHeight;
changed = true;
}
}
if (changed) {
p.onComponentUpdate(updated);
}
}
// Clean up if component unmounts while dragging
useEffect(() => {
return () => {
window.removeEventListener("mousemove", onMouseMove);
window.removeEventListener("mouseup", onMouseUp);
};
}, []);
// ── Start a drag / resize session ─────────────────────────
function startSession(
e: React.MouseEvent,
mode: DragSession["mode"],
): void {
e.preventDefault();
e.stopPropagation();
const c: DashboardBaseComponent = latestComponent.current;
const session: DragSession = {
mode,
startMouseX: e.clientX,
startMouseY: e.clientY,
originTop: c.topInDashboardUnits,
originLeft: c.leftInDashboardUnits,
originWidth: c.widthInDashboardUnits,
originHeight: c.heightInDashboardUnits,
liveTop: c.topInDashboardUnits,
liveLeft: c.leftInDashboardUnits,
liveWidth: c.widthInDashboardUnits,
liveHeight: c.heightInDashboardUnits,
};
sessionRef.current = session;
setIsDragging(true);
// Show initial tooltip value
updateTooltip(session);
window.addEventListener("mousemove", onMouseMove);
window.addEventListener("mouseup", onMouseUp);
document.body.style.userSelect = "none";
if (mode === "move") {
document.body.style.cursor = "grabbing";
} else if (mode === "resize-w") {
document.body.style.cursor = "ew-resize";
} else if (mode === "resize-h") {
document.body.style.cursor = "ns-resize";
} else {
document.body.style.cursor = "nwse-resize";
}
}
// ── Styling ───────────────────────────────────────────────
const showHandles: boolean =
props.isEditMode && (props.isSelected || isHovered);
props.isEditMode && (props.isSelected || isHovered || isDragging);
let borderClass: string = "border-gray-200";
let extraClass: string = "";
if (isDragging) {
borderClass = "border-blue-400";
extraClass = "ring-2 ring-blue-400/40 shadow-2xl";
} else if (props.isSelected && props.isEditMode) {
borderClass = "border-blue-400";
extraClass = "ring-2 ring-blue-100 shadow-lg z-10";
} else if (props.isEditMode && isHovered) {
borderClass = "border-blue-300";
extraClass = "shadow-md z-10 cursor-pointer";
} else if (props.isEditMode) {
extraClass =
"hover:border-blue-300 hover:shadow-md cursor-pointer transition-all duration-200";
} else {
extraClass = "hover:shadow-md transition-shadow duration-200";
}
const className: string = [
"relative rounded-lg bg-white border overflow-hidden",
`col-span-${widthOfComponent} row-span-${heightOfComponent}`,
borderClass,
extraClass,
].join(" ");
// ── Computed sizes (React-controlled, used when NOT dragging) ──
const componentHeight: number =
unitH * heightOfComponent +
SpaceBetweenUnitsInPx * (heightOfComponent - 1);
const componentWidth: number =
unitW * widthOfComponent +
(SpaceBetweenUnitsInPx - 2) * (widthOfComponent - 1);
// ── Render ────────────────────────────────────────────────
const getMoveHandle: GetReactElementFunction = (): ReactElement => {
if (!showHandles) {
return <></>;
}
return (
<div
className="absolute top-0 left-0 right-0 z-20 flex items-center justify-center cursor-grab active:cursor-grabbing"
@@ -348,13 +380,10 @@ const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
"linear-gradient(180deg, rgba(59,130,246,0.08) 0%, rgba(59,130,246,0.02) 100%)",
borderBottom: "1px solid rgba(59,130,246,0.12)",
}}
onMouseDown={(
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
) => {
startInteraction(event, "moving");
onMouseDown={(e: React.MouseEvent) => {
startSession(e, "move");
}}
>
{/* Grip dots pattern */}
<div className="flex items-center gap-0.5 opacity-40 hover:opacity-70 transition-opacity">
<svg width="20" height="10" viewBox="0 0 20 10" fill="none">
<circle cx="4" cy="3" r="1.2" fill="#3b82f6" />
@@ -373,7 +402,6 @@ const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
if (!showHandles) {
return <></>;
}
return (
<div
className="absolute z-20 group"
@@ -384,10 +412,8 @@ const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
width: "8px",
cursor: "ew-resize",
}}
onMouseDown={(
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
) => {
startInteraction(event, "resizing-width");
onMouseDown={(e: React.MouseEvent) => {
startSession(e, "resize-w");
}}
>
<div
@@ -406,7 +432,6 @@ const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
if (!showHandles) {
return <></>;
}
return (
<div
className="absolute z-20 group"
@@ -417,10 +442,8 @@ const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
height: "8px",
cursor: "ns-resize",
}}
onMouseDown={(
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
) => {
startInteraction(event, "resizing-height");
onMouseDown={(e: React.MouseEvent) => {
startSession(e, "resize-h");
}}
>
<div
@@ -439,7 +462,6 @@ const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
if (!showHandles) {
return <></>;
}
return (
<div
className="absolute z-30 group"
@@ -450,14 +472,12 @@ const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
height: "16px",
cursor: "nwse-resize",
}}
onMouseDown={(
event: React.MouseEvent<HTMLDivElement, MouseEvent>,
) => {
startInteraction(event, "resizing-corner");
onMouseDown={(e: React.MouseEvent) => {
startSession(e, "resize-corner");
}}
>
<div
className="absolute bottom-1 right-1 transition-all duration-150"
className="absolute bottom-1 right-1"
style={{
width: "8px",
height: "8px",
@@ -470,53 +490,6 @@ const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
);
};
// Size tooltip during resize
const getSizeTooltip: GetReactElementFunction = (): ReactElement => {
if (!isDraggingOrResizing) {
return <></>;
}
let label: string = "";
if (interactionMode === "moving") {
label = `${component.leftInDashboardUnits}, ${component.topInDashboardUnits}`;
} else {
label = `${component.widthInDashboardUnits} \u00d7 ${component.heightInDashboardUnits}`;
}
return (
<div
className="absolute z-50 pointer-events-none"
style={{
top: "-32px",
left: "50%",
transform: "translateX(-50%)",
}}
>
<div
className="px-2 py-1 rounded-md text-xs font-mono font-medium text-white whitespace-nowrap"
style={{
background: "rgba(30, 41, 59, 0.9)",
backdropFilter: "blur(4px)",
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
}}
>
{label}
</div>
</div>
);
};
const componentHeight: number =
GetDashboardUnitHeightInPx(props.totalCurrentDashboardWidthInPx) *
heightOfComponent +
SpaceBetweenUnitsInPx * (heightOfComponent - 1);
const componentWidth: number =
GetDashboardUnitWidthInPx(props.totalCurrentDashboardWidthInPx) *
widthOfComponent +
(SpaceBetweenUnitsInPx - 2) * (widthOfComponent - 1);
return (
<div
className={className}
@@ -524,20 +497,19 @@ const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
margin: `${MarginForEachUnitInPx}px`,
height: `${componentHeight}px`,
width: `${componentWidth}px`,
boxShadow: isDraggingOrResizing
? "0 20px 40px -8px rgba(59, 130, 246, 0.15), 0 8px 16px -4px rgba(0, 0, 0, 0.08)"
boxShadow: isDragging
? "0 20px 40px -8px rgba(59,130,246,0.15), 0 8px 16px -4px rgba(0,0,0,0.08)"
: props.isSelected && props.isEditMode
? "0 4px 12px -2px rgba(59, 130, 246, 0.12), 0 2px 4px -1px rgba(0, 0, 0, 0.04)"
: "0 1px 3px 0 rgba(0, 0, 0, 0.04), 0 1px 2px -1px rgba(0, 0, 0, 0.03)",
transform: isDraggingOrResizing ? "scale(1.01)" : "scale(1)",
transition: isDraggingOrResizing
? "0 4px 12px -2px rgba(59,130,246,0.12), 0 2px 4px -1px rgba(0,0,0,0.04)"
: "0 1px 3px 0 rgba(0,0,0,0.04), 0 1px 2px -1px rgba(0,0,0,0.03)",
// transition is disabled during drag so the transform is instant
transition: isDragging
? "none"
: "box-shadow 0.2s ease, transform 0.15s ease, border-color 0.2s ease",
: "box-shadow 0.2s ease, border-color 0.2s ease",
}}
key={component.componentId?.toString() || Math.random().toString()}
ref={dashboardComponentRef}
ref={elRef}
onClick={(e: React.MouseEvent) => {
if (!isDraggingOrResizing) {
if (!isDragging) {
props.onClick();
}
e.stopPropagation();
@@ -546,16 +518,36 @@ const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
setIsHovered(true);
}}
onMouseLeave={() => {
if (!isDraggingOrResizing) {
if (!isDragging) {
setIsHovered(false);
}
}}
>
{getMoveHandle()}
{getSizeTooltip()}
{/* Tooltip — updated imperatively via ref, never causes a render */}
<div
className="absolute z-50 pointer-events-none"
style={{
top: "-32px",
left: "50%",
transform: "translateX(-50%)",
display: isDragging ? "block" : "none",
}}
>
<div
ref={tooltipRef}
className="px-2 py-1 rounded-md text-xs font-mono font-medium text-white whitespace-nowrap"
style={{
background: "rgba(30, 41, 59, 0.9)",
backdropFilter: "blur(4px)",
boxShadow: "0 2px 8px rgba(0,0,0,0.15)",
}}
/>
</div>
{/* Component type badge */}
{props.isEditMode && (props.isSelected || isHovered) && (
{props.isEditMode && (props.isSelected || isHovered) && !isDragging && (
<div
className="absolute z-10 pointer-events-none"
style={{
@@ -568,7 +560,6 @@ const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
style={{
background: "rgba(241, 245, 249, 0.9)",
color: "#64748b",
backdropFilter: "blur(4px)",
}}
>
{component.componentType}
@@ -576,7 +567,7 @@ const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
</div>
)}
{/* Component content area */}
{/* Component content */}
<div
className="w-full h-full"
style={{

View File

@@ -407,7 +407,7 @@ const DashboardViewer: FunctionComponent<ComponentProps> = (
setDashboardViewConfig(newDashboardConfig);
}}
/>
<div ref={dashboardCanvasRef} className="px-1 pb-4">
<div ref={dashboardCanvasRef} className="px-1 pb-4" style={{ background: "#ffffff" }}>
<DashboardCanvas
dashboardViewConfig={dashboardViewConfig}
onDashboardViewConfigChange={(newConfig: DashboardViewConfig) => {

View File

@@ -361,7 +361,7 @@ const DashboardViewPage: FunctionComponent<ComponentProps> = (
</div>
{/* Dashboard Canvas */}
<div ref={dashboardCanvasRef} className="mt-3">
<div ref={dashboardCanvasRef} className="mt-3" style={{ background: "#ffffff" }}>
<DashboardCanvas
dashboardViewConfig={dashboardViewConfig}
onDashboardViewConfigChange={(_config: DashboardViewConfig) => {

View File

@@ -212,16 +212,8 @@ export default class DashboardAPI extends BaseAPI<
enableMasterPassword: dashboard.enableMasterPassword || false,
pageTitle: dashboard.pageTitle || "",
pageDescription: dashboard.pageDescription || "",
logoFile: dashboard.logoFile
? JSONFunctions.serialize(
dashboard.logoFile as any,
)
: null,
faviconFile: dashboard.faviconFile
? JSONFunctions.serialize(
dashboard.faviconFile as any,
)
: null,
logoFile: DashboardAPI.getFileAsBase64JSONObject(dashboard.logoFile),
faviconFile: DashboardAPI.getFileAsBase64JSONObject(dashboard.faviconFile),
});
} catch (err) {
next(err);
@@ -287,11 +279,7 @@ export default class DashboardAPI extends BaseAPI<
description: dashboard.description || "",
pageTitle: dashboard.pageTitle || "",
pageDescription: dashboard.pageDescription || "",
logoFile: dashboard.logoFile
? JSONFunctions.serialize(
dashboard.logoFile as any,
)
: null,
logoFile: DashboardAPI.getFileAsBase64JSONObject(dashboard.logoFile),
dashboardViewConfig: dashboard.dashboardViewConfig
? JSONFunctions.serialize(dashboard.dashboardViewConfig as any)
: null,
@@ -378,4 +366,35 @@ export default class DashboardAPI extends BaseAPI<
},
);
}
private static getFileAsBase64JSONObject(
file: any,
): { file: string; fileType: string } | null {
if (!file || !file.file) {
return null;
}
let base64: string;
const fileBuffer: any = file.file;
if (Buffer.isBuffer(fileBuffer)) {
base64 = fileBuffer.toString("base64");
} else if (
fileBuffer &&
typeof fileBuffer === "object" &&
fileBuffer.value &&
fileBuffer.value.data
) {
base64 = Buffer.from(fileBuffer.value.data).toString("base64");
} else if (typeof fileBuffer === "string") {
base64 = fileBuffer;
} else {
return null;
}
return {
file: base64,
fileType: (file.fileType as string) || "image/png",
};
}
}