feat: Implement master password functionality for dashboards and status pages

- Added modal for setting and updating master passwords in DashboardAuthenticationSettings and StatusPageDelete components.
- Updated UI elements to reflect the master password status and provide appropriate actions.
- Enhanced descriptions to clarify the security implications of the master password feature.
- Refactored API calls in DashboardAPI to simplify logo and favicon file handling.
- Updated OpenTelemetry profiles documentation to include new message structures and important migration notes.
This commit is contained in:
Nawaz Dhandala
2026-03-26 21:28:59 +00:00
parent 50717e5167
commit b939b4ebf0
7 changed files with 678 additions and 450 deletions

View File

@@ -24,7 +24,7 @@ const BlankDashboardUnitElement: FunctionComponent<ComponentProps> = (
if (props.isEditMode) {
className +=
" border border-dashed border-gray-200 rounded-md hover:border-gray-300 hover:bg-blue-50/30 cursor-pointer";
" rounded-md cursor-pointer";
}
return (
@@ -38,6 +38,8 @@ const BlankDashboardUnitElement: FunctionComponent<ComponentProps> = (
width: widthOfUnitInPx + "px",
height: heightOfUnitInPx + "px",
margin: MarginForEachUnitInPx + "px",
border: props.isEditMode ? "1px dashed rgba(203, 213, 225, 0.5)" : "none",
borderRadius: "6px",
}}
></div>
);

View File

@@ -139,9 +139,7 @@ const DashboardCanvas: FunctionComponent<ComponentProps> = (
const width: number = DefaultDashboardSize.widthInDashboardUnits;
const canvasClassName: string = props.isEditMode
? `grid grid-cols-${width}`
: `grid grid-cols-${width}`;
const canvasClassName: string = `grid grid-cols-${width}`;
return (
<div
@@ -151,11 +149,15 @@ const DashboardCanvas: FunctionComponent<ComponentProps> = (
props.isEditMode
? {
backgroundImage:
"radial-gradient(circle, #d1d5db 0.8px, transparent 0.8px)",
"radial-gradient(circle, rgba(148, 163, 184, 0.3) 0.8px, transparent 0.8px)",
backgroundSize: "20px 20px",
borderRadius: "8px",
borderRadius: "12px",
padding: "4px",
border: "1px dashed rgba(148, 163, 184, 0.25)",
}
: {
padding: "4px",
}
: {}
}
>
{finalRenderedComponents}

View File

@@ -1,4 +1,11 @@
import React, { FunctionComponent, ReactElement, useEffect } from "react";
import React, {
FunctionComponent,
ReactElement,
useCallback,
useEffect,
useRef,
useState,
} from "react";
import DashboardTextComponentType from "Common/Types/Dashboard/DashboardComponents/DashboardTextComponent";
import DashboardChartComponentType from "Common/Types/Dashboard/DashboardComponents/DashboardChartComponent";
import DashboardValueComponentType from "Common/Types/Dashboard/DashboardComponents/DashboardValueComponent";
@@ -52,6 +59,17 @@ export interface ComponentProps extends DashboardBaseComponentProps {
onClick: () => void;
}
type InteractionMode = "idle" | "moving" | "resizing-width" | "resizing-height" | "resizing-corner";
interface DragState {
startMouseX: number;
startMouseY: number;
startComponentTop: number;
startComponentLeft: number;
startComponentWidth: number;
startComponentHeight: number;
}
const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
@@ -67,403 +85,548 @@ const DashboardBaseComponentElement: FunctionComponent<ComponentProps> = (
const widthOfComponent: number = component.widthInDashboardUnits;
const heightOfComponent: number = component.heightInDashboardUnits;
const [topInPx, setTopInPx] = React.useState<number>(0);
const [leftInPx, setLeftInPx] = React.useState<number>(0);
let className: string = `relative rounded-lg col-span-${widthOfComponent} row-span-${heightOfComponent} p-3 bg-white border border-gray-200 transition-all duration-200 overflow-hidden`;
if (props.isEditMode && !props.isSelected) {
className += " cursor-pointer hover:border-gray-300 hover:shadow-md";
}
if (props.isSelected && props.isEditMode) {
className +=
" !border-blue-400 ring-2 ring-blue-50 shadow-lg shadow-blue-100/50";
}
if (!props.isEditMode) {
className += " hover:shadow-md";
}
const [interactionMode, setInteractionMode] = useState<InteractionMode>("idle");
const [isHovered, setIsHovered] = useState<boolean>(false);
const dragStateRef: React.MutableRefObject<DragState | null> = useRef<DragState | null>(null);
const dashboardComponentRef: React.RefObject<HTMLDivElement> =
React.useRef<HTMLDivElement>(null);
useRef<HTMLDivElement>(null);
const refreshTopAndLeftInPx: () => void = () => {
if (dashboardComponentRef.current === null) {
return;
const isDraggingOrResizing: boolean = interactionMode !== "idle";
const eachDashboardUnitInPx: number = GetDashboardUnitWidthInPx(
props.totalCurrentDashboardWidthInPx,
);
const clampPosition: (data: {
top: number;
left: number;
width: number;
height: number;
}) => { top: number; left: number } = useCallback((data: {
top: number;
left: number;
width: number;
height: number;
}): { top: number; left: number } => {
let newTop: number = data.top;
let newLeft: number = data.left;
const maxLeft: number = DefaultDashboardSize.widthInDashboardUnits - data.width;
const maxTop: number = props.dashboardViewConfig.heightInDashboardUnits - data.height;
if (newTop > maxTop) {
newTop = maxTop;
}
if (newLeft > maxLeft) {
newLeft = maxLeft;
}
if (newTop < 0) {
newTop = 0;
}
if (newLeft < 0) {
newLeft = 0;
}
const topInPx: number =
dashboardComponentRef.current.getBoundingClientRect().top;
const leftInPx: number =
dashboardComponentRef.current.getBoundingClientRect().left;
return { top: newTop, left: newLeft };
}, [props.dashboardViewConfig.heightInDashboardUnits]);
setTopInPx(topInPx);
setLeftInPx(leftInPx);
};
const clampSize: (data: {
width: number;
height: number;
}) => { width: number; height: number } = useCallback((data: {
width: number;
height: number;
}): { width: number; height: number } => {
let newWidth: number = data.width;
let newHeight: number = data.height;
if (newWidth < component.minWidthInDashboardUnits) {
newWidth = component.minWidthInDashboardUnits;
}
if (newWidth > DefaultDashboardSize.widthInDashboardUnits) {
newWidth = DefaultDashboardSize.widthInDashboardUnits;
}
if (newHeight < component.minHeightInDashboardUnits) {
newHeight = component.minHeightInDashboardUnits;
}
return { width: newWidth, height: newHeight };
}, [component.minWidthInDashboardUnits, component.minHeightInDashboardUnits]);
const handleMouseMove: (event: MouseEvent) => void = useCallback(
(event: MouseEvent): void => {
if (!dragStateRef.current) {
return;
}
const state: DragState = dragStateRef.current;
const deltaXInPx: number = event.clientX - state.startMouseX;
const deltaYInPx: number = event.clientY - state.startMouseY;
if (interactionMode === "moving") {
const deltaXUnits: number = Math.round(deltaXInPx / eachDashboardUnitInPx);
const deltaYUnits: number = Math.round(deltaYInPx / eachDashboardUnitInPx);
const clamped: { top: number; left: number } = clampPosition({
top: state.startComponentTop + deltaYUnits,
left: state.startComponentLeft + deltaXUnits,
width: component.widthInDashboardUnits,
height: component.heightInDashboardUnits,
});
props.onComponentUpdate({
...component,
topInDashboardUnits: clamped.top,
leftInDashboardUnits: clamped.left,
});
} else if (interactionMode === "resizing-width") {
if (!dashboardComponentRef.current) {
return;
}
const newWidthPx: number =
event.pageX -
(window.scrollX + dashboardComponentRef.current.getBoundingClientRect().left);
let widthUnits: number = GetDashboardComponentWidthInDashboardUnits(
props.totalCurrentDashboardWidthInPx,
newWidthPx,
);
const clamped: { width: number; height: number } = clampSize({
width: widthUnits,
height: component.heightInDashboardUnits,
});
widthUnits = clamped.width;
props.onComponentUpdate({
...component,
widthInDashboardUnits: widthUnits,
});
} else if (interactionMode === "resizing-height") {
if (!dashboardComponentRef.current) {
return;
}
const newHeightPx: number =
event.pageY -
(window.scrollY + dashboardComponentRef.current.getBoundingClientRect().top);
let heightUnits: number = GetDashboardComponentHeightInDashboardUnits(
props.totalCurrentDashboardWidthInPx,
newHeightPx,
);
const clamped: { width: number; height: number } = clampSize({
width: component.widthInDashboardUnits,
height: heightUnits,
});
heightUnits = clamped.height;
props.onComponentUpdate({
...component,
heightInDashboardUnits: heightUnits,
});
} else if (interactionMode === "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(
props.totalCurrentDashboardWidthInPx,
newWidthPx,
);
let heightUnits: number = GetDashboardComponentHeightInDashboardUnits(
props.totalCurrentDashboardWidthInPx,
newHeightPx,
);
const clamped: { width: number; height: number } = clampSize({
width: widthUnits,
height: heightUnits,
});
widthUnits = clamped.width;
heightUnits = clamped.height;
props.onComponentUpdate({
...component,
widthInDashboardUnits: widthUnits,
heightInDashboardUnits: heightUnits,
});
}
},
[interactionMode, eachDashboardUnitInPx, component, clampPosition, clampSize, props],
);
const handleMouseUp: () => void = useCallback((): void => {
dragStateRef.current = null;
setInteractionMode("idle");
document.body.style.cursor = "";
document.body.style.userSelect = "";
}, []);
useEffect(() => {
refreshTopAndLeftInPx();
}, [props.dashboardViewConfig]);
type MoveComponentFunction = (mouseEvent: MouseEvent) => void;
const moveComponent: MoveComponentFunction = (
mouseEvent: MouseEvent,
): void => {
const dashboardComponentOldTopInPx: number = topInPx;
const dashboardComponentOldLeftInPx: number = leftInPx;
const newMoveToTop: number = mouseEvent.clientY;
const newMoveToLeft: number = mouseEvent.clientX;
const deltaXInPx: number = newMoveToLeft - dashboardComponentOldLeftInPx;
const deltaYInPx: number = newMoveToTop - dashboardComponentOldTopInPx;
const eachDashboardUnitInPx: number = GetDashboardUnitWidthInPx(
props.totalCurrentDashboardWidthInPx,
);
const deltaXInDashboardUnits: number = Math.round(
deltaXInPx / eachDashboardUnitInPx,
);
const deltaYInDashboardUnits: number = Math.round(
deltaYInPx / eachDashboardUnitInPx,
);
let newTopInDashboardUnits: number =
component.topInDashboardUnits + deltaYInDashboardUnits;
let newLeftInDashboardUnits: number =
component.leftInDashboardUnits + deltaXInDashboardUnits;
// now make sure these are within the bounds of the dashboard inch component width and height in dashbosrd units
const dahsboardTotalWidthInDashboardUnits: number =
DefaultDashboardSize.widthInDashboardUnits; // width does not change
const dashboardTotalHeightInDashboardUnits: number =
props.dashboardViewConfig.heightInDashboardUnits;
const heightOfTheComponntInDashboardUnits: number =
component.heightInDashboardUnits;
const widthOfTheComponentInDashboardUnits: number =
component.widthInDashboardUnits;
// if it goes outside the bounds then max it out to the bounds
if (
newTopInDashboardUnits + heightOfTheComponntInDashboardUnits >
dashboardTotalHeightInDashboardUnits
) {
newTopInDashboardUnits =
dashboardTotalHeightInDashboardUnits -
heightOfTheComponntInDashboardUnits;
if (interactionMode !== "idle") {
window.addEventListener("mousemove", handleMouseMove);
window.addEventListener("mouseup", handleMouseUp);
document.body.style.userSelect = "none";
}
if (
newLeftInDashboardUnits + widthOfTheComponentInDashboardUnits >
dahsboardTotalWidthInDashboardUnits
) {
newLeftInDashboardUnits =
dahsboardTotalWidthInDashboardUnits -
widthOfTheComponentInDashboardUnits;
}
return () => {
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("mouseup", handleMouseUp);
};
}, [interactionMode, handleMouseMove, handleMouseUp]);
// make sure they are not negative
const startInteraction: (
event: React.MouseEvent,
mode: InteractionMode,
) => void = (event: React.MouseEvent, mode: InteractionMode): void => {
event.preventDefault();
event.stopPropagation();
if (newTopInDashboardUnits < 0) {
newTopInDashboardUnits = 0;
}
if (newLeftInDashboardUnits < 0) {
newLeftInDashboardUnits = 0;
}
// update the component
const newComponentProps: DashboardBaseComponent = {
...component,
topInDashboardUnits: newTopInDashboardUnits,
leftInDashboardUnits: newLeftInDashboardUnits,
dragStateRef.current = {
startMouseX: event.clientX,
startMouseY: event.clientY,
startComponentTop: component.topInDashboardUnits,
startComponentLeft: component.leftInDashboardUnits,
startComponentWidth: component.widthInDashboardUnits,
startComponentHeight: component.heightInDashboardUnits,
};
props.onComponentUpdate(newComponentProps);
setInteractionMode(mode);
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";
}
};
const resizeWidth: (event: MouseEvent) => void = (event: MouseEvent) => {
if (dashboardComponentRef.current === null) {
return;
}
// Build class name
let className: string = `relative rounded-lg col-span-${widthOfComponent} row-span-${heightOfComponent} bg-white border overflow-hidden`;
let newDashboardComponentwidthInPx: number =
event.pageX -
(window.scrollX +
dashboardComponentRef.current.getBoundingClientRect().left);
if (
GetDashboardUnitWidthInPx(props.totalCurrentDashboardWidthInPx) >
newDashboardComponentwidthInPx
) {
newDashboardComponentwidthInPx = GetDashboardUnitWidthInPx(
props.totalCurrentDashboardWidthInPx,
);
}
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";
}
// get this in dashboard units.,
let widthInDashboardUnits: number =
GetDashboardComponentWidthInDashboardUnits(
props.totalCurrentDashboardWidthInPx,
newDashboardComponentwidthInPx,
);
const showHandles: boolean = props.isEditMode && (props.isSelected || isHovered);
// if this width is less than the min width then set it to min width
if (widthInDashboardUnits < component.minWidthInDashboardUnits) {
widthInDashboardUnits = component.minWidthInDashboardUnits;
}
// if its more than the max width of dashboard.
if (widthInDashboardUnits > DefaultDashboardSize.widthInDashboardUnits) {
widthInDashboardUnits = DefaultDashboardSize.widthInDashboardUnits;
}
// update the component
const newComponentProps: DashboardBaseComponent = {
...component,
widthInDashboardUnits: widthInDashboardUnits,
};
props.onComponentUpdate(newComponentProps);
};
const resizeHeight: (event: MouseEvent) => void = (event: MouseEvent) => {
if (dashboardComponentRef.current === null) {
return;
}
let newDashboardComponentHeightInPx: number =
event.pageY -
(window.scrollY +
dashboardComponentRef.current.getBoundingClientRect().top);
if (
GetDashboardUnitHeightInPx(props.totalCurrentDashboardWidthInPx) >
newDashboardComponentHeightInPx
) {
newDashboardComponentHeightInPx = GetDashboardUnitHeightInPx(
props.totalCurrentDashboardWidthInPx,
);
}
// get this in dashboard units
let heightInDashboardUnits: number =
GetDashboardComponentHeightInDashboardUnits(
props.totalCurrentDashboardWidthInPx,
newDashboardComponentHeightInPx,
);
// if this height is less tan the min height then set it to min height
if (heightInDashboardUnits < component.minHeightInDashboardUnits) {
heightInDashboardUnits = component.minHeightInDashboardUnits;
}
// update the component
const newComponentProps: DashboardBaseComponent = {
...component,
heightInDashboardUnits: heightInDashboardUnits,
};
props.onComponentUpdate(newComponentProps);
};
const stopResizeAndMove: () => void = () => {
window.removeEventListener("mousemove", resizeHeight);
window.removeEventListener("mousemove", resizeWidth);
window.removeEventListener("mousemove", moveComponent);
window.removeEventListener("mouseup", stopResizeAndMove);
};
const getResizeWidthElement: GetReactElementFunction = (): ReactElement => {
if (!props.isSelected || !props.isEditMode) {
const getMoveHandle: GetReactElementFunction = (): ReactElement => {
if (!props.isEditMode) {
return <></>;
}
let resizeCursorIcon: string = "cursor-ew-resize";
// if already at min width then change icon to e-resize
if (component.widthInDashboardUnits <= component.minWidthInDashboardUnits) {
resizeCursorIcon = "cursor-e-resize";
}
return (
<div
style={{
top: "calc(50% - 20px)",
right: "-5px",
}}
onMouseDown={(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
event.preventDefault();
window.addEventListener("mousemove", resizeWidth);
window.addEventListener("mouseup", stopResizeAndMove);
}}
className={`resize-width-element ${resizeCursorIcon} absolute right-0 w-1.5 h-10 bg-blue-400 hover:bg-blue-500 rounded-full cursor-pointer transition-colors duration-150 opacity-70 hover:opacity-100`}
></div>
);
};
const getMoveElement: GetReactElementFunction = (): ReactElement => {
// if not selected, then return null
if (!props.isSelected || !props.isEditMode) {
// Full-width top drag bar visible on hover or selection
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"
style={{
top: "-9px",
left: "-9px",
height: "28px",
background: "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)",
}}
key={props.key}
onMouseDown={(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
event.preventDefault();
window.addEventListener("mousemove", moveComponent);
window.addEventListener("mouseup", stopResizeAndMove);
startInteraction(event, "moving");
}}
onMouseUp={() => {
stopResizeAndMove();
}}
className="move-element cursor-move absolute w-4 h-4 bg-blue-400 hover:bg-blue-500 rounded-full cursor-pointer transition-colors duration-150 opacity-70 hover:opacity-100 shadow-sm"
onDragStart={(_event: React.DragEvent<HTMLDivElement>) => {}}
onDragEnd={(_event: React.DragEvent<HTMLDivElement>) => {}}
></div>
>
{/* 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" />
<circle cx="10" cy="3" r="1.2" fill="#3b82f6" />
<circle cx="16" cy="3" r="1.2" fill="#3b82f6" />
<circle cx="4" cy="7" r="1.2" fill="#3b82f6" />
<circle cx="10" cy="7" r="1.2" fill="#3b82f6" />
<circle cx="16" cy="7" r="1.2" fill="#3b82f6" />
</svg>
</div>
</div>
);
};
const getResizeHeightElement: GetReactElementFunction = (): ReactElement => {
if (!props.isSelected || !props.isEditMode) {
const getResizeWidthHandle: GetReactElementFunction = (): ReactElement => {
if (!showHandles) {
return <></>;
}
let resizeCursorIcon: string = "cursor-ns-resize";
// if already at min height then change icon to s-resize
if (
component.heightInDashboardUnits <= component.minHeightInDashboardUnits
) {
resizeCursorIcon = "cursor-s-resize";
}
return (
<div
className="absolute z-20 group"
style={{
bottom: "-5px",
left: "calc(50% - 20px)",
top: "28px",
right: "-4px",
bottom: "4px",
width: "8px",
cursor: "ew-resize",
}}
onMouseDown={(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
event.preventDefault();
window.addEventListener("mousemove", resizeHeight);
window.addEventListener("mouseup", stopResizeAndMove);
startInteraction(event, "resizing-width");
}}
className={`resize-height-element ${resizeCursorIcon} absolute bottom-0 left-0 w-10 h-1.5 bg-blue-400 hover:bg-blue-500 rounded-full cursor-pointer transition-colors duration-150 opacity-70 hover:opacity-100`}
></div>
>
{/* Visible handle bar */}
<div
className="absolute top-1/2 right-0.5 w-1 rounded-full bg-blue-400 group-hover:bg-blue-500 transition-all duration-150"
style={{
height: "32px",
transform: "translateY(-50%)",
opacity: props.isSelected ? 0.8 : 0.5,
}}
/>
</div>
);
};
const getResizeHeightHandle: GetReactElementFunction = (): ReactElement => {
if (!showHandles) {
return <></>;
}
return (
<div
className="absolute z-20 group"
style={{
bottom: "-4px",
left: "4px",
right: "12px",
height: "8px",
cursor: "ns-resize",
}}
onMouseDown={(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
startInteraction(event, "resizing-height");
}}
>
{/* Visible handle bar */}
<div
className="absolute bottom-0.5 left-1/2 h-1 rounded-full bg-blue-400 group-hover:bg-blue-500 transition-all duration-150"
style={{
width: "32px",
transform: "translateX(-50%)",
opacity: props.isSelected ? 0.8 : 0.5,
}}
/>
</div>
);
};
const getResizeCornerHandle: GetReactElementFunction = (): ReactElement => {
if (!showHandles) {
return <></>;
}
return (
<div
className="absolute z-30 group"
style={{
bottom: "-4px",
right: "-4px",
width: "16px",
height: "16px",
cursor: "nwse-resize",
}}
onMouseDown={(event: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
startInteraction(event, "resizing-corner");
}}
>
{/* Corner triangle indicator */}
<div
className="absolute bottom-1 right-1 transition-all duration-150"
style={{
width: "8px",
height: "8px",
borderRight: `2px solid ${props.isSelected ? "rgba(59,130,246,0.8)" : "rgba(59,130,246,0.5)"}`,
borderBottom: `2px solid ${props.isSelected ? "rgba(59,130,246,0.8)" : "rgba(59,130,246,0.5)"}`,
borderRadius: "0 0 2px 0",
}}
/>
</div>
);
};
// 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}
style={{
margin: `${MarginForEachUnitInPx}px`,
height: `${
GetDashboardUnitHeightInPx(props.totalCurrentDashboardWidthInPx) *
heightOfComponent +
SpaceBetweenUnitsInPx * (heightOfComponent - 1)
}px`,
width: `${
GetDashboardUnitWidthInPx(props.totalCurrentDashboardWidthInPx) *
widthOfComponent +
(SpaceBetweenUnitsInPx - 2) * (widthOfComponent - 1)
}px`,
boxShadow:
"0 1px 3px 0 rgba(0, 0, 0, 0.04), 0 1px 2px -1px rgba(0, 0, 0, 0.03)",
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)"
: 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 ? "none" : "box-shadow 0.2s ease, transform 0.15s ease, border-color 0.2s ease",
}}
key={component.componentId?.toString() || Math.random().toString()}
ref={dashboardComponentRef}
onClick={props.onClick}
onClick={(e: React.MouseEvent) => {
if (!isDraggingOrResizing) {
props.onClick();
}
e.stopPropagation();
}}
onMouseEnter={() => {
setIsHovered(true);
}}
onMouseLeave={() => {
setIsHovered(false);
}}
>
{getMoveElement()}
{getMoveHandle()}
{getSizeTooltip()}
{/* Component type badge - visible in edit mode */}
{props.isEditMode && props.isSelected && (
<div className="absolute top-1.5 right-1.5 z-10">
<span className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-gray-100 text-gray-500 capitalize">
{/* Component type badge - visible on hover or selection in edit mode */}
{props.isEditMode && (props.isSelected || isHovered) && (
<div
className="absolute z-10"
style={{
top: showHandles ? "32px" : "6px",
right: "6px",
}}
>
<span
className="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium capitalize"
style={{
background: "rgba(241, 245, 249, 0.9)",
color: "#64748b",
backdropFilter: "blur(4px)",
}}
>
{component.componentType}
</span>
</div>
)}
{component.componentType === DashboardComponentType.Text && (
<DashboardTextComponent
{...props}
isEditMode={props.isEditMode}
isSelected={props.isSelected}
component={component as DashboardTextComponentType}
/>
)}
{component.componentType === DashboardComponentType.Chart && (
<DashboardChartComponent
{...props}
isEditMode={props.isEditMode}
isSelected={props.isSelected}
component={component as DashboardChartComponentType}
/>
)}
{component.componentType === DashboardComponentType.Value && (
<DashboardValueComponent
{...props}
isSelected={props.isSelected}
isEditMode={props.isEditMode}
component={component as DashboardValueComponentType}
/>
)}
{component.componentType === DashboardComponentType.Table && (
<DashboardTableComponent
{...props}
isEditMode={props.isEditMode}
isSelected={props.isSelected}
component={component as DashboardTableComponentType}
/>
)}
{component.componentType === DashboardComponentType.Gauge && (
<DashboardGaugeComponent
{...props}
isEditMode={props.isEditMode}
isSelected={props.isSelected}
component={component as DashboardGaugeComponentType}
/>
)}
{component.componentType === DashboardComponentType.LogStream && (
<DashboardLogStreamComponent
{...props}
isEditMode={props.isEditMode}
isSelected={props.isSelected}
component={component as DashboardLogStreamComponentType}
/>
)}
{component.componentType === DashboardComponentType.TraceList && (
<DashboardTraceListComponent
{...props}
isEditMode={props.isEditMode}
isSelected={props.isSelected}
component={component as DashboardTraceListComponentType}
/>
)}
{/* Component content area */}
<div
className="w-full h-full"
style={{
paddingTop: showHandles ? "28px" : "0px",
padding: showHandles ? "28px 12px 12px 12px" : "12px",
}}
>
{component.componentType === DashboardComponentType.Text && (
<DashboardTextComponent
{...props}
isEditMode={props.isEditMode}
isSelected={props.isSelected}
component={component as DashboardTextComponentType}
/>
)}
{component.componentType === DashboardComponentType.Chart && (
<DashboardChartComponent
{...props}
isEditMode={props.isEditMode}
isSelected={props.isSelected}
component={component as DashboardChartComponentType}
/>
)}
{component.componentType === DashboardComponentType.Value && (
<DashboardValueComponent
{...props}
isSelected={props.isSelected}
isEditMode={props.isEditMode}
component={component as DashboardValueComponentType}
/>
)}
{component.componentType === DashboardComponentType.Table && (
<DashboardTableComponent
{...props}
isEditMode={props.isEditMode}
isSelected={props.isSelected}
component={component as DashboardTableComponentType}
/>
)}
{component.componentType === DashboardComponentType.Gauge && (
<DashboardGaugeComponent
{...props}
isEditMode={props.isEditMode}
isSelected={props.isSelected}
component={component as DashboardGaugeComponentType}
/>
)}
{component.componentType === DashboardComponentType.LogStream && (
<DashboardLogStreamComponent
{...props}
isEditMode={props.isEditMode}
isSelected={props.isSelected}
component={component as DashboardLogStreamComponentType}
/>
)}
{component.componentType === DashboardComponentType.TraceList && (
<DashboardTraceListComponent
{...props}
isEditMode={props.isEditMode}
isSelected={props.isSelected}
component={component as DashboardTraceListComponentType}
/>
)}
</div>
{getResizeWidthElement()}
{getResizeHeightElement()}
{getResizeWidthHandle()}
{getResizeHeightHandle()}
{getResizeCornerHandle()}
</div>
);
};

View File

@@ -7,12 +7,19 @@ import Navigation from "Common/UI/Utils/Navigation";
import Dashboard from "Common/Models/DatabaseModels/Dashboard";
import React, { Fragment, FunctionComponent, ReactElement, useState } from "react";
import DashboardPreviewLink from "./DashboardPreviewLink";
import ModelFormModal from "Common/UI/Components/ModelFormModal/ModelFormModal";
import { FormType } from "Common/UI/Components/Forms/ModelForm";
import { ButtonStyleType } from "Common/UI/Components/Button/Button";
import IconProp from "Common/Types/Icon/IconProp";
const DashboardAuthenticationSettings: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
const [isPublicDashboard, setIsPublicDashboard] = useState<boolean>(false);
const [showPasswordModal, setShowPasswordModal] = useState<boolean>(false);
const [refreshMasterPassword, setRefreshMasterPassword] = useState<boolean>(false);
const [isMasterPasswordSet, setIsMasterPasswordSet] = useState<boolean>(false);
return (
<Fragment>
@@ -60,14 +67,25 @@ const DashboardAuthenticationSettings: FunctionComponent<
<DashboardPreviewLink modelId={modelId} />
<CardModelDetail<Dashboard>
name="Dashboard > Require Master Password"
name="Dashboard > Master Password"
cardProps={{
title: "Require Master Password",
title: "Master Password",
description:
"When enabled, visitors must enter the master password before viewing this public dashboard.",
"When enabled, visitors must enter the master password before viewing this public dashboard. This value is stored as a secure hash and cannot be retrieved.",
buttons: [
{
title: isMasterPasswordSet ? "Update Master Password" : "Set Master Password",
buttonStyle: ButtonStyleType.NORMAL,
onClick: () => {
setShowPasswordModal(true);
},
icon: IconProp.Lock,
},
],
}}
editButtonText="Edit Settings"
isEditable={true}
refresher={refreshMasterPassword}
formFields={[
{
field: {
@@ -93,57 +111,63 @@ const DashboardAuthenticationSettings: FunctionComponent<
title: "Require Master Password",
placeholder: "No",
},
],
modelId: modelId,
}}
/>
<CardModelDetail<Dashboard>
name="Dashboard > Update Master Password"
cardProps={{
title: "Update Master Password",
description:
"Rotate the password required to unlock a public dashboard. This value is stored as a secure hash and cannot be retrieved.",
}}
editButtonText="Update Master Password"
isEditable={true}
formFields={[
{
field: {
masterPassword: true,
},
title: "Master Password",
fieldType: FormFieldSchemaType.Password,
required: false,
placeholder: "Enter a new master password",
description:
"Updating this value immediately replaces the existing master password.",
},
]}
modelDetailProps={{
showDetailsInNumberOfColumns: 1,
modelType: Dashboard,
id: "model-detail-dashboard-master-password",
fields: [
{
title: "Master Password",
fieldType: FieldType.Element,
placeholder: "Hidden",
getElement: (): ReactElement => {
return (
<p className="text-sm text-gray-500">
For security reasons, the current master password is never
displayed. Use the update button to set a new password at
any time.
<p>
{isMasterPasswordSet ? "Password is set." : "Not set."}
</p>
);
},
},
],
modelId: modelId,
onItemLoaded: (item: Dashboard) => {
setIsMasterPasswordSet(Boolean(item.masterPassword));
},
}}
/>
{showPasswordModal && (
<ModelFormModal<Dashboard>
title={isMasterPasswordSet ? "Update Master Password" : "Set Master Password"}
onClose={() => {
setShowPasswordModal(false);
}}
submitButtonText="Save"
onSuccess={() => {
setShowPasswordModal(false);
setRefreshMasterPassword(!refreshMasterPassword);
setIsMasterPasswordSet(true);
}}
name="Dashboard > Master Password"
modelType={Dashboard}
formProps={{
id: "edit-dashboard-master-password-from",
fields: [
{
field: {
masterPassword: true,
},
title: "Master Password",
fieldType: FormFieldSchemaType.Password,
required: true,
placeholder: "Enter a new master password",
description:
"Updating this value immediately replaces the existing master password.",
},
],
name: "Dashboard > Master Password",
formType: FormType.Update,
modelType: Dashboard,
steps: [],
}}
modelIdToEdit={modelId}
/>
)}
<CardModelDetail<Dashboard>
name="Dashboard > IP Whitelist"
cardProps={{

View File

@@ -5,12 +5,19 @@ import CardModelDetail from "Common/UI/Components/ModelDetail/CardModelDetail";
import FieldType from "Common/UI/Components/Types/FieldType";
import Navigation from "Common/UI/Utils/Navigation";
import StatusPage from "Common/Models/DatabaseModels/StatusPage";
import React, { Fragment, FunctionComponent, ReactElement } from "react";
import React, { Fragment, FunctionComponent, ReactElement, useState } from "react";
import ModelFormModal from "Common/UI/Components/ModelFormModal/ModelFormModal";
import { FormType } from "Common/UI/Components/Forms/ModelForm";
import { ButtonStyleType } from "Common/UI/Components/Button/Button";
import IconProp from "Common/Types/Icon/IconProp";
const StatusPageDelete: FunctionComponent<
PageComponentProps
> = (): ReactElement => {
const modelId: ObjectID = Navigation.getLastParamAsObjectID(1);
const [showPasswordModal, setShowPasswordModal] = useState<boolean>(false);
const [refreshMasterPassword, setRefreshMasterPassword] = useState<boolean>(false);
const [isMasterPasswordSet, setIsMasterPasswordSet] = useState<boolean>(false);
return (
<Fragment>
@@ -51,14 +58,25 @@ const StatusPageDelete: FunctionComponent<
/>
<CardModelDetail<StatusPage>
name="Status Page > Require Master Password"
name="Status Page > Master Password"
cardProps={{
title: "Require Master Password",
title: "Master Password",
description:
"When enabled, visitors must enter the master password before viewing a private status page. When master password is enabled, SSO/SCIM and Email + Password authentication are disabled.",
"When enabled, visitors must enter the master password before viewing a private status page. When master password is enabled, SSO/SCIM and Email + Password authentication are disabled. This value is stored as a secure hash and cannot be retrieved.",
buttons: [
{
title: isMasterPasswordSet ? "Update Master Password" : "Set Master Password",
buttonStyle: ButtonStyleType.NORMAL,
onClick: () => {
setShowPasswordModal(true);
},
icon: IconProp.Lock,
},
],
}}
editButtonText="Edit Settings"
isEditable={true}
refresher={refreshMasterPassword}
formFields={[
{
field: {
@@ -84,56 +102,59 @@ const StatusPageDelete: FunctionComponent<
title: "Require Master Password",
placeholder: "No",
},
{
field: {
masterPassword: true,
},
title: "Master Password",
fieldType: FieldType.HiddenText,
placeholder: "Not Set",
},
],
modelId: modelId,
onItemLoaded: (item: StatusPage) => {
setIsMasterPasswordSet(Boolean(item.masterPassword));
},
}}
/>
<CardModelDetail<StatusPage>
name="Status Page > Update Master Password"
cardProps={{
title: "Update Master Password",
description:
"Rotate the password required to unlock a private status page. This value is stored as a secure hash and cannot be retrieved.",
}}
editButtonText="Update Master Password"
isEditable={true}
formFields={[
{
field: {
masterPassword: true,
},
title: "Master Password",
fieldType: FormFieldSchemaType.Password,
required: false,
placeholder: "Enter a new master password",
description:
"Updating this value immediately replaces the existing master password.",
},
]}
modelDetailProps={{
showDetailsInNumberOfColumns: 1,
modelType: StatusPage,
id: "model-detail-status-page-master-password",
fields: [
{
title: "Master Password",
fieldType: FieldType.Element,
placeholder: "Hidden",
getElement: (): ReactElement => {
return (
<p className="text-sm text-gray-500">
For security reasons, the current master password is never
displayed. Use the update button to set a new password at
any time.
</p>
);
{showPasswordModal && (
<ModelFormModal<StatusPage>
title={isMasterPasswordSet ? "Update Master Password" : "Set Master Password"}
onClose={() => {
setShowPasswordModal(false);
}}
submitButtonText="Save"
onSuccess={() => {
setShowPasswordModal(false);
setRefreshMasterPassword(!refreshMasterPassword);
setIsMasterPasswordSet(true);
}}
name="Status Page > Master Password"
modelType={StatusPage}
formProps={{
id: "edit-status-page-master-password-from",
fields: [
{
field: {
masterPassword: true,
},
title: "Master Password",
fieldType: FormFieldSchemaType.Password,
required: true,
placeholder: "Enter a new master password",
description:
"Updating this value immediately replaces the existing master password.",
},
},
],
modelId: modelId,
}}
/>
],
name: "Status Page > Master Password",
formType: FormType.Update,
modelType: StatusPage,
steps: [],
}}
modelIdToEdit={modelId}
/>
)}
<CardModelDetail<StatusPage>
name="Status Page > IP Whitelist"

View File

@@ -18,7 +18,6 @@ import HashedString from "../../Types/HashedString";
import ObjectID from "../../Types/ObjectID";
import Dashboard from "../../Models/DatabaseModels/Dashboard";
import DashboardDomain from "../../Models/DatabaseModels/DashboardDomain";
import File from "../../Models/DatabaseModels/File";
import { EncryptionSecret } from "../EnvironmentConfig";
import { DASHBOARD_MASTER_PASSWORD_INVALID_MESSAGE } from "../../Types/Dashboard/MasterPassword";
import NotAuthenticatedException from "../../Types/Exception/NotAuthenticatedException";
@@ -215,16 +214,12 @@ export default class DashboardAPI extends BaseAPI<
pageDescription: dashboard.pageDescription || "",
logoFile: dashboard.logoFile
? JSONFunctions.serialize(
dashboard.logoFile instanceof File
? (dashboard.logoFile.toJSON() as any)
: (dashboard.logoFile as any),
dashboard.logoFile as any,
)
: null,
faviconFile: dashboard.faviconFile
? JSONFunctions.serialize(
dashboard.faviconFile instanceof File
? (dashboard.faviconFile.toJSON() as any)
: (dashboard.faviconFile as any),
dashboard.faviconFile as any,
)
: null,
});
@@ -294,9 +289,7 @@ export default class DashboardAPI extends BaseAPI<
pageDescription: dashboard.pageDescription || "",
logoFile: dashboard.logoFile
? JSONFunctions.serialize(
dashboard.logoFile instanceof File
? (dashboard.logoFile.toJSON() as any)
: (dashboard.logoFile as any),
dashboard.logoFile as any,
)
: null,
dashboardViewConfig: dashboard.dashboardViewConfig

View File

@@ -53,6 +53,8 @@ Add the profiles proto files to `Telemetry/ProtoFiles/OTel/v1/`:
- `profiles.proto` — Core profiles data model (from `opentelemetry/proto/profiles/v1development/profiles.proto`)
- `profiles_service.proto` — ProfilesService with `Export` RPC
**Important:** The proto package is `opentelemetry.proto.profiles.v1development` (not `v1`). This `v1development` path will change to `v1` when Profiles reaches GA. Plan for this migration (see Risks section).
The OTLP Profiles format uses a **deduplicated stack representation** where each unique callstack is stored once, with dictionary tables for common entities (functions, locations, mappings). Key message types:
```protobuf
@@ -79,17 +81,38 @@ message ProfileContainer {
// ...attributes, dropped_attributes_count
}
// NOTE: ProfilesDictionary is batch-scoped (shared across all profiles
// in a ProfilesData message), NOT per-profile. The ingestion service
// must pass the dictionary context when processing individual profiles.
message ProfilesDictionary {
repeated string string_table = 1;
repeated Mapping mapping_table = 2;
repeated Location location_table = 3;
repeated Function function_table = 4;
repeated Link link_table = 5;
// ...
}
message Profile {
// Dictionary tables for deduplication
repeated ValueType sample_type = 1;
repeated Sample sample = 2;
repeated Location location = 4;
repeated Function function = 5;
repeated Mapping mapping = 3;
repeated AttributeUnit attribute_units = 15;
repeated Link link_table = 16;
repeated string string_table = 6;
// ...
int64 time_unix_nano = 3;
int64 duration_nano = 4;
ValueType period_type = 5;
int64 period = 6;
bytes profile_id = 7;
repeated int32 attribute_indices = 8;
uint32 dropped_attributes_count = 9;
string original_payload_format = 10; // e.g., "pprofext"
bytes original_payload = 11; // raw pprof bytes for round-tripping
}
message Sample {
int32 stack_index = 1;
repeated int64 values = 2;
repeated int32 attribute_indices = 3;
int32 link_index = 4;
repeated int64 timestamps_unix_nano = 5; // NOTE: repeated — multiple timestamps per sample
}
```