mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
1cf48618de
commit
a58e6ab622
@ -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}
|
||||
|
@ -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} />}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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 && (
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
53
packages/jaeger-ui-components/src/common/Divider.tsx
Normal file
53
packages/jaeger-ui-components/src/common/Divider.tsx
Normal 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
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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} />;
|
||||
}
|
||||
|
@ -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 [
|
||||
|
@ -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,
|
||||
};
|
38
packages/jaeger-ui-components/src/common/Popover.tsx
Normal file
38
packages/jaeger-ui-components/src/common/Popover.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -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>;
|
||||
}
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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}
|
||||
/>
|
||||
|
@ -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>
|
||||
`;
|
||||
|
@ -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 />}
|
||||
|
@ -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>';
|
||||
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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', () => {
|
||||
|
@ -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
|
||||
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -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)} />;
|
||||
}
|
@ -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 };
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user