mirror of
https://github.com/OneUptime/oneuptime.git
synced 2026-04-06 00:32:12 +02:00
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:
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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={{
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Reference in New Issue
Block a user