diff --git a/App/FeatureSet/Dashboard/src/Components/Dashboard/Canvas/BlankDashboardUnit.tsx b/App/FeatureSet/Dashboard/src/Components/Dashboard/Canvas/BlankDashboardUnit.tsx index 1a1e854d9f..2b120dfa81 100644 --- a/App/FeatureSet/Dashboard/src/Components/Dashboard/Canvas/BlankDashboardUnit.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Dashboard/Canvas/BlankDashboardUnit.tsx @@ -24,7 +24,7 @@ const BlankDashboardUnitElement: FunctionComponent = ( 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 = ( width: widthOfUnitInPx + "px", height: heightOfUnitInPx + "px", margin: MarginForEachUnitInPx + "px", + border: props.isEditMode ? "1px dashed rgba(203, 213, 225, 0.5)" : "none", + borderRadius: "6px", }} > ); diff --git a/App/FeatureSet/Dashboard/src/Components/Dashboard/Canvas/Index.tsx b/App/FeatureSet/Dashboard/src/Components/Dashboard/Canvas/Index.tsx index ecf1b63e13..cff7a80c33 100644 --- a/App/FeatureSet/Dashboard/src/Components/Dashboard/Canvas/Index.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Dashboard/Canvas/Index.tsx @@ -139,9 +139,7 @@ const DashboardCanvas: FunctionComponent = ( 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 (
= ( 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} diff --git a/App/FeatureSet/Dashboard/src/Components/Dashboard/Components/DashboardBaseComponent.tsx b/App/FeatureSet/Dashboard/src/Components/Dashboard/Components/DashboardBaseComponent.tsx index 947ef43d6f..fef88091d0 100644 --- a/App/FeatureSet/Dashboard/src/Components/Dashboard/Components/DashboardBaseComponent.tsx +++ b/App/FeatureSet/Dashboard/src/Components/Dashboard/Components/DashboardBaseComponent.tsx @@ -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 = ( props: ComponentProps, ): ReactElement => { @@ -67,403 +85,548 @@ const DashboardBaseComponentElement: FunctionComponent = ( const widthOfComponent: number = component.widthInDashboardUnits; const heightOfComponent: number = component.heightInDashboardUnits; - const [topInPx, setTopInPx] = React.useState(0); - const [leftInPx, setLeftInPx] = React.useState(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("idle"); + const [isHovered, setIsHovered] = useState(false); + const dragStateRef: React.MutableRefObject = useRef(null); const dashboardComponentRef: React.RefObject = - React.useRef(null); + useRef(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 ( -
) => { - 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`} - >
- ); - }; - - 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 (
) => { - 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) => {}} - onDragEnd={(_event: React.DragEvent) => {}} - >
+ > + {/* Grip dots pattern */} +
+ + + + + + + + +
+
); }; - 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 (
) => { - 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`} - >
+ > + {/* Visible handle bar */} +
+
); }; + const getResizeHeightHandle: GetReactElementFunction = (): ReactElement => { + if (!showHandles) { + return <>; + } + + return ( +
) => { + startInteraction(event, "resizing-height"); + }} + > + {/* Visible handle bar */} +
+
+ ); + }; + + const getResizeCornerHandle: GetReactElementFunction = (): ReactElement => { + if (!showHandles) { + return <>; + } + + return ( +
) => { + startInteraction(event, "resizing-corner"); + }} + > + {/* Corner triangle indicator */} +
+
+ ); + }; + + // 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 ( +
+
+ {label} +
+
+ ); + }; + + const componentHeight: number = + GetDashboardUnitHeightInPx(props.totalCurrentDashboardWidthInPx) * + heightOfComponent + + SpaceBetweenUnitsInPx * (heightOfComponent - 1); + + const componentWidth: number = + GetDashboardUnitWidthInPx(props.totalCurrentDashboardWidthInPx) * + widthOfComponent + + (SpaceBetweenUnitsInPx - 2) * (widthOfComponent - 1); + return (
{ + 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 && ( -
- + {/* Component type badge - visible on hover or selection in edit mode */} + {props.isEditMode && (props.isSelected || isHovered) && ( +
+ {component.componentType}
)} - {component.componentType === DashboardComponentType.Text && ( - - )} - {component.componentType === DashboardComponentType.Chart && ( - - )} - {component.componentType === DashboardComponentType.Value && ( - - )} - {component.componentType === DashboardComponentType.Table && ( - - )} - {component.componentType === DashboardComponentType.Gauge && ( - - )} - {component.componentType === DashboardComponentType.LogStream && ( - - )} - {component.componentType === DashboardComponentType.TraceList && ( - - )} + {/* Component content area */} +
+ {component.componentType === DashboardComponentType.Text && ( + + )} + {component.componentType === DashboardComponentType.Chart && ( + + )} + {component.componentType === DashboardComponentType.Value && ( + + )} + {component.componentType === DashboardComponentType.Table && ( + + )} + {component.componentType === DashboardComponentType.Gauge && ( + + )} + {component.componentType === DashboardComponentType.LogStream && ( + + )} + {component.componentType === DashboardComponentType.TraceList && ( + + )} +
- {getResizeWidthElement()} - {getResizeHeightElement()} + {getResizeWidthHandle()} + {getResizeHeightHandle()} + {getResizeCornerHandle()}
); }; diff --git a/App/FeatureSet/Dashboard/src/Pages/Dashboards/View/AuthenticationSettings.tsx b/App/FeatureSet/Dashboard/src/Pages/Dashboards/View/AuthenticationSettings.tsx index 92d083c476..9efc6c3b17 100644 --- a/App/FeatureSet/Dashboard/src/Pages/Dashboards/View/AuthenticationSettings.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/Dashboards/View/AuthenticationSettings.tsx @@ -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(false); + const [showPasswordModal, setShowPasswordModal] = useState(false); + const [refreshMasterPassword, setRefreshMasterPassword] = useState(false); + const [isMasterPasswordSet, setIsMasterPasswordSet] = useState(false); return ( @@ -60,14 +67,25 @@ const DashboardAuthenticationSettings: FunctionComponent< - 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, - }} - /> - - - 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 ( -

- For security reasons, the current master password is never - displayed. Use the update button to set a new password at - any time. +

+ {isMasterPasswordSet ? "Password is set." : "Not set."}

); }, }, ], modelId: modelId, + onItemLoaded: (item: Dashboard) => { + setIsMasterPasswordSet(Boolean(item.masterPassword)); + }, }} /> + {showPasswordModal && ( + + 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} + /> + )} + name="Dashboard > IP Whitelist" cardProps={{ diff --git a/App/FeatureSet/Dashboard/src/Pages/StatusPages/View/AuthenticationSettings.tsx b/App/FeatureSet/Dashboard/src/Pages/StatusPages/View/AuthenticationSettings.tsx index 1591197653..8b4b80f415 100644 --- a/App/FeatureSet/Dashboard/src/Pages/StatusPages/View/AuthenticationSettings.tsx +++ b/App/FeatureSet/Dashboard/src/Pages/StatusPages/View/AuthenticationSettings.tsx @@ -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(false); + const [refreshMasterPassword, setRefreshMasterPassword] = useState(false); + const [isMasterPasswordSet, setIsMasterPasswordSet] = useState(false); return ( @@ -51,14 +58,25 @@ const StatusPageDelete: FunctionComponent< /> - 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)); + }, }} /> - - 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 ( -

- For security reasons, the current master password is never - displayed. Use the update button to set a new password at - any time. -

- ); + {showPasswordModal && ( + + 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} + /> + )} name="Status Page > IP Whitelist" diff --git a/Common/Server/API/DashboardAPI.ts b/Common/Server/API/DashboardAPI.ts index 956bf99078..0e517f6fb7 100644 --- a/Common/Server/API/DashboardAPI.ts +++ b/Common/Server/API/DashboardAPI.ts @@ -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 diff --git a/Telemetry/Docs/opentelemetry-profiles-roadmap.md b/Telemetry/Docs/opentelemetry-profiles-roadmap.md index 841ad897fb..59484d0036 100644 --- a/Telemetry/Docs/opentelemetry-profiles-roadmap.md +++ b/Telemetry/Docs/opentelemetry-profiles-roadmap.md @@ -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 } ```