TraceView: refactor UI components injection (#44289)

* Remove UIDivider

* Update TracePageSearchBar buttons

* Remove UIButton

* Remove UIInput

* Remove LoadingIndicator

* Remove UIIcon

* Remove UITooltip

* Remove UIDropdown

* Remove UIMenu
This commit is contained in:
Andrej Ocenas 2022-02-08 17:14:33 +01:00 committed by GitHub
parent 1cf48618de
commit a58e6ab622
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 218 additions and 806 deletions

View File

@ -16,11 +16,11 @@ import cx from 'classnames';
import * as React from 'react';
import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { withTheme2, stylesFactory } from '@grafana/ui';
import { withTheme2, stylesFactory, Button } from '@grafana/ui';
import GraphTicks from './GraphTicks';
import Scrubber from './Scrubber';
import { TUpdateViewRangeTimeFunction, UIButton, ViewRange, ViewRangeTimeUpdate, TNil } from '../..';
import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate, TNil } from '../..';
import { autoColor } from '../../Theme';
import DraggableManager, { DraggableBounds, DraggingUpdate, EUpdateTypes } from '../../utils/DraggableManager';
@ -344,13 +344,14 @@ export class UnthemedViewingLayer extends React.PureComponent<ViewingLayerProps,
return (
<div aria-hidden className={styles.ViewingLayer} style={{ height }}>
{(viewStart !== 0 || viewEnd !== 1) && (
<UIButton
<Button
onClick={this._resetTimeZoomClickHandler}
className={cx(styles.ViewingLayerResetZoom, styles.ViewingLayerResetZoomHoverClassName)}
htmlType="button"
type="button"
variant="secondary"
>
Reset Selection
</UIButton>
</Button>
)}
<svg
height={height}

View File

@ -26,7 +26,6 @@ import { autoColor, TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate
import LabeledList from '../common/LabeledList';
import TraceName from '../common/TraceName';
import { getTraceName } from '../model/trace-viewer';
import { TNil } from '../types';
import { Trace } from '../types/trace';
import { formatDuration } from '../utils/date';
import { getTraceLinks } from '../model/link-patterns';
@ -149,22 +148,19 @@ type TracePageHeaderEmbedProps = {
prevResult: () => void;
resultCount: number;
slimView: boolean;
textFilter: string | TNil;
trace: Trace;
traceGraphView: boolean;
updateNextViewRangeTime: (update: ViewRangeTimeUpdate) => void;
updateViewRangeTime: TUpdateViewRangeTimeFunction;
viewRange: ViewRange;
searchValue: string;
onSearchValueChange: (value: string) => void;
hideSearchButtons?: boolean;
timeZone: TimeZone;
};
export const HEADER_ITEMS = [
{
key: 'timestamp',
label: 'Trace Start',
label: 'Trace Start:',
renderer(trace: Trace, timeZone: TimeZone, styles: ReturnType<typeof getStyles>) {
// Convert date from micro to milli seconds
const dateStr = dateTimeFormat(trace.startTime / 1000, { timeZone, defaultWithMS: true });
@ -181,22 +177,22 @@ export const HEADER_ITEMS = [
},
{
key: 'duration',
label: 'Duration',
label: 'Duration:',
renderer: (trace: Trace) => formatDuration(trace.duration),
},
{
key: 'service-count',
label: 'Services',
label: 'Services:',
renderer: (trace: Trace) => new Set(_values(trace.processes).map((p) => p.serviceName)).size,
},
{
key: 'depth',
label: 'Depth',
label: 'Depth:',
renderer: (trace: Trace) => _get(_maxBy(trace.spans, 'depth'), 'depth', 0) + 1,
},
{
key: 'span-count',
label: 'Total Spans',
label: 'Total Spans:',
renderer: (trace: Trace) => trace.spans.length,
},
];
@ -213,15 +209,12 @@ export default function TracePageHeader(props: TracePageHeaderEmbedProps) {
prevResult,
resultCount,
slimView,
textFilter,
trace,
traceGraphView,
updateNextViewRangeTime,
updateViewRangeTime,
viewRange,
searchValue,
onSearchValueChange,
hideSearchButtons,
timeZone,
} = props;
@ -280,11 +273,10 @@ export default function TracePageHeader(props: TracePageHeaderEmbedProps) {
nextResult={nextResult}
prevResult={prevResult}
resultCount={resultCount}
textFilter={textFilter}
navigable={!traceGraphView}
// TODO: we can change this when we have scroll to span functionality
navigable={false}
searchValue={searchValue}
onSearchValueChange={onSearchValueChange}
hideSearchButtons={hideSearchButtons}
/>
</div>
{summaryItems && <LabeledList className={styles.TracePageHeaderOverviewItems} items={summaryItems} />}

View File

@ -25,7 +25,7 @@ const defaultProps = {
nextResult: () => {},
prevResult: () => {},
resultCount: 0,
textFilter: 'something',
searchValue: 'something',
};
describe('<TracePageSearchBar>', () => {
@ -50,29 +50,27 @@ describe('<TracePageSearchBar>', () => {
});
it('renders buttons', () => {
const buttons = wrapper.find('UIButton');
const buttons = wrapper.find('Button');
expect(buttons.length).toBe(4);
buttons.forEach((button) => {
expect(button.hasClass(getStyles().TracePageSearchBarBtn)).toBe(true);
expect(button.hasClass(getStyles().TracePageSearchBarBtnDisabled)).toBe(false);
expect(button.prop('disabled')).toBe(false);
});
expect(wrapper.find('UIButton[icon="up"]').prop('onClick')).toBe(defaultProps.prevResult);
expect(wrapper.find('UIButton[icon="down"]').prop('onClick')).toBe(defaultProps.nextResult);
expect(wrapper.find('UIButton[icon="close"]').prop('onClick')).toBe(defaultProps.clearSearch);
expect(wrapper.find('Button[icon="arrow-up"]').prop('onClick')).toBe(defaultProps.prevResult);
expect(wrapper.find('Button[icon="arrow-down"]').prop('onClick')).toBe(defaultProps.nextResult);
expect(wrapper.find('Button[icon="times"]').prop('onClick')).toBe(defaultProps.clearSearch);
});
it('hides navigation buttons when not navigable', () => {
wrapper.setProps({ navigable: false });
const button = wrapper.find('UIButton');
const button = wrapper.find('Button');
expect(button.length).toBe(1);
expect(button.prop('icon')).toBe('close');
expect(button.prop('icon')).toBe('times');
});
});
describe('falsy textFilter', () => {
beforeEach(() => {
wrapper.setProps({ textFilter: '' });
wrapper.setProps({ searchValue: '' });
});
it('renders UiFindInput with correct props', () => {
@ -80,11 +78,9 @@ describe('<TracePageSearchBar>', () => {
});
it('renders buttons', () => {
const buttons = wrapper.find('UIButton');
const buttons = wrapper.find('Button');
expect(buttons.length).toBe(4);
buttons.forEach((button) => {
expect(button.hasClass(getStyles().TracePageSearchBarBtn)).toBe(true);
expect(button.hasClass(getStyles().TracePageSearchBarBtnDisabled)).toBe(true);
expect(button.prop('disabled')).toBe(true);
});
});

View File

@ -16,13 +16,11 @@ import * as React from 'react';
import cx from 'classnames';
import IoAndroidLocate from 'react-icons/lib/io/android-locate';
import { css } from '@emotion/css';
import { useStyles2 } from '@grafana/ui';
import { Button, useStyles2 } from '@grafana/ui';
import * as markers from './TracePageSearchBar.markers';
import UiFindInput from '../common/UiFindInput';
import { TNil } from '../types';
import { UIButton, UIInputGroup } from '../uiElementsContext';
import { ubFlexAuto, ubJustifyEnd } from '../uberUtilityStyles';
// eslint-disable-next-line no-duplicate-imports
import { memo } from 'react';
@ -61,7 +59,6 @@ export const getStyles = () => {
};
type TracePageSearchBarProps = {
textFilter: string | TNil;
prevResult: () => void;
nextResult: () => void;
clearSearch: () => void;
@ -70,7 +67,6 @@ type TracePageSearchBarProps = {
navigable: boolean;
searchValue: string;
onSearchValueChange: (value: string) => void;
hideSearchButtons?: boolean;
};
export default memo(function TracePageSearchBar(props: TracePageSearchBarProps) {
@ -81,16 +77,14 @@ export default memo(function TracePageSearchBar(props: TracePageSearchBarProps)
nextResult,
prevResult,
resultCount,
textFilter,
onSearchValueChange,
searchValue,
hideSearchButtons,
} = props;
const styles = useStyles2(getStyles);
const count = textFilter ? <span className={styles.TracePageSearchBarCount}>{resultCount}</span> : null;
const count = searchValue ? <span className={styles.TracePageSearchBarCount}>{resultCount}</span> : null;
const btnClass = cx(styles.TracePageSearchBarBtn, { [styles.TracePageSearchBarBtnDisabled]: !textFilter });
const btnClass = cx(styles.TracePageSearchBarBtn, { [styles.TracePageSearchBarBtnDisabled]: !searchValue });
const uiFindInputInputProps = {
'data-test': markers.IN_TRACE_SEARCH,
className: cx(styles.TracePageSearchBarBar, ubFlexAuto),
@ -100,47 +94,41 @@ export default memo(function TracePageSearchBar(props: TracePageSearchBarProps)
return (
<div className={styles.TracePageSearchBar}>
{/* style inline because compact overwrites the display */}
<UIInputGroup className={ubJustifyEnd} compact style={{ display: 'flex' }}>
<span className={ubJustifyEnd} style={{ display: 'flex' }}>
<UiFindInput onChange={onSearchValueChange} value={searchValue} inputProps={uiFindInputInputProps} />
{!hideSearchButtons && (
<>
{navigable && (
<>
<UIButton
className={cx(btnClass, styles.TracePageSearchBarLocateBtn)}
disabled={!textFilter}
htmlType="button"
onClick={focusUiFindMatches}
>
<IoAndroidLocate />
</UIButton>
<UIButton
className={btnClass}
disabled={!textFilter}
htmlType="button"
icon="up"
onClick={prevResult}
/>
<UIButton
className={btnClass}
disabled={!textFilter}
htmlType="button"
icon="down"
onClick={nextResult}
/>
</>
)}
<UIButton
className={btnClass}
disabled={!textFilter}
htmlType="button"
icon="close"
onClick={clearSearch}
/>
</>
)}
</UIInputGroup>
<>
{navigable && (
<>
<Button
className={cx(btnClass, styles.TracePageSearchBarLocateBtn)}
disabled={!searchValue}
type="button"
onClick={focusUiFindMatches}
>
<IoAndroidLocate />
</Button>
<Button className={btnClass} disabled={!searchValue} type="button" icon="arrow-up" onClick={prevResult} />
<Button
className={btnClass}
disabled={!searchValue}
type="button"
icon="arrow-down"
onClick={nextResult}
/>
</>
)}
<Button
variant={'secondary'}
fill={'text'}
// className={btnClass}
disabled={!searchValue}
type="button"
icon="times"
onClick={clearSearch}
title={'Clear search'}
/>
</>
</span>
</div>
);
});

View File

@ -14,12 +14,12 @@
import React from 'react';
import { shallow } from 'enzyme';
import { Tooltip } from '@grafana/ui';
import ReferencesButton, { getStyles } from './ReferencesButton';
import transformTraceData from '../model/transform-trace-data';
import traceGenerator from '../demo/trace-generators';
import ReferenceLink from '../url/ReferenceLink';
import { UIDropdown, UIMenuItem, UITooltip } from '../uiElementsContext';
describe(ReferencesButton, () => {
const trace = transformTraceData(traceGenerator.trace({ numberOfSpans: 10 }));
@ -49,40 +49,14 @@ describe(ReferencesButton, () => {
it('renders single reference', () => {
const props = { ...baseProps, references: oneReference };
const wrapper = shallow(<ReferencesButton {...props} />);
const dropdown = wrapper.find(UIDropdown);
const refLink = wrapper.find(ReferenceLink);
const tooltip = wrapper.find(UITooltip);
const tooltip = wrapper.find(Tooltip);
const styles = getStyles();
expect(dropdown.length).toBe(0);
expect(refLink.length).toBe(1);
expect(refLink.prop('reference')).toBe(oneReference[0]);
expect(refLink.first().props().className).toBe(styles.MultiParent);
expect(tooltip.length).toBe(1);
expect(tooltip.prop('title')).toBe(props.tooltipText);
});
it('renders multiple references', () => {
const props = { ...baseProps, references: moreReferences };
const wrapper = shallow(<ReferencesButton {...props} />);
const dropdown = wrapper.find(UIDropdown);
expect(dropdown.length).toBe(1);
// We have some wrappers here that dynamically inject specific component so we need to traverse a bit
// here
const menuInstance = shallow(
shallow(dropdown.first().props().overlay).prop('children')({
Menu({ children }) {
return <div>{children}</div>;
},
})
);
const submenuItems = menuInstance.find(UIMenuItem);
expect(submenuItems.length).toBe(3);
submenuItems.forEach((submenuItem, i) => {
expect(submenuItem.find(ReferenceLink).prop('reference')).toBe(moreReferences[i]);
});
expect(submenuItems.at(2).find(ReferenceLink).childAt(0).text()).toBe(
`(another trace) - ${moreReferences[2].spanID}`
);
expect(tooltip.prop('content')).toBe(props.tooltipText);
});
});

View File

@ -14,11 +14,9 @@
import React from 'react';
import { css } from '@emotion/css';
import { stylesFactory } from '@grafana/ui';
import { stylesFactory, Tooltip } from '@grafana/ui';
import NewWindowIcon from '../common/NewWindowIcon';
import { TraceSpanReference } from '../types/trace';
import { UITooltip, UIDropdown, UIMenuItem, UIMenu, TooltipPlacement } from '../uiElementsContext';
import ReferenceLink from '../url/ReferenceLink';
export const getStyles = stylesFactory(() => {
@ -51,55 +49,18 @@ type TReferencesButtonProps = {
};
export default class ReferencesButton extends React.PureComponent<TReferencesButtonProps> {
referencesList = (references: TraceSpanReference[]) => {
const styles = getStyles();
return (
<UIMenu>
{references.map((ref) => {
const { span, spanID } = ref;
return (
<UIMenuItem key={`${spanID}`}>
<ReferenceLink reference={ref} focusSpan={this.props.focusSpan} className={styles.TraceRefLink}>
{span
? `${span.process.serviceName}:${span.operationName} - ${ref.spanID}`
: `(another trace) - ${ref.spanID}`}
{!span && <NewWindowIcon className={styles.NewWindowIcon} />}
</ReferenceLink>
</UIMenuItem>
);
})}
</UIMenu>
);
};
render() {
const { references, children, tooltipText, focusSpan } = this.props;
const styles = getStyles();
const tooltipProps = {
arrowPointAtCenter: true,
mouseLeaveDelay: 0.5,
placement: 'bottom' as TooltipPlacement,
title: tooltipText,
overlayClassName: styles.tooltip,
};
if (references.length > 1) {
return (
<UITooltip {...tooltipProps}>
<UIDropdown overlay={this.referencesList(references)} placement="bottomRight" trigger={['click']}>
<a className={styles.MultiParent}>{children}</a>
</UIDropdown>
</UITooltip>
);
}
// TODO: handle multiple items with some dropdown
const ref = references[0];
return (
<UITooltip {...tooltipProps}>
<Tooltip content={tooltipText}>
<ReferenceLink reference={ref} focusSpan={focusSpan} className={styles.MultiParent}>
{children}
</ReferenceLink>
</UITooltip>
</Tooltip>
);
}
}

View File

@ -14,7 +14,7 @@
import React from 'react';
import { mount } from 'enzyme';
import UIElementsContext, { UIPopover } from '../uiElementsContext';
import { Popover } from '../common/Popover';
import SpanBar from './SpanBar';
@ -74,11 +74,7 @@ describe('<SpanBar>', () => {
};
it('renders without exploding', () => {
const wrapper = mount(
<UIElementsContext.Provider value={{ Popover: () => '' }}>
<SpanBar {...props} />
</UIElementsContext.Provider>
);
const wrapper = mount(<SpanBar {...props} />);
expect(wrapper).toBeDefined();
const { onMouseOver, onMouseLeave } = wrapper.find('[data-test-id="SpanBar--wrapper"]').props();
const labelElm = wrapper.find('[data-test-id="SpanBar--label"]');
@ -91,11 +87,7 @@ describe('<SpanBar>', () => {
it('log markers count', () => {
// 3 log entries, two grouped together with the same timestamp
const wrapper = mount(
<UIElementsContext.Provider value={{ Popover: () => '' }}>
<SpanBar {...props} />
</UIElementsContext.Provider>
);
expect(wrapper.find(UIPopover).length).toEqual(2);
const wrapper = mount(<SpanBar {...props} />);
expect(wrapper.find(Popover).length).toEqual(2);
});
});

View File

@ -22,9 +22,9 @@ import { useStyles2 } from '@grafana/ui';
import { autoColor } from '../Theme';
import { TraceSpan } from '../types/trace';
import { TNil } from '../types';
import { UIPopover } from '../uiElementsContext';
import AccordianLogs from './SpanDetail/AccordianLogs';
import { ViewedBoundsFunctionType } from './utils';
import { Popover } from '../common/Popover';
const getStyles = (theme: GrafanaTheme2) => {
return {
@ -172,15 +172,14 @@ function SpanBar(props: TInnerProps) {
</div>
<div>
{Object.keys(logGroups).map((positionKey) => (
<UIPopover
<Popover
key={positionKey}
placement="topLeft"
content={
<AccordianLogs interactive={false} isOpen logs={logGroups[positionKey]} timestamp={traceStartTime} />
}
>
<div className={styles.logMarker} style={{ left: positionKey }} />
</UIPopover>
</Popover>
))}
</div>
{rpc && (

View File

@ -14,12 +14,10 @@
import React from 'react';
import { shallow } from 'enzyme';
import { createTheme } from '@grafana/data';
import CopyIcon from '../../common/CopyIcon';
import KeyValuesTable, { LinkValue, getStyles } from './KeyValuesTable';
import { UIDropdown, UIIcon } from '../../uiElementsContext';
import KeyValuesTable, { LinkValue } from './KeyValuesTable';
import { ubInlineBlock } from '../../uberUtilityStyles';
describe('LinkValue', () => {
@ -37,12 +35,6 @@ describe('LinkValue', () => {
expect(wrapper.find('a').prop('title')).toBe(title);
expect(wrapper.find('a').text()).toMatch(/childrenText/);
});
it('renders correct Icon', () => {
const styles = getStyles(createTheme());
expect(wrapper.find(UIIcon).hasClass(styles.linkIcon)).toBe(true);
expect(wrapper.find(UIIcon).prop('type')).toBe('export');
});
});
describe('<KeyValuesTable>', () => {
@ -92,38 +84,6 @@ describe('<KeyValuesTable>', () => {
expect(anchor.closest('tr').find('td').first().text()).toBe('span.kind');
});
it('renders multiple links correctly', () => {
wrapper.setProps({
linksGetter: (array, i) =>
array[i].key === 'span.kind'
? [
{ url: `http://example.com/1?kind=${encodeURIComponent(array[i].value)}`, text: 'Example 1' },
{ url: `http://example.com/2?kind=${encodeURIComponent(array[i].value)}`, text: 'Example 2' },
]
: [],
});
const dropdown = wrapper.find(UIDropdown);
const overlay = shallow(dropdown.prop('overlay'));
// We have some wrappers here that dynamically inject specific component so we need to traverse a bit
// here
const menu = shallow(
overlay.prop('children')({
Menu({ children }) {
return <div>{children}</div>;
},
})
);
const anchors = menu.find(LinkValue);
expect(anchors).toHaveLength(2);
const firstAnchor = anchors.first();
expect(firstAnchor.prop('href')).toBe('http://example.com/1?kind=client');
expect(firstAnchor.children().text()).toBe('Example 1');
const secondAnchor = anchors.last();
expect(secondAnchor.prop('href')).toBe('http://example.com/2?kind=client');
expect(secondAnchor.children().text()).toBe('Example 2');
expect(dropdown.closest('tr').find('td').first().text()).toBe('span.kind');
});
it('renders a <CopyIcon /> with correct copyText for each data element', () => {
const copyIcons = wrapper.find(CopyIcon);
expect(copyIcons.length).toBe(data.length);

View File

@ -16,13 +16,12 @@ import * as React from 'react';
import jsonMarkup from 'json-markup';
import { css } from '@emotion/css';
import cx from 'classnames';
import { useStyles2 } from '@grafana/ui';
import { Icon, useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import CopyIcon from '../../common/CopyIcon';
import { TNil } from '../../types';
import { TraceKeyValuePair, TraceLink } from '../../types/trace';
import { UIDropdown, UIIcon, UIMenu, UIMenuItem } from '../../uiElementsContext';
import { autoColor } from '../../Theme';
import { ubInlineBlock, uWidth100 } from '../../uberUtilityStyles';
@ -89,10 +88,9 @@ function parseIfComplexJson(value: any) {
}
export const LinkValue = (props: { href: string; title?: string; children: React.ReactNode }) => {
const styles = useStyles2(getStyles);
return (
<a href={props.href} title={props.title} target="_blank" rel="noopener noreferrer">
{props.children} <UIIcon className={styles.linkIcon} type="export" />
{props.children} <Icon name="external-link-alt" />
</a>
);
};
@ -101,17 +99,6 @@ LinkValue.defaultProps = {
title: '',
};
const linkValueList = (links: TraceLink[]) => (
<UIMenu>
{links.map(({ text, url }, index) => (
// `index` is necessary in the key because url can repeat
<UIMenuItem key={`${url}-${index}`}>
<LinkValue href={url}>{text}</LinkValue>
</UIMenuItem>
))}
</UIMenu>
);
type KeyValuesTableProps = {
data: TraceKeyValuePair[];
linksGetter: ((pairs: TraceKeyValuePair[], index: number) => TraceLink[]) | TNil;
@ -131,7 +118,8 @@ export default function KeyValuesTable(props: KeyValuesTableProps) {
const jsonTable = <div className={ubInlineBlock} dangerouslySetInnerHTML={markup} />;
const links = linksGetter ? linksGetter(data, i) : null;
let valueMarkup;
if (links && links.length === 1) {
if (links && links.length) {
// TODO: handle multiple items
valueMarkup = (
<div>
<LinkValue href={links[0].url} title={links[0].text}>
@ -139,16 +127,6 @@ export default function KeyValuesTable(props: KeyValuesTableProps) {
</LinkValue>
</div>
);
} else if (links && links.length > 1) {
valueMarkup = (
<div>
<UIDropdown overlay={linkValueList(links)} placement="bottomRight" trigger={['click']}>
<a>
{jsonTable} <UIIcon className={styles.linkIcon} type="profile" />
</a>
</UIDropdown>
</div>
);
} else {
valueMarkup = jsonTable;
}

View File

@ -29,7 +29,7 @@ import { SpanLinkFunc, TNil } from '../../types';
import { TraceKeyValuePair, TraceLink, TraceLog, TraceSpan } from '../../types/trace';
import AccordianReferences from './AccordianReferences';
import { autoColor } from '../../Theme';
import { UIDivider } from '../../uiElementsContext';
import { Divider } from '../../common/Divider';
import {
uAlignIcon,
ubFlex,
@ -43,21 +43,6 @@ import {
const getStyles = (theme: GrafanaTheme2) => {
return {
divider: css`
label: divider;
background: ${autoColor(theme, '#ddd')};
`,
dividerVertical: css`
label: dividerVertical;
display: block;
height: 1px;
width: 100%;
margin: 24px 0;
clear: both;
vertical-align: middle;
position: relative;
top: -0.06em;
`,
debugInfo: css`
label: debugInfo;
display: block;
@ -195,12 +180,12 @@ export default function SpanDetail(props: SpanDetailProps) {
<div>
<div className={cx(ubFlex, ubItemsCenter, ubMb1)}>
<h2 className={cx(ubFlexAuto, ubM0)}>{operationName}</h2>
<LabeledList className={ubTxRightAlign} dividerClassName={styles.divider} items={overviewItems} />
<LabeledList className={ubTxRightAlign} items={overviewItems} />
</div>
{link ? (
<DataLinkButton link={{ ...link, title: 'Logs for this span' } as any} buttonProps={{ icon: 'gf-logs' }} />
) : null}
<UIDivider className={cx(styles.divider, styles.dividerVertical, ubMy1)} />
<Divider className={ubMy1} type={'horizontal'} />
<div>
<div>
<AccordianKeyValues

View File

@ -15,8 +15,7 @@
import React from 'react';
import { shallow } from 'enzyme';
import * as copy from 'copy-to-clipboard';
import { UIButton, UITooltip } from '../uiElementsContext';
import { Button, Tooltip } from '@grafana/ui';
import CopyIcon from './CopyIcon';
jest.mock('copy-to-clipboard');
@ -47,24 +46,13 @@ describe('<CopyIcon />', () => {
expect(wrapper.state().hasCopied).toBe(false);
expect(copySpy).not.toHaveBeenCalled();
wrapper.find(UIButton).simulate('click');
wrapper.find(Button).simulate('click');
expect(wrapper.state().hasCopied).toBe(true);
expect(copySpy).toHaveBeenCalledWith(props.copyText);
});
it('updates state when tooltip hides and state.hasCopied is true', () => {
wrapper.setState({ hasCopied: true });
wrapper.find(UITooltip).prop('onVisibleChange')(false);
expect(wrapper.state().hasCopied).toBe(false);
const state = wrapper.state();
wrapper.find(UITooltip).prop('onVisibleChange')(false);
expect(wrapper.state()).toBe(state);
});
it('persists state when tooltip opens', () => {
wrapper.setState({ hasCopied: true });
wrapper.find(UITooltip).prop('onVisibleChange')(true);
expect(wrapper.state().hasCopied).toBe(true);
});
});

View File

@ -16,9 +16,7 @@ import * as React from 'react';
import { css } from '@emotion/css';
import cx from 'classnames';
import copy from 'copy-to-clipboard';
import { stylesFactory } from '@grafana/ui';
import { UITooltip, TooltipPlacement, UIButton } from '../uiElementsContext';
import { Button, IconName, stylesFactory, Tooltip } from '@grafana/ui';
const getStyles = stylesFactory(() => {
return {
@ -40,8 +38,7 @@ const getStyles = stylesFactory(() => {
type PropsType = {
className?: string;
copyText: string;
icon?: string;
placement?: TooltipPlacement;
icon?: IconName;
tooltipTitle: string;
};
@ -53,7 +50,6 @@ export default class CopyIcon extends React.PureComponent<PropsType, StateType>
static defaultProps: Partial<PropsType> = {
className: undefined,
icon: 'copy',
placement: 'left',
};
state = {
@ -78,20 +74,14 @@ export default class CopyIcon extends React.PureComponent<PropsType, StateType>
render() {
const styles = getStyles();
return (
<UITooltip
arrowPointAtCenter
mouseLeaveDelay={0.5}
onVisibleChange={this.handleTooltipVisibilityChange}
placement={this.props.placement}
title={this.state.hasCopied ? 'Copied' : this.props.tooltipTitle}
>
<UIButton
<Tooltip content={this.state.hasCopied ? 'Copied' : this.props.tooltipTitle}>
<Button
className={cx(styles.CopyIcon, this.props.className)}
htmlType="button"
type="button"
icon={this.props.icon}
onClick={this.handleClick}
/>
</UITooltip>
</Tooltip>
);
}
}

View File

@ -0,0 +1,53 @@
import React from 'react';
import { css, cx } from '@emotion/css';
import { useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { autoColor } from '../Theme';
const getStyles = (theme: GrafanaTheme2) => {
return {
Divider: css`
background: ${autoColor(theme, '#ddd')};
`,
DividerVertical: css`
label: DividerVertical;
display: inline-block;
width: 1px;
height: 0.9em;
margin: 0 8px;
vertical-align: middle;
`,
DividerHorizontal: css`
label: DividerHorizontal;
display: block;
height: 1px;
width: 100%;
margin: 24px 0;
clear: both;
vertical-align: middle;
position: relative;
top: -0.06em;
`,
};
};
interface Props {
className?: string;
style?: React.CSSProperties;
type?: 'vertical' | 'horizontal';
}
export function Divider({ className, style, type }: Props) {
const styles = useStyles2(getStyles);
return (
<div
style={style}
className={cx(
styles.Divider,
type === 'horizontal' ? styles.DividerHorizontal : styles.DividerVertical,
className
)}
/>
);
}

View File

@ -13,7 +13,6 @@
// limitations under the License.
import * as React from 'react';
import { UIDropdown, UIMenu, UIMenuItem } from '..';
import NewWindowIcon from './NewWindowIcon';
type Link = {
@ -32,28 +31,8 @@ const LinkValue = (props: { href: string; title?: string; children?: React.React
</a>
);
// export for testing
export const linkValueList = (links: Link[]) => (
<UIMenu>
{links.map(({ text, url }, index) => (
// `index` is necessary in the key because url can repeat
<UIMenuItem key={`${url}-${index}`}>
<LinkValue href={url}>{text}</LinkValue>
</UIMenuItem>
))}
</UIMenu>
);
export default function ExternalLinks(props: ExternalLinksProps) {
const { links } = props;
if (links.length === 1) {
return <LinkValue href={links[0].url} title={links[0].text} className={props.className} />;
}
return (
<UIDropdown overlay={linkValueList(links)} placement="bottomRight" trigger={['click']}>
<a className={props.className}>
<NewWindowIcon isLarge />
</a>
</UIDropdown>
);
// TODO: handle multiple items
return <LinkValue href={links[0].url} title={links[0].text} className={props.className} />;
}

View File

@ -18,7 +18,7 @@ import cx from 'classnames';
import { useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import { UIDivider } from '../uiElementsContext';
import { Divider } from './Divider';
const getStyles = (theme: GrafanaTheme2) => {
return {
@ -42,19 +42,18 @@ const getStyles = (theme: GrafanaTheme2) => {
type LabeledListProps = {
className?: string;
dividerClassName?: string;
items: Array<{ key: string; label: React.ReactNode; value: React.ReactNode }>;
};
export default function LabeledList(props: LabeledListProps) {
const { className, dividerClassName, items } = props;
const { className, items } = props;
const styles = useStyles2(getStyles);
return (
<ul className={cx(styles.LabeledList, className)}>
{items.map(({ key, label, value }, i) => {
const divider = i < items.length - 1 && (
<li className={styles.LabeledListItem} key={`${key}--divider`}>
<UIDivider className={dividerClassName} type="vertical" />
<Divider />
</li>
);
return [

View File

@ -1,78 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React from 'react';
import cx from 'classnames';
import { css, keyframes } from '@emotion/css';
import { useStyles2 } from '@grafana/ui';
import { UIIcon } from '../uiElementsContext';
const getStyles = () => {
const LoadingIndicatorColorAnim = keyframes`
/*
rgb(0, 128, 128) == teal
rgba(0, 128, 128, 0.3) == #bedfdf
*/
from {
color: #bedfdf;
}
to {
color: teal;
}
`;
return {
LoadingIndicator: css`
label: LoadingIndicator;
animation: ${LoadingIndicatorColorAnim} 1s infinite alternate;
font-size: 36px;
/* outline / stroke the loading indicator */
text-shadow: -0.5px 0 rgba(0, 128, 128, 0.6), 0 0.5px rgba(0, 128, 128, 0.6), 0.5px 0 rgba(0, 128, 128, 0.6),
0 -0.5px rgba(0, 128, 128, 0.6);
`,
LoadingIndicatorCentered: css`
label: LoadingIndicatorCentered;
display: block;
margin-left: auto;
margin-right: auto;
`,
LoadingIndicatorSmall: css`
label: LoadingIndicatorSmall;
font-size: 0.7em;
`,
};
};
type LoadingIndicatorProps = {
centered?: boolean;
className?: string;
small?: boolean;
};
export default function LoadingIndicator(props: LoadingIndicatorProps) {
const { centered, className, small, ...rest } = props;
const styles = useStyles2(getStyles);
const cls = cx(styles.LoadingIndicator, {
[styles.LoadingIndicatorCentered]: centered,
[styles.LoadingIndicatorSmall]: small,
className,
});
return <UIIcon type="loading" className={cls} {...rest} />;
}
LoadingIndicator.defaultProps = {
centered: false,
className: undefined,
small: false,
};

View File

@ -0,0 +1,38 @@
import React, { ReactElement, useRef } from 'react';
import { Popover as GrafanaPopover, PopoverController } from '@grafana/ui';
export type PopoverProps = {
children: ReactElement;
content: ReactElement;
overlayClassName?: string;
};
export function Popover({ children, content, overlayClassName }: PopoverProps) {
const popoverRef = useRef<HTMLElement>(null);
return (
<PopoverController content={content} hideAfter={300}>
{(showPopper, hidePopper, popperProps) => {
return (
<>
{popoverRef.current && (
<GrafanaPopover
{...popperProps}
referenceElement={popoverRef.current}
wrapperClassName={overlayClassName}
onMouseLeave={hidePopper}
onMouseEnter={showPopper}
/>
)}
{React.cloneElement(children, {
ref: popoverRef,
onMouseEnter: showPopper,
onMouseLeave: hidePopper,
})}
</>
);
}}
</PopoverController>
);
}

View File

@ -19,10 +19,8 @@ import { useStyles2 } from '@grafana/ui';
import { GrafanaTheme2 } from '@grafana/data';
import BreakableText from './BreakableText';
import LoadingIndicator from './LoadingIndicator';
import { fetchedState, FALLBACK_TRACE_NAME } from '../constants';
import { FetchedState, TNil } from '../types';
import { ApiError } from '../types/api-error';
import { FALLBACK_TRACE_NAME } from '../constants';
import { TNil } from '../types';
const getStyles = (theme: GrafanaTheme2) => {
return {
@ -30,42 +28,18 @@ const getStyles = (theme: GrafanaTheme2) => {
label: TraceName;
font-size: ${theme.typography.size.lg};
`,
TraceNameError: css`
label: TraceNameError;
color: #c00;
`,
};
};
type Props = {
className?: string;
error?: ApiError | TNil;
state?: FetchedState | TNil;
traceName?: string | TNil;
};
export default function TraceName(props: Props) {
const { className, error, state, traceName } = props;
const isErred = state === fetchedState.ERROR;
let title: string | React.ReactNode = traceName || FALLBACK_TRACE_NAME;
const { className, traceName } = props;
const styles = useStyles2(getStyles);
let errorCssClass = '';
if (isErred) {
errorCssClass = styles.TraceNameError;
let titleStr = '';
if (error) {
titleStr = typeof error === 'string' ? error : error.message || String(error);
}
if (!titleStr) {
titleStr = 'Error: Unknown error';
}
title = titleStr;
title = <BreakableText text={titleStr} />;
} else if (state === fetchedState.LOADING) {
title = <LoadingIndicator small />;
} else {
const text = String(traceName || FALLBACK_TRACE_NAME);
title = <BreakableText text={text} />;
}
return <span className={cx(styles.TraceName, errorCssClass, className)}>{title}</span>;
const text = String(traceName || FALLBACK_TRACE_NAME);
const title = <BreakableText text={text} />;
return <span className={cx(styles.TraceName, className)}>{title}</span>;
}

View File

@ -16,9 +16,9 @@ import * as React from 'react';
import { shallow } from 'enzyme';
// eslint-disable-next-line lodash/import-scope
import _ from 'lodash';
import { Input } from '@grafana/ui';
import UiFindInput from './UiFindInput';
import { UIInput } from '../uiElementsContext';
const debounceMock = jest.spyOn(_, 'debounce').mockImplementation((func) => {
return Object.assign(func, { cancel: jest.fn(), flush: jest.fn() });
@ -62,7 +62,7 @@ describe('UiFindInput', () => {
it('renders props.uiFind when state.ownInputValue is `undefined`', () => {
wrapper.setProps({ value: uiFind });
expect(wrapper.find(UIInput).prop('value')).toBe(uiFind);
expect(wrapper.find(Input).prop('value')).toBe(uiFind);
});
});
});

View File

@ -12,10 +12,10 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { IconButton, Input } from '@grafana/ui';
import * as React from 'react';
import { TNil } from '../types/index';
import { UIIcon, UIInput } from '../uiElementsContext';
type Props = {
allowClear?: boolean;
@ -43,17 +43,16 @@ export default class UiFindInput extends React.PureComponent<Props> {
const suffix = (
<>
{allowClear && value && value.length && <UIIcon type="close" onClick={this.clearUiFind} />}
{inputProps.suffix}
{allowClear && value && value.length && <IconButton name="times" onClick={this.clearUiFind} />}
</>
);
return (
<UIInput
autosize={null}
<Input
placeholder="Find..."
{...inputProps}
onChange={(e) => this.props.onChange(e.target.value)}
onChange={(e) => this.props.onChange(e.currentTarget.value)}
suffix={suffix}
value={value}
/>

View File

@ -1,18 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<CopyIcon /> renders as expected 1`] = `
<UITooltip
arrowPointAtCenter={true}
mouseLeaveDelay={0.5}
onVisibleChange={[Function]}
placement="left"
title="tooltipTitleValue"
<Tooltip
content="tooltipTitleValue"
>
<UIButton
<Button
className="css-oqwzau classNameValue"
htmlType="button"
icon="copy"
onClick={[Function]}
type="button"
/>
</UITooltip>
</Tooltip>
`;

View File

@ -1,8 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`UiFindInput rendering renders as expected 1`] = `
<UIInput
autosize={null}
<Input
onChange={[Function]}
placeholder="Find..."
suffix={<React.Fragment />}

View File

@ -12,8 +12,6 @@
// See the License for the specific language governing permissions and
// limitations under the License.
export const TOP_NAV_HEIGHT = 46 as 46;
export const FALLBACK_DAG_MAX_NUM_SERVICES = 100 as 100;
export const FALLBACK_TRACE_NAME = '<trace-without-root-span>' as '<trace-without-root-span>';

View File

@ -1,7 +1,5 @@
export { default as TraceTimelineViewer } from './TraceTimelineViewer';
export { default as TracePageHeader } from './TracePageHeader';
export { default as UIElementsContext } from './uiElementsContext';
export * from './uiElementsContext';
export * from './types';
export * from './TraceTimelineViewer/types';
export { default as DetailState } from './TraceTimelineViewer/SpanDetail/DetailState';

View File

@ -12,19 +12,7 @@
// See the License for the specific language governing permissions and
// limitations under the License.
import { ApiError } from './api-error';
import { Trace } from './trace';
export { TraceSpan, TraceResponse, Trace, TraceProcess, TraceKeyValuePair, TraceLink } from './trace';
export { SpanLinkFunc, SpanLinkDef } from './links';
export { default as TTraceTimeline } from './TTraceTimeline';
export { default as TNil } from './TNil';
export type FetchedState = 'FETCH_DONE' | 'FETCH_ERROR' | 'FETCH_LOADING';
export type FetchedTrace = {
data?: Trace;
error?: ApiError;
id: string;
state?: FetchedState;
};

View File

@ -1,239 +0,0 @@
// Copyright (c) 2017 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import React, { ReactElement } from 'react';
export type TooltipPlacement =
| 'top'
| 'left'
| 'right'
| 'bottom'
| 'topLeft'
| 'topRight'
| 'bottomLeft'
| 'bottomRight'
| 'leftTop'
| 'leftBottom'
| 'rightTop'
| 'rightBottom';
export type PopoverProps = {
children: ReactElement;
content: ReactElement;
arrowPointAtCenter?: boolean;
overlayClassName?: string;
placement?: TooltipPlacement;
};
export const UIPopover: React.ComponentType<PopoverProps> = function UIPopover(props: PopoverProps) {
return (
<GetElementsContext>
{(elements: Elements) => {
return <elements.Popover {...props} />;
}}
</GetElementsContext>
);
};
export type TooltipProps = {
title: string | ReactElement;
getPopupContainer?: (triggerNode: Element) => HTMLElement;
overlayClassName?: string;
children: ReactElement;
placement?: TooltipPlacement;
mouseLeaveDelay?: number;
arrowPointAtCenter?: boolean;
onVisibleChange?: (visible: boolean) => void;
};
export const UITooltip: React.ComponentType<TooltipProps> = function UITooltip(props: TooltipProps) {
return (
<GetElementsContext>
{(elements: Elements) => {
return <elements.Tooltip {...props} />;
}}
</GetElementsContext>
);
};
export type IconProps = {
type: string;
className?: string;
onClick?: React.MouseEventHandler<any>;
};
export const UIIcon: React.ComponentType<IconProps> = function UIIcon(props: IconProps) {
return (
<GetElementsContext>
{(elements: Elements) => {
return <elements.Icon {...props} />;
}}
</GetElementsContext>
);
};
export type DropdownProps = {
overlay: React.ReactNode;
placement?: 'topLeft' | 'topCenter' | 'topRight' | 'bottomLeft' | 'bottomCenter' | 'bottomRight';
trigger?: Array<'click' | 'hover' | 'contextMenu'>;
children?: React.ReactNode;
};
export const UIDropdown = function UIDropdown(props: DropdownProps) {
return (
<GetElementsContext>
{(elements: Elements) => {
return <elements.Dropdown {...props} />;
}}
</GetElementsContext>
);
};
export type MenuProps = {
children?: React.ReactNode;
};
export const UIMenu = function UIMenu(props: MenuProps) {
return (
<GetElementsContext>
{(elements: Elements) => {
return <elements.Menu {...props} />;
}}
</GetElementsContext>
);
};
export type MenuItemProps = {
children?: React.ReactNode;
};
export const UIMenuItem = function UIMenuItem(props: MenuItemProps) {
return (
<GetElementsContext>
{(elements: Elements) => {
return <elements.MenuItem {...props} />;
}}
</GetElementsContext>
);
};
export type ButtonHTMLType = 'submit' | 'button' | 'reset';
export type ButtonProps = {
children?: React.ReactNode;
className?: string;
htmlType?: ButtonHTMLType;
icon?: string;
onClick?: React.MouseEventHandler<HTMLButtonElement>;
disabled?: boolean;
};
export const UIButton = function UIButton(props: ButtonProps) {
return (
<GetElementsContext>
{(elements: Elements) => {
return <elements.Button {...props} />;
}}
</GetElementsContext>
);
};
export type DividerProps = {
className?: string;
type?: 'vertical' | 'horizontal';
};
export const UIDivider = function UIDivider(props: DividerProps) {
return (
<GetElementsContext>
{(elements: Elements) => {
return <elements.Divider {...props} />;
}}
</GetElementsContext>
);
};
export type InputProps = {
autosize?: boolean | null;
placeholder?: string;
onChange: (value: React.ChangeEvent<HTMLInputElement>) => void;
suffix: React.ReactNode;
value?: string;
};
export const UIInput: React.FC<InputProps> = function UIInput(props: InputProps) {
return (
<GetElementsContext>
{(elements: Elements) => {
return <elements.Input {...props} />;
}}
</GetElementsContext>
);
};
export type InputGroupProps = {
className?: string;
compact?: boolean;
style?: React.CSSProperties;
children?: React.ReactNode;
};
export const UIInputGroup = function UIInputGroup(props: InputGroupProps) {
return (
<GetElementsContext>
{(elements: Elements) => {
return <elements.InputGroup {...props} />;
}}
</GetElementsContext>
);
};
export type Elements = {
Popover: React.ComponentType<PopoverProps>;
Tooltip: React.ComponentType<TooltipProps>;
Icon: React.ComponentType<IconProps>;
Dropdown: React.ComponentType<DropdownProps>;
Menu: React.ComponentType<MenuProps>;
MenuItem: React.ComponentType<MenuItemProps>;
Button: React.ComponentType<ButtonProps>;
Divider: React.ComponentType<DividerProps>;
Input: React.ComponentType<InputProps>;
InputGroup: React.ComponentType<InputGroupProps>;
};
/**
* Allows for injecting custom UI elements that will be used. Mainly for styling and removing dependency on
* any specific UI library but can also inject specific behaviour.
*/
const UIElementsContext = React.createContext<Elements | undefined>(undefined);
UIElementsContext.displayName = 'UIElementsContext';
export default UIElementsContext;
type GetElementsContextProps = {
children: (elements: Elements) => React.ReactNode;
};
/**
* Convenience render prop style component to handle error state when elements are not defined.
*/
export function GetElementsContext(props: GetElementsContextProps) {
return (
<UIElementsContext.Consumer>
{(value: Elements | undefined) => {
if (!value) {
throw new Error('Elements context is required. You probably forget to use UIElementsContext.Provider');
}
return props.children(value);
}}
</UIElementsContext.Consumer>
);
}

View File

@ -104,8 +104,8 @@ describe('filterSpans', () => {
};
const spans = [span0, span2];
it('should return `null` if spans is falsy', () => {
expect(filterSpans('operationName', null)).toBe(null);
it('should return `undefined` if spans is falsy', () => {
expect(filterSpans('operationName', null)).toBe(undefined);
});
it('should return spans whose spanID exactly match a filter', () => {

View File

@ -17,7 +17,7 @@ import { TNil } from '../types';
export default function filterSpans(textFilter: string, spans: TraceSpan[] | TNil) {
if (!spans) {
return null;
return undefined;
}
// if a span field includes at least one filter in includeFilters, the span is a match

View File

@ -18,7 +18,6 @@ import {
TraceTimelineViewer,
transformTraceData,
TTraceTimeline,
UIElementsContext,
} from '@jaegertracing/jaeger-ui-components';
import { TraceToLogsData } from 'app/core/components/TraceToLogsSettings';
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
@ -29,7 +28,6 @@ import React, { useCallback, useMemo, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { changePanelState } from '../state/explorePane';
import { createSpanLinkFactory } from './createSpanLink';
import { UIElements } from './uiElements';
import { useChildrenState } from './useChildrenState';
import { useDetailState } from './useDetailState';
import { useHoverIndentGuide } from './useHoverIndentGuide';
@ -77,7 +75,7 @@ export function TraceView(props: Props) {
const [slim, setSlim] = useState(false);
const traceProp = useMemo(() => transformDataFrames(frame), [frame]);
const { search, setSearch, spanFindMatches } = useSearch(traceProp?.spans);
const { search, setSearch, spanFindMatches, clearSearch } = useSearch(traceProp?.spans);
const datasource = useSelector(
(state: StoreState) => state.explore[props.exploreId]?.datasourceInstance ?? undefined
@ -115,10 +113,10 @@ export function TraceView(props: Props) {
}
return (
<UIElementsContext.Provider value={UIElements}>
<>
<TracePageHeader
canCollapse={false}
clearSearch={noop}
clearSearch={clearSearch}
focusUiFindMatches={noop}
hideMap={false}
hideSummary={false}
@ -126,17 +124,14 @@ export function TraceView(props: Props) {
onSlimViewClicked={onSlimViewClicked}
onTraceGraphViewClicked={noop}
prevResult={noop}
resultCount={0}
resultCount={spanFindMatches?.size ?? 0}
slimView={slim}
textFilter={null}
trace={traceProp}
traceGraphView={false}
updateNextViewRangeTime={updateNextViewRangeTime}
updateViewRangeTime={updateViewRangeTime}
viewRange={viewRange}
searchValue={search}
onSearchValueChange={setSearch}
hideSearchButtons={true}
timeZone={timeZone}
/>
<TraceTimelineViewer
@ -175,7 +170,7 @@ export function TraceView(props: Props) {
focusedSpanId={focusedSpanId}
createFocusSpanLink={createFocusSpanLink}
/>
</UIElementsContext.Provider>
</>
);
}

View File

@ -1,95 +0,0 @@
import { GrafanaTheme } from '@grafana/data';
import {
Button,
Input,
Popover,
PopoverController,
stylesFactory,
Tooltip as GrafanaTooltip,
useTheme,
} from '@grafana/ui';
import { ButtonProps, Elements, PopoverProps, TooltipProps } from '@jaegertracing/jaeger-ui-components';
import cx from 'classnames';
import { css } from '@emotion/css';
import React, { useRef } from 'react';
/**
* Right now Jaeger components need some UI elements to be injected. This is to get rid of AntD UI library that was
* used by default.
*/
// This needs to be static to prevent remounting on every render.
export const UIElements: Elements = {
Popover({ children, content, overlayClassName }: PopoverProps) {
const popoverRef = useRef<HTMLElement>(null);
return (
<PopoverController content={content} hideAfter={300}>
{(showPopper, hidePopper, popperProps) => {
return (
<>
{popoverRef.current && (
<Popover
{...popperProps}
referenceElement={popoverRef.current}
wrapperClassName={overlayClassName}
onMouseLeave={hidePopper}
onMouseEnter={showPopper}
/>
)}
{React.cloneElement(children, {
ref: popoverRef,
onMouseEnter: showPopper,
onMouseLeave: hidePopper,
})}
</>
);
}}
</PopoverController>
);
},
Tooltip({ children, title }: TooltipProps) {
return <GrafanaTooltip content={title}>{children}</GrafanaTooltip>;
},
Icon: (() => null as any) as any,
Dropdown: (() => null as any) as any,
Menu: (() => null as any) as any,
MenuItem: (() => null as any) as any,
Button({ onClick, children, className }: ButtonProps) {
return (
<Button variant="secondary" onClick={onClick} className={className}>
{children}
</Button>
);
},
Divider,
Input(props) {
return <Input {...props} />;
},
InputGroup({ children, className, style }) {
return (
<span className={className} style={style}>
{children}
</span>
);
},
};
const getStyles = stylesFactory((theme: GrafanaTheme) => {
return {
Divider: css`
display: inline-block;
background: ${theme.isDark ? '#242424' : '#e8e8e8'};
width: 1px;
height: 0.9em;
margin: 0 8px;
vertical-align: middle;
`,
};
});
function Divider({ className }: { className?: string }) {
const styles = getStyles(useTheme());
return <div style={{}} className={cx(styles.Divider, className)} />;
}

View File

@ -1,4 +1,4 @@
import { useMemo, useState } from 'react';
import { useCallback, useMemo, useState } from 'react';
import { filterSpans, TraceSpan } from '@jaegertracing/jaeger-ui-components';
/**
@ -7,9 +7,13 @@ import { filterSpans, TraceSpan } from '@jaegertracing/jaeger-ui-components';
*/
export function useSearch(spans?: TraceSpan[]) {
const [search, setSearch] = useState('');
const spanFindMatches: Set<string> | undefined | null = useMemo(() => {
const spanFindMatches: Set<string> | undefined = useMemo(() => {
return search && spans ? filterSpans(search, spans) : undefined;
}, [search, spans]);
return { search, setSearch, spanFindMatches };
const clearSearch = useCallback(() => {
setSearch('');
}, [setSearch]);
return { search, setSearch, spanFindMatches, clearSearch };
}