Compare commits

...

4 Commits

Author SHA1 Message Date
aks07
cee4a3e2e2 feat: panel markers with dummy data 2025-10-28 19:03:52 +05:30
aks07
c0336001f5 Merge branch 'main' of github.com:SigNoz/signoz into feat/deployment-markers 2025-10-24 18:03:11 +05:30
aks07
141380f1c7 feat: add tooltip to markers 2025-10-17 18:00:17 +05:30
aks07
7ba6a56115 feat: vertical markers plugin init 2025-10-17 02:26:52 +05:30
14 changed files with 965 additions and 24 deletions

View File

@@ -27,6 +27,7 @@ import { IUser } from 'providers/App/types';
import { DashboardProvider } from 'providers/Dashboard/Dashboard';
import { ErrorModalProvider } from 'providers/ErrorModalProvider';
import { KBarCommandPaletteProvider } from 'providers/KBarCommandPaletteProvider';
import { MarkersProvider } from 'providers/Markers/Markers';
import { PreferenceContextProvider } from 'providers/preferences/context/PreferenceContextProvider';
import { QueryBuilderProvider } from 'providers/QueryBuilder';
import { Suspense, useCallback, useEffect, useState } from 'react';
@@ -379,30 +380,34 @@ function App(): JSX.Element {
<PrivateRoute>
<ResourceProvider>
<QueryBuilderProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<PreferenceContextProvider>
<Suspense fallback={<Spinner size="large" tip="Loading..." />}>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</PreferenceContextProvider>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
<MarkersProvider>
<DashboardProvider>
<KeyboardHotkeysProvider>
<AlertRuleProvider>
<AppLayout>
<PreferenceContextProvider>
<Suspense
fallback={<Spinner size="large" tip="Loading..." />}
>
<Switch>
{routes.map(({ path, component, exact }) => (
<Route
key={`${path}`}
exact={exact}
path={path}
component={component}
/>
))}
<Route exact path="/" component={Home} />
<Route path="*" component={NotFound} />
</Switch>
</Suspense>
</PreferenceContextProvider>
</AppLayout>
</AlertRuleProvider>
</KeyboardHotkeysProvider>
</DashboardProvider>
</MarkersProvider>
</QueryBuilderProvider>
</ResourceProvider>
</PrivateRoute>

View File

@@ -0,0 +1,58 @@
.panel-markers-control {
padding: 16px;
.panel-markers-view-controller {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 16px;
}
.markers-control-skeleton {
display: flex;
align-items: center;
gap: 24px;
margin-top: 8px;
.ant-skeleton-input{
width: 230px;
}
}
.panel-markers-inputs-section {
display: flex;
align-items: center;
gap: 24px;
margin-top: 8px;
.panel-markers-select-container {
display: flex;
align-items: center;
margin-bottom: 8px;
.custom-multiselect-wrapper {
width: fit-content;
min-width: 230px;
}
}
}
}
.variable-name {
display: flex;
min-width: 56px;
height: 32px;
padding: 6px 6px 6px 8px;
align-items: center;
gap: 4px;
border-radius: 2px 0px 0px 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
color: var(--bg-robin-300);
font-family: Inter;
font-size: 12px;
font-style: normal;
font-weight: 400;
line-height: 16px;
.info-icon {
margin-left: 4px;
color: var(--bg-vanilla-400);
}
}

View File

@@ -0,0 +1,3 @@
export const MARKER_TYPES = {
DEPLOYMENT: 'deployment',
};

View File

@@ -0,0 +1,70 @@
import type { Dispatch } from 'react';
import { useReducer } from 'react';
import type { MarkerControlState, MarkerQueryState } from '../types';
export const MARKER_ACTIONS = {
TOGGLE_SHOW_MARKERS: 'toggleShowMarkers',
SET_MARKER_SERVICES: 'setMarkerServices',
SET_MARKER_TYPES: 'setMarkerTypes',
SET_DEFAULTS_ON: 'setDefaultsOn',
RESET: 'reset',
} as const;
export type MarkerActionType = typeof MARKER_ACTIONS[keyof typeof MARKER_ACTIONS];
export type MarkerControlAction =
| { type: typeof MARKER_ACTIONS.TOGGLE_SHOW_MARKERS; payload: boolean }
| { type: typeof MARKER_ACTIONS.SET_MARKER_SERVICES; payload: string[] }
| { type: typeof MARKER_ACTIONS.SET_MARKER_TYPES; payload: string[] }
| {
type: typeof MARKER_ACTIONS.SET_DEFAULTS_ON;
payload: { markerServices: string[]; markerTypes: string[] };
}
| { type: typeof MARKER_ACTIONS.RESET };
function normalizeInitialState(
state: MarkerQueryState | null,
): MarkerControlState {
return {
showMarkers: state?.showMarkers ? 1 : 0,
markerServices: state?.markerServices || [],
markerTypes: state?.markerTypes || [],
};
}
function reducer(
state: MarkerControlState,
action: MarkerControlAction,
): MarkerControlState {
switch (action.type) {
case MARKER_ACTIONS.TOGGLE_SHOW_MARKERS:
return { ...state, showMarkers: action.payload ? 1 : 0 };
case MARKER_ACTIONS.SET_MARKER_SERVICES:
return { ...state, markerServices: action.payload };
case MARKER_ACTIONS.SET_MARKER_TYPES:
return { ...state, markerTypes: action.payload };
case MARKER_ACTIONS.SET_DEFAULTS_ON:
return {
...state,
showMarkers: 1,
markerServices: action.payload.markerServices,
markerTypes: action.payload.markerTypes,
};
case MARKER_ACTIONS.RESET:
return { showMarkers: 0, markerServices: [], markerTypes: [] };
default:
return state;
}
}
export default function useMarkerControlState(
initialQueryState: MarkerQueryState | null,
): {
store: MarkerControlState;
dispatch: Dispatch<MarkerControlAction>;
} {
const initial = normalizeInitialState(initialQueryState);
const [store, dispatch] = useReducer(reducer, initial);
return { store, dispatch };
}

View File

@@ -0,0 +1,57 @@
import {
clearLocalStorageState,
getLocalStorageState,
getMarkerStateFromQuery,
getQueryParamsFromState,
setLocalStorageState,
} from 'components/PanelMarkersControl/utils';
import useUrlQuery from 'hooks/useUrlQuery';
import { useCallback, useEffect } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
type MarkerHandlers = {
onMarkerToggleOn: () => void;
onMarkerToggleOff: () => void;
};
const useMarkerHandlers = ({ key }: { key: string }): MarkerHandlers => {
const urlQuery = useUrlQuery();
const { search } = useLocation();
const history = useHistory();
// useEffect to sync url query with local storage
useEffect(() => {
const queryState = getMarkerStateFromQuery(urlQuery);
const localStorageState = getLocalStorageState(key);
if (queryState === null && localStorageState?.showMarkers) {
const params = new URLSearchParams(search);
const queryParams = getQueryParamsFromState(params, localStorageState);
history.replace({ search: queryParams.toString() });
} else {
setLocalStorageState(key, queryState);
}
}, [urlQuery, key, search, history]);
const onMarkerToggleOn = useCallback(() => {
// set defaults for service and marker type
const params = new URLSearchParams(search);
params.set('showMarkers', '1');
history.replace({ search: params.toString() });
}, [search, history]);
const onMarkerToggleOff = useCallback(() => {
// important to clear both url query and local storage here. Else url local storage sync useEffect will not work as expected.
clearLocalStorageState(key);
const params = new URLSearchParams(search);
params.delete('showMarkers');
params.delete('markerServices');
params.delete('markerTypes');
history.replace({ search: params.toString() });
}, [key, search, history]);
return { onMarkerToggleOn, onMarkerToggleOff };
};
export default useMarkerHandlers;

View File

@@ -0,0 +1,234 @@
import './PanelMarkersControl.scss';
import { Skeleton, Switch, Typography } from 'antd';
import CustomMultiSelect from 'components/NewSelect/CustomMultiSelect';
import { MARKER_TYPES } from 'components/PanelMarkersControl/constants';
import useMarkerControlState, {
MARKER_ACTIONS,
} from 'components/PanelMarkersControl/hooks/useMarkerControlState';
import useMarkerHandlers from 'components/PanelMarkersControl/hooks/useMarkerHandlers';
import {
getInitialStateForControls,
getQueryParamsFromState,
} from 'components/PanelMarkersControl/utils';
import useUrlQuery from 'hooks/useUrlQuery';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useFetchMarkersData, useMarkers } from 'providers/Markers/Markers';
import { useCallback, useEffect, useMemo } from 'react';
import { useHistory, useLocation } from 'react-router-dom';
function PanelMarkersControl(): JSX.Element {
const urlQuery = useUrlQuery();
const { search } = useLocation();
const history = useHistory();
const { selectedDashboard } = useDashboard();
const { store: markerControlState, dispatch } = useMarkerControlState(
getInitialStateForControls(selectedDashboard?.id || '', urlQuery),
);
const { markersData, setMarkersData } = useMarkers();
const { loadingMarkers } = useFetchMarkersData({
isFetchEnabled: markerControlState.showMarkers === 1,
});
const { onMarkerToggleOn, onMarkerToggleOff } = useMarkerHandlers({
key: selectedDashboard?.id || '',
});
// API integration: check if this is correct
const markerTypeOptions = useMemo(() => {
const uniqueTypes = Array.from(
new Set((markersData || []).map((m: any) => m?.type).filter(Boolean)),
);
return uniqueTypes.map((t: string) => ({
label: t.charAt(0).toUpperCase() + t.slice(1),
value: t,
}));
}, [markersData]);
// API integration: check if this is correct
const serviceNameOptions = useMemo(() => {
const uniqueServices = Array.from(
new Set(
(markersData || [])
.map((m: any) => m?.attr?.['service.name'])
.filter(Boolean),
),
);
return uniqueServices.map((s: string) => ({ label: s, value: s }));
}, [markersData]);
const handleServiceChange = useCallback(
(serviceOrServices: string | string[] | undefined): void => {
let servicesArray: string[] = [];
if (Array.isArray(serviceOrServices)) {
servicesArray = serviceOrServices;
} else if (serviceOrServices) {
servicesArray = [serviceOrServices];
}
dispatch({
type: MARKER_ACTIONS.SET_MARKER_SERVICES,
payload: servicesArray,
});
// sync URL param
const params = new URLSearchParams(search);
if (servicesArray.length > 0) {
params.set('markerServices', servicesArray.join(','));
} else {
params.delete('markerServices');
}
history.replace({ search: params.toString() });
},
[history, search, dispatch],
);
const handleMarkerTypesChange = useCallback(
(typesOrArray: string | string[] | undefined): void => {
let typesArray: string[] = [];
if (Array.isArray(typesOrArray)) {
typesArray = typesOrArray;
} else if (typesOrArray) {
typesArray = [typesOrArray];
}
dispatch({ type: MARKER_ACTIONS.SET_MARKER_TYPES, payload: typesArray });
const params = new URLSearchParams(search);
if (typesArray.length > 0) {
params.set('markerTypes', typesArray.join(','));
} else {
params.delete('markerTypes');
}
history.replace({ search: params.toString() });
},
[history, search, dispatch],
);
const handleToggleShowMarkers = useCallback(
(checked: boolean): void => {
dispatch({ type: MARKER_ACTIONS.TOGGLE_SHOW_MARKERS, payload: checked });
if (checked) {
// get default services and marker types from markersData
onMarkerToggleOn();
} else {
// consider using useReducer to reset the state
setMarkersData([]);
dispatch({ type: MARKER_ACTIONS.RESET });
onMarkerToggleOff();
}
},
[onMarkerToggleOn, onMarkerToggleOff, dispatch, setMarkersData],
);
useEffect(() => {
if (markersData.length < 1) return;
/**
* On markers data change, derive defaults (or use query selections if present)
* and set them in a single reducer dispatch.
*/
const queryMarkerServicesRaw = urlQuery.get('markerServices') || '';
const queryMarkerTypesRaw = urlQuery.get('markerTypes') || '';
const defMarkerServices = ['cart-service'];
const defMarkerTypes = [MARKER_TYPES.DEPLOYMENT];
const servicesArray = queryMarkerServicesRaw
? queryMarkerServicesRaw
.split(',')
.map((s) => s.trim())
.filter((s) => s.length > 0)
: defMarkerServices;
const typesArray = queryMarkerTypesRaw
? queryMarkerTypesRaw
.split(',')
.map((t) => t.trim())
.filter((t) => t.length > 0)
: defMarkerTypes;
dispatch({
type: MARKER_ACTIONS.SET_DEFAULTS_ON,
payload: { markerServices: servicesArray, markerTypes: typesArray },
});
// reflect in URL params as well
const params = new URLSearchParams(search);
const queryParams = getQueryParamsFromState(params, {
showMarkers: 1,
markerServices: servicesArray,
markerTypes: typesArray,
});
history.replace({ search: queryParams.toString() });
console.log('>>> markersData', markersData);
// urlQuery removed from dependencies as not able to unset markerTypes. But works with markerServices. [CHECK THIS]
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [markersData]);
// ADD INITIAL STATE FOR SELECTED SERVICES AND MARKER TYPES
return (
<div className="panel-markers-control">
<div className="panel-markers-view-controller">
<Typography>Show Markers</Typography>
<Switch
size="small"
checked={markerControlState.showMarkers === 1}
onChange={handleToggleShowMarkers}
/>
</div>
{markerControlState.showMarkers === 1 && (
<div className="panel-markers-inputs-section">
{loadingMarkers ? (
<div className="markers-control-skeleton">
<Skeleton.Input active size="small" />
<Skeleton.Input active size="small" />
</div>
) : (
<>
<div className="panel-markers-select-container">
<Typography.Text className="variable-name" ellipsis>
Marker type
</Typography.Text>
<CustomMultiSelect
className="panel-markers-select"
placeholder="Select one or more marker types"
enableAllSelection={false}
options={markerTypeOptions}
maxTagCount={3}
value={markerControlState.markerTypes}
onChange={handleMarkerTypesChange}
/>
</div>
<div className="panel-markers-select-container">
<Typography.Text className="variable-name" ellipsis>
Service name
</Typography.Text>
<CustomMultiSelect
className="panel-markers-select"
placeholder="Select one or more service names"
maxTagCount={3}
enableAllSelection={false}
options={serviceNameOptions}
value={markerControlState.markerServices}
onChange={handleServiceChange}
/>
</div>
</>
)}
</div>
)}
</div>
);
}
// select bright color for the markers
// convert panel marker state to useReducer
// removed urlQuery from dependencies.
// filters on markersData should work properly. If no markers selected. Show no markers.
export default PanelMarkersControl;

View File

@@ -0,0 +1,7 @@
export interface MarkerControlState {
showMarkers: number;
markerServices: string[];
markerTypes: string[];
}
export type MarkerQueryState = MarkerControlState | null;

View File

@@ -0,0 +1,108 @@
import type { MarkerQueryState } from 'components/PanelMarkersControl/types';
import { LOCALSTORAGE } from 'constants/localStorage';
// CHECK LOGIC AND CREATE UTIL
export function getMarkerStateFromQuery(
urlQuery: URLSearchParams,
): MarkerQueryState | null {
const showMarkers = urlQuery.get('showMarkers') === '1';
const servicesRaw = urlQuery.get('markerServices') || '';
if (!showMarkers) {
return null;
}
const markerTypesParam = urlQuery.get('markerTypes') || '';
const markerTypes = markerTypesParam
.split(',')
.map((t) => t.trim())
.filter((t) => t.length > 0);
const markerServices = servicesRaw
.split(',')
.map((s) => s.trim())
.filter((s) => s.length > 0);
return {
showMarkers: 1,
markerServices,
markerTypes,
};
}
export const getLocalStorageState = (key: string): MarkerQueryState | null => {
const raw = localStorage.getItem(LOCALSTORAGE.MARKERS_OVERLAY_STATE);
try {
const parsed = raw ? JSON.parse(raw) : null;
return parsed?.[key] ?? null;
} catch (_) {
return null;
}
};
export const getQueryParamsFromState = (
params: URLSearchParams,
state: MarkerQueryState,
): URLSearchParams => {
if (!state) {
return params;
}
if (state.showMarkers) {
params.set('showMarkers', String(state.showMarkers) || '0');
}
if (Array.isArray(state.markerServices) && state.markerServices.length > 0) {
params.set('markerServices', state.markerServices.join(','));
}
if (Array.isArray(state.markerTypes) && state.markerTypes.length > 0) {
params.set('markerTypes', state.markerTypes.join(','));
}
return params;
};
export const setLocalStorageState = (
key: string,
state: MarkerQueryState | null,
): void => {
if (!key || key.trim().length === 0) {
return;
}
try {
const raw = localStorage.getItem(LOCALSTORAGE.MARKERS_OVERLAY_STATE);
let obj: Record<string, unknown> = {};
try {
obj = raw ? JSON.parse(raw) : {};
} catch (_) {
obj = {};
}
obj[key] = state;
localStorage.setItem(LOCALSTORAGE.MARKERS_OVERLAY_STATE, JSON.stringify(obj));
} catch (_) {
// ignore storage errors
}
};
export const getInitialStateForControls = (
key: string,
urlQuery: URLSearchParams,
): MarkerQueryState | null => {
const queryState = getMarkerStateFromQuery(urlQuery);
const localStorageState = getLocalStorageState(key);
return queryState ?? localStorageState;
};
export const clearLocalStorageState = (key: string): void => {
if (!key || key.trim().length === 0) {
return;
}
const raw = localStorage.getItem(LOCALSTORAGE.MARKERS_OVERLAY_STATE);
let obj: Record<string, unknown> = {};
try {
obj = raw ? JSON.parse(raw) : {};
} catch (_) {
obj = {};
}
delete obj[key];
localStorage.setItem(LOCALSTORAGE.MARKERS_OVERLAY_STATE, JSON.stringify(obj));
};

View File

@@ -0,0 +1,177 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable sonarjs/no-duplicate-string */
import uPlot from 'uplot';
type MarkersData = { id: string | number; val: number; stroke?: string };
export function verticalMarkersPlugin({
markersData = [],
lineType = [5, 3],
width = 1,
}: {
markersData?: MarkersData[];
lineType?: number[];
width?: number;
} = {}): uPlot.Plugin {
const DEFAULT_STROKE = 'rgba(0, 102, 255, 0.95)';
let removeListeners: (() => void) | null = null;
let tooltipEl: HTMLDivElement | null = null;
const renderAxisMarkers = (uu: uPlot): void => {
const axes = uu.root.querySelectorAll('.u-axis');
const xAxis = (axes && (axes[0] as HTMLElement)) || null;
if (!xAxis) return;
// attach delegated hover/mouseout listeners once on the x-axis container
if (!(xAxis as HTMLElement).dataset?.vlineHoverAttached) {
const onMouseOver = (e: MouseEvent): void => {
const target = e.target as HTMLElement;
if (!target?.classList?.contains('vline-triangle-marker')) return;
const { id } = target.dataset;
const valStr = target.dataset.val;
const val = valStr ? Number(valStr) : undefined;
// const mData = markersData.find((d) => String(d.id) === String(id));
// create tooltip
if (!tooltipEl) {
tooltipEl = document.createElement('div');
tooltipEl.className = 'vline-marker-tooltip';
Object.assign(tooltipEl.style, {
position: 'fixed',
padding: '6px 8px',
borderRadius: '4px',
fontSize: '12px',
zIndex: '10000',
pointerEvents: 'none',
background: '#111827',
color: '#e5e7eb',
border: '1px solid #374151',
});
document.body.appendChild(tooltipEl);
}
tooltipEl.textContent = `id: ${id ?? ''} • ts: ${val ?? ''}`;
// position near cursor
tooltipEl.style.left = `${e.clientX + 10}px`;
tooltipEl.style.top = `${e.clientY - 28}px`;
};
const onMouseOut = (e: MouseEvent): void => {
const target = e.target as HTMLElement;
if (!target?.classList?.contains('vline-triangle-marker')) return;
if (tooltipEl) {
tooltipEl.remove();
tooltipEl = null;
}
};
xAxis.addEventListener('mouseover', onMouseOver);
xAxis.addEventListener('mouseout', onMouseOut);
removeListeners = (): void => {
xAxis.removeEventListener('mouseover', onMouseOver);
xAxis.removeEventListener('mouseout', onMouseOut);
};
(xAxis as HTMLElement).dataset.vlineHoverAttached = '1';
}
// cleanup markers to avoid duplicates on rerender/resize
xAxis.querySelectorAll('.vline-triangle-marker').forEach((el) => el.remove());
const plotLeft = uu.bbox.left;
const plotRight = plotLeft + uu.bbox.width;
for (let i = 0; i < markersData.length; i++) {
const mData = markersData[i];
const xAbs = uu.valToPos(mData.val, 'x', true);
if (xAbs >= plotLeft && xAbs <= plotRight) {
const xPx = (xAbs - plotLeft) / window.devicePixelRatio;
const marker = document.createElement('div');
marker.className = 'vline-triangle-marker';
marker.dataset.id = String(mData.id); // may change later after BE discussion
marker.dataset.val = String(mData.val); // TODO: remove this later
Object.assign(marker.style, {
position: 'absolute',
width: '0px',
height: '0px',
borderLeft: '5px solid transparent',
borderRight: '5px solid transparent',
borderBottomWidth: '5px',
borderBottomStyle: 'solid',
borderBottomColor: mData.stroke || DEFAULT_STROKE,
transform: 'translateX(-50%)',
cursor: 'pointer',
zIndex: '1',
left: `${xPx}px`,
});
xAxis.appendChild(marker);
}
}
};
return {
hooks: {
destroy: [
(): void => {
if (tooltipEl) {
tooltipEl.remove();
tooltipEl = null;
}
if (removeListeners) {
removeListeners();
removeListeners = null;
}
},
],
drawAxes: [
(uu: uPlot): void => {
renderAxisMarkers(uu);
},
],
draw: [
(uu: uPlot): void => {
const { ctx } = uu;
const { top } = uu.bbox;
const bottom = top + uu.bbox.height;
const plotLeft = uu.bbox.left;
const plotRight = plotLeft + uu.bbox.width;
ctx.save();
for (let i = 0; i < markersData.length; i++) {
const mData = markersData[i];
const x = uu.valToPos(mData.val, 'x', true);
// only draw if within plot bounds
if (x >= plotLeft && x <= plotRight) {
ctx.beginPath();
ctx.strokeStyle = mData.stroke || DEFAULT_STROKE;
ctx.lineWidth = width;
ctx.setLineDash(lineType || []);
ctx.moveTo(x, top);
ctx.lineTo(x, bottom);
ctx.stroke();
}
}
ctx.restore();
},
],
},
};
}
// MOVE TO REACT. or use portal for tooltip.
// correct the format should work with expected data from BE
// Remove cognitive complexity rule.
// logic to get marker plugin to be added to context
// depending on type of marker parse the data to choose color. example deployment should be red etc.
// pass such data with multple colors to check render.
// PERF CHECK.
// drive data passing from the context for markers. data in this is enriched only when we
// use <ShowMarkers /> component. else it will be empty.
// plugins. and pass marker hooks using this
// should only pass marker hook if data is present(shouldRenderMarker memo)
// depending on type of marker parse the data to choose color. example deployment should be red etc.
// pass such data with multple colors to check render.
// PERF CHECK.
// drive data passing from the context for markers. data in this is enriched only when we
// use <ShowMarkers /> component. else it will be empty.

View File

@@ -36,4 +36,5 @@ export enum LOCALSTORAGE {
LAST_USED_CUSTOM_TIME_RANGES = 'LAST_USED_CUSTOM_TIME_RANGES',
SHOW_FREQUENCY_CHART = 'SHOW_FREQUENCY_CHART',
DISSMISSED_COST_METER_INFO = 'DISMISSED_COST_METER_INFO',
MARKERS_OVERLAY_STATE = 'MARKERS_OVERLAY_STATE',
}

View File

@@ -13,6 +13,7 @@ import {
} from 'antd';
import logEvent from 'api/common/logEvent';
import HeaderRightSection from 'components/HeaderRightSection/HeaderRightSection';
import PanelMarkersControl from 'components/PanelMarkersControl';
import { QueryParams } from 'constants/query';
import { PANEL_GROUP_TYPES, PANEL_TYPES } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
@@ -488,6 +489,7 @@ function DashboardDescription(props: DashboardDescriptionProps): JSX.Element {
<DashboardVariableSelection />
</section>
)}
<PanelMarkersControl />
<DashboardGraphSlider />
<Modal

View File

@@ -1,8 +1,11 @@
/* eslint-disable sonarjs/no-duplicate-string */
import './UplotPanelWrapper.styles.scss';
import { Alert } from 'antd';
import { ToggleGraphProps } from 'components/Graph/types';
import Uplot from 'components/Uplot';
import { verticalMarkersPlugin } from 'components/Uplot/plugins/verticalMarkersPlugin';
import { PANEL_TYPES } from 'constants/queryBuilder';
import GraphManager from 'container/GridCardLayout/GridCard/FullView/GraphManager';
import { getLocalStorageGraphVisibilityState } from 'container/GridCardLayout/GridCard/utils';
@@ -17,6 +20,7 @@ import { cloneDeep, isEqual, isUndefined } from 'lodash-es';
import _noop from 'lodash-es/noop';
import { ContextMenu, useCoordinates } from 'periscope/components/ContextMenu';
import { useDashboard } from 'providers/Dashboard/Dashboard';
import { useMarkers } from 'providers/Markers/Markers';
import { useTimezone } from 'providers/Timezone';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { DataSource } from 'types/common/queryBuilder';
@@ -55,6 +59,32 @@ function UplotPanelWrapper({
const [minTimeScale, setMinTimeScale] = useState<number>();
const [maxTimeScale, setMaxTimeScale] = useState<number>();
const { currentQuery } = useQueryBuilder();
const { filteredMarkersData, shouldShowMarkers } = useMarkers();
const markersPlugin: uPlot.Plugin | null = useMemo(() => {
if (shouldShowMarkers) {
console.log('*** filteredMarkersData', filteredMarkersData);
console.log('*** shouldShowMarkers', {
markersData: [
{ id: 'm1', val: 1760625000, stroke: 'rgba(96, 255, 128, 0.95)' },
{ id: 'm2', val: 1760630000, stroke: 'rgba(255, 96, 96, 0.95)' },
{ id: 'm3', val: 1760640000, stroke: 'rgba(255, 96, 96, 0.95)' },
],
lineType: [6, 4],
width: 1,
});
return verticalMarkersPlugin({
markersData: [
{ id: 'm1', val: 1760625000, stroke: 'rgba(96, 255, 128, 0.95)' },
{ id: 'm2', val: 1760630000, stroke: 'rgba(255, 96, 96, 0.95)' },
{ id: 'm3', val: 1760640000, stroke: 'rgba(255, 96, 96, 0.95)' },
],
lineType: [6, 4],
width: 1,
});
}
return null;
}, [shouldShowMarkers, filteredMarkersData]);
const [hiddenGraph, setHiddenGraph] = useState<{ [key: string]: boolean }>();
@@ -249,6 +279,7 @@ function UplotPanelWrapper({
}) => {
legendScrollPositionRef.current = position;
},
customPlugins: [...(markersPlugin ? [markersPlugin] : [])],
}),
[
queryResponse.data?.payload,
@@ -270,6 +301,7 @@ function UplotPanelWrapper({
onClickHandler,
widget,
stackedBarChart,
markersPlugin,
],
);

View File

@@ -87,6 +87,7 @@ export interface GetUPlotChartOptions {
scrollTop: number;
scrollLeft: number;
}) => void;
customPlugins?: uPlot.Plugin[];
}
/** the function converts series A , series B , series C to
@@ -218,6 +219,7 @@ export const getUPlotChartOptions = ({
query,
legendScrollPosition,
setLegendScrollPosition,
customPlugins = [],
}: GetUPlotChartOptions): uPlot.Options => {
const timeScaleProps = getXAxisScale(minTimeScale, maxTimeScale);
@@ -387,6 +389,7 @@ export const getUPlotChartOptions = ({
],
},
},
...customPlugins,
],
hooks: {
draw: [

View File

@@ -0,0 +1,184 @@
import useUrlQuery from 'hooks/useUrlQuery';
import { createContext, useContext, useEffect, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
type Marker = {
type: string;
env: string;
source: string;
attr: {
version: string;
firstSeen: number;
'service.name': string;
region: string;
spanCount: number;
};
};
function generateMockMarkers(
minTime: number,
maxTime: number,
count = 10,
): { markers: Marker[] } {
const step = (maxTime - minTime) / count;
const services = [
'cart-service',
'checkout-service',
'inventory-service',
'auth-service',
'payment-service',
'recommendation-service',
'search-service',
'user-service',
'notification-service',
'analytics-service',
];
const regions = [
'us-east-1',
'us-west-1',
'us-east-2',
'eu-central-1',
'ap-southeast-1',
'eu-west-1',
];
const envs = ['prod-us', 'prod-eu', 'staging', 'prod-ap'];
const sources = ['traces', 'logs'];
const markers: Marker[] = Array.from({ length: count }).map((_, i) => {
const firstSeen = Math.round(minTime + i * step);
const service = services[i % services.length];
const env = envs[i % envs.length];
const region = regions[i % regions.length];
const source = sources[i % sources.length];
return {
type: 'deployment',
env,
source,
attr: {
version: `1.${160 + i}.0-${env.includes('prod') ? 'prod' : 'stg'}`,
firstSeen,
'service.name': service,
region,
spanCount: 100 + Math.floor(Math.random() * 150),
},
};
});
return { markers };
}
type MarkersContextValue = {
markersData: Marker[];
filteredMarkersData: Marker[];
setMarkersData: (markersData: Marker[]) => void;
shouldShowMarkers: boolean;
};
const MarkersContext = createContext<MarkersContextValue | undefined>(
undefined,
);
export function MarkersProvider({
children,
}: {
children: React.ReactNode;
}): JSX.Element {
const [markersData, setMarkersData] = useState<Marker[]>([]);
const urlQuery = useUrlQuery();
// CHECK LOGIC AND CREATE UTIL
const filteredMarkersData = useMemo(() => {
const servicesRaw = urlQuery.get('markerServices') || '';
const typesRaw = urlQuery.get('markerTypes') || '';
const selectedServices = servicesRaw
.split(',')
.map((s) => s.trim())
.filter((s) => s.length > 0);
const selectedTypes = typesRaw
.split(',')
.map((t) => t.trim())
.filter((t) => t.length > 0);
if (selectedServices.length === 0 && selectedTypes.length === 0) {
return markersData;
}
return (markersData || []).filter((m: Marker) => {
const typeOk = selectedTypes.includes(m?.type);
const serviceOk = selectedServices.includes(m?.attr?.['service.name']);
return typeOk && serviceOk;
});
}, [urlQuery, markersData]);
const shouldShowMarkers = useMemo(() => !!urlQuery.get('showMarkers'), [
urlQuery,
]);
console.log('*** filteredMarkersData', filteredMarkersData);
const value = useMemo(
() => ({
markersData,
filteredMarkersData,
setMarkersData,
shouldShowMarkers,
}),
[markersData, filteredMarkersData, setMarkersData, shouldShowMarkers],
);
return (
<MarkersContext.Provider value={value}>{children}</MarkersContext.Provider>
);
}
export function useMarkers(): MarkersContextValue {
const ctx = useContext(MarkersContext);
if (!ctx) {
throw new Error('useMarkers must be used within a MarkersProvider');
}
return ctx;
}
export function useFetchMarkersData({
isFetchEnabled,
}: {
isFetchEnabled: boolean;
}): { loadingMarkers: boolean } {
const { setMarkersData } = useMarkers();
const { maxTime, minTime } = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const { data, isLoading, isFetching } = useQuery<{ markers: Marker[] }>(
['mock-markers', minTime, maxTime],
async () => {
// simulate network latency without returning from the executor
await new Promise<void>((resolve) => {
setTimeout(resolve, 2000);
});
return generateMockMarkers(minTime, maxTime, 10);
},
{
enabled: isFetchEnabled,
},
);
useEffect(() => {
if (data) {
console.log('*** setting markers data', data.markers);
setMarkersData(data.markers);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [data]);
return {
loadingMarkers: isLoading || isFetching,
};
}
export default MarkersProvider;