mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
GraphNG: Remove plot context (#38928)
* Remove plot ctx usage: Tooltip plugin * Remove plot ctx usage: Context menu plugin * Remove plot ctx usage: Annotations plugin * Remove plot ctx usage: Exemplars plugin * Remove plot ctx usage: EventsCanvas plugin * Remove plot ctx usage: EventsCanvas/XYCanvas plugin * Remove plot ctx usage: AnnotationEditor plugin * Remove plot ctx usage: AnnotationMarker * Remove plot context * Do not throw react warnings from uPlot performed hooks
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import React, { useLayoutEffect, useState, useCallback } from 'react';
|
||||
|
||||
import { UPlotConfigBuilder, PlotSelection, usePlotContext } from '@grafana/ui';
|
||||
import { CartesianCoords2D, DataFrame, TimeZone } from '@grafana/data';
|
||||
import { PlotSelection, UPlotConfigBuilder } from '@grafana/ui';
|
||||
import React, { useCallback, useLayoutEffect, useRef, useState } from 'react';
|
||||
import { useMountedState } from 'react-use';
|
||||
import { AnnotationEditor } from './annotations/AnnotationEditor';
|
||||
|
||||
type StartAnnotatingFn = (props: {
|
||||
@@ -20,22 +20,50 @@ interface AnnotationEditorPluginProps {
|
||||
* @alpha
|
||||
*/
|
||||
export const AnnotationEditorPlugin: React.FC<AnnotationEditorPluginProps> = ({ data, timeZone, config, children }) => {
|
||||
const plotCtx = usePlotContext();
|
||||
const plotInstance = useRef<uPlot>();
|
||||
const [bbox, setBbox] = useState<DOMRect>();
|
||||
const [isAddingAnnotation, setIsAddingAnnotation] = useState(false);
|
||||
const [selection, setSelection] = useState<PlotSelection | null>(null);
|
||||
const isMounted = useMountedState();
|
||||
|
||||
const clearSelection = useCallback(() => {
|
||||
setSelection(null);
|
||||
const plotInstance = plotCtx.plot;
|
||||
if (plotInstance) {
|
||||
plotInstance.setSelect({ top: 0, left: 0, width: 0, height: 0 });
|
||||
|
||||
if (plotInstance.current) {
|
||||
plotInstance.current.setSelect({ top: 0, left: 0, width: 0, height: 0 });
|
||||
}
|
||||
setIsAddingAnnotation(false);
|
||||
}, [plotCtx]);
|
||||
}, [setIsAddingAnnotation, setSelection]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
let annotating = false;
|
||||
|
||||
config.addHook('init', (u) => {
|
||||
plotInstance.current = u;
|
||||
// Wrap all setSelect hooks to prevent them from firing if user is annotating
|
||||
const setSelectHooks = u.hooks.setSelect;
|
||||
|
||||
if (setSelectHooks) {
|
||||
for (let i = 0; i < setSelectHooks.length; i++) {
|
||||
const hook = setSelectHooks[i];
|
||||
|
||||
if (hook !== setSelect) {
|
||||
setSelectHooks[i] = (...args) => {
|
||||
!annotating && hook!(...args);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// cache uPlot plotting area bounding box
|
||||
config.addHook('syncRect', (u, rect) => {
|
||||
if (!isMounted()) {
|
||||
return;
|
||||
}
|
||||
setBbox(rect);
|
||||
});
|
||||
|
||||
const setSelect = (u: uPlot) => {
|
||||
if (annotating) {
|
||||
setIsAddingAnnotation(true);
|
||||
@@ -55,23 +83,6 @@ export const AnnotationEditorPlugin: React.FC<AnnotationEditorPluginProps> = ({
|
||||
|
||||
config.addHook('setSelect', setSelect);
|
||||
|
||||
config.addHook('init', (u) => {
|
||||
// Wrap all setSelect hooks to prevent them from firing if user is annotating
|
||||
const setSelectHooks = u.hooks.setSelect;
|
||||
|
||||
if (setSelectHooks) {
|
||||
for (let i = 0; i < setSelectHooks.length; i++) {
|
||||
const hook = setSelectHooks[i];
|
||||
|
||||
if (hook !== setSelect) {
|
||||
setSelectHooks[i] = (...args) => {
|
||||
!annotating && hook!(...args);
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
config.setCursor({
|
||||
bind: {
|
||||
mousedown: (u, targ, handler) => (e) => {
|
||||
@@ -91,21 +102,15 @@ export const AnnotationEditorPlugin: React.FC<AnnotationEditorPluginProps> = ({
|
||||
},
|
||||
},
|
||||
});
|
||||
}, [config]);
|
||||
}, [config, setBbox, isMounted]);
|
||||
|
||||
const startAnnotating = useCallback<StartAnnotatingFn>(
|
||||
({ coords }) => {
|
||||
if (!plotCtx || !plotCtx.plot || !coords) {
|
||||
if (!plotInstance.current || !bbox || !coords) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bbox = plotCtx.getCanvasBoundingBox();
|
||||
|
||||
if (!bbox) {
|
||||
return;
|
||||
}
|
||||
|
||||
const min = plotCtx.plot.posToVal(coords.plotCanvas.x, 'x');
|
||||
const min = plotInstance.current.posToVal(coords.plotCanvas.x, 'x');
|
||||
|
||||
if (!min) {
|
||||
return;
|
||||
@@ -123,18 +128,25 @@ export const AnnotationEditorPlugin: React.FC<AnnotationEditorPluginProps> = ({
|
||||
});
|
||||
setIsAddingAnnotation(true);
|
||||
},
|
||||
[plotCtx]
|
||||
[bbox]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{isAddingAnnotation && selection && (
|
||||
{isAddingAnnotation && selection && bbox && (
|
||||
<AnnotationEditor
|
||||
selection={selection}
|
||||
onDismiss={clearSelection}
|
||||
onSave={clearSelection}
|
||||
data={data}
|
||||
timeZone={timeZone}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: `${bbox.top}px`,
|
||||
left: `${bbox.left}px`,
|
||||
width: `${bbox.width}px`,
|
||||
height: `${bbox.height}px`,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{children ? children({ startAnnotating }) : null}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { colorManipulator, DataFrame, DataFrameFieldIndex, DataFrameView, TimeZone } from '@grafana/data';
|
||||
import { EventsCanvas, UPlotConfigBuilder, usePlotContext, useTheme } from '@grafana/ui';
|
||||
import { EventsCanvas, UPlotConfigBuilder, useTheme2 } from '@grafana/ui';
|
||||
import React, { useCallback, useEffect, useLayoutEffect, useRef } from 'react';
|
||||
import { AnnotationMarker } from './annotations/AnnotationMarker';
|
||||
|
||||
@@ -10,8 +10,8 @@ interface AnnotationsPluginProps {
|
||||
}
|
||||
|
||||
export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotations, timeZone, config }) => {
|
||||
const theme = useTheme();
|
||||
const plotCtx = usePlotContext();
|
||||
const theme = useTheme2();
|
||||
const plotInstance = useRef<uPlot>();
|
||||
|
||||
const annotationsRef = useRef<Array<DataFrameView<AnnotationsDataFrameViewDTO>>>();
|
||||
|
||||
@@ -27,6 +27,10 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
|
||||
}, [annotations]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
config.addHook('init', (u) => {
|
||||
plotInstance.current = u;
|
||||
});
|
||||
|
||||
config.addHook('draw', (u) => {
|
||||
// Render annotation lines on the canvas
|
||||
/**
|
||||
@@ -86,32 +90,47 @@ export const AnnotationsPlugin: React.FC<AnnotationsPluginProps> = ({ annotation
|
||||
});
|
||||
}, [config, theme]);
|
||||
|
||||
const mapAnnotationToXYCoords = useCallback(
|
||||
(frame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
|
||||
const view = new DataFrameView<AnnotationsDataFrameViewDTO>(frame);
|
||||
const annotation = view.get(dataFrameFieldIndex.fieldIndex);
|
||||
const plotInstance = plotCtx.plot;
|
||||
if (!annotation.time || !plotInstance) {
|
||||
return undefined;
|
||||
}
|
||||
let x = plotInstance.valToPos(annotation.time, 'x');
|
||||
const mapAnnotationToXYCoords = useCallback((frame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
|
||||
const view = new DataFrameView<AnnotationsDataFrameViewDTO>(frame);
|
||||
const annotation = view.get(dataFrameFieldIndex.fieldIndex);
|
||||
|
||||
if (x < 0) {
|
||||
x = 0;
|
||||
}
|
||||
return {
|
||||
x,
|
||||
y: plotInstance.bbox.height / window.devicePixelRatio + 4,
|
||||
};
|
||||
},
|
||||
[plotCtx]
|
||||
);
|
||||
if (!annotation.time || !plotInstance.current) {
|
||||
return undefined;
|
||||
}
|
||||
let x = plotInstance.current.valToPos(annotation.time, 'x');
|
||||
|
||||
if (x < 0) {
|
||||
x = 0;
|
||||
}
|
||||
return {
|
||||
x,
|
||||
y: plotInstance.current.bbox.height / window.devicePixelRatio + 4,
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderMarker = useCallback(
|
||||
(frame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
|
||||
let markerStyle;
|
||||
const view = new DataFrameView<AnnotationsDataFrameViewDTO>(frame);
|
||||
const annotation = view.get(dataFrameFieldIndex.fieldIndex);
|
||||
return <AnnotationMarker annotation={annotation} timeZone={timeZone} />;
|
||||
const isRegionAnnotation = Boolean(annotation.isRegion);
|
||||
|
||||
if (isRegionAnnotation && plotInstance.current) {
|
||||
let x0 = plotInstance.current.valToPos(annotation.time, 'x');
|
||||
let x1 = plotInstance.current.valToPos(annotation.timeEnd, 'x');
|
||||
|
||||
// markers are rendered relatively to uPlot canvas overly, not caring about axes width
|
||||
if (x0 < 0) {
|
||||
x0 = 0;
|
||||
}
|
||||
|
||||
if (x1 > plotInstance.current.bbox.width / window.devicePixelRatio) {
|
||||
x1 = plotInstance.current.bbox.width / window.devicePixelRatio;
|
||||
}
|
||||
markerStyle = { width: `${x1 - x0}px` };
|
||||
}
|
||||
|
||||
return <AnnotationMarker annotation={annotation} timeZone={timeZone} style={markerStyle} />;
|
||||
},
|
||||
[timeZone]
|
||||
);
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
MenuGroup,
|
||||
MenuItem,
|
||||
UPlotConfigBuilder,
|
||||
usePlotContext,
|
||||
} from '@grafana/ui';
|
||||
import { CartesianCoords2D, DataFrame, getFieldDisplayName, InterpolateFunction, TimeZone } from '@grafana/data';
|
||||
import { useClickAway } from 'react-use';
|
||||
@@ -40,7 +39,6 @@ export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({
|
||||
replaceVariables,
|
||||
...otherProps
|
||||
}) => {
|
||||
const plotCtx = usePlotContext();
|
||||
const plotCanvas = useRef<HTMLDivElement>();
|
||||
const [coords, setCoords] = useState<ContextMenuSelectionCoords | null>(null);
|
||||
const [point, setPoint] = useState<ContextMenuSelectionPoint | null>(null);
|
||||
@@ -61,8 +59,9 @@ export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({
|
||||
|
||||
// Add uPlot hooks to the config, or re-add when the config changed
|
||||
useLayoutEffect(() => {
|
||||
let bbox: DOMRect | undefined = undefined;
|
||||
|
||||
const onMouseCapture = (e: MouseEvent) => {
|
||||
const bbox = plotCtx.getCanvasBoundingBox();
|
||||
let update = {
|
||||
viewport: {
|
||||
x: e.clientX,
|
||||
@@ -85,6 +84,11 @@ export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({
|
||||
setCoords(update);
|
||||
};
|
||||
|
||||
// cache uPlot plotting area bounding box
|
||||
config.addHook('syncRect', (u, rect) => {
|
||||
bbox = rect;
|
||||
});
|
||||
|
||||
config.addHook('init', (u) => {
|
||||
const canvas = u.over;
|
||||
plotCanvas.current = canvas || undefined;
|
||||
@@ -137,7 +141,7 @@ export const ContextMenuPlugin: React.FC<ContextMenuPluginProps> = ({
|
||||
});
|
||||
}
|
||||
});
|
||||
}, [config, openMenu, setCoords, setPoint, plotCtx]);
|
||||
}, [config, openMenu, setCoords, setPoint]);
|
||||
|
||||
const defaultItems = useMemo(() => {
|
||||
return otherProps.defaultItems
|
||||
|
||||
@@ -7,8 +7,8 @@ import {
|
||||
TIME_SERIES_TIME_FIELD_NAME,
|
||||
TIME_SERIES_VALUE_FIELD_NAME,
|
||||
} from '@grafana/data';
|
||||
import { EventsCanvas, FIXED_UNIT, UPlotConfigBuilder, usePlotContext } from '@grafana/ui';
|
||||
import React, { useCallback } from 'react';
|
||||
import { EventsCanvas, FIXED_UNIT, UPlotConfigBuilder } from '@grafana/ui';
|
||||
import React, { useCallback, useLayoutEffect, useRef } from 'react';
|
||||
import { ExemplarMarker } from './ExemplarMarker';
|
||||
|
||||
interface ExemplarsPluginProps {
|
||||
@@ -19,42 +19,44 @@ interface ExemplarsPluginProps {
|
||||
}
|
||||
|
||||
export const ExemplarsPlugin: React.FC<ExemplarsPluginProps> = ({ exemplars, timeZone, getFieldLinks, config }) => {
|
||||
const plotCtx = usePlotContext();
|
||||
const plotInstance = useRef<uPlot>();
|
||||
|
||||
const mapExemplarToXYCoords = useCallback(
|
||||
(dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
|
||||
const plotInstance = plotCtx.plot;
|
||||
const time = dataFrame.fields.find((f) => f.name === TIME_SERIES_TIME_FIELD_NAME);
|
||||
const value = dataFrame.fields.find((f) => f.name === TIME_SERIES_VALUE_FIELD_NAME);
|
||||
useLayoutEffect(() => {
|
||||
config.addHook('init', (u) => {
|
||||
plotInstance.current = u;
|
||||
});
|
||||
}, [config]);
|
||||
|
||||
if (!time || !value || !plotInstance) {
|
||||
return undefined;
|
||||
}
|
||||
const mapExemplarToXYCoords = useCallback((dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
|
||||
const time = dataFrame.fields.find((f) => f.name === TIME_SERIES_TIME_FIELD_NAME);
|
||||
const value = dataFrame.fields.find((f) => f.name === TIME_SERIES_VALUE_FIELD_NAME);
|
||||
|
||||
// Filter x, y scales out
|
||||
const yScale =
|
||||
Object.keys(plotInstance.scales).find((scale) => !['x', 'y'].some((key) => key === scale)) ?? FIXED_UNIT;
|
||||
if (!time || !value || !plotInstance.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const yMin = plotInstance.scales[yScale].min;
|
||||
const yMax = plotInstance.scales[yScale].max;
|
||||
// Filter x, y scales out
|
||||
const yScale =
|
||||
Object.keys(plotInstance.current.scales).find((scale) => !['x', 'y'].some((key) => key === scale)) ?? FIXED_UNIT;
|
||||
|
||||
let y = value.values.get(dataFrameFieldIndex.fieldIndex);
|
||||
// To not to show exemplars outside of the graph we set the y value to min if it is smaller and max if it is bigger than the size of the graph
|
||||
if (yMin != null && y < yMin) {
|
||||
y = yMin;
|
||||
}
|
||||
const yMin = plotInstance.current.scales[yScale].min;
|
||||
const yMax = plotInstance.current.scales[yScale].max;
|
||||
|
||||
if (yMax != null && y > yMax) {
|
||||
y = yMax;
|
||||
}
|
||||
let y = value.values.get(dataFrameFieldIndex.fieldIndex);
|
||||
// To not to show exemplars outside of the graph we set the y value to min if it is smaller and max if it is bigger than the size of the graph
|
||||
if (yMin != null && y < yMin) {
|
||||
y = yMin;
|
||||
}
|
||||
|
||||
return {
|
||||
x: plotInstance.valToPos(time.values.get(dataFrameFieldIndex.fieldIndex), 'x'),
|
||||
y: plotInstance.valToPos(y, yScale),
|
||||
};
|
||||
},
|
||||
[plotCtx]
|
||||
);
|
||||
if (yMax != null && y > yMax) {
|
||||
y = yMax;
|
||||
}
|
||||
|
||||
return {
|
||||
x: plotInstance.current.valToPos(time.values.get(dataFrameFieldIndex.fieldIndex), 'x'),
|
||||
y: plotInstance.current.valToPos(y, yScale),
|
||||
};
|
||||
}, []);
|
||||
|
||||
const renderMarker = useCallback(
|
||||
(dataFrame: DataFrame, dataFrameFieldIndex: DataFrameFieldIndex) => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import React, { useState } from 'react';
|
||||
import React, { HTMLAttributes, useState } from 'react';
|
||||
import { usePopper } from 'react-popper';
|
||||
import { css, cx } from '@emotion/css';
|
||||
import { PlotSelection, usePlotContext, useStyles2, useTheme2, Portal, DEFAULT_ANNOTATION_COLOR } from '@grafana/ui';
|
||||
import { PlotSelection, useStyles2, useTheme2, Portal, DEFAULT_ANNOTATION_COLOR } from '@grafana/ui';
|
||||
import { colorManipulator, DataFrame, getDisplayProcessor, GrafanaTheme2, TimeZone } from '@grafana/data';
|
||||
import { getCommonAnnotationStyles } from '../styles';
|
||||
import { AnnotationEditorForm } from './AnnotationEditorForm';
|
||||
|
||||
interface AnnotationEditorProps {
|
||||
interface AnnotationEditorProps extends HTMLAttributes<HTMLDivElement> {
|
||||
data: DataFrame;
|
||||
timeZone: TimeZone;
|
||||
selection: PlotSelection;
|
||||
@@ -22,11 +22,11 @@ export const AnnotationEditor: React.FC<AnnotationEditorProps> = ({
|
||||
data,
|
||||
selection,
|
||||
annotation,
|
||||
style,
|
||||
}) => {
|
||||
const theme = useTheme2();
|
||||
const styles = useStyles2(getStyles);
|
||||
const commonStyles = useStyles2(getCommonAnnotationStyles);
|
||||
const plotCtx = usePlotContext();
|
||||
const [popperTrigger, setPopperTrigger] = useState<HTMLDivElement | null>(null);
|
||||
const [editorPopover, setEditorPopover] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -43,11 +43,6 @@ export const AnnotationEditor: React.FC<AnnotationEditorProps> = ({
|
||||
],
|
||||
});
|
||||
|
||||
if (!plotCtx || !plotCtx.getCanvasBoundingBox()) {
|
||||
return null;
|
||||
}
|
||||
const canvasBbox = plotCtx.getCanvasBoundingBox();
|
||||
|
||||
let xField = data.fields[0];
|
||||
if (!xField) {
|
||||
return null;
|
||||
@@ -59,13 +54,7 @@ export const AnnotationEditor: React.FC<AnnotationEditorProps> = ({
|
||||
<Portal>
|
||||
<>
|
||||
<div // div overlay matching uPlot canvas bbox
|
||||
className={css`
|
||||
position: absolute;
|
||||
top: ${canvasBbox!.top}px;
|
||||
left: ${canvasBbox!.left}px;
|
||||
width: ${canvasBbox!.width}px;
|
||||
height: ${canvasBbox!.height}px;
|
||||
`}
|
||||
style={style}
|
||||
>
|
||||
<div // Annotation marker
|
||||
className={cx(
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useRef, useState } from 'react';
|
||||
import React, { HTMLAttributes, useCallback, useRef, useState } from 'react';
|
||||
import { GrafanaTheme2, dateTimeFormat, systemDateFormats, TimeZone } from '@grafana/data';
|
||||
import { Portal, useStyles2, usePanelContext, usePlotContext } from '@grafana/ui';
|
||||
import { Portal, useStyles2, usePanelContext } from '@grafana/ui';
|
||||
import { css } from '@emotion/css';
|
||||
import { AnnotationEditorForm } from './AnnotationEditorForm';
|
||||
import { getCommonAnnotationStyles } from '../styles';
|
||||
@@ -8,7 +8,7 @@ import { usePopper } from 'react-popper';
|
||||
import { getTooltipContainerStyles } from '@grafana/ui/src/themes/mixins';
|
||||
import { AnnotationTooltip } from './AnnotationTooltip';
|
||||
|
||||
interface Props {
|
||||
interface Props extends HTMLAttributes<HTMLDivElement> {
|
||||
timeZone: TimeZone;
|
||||
annotation: AnnotationsDataFrameViewDTO;
|
||||
}
|
||||
@@ -26,11 +26,10 @@ const POPPER_CONFIG = {
|
||||
],
|
||||
};
|
||||
|
||||
export function AnnotationMarker({ annotation, timeZone }: Props) {
|
||||
export function AnnotationMarker({ annotation, timeZone, style }: Props) {
|
||||
const { canAddAnnotations, ...panelCtx } = usePanelContext();
|
||||
const commonStyles = useStyles2(getCommonAnnotationStyles);
|
||||
const styles = useStyles2(getStyles);
|
||||
const plotCtx = usePlotContext();
|
||||
const { canAddAnnotations, ...panelCtx } = usePanelContext();
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
@@ -101,24 +100,9 @@ export function AnnotationMarker({ annotation, timeZone }: Props) {
|
||||
<div className={commonStyles(annotation).markerTriangle} style={{ transform: 'translate3d(-100%,-50%, 0)' }} />
|
||||
);
|
||||
|
||||
if (isRegionAnnotation && plotCtx.plot) {
|
||||
let x0 = plotCtx.plot!.valToPos(annotation.time, 'x');
|
||||
let x1 = plotCtx.plot!.valToPos(annotation.timeEnd, 'x');
|
||||
|
||||
// markers are rendered relatively to uPlot canvas overly, not caring about axes width
|
||||
if (x0 < 0) {
|
||||
x0 = 0;
|
||||
}
|
||||
|
||||
if (x1 > plotCtx.plot!.bbox.width / window.devicePixelRatio) {
|
||||
x1 = plotCtx.plot!.bbox.width / window.devicePixelRatio;
|
||||
}
|
||||
|
||||
if (isRegionAnnotation) {
|
||||
marker = (
|
||||
<div
|
||||
className={commonStyles(annotation).markerBar}
|
||||
style={{ width: `${x1 - x0}px`, transform: 'translate3d(0,-50%, 0)' }}
|
||||
/>
|
||||
<div className={commonStyles(annotation).markerBar} style={{ ...style, transform: 'translate3d(0,-50%, 0)' }} />
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
||||
Reference in New Issue
Block a user