diff --git a/frontend/src/components/HostMetricsDetail/HostMetricsDetail.styles.scss b/frontend/src/components/HostMetricsDetail/HostMetricsDetail.styles.scss index 511348c463c1..219c0bd46457 100644 --- a/frontend/src/components/HostMetricsDetail/HostMetricsDetail.styles.scss +++ b/frontend/src/components/HostMetricsDetail/HostMetricsDetail.styles.scss @@ -169,6 +169,7 @@ box-shadow: 0px 0px 8px 0px rgba(0, 0, 0, 0.1); } } + .ant-drawer-close { padding: 0px; } diff --git a/frontend/src/constants/query.ts b/frontend/src/constants/query.ts index a7852cdb92e2..b50eb38d272a 100644 --- a/frontend/src/constants/query.ts +++ b/frontend/src/constants/query.ts @@ -46,6 +46,7 @@ export enum QueryParams { msgSystem = 'msgSystem', destination = 'destination', kindString = 'kindString', + summaryFilters = 'summaryFilters', tab = 'tab', thresholds = 'thresholds', selectedExplorerView = 'selectedExplorerView', diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/PanelTypeSelector.scss b/frontend/src/container/GridCardLayout/GridCard/FullView/PanelTypeSelector.scss new file mode 100644 index 000000000000..757921952b6e --- /dev/null +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/PanelTypeSelector.scss @@ -0,0 +1,42 @@ +.panel-type-selector { + display: flex; + flex-direction: column; + gap: 8px; + margin-left: 16px; + min-width: 180px; + + .typography { + font-size: 12px; + font-weight: 500; + color: var(--bg-slate-600); + line-height: 16px; + white-space: nowrap; + } + + .panel-type-select { + .view-panel-select-option { + display: flex; + align-items: center; + gap: 8px; + + .icon { + display: flex; + align-items: center; + justify-content: center; + } + + .display { + font-size: 14px; + line-height: 20px; + } + } + } +} + +.ant-select-item-option-content { + .view-panel-select-option { + display: flex; + align-items: center; + gap: 8px; + } +} diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/PanelTypeSelector.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/PanelTypeSelector.tsx new file mode 100644 index 000000000000..19355d3ba5a1 --- /dev/null +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/PanelTypeSelector.tsx @@ -0,0 +1,79 @@ +import './PanelTypeSelector.scss'; + +import { Select, Typography } from 'antd'; +import { QueryParams } from 'constants/query'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import GraphTypes from 'container/NewDashboard/ComponentsSlider/menuItems'; +import { handleQueryChange } from 'container/NewWidget/utils'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { useCallback } from 'react'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; + +const { Option } = Select; + +interface PanelTypeSelectorProps { + selectedPanelType: PANEL_TYPES; + disabled?: boolean; + query: Query; + widgetId: string; +} + +function PanelTypeSelector({ + selectedPanelType, + disabled = false, + query, + widgetId, +}: PanelTypeSelectorProps): JSX.Element { + const { redirectWithQueryBuilderData } = useQueryBuilder(); + + const handleChange = useCallback( + (newPanelType: PANEL_TYPES): void => { + // Transform the query for the new panel type using handleQueryChange + const transformedQuery = handleQueryChange( + newPanelType as any, + query, + selectedPanelType, + ); + + // Use redirectWithQueryBuilderData to update URL with transformed query and new panel type + redirectWithQueryBuilderData( + transformedQuery, + { + [QueryParams.expandedWidgetId]: widgetId, + [QueryParams.graphType]: newPanelType, + }, + undefined, + true, + ); + }, + [redirectWithQueryBuilderData, query, selectedPanelType, widgetId], + ); + + return ( +
+ +
+ ); +} + +PanelTypeSelector.defaultProps = { + disabled: false, +}; + +export default PanelTypeSelector; diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss b/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss index d34d9ba45cff..6bfc863ac470 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/WidgetFullView.styles.scss @@ -4,7 +4,9 @@ overflow-y: hidden; .full-view-header-container { - height: 40px; + display: flex; + flex-direction: column; + gap: 16px; } .graph-container { diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx index 58ecb34a645e..f27089041c1e 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/index.tsx @@ -1,3 +1,4 @@ +/* eslint-disable sonarjs/cognitive-complexity */ import './WidgetFullView.styles.scss'; import { @@ -8,36 +9,47 @@ import { import { Button, Input, Spin } from 'antd'; import cx from 'classnames'; import { ToggleGraphProps } from 'components/Graph/types'; +import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar'; +import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2'; import Spinner from 'components/Spinner'; import TimePreference from 'components/TimePreferenceDropDown'; +import WarningPopover from 'components/WarningPopover/WarningPopover'; import { ENTITY_VERSION_V5 } from 'constants/app'; import { QueryParams } from 'constants/query'; import { PANEL_TYPES } from 'constants/queryBuilder'; +import useDrilldown from 'container/GridCardLayout/GridCard/FullView/useDrilldown'; import { populateMultipleResults } from 'container/NewWidget/LeftContainer/WidgetGraph/util'; import { timeItems, timePreferance, } from 'container/NewWidget/RightContainer/timeItems'; import PanelWrapper from 'container/PanelWrapper/PanelWrapper'; +import RightToolbarActions from 'container/QueryBuilder/components/ToolbarActions/RightToolbarActions'; import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useChartMutable } from 'hooks/useChartMutable'; +import useComponentPermission from 'hooks/useComponentPermission'; import { useSafeNavigate } from 'hooks/useSafeNavigate'; import useUrlQuery from 'hooks/useUrlQuery'; import { getDashboardVariables } from 'lib/dashbaordVariables/getDashboardVariables'; import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; import GetMinMax from 'lib/getMinMax'; +import { isEmpty } from 'lodash-es'; +import { useAppContext } from 'providers/App/App'; import { useDashboard } from 'providers/Dashboard/Dashboard'; -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { useLocation } from 'react-router-dom'; import { UpdateTimeInterval } from 'store/actions'; import { AppState } from 'store/reducers'; +import { Warning } from 'types/api'; import { GlobalReducer } from 'types/reducer/globalTime'; import { getGraphType } from 'utils/getGraphType'; import { getSortedSeriesData } from 'utils/getSortedSeriesData'; import { getLocalStorageGraphVisibilityState } from '../utils'; import { PANEL_TYPES_VS_FULL_VIEW_TABLE } from './contants'; +import PanelTypeSelector from './PanelTypeSelector'; import { GraphContainer, TimeContainer } from './styles'; import { FullViewProps } from './types'; @@ -52,6 +64,7 @@ function FullView({ onClickHandler, customOnDragSelect, setCurrentGraphRef, + enableDrillDown = false, }: FullViewProps): JSX.Element { const { safeNavigate } = useSafeNavigate(); const { selectedTime: globalSelectedTime } = useSelector< @@ -63,12 +76,16 @@ function FullView({ const location = useLocation(); const fullViewRef = useRef(null); + const { handleRunQuery } = useQueryBuilder(); useEffect(() => { setCurrentGraphRef(fullViewRef); }, [setCurrentGraphRef]); const { selectedDashboard, isDashboardLocked } = useDashboard(); + const { user } = useAppContext(); + + const [editWidget] = useComponentPermission(['edit_widget'], user.role); const getSelectedTime = useCallback( () => @@ -85,17 +102,26 @@ function FullView({ const updatedQuery = widget?.query; + // Panel type derived from URL with fallback to widget setting + const selectedPanelType = useMemo(() => { + const urlPanelType = urlQuery.get(QueryParams.graphType) as PANEL_TYPES; + if (urlPanelType && Object.values(PANEL_TYPES).includes(urlPanelType)) { + return urlPanelType; + } + return widget?.panelTypes || PANEL_TYPES.TIME_SERIES; + }, [urlQuery, widget?.panelTypes]); + const [requestData, setRequestData] = useState(() => { - if (widget.panelTypes !== PANEL_TYPES.LIST) { + if (selectedPanelType !== PANEL_TYPES.LIST) { return { selectedTime: selectedTime.enum, - graphType: getGraphType(widget.panelTypes), + graphType: getGraphType(selectedPanelType), query: updatedQuery, globalSelectedInterval: globalSelectedTime, variables: getDashboardVariables(selectedDashboard?.data.variables), fillGaps: widget.fillSpans, - formatForWeb: widget.panelTypes === PANEL_TYPES.TABLE, - originalGraphType: widget?.panelTypes, + formatForWeb: selectedPanelType === PANEL_TYPES.TABLE, + originalGraphType: selectedPanelType, }; } updatedQuery.builder.queryData[0].pageSize = 10; @@ -114,6 +140,19 @@ function FullView({ }; }); + const { + drilldownQuery, + dashboardEditView, + handleResetQuery, + showResetQuery, + } = useDrilldown({ + enableDrillDown, + widget, + setRequestData, + selectedDashboard, + selectedPanelType, + }); + useEffect(() => { setRequestData((prev) => ({ ...prev, @@ -121,12 +160,33 @@ function FullView({ })); }, [selectedTime]); + // Update requestData when panel type changes + useEffect(() => { + setRequestData((prev) => { + if (selectedPanelType !== PANEL_TYPES.LIST) { + return { + ...prev, + graphType: getGraphType(selectedPanelType), + formatForWeb: selectedPanelType === PANEL_TYPES.TABLE, + originalGraphType: selectedPanelType, + }; + } + // For LIST panels, ensure proper configuration + return { + ...prev, + graphType: PANEL_TYPES.LIST, + formatForWeb: false, + originalGraphType: selectedPanelType, + }; + }); + }, [selectedPanelType]); + const response = useGetQueryRange( requestData, // selectedDashboard?.data?.version || version || DEFAULT_ENTITY_VERSION, ENTITY_VERSION_V5, { - queryKey: [widget?.query, widget?.panelTypes, requestData, version], + queryKey: [widget?.query, selectedPanelType, requestData, version], enabled: !isDependedDataLoaded, keepPreviousData: true, }, @@ -169,18 +229,18 @@ function FullView({ }, [originalName, response.data?.payload.data.result]); const canModifyChart = useChartMutable({ - panelType: widget.panelTypes, + panelType: selectedPanelType, panelTypeAndGraphManagerVisibility: PANEL_TYPES_VS_FULL_VIEW_TABLE, }); - if (response.data && widget.panelTypes === PANEL_TYPES.BAR) { + if (response.data && selectedPanelType === PANEL_TYPES.BAR) { const sortedSeriesData = getSortedSeriesData( response.data?.payload.data.result, ); response.data.payload.data.result = sortedSeriesData; } - if (response.data && widget.panelTypes === PANEL_TYPES.PIE) { + if (response.data && selectedPanelType === PANEL_TYPES.PIE) { const transformedData = populateMultipleResults(response?.data); // eslint-disable-next-line no-param-reassign response.data = transformedData; @@ -192,83 +252,139 @@ function FullView({ }); }, [graphsVisibilityStates]); - const isListView = widget.panelTypes === PANEL_TYPES.LIST; + const isListView = selectedPanelType === PANEL_TYPES.LIST; - const isTablePanel = widget.panelTypes === PANEL_TYPES.TABLE; + const isTablePanel = selectedPanelType === PANEL_TYPES.TABLE; const [searchTerm, setSearchTerm] = useState(''); - if (response.isLoading && widget.panelTypes !== PANEL_TYPES.LIST) { + if (response.isLoading && selectedPanelType !== PANEL_TYPES.LIST) { return ; } return (
-
- {fullViewOptions && ( - - {response.isFetching && ( - } /> + + <> +
+ {fullViewOptions && ( + + {enableDrillDown && ( +
+ {showResetQuery && ( + + )} + {editWidget && ( + + )} + +
+ )} + {!isEmpty(response.data?.warning) && ( + + )} +
+ {response.isFetching && ( + } /> + )} + +
+
)} - -
+ {enableDrillDown && ( + <> + + { + handleRunQuery(); + }} + /> + + )} +
-
- - {isTablePanel && ( - } - className="global-search" - placeholder="Search..." - allowClear - key={widget.id} - onChange={(e): void => { - setSearchTerm(e.target.value || ''); +
+ - )} - - -
+ isGraphLegendToggleAvailable={canModifyChart} + > + {isTablePanel && ( + } + className="global-search" + placeholder="Search..." + allowClear + key={widget.id} + onChange={(e): void => { + setSearchTerm(e.target.value || ''); + }} + /> + )} + +
+
+ +
); } diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/styles.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/styles.ts index 0133b1a49bf0..b15162aa9f97 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/styles.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/styles.ts @@ -18,6 +18,7 @@ export const NotFoundContainer = styled.div` export const TimeContainer = styled.div` display: flex; justify-content: flex-end; + gap: 16px; align-items: center; ${({ $panelType }): FlattenSimpleInterpolation => $panelType === PANEL_TYPES.TABLE @@ -25,6 +26,14 @@ export const TimeContainer = styled.div` margin-bottom: 1rem; ` : css``} + + .time-container { + display: flex; + } + .drildown-options-container { + display: flex; + align-items: center; + } `; export const GraphContainer = styled.div` diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts b/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts index cc90a02ee070..27e596e302c6 100644 --- a/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/types.ts @@ -59,6 +59,7 @@ export interface FullViewProps { isDependedDataLoaded?: boolean; onToggleModelHandler?: GraphManagerProps['onToggleModelHandler']; setCurrentGraphRef: Dispatch | null>>; + enableDrillDown?: boolean; } export interface GraphManagerProps extends UplotProps { diff --git a/frontend/src/container/GridCardLayout/GridCard/FullView/useDrilldown.tsx b/frontend/src/container/GridCardLayout/GridCard/FullView/useDrilldown.tsx new file mode 100644 index 000000000000..1a81976483a1 --- /dev/null +++ b/frontend/src/container/GridCardLayout/GridCard/FullView/useDrilldown.tsx @@ -0,0 +1,99 @@ +import { QueryParams } from 'constants/query'; +import { PANEL_TYPES } from 'constants/queryBuilder'; +import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam'; +import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; +import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults'; +import { + Dispatch, + SetStateAction, + useCallback, + useEffect, + useMemo, + useRef, +} from 'react'; +import { Dashboard, Widgets } from 'types/api/dashboard/getAll'; +import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink'; + +export interface DrilldownQueryProps { + widget: Widgets; + setRequestData: Dispatch>; + enableDrillDown: boolean; + selectedDashboard: Dashboard | undefined; + selectedPanelType: PANEL_TYPES; +} + +export interface UseDrilldownReturn { + drilldownQuery: Query; + dashboardEditView: string; + handleResetQuery: () => void; + showResetQuery: boolean; +} + +const useDrilldown = ({ + enableDrillDown, + widget, + setRequestData, + selectedDashboard, + selectedPanelType, +}: DrilldownQueryProps): UseDrilldownReturn => { + const isMounted = useRef(false); + const { redirectWithQueryBuilderData, currentQuery } = useQueryBuilder(); + const compositeQuery = useGetCompositeQueryParam(); + + useEffect(() => { + if (enableDrillDown && !!compositeQuery) { + setRequestData((prev) => ({ + ...prev, + query: compositeQuery, + })); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentQuery, compositeQuery]); + + // update composite query with widget query if composite query is not present in url. + // Composite query should be in the url if switch to edit mode is clicked or drilldown happens from dashboard. + useEffect(() => { + if (enableDrillDown && !isMounted.current) { + redirectWithQueryBuilderData(compositeQuery || widget.query); + } + isMounted.current = true; + }, [widget, enableDrillDown, compositeQuery, redirectWithQueryBuilderData]); + + const dashboardEditView = selectedDashboard?.id + ? generateExportToDashboardLink({ + query: currentQuery, + panelType: selectedPanelType, + dashboardId: selectedDashboard?.id || '', + widgetId: widget.id, + }) + : ''; + + const showResetQuery = useMemo( + () => + JSON.stringify(widget.query?.builder) !== + JSON.stringify(compositeQuery?.builder), + [widget.query, compositeQuery], + ); + + const handleResetQuery = useCallback((): void => { + redirectWithQueryBuilderData( + widget.query, + { + [QueryParams.expandedWidgetId]: widget.id, + [QueryParams.graphType]: widget.panelTypes, + }, + undefined, + true, + ); + }, [redirectWithQueryBuilderData, widget.query, widget.id, widget.panelTypes]); + + return { + drilldownQuery: compositeQuery || widget.query, + dashboardEditView, + handleResetQuery, + showResetQuery, + }; +}; + +export default useDrilldown; diff --git a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx index 6279fe9100ba..19759b1d4e43 100644 --- a/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/WidgetGraphComponent.tsx @@ -62,6 +62,7 @@ function WidgetGraphComponent({ customErrorMessage, customOnRowClick, customTimeRangeWindowForCoRelation, + enableDrillDown, }: WidgetGraphComponentProps): JSX.Element { const { safeNavigate } = useSafeNavigate(); const [deleteModal, setDeleteModal] = useState(false); @@ -236,6 +237,8 @@ function WidgetGraphComponent({ const onToggleModelHandler = (): void => { const existingSearchParams = new URLSearchParams(search); existingSearchParams.delete(QueryParams.expandedWidgetId); + existingSearchParams.delete(QueryParams.compositeQuery); + existingSearchParams.delete(QueryParams.graphType); const updatedQueryParams = Object.fromEntries(existingSearchParams.entries()); if (queryResponse.data?.payload) { const { @@ -365,6 +368,7 @@ function WidgetGraphComponent({ onClickHandler={onClickHandler ?? graphClickHandler} customOnDragSelect={customOnDragSelect} setCurrentGraphRef={setCurrentGraphRef} + enableDrillDown={enableDrillDown} /> @@ -418,6 +422,7 @@ function WidgetGraphComponent({ onOpenTraceBtnClick={onOpenTraceBtnClick} customSeries={customSeries} customOnRowClick={customOnRowClick} + enableDrillDown={enableDrillDown} /> )} @@ -430,6 +435,7 @@ WidgetGraphComponent.defaultProps = { setLayout: undefined, onClickHandler: undefined, customTimeRangeWindowForCoRelation: undefined, + enableDrillDown: false, }; export default WidgetGraphComponent; diff --git a/frontend/src/container/GridCardLayout/GridCard/index.tsx b/frontend/src/container/GridCardLayout/GridCard/index.tsx index 12fda02953f9..5c8a92d84f7b 100644 --- a/frontend/src/container/GridCardLayout/GridCard/index.tsx +++ b/frontend/src/container/GridCardLayout/GridCard/index.tsx @@ -52,6 +52,7 @@ function GridCardGraph({ customTimeRange, customOnRowClick, customTimeRangeWindowForCoRelation, + enableDrillDown, widgetsHavingDynamicVariables, }: GridCardGraphProps): JSX.Element { const dispatch = useDispatch(); @@ -330,6 +331,7 @@ function GridCardGraph({ customErrorMessage={isInternalServerError ? customErrorMessage : undefined} customOnRowClick={customOnRowClick} customTimeRangeWindowForCoRelation={customTimeRangeWindowForCoRelation} + enableDrillDown={enableDrillDown} /> )} @@ -345,6 +347,7 @@ GridCardGraph.defaultProps = { version: 'v3', analyticsEvent: undefined, customTimeRangeWindowForCoRelation: undefined, + enableDrillDown: false, }; export default memo(GridCardGraph); diff --git a/frontend/src/container/GridCardLayout/GridCard/types.ts b/frontend/src/container/GridCardLayout/GridCard/types.ts index 5c44d4266903..feb14763d00c 100644 --- a/frontend/src/container/GridCardLayout/GridCard/types.ts +++ b/frontend/src/container/GridCardLayout/GridCard/types.ts @@ -41,6 +41,7 @@ export interface WidgetGraphComponentProps { customErrorMessage?: string; customOnRowClick?: (record: RowData) => void; customTimeRangeWindowForCoRelation?: string | undefined; + enableDrillDown?: boolean; } export interface GridCardGraphProps { @@ -69,6 +70,7 @@ export interface GridCardGraphProps { }; customOnRowClick?: (record: RowData) => void; customTimeRangeWindowForCoRelation?: string | undefined; + enableDrillDown?: boolean; widgetsHavingDynamicVariables?: Record; } diff --git a/frontend/src/container/GridCardLayout/GridCardLayout.tsx b/frontend/src/container/GridCardLayout/GridCardLayout.tsx index 1ff67dd72cbf..4fa6abf0c761 100644 --- a/frontend/src/container/GridCardLayout/GridCardLayout.tsx +++ b/frontend/src/container/GridCardLayout/GridCardLayout.tsx @@ -54,11 +54,12 @@ import { WidgetRowHeader } from './WidgetRow'; interface GraphLayoutProps { handle: FullScreenHandle; + enableDrillDown?: boolean; } // eslint-disable-next-line sonarjs/cognitive-complexity function GraphLayout(props: GraphLayoutProps): JSX.Element { - const { handle } = props; + const { handle, enableDrillDown = false } = props; const { safeNavigate } = useSafeNavigate(); const { selectedDashboard, @@ -601,6 +602,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element { version={ENTITY_VERSION_V5} onDragSelect={onDragSelect} dataAvailable={checkIfDataExists} + enableDrillDown={enableDrillDown} widgetsHavingDynamicVariables={widgetsHavingDynamicVariables} /> @@ -688,3 +690,7 @@ function GraphLayout(props: GraphLayoutProps): JSX.Element { } export default GraphLayout; + +GraphLayout.defaultProps = { + enableDrillDown: false, +}; diff --git a/frontend/src/container/GridCardLayout/index.tsx b/frontend/src/container/GridCardLayout/index.tsx index 9ef053491f14..088853a22fa7 100644 --- a/frontend/src/container/GridCardLayout/index.tsx +++ b/frontend/src/container/GridCardLayout/index.tsx @@ -4,10 +4,17 @@ import GraphLayoutContainer from './GridCardLayout'; interface GridGraphProps { handle: FullScreenHandle; + enableDrillDown?: boolean; } function GridGraph(props: GridGraphProps): JSX.Element { - const { handle } = props; - return ; + const { handle, enableDrillDown = false } = props; + return ( + + ); } export default GridGraph; + +GridGraph.defaultProps = { + enableDrillDown: false, +}; diff --git a/frontend/src/container/GridTableComponent/index.tsx b/frontend/src/container/GridTableComponent/index.tsx index b9405bc9fd07..6e5d3e9d2b04 100644 --- a/frontend/src/container/GridTableComponent/index.tsx +++ b/frontend/src/container/GridTableComponent/index.tsx @@ -46,6 +46,8 @@ function GridTableComponent({ onOpenTraceBtnClick, customOnRowClick, widgetId, + panelType, + queryRangeRequest, ...props }: GridTableComponentProps): JSX.Element { const { t } = useTranslation(['valueGraph']); @@ -266,6 +268,8 @@ function GridTableComponent({ dataSource={dataSource} sticky={sticky} widgetId={widgetId} + panelType={panelType} + queryRangeRequest={queryRangeRequest} onRow={ openTracesButton || customOnRowClick ? (record): React.HTMLAttributes => ({ diff --git a/frontend/src/container/GridTableComponent/types.ts b/frontend/src/container/GridTableComponent/types.ts index 037847674521..a1dac8104f63 100644 --- a/frontend/src/container/GridTableComponent/types.ts +++ b/frontend/src/container/GridTableComponent/types.ts @@ -1,4 +1,5 @@ import { TableProps } from 'antd'; +import { PANEL_TYPES } from 'constants/queryBuilder'; import { LogsExplorerTableProps } from 'container/LogsExplorerTable/LogsExplorerTable.interfaces'; import { ThresholdOperators, @@ -6,8 +7,9 @@ import { } from 'container/NewWidget/RightContainer/Threshold/types'; import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces'; import { RowData } from 'lib/query/createTableColumnsFromQuery'; -import { ColumnUnit } from 'types/api/dashboard/getAll'; +import { ColumnUnit, ContextLinksData } from 'types/api/dashboard/getAll'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; +import { QueryRangeRequestV5 } from 'types/api/v5/queryRange'; export type GridTableComponentProps = { query: Query; @@ -22,6 +24,10 @@ export type GridTableComponentProps = { widgetId?: string; renderColumnCell?: QueryTableProps['renderColumnCell']; customColTitles?: Record; + enableDrillDown?: boolean; + contextLinks?: ContextLinksData; + panelType?: PANEL_TYPES; + queryRangeRequest?: QueryRangeRequestV5; } & Pick & Omit, 'columns' | 'dataSource'>; diff --git a/frontend/src/container/GridTableComponent/utils.ts b/frontend/src/container/GridTableComponent/utils.ts index c2b735098d16..97a907bc041b 100644 --- a/frontend/src/container/GridTableComponent/utils.ts +++ b/frontend/src/container/GridTableComponent/utils.ts @@ -1,5 +1,5 @@ /* eslint-disable sonarjs/cognitive-complexity */ -import { ColumnsType, ColumnType } from 'antd/es/table'; +import { ColumnType } from 'antd/es/table'; import { convertUnit } from 'container/NewWidget/RightContainer/dataFormatCategories'; import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types'; import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config'; @@ -9,6 +9,12 @@ import { isEmpty, isNaN } from 'lodash-es'; import { Query } from 'types/api/queryBuilder/queryBuilderData'; import { EQueryType } from 'types/common/dashboard'; +// Custom column type that extends ColumnType to include isValueColumn +export interface CustomDataColumnType extends ColumnType { + isValueColumn?: boolean; + queryName?: string; +} + // Helper function to evaluate the condition based on the operator function evaluateCondition( operator: string | undefined, @@ -184,9 +190,9 @@ export function createColumnsAndDataSource( data: TableData, currentQuery: Query, renderColumnCell?: QueryTableProps['renderColumnCell'], -): { columns: ColumnsType; dataSource: RowData[] } { - const columns: ColumnsType = - data.columns?.reduce>((acc, item) => { +): { columns: CustomDataColumnType[]; dataSource: RowData[] } { + const columns: CustomDataColumnType[] = + data.columns?.reduce[]>((acc, item) => { // is the column is the value column then we need to check for the available legend const legend = item.isValueColumn ? getQueryLegend(currentQuery, item.queryName) @@ -197,11 +203,13 @@ export function createColumnsAndDataSource( (query) => query.queryName === item.queryName, )?.aggregations?.length || 0; - const column: ColumnType = { + const column: CustomDataColumnType = { dataIndex: item.id || item.name, // if no legend present then rely on the column name value title: !isNewAggregation && !isEmpty(legend) ? legend : item.name, width: QUERY_TABLE_CONFIG.width, + isValueColumn: item.isValueColumn, + queryName: item.queryName, render: renderColumnCell && renderColumnCell[item.id], sorter: (a: RowData, b: RowData): number => sortFunction(a, b, item), }; diff --git a/frontend/src/container/GridValueComponent/index.tsx b/frontend/src/container/GridValueComponent/index.tsx index 21e1c5ee606a..bb1b6851f49d 100644 --- a/frontend/src/container/GridValueComponent/index.tsx +++ b/frontend/src/container/GridValueComponent/index.tsx @@ -2,8 +2,11 @@ import { Typography } from 'antd'; import { getYAxisFormattedValue } from 'components/Graph/yAxisConfig'; import ValueGraph from 'components/ValueGraph'; import { generateGridTitle } from 'container/GridPanelSwitch/utils'; +import useGraphContextMenu from 'container/QueryTable/Drilldown/useGraphContextMenu'; +import ContextMenu, { useCoordinates } from 'periscope/components/ContextMenu'; import { memo, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; +import { EQueryType } from 'types/common/dashboard'; import { TitleContainer, ValueContainer } from './styles'; import { GridValueComponentProps } from './types'; @@ -13,6 +16,10 @@ function GridValueComponent({ title, yAxisUnit, thresholds, + widget, + queryResponse, + contextLinks, + enableDrillDown = false, }: GridValueComponentProps): JSX.Element { const value = ((data[1] || [])[0] || 0) as number; @@ -21,6 +28,39 @@ function GridValueComponent({ const isDashboardPage = location.pathname.split('/').length === 3; + const { + coordinates, + popoverPosition, + onClose, + onClick, + subMenu, + setSubMenu, + clickedData, + } = useCoordinates(); + + const { menuItemsConfig } = useGraphContextMenu({ + widgetId: widget?.id || '', + query: widget?.query || { + queryType: EQueryType.QUERY_BUILDER, + promql: [], + builder: { + queryFormulas: [], + queryData: [], + queryTraceOperator: [], + }, + clickhouse_sql: [], + id: '', + }, + graphData: clickedData, + onClose, + coordinates, + subMenu, + setSubMenu, + contextLinks: contextLinks || { linksData: [] }, + panelType: widget?.panelTypes, + queryRange: queryResponse, + }); + if (data.length === 0) { return ( @@ -29,12 +69,31 @@ function GridValueComponent({ ); } + const isQueryTypeBuilder = + widget?.query?.queryType === EQueryType.QUERY_BUILDER; + return ( <> {gridTitle} - + { + const queryName = (queryResponse?.data?.params as any)?.compositeQuery + ?.queries[0]?.spec?.name; + + if (!enableDrillDown || !queryName || !isQueryTypeBuilder) return; + + // when multiple queries are present, we need to get the query name from the queryResponse + // since value panel shows result for the first query + const clickedData = { + queryName, + filters: [], + }; + onClick({ x: e.clientX, y: e.clientY }, clickedData); + }} + > + ); } diff --git a/frontend/src/container/GridValueComponent/styles.ts b/frontend/src/container/GridValueComponent/styles.ts index 354474b2527c..74e861914a6c 100644 --- a/frontend/src/container/GridValueComponent/styles.ts +++ b/frontend/src/container/GridValueComponent/styles.ts @@ -4,12 +4,19 @@ interface Props { isDashboardPage: boolean; } -export const ValueContainer = styled.div` +interface ValueContainerProps { + showClickable?: boolean; +} + +export const ValueContainer = styled.div` height: 100%; display: flex; justify-content: center; align-items: center; flex-direction: column; + user-select: none; + cursor: ${({ showClickable = false }): string => + showClickable ? 'pointer' : 'default'}; `; export const TitleContainer = styled.div` diff --git a/frontend/src/container/GridValueComponent/types.ts b/frontend/src/container/GridValueComponent/types.ts index 709cb3de284d..e92bb81fb659 100644 --- a/frontend/src/container/GridValueComponent/types.ts +++ b/frontend/src/container/GridValueComponent/types.ts @@ -1,4 +1,8 @@ import { ThresholdProps } from 'container/NewWidget/RightContainer/Threshold/types'; +import { UseQueryResult } from 'react-query'; +import { SuccessResponse } from 'types/api'; +import { ContextLinksData, Widgets } from 'types/api/dashboard/getAll'; +import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange'; import uPlot from 'uplot'; export type GridValueComponentProps = { @@ -7,4 +11,12 @@ export type GridValueComponentProps = { title?: React.ReactNode; yAxisUnit?: string; thresholds?: ThresholdProps[]; + // Context menu related props + widget?: Widgets; + queryResponse?: UseQueryResult< + SuccessResponse, + Error + >; + contextLinks?: ContextLinksData; + enableDrillDown?: boolean; }; diff --git a/frontend/src/container/LogsExplorerList/index.tsx b/frontend/src/container/LogsExplorerList/index.tsx index 1f15539de706..5bedd1d1fc8a 100644 --- a/frontend/src/container/LogsExplorerList/index.tsx +++ b/frontend/src/container/LogsExplorerList/index.tsx @@ -50,7 +50,6 @@ function LogsExplorerList({ isFilterApplied, }: LogsExplorerListProps): JSX.Element { const ref = useRef(null); - const { activeLogId } = useCopyLogLink(); const { diff --git a/frontend/src/container/NewDashboard/DashboardVariablesSelection/useDashboardVariableUpdate.ts b/frontend/src/container/NewDashboard/DashboardVariablesSelection/useDashboardVariableUpdate.ts new file mode 100644 index 000000000000..74cab6908406 --- /dev/null +++ b/frontend/src/container/NewDashboard/DashboardVariablesSelection/useDashboardVariableUpdate.ts @@ -0,0 +1,239 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +import { useAddDynamicVariableToPanels } from 'hooks/dashboard/useAddDynamicVariableToPanels'; +import { useUpdateDashboard } from 'hooks/dashboard/useUpdateDashboard'; +import { useDashboard } from 'providers/Dashboard/Dashboard'; +import { useCallback } from 'react'; +import { Dashboard, IDashboardVariable } from 'types/api/dashboard/getAll'; +import { v4 as uuidv4 } from 'uuid'; + +import { convertVariablesToDbFormat } from './util'; + +// Note: This logic completely mimics the logic in DashboardVariableSelection.tsx +// but is separated to avoid unnecessary logic addition. +interface UseDashboardVariableUpdateReturn { + onValueUpdate: ( + name: string, + id: string, + value: IDashboardVariable['selectedValue'], + allSelected: boolean, + haveCustomValuesSelected?: boolean, + ) => void; + createVariable: ( + name: string, + value: IDashboardVariable['selectedValue'], + // type?: IDashboardVariable['type'], + description?: string, + source?: 'logs' | 'traces' | 'metrics' | 'all sources', + widgetId?: string, + ) => void; + updateVariables: ( + updatedVariablesData: Dashboard['data']['variables'], + currentRequestedId?: string, + widgetIds?: string[], + applyToAll?: boolean, + ) => void; +} + +export const useDashboardVariableUpdate = (): UseDashboardVariableUpdateReturn => { + const { + selectedDashboard, + setSelectedDashboard, + updateLocalStorageDashboardVariables, + } = useDashboard(); + + const addDynamicVariableToPanels = useAddDynamicVariableToPanels(); + const updateMutation = useUpdateDashboard(); + + const onValueUpdate = useCallback( + ( + name: string, + id: string, + value: IDashboardVariable['selectedValue'], + allSelected: boolean, + haveCustomValuesSelected?: boolean, + ): void => { + if (id) { + // Performance optimization: For dynamic variables with allSelected=true, we don't store + // individual values in localStorage since we can always derive them from available options. + // This makes localStorage much lighter and more efficient. + // currently all the variables are dynamic + const isDynamic = true; + updateLocalStorageDashboardVariables(name, value, allSelected, isDynamic); + + if (selectedDashboard) { + setSelectedDashboard((prev) => { + if (prev) { + const oldVariables = prev?.data.variables; + // this is added to handle case where we have two different + // schemas for variable response + if (oldVariables?.[id]) { + oldVariables[id] = { + ...oldVariables[id], + selectedValue: value, + allSelected, + haveCustomValuesSelected, + }; + } + if (oldVariables?.[name]) { + oldVariables[name] = { + ...oldVariables[name], + selectedValue: value, + allSelected, + haveCustomValuesSelected, + }; + } + return { + ...prev, + data: { + ...prev?.data, + variables: { + ...oldVariables, + }, + }, + }; + } + return prev; + }); + } + } + }, + [ + selectedDashboard, + setSelectedDashboard, + updateLocalStorageDashboardVariables, + ], + ); + + const updateVariables = useCallback( + ( + updatedVariablesData: Dashboard['data']['variables'], + currentRequestedId?: string, + widgetIds?: string[], + applyToAll?: boolean, + ): void => { + if (!selectedDashboard) { + return; + } + + const newDashboard = + (currentRequestedId && + addDynamicVariableToPanels( + selectedDashboard, + updatedVariablesData[currentRequestedId || ''], + widgetIds, + applyToAll, + )) || + selectedDashboard; + + updateMutation.mutateAsync( + { + id: selectedDashboard.id, + + data: { + ...newDashboard.data, + variables: updatedVariablesData, + }, + }, + { + onSuccess: (updatedDashboard) => { + if (updatedDashboard.data) { + setSelectedDashboard(updatedDashboard.data); + // notifications.success({ + // message: t('variable_updated_successfully'), + // }); + } + }, + }, + ); + }, + [ + selectedDashboard, + addDynamicVariableToPanels, + updateMutation, + setSelectedDashboard, + ], + ); + + const createVariable = useCallback( + ( + name: string, + value: IDashboardVariable['selectedValue'], + // type: IDashboardVariable['type'] = 'DYNAMIC', + description = '', + source: 'logs' | 'traces' | 'metrics' | 'all sources' = 'all sources', + // widgetId?: string, + ): void => { + if (!selectedDashboard) { + console.warn('No dashboard selected for variable creation'); + return; + } + + // Get current dashboard variables + const currentVariables = selectedDashboard.data.variables || {}; + + // Create tableRowData like Dashboard Settings does + const tableRowData = []; + const variableOrderArr = []; + // eslint-disable-next-line no-restricted-syntax + for (const [key, value] of Object.entries(currentVariables)) { + const { order, id } = value; + + tableRowData.push({ + key, + name: key, + ...currentVariables[key], + id, + }); + + if (order) { + variableOrderArr.push(order); + } + } + + // Sort by order + tableRowData.sort((a, b) => a.order - b.order); + variableOrderArr.sort((a, b) => a - b); + + // Create new variable + const nextOrder = + variableOrderArr.length > 0 ? Math.max(...variableOrderArr) + 1 : 0; + const newVariable: any = { + id: uuidv4(), + name, + type: 'DYNAMIC' as const, + description, + order: nextOrder, + selectedValue: value, + allSelected: false, + haveCustomValuesSelected: false, + sort: 'ASC' as const, + multiSelect: true, + showALLOption: true, + dynamicVariablesAttribute: name, + dynamicVariablesSource: source, + dynamicVariablesWidgetIds: [], + queryValue: '', + }; + + // Add to tableRowData + tableRowData.push({ + key: newVariable.id, + ...newVariable, + id: newVariable.id, + }); + + // Convert to dashboard format and update + const updatedVariables = convertVariablesToDbFormat(tableRowData); + updateVariables(updatedVariables, newVariable.id, [], false); + }, + [selectedDashboard, updateVariables], + ); + + return { + onValueUpdate, + createVariable, + updateVariables, + }; +}; + +export default useDashboardVariableUpdate; diff --git a/frontend/src/container/NewDashboard/GridGraphs/index.tsx b/frontend/src/container/NewDashboard/GridGraphs/index.tsx index 6ff37d69397a..fe2eec0a2244 100644 --- a/frontend/src/container/NewDashboard/GridGraphs/index.tsx +++ b/frontend/src/container/NewDashboard/GridGraphs/index.tsx @@ -1,4 +1,5 @@ import GridGraphLayout from 'container/GridCardLayout'; +import { isDrilldownEnabled } from 'container/QueryTable/Drilldown/drilldownUtils'; import { FullScreenHandle } from 'react-full-screen'; import { GridComponentSliderContainer } from './styles'; @@ -11,7 +12,7 @@ function GridGraphs(props: GridGraphsProps): JSX.Element { const { handle } = props; return ( - + ); } diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx index e86a46d8f788..86982b13d677 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphContainer.tsx @@ -16,6 +16,7 @@ function WidgetGraphContainer({ setRequestData, selectedWidget, isLoadingPanelData, + enableDrillDown = false, }: WidgetGraphContainerProps): JSX.Element { if (queryResponse.data && selectedGraph === PANEL_TYPES.BAR) { const sortedSeriesData = getSortedSeriesData( @@ -86,6 +87,7 @@ function WidgetGraphContainer({ queryResponse={queryResponse} setRequestData={setRequestData} selectedGraph={selectedGraph} + enableDrillDown={enableDrillDown} /> ); } diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx index c0ad4c8aaea8..79e368baa102 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/WidgetGraphs.tsx @@ -36,6 +36,7 @@ function WidgetGraph({ queryResponse, setRequestData, selectedGraph, + enableDrillDown = false, }: WidgetGraphProps): JSX.Element { const graphRef = useRef(null); const lineChartRef = useRef(); @@ -188,6 +189,7 @@ function WidgetGraph({ onClickHandler={graphClickHandler} graphVisibility={graphVisibility} setGraphVisibility={setGraphVisibility} + enableDrillDown={enableDrillDown} /> ); @@ -201,6 +203,11 @@ interface WidgetGraphProps { >; setRequestData: Dispatch>; selectedGraph: PANEL_TYPES; + enableDrillDown?: boolean; } export default WidgetGraph; + +WidgetGraph.defaultProps = { + enableDrillDown: false, +}; diff --git a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx index d279db4d2fb2..0fa16c20995f 100644 --- a/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/WidgetGraph/index.tsx @@ -21,6 +21,7 @@ function WidgetGraph({ setRequestData, selectedWidget, isLoadingPanelData, + enableDrillDown = false, }: WidgetGraphContainerProps): JSX.Element { const { currentQuery } = useQueryBuilder(); @@ -57,6 +58,7 @@ function WidgetGraph({ queryResponse={queryResponse} setRequestData={setRequestData} selectedWidget={selectedWidget} + enableDrillDown={enableDrillDown} /> ); diff --git a/frontend/src/container/NewWidget/LeftContainer/index.tsx b/frontend/src/container/NewWidget/LeftContainer/index.tsx index 434d019e051d..fe5406620fef 100644 --- a/frontend/src/container/NewWidget/LeftContainer/index.tsx +++ b/frontend/src/container/NewWidget/LeftContainer/index.tsx @@ -27,6 +27,7 @@ function LeftContainer({ setRequestData, isLoadingPanelData, setQueryResponse, + enableDrillDown = false, }: WidgetGraphProps): JSX.Element { const { stagedQuery } = useQueryBuilder(); // const { selectedDashboard } = useDashboard(); @@ -64,6 +65,7 @@ function LeftContainer({ setRequestData={setRequestData} selectedWidget={selectedWidget} isLoadingPanelData={isLoadingPanelData} + enableDrillDown={enableDrillDown} /> diff --git a/frontend/src/container/NewWidget/NewWidget.styles.scss b/frontend/src/container/NewWidget/NewWidget.styles.scss index e215b72af523..9c16a711e2ba 100644 --- a/frontend/src/container/NewWidget/NewWidget.styles.scss +++ b/frontend/src/container/NewWidget/NewWidget.styles.scss @@ -36,6 +36,11 @@ } } + .right-header { + display: flex; + gap: 16px; + } + .save-btn { display: flex; height: 32px; diff --git a/frontend/src/container/NewWidget/RightContainer/ContextLinks/UpdateContextLinks.styles.scss b/frontend/src/container/NewWidget/RightContainer/ContextLinks/UpdateContextLinks.styles.scss new file mode 100644 index 000000000000..d9b142ea4a31 --- /dev/null +++ b/frontend/src/container/NewWidget/RightContainer/ContextLinks/UpdateContextLinks.styles.scss @@ -0,0 +1,92 @@ +.context-link-form-container { + margin-top: 16px; + min-height: 500px; + display: flex; + flex-direction: column; + justify-content: space-between; + + .form-label { + margin-left: 4px; + } + + .add-url-parameter-btn { + display: flex; + align-items: center; + width: fit-content; + margin-top: 16px; + margin-left: 16px; + } + + .url-parameters-section { + margin-top: 16px; + margin-bottom: 16px; + + .parameter-header { + margin-bottom: 8px; + + strong { + color: #666; + font-size: 14px; + } + } + + .parameter-row { + margin-bottom: 8px; + align-items: center; + + .ant-input { + border-radius: 4px; + } + + .delete-parameter-btn { + color: var(--bg-vanilla-400); + padding: 4px; + height: 32px; + width: 32px; + display: flex; + align-items: center; + justify-content: center; + + &:hover { + color: var(--bg-cherry-400) !important; + border-color: var(--bg-cherry-400) !important; + } + } + } + } + + .params-container { + margin-left: 16px; + } + + .context-link-footer { + display: flex; + justify-content: flex-end; + gap: 8px; + margin-top: 24px; + padding-top: 16px; + border-top: 1px solid var(--bg-slate-400); + } +} + +.lightMode { + .context-link-form-container { + .url-parameters-section { + .parameter-row { + .delete-parameter-btn { + color: var(--bg-slate-400); + + &:hover { + color: var(--bg-cherry-500) !important; + border-color: var(--bg-cherry-500) !important; + background-color: var(--bg-cherry-100); + } + } + } + } + + .context-link-footer { + border-top-color: var(--bg-vanilla-200); + } + } +} diff --git a/frontend/src/container/NewWidget/RightContainer/ContextLinks/UpdateContextLinks.tsx b/frontend/src/container/NewWidget/RightContainer/ContextLinks/UpdateContextLinks.tsx new file mode 100644 index 000000000000..eb5a6529104f --- /dev/null +++ b/frontend/src/container/NewWidget/RightContainer/ContextLinks/UpdateContextLinks.tsx @@ -0,0 +1,363 @@ +import './UpdateContextLinks.styles.scss'; + +import { + Button, + Col, + Form, + Input as AntInput, + Input, + Row, + Typography, +} from 'antd'; +import { CONTEXT_LINK_FIELDS } from 'container/NewWidget/RightContainer/ContextLinks/constants'; +import { + getInitialValues, + getUrlParams, + transformContextVariables, + updateUrlWithParams, +} from 'container/NewWidget/RightContainer/ContextLinks/utils'; +import useContextVariables from 'hooks/dashboard/useContextVariables'; +import { Plus, Trash2 } from 'lucide-react'; +import { useEffect, useMemo, useState } from 'react'; +import { ContextLinkProps, Widgets } from 'types/api/dashboard/getAll'; + +import VariablesDropdown from './VariablesDropdown'; + +const { TextArea } = AntInput; + +interface UpdateContextLinksProps { + selectedContextLink: ContextLinkProps | null; + onSave: (newContextLink: ContextLinkProps) => void; + onCancel: () => void; + selectedWidget?: Widgets; +} + +function UpdateContextLinks({ + selectedContextLink, + onSave, + onCancel, + selectedWidget, +}: UpdateContextLinksProps): JSX.Element { + const [form] = Form.useForm(); + // const label = Form.useWatch(CONTEXT_LINK_FIELDS.LABEL, form); + const url = Form.useWatch(CONTEXT_LINK_FIELDS.URL, form); + + const [params, setParams] = useState< + { + key: string; + value: string; + }[] + >([]); + + // Extract field variables from the widget's query (all groupBy fields from all queries) + const fieldVariables = useMemo(() => { + if (!selectedWidget?.query?.builder?.queryData) return {}; + + const fieldVars: Record = {}; + + // Get all groupBy fields from all queries + selectedWidget.query.builder.queryData.forEach((queryData) => { + if (queryData.groupBy) { + queryData.groupBy.forEach((field) => { + if (field.key && !(field.key in fieldVars)) { + fieldVars[field.key] = ''; // Placeholder value + } + }); + } + }); + + return fieldVars; + }, [selectedWidget?.query]); + + // Use useContextVariables to get dashboard, global, and field variables + const { variables } = useContextVariables({ + maxValues: 2, + customVariables: fieldVariables, + }); + + // Transform variables into the format expected by VariablesDropdown + const transformedVariables = useMemo( + () => transformContextVariables(variables), + [variables], + ); + + // Function to get current domain + const getCurrentDomain = (): string => window.location.origin; + + // Function to handle variable selection from dropdown + const handleVariableSelect = ( + variableName: string, + cursorPosition?: number, + ): void => { + // Get current URL value from form + const currentValue = form.getFieldValue(CONTEXT_LINK_FIELDS.URL) || ''; + + // Insert at cursor position if provided, otherwise append to end + const newValue = + cursorPosition !== undefined + ? currentValue.slice(0, cursorPosition) + + variableName + + currentValue.slice(cursorPosition) + : currentValue + variableName; + + // Update form value + form.setFieldValue(CONTEXT_LINK_FIELDS.URL, newValue); + }; + + // Function to handle variable selection for parameter values + const handleParamChange = ( + index: number, + field: 'key' | 'value', + value: string, + ): void => { + const newParams = [...params]; + newParams[index][field] = value; + setParams(newParams); + const updatedUrl = updateUrlWithParams(url, newParams); + form.setFieldValue(CONTEXT_LINK_FIELDS.URL, updatedUrl); + }; + + const handleParamVariableSelect = ( + index: number, + variableName: string, + cursorPosition?: number, + ): void => { + // Get current parameter value + const currentValue = params[index].value; + + // Insert at cursor position if provided, otherwise append to end + const newValue = + cursorPosition !== undefined + ? currentValue.slice(0, cursorPosition) + + variableName + + currentValue.slice(cursorPosition) + : currentValue + variableName; + + // Update the parameter value + handleParamChange(index, 'value', newValue); + }; + + useEffect(() => { + ((window as unknown) as Record).form = form; + }, [form]); + + // Parse URL and update params when URL changes + useEffect(() => { + if (url) { + const urlParams = getUrlParams(url); + setParams(urlParams); + } + }, [url]); + + const handleSave = async (): Promise => { + try { + // Validate form fields + await form.validateFields(); + const newContextLink = { + id: form.getFieldValue(CONTEXT_LINK_FIELDS.ID), + label: + form.getFieldValue(CONTEXT_LINK_FIELDS.LABEL) || + form.getFieldValue(CONTEXT_LINK_FIELDS.URL), + url: form.getFieldValue(CONTEXT_LINK_FIELDS.URL), + }; + // If validation passes, call onSave + onSave(newContextLink); + } catch (error) { + // Form validation failed, don't call onSave + console.log('Form validation failed:', error); + } + }; + + const handleAddUrlParameter = (): void => { + const isLastParamEmpty = + params.length > 0 && + params[params.length - 1].key.trim() === '' && + params[params.length - 1].value.trim() === ''; + const canAddParam = params.length === 0 || !isLastParamEmpty; + + if (canAddParam) { + const newParams = [ + ...params, + { + key: '', + value: '', + }, + ]; + setParams(newParams); + const updatedUrl = updateUrlWithParams(url, newParams); + form.setFieldValue(CONTEXT_LINK_FIELDS.URL, updatedUrl); + } + }; + + const handleDeleteParameter = (index: number): void => { + const newParams = params.filter((_, i) => i !== index); + setParams(newParams); + const updatedUrl = updateUrlWithParams(url, newParams); + form.setFieldValue(CONTEXT_LINK_FIELDS.URL, updatedUrl); + }; + + return ( +
+
+
{}} + > + {/* //label */} + Label + + + + {/* //url */} + + URL * + + + + {({ setIsOpen, setCursorPosition }): JSX.Element => ( +
+ { + setCursorPosition(e.target.selectionStart || 0); + form.setFieldValue(CONTEXT_LINK_FIELDS.URL, e.target.value); + }} + onFocus={(): void => setIsOpen(true)} + // eslint-disable-next-line sonarjs/no-identical-functions + onClick={(e): void => + setCursorPosition((e.target as HTMLInputElement).selectionStart || 0) + } + // eslint-disable-next-line sonarjs/no-identical-functions + onKeyUp={(e): void => + setCursorPosition((e.target as HTMLInputElement).selectionStart || 0) + } + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + spellCheck="false" + className="url-input-field" + placeholder={`${getCurrentDomain()}/trace/{{_traceId}}`} + /> +
+ )} +
+
+ + {/* Remove the separate variables section */} +
+ +
+ {/* URL Parameters Section */} + {params.length > 0 && ( +
+ + Key + Value + {/* Empty column for spacing */} + + + {params.map((param, index) => ( + // eslint-disable-next-line react/no-array-index-key + + + + handleParamChange(index, 'key', e.target.value) + } + /> + + + + handleParamVariableSelect(index, variableName, cursorPosition) + } + variables={transformedVariables} + > + {({ setIsOpen, setCursorPosition }): JSX.Element => ( +