Tracing: Dark theme styling for TraceView (#23406)

* Add integration with Jeager
Add Jaeger datasource and modify derived fields in loki to allow for opening a trace in Jager in separate split.
Modifies build so that this branch docker images are pushed to docker hub
Add a traceui dir with docker-compose and provision files for demoing.:wq

* Enable docker logger plugin to send logs to loki

* Add placeholder zipkin datasource

* Fixed rebase issues, added enhanceDataFrame to non-legacy code path

* Trace selector for jaeger query field

* Fix logs default mode for Loki

* Fix loading jaeger query field services on split

* Updated grafana image in traceui/compose file

* Fix prettier error

* Hide behind feature flag, clean up unused code.

* Fix tests

* Fix tests

* Cleanup code and review feedback

* Remove traceui directory

* Remove circle build changes

* Fix feature toggles object

* Fix merge issues

* Add trace ui in Explore

* WIP

* WIP

* WIP

* Make jaeger datasource return trace data instead of link

* Allow js in jest tests

* Return data from Jaeger datasource

* Take yarn.lock from master

* Fix missing component

* Update yarn lock

* Fix some ts and lint errors

* Fix merge

* Fix type errors

* Make tests pass again

* Add tests

* Fix es5 compatibility

* Add header with minimap

* Fix sizing issue due to column resizer handle

* Fix issues with sizing, search functionality, duplicate react, tests

* Refactor TraceView component, fix tests

* Fix type errors

* Add dark theme styling

* Add tests for hooks

* More color changes

* Fix tests to deal with additional theme wrappers.

Co-authored-by: David Kaltschmidt <david.kaltschmidt@gmail.com>
This commit is contained in:
Andrej Ocenas 2020-04-14 13:26:33 +02:00 committed by GitHub
parent 754dfdfa87
commit cf1ebd5a3d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
34 changed files with 562 additions and 339 deletions

View File

@ -36,6 +36,7 @@
"moment": "^2.18.1", "moment": "^2.18.1",
"react-icons": "2.2.7", "react-icons": "2.2.7",
"recompose": "^0.25.0", "recompose": "^0.25.0",
"tinycolor2": "1.4.1",
"tween-functions": "^1.2.0" "tween-functions": "^1.2.0"
} }
} }

View File

@ -12,20 +12,32 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import React from 'react'; import React, { useContext } from 'react';
import hoistNonReactStatics from 'hoist-non-react-statics'; import hoistNonReactStatics from 'hoist-non-react-statics';
import memoizeOne from 'memoize-one'; import memoizeOne from 'memoize-one';
import tinycolor from 'tinycolor2';
export type ThemeOptions = Partial<Theme>; export type ThemeOptions = Partial<Theme>;
export enum ThemeType {
Dark,
Light,
}
export type Theme = { export type Theme = {
type: ThemeType;
borderStyle: string; borderStyle: string;
}; };
export const defaultTheme: Theme = { export const defaultTheme: Theme = {
type: ThemeType.Light,
borderStyle: '1px solid #bbb', borderStyle: '1px solid #bbb',
}; };
export function isLight(theme: Theme) {
return theme.type === ThemeType.Light;
}
const ThemeContext = React.createContext<ThemeOptions | undefined>(undefined); const ThemeContext = React.createContext<ThemeOptions | undefined>(undefined);
ThemeContext.displayName = 'ThemeContext'; ThemeContext.displayName = 'ThemeContext';
@ -60,14 +72,16 @@ export const withTheme = <Props extends { theme: Theme }, Statics extends {} = {
let WithTheme: React.ComponentType<Omit<Props, 'theme'>> = props => { let WithTheme: React.ComponentType<Omit<Props, 'theme'>> = props => {
return ( return (
<ThemeConsumer> <ThemeConsumer>
{(theme: Theme) => ( {(theme: Theme) => {
return (
<Component <Component
{...({ {...({
...props, ...props,
theme, theme,
} as Props & { theme: Theme })} } as Props & { theme: Theme })}
/> />
)} );
}}
</ThemeConsumer> </ThemeConsumer>
); );
}; };
@ -81,6 +95,51 @@ export const withTheme = <Props extends { theme: Theme }, Statics extends {} = {
return WithTheme as WrappedWithThemeComponent<Props>; return WithTheme as WrappedWithThemeComponent<Props>;
}; };
export function useTheme(): Theme {
const theme = useContext(ThemeContext);
return {
...defaultTheme,
...theme,
};
}
export const createStyle = <Fn extends (this: any, ...newArgs: any[]) => ReturnType<Fn>>(fn: Fn) => { export const createStyle = <Fn extends (this: any, ...newArgs: any[]) => ReturnType<Fn>>(fn: Fn) => {
return memoizeOne(fn); return memoizeOne(fn);
}; };
/**
* Tries to get a dark variant color. Either by simply inverting the luminosity and darkening or lightening the color
* a bit, or if base is provided, tries 2 variants of lighter and darker colors and checks which is more readable with
* the base.
* @param theme
* @param hex
* @param base
*/
export function autoColor(theme: Theme, hex: string, base?: string) {
if (isLight(theme)) {
return hex;
} else {
if (base) {
const color = tinycolor(hex);
return tinycolor
.mostReadable(
base,
[
color.clone().lighten(25),
color.clone().lighten(10),
color,
color.clone().darken(10),
color.clone().darken(25),
],
{
includeFallbackColors: false,
}
)
.toHex8String();
}
const color = tinycolor(hex).toHsl();
color.l = 1 - color.l;
const newColor = tinycolor(color);
return newColor.isLight() ? newColor.darken(5).toHex8String() : newColor.lighten(5).toHex8String();
}
}

View File

@ -15,12 +15,13 @@
import React from 'react'; import React from 'react';
import { shallow } from 'enzyme'; import { shallow } from 'enzyme';
import CanvasSpanGraph from './CanvasSpanGraph'; import { UnthemedCanvasSpanGraph } from './CanvasSpanGraph';
import { defaultTheme } from '../../Theme';
describe('<CanvasSpanGraph>', () => { describe('<CanvasSpanGraph>', () => {
it('renders without exploding', () => { it('renders without exploding', () => {
const items = [{ valueWidth: 1, valueOffset: 1, serviceName: 'service-name-0' }]; const items = [{ valueWidth: 1, valueOffset: 1, serviceName: 'service-name-0' }];
const wrapper = shallow(<CanvasSpanGraph items={[]} valueWidth={4000} />); const wrapper = shallow(<UnthemedCanvasSpanGraph items={[]} valueWidth={4000} theme={defaultTheme} />);
expect(wrapper).toBeDefined(); expect(wrapper).toBeDefined();
wrapper.instance()._setCanvasRef({ wrapper.instance()._setCanvasRef({
getContext: () => ({ getContext: () => ({

View File

@ -16,16 +16,16 @@ import * as React from 'react';
import { css } from 'emotion'; import { css } from 'emotion';
import renderIntoCanvas from './render-into-canvas'; import renderIntoCanvas from './render-into-canvas';
import colorGenerator from '../../utils/color-generator'; import { getRgbColorByKey } from '../../utils/color-generator';
import { TNil } from '../../types'; import { TNil } from '../../types';
import { createStyle } from '../../Theme'; import { autoColor, createStyle, Theme, withTheme } from '../../Theme';
const getStyles = createStyle(() => { const getStyles = createStyle((theme: Theme) => {
return { return {
CanvasSpanGraph: css` CanvasSpanGraph: css`
label: CanvasSpanGraph; label: CanvasSpanGraph;
background: #fafafa; background: ${autoColor(theme, '#fafafa')};
height: 60px; height: 60px;
position: absolute; position: absolute;
width: 100%; width: 100%;
@ -36,11 +36,10 @@ const getStyles = createStyle(() => {
type CanvasSpanGraphProps = { type CanvasSpanGraphProps = {
items: Array<{ valueWidth: number; valueOffset: number; serviceName: string }>; items: Array<{ valueWidth: number; valueOffset: number; serviceName: string }>;
valueWidth: number; valueWidth: number;
theme: Theme;
}; };
const getColor = (hex: string) => colorGenerator.getRgbColorByKey(hex); export class UnthemedCanvasSpanGraph extends React.PureComponent<CanvasSpanGraphProps> {
export default class CanvasSpanGraph extends React.PureComponent<CanvasSpanGraphProps> {
_canvasElm: HTMLCanvasElement | TNil; _canvasElm: HTMLCanvasElement | TNil;
constructor(props: CanvasSpanGraphProps) { constructor(props: CanvasSpanGraphProps) {
@ -48,6 +47,8 @@ export default class CanvasSpanGraph extends React.PureComponent<CanvasSpanGraph
this._canvasElm = undefined; this._canvasElm = undefined;
} }
getColor = (key: string) => getRgbColorByKey(key, this.props.theme);
componentDidMount() { componentDidMount() {
this._draw(); this._draw();
} }
@ -63,11 +64,13 @@ export default class CanvasSpanGraph extends React.PureComponent<CanvasSpanGraph
_draw() { _draw() {
if (this._canvasElm) { if (this._canvasElm) {
const { valueWidth: totalValueWidth, items } = this.props; const { valueWidth: totalValueWidth, items } = this.props;
renderIntoCanvas(this._canvasElm, items, totalValueWidth, getColor); renderIntoCanvas(this._canvasElm, items, totalValueWidth, this.getColor, autoColor(this.props.theme, '#fff'));
} }
} }
render() { render() {
return <canvas className={getStyles().CanvasSpanGraph} ref={this._setCanvasRef} />; return <canvas className={getStyles(this.props.theme).CanvasSpanGraph} ref={this._setCanvasRef} />;
} }
} }
export default withTheme(UnthemedCanvasSpanGraph);

View File

@ -20,6 +20,7 @@ import Scrubber from './Scrubber';
import ViewingLayer, { dragTypes, getStyles } from './ViewingLayer'; import ViewingLayer, { dragTypes, getStyles } from './ViewingLayer';
import { EUpdateTypes } from '../../utils/DraggableManager'; import { EUpdateTypes } from '../../utils/DraggableManager';
import { polyfill as polyfillAnimationFrame } from '../../utils/test/requestAnimationFrame'; import { polyfill as polyfillAnimationFrame } from '../../utils/test/requestAnimationFrame';
import { defaultTheme } from '../../Theme';
function getViewRange(viewStart, viewEnd) { function getViewRange(viewStart, viewEnd) {
return { return {
@ -43,13 +44,19 @@ describe('<SpanGraph>', () => {
updateViewRangeTime: jest.fn(), updateViewRangeTime: jest.fn(),
viewRange: getViewRange(0, 1), viewRange: getViewRange(0, 1),
}; };
wrapper = shallow(<ViewingLayer {...props} />); wrapper = shallow(<ViewingLayer {...props} />)
.dive()
.dive()
.dive();
}); });
describe('_getDraggingBounds()', () => { describe('_getDraggingBounds()', () => {
beforeEach(() => { beforeEach(() => {
props = { ...props, viewRange: getViewRange(0.1, 0.9) }; props = { ...props, viewRange: getViewRange(0.1, 0.9) };
wrapper = shallow(<ViewingLayer {...props} />); wrapper = shallow(<ViewingLayer {...props} />)
.dive()
.dive()
.dive();
wrapper.instance()._setRoot({ wrapper.instance()._setRoot({
getBoundingClientRect() { getBoundingClientRect() {
return { left: 10, width: 100 }; return { left: 10, width: 100 };
@ -122,7 +129,10 @@ describe('<SpanGraph>', () => {
const anchor = 0.1; const anchor = 0.1;
const time = { ...props.viewRange.time, reframe: { anchor } }; const time = { ...props.viewRange.time, reframe: { anchor } };
props = { ...props, viewRange: { time } }; props = { ...props, viewRange: { time } };
wrapper = shallow(<ViewingLayer {...props} />); wrapper = shallow(<ViewingLayer {...props} />)
.dive()
.dive()
.dive();
wrapper.instance()._handleReframeDragUpdate({ value }); wrapper.instance()._handleReframeDragUpdate({ value });
const calls = props.updateNextViewRangeTime.mock.calls; const calls = props.updateNextViewRangeTime.mock.calls;
expect(calls).toEqual([[{ reframe: { anchor, shift: value } }]]); expect(calls).toEqual([[{ reframe: { anchor, shift: value } }]]);
@ -149,7 +159,10 @@ describe('<SpanGraph>', () => {
const anchor = 0.6; const anchor = 0.6;
const time = { ...props.viewRange.time, reframe: { anchor } }; const time = { ...props.viewRange.time, reframe: { anchor } };
props = { ...props, viewRange: { time } }; props = { ...props, viewRange: { time } };
wrapper = shallow(<ViewingLayer {...props} />); wrapper = shallow(<ViewingLayer {...props} />)
.dive()
.dive()
.dive();
wrapper.instance()._handleReframeDragEnd({ manager, value }); wrapper.instance()._handleReframeDragEnd({ manager, value });
expect(manager.resetBounds.mock.calls).toEqual([[]]); expect(manager.resetBounds.mock.calls).toEqual([[]]);
@ -162,7 +175,10 @@ describe('<SpanGraph>', () => {
const anchor = 0.4; const anchor = 0.4;
const time = { ...props.viewRange.time, reframe: { anchor } }; const time = { ...props.viewRange.time, reframe: { anchor } };
props = { ...props, viewRange: { time } }; props = { ...props, viewRange: { time } };
wrapper = shallow(<ViewingLayer {...props} />); wrapper = shallow(<ViewingLayer {...props} />)
.dive()
.dive()
.dive();
wrapper.instance()._handleReframeDragEnd({ manager, value }); wrapper.instance()._handleReframeDragEnd({ manager, value });
expect(manager.resetBounds.mock.calls).toEqual([[]]); expect(manager.resetBounds.mock.calls).toEqual([[]]);
@ -258,28 +274,28 @@ describe('<SpanGraph>', () => {
describe('.ViewingLayer--resetZoom', () => { describe('.ViewingLayer--resetZoom', () => {
it('should not render .ViewingLayer--resetZoom if props.viewRange.time.current = [0,1]', () => { it('should not render .ViewingLayer--resetZoom if props.viewRange.time.current = [0,1]', () => {
expect(wrapper.find(`.${getStyles().ViewingLayerResetZoom}`).length).toBe(0); expect(wrapper.find(`.${getStyles(defaultTheme).ViewingLayerResetZoom}`).length).toBe(0);
wrapper.setProps({ viewRange: { time: { current: [0, 1] } } }); wrapper.setProps({ viewRange: { time: { current: [0, 1] } } });
expect(wrapper.find(`.${getStyles().ViewingLayerResetZoom}`).length).toBe(0); expect(wrapper.find(`.${getStyles(defaultTheme).ViewingLayerResetZoom}`).length).toBe(0);
}); });
it('should render ViewingLayer--resetZoom if props.viewRange.time.current[0] !== 0', () => { it('should render ViewingLayer--resetZoom if props.viewRange.time.current[0] !== 0', () => {
// If the test fails on the following expect statement, this may be a false negative // If the test fails on the following expect statement, this may be a false negative
expect(wrapper.find(`.${getStyles().ViewingLayerResetZoom}`).length).toBe(0); expect(wrapper.find(`.${getStyles(defaultTheme).ViewingLayerResetZoom}`).length).toBe(0);
wrapper.setProps({ viewRange: { time: { current: [0.1, 1] } } }); wrapper.setProps({ viewRange: { time: { current: [0.1, 1] } } });
expect(wrapper.find(`.${getStyles().ViewingLayerResetZoom}`).length).toBe(1); expect(wrapper.find(`.${getStyles(defaultTheme).ViewingLayerResetZoom}`).length).toBe(1);
}); });
it('should render ViewingLayer--resetZoom if props.viewRange.time.current[1] !== 1', () => { it('should render ViewingLayer--resetZoom if props.viewRange.time.current[1] !== 1', () => {
// If the test fails on the following expect statement, this may be a false negative // If the test fails on the following expect statement, this may be a false negative
expect(wrapper.find(`.${getStyles().ViewingLayerResetZoom}`).length).toBe(0); expect(wrapper.find(`.${getStyles(defaultTheme).ViewingLayerResetZoom}`).length).toBe(0);
wrapper.setProps({ viewRange: { time: { current: [0, 0.9] } } }); wrapper.setProps({ viewRange: { time: { current: [0, 0.9] } } });
expect(wrapper.find(`.${getStyles().ViewingLayerResetZoom}`).length).toBe(1); expect(wrapper.find(`.${getStyles(defaultTheme).ViewingLayerResetZoom}`).length).toBe(1);
}); });
it('should call props.updateViewRangeTime when clicked', () => { it('should call props.updateViewRangeTime when clicked', () => {
wrapper.setProps({ viewRange: { time: { current: [0.1, 0.9] } } }); wrapper.setProps({ viewRange: { time: { current: [0.1, 0.9] } } });
const resetZoomButton = wrapper.find(`.${getStyles().ViewingLayerResetZoom}`); const resetZoomButton = wrapper.find(`.${getStyles(defaultTheme).ViewingLayerResetZoom}`);
// If the test fails on the following expect statement, this may be a false negative caused // If the test fails on the following expect statement, this may be a false negative caused
// by a regression to rendering. // by a regression to rendering.
expect(resetZoomButton.length).toBe(1); expect(resetZoomButton.length).toBe(1);
@ -296,9 +312,12 @@ describe('<SpanGraph>', () => {
it('renders a filtering box if leftBound exists', () => { it('renders a filtering box if leftBound exists', () => {
const _props = { ...props, viewRange: getViewRange(0.2, 1) }; const _props = { ...props, viewRange: getViewRange(0.2, 1) };
wrapper = shallow(<ViewingLayer {..._props} />); wrapper = shallow(<ViewingLayer {..._props} />)
.dive()
.dive()
.dive();
const leftBox = wrapper.find(`.${getStyles().ViewingLayerInactive}`); const leftBox = wrapper.find(`.${getStyles(defaultTheme).ViewingLayerInactive}`);
expect(leftBox.length).toBe(1); expect(leftBox.length).toBe(1);
const width = Number(leftBox.prop('width').slice(0, -1)); const width = Number(leftBox.prop('width').slice(0, -1));
const x = leftBox.prop('x'); const x = leftBox.prop('x');
@ -308,9 +327,12 @@ describe('<SpanGraph>', () => {
it('renders a filtering box if rightBound exists', () => { it('renders a filtering box if rightBound exists', () => {
const _props = { ...props, viewRange: getViewRange(0, 0.8) }; const _props = { ...props, viewRange: getViewRange(0, 0.8) };
wrapper = shallow(<ViewingLayer {..._props} />); wrapper = shallow(<ViewingLayer {..._props} />)
.dive()
.dive()
.dive();
const rightBox = wrapper.find(`.${getStyles().ViewingLayerInactive}`); const rightBox = wrapper.find(`.${getStyles(defaultTheme).ViewingLayerInactive}`);
expect(rightBox.length).toBe(1); expect(rightBox.length).toBe(1);
const width = Number(rightBox.prop('width').slice(0, -1)); const width = Number(rightBox.prop('width').slice(0, -1));
const x = Number(rightBox.prop('x').slice(0, -1)); const x = Number(rightBox.prop('x').slice(0, -1));

View File

@ -19,12 +19,13 @@ import { css } from 'emotion';
import GraphTicks from './GraphTicks'; import GraphTicks from './GraphTicks';
import Scrubber from './Scrubber'; import Scrubber from './Scrubber';
import { TUpdateViewRangeTimeFunction, UIButton, ViewRange, ViewRangeTimeUpdate } from '../..'; import { TUpdateViewRangeTimeFunction, UIButton, ViewRange, ViewRangeTimeUpdate } from '../..';
import { withTheme, Theme, autoColor } from '../../Theme';
import { TNil } from '../..'; import { TNil } from '../..';
import DraggableManager, { DraggableBounds, DraggingUpdate, EUpdateTypes } from '../../utils/DraggableManager'; import DraggableManager, { DraggableBounds, DraggingUpdate, EUpdateTypes } from '../../utils/DraggableManager';
import { createStyle } from '../../Theme'; import { createStyle } from '../../Theme';
export const getStyles = createStyle(() => { export const getStyles = createStyle((theme: Theme) => {
// Need this cause emotion will merge emotion generated classes into single className if used with cx from emotion // Need this cause emotion will merge emotion generated classes into single className if used with cx from emotion
// package and the selector won't work // package and the selector won't work
const ViewingLayerResetZoomHoverClassName = 'JaegerUiComponents__ViewingLayerResetZoomHoverClassName'; const ViewingLayerResetZoomHoverClassName = 'JaegerUiComponents__ViewingLayerResetZoomHoverClassName';
@ -48,7 +49,7 @@ export const getStyles = createStyle(() => {
`, `,
ViewingLayerGraph: css` ViewingLayerGraph: css`
label: ViewingLayerGraph; label: ViewingLayerGraph;
border: 1px solid #999; border: 1px solid ${autoColor(theme, '#999')};
/* need !important here to overcome something from semantic UI */ /* need !important here to overcome something from semantic UI */
overflow: visible !important; overflow: visible !important;
position: relative; position: relative;
@ -57,11 +58,11 @@ export const getStyles = createStyle(() => {
`, `,
ViewingLayerInactive: css` ViewingLayerInactive: css`
label: ViewingLayerInactive; label: ViewingLayerInactive;
fill: rgba(214, 214, 214, 0.5); fill: ${autoColor(theme, 'rgba(214, 214, 214, 0.5)')};
`, `,
ViewingLayerCursorGuide: css` ViewingLayerCursorGuide: css`
label: ViewingLayerCursorGuide; label: ViewingLayerCursorGuide;
stroke: #f44; stroke: ${autoColor(theme, '#f44')};
stroke-width: 1; stroke-width: 1;
`, `,
ViewingLayerDraggedShift: css` ViewingLayerDraggedShift: css`
@ -70,7 +71,7 @@ export const getStyles = createStyle(() => {
`, `,
ViewingLayerDrag: css` ViewingLayerDrag: css`
label: ViewingLayerDrag; label: ViewingLayerDrag;
fill: #44f; fill: ${autoColor(theme, '#44f')};
`, `,
ViewingLayerFullOverlay: css` ViewingLayerFullOverlay: css`
label: ViewingLayerFullOverlay; label: ViewingLayerFullOverlay;
@ -93,6 +94,7 @@ type ViewingLayerProps = {
updateViewRangeTime: TUpdateViewRangeTimeFunction; updateViewRangeTime: TUpdateViewRangeTimeFunction;
updateNextViewRangeTime: (update: ViewRangeTimeUpdate) => void; updateNextViewRangeTime: (update: ViewRangeTimeUpdate) => void;
viewRange: ViewRange; viewRange: ViewRange;
theme: Theme;
}; };
type ViewingLayerState = { type ViewingLayerState = {
@ -140,7 +142,7 @@ function getNextViewLayout(start: number, position: number) {
* `ViewingLayer` is rendered on top of the Canvas rendering of the minimap and * `ViewingLayer` is rendered on top of the Canvas rendering of the minimap and
* handles showing the current view range and handles mouse UX for modifying it. * handles showing the current view range and handles mouse UX for modifying it.
*/ */
export default class ViewingLayer extends React.PureComponent<ViewingLayerProps, ViewingLayerState> { export class UnthemedViewingLayer extends React.PureComponent<ViewingLayerProps, ViewingLayerState> {
state: ViewingLayerState; state: ViewingLayerState;
_root: Element | TNil; _root: Element | TNil;
@ -298,7 +300,7 @@ export default class ViewingLayer extends React.PureComponent<ViewingLayerProps,
* @returns React.Node[] * @returns React.Node[]
*/ */
_getMarkers(from: number, to: number) { _getMarkers(from: number, to: number) {
const styles = getStyles(); const styles = getStyles(this.props.theme);
const layout = getNextViewLayout(from, to); const layout = getNextViewLayout(from, to);
return [ return [
<rect <rect
@ -321,7 +323,7 @@ export default class ViewingLayer extends React.PureComponent<ViewingLayerProps,
} }
render() { render() {
const { height, viewRange, numTicks } = this.props; const { height, viewRange, numTicks, theme } = this.props;
const { preventCursorLine } = this.state; const { preventCursorLine } = this.state;
const { current, cursor, shiftStart, shiftEnd, reframe } = viewRange.time; const { current, cursor, shiftStart, shiftEnd, reframe } = viewRange.time;
const haveNextTimeRange = shiftStart != null || shiftEnd != null || reframe != null; const haveNextTimeRange = shiftStart != null || shiftEnd != null || reframe != null;
@ -338,7 +340,7 @@ export default class ViewingLayer extends React.PureComponent<ViewingLayerProps,
if (!haveNextTimeRange && cursor != null && !preventCursorLine) { if (!haveNextTimeRange && cursor != null && !preventCursorLine) {
cursorPosition = `${cursor * 100}%`; cursorPosition = `${cursor * 100}%`;
} }
const styles = getStyles(); const styles = getStyles(theme);
return ( return (
<div aria-hidden className={styles.ViewingLayer} style={{ height }}> <div aria-hidden className={styles.ViewingLayer} style={{ height }}>
@ -406,3 +408,5 @@ export default class ViewingLayer extends React.PureComponent<ViewingLayerProps,
); );
} }
} }
export default withTheme(UnthemedViewingLayer);

View File

@ -15,7 +15,6 @@
import { TNil } from '../..'; import { TNil } from '../..';
// exported for tests // exported for tests
export const BG_COLOR = '#fff';
export const ITEM_ALPHA = 0.8; export const ITEM_ALPHA = 0.8;
export const MIN_ITEM_HEIGHT = 2; export const MIN_ITEM_HEIGHT = 2;
export const MAX_TOTAL_HEIGHT = 200; export const MAX_TOTAL_HEIGHT = 200;
@ -27,7 +26,8 @@ export default function renderIntoCanvas(
canvas: HTMLCanvasElement, canvas: HTMLCanvasElement,
items: Array<{ valueWidth: number; valueOffset: number; serviceName: string }>, items: Array<{ valueWidth: number; valueOffset: number; serviceName: string }>,
totalValueWidth: number, totalValueWidth: number,
getFillColor: (serviceName: string) => [number, number, number] getFillColor: (serviceName: string) => [number, number, number],
bgColor: string
) { ) {
const fillCache: Map<string, string | TNil> = new Map(); const fillCache: Map<string, string | TNil> = new Map();
const cHeight = items.length < MIN_TOTAL_HEIGHT ? MIN_TOTAL_HEIGHT : Math.min(items.length, MAX_TOTAL_HEIGHT); const cHeight = items.length < MIN_TOTAL_HEIGHT ? MIN_TOTAL_HEIGHT : Math.min(items.length, MAX_TOTAL_HEIGHT);
@ -40,7 +40,7 @@ export default function renderIntoCanvas(
const itemYChange = cHeight / items.length; const itemYChange = cHeight / items.length;
const ctx = canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D; const ctx = canvas.getContext('2d', { alpha: false }) as CanvasRenderingContext2D;
ctx.fillStyle = BG_COLOR; ctx.fillStyle = bgColor;
ctx.fillRect(0, 0, cWidth, cHeight); ctx.fillRect(0, 0, cWidth, cHeight);
for (let i = 0; i < items.length; i++) { for (let i = 0; i < items.length; i++) {
const { valueWidth, valueOffset, serviceName } = items[i]; const { valueWidth, valueOffset, serviceName } = items[i];

View File

@ -22,7 +22,7 @@ import cx from 'classnames';
import SpanGraph from './SpanGraph'; import SpanGraph from './SpanGraph';
import TracePageSearchBar from './TracePageSearchBar'; import TracePageSearchBar from './TracePageSearchBar';
import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from '..'; import { autoColor, Theme, TUpdateViewRangeTimeFunction, useTheme, ViewRange, ViewRangeTimeUpdate } from '..';
import LabeledList from '../common/LabeledList'; import LabeledList from '../common/LabeledList';
import TraceName from '../common/TraceName'; import TraceName from '../common/TraceName';
import { getTraceName } from '../model/trace-viewer'; import { getTraceName } from '../model/trace-viewer';
@ -35,7 +35,7 @@ import ExternalLinks from '../common/ExternalLinks';
import { createStyle } from '../Theme'; import { createStyle } from '../Theme';
import { uTxMuted } from '../uberUtilityStyles'; import { uTxMuted } from '../uberUtilityStyles';
const getStyles = createStyle(() => { const getStyles = createStyle((theme: Theme) => {
const TracePageHeaderOverviewItemValueDetail = css` const TracePageHeaderOverviewItemValueDetail = css`
label: TracePageHeaderOverviewItemValueDetail; label: TracePageHeaderOverviewItemValueDetail;
color: #aaa; color: #aaa;
@ -44,21 +44,19 @@ const getStyles = createStyle(() => {
TracePageHeader: css` TracePageHeader: css`
label: TracePageHeader; label: TracePageHeader;
& > :first-child { & > :first-child {
border-bottom: 1px solid #e8e8e8; border-bottom: 1px solid ${autoColor(theme, '#e8e8e8')};
} }
& > :nth-child(2) { & > :nth-child(2) {
background-color: #eee; background-color: ${autoColor(theme, '#eee')};
border-bottom: 1px solid #e4e4e4; border-bottom: 1px solid ${autoColor(theme, '#e4e4e4')};
} }
& > :last-child { & > :last-child {
background-color: #f8f8f8; border-bottom: 1px solid ${autoColor(theme, '#ccc')};
border-bottom: 1px solid #ccc;
} }
`, `,
TracePageHeaderTitleRow: css` TracePageHeaderTitleRow: css`
label: TracePageHeaderTitleRow; label: TracePageHeaderTitleRow;
align-items: center; align-items: center;
background-color: #fff;
display: flex; display: flex;
`, `,
TracePageHeaderBack: css` TracePageHeaderBack: css`
@ -81,7 +79,6 @@ const getStyles = createStyle(() => {
TracePageHeaderTitleLink: css` TracePageHeaderTitleLink: css`
label: TracePageHeaderTitleLink; label: TracePageHeaderTitleLink;
align-items: center; align-items: center;
color: rgba(0, 0, 0, 0.85);
display: flex; display: flex;
flex: 1; flex: 1;
@ -118,7 +115,7 @@ const getStyles = createStyle(() => {
TracePageHeaderOverviewItems: css` TracePageHeaderOverviewItems: css`
label: TracePageHeaderOverviewItems; label: TracePageHeaderOverviewItems;
border-bottom: 1px solid #e4e4e4; border-bottom: 1px solid #e4e4e4;
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem !important;
`, `,
TracePageHeaderOverviewItemValueDetail, TracePageHeaderOverviewItemValueDetail,
TracePageHeaderOverviewItemValue: css` TracePageHeaderOverviewItemValue: css`
@ -163,7 +160,7 @@ export const HEADER_ITEMS = [
key: 'timestamp', key: 'timestamp',
label: 'Trace Start', label: 'Trace Start',
renderer: (trace: Trace) => { renderer: (trace: Trace) => {
const styles = getStyles(); const styles = getStyles(useTheme());
const dateStr = formatDatetime(trace.startTime); const dateStr = formatDatetime(trace.startTime);
const match = dateStr.match(/^(.+)(:\d\d\.\d+)$/); const match = dateStr.match(/^(.+)(:\d\d\.\d+)$/);
return match ? ( return match ? (
@ -235,7 +232,7 @@ export default function TracePageHeader(props: TracePageHeaderEmbedProps) {
return { ...rest, value: renderer(trace) }; return { ...rest, value: renderer(trace) };
}); });
const styles = getStyles(); const styles = getStyles(useTheme());
const title = ( const title = (
<h1 className={cx(styles.TracePageHeaderTitle, canCollapse && styles.TracePageHeaderTitleCollapsible)}> <h1 className={cx(styles.TracePageHeaderTitle, canCollapse && styles.TracePageHeaderTitleCollapsible)}>

View File

@ -59,6 +59,7 @@ describe('<SpanBarRow>', () => {
beforeEach(() => { beforeEach(() => {
props.onDetailToggled.mockReset(); props.onDetailToggled.mockReset();
props.onChildrenToggled.mockReset(); props.onChildrenToggled.mockReset();
SpanTreeOffset.mockReturnValue(() => {});
wrapper = mount(<SpanBarRow {...props} />); wrapper = mount(<SpanBarRow {...props} />);
}); });
@ -105,7 +106,7 @@ describe('<SpanBarRow>', () => {
props.span props.span
); );
const spanRow = shallow(<SpanBarRow {...props} span={span} />); const spanRow = shallow(<SpanBarRow {...props} span={span} />).dive().dive().dive();
const refButton = spanRow.find(ReferencesButton); const refButton = spanRow.find(ReferencesButton);
expect(refButton.length).toEqual(1); expect(refButton.length).toEqual(1);
expect(refButton.at(0).props().tooltipText).toEqual('Contains multiple references'); expect(refButton.at(0).props().tooltipText).toEqual('Contains multiple references');
@ -127,7 +128,7 @@ describe('<SpanBarRow>', () => {
}, },
props.span props.span
); );
const spanRow = shallow(<SpanBarRow {...props} span={span} />); const spanRow = shallow(<SpanBarRow {...props} span={span} />).dive().dive().dive();
const refButton = spanRow.find(ReferencesButton); const refButton = spanRow.find(ReferencesButton);
expect(refButton.length).toEqual(1); expect(refButton.length).toEqual(1);
expect(refButton.at(0).props().tooltipText).toEqual('This span is referenced by another span'); expect(refButton.at(0).props().tooltipText).toEqual('This span is referenced by another span');
@ -157,7 +158,7 @@ describe('<SpanBarRow>', () => {
}, },
props.span props.span
); );
const spanRow = shallow(<SpanBarRow {...props} span={span} />); const spanRow = shallow(<SpanBarRow {...props} span={span} />).dive().dive().dive();
const refButton = spanRow.find(ReferencesButton); const refButton = spanRow.find(ReferencesButton);
expect(refButton.length).toEqual(1); expect(refButton.length).toEqual(1);
expect(refButton.at(0).props().tooltipText).toEqual('This span is referenced by multiple other spans'); expect(refButton.at(0).props().tooltipText).toEqual('This span is referenced by multiple other spans');

View File

@ -29,9 +29,9 @@ import Ticks from './Ticks';
import { TNil } from '../types'; import { TNil } from '../types';
import { Span } from '../types/trace'; import { Span } from '../types/trace';
import { createStyle } from '../Theme'; import { autoColor, createStyle, Theme, withTheme } from '../Theme';
const getStyles = createStyle(() => { const getStyles = createStyle((theme: Theme) => {
const spanBar = css` const spanBar = css`
label: spanBar; label: spanBar;
`; `;
@ -40,12 +40,12 @@ const getStyles = createStyle(() => {
`; `;
const nameWrapper = css` const nameWrapper = css`
label: nameWrapper; label: nameWrapper;
background: #f8f8f8; background: ${autoColor(theme, '#f8f8f8')};
line-height: 27px; line-height: 27px;
overflow: hidden; overflow: hidden;
display: flex; display: flex;
&:hover { &:hover {
border-right: 1px solid #bbb; border-right: 1px solid ${autoColor(theme, '#bbb')};
float: left; float: left;
min-width: calc(100% + 1px); min-width: calc(100% + 1px);
overflow: visible; overflow: visible;
@ -54,12 +54,12 @@ const getStyles = createStyle(() => {
const nameWrapperMatchingFilter = css` const nameWrapperMatchingFilter = css`
label: nameWrapperMatchingFilter; label: nameWrapperMatchingFilter;
background-color: #fffce4; background-color: ${autoColor(theme, '#fffce4')};
`; `;
const endpointName = css` const endpointName = css`
label: endpointName; label: endpointName;
color: #808080; color: ${autoColor(theme, '#808080')};
`; `;
const view = css` const view = css`
@ -69,14 +69,14 @@ const getStyles = createStyle(() => {
const viewExpanded = css` const viewExpanded = css`
label: viewExpanded; label: viewExpanded;
background: #f8f8f8; background: ${autoColor(theme, '#f8f8f8')};
outline: 1px solid #ddd; outline: 1px solid ${autoColor(theme, '#ddd')};
`; `;
const viewExpandedAndMatchingFilter = css` const viewExpandedAndMatchingFilter = css`
label: viewExpandedAndMatchingFilter; label: viewExpandedAndMatchingFilter;
background: #fff3d7; background: ${autoColor(theme, '#fff3d7')};
outline: 1px solid #ddd; outline: 1px solid ${autoColor(theme, '#ddd')};
`; `;
const nameColumn = css` const nameColumn = css`
@ -105,15 +105,20 @@ const getStyles = createStyle(() => {
opacity: 1; opacity: 1;
} }
&:hover .${spanBarLabel} { &:hover .${spanBarLabel} {
color: #000; color: ${autoColor(theme, '#000')};
} }
&:hover .${nameWrapper} { &:hover .${nameWrapper} {
background: #f8f8f8; background: #f8f8f8;
background: linear-gradient(90deg, #fafafa, #f8f8f8 75%, #eee); background: linear-gradient(
90deg,
${autoColor(theme, '#fafafa')},
${autoColor(theme, '#f8f8f8')} 75%,
${autoColor(theme, '#eee')}
);
} }
&:hover .${view} { &:hover .${view} {
background-color: #f5f5f5; background-color: ${autoColor(theme, '#f5f5f5')};
outline: 1px solid #ddd; outline: 1px solid ${autoColor(theme, '#ddd')};
} }
`, `,
rowClippingLeft: css` rowClippingLeft: css`
@ -123,7 +128,11 @@ const getStyles = createStyle(() => {
height: 100%; height: 100%;
position: absolute; position: absolute;
width: 6px; width: 6px;
background-image: linear-gradient(to right, rgba(25, 25, 25, 0.25), rgba(32, 32, 32, 0)); background-image: linear-gradient(
to right,
${autoColor(theme, 'rgba(25, 25, 25, 0.25)')},
${autoColor(theme, 'rgba(32, 32, 32, 0)')}
);
left: 100%; left: 100%;
z-index: -1; z-index: -1;
} }
@ -135,7 +144,11 @@ const getStyles = createStyle(() => {
height: 100%; height: 100%;
position: absolute; position: absolute;
width: 6px; width: 6px;
background-image: linear-gradient(to left, rgba(25, 25, 25, 0.25), rgba(32, 32, 32, 0)); background-image: linear-gradient(
to left,
${autoColor(theme, 'rgba(25, 25, 25, 0.25)')},
${autoColor(theme, 'rgba(25, 25, 25, 0.25)')}
);
right: 0%; right: 0%;
z-index: 1; z-index: 1;
} }
@ -146,41 +159,46 @@ const getStyles = createStyle(() => {
opacity: 1; opacity: 1;
} }
& .${spanBarLabel} { & .${spanBarLabel} {
color: #000; color: ${autoColor(theme, '#000')};
} }
& .${nameWrapper}, &:hover .${nameWrapper} { & .${nameWrapper}, &:hover .${nameWrapper} {
background: #f0f0f0; background: ${autoColor(theme, '#f0f0f0')};
box-shadow: 0 1px 0 #ddd; box-shadow: 0 1px 0 ${autoColor(theme, '#ddd')};
} }
& .${nameWrapperMatchingFilter} { & .${nameWrapperMatchingFilter} {
background: #fff3d7; background: ${autoColor(theme, '#fff3d7')};
} }
&:hover .${view} { &:hover .${view} {
background: #eee; background: ${autoColor(theme, '#eee')};
} }
`, `,
rowMatchingFilter: css` rowMatchingFilter: css`
label: rowMatchingFilter; label: rowMatchingFilter;
background-color: #fffce4; background-color: ${autoColor(theme, '#fffce4')};
&:hover .${nameWrapper} { &:hover .${nameWrapper} {
background: linear-gradient(90deg, #fff5e1, #fff5e1 75%, #ffe6c9); background: linear-gradient(
90deg,
${autoColor(theme, '#fff5e1')},
${autoColor(theme, '#fff5e1')} 75%,
${autoColor(theme, '#ffe6c9')}
);
} }
&:hover .${view} { &:hover .${view} {
background-color: #fff3d7; background-color: ${autoColor(theme, '#fff3d7')};
outline: 1px solid #ddd; outline: 1px solid ${autoColor(theme, '#ddd')};
} }
`, `,
rowExpandedAndMatchingFilter: css` rowExpandedAndMatchingFilter: css`
label: rowExpandedAndMatchingFilter; label: rowExpandedAndMatchingFilter;
&:hover .${view} { &:hover .${view} {
background: #ffeccf; background: ${autoColor(theme, '#ffeccf')};
} }
`, `,
name: css` name: css`
label: name; label: name;
color: #000; color: ${autoColor(theme, '#000')};
cursor: pointer; cursor: pointer;
flex: 1 1 auto; flex: 1 1 auto;
outline: none; outline: none;
@ -213,7 +231,7 @@ const getStyles = createStyle(() => {
text-decoration: none; text-decoration: none;
} }
&:hover > .${endpointName} { &:hover > .${endpointName} {
color: #000; color: ${autoColor(theme, '#000')};
} }
`, `,
nameDetailExpanded: css` nameDetailExpanded: css`
@ -234,9 +252,9 @@ const getStyles = createStyle(() => {
`, `,
errorIcon: css` errorIcon: css`
label: errorIcon; label: errorIcon;
background: #db2828; background: ${autoColor(theme, '#db2828')};
border-radius: 6.5px; border-radius: 6.5px;
color: #fff; color: ${autoColor(theme, '#fff')};
font-size: 0.85em; font-size: 0.85em;
margin-right: 0.25rem; margin-right: 0.25rem;
padding: 1px; padding: 1px;
@ -265,6 +283,7 @@ const getStyles = createStyle(() => {
type SpanBarRowProps = { type SpanBarRowProps = {
className?: string; className?: string;
theme: Theme;
color: string; color: string;
columnDivision: number; columnDivision: number;
isChildrenExpanded: boolean; isChildrenExpanded: boolean;
@ -302,7 +321,8 @@ type SpanBarRowProps = {
* handlers to the onClick props. E.g. for now, the PureComponent is more * handlers to the onClick props. E.g. for now, the PureComponent is more
* performance than the stateless function. * performance than the stateless function.
*/ */
export default class SpanBarRow extends React.PureComponent<SpanBarRowProps> { export class UnthemedSpanBarRow extends React.PureComponent<SpanBarRowProps> {
static displayName = 'UnthemedSpanBarRow';
static defaultProps: Partial<SpanBarRowProps> = { static defaultProps: Partial<SpanBarRowProps> = {
className: '', className: '',
rpc: null, rpc: null,
@ -336,6 +356,7 @@ export default class SpanBarRow extends React.PureComponent<SpanBarRowProps> {
removeHoverIndentGuideId, removeHoverIndentGuideId,
clippingLeft, clippingLeft,
clippingRight, clippingRight,
theme,
} = this.props; } = this.props;
const { const {
duration, duration,
@ -347,7 +368,7 @@ export default class SpanBarRow extends React.PureComponent<SpanBarRowProps> {
const viewBounds = getViewedBounds(span.startTime, span.startTime + span.duration); const viewBounds = getViewedBounds(span.startTime, span.startTime + span.duration);
const viewStart = viewBounds.start; const viewStart = viewBounds.start;
const viewEnd = viewBounds.end; const viewEnd = viewBounds.end;
const styles = getStyles(); const styles = getStyles(theme);
const labelDetail = `${serviceName}::${operationName}`; const labelDetail = `${serviceName}::${operationName}`;
let longLabel; let longLabel;
@ -459,3 +480,5 @@ export default class SpanBarRow extends React.PureComponent<SpanBarRowProps> {
); );
} }
} }
export default withTheme(UnthemedSpanBarRow);

View File

@ -22,53 +22,61 @@ import * as markers from './AccordianKeyValues.markers';
import KeyValuesTable from './KeyValuesTable'; import KeyValuesTable from './KeyValuesTable';
import { TNil } from '../../types'; import { TNil } from '../../types';
import { KeyValuePair, Link } from '../../types/trace'; import { KeyValuePair, Link } from '../../types/trace';
import { createStyle } from '../../Theme'; import { autoColor, createStyle, Theme, useTheme } from '../../Theme';
import { uAlignIcon, uTxEllipsis } from '../../uberUtilityStyles'; import { uAlignIcon, uTxEllipsis } from '../../uberUtilityStyles';
export const getStyles = createStyle(() => { export const getStyles = createStyle((theme: Theme) => {
return { return {
header: css` header: css`
label: header;
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
padding: 0.25em 0.1em; padding: 0.25em 0.1em;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
&:hover { &:hover {
background: #e8e8e8; background: ${autoColor(theme, '#e8e8e8')};
} }
`, `,
headerEmpty: css` headerEmpty: css`
label: headerEmpty;
background: none; background: none;
cursor: initial; cursor: initial;
`, `,
headerHighContrast: css` headerHighContrast: css`
label: headerHighContrast;
&:hover { &:hover {
background: #ddd; background: ${autoColor(theme, '#ddd')};
} }
`, `,
emptyIcon: css` emptyIcon: css`
color: #aaa; label: emptyIcon;
color: ${autoColor(theme, '#aaa')};
`, `,
summary: css` summary: css`
label: summary;
display: inline; display: inline;
list-style: none; list-style: none;
padding: 0; padding: 0;
`, `,
summaryItem: css` summaryItem: css`
label: summaryItem;
display: inline; display: inline;
margin-left: 0.7em; margin-left: 0.7em;
padding-right: 0.5rem; padding-right: 0.5rem;
border-right: 1px solid #ddd; border-right: 1px solid ${autoColor(theme, '#ddd')};
&:last-child { &:last-child {
padding-right: 0; padding-right: 0;
border-right: none; border-right: none;
} }
`, `,
summaryLabel: css` summaryLabel: css`
color: #777; label: summaryLabel;
color: ${autoColor(theme, '#777')};
`, `,
summaryDelim: css` summaryDelim: css`
color: #bbb; label: summaryDelim;
color: ${autoColor(theme, '#bbb')};
padding: 0 0.2em; padding: 0 0.2em;
`, `,
}; };
@ -91,7 +99,7 @@ export function KeyValuesSummary(props: { data?: KeyValuePair[] }) {
if (!Array.isArray(data) || !data.length) { if (!Array.isArray(data) || !data.length) {
return null; return null;
} }
const styles = getStyles(); const styles = getStyles(useTheme());
return ( return (
<ul className={styles.summary}> <ul className={styles.summary}>
{data.map((item, i) => ( {data.map((item, i) => (
@ -113,7 +121,7 @@ KeyValuesSummary.defaultProps = {
export default function AccordianKeyValues(props: AccordianKeyValuesProps) { export default function AccordianKeyValues(props: AccordianKeyValuesProps) {
const { className, data, highContrast, interactive, isOpen, label, linksGetter, onToggle } = props; const { className, data, highContrast, interactive, isOpen, label, linksGetter, onToggle } = props;
const isEmpty = !Array.isArray(data) || !data.length; const isEmpty = !Array.isArray(data) || !data.length;
const styles = getStyles(); const styles = getStyles(useTheme());
const iconCls = cx(uAlignIcon, { [styles.emptyIcon]: isEmpty }); const iconCls = cx(uAlignIcon, { [styles.emptyIcon]: isEmpty });
let arrow: React.ReactNode | null = null; let arrow: React.ReactNode | null = null;
let headerProps: {} | null = null; let headerProps: {} | null = null;

View File

@ -22,32 +22,36 @@ import AccordianKeyValues from './AccordianKeyValues';
import { formatDuration } from '../utils'; import { formatDuration } from '../utils';
import { TNil } from '../../types'; import { TNil } from '../../types';
import { Log, KeyValuePair, Link } from '../../types/trace'; import { Log, KeyValuePair, Link } from '../../types/trace';
import { createStyle } from '../../Theme'; import { autoColor, createStyle, Theme, useTheme } from '../../Theme';
import { uAlignIcon, ubMb1 } from '../../uberUtilityStyles'; import { uAlignIcon, ubMb1 } from '../../uberUtilityStyles';
const getStyles = createStyle(() => { const getStyles = createStyle((theme: Theme) => {
return { return {
AccordianLogs: css` AccordianLogs: css`
border: 1px solid #d8d8d8; label: AccordianLogs;
border: 1px solid ${autoColor(theme, '#d8d8d8')};
position: relative; position: relative;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
`, `,
header: css` AccordianLogsHeader: css`
background: #e4e4e4; label: AccordianLogsHeader;
background: ${autoColor(theme, '#e4e4e4')};
color: inherit; color: inherit;
display: block; display: block;
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
&:hover { &:hover {
background: #dadada; background: ${autoColor(theme, '#dadada')};
} }
`, `,
content: css` AccordianLogsContent: css`
background: #f0f0f0; label: AccordianLogsContent;
border-top: 1px solid #d8d8d8; background: ${autoColor(theme, '#f0f0f0')};
border-top: 1px solid ${autoColor(theme, '#d8d8d8')};
padding: 0.5rem 0.5rem 0.25rem 0.5rem; padding: 0.5rem 0.5rem 0.25rem 0.5rem;
`, `,
footer: css` AccordianLogsFooter: css`
color: #999; label: AccordianLogsFooter;
color: ${autoColor(theme, '#999')};
`, `,
}; };
}); });
@ -78,14 +82,14 @@ export default function AccordianLogs(props: AccordianLogsProps) {
}; };
} }
const styles = getStyles(); const styles = getStyles(useTheme());
return ( return (
<div className={styles.AccordianLogs}> <div className={styles.AccordianLogs}>
<HeaderComponent className={styles.header} {...headerProps}> <HeaderComponent className={styles.AccordianLogsHeader} {...headerProps}>
{arrow} <strong>Logs</strong> ({logs.length}) {arrow} <strong>Logs</strong> ({logs.length})
</HeaderComponent> </HeaderComponent>
{isOpen && ( {isOpen && (
<div className={styles.content}> <div className={styles.AccordianLogsContent}>
{_sortBy(logs, 'timestamp').map((log, i) => ( {_sortBy(logs, 'timestamp').map((log, i) => (
<AccordianKeyValues <AccordianKeyValues
// `i` is necessary in the key because timestamps can repeat // `i` is necessary in the key because timestamps can repeat
@ -100,7 +104,9 @@ export default function AccordianLogs(props: AccordianLogsProps) {
onToggle={interactive && onItemToggle ? () => onItemToggle(log) : null} onToggle={interactive && onItemToggle ? () => onItemToggle(log) : null}
/> />
))} ))}
<small className={styles.footer}>Log timestamps are relative to the start time of the full trace.</small> <small className={styles.AccordianLogsFooter}>
Log timestamps are relative to the start time of the full trace.
</small>
</div> </div>
)} )}
</div> </div>

View File

@ -20,10 +20,10 @@ import IoIosArrowRight from 'react-icons/lib/io/ios-arrow-right';
import TextList from './TextList'; import TextList from './TextList';
import { TNil } from '../../types'; import { TNil } from '../../types';
import { getStyles as getAccordianKeyValuesStyles } from './AccordianKeyValues'; import { getStyles as getAccordianKeyValuesStyles } from './AccordianKeyValues';
import { createStyle } from '../../Theme'; import { autoColor, createStyle, Theme, useTheme } from '../../Theme';
import { uAlignIcon } from '../../uberUtilityStyles'; import { uAlignIcon } from '../../uberUtilityStyles';
const getStyles = createStyle(() => { const getStyles = createStyle((theme: Theme) => {
return { return {
header: css` header: css`
cursor: pointer; cursor: pointer;
@ -32,7 +32,7 @@ const getStyles = createStyle(() => {
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
&:hover { &:hover {
background: #e8e8e8; background: ${autoColor(theme, '#e8e8e8')};
} }
`, `,
}; };
@ -52,7 +52,7 @@ type AccordianTextProps = {
export default function AccordianText(props: AccordianTextProps) { export default function AccordianText(props: AccordianTextProps) {
const { className, data, headerClassName, interactive, isOpen, label, onToggle } = props; const { className, data, headerClassName, interactive, isOpen, label, onToggle } = props;
const isEmpty = !Array.isArray(data) || !data.length; const isEmpty = !Array.isArray(data) || !data.length;
const accordianKeyValuesStyles = getAccordianKeyValuesStyles(); const accordianKeyValuesStyles = getAccordianKeyValuesStyles(useTheme());
const iconCls = cx(uAlignIcon, { [accordianKeyValuesStyles.emptyIcon]: isEmpty }); const iconCls = cx(uAlignIcon, { [accordianKeyValuesStyles.emptyIcon]: isEmpty });
let arrow: React.ReactNode | null = null; let arrow: React.ReactNode | null = null;
let headerProps: {} | null = null; let headerProps: {} | null = null;
@ -64,7 +64,7 @@ export default function AccordianText(props: AccordianTextProps) {
role: 'switch', role: 'switch',
}; };
} }
const styles = getStyles(); const styles = getStyles(useTheme());
return ( return (
<div className={className || ''}> <div className={className || ''}>
<div className={cx(styles.header, headerClassName)} {...headerProps} data-test-id="AccordianText--header"> <div className={cx(styles.header, headerClassName)} {...headerProps} data-test-id="AccordianText--header">

View File

@ -19,7 +19,8 @@ import CopyIcon from '../../common/CopyIcon';
import KeyValuesTable, { LinkValue, getStyles } from './KeyValuesTable'; import KeyValuesTable, { LinkValue, getStyles } from './KeyValuesTable';
import { UIDropdown, UIIcon } from '../../uiElementsContext'; import { UIDropdown, UIIcon } from '../../uiElementsContext';
import {ubInlineBlock} from "../../uberUtilityStyles"; import { ubInlineBlock } from '../../uberUtilityStyles';
import { defaultTheme } from '../../Theme';
describe('LinkValue', () => { describe('LinkValue', () => {
const title = 'titleValue'; const title = 'titleValue';
@ -38,7 +39,7 @@ describe('LinkValue', () => {
}); });
it('renders correct Icon', () => { it('renders correct Icon', () => {
const styles = getStyles(); const styles = getStyles(defaultTheme);
expect(wrapper.find(UIIcon).hasClass(styles.linkIcon)).toBe(true); expect(wrapper.find(UIIcon).hasClass(styles.linkIcon)).toBe(true);
expect(wrapper.find(UIIcon).prop('type')).toBe('export'); expect(wrapper.find(UIIcon).prop('type')).toBe('export');
}); });

View File

@ -22,18 +22,18 @@ import CopyIcon from '../../common/CopyIcon';
import { TNil } from '../../types'; import { TNil } from '../../types';
import { KeyValuePair, Link } from '../../types/trace'; import { KeyValuePair, Link } from '../../types/trace';
import { UIDropdown, UIIcon, UIMenu, UIMenuItem } from '../../uiElementsContext'; import { UIDropdown, UIIcon, UIMenu, UIMenuItem } from '../../uiElementsContext';
import { createStyle } from '../../Theme'; import { autoColor, createStyle, Theme, useTheme } from '../../Theme';
import { ubInlineBlock, uWidth100 } from '../../uberUtilityStyles'; import { ubInlineBlock, uWidth100 } from '../../uberUtilityStyles';
export const getStyles = createStyle(() => { export const getStyles = createStyle((theme: Theme) => {
const copyIcon = css` const copyIcon = css`
label: copyIcon; label: copyIcon;
`; `;
return { return {
KeyValueTable: css` KeyValueTable: css`
label: KeyValueTable; label: KeyValueTable;
background: #fff; background: ${autoColor(theme, '#fff')};
border: 1px solid #ddd; border: 1px solid ${autoColor(theme, '#ddd')};
margin-bottom: 0.7em; margin-bottom: 0.7em;
max-height: 450px; max-height: 450px;
overflow: auto; overflow: auto;
@ -50,7 +50,7 @@ export const getStyles = createStyle(() => {
vertical-align: top; vertical-align: top;
} }
&:nth-child(2n) > td { &:nth-child(2n) > td {
background: #f5f5f5; background: ${autoColor(theme, '#f5f5f5')};
} }
&:not(:hover) .${copyIcon} { &:not(:hover) .${copyIcon} {
display: none; display: none;
@ -58,7 +58,7 @@ export const getStyles = createStyle(() => {
`, `,
keyColumn: css` keyColumn: css`
label: keyColumn; label: keyColumn;
color: #888; color: ${autoColor(theme, '#888')};
white-space: pre; white-space: pre;
width: 125px; width: 125px;
`, `,
@ -90,7 +90,7 @@ function parseIfComplexJson(value: any) {
} }
export const LinkValue = (props: { href: string; title?: string; children: React.ReactNode }) => { export const LinkValue = (props: { href: string; title?: string; children: React.ReactNode }) => {
const styles = getStyles(); const styles = getStyles(useTheme());
return ( return (
<a href={props.href} title={props.title} target="_blank" rel="noopener noreferrer"> <a href={props.href} title={props.title} target="_blank" rel="noopener noreferrer">
{props.children} <UIIcon className={styles.linkIcon} type="export" /> {props.children} <UIIcon className={styles.linkIcon} type="export" />
@ -120,7 +120,7 @@ type KeyValuesTableProps = {
export default function KeyValuesTable(props: KeyValuesTableProps) { export default function KeyValuesTable(props: KeyValuesTableProps) {
const { data, linksGetter } = props; const { data, linksGetter } = props;
const styles = getStyles(); const styles = getStyles(useTheme());
return ( return (
<div className={cx(styles.KeyValueTable)} data-test-id="KeyValueTable"> <div className={cx(styles.KeyValueTable)} data-test-id="KeyValueTable">
<table className={uWidth100}> <table className={uWidth100}>

View File

@ -27,53 +27,72 @@ import LabeledList from '../../common/LabeledList';
import { TNil } from '../../types'; import { TNil } from '../../types';
import { KeyValuePair, Link, Log, Span } from '../../types/trace'; import { KeyValuePair, Link, Log, Span } from '../../types/trace';
import AccordianReferences from './AccordianReferences'; import AccordianReferences from './AccordianReferences';
import { createStyle } from '../../Theme'; import { autoColor, createStyle, Theme, useTheme } from '../../Theme';
import { UIDivider } from '../../uiElementsContext'; import { UIDivider } from '../../uiElementsContext';
import { ubFlex, ubFlexAuto, ubItemsCenter, ubM0, ubMb1, ubMy1, ubTxRightAlign } from '../../uberUtilityStyles'; import { ubFlex, ubFlexAuto, ubItemsCenter, ubM0, ubMb1, ubMy1, ubTxRightAlign } from '../../uberUtilityStyles';
const getStyles = createStyle(() => { const getStyles = createStyle((theme: Theme) => {
return { return {
divider: css` divider: css`
background: #ddd; 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` debugInfo: css`
label: debugInfo;
display: block; display: block;
letter-spacing: 0.25px; letter-spacing: 0.25px;
margin: 0.5em 0 -0.75em; margin: 0.5em 0 -0.75em;
text-align: right; text-align: right;
`, `,
debugLabel: css` debugLabel: css`
label: debugLabel;
&::before { &::before {
color: #bbb; color: ${autoColor(theme, '#bbb')};
content: attr(data-label); content: attr(data-label);
} }
`, `,
debugValue: css` debugValue: css`
label: debugValue;
background-color: inherit; background-color: inherit;
border: none; border: none;
color: #888; color: ${autoColor(theme, '#888')};
cursor: pointer; cursor: pointer;
&:hover { &:hover {
color: #333; color: ${autoColor(theme, '#333')};
} }
`, `,
AccordianWarnings: css` AccordianWarnings: css`
background: #fafafa; label: AccordianWarnings;
border: 1px solid #e4e4e4; background: ${autoColor(theme, '#fafafa')};
border: 1px solid ${autoColor(theme, '#e4e4e4')};
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
`, `,
AccordianWarningsHeader: css` AccordianWarningsHeader: css`
background: #fff7e6; label: AccordianWarningsHeader;
background: ${autoColor(theme, '#fff7e6')};
padding: 0.25rem 0.5rem; padding: 0.25rem 0.5rem;
&:hover { &:hover {
background: #ffe7ba; background: ${autoColor(theme, '#ffe7ba')};
} }
`, `,
AccordianWarningsHeaderOpen: css` AccordianWarningsHeaderOpen: css`
border-bottom: 1px solid #e8e8e8; label: AccordianWarningsHeaderOpen;
border-bottom: 1px solid ${autoColor(theme, '#e8e8e8')};
`, `,
AccordianWarningsLabel: css` AccordianWarningsLabel: css`
color: #d36c08; label: AccordianWarningsLabel;
color: ${autoColor(theme, '#d36c08')};
`, `,
}; };
}); });
@ -126,7 +145,7 @@ export default function SpanDetail(props: SpanDetailProps) {
}, },
]; ];
const deepLinkCopyText = `${window.location.origin}${window.location.pathname}?uiFind=${spanID}`; const deepLinkCopyText = `${window.location.origin}${window.location.pathname}?uiFind=${spanID}`;
const styles = getStyles(); const styles = getStyles(useTheme());
return ( return (
<div> <div>
@ -134,7 +153,7 @@ export default function SpanDetail(props: SpanDetailProps) {
<h2 className={cx(ubFlexAuto, ubM0)}>{operationName}</h2> <h2 className={cx(ubFlexAuto, ubM0)}>{operationName}</h2>
<LabeledList className={ubTxRightAlign} dividerClassName={styles.divider} items={overviewItems} /> <LabeledList className={ubTxRightAlign} dividerClassName={styles.divider} items={overviewItems} />
</div> </div>
<UIDivider className={cx(styles.divider, ubMy1)} /> <UIDivider className={cx(styles.divider, styles.dividerVertical, ubMy1)} />
<div> <div>
<div> <div>
<AccordianKeyValues <AccordianKeyValues

View File

@ -48,7 +48,7 @@ describe('<SpanDetailRow>', () => {
props.logsToggle.mockReset(); props.logsToggle.mockReset();
props.processToggle.mockReset(); props.processToggle.mockReset();
props.tagsToggle.mockReset(); props.tagsToggle.mockReset();
wrapper = shallow(<SpanDetailRow {...props} />); wrapper = shallow(<SpanDetailRow {...props} />).dive().dive().dive();
}); });
it('renders without exploding', () => { it('renders without exploding', () => {

View File

@ -19,11 +19,11 @@ import SpanDetail from './SpanDetail';
import DetailState from './SpanDetail/DetailState'; import DetailState from './SpanDetail/DetailState';
import SpanTreeOffset from './SpanTreeOffset'; import SpanTreeOffset from './SpanTreeOffset';
import TimelineRow from './TimelineRow'; import TimelineRow from './TimelineRow';
import { createStyle } from '../Theme'; import { autoColor, createStyle, Theme, withTheme } from '../Theme';
import { Log, Span, KeyValuePair, Link } from '../types/trace'; import { Log, Span, KeyValuePair, Link } from '../types/trace';
const getStyles = createStyle(() => { const getStyles = createStyle((theme: Theme) => {
return { return {
expandedAccent: css` expandedAccent: css`
cursor: pointer; cursor: pointer;
@ -57,8 +57,8 @@ const getStyles = createStyle(() => {
} }
`, `,
infoWrapper: css` infoWrapper: css`
background: #f5f5f5; background: ${autoColor(theme, '#f5f5f5')};
border: 1px solid #d3d3d3; border: 1px solid ${autoColor(theme, '#d3d3d3')};
border-top: 3px solid; border-top: 3px solid;
padding: 0.75rem; padding: 0.75rem;
`, `,
@ -83,9 +83,10 @@ type SpanDetailRowProps = {
hoverIndentGuideIds: Set<string>; hoverIndentGuideIds: Set<string>;
addHoverIndentGuideId: (spanID: string) => void; addHoverIndentGuideId: (spanID: string) => void;
removeHoverIndentGuideId: (spanID: string) => void; removeHoverIndentGuideId: (spanID: string) => void;
theme: Theme;
}; };
export default class SpanDetailRow extends React.PureComponent<SpanDetailRowProps> { export class UnthemedSpanDetailRow extends React.PureComponent<SpanDetailRowProps> {
_detailToggle = () => { _detailToggle = () => {
this.props.onDetailToggled(this.props.span.spanID); this.props.onDetailToggled(this.props.span.spanID);
}; };
@ -112,8 +113,9 @@ export default class SpanDetailRow extends React.PureComponent<SpanDetailRowProp
hoverIndentGuideIds, hoverIndentGuideIds,
addHoverIndentGuideId, addHoverIndentGuideId,
removeHoverIndentGuideId, removeHoverIndentGuideId,
theme,
} = this.props; } = this.props;
const styles = getStyles(); const styles = getStyles(theme);
return ( return (
<TimelineRow> <TimelineRow>
<TimelineRow.Cell width={columnDivision}> <TimelineRow.Cell width={columnDivision}>
@ -156,3 +158,5 @@ export default class SpanDetailRow extends React.PureComponent<SpanDetailRowProp
); );
} }
} }
export default withTheme(UnthemedSpanDetailRow);

View File

@ -19,6 +19,7 @@ import IoIosArrowDown from 'react-icons/lib/io/ios-arrow-down';
import SpanTreeOffset, { getStyles } from './SpanTreeOffset'; import SpanTreeOffset, { getStyles } from './SpanTreeOffset';
import spanAncestorIdsSpy from '../utils/span-ancestor-ids'; import spanAncestorIdsSpy from '../utils/span-ancestor-ids';
import {defaultTheme} from "../Theme";
jest.mock('../utils/span-ancestor-ids'); jest.mock('../utils/span-ancestor-ids');
@ -42,13 +43,13 @@ describe('SpanTreeOffset', () => {
spanID: ownSpanID, spanID: ownSpanID,
}, },
}; };
wrapper = shallow(<SpanTreeOffset {...props} />); wrapper = shallow(<SpanTreeOffset {...props} />).dive().dive().dive();
}); });
describe('.SpanTreeOffset--indentGuide', () => { describe('.SpanTreeOffset--indentGuide', () => {
it('renders only one .SpanTreeOffset--indentGuide for entire trace if span has no ancestors', () => { it('renders only one .SpanTreeOffset--indentGuide for entire trace if span has no ancestors', () => {
spanAncestorIdsSpy.mockReturnValue([]); spanAncestorIdsSpy.mockReturnValue([]);
wrapper = shallow(<SpanTreeOffset {...props} />); wrapper = shallow(<SpanTreeOffset {...props} />).dive().dive().dive();
const indentGuides = wrapper.find('[data-test-id="SpanTreeOffset--indentGuide"]'); const indentGuides = wrapper.find('[data-test-id="SpanTreeOffset--indentGuide"]');
expect(indentGuides.length).toBe(1); expect(indentGuides.length).toBe(1);
expect(indentGuides.prop('data-ancestor-id')).toBe(specialRootID); expect(indentGuides.prop('data-ancestor-id')).toBe(specialRootID);
@ -64,8 +65,8 @@ describe('SpanTreeOffset', () => {
it('adds .is-active to correct indentGuide', () => { it('adds .is-active to correct indentGuide', () => {
props.hoverIndentGuideIds = new Set([parentSpanID]); props.hoverIndentGuideIds = new Set([parentSpanID]);
wrapper = shallow(<SpanTreeOffset {...props} />); wrapper = shallow(<SpanTreeOffset {...props} />).dive().dive().dive();
const styles = getStyles(); const styles = getStyles(defaultTheme);
const activeIndentGuide = wrapper.find(`.${styles.indentGuideActive}`); const activeIndentGuide = wrapper.find(`.${styles.indentGuideActive}`);
expect(activeIndentGuide.length).toBe(1); expect(activeIndentGuide.length).toBe(1);
expect(activeIndentGuide.prop('data-ancestor-id')).toBe(parentSpanID); expect(activeIndentGuide.prop('data-ancestor-id')).toBe(parentSpanID);

View File

@ -22,19 +22,19 @@ import cx from 'classnames';
import { Span } from '../types/trace'; import { Span } from '../types/trace';
import spanAncestorIds from '../utils/span-ancestor-ids'; import spanAncestorIds from '../utils/span-ancestor-ids';
import { createStyle } from '../Theme'; import { autoColor, createStyle, Theme, withTheme } from '../Theme';
export const getStyles = createStyle(() => { export const getStyles = createStyle((theme: Theme) => {
return { return {
SpanTreeOffset: css` SpanTreeOffset: css`
label: SpanTreeOffset; label: SpanTreeOffset;
color: #000; color: ${autoColor(theme, '#000')};
position: relative; position: relative;
`, `,
SpanTreeOffsetParent: css` SpanTreeOffsetParent: css`
label: SpanTreeOffsetParent; label: SpanTreeOffsetParent;
&:hover { &:hover {
background-color: #e8e8e8; background-color: ${autoColor(theme, '#e8e8e8')};
cursor: pointer; cursor: pointer;
} }
`, `,
@ -48,7 +48,7 @@ export const getStyles = createStyle(() => {
&::before { &::before {
content: ''; content: '';
padding-left: 1px; padding-left: 1px;
background-color: lightgrey; background-color: ${autoColor(theme, 'lightgrey')};
} }
`, `,
indentGuideActive: css` indentGuideActive: css`
@ -58,7 +58,7 @@ export const getStyles = createStyle(() => {
&::before { &::before {
content: ''; content: '';
padding-left: 3px; padding-left: 3px;
background-color: darkgrey; background-color: ${autoColor(theme, 'darkgrey')};
} }
`, `,
iconWrapper: css` iconWrapper: css`
@ -78,9 +78,12 @@ type TProps = {
hoverIndentGuideIds: Set<string>; hoverIndentGuideIds: Set<string>;
addHoverIndentGuideId: (spanID: string) => void; addHoverIndentGuideId: (spanID: string) => void;
removeHoverIndentGuideId: (spanID: string) => void; removeHoverIndentGuideId: (spanID: string) => void;
theme: Theme;
}; };
export default class SpanTreeOffset extends React.PureComponent<TProps> { export class UnthemedSpanTreeOffset extends React.PureComponent<TProps> {
static displayName = 'UnthemedSpanTreeOffset';
ancestorIds: string[]; ancestorIds: string[];
static defaultProps = { static defaultProps = {
@ -134,11 +137,11 @@ export default class SpanTreeOffset extends React.PureComponent<TProps> {
}; };
render() { render() {
const { childrenVisible, onClick, showChildrenIcon, span } = this.props; const { childrenVisible, onClick, showChildrenIcon, span, theme } = this.props;
const { hasChildren, spanID } = span; const { hasChildren, spanID } = span;
const wrapperProps = hasChildren ? { onClick, role: 'switch', 'aria-checked': childrenVisible } : null; const wrapperProps = hasChildren ? { onClick, role: 'switch', 'aria-checked': childrenVisible } : null;
const icon = showChildrenIcon && hasChildren && (childrenVisible ? <IoIosArrowDown /> : <IoChevronRight />); const icon = showChildrenIcon && hasChildren && (childrenVisible ? <IoIosArrowDown /> : <IoChevronRight />);
const styles = getStyles(); const styles = getStyles(theme);
return ( return (
<span className={cx(styles.SpanTreeOffset, { [styles.SpanTreeOffsetParent]: hasChildren })} {...wrapperProps}> <span className={cx(styles.SpanTreeOffset, { [styles.SpanTreeOffsetParent]: hasChildren })} {...wrapperProps}>
{this.ancestorIds.map(ancestorId => ( {this.ancestorIds.map(ancestorId => (
@ -167,3 +170,5 @@ export default class SpanTreeOffset extends React.PureComponent<TProps> {
); );
} }
} }
export default withTheme(UnthemedSpanTreeOffset);

View File

@ -18,27 +18,31 @@ import cx from 'classnames';
import { formatDuration } from './utils'; import { formatDuration } from './utils';
import { TNil } from '../types'; import { TNil } from '../types';
import { createStyle } from '../Theme'; import { autoColor, createStyle, Theme, useTheme } from '../Theme';
const getStyles = createStyle(() => { const getStyles = createStyle((theme: Theme) => {
return { return {
Ticks: css` Ticks: css`
label: Ticks;
pointer-events: none; pointer-events: none;
`, `,
tick: css` TicksTick: css`
label: TicksTick;
position: absolute; position: absolute;
height: 100%; height: 100%;
width: 1px; width: 1px;
background: #d8d8d8; background: ${autoColor(theme, '#d8d8d8')};
&:last-child { &:last-child {
width: 0; width: 0;
} }
`, `,
tickLabel: css` TicksTickLabel: css`
label: TicksTickLabel;
left: 0.25rem; left: 0.25rem;
position: absolute; position: absolute;
`, `,
tickLabelEndAnchor: css` TicksTickLabelEndAnchor: css`
label: TicksTickLabelEndAnchor;
left: initial; left: initial;
right: 0.25rem; right: 0.25rem;
`, `,
@ -64,20 +68,22 @@ export default function Ticks(props: TicksProps) {
labels.push(formatDuration(durationAtTick)); labels.push(formatDuration(durationAtTick));
} }
} }
const styles = getStyles(); const styles = getStyles(useTheme());
const ticks: React.ReactNode[] = []; const ticks: React.ReactNode[] = [];
for (let i = 0; i < numTicks; i++) { for (let i = 0; i < numTicks; i++) {
const portion = i / (numTicks - 1); const portion = i / (numTicks - 1);
ticks.push( ticks.push(
<div <div
key={portion} key={portion}
className={styles.tick} className={styles.TicksTick}
style={{ style={{
left: `${portion * 100}%`, left: `${portion * 100}%`,
}} }}
> >
{labels && ( {labels && (
<span className={cx(styles.tickLabel, { [styles.tickLabelEndAnchor]: portion >= 1 })}>{labels[i]}</span> <span className={cx(styles.TicksTickLabel, { [styles.TicksTickLabelEndAnchor]: portion >= 1 })}>
{labels[i]}
</span>
)} )}
</div> </div>
); );

View File

@ -58,7 +58,7 @@ describe('<TimelineHeaderRow>', () => {
}); });
it('renders the title', () => { it('renders the title', () => {
expect(wrapper.find('h3').text()).toMatch(/Service.*?Operation/); expect(wrapper.find('h4').text()).toMatch(/Service.*?Operation/);
}); });
it('renders the TimelineViewingLayer', () => { it('renders the TimelineViewingLayer', () => {
@ -88,12 +88,7 @@ describe('<TimelineHeaderRow>', () => {
it('renders the TimelineColumnResizer', () => { it('renders the TimelineColumnResizer', () => {
const elm = ( const elm = (
<TimelineColumnResizer <TimelineColumnResizer position={nameColumnWidth} onChange={props.onColummWidthChange} min={0.2} max={0.85} />
position={nameColumnWidth}
onChange={props.onColummWidthChange}
min={0.2}
max={0.85}
/>
); );
expect(wrapper.containsMatchingElement(elm)).toBe(true); expect(wrapper.containsMatchingElement(elm)).toBe(true);
}); });

View File

@ -22,27 +22,33 @@ import TimelineViewingLayer from './TimelineViewingLayer';
import Ticks from '../Ticks'; import Ticks from '../Ticks';
import TimelineRow from '../TimelineRow'; import TimelineRow from '../TimelineRow';
import { TUpdateViewRangeTimeFunction, ViewRangeTime, ViewRangeTimeUpdate } from '../types'; import { TUpdateViewRangeTimeFunction, ViewRangeTime, ViewRangeTimeUpdate } from '../types';
import { createStyle } from '../../Theme'; import { autoColor, createStyle, Theme, useTheme } from '../../Theme';
import { ubFlex, ubPx2 } from '../../uberUtilityStyles'; import { ubFlex, ubPx2 } from '../../uberUtilityStyles';
const getStyles = createStyle(() => { const getStyles = createStyle((theme: Theme) => {
return { return {
TimelineHeaderRow: css` TimelineHeaderRow: css`
background: #ececec; label: TimelineHeaderRow;
border-bottom: 1px solid #ccc; background: ${autoColor(theme, '#ececec')};
border-bottom: 1px solid ${autoColor(theme, '#ccc')};
height: 38px; height: 38px;
line-height: 38px; line-height: 38px;
width: 100%; width: 100%;
z-index: 4; z-index: 4;
position: relative; position: relative;
`, `,
title: css` TimelineHeaderRowTitle: css`
label: TimelineHeaderRowTitle;
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
margin: 0; margin: 0;
text-overflow: ellipsis; text-overflow: ellipsis;
white-space: nowrap; white-space: nowrap;
`, `,
TimelineHeaderWrapper: css`
label: TimelineHeaderWrapper;
align-items: center;
`,
}; };
}); });
@ -77,11 +83,11 @@ export default function TimelineHeaderRow(props: TimelineHeaderRowProps) {
columnResizeHandleHeight, columnResizeHandleHeight,
} = props; } = props;
const [viewStart, viewEnd] = viewRangeTime.current; const [viewStart, viewEnd] = viewRangeTime.current;
const styles = getStyles(); const styles = getStyles(useTheme());
return ( return (
<TimelineRow className={styles.TimelineHeaderRow} data-test-id="TimelineHeaderRow"> <TimelineRow className={styles.TimelineHeaderRow} data-test-id="TimelineHeaderRow">
<TimelineRow.Cell className={cx(ubFlex, ubPx2)} width={nameColumnWidth}> <TimelineRow.Cell className={cx(ubFlex, ubPx2, styles.TimelineHeaderWrapper)} width={nameColumnWidth}>
<h3 className={styles.TimelineHeaderRow}>Service &amp; Operation</h3> <h4 className={styles.TimelineHeaderRowTitle}>Service &amp; Operation</h4>
<TimelineCollapser <TimelineCollapser
onCollapseAll={onCollapseAll} onCollapseAll={onCollapseAll}
onExpandAll={onExpandAll} onExpandAll={onExpandAll}

View File

@ -24,6 +24,7 @@ import { createStyle } from '../../Theme';
export const getStyles = createStyle(() => { export const getStyles = createStyle(() => {
return { return {
TimelineViewingLayer: css` TimelineViewingLayer: css`
label: TimelineViewingLayer;
bottom: 0; bottom: 0;
cursor: vertical-text; cursor: vertical-text;
left: 0; left: 0;
@ -31,7 +32,8 @@ export const getStyles = createStyle(() => {
right: 0; right: 0;
top: 0; top: 0;
`, `,
cursorGuide: css` TimelineViewingLayerCursorGuide: css`
label: TimelineViewingLayerCursorGuide;
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
@ -39,26 +41,32 @@ export const getStyles = createStyle(() => {
width: 1px; width: 1px;
background-color: red; background-color: red;
`, `,
dragged: css` TimelineViewingLayerDragged: css`
label: TimelineViewingLayerDragged;
position: absolute; position: absolute;
top: 0; top: 0;
bottom: 0; bottom: 0;
`, `,
draggedDraggingLeft: css` TimelineViewingLayerDraggedDraggingLeft: css`
label: TimelineViewingLayerDraggedDraggingLeft;
border-left: 1px solid; border-left: 1px solid;
`, `,
draggedDraggingRight: css` TimelineViewingLayerDraggedDraggingRight: css`
label: TimelineViewingLayerDraggedDraggingRight;
border-right: 1px solid; border-right: 1px solid;
`, `,
draggedShiftDrag: css` TimelineViewingLayerDraggedShiftDrag: css`
label: TimelineViewingLayerDraggedShiftDrag;
background-color: rgba(68, 68, 255, 0.2); background-color: rgba(68, 68, 255, 0.2);
border-color: #44f; border-color: #44f;
`, `,
draggedReframeDrag: css` TimelineViewingLayerDraggedReframeDrag: css`
label: TimelineViewingLayerDraggedReframeDrag;
background-color: rgba(255, 68, 68, 0.2); background-color: rgba(255, 68, 68, 0.2);
border-color: #f44; border-color: #f44;
`, `,
fullOverlay: css` TimelineViewingLayerFullOverlay: css`
label: TimelineViewingLayerFullOverlay;
bottom: 0; bottom: 0;
cursor: col-resize; cursor: col-resize;
left: 0; left: 0;
@ -151,13 +159,13 @@ function getMarkers(viewStart: number, viewEnd: number, from: number, to: number
const { isDraggingLeft, left, width } = layout; const { isDraggingLeft, left, width } = layout;
const styles = getStyles(); const styles = getStyles();
const cls = cx({ const cls = cx({
[styles.draggedDraggingRight]: !isDraggingLeft, [styles.TimelineViewingLayerDraggedDraggingRight]: !isDraggingLeft,
[styles.draggedReframeDrag]: !isShift, [styles.TimelineViewingLayerDraggedReframeDrag]: !isShift,
[styles.draggedShiftDrag]: isShift, [styles.TimelineViewingLayerDraggedShiftDrag]: isShift,
}); });
return ( return (
<div <div
className={cx(styles.dragged, styles.draggedDraggingLeft, cls)} className={cx(styles.TimelineViewingLayerDragged, styles.TimelineViewingLayerDraggedDraggingLeft, cls)}
style={{ left, width }} style={{ left, width }}
data-test-id="Dragged" data-test-id="Dragged"
/> />
@ -260,7 +268,7 @@ export default class TimelineViewingLayer extends React.PureComponent<TimelineVi
> >
{cusrorPosition != null && ( {cusrorPosition != null && (
<div <div
className={styles.cursorGuide} className={styles.TimelineViewingLayerCursorGuide}
style={{ left: cusrorPosition }} style={{ left: cusrorPosition }}
data-test-id="TimelineViewingLayer--cursorGuide" data-test-id="TimelineViewingLayer--cursorGuide"
/> />

View File

@ -21,6 +21,7 @@ import SpanDetailRow from './SpanDetailRow';
import VirtualizedTraceView, { DEFAULT_HEIGHTS } from './VirtualizedTraceView'; import VirtualizedTraceView, { DEFAULT_HEIGHTS } from './VirtualizedTraceView';
import traceGenerator from '../demo/trace-generators'; import traceGenerator from '../demo/trace-generators';
import transformTraceData from '../model/transform-trace-data'; import transformTraceData from '../model/transform-trace-data';
import SpanTreeOffset from './SpanTreeOffset';
jest.mock('./SpanTreeOffset'); jest.mock('./SpanTreeOffset');
@ -82,12 +83,16 @@ describe('<VirtualizedTraceViewImpl>', () => {
} }
beforeEach(() => { beforeEach(() => {
SpanTreeOffset.mockReturnValue(<div />);
Object.keys(props).forEach(key => { Object.keys(props).forEach(key => {
if (typeof props[key] === 'function') { if (typeof props[key] === 'function') {
props[key].mockReset(); props[key].mockReset();
} }
}); });
wrapper = shallow(<VirtualizedTraceView {...props} />); wrapper = shallow(<VirtualizedTraceView {...props} />)
.dive()
.dive()
.dive();
instance = wrapper.instance(); instance = wrapper.instance();
}); });
@ -366,9 +371,7 @@ describe('<VirtualizedTraceViewImpl>', () => {
describe('shouldComponentUpdate', () => { describe('shouldComponentUpdate', () => {
it('returns true if props.shouldScrollToFirstUiFindMatch changes to true', () => { it('returns true if props.shouldScrollToFirstUiFindMatch changes to true', () => {
expect(wrapper.instance().shouldComponentUpdate(propsWithTrueShouldScrollToFirstUiFindMatch)).toBe( expect(wrapper.instance().shouldComponentUpdate(propsWithTrueShouldScrollToFirstUiFindMatch)).toBe(true);
true
);
}); });
it('returns true if props.shouldScrollToFirstUiFindMatch changes to false and another props change', () => { it('returns true if props.shouldScrollToFirstUiFindMatch changes to false and another props change', () => {

View File

@ -27,12 +27,12 @@ import {
ViewedBoundsFunctionType, ViewedBoundsFunctionType,
} from './utils'; } from './utils';
import { Accessors } from '../ScrollManager'; import { Accessors } from '../ScrollManager';
import colorGenerator from '../utils/color-generator'; import { getColorByKey } from '../utils/color-generator';
import { TNil } from '../types'; import { TNil } from '../types';
import { Log, Span, Trace, KeyValuePair, Link } from '../types/trace'; import { Log, Span, Trace, KeyValuePair, Link } from '../types/trace';
import TTraceTimeline from '../types/TTraceTimeline'; import TTraceTimeline from '../types/TTraceTimeline';
import { createStyle } from '../Theme'; import { createStyle, Theme, withTheme } from '../Theme';
type TExtractUiFindFromStateReturn = { type TExtractUiFindFromStateReturn = {
uiFind: string | undefined; uiFind: string | undefined;
@ -63,8 +63,6 @@ type TVirtualizedTraceViewOwnProps = {
trace: Trace; trace: Trace;
focusSpan: (uiFind: string) => void; focusSpan: (uiFind: string) => void;
linksGetter: (span: Span, items: KeyValuePair[], itemIndex: number) => Link[]; linksGetter: (span: Span, items: KeyValuePair[], itemIndex: number) => Link[];
// was from redux
childrenToggle: (spanID: string) => void; childrenToggle: (spanID: string) => void;
clearShouldScrollToFirstUiFindMatch: () => void; clearShouldScrollToFirstUiFindMatch: () => void;
detailLogItemToggle: (spanID: string, log: Log) => void; detailLogItemToggle: (spanID: string, log: Log) => void;
@ -79,6 +77,7 @@ type TVirtualizedTraceViewOwnProps = {
hoverIndentGuideIds: Set<string>; hoverIndentGuideIds: Set<string>;
addHoverIndentGuideId: (spanID: string) => void; addHoverIndentGuideId: (spanID: string) => void;
removeHoverIndentGuideId: (spanID: string) => void; removeHoverIndentGuideId: (spanID: string) => void;
theme: Theme;
}; };
type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TExtractUiFindFromStateReturn & TTraceTimeline; type VirtualizedTraceViewProps = TVirtualizedTraceViewOwnProps & TExtractUiFindFromStateReturn & TTraceTimeline;
@ -144,7 +143,7 @@ function getClipping(currentViewRange: [number, number]) {
} }
// export from tests // export from tests
export default class VirtualizedTraceView extends React.Component<VirtualizedTraceViewProps> { export class UnthemedVirtualizedTraceView extends React.Component<VirtualizedTraceViewProps> {
clipping: { left: boolean; right: boolean }; clipping: { left: boolean; right: boolean };
listView: ListView | TNil; listView: ListView | TNil;
rowStates: RowState[]; rowStates: RowState[];
@ -329,12 +328,13 @@ export default class VirtualizedTraceView extends React.Component<VirtualizedTra
hoverIndentGuideIds, hoverIndentGuideIds,
addHoverIndentGuideId, addHoverIndentGuideId,
removeHoverIndentGuideId, removeHoverIndentGuideId,
theme,
} = this.props; } = this.props;
// to avert flow error // to avert flow error
if (!trace) { if (!trace) {
return null; return null;
} }
const color = colorGenerator.getColorByKey(serviceName); const color = getColorByKey(serviceName, theme);
const isCollapsed = childrenHiddenIDs.has(spanID); const isCollapsed = childrenHiddenIDs.has(spanID);
const isDetailExpanded = detailStates.has(spanID); const isDetailExpanded = detailStates.has(spanID);
const isMatchingFilter = findMatchesIDs ? findMatchesIDs.has(spanID) : false; const isMatchingFilter = findMatchesIDs ? findMatchesIDs.has(spanID) : false;
@ -347,7 +347,7 @@ export default class VirtualizedTraceView extends React.Component<VirtualizedTra
if (rpcSpan) { if (rpcSpan) {
const rpcViewBounds = this.getViewedBounds(rpcSpan.startTime, rpcSpan.startTime + rpcSpan.duration); const rpcViewBounds = this.getViewedBounds(rpcSpan.startTime, rpcSpan.startTime + rpcSpan.duration);
rpc = { rpc = {
color: colorGenerator.getColorByKey(rpcSpan.process.serviceName), color: getColorByKey(rpcSpan.process.serviceName, theme),
operationName: rpcSpan.operationName, operationName: rpcSpan.operationName,
serviceName: rpcSpan.process.serviceName, serviceName: rpcSpan.process.serviceName,
viewEnd: rpcViewBounds.end, viewEnd: rpcViewBounds.end,
@ -402,12 +402,13 @@ export default class VirtualizedTraceView extends React.Component<VirtualizedTra
addHoverIndentGuideId, addHoverIndentGuideId,
removeHoverIndentGuideId, removeHoverIndentGuideId,
linksGetter, linksGetter,
theme,
} = this.props; } = this.props;
const detailState = detailStates.get(spanID); const detailState = detailStates.get(spanID);
if (!trace || !detailState) { if (!trace || !detailState) {
return null; return null;
} }
const color = colorGenerator.getColorByKey(serviceName); const color = getColorByKey(serviceName, theme);
const styles = getStyles(); const styles = getStyles();
return ( return (
<div className={styles.row} key={key} style={{ ...style, zIndex: 1 }} {...attrs}> <div className={styles.row} key={key} style={{ ...style, zIndex: 1 }} {...attrs}>
@ -454,3 +455,5 @@ export default class VirtualizedTraceView extends React.Component<VirtualizedTra
); );
} }
} }
export default withTheme(UnthemedVirtualizedTraceView);

View File

@ -46,22 +46,10 @@ describe('<TraceTimelineViewer>', () => {
search: null, search: null,
}, },
}; };
const options = {
context: {
store: {
getState() {
return { traceTimeline: { spanNameColumnWidth: 0.25 } };
},
subscribe() {},
dispatch() {},
},
},
};
let wrapper; let wrapper;
beforeEach(() => { beforeEach(() => {
wrapper = shallow(<TraceTimelineViewer {...props} />, options); wrapper = shallow(<TraceTimelineViewer {...props} />).dive().dive().dive();
}); });
it('it does not explode', () => { it('it does not explode', () => {

View File

@ -23,18 +23,18 @@ import { TUpdateViewRangeTimeFunction, ViewRange, ViewRangeTimeUpdate } from './
import { TNil } from '../types'; import { TNil } from '../types';
import { Span, Trace, Log, KeyValuePair, Link } from '../types/trace'; import { Span, Trace, Log, KeyValuePair, Link } from '../types/trace';
import TTraceTimeline from '../types/TTraceTimeline'; import TTraceTimeline from '../types/TTraceTimeline';
import { createStyle } from '../Theme'; import { autoColor, createStyle, Theme, withTheme } from '../Theme';
import ExternalLinkContext from '../url/externalLinkContext'; import ExternalLinkContext from '../url/externalLinkContext';
type TExtractUiFindFromStateReturn = { type TExtractUiFindFromStateReturn = {
uiFind: string | undefined; uiFind: string | undefined;
}; };
const getStyles = createStyle(() => { const getStyles = createStyle((theme: Theme) => {
return { return {
TraceTimelineViewer: css` TraceTimelineViewer: css`
label: TraceTimelineViewer; label: TraceTimelineViewer;
border-bottom: 1px solid #bbb; border-bottom: 1px solid ${autoColor(theme, '#bbb')};
& .json-markup { & .json-markup {
line-height: 17px; line-height: 17px;
@ -48,19 +48,19 @@ const getStyles = createStyle(() => {
} }
& .json-markup-bool { & .json-markup-bool {
color: firebrick; color: ${autoColor(theme, 'firebrick')};
} }
& .json-markup-string { & .json-markup-string {
color: teal; color: ${autoColor(theme, 'teal')};
} }
& .json-markup-null { & .json-markup-null {
color: teal; color: ${autoColor(theme, 'teal')};
} }
& .json-markup-number { & .json-markup-number {
color: blue; color: ${autoColor(theme, 'blue', 'black')};
} }
`, `,
}; };
@ -97,6 +97,7 @@ type TProps = TExtractUiFindFromStateReturn & {
addHoverIndentGuideId: (spanID: string) => void; addHoverIndentGuideId: (spanID: string) => void;
removeHoverIndentGuideId: (spanID: string) => void; removeHoverIndentGuideId: (spanID: string) => void;
linksGetter: (span: Span, items: KeyValuePair[], itemIndex: number) => Link[]; linksGetter: (span: Span, items: KeyValuePair[], itemIndex: number) => Link[];
theme: Theme;
}; };
type State = { type State = {
@ -112,7 +113,7 @@ const NUM_TICKS = 5;
* re-render the ListView every time the cursor is moved on the trace minimap * re-render the ListView every time the cursor is moved on the trace minimap
* or `TimelineHeaderRow`. * or `TimelineHeaderRow`.
*/ */
export default class TraceTimelineViewer extends React.PureComponent<TProps, State> { export class UnthemedTraceTimelineViewer extends React.PureComponent<TProps, State> {
constructor(props: TProps) { constructor(props: TProps) {
super(props); super(props);
this.state = { height: 0 }; this.state = { height: 0 };
@ -151,10 +152,11 @@ export default class TraceTimelineViewer extends React.PureComponent<TProps, Sta
viewRange, viewRange,
createLinkToExternalSpan, createLinkToExternalSpan,
traceTimeline, traceTimeline,
theme,
...rest ...rest
} = this.props; } = this.props;
const { trace } = rest; const { trace } = rest;
const styles = getStyles(); const styles = getStyles(theme);
return ( return (
<ExternalLinkContext.Provider value={createLinkToExternalSpan}> <ExternalLinkContext.Provider value={createLinkToExternalSpan}>
@ -187,3 +189,5 @@ export default class TraceTimelineViewer extends React.PureComponent<TProps, Sta
); );
} }
} }
export default withTheme(UnthemedTraceTimelineViewer);

View File

@ -16,10 +16,10 @@ import * as React from 'react';
import { css } from 'emotion'; import { css } from 'emotion';
import cx from 'classnames'; import cx from 'classnames';
import { createStyle } from '../Theme'; import { createStyle, isLight, Theme, useTheme } from '../Theme';
import { UIDivider } from '../uiElementsContext'; import { UIDivider } from '../uiElementsContext';
const getStyles = createStyle(() => { const getStyles = createStyle((theme: Theme) => {
return { return {
LabeledList: css` LabeledList: css`
label: LabeledList; label: LabeledList;
@ -33,7 +33,7 @@ const getStyles = createStyle(() => {
`, `,
LabeledListLabel: css` LabeledListLabel: css`
label: LabeledListLabel; label: LabeledListLabel;
color: #999; color: ${isLight(theme) ? '#999' : '#666'};
margin-right: 0.25rem; margin-right: 0.25rem;
`, `,
}; };
@ -47,7 +47,7 @@ type LabeledListProps = {
export default function LabeledList(props: LabeledListProps) { export default function LabeledList(props: LabeledListProps) {
const { className, dividerClassName, items } = props; const { className, dividerClassName, items } = props;
const styles = getStyles(); const styles = getStyles(useTheme());
return ( return (
<ul className={cx(styles.LabeledList, className)}> <ul className={cx(styles.LabeledList, className)}>
{items.map(({ key, label, value }, i) => { {items.map(({ key, label, value }, i) => {

View File

@ -7,6 +7,7 @@ export * from './TraceTimelineViewer/types';
export { default as DetailState } from './TraceTimelineViewer/SpanDetail/DetailState'; export { default as DetailState } from './TraceTimelineViewer/SpanDetail/DetailState';
export { default as transformTraceData } from './model/transform-trace-data'; export { default as transformTraceData } from './model/transform-trace-data';
export { default as filterSpans } from './utils/filter-spans'; export { default as filterSpans } from './utils/filter-spans';
export * from './Theme';
import { onlyUpdateForKeys } from 'recompose'; import { onlyUpdateForKeys } from 'recompose';

View File

@ -12,26 +12,27 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import colorGenerator from './color-generator'; import { getColorByKey, clear } from './color-generator';
import { defaultTheme } from '../Theme';
it('gives the same color for the same key', () => { it('gives the same color for the same key', () => {
colorGenerator.clear(); clear();
const colorOne = colorGenerator.getColorByKey('serviceA'); const colorOne = getColorByKey('serviceA', defaultTheme);
const colorTwo = colorGenerator.getColorByKey('serviceA'); const colorTwo = getColorByKey('serviceA', defaultTheme);
expect(colorOne).toBe(colorTwo); expect(colorOne).toBe(colorTwo);
}); });
it('gives different colors for each for each key', () => { it('gives different colors for each for each key', () => {
colorGenerator.clear(); clear();
const colorOne = colorGenerator.getColorByKey('serviceA'); const colorOne = getColorByKey('serviceA', defaultTheme);
const colorTwo = colorGenerator.getColorByKey('serviceB'); const colorTwo = getColorByKey('serviceB', defaultTheme);
expect(colorOne).not.toBe(colorTwo); expect(colorOne).not.toBe(colorTwo);
}); });
it('should clear cache', () => { it('should clear cache', () => {
colorGenerator.clear(); clear();
const colorOne = colorGenerator.getColorByKey('serviceA'); const colorOne = getColorByKey('serviceA', defaultTheme);
colorGenerator.clear(); clear();
const colorTwo = colorGenerator.getColorByKey('serviceB'); const colorTwo = getColorByKey('serviceB', defaultTheme);
expect(colorOne).toBe(colorTwo); expect(colorOne).toBe(colorTwo);
}); });

View File

@ -12,6 +12,8 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import { isLight, Theme } from '../Theme';
const COLORS_HEX = [ const COLORS_HEX = [
'#17B8BE', '#17B8BE',
'#F8DCA1', '#F8DCA1',
@ -35,6 +37,27 @@ const COLORS_HEX = [
'#776E57', '#776E57',
]; ];
const COLORS_HEX_DARK = [
'#17B8BE',
'#F8DCA1',
'#B7885E',
'#FFCB99',
'#F89570',
'#829AE3',
'#E79FD5',
'#1E96BE',
'#89DAC1',
'#B3AD9E',
'#12939A',
'#DDB27C',
'#88572C',
'#FF9833',
'#EF5D28',
'#DA70BF',
'#4DC19C',
'#776E57',
];
// TS needs the precise return type // TS needs the precise return type
function strToRgb(s: string): [number, number, number] { function strToRgb(s: string): [number, number, number] {
if (s.length !== 7) { if (s.length !== 7) {
@ -46,13 +69,13 @@ function strToRgb(s: string): [number, number, number] {
return [parseInt(r, 16), parseInt(g, 16), parseInt(b, 16)]; return [parseInt(r, 16), parseInt(g, 16), parseInt(b, 16)];
} }
export class ColorGenerator { class ColorGenerator {
colorsHex: string[]; colorsHex: string[];
colorsRgb: Array<[number, number, number]>; colorsRgb: Array<[number, number, number]>;
cache: Map<string, number>; cache: Map<string, number>;
currentIdx: number; currentIdx: number;
constructor(colorsHex: string[] = COLORS_HEX) { constructor(colorsHex: string[]) {
this.colorsHex = colorsHex; this.colorsHex = colorsHex;
this.colorsRgb = colorsHex.map(strToRgb); this.colorsRgb = colorsHex.map(strToRgb);
this.cache = new Map(); this.cache = new Map();
@ -95,4 +118,18 @@ export class ColorGenerator {
} }
} }
export default new ColorGenerator(); let darkGenerator = new ColorGenerator(COLORS_HEX_DARK);
let lightGenerator = new ColorGenerator(COLORS_HEX);
export function clear() {
darkGenerator = new ColorGenerator(COLORS_HEX_DARK);
lightGenerator = new ColorGenerator(COLORS_HEX);
}
export function getColorByKey(key: string, theme: Theme) {
return (isLight(theme) ? lightGenerator : darkGenerator).getColorByKey(key);
}
export function getRgbColorByKey(key: string, theme: Theme): [number, number, number] {
return (isLight(theme) ? lightGenerator : darkGenerator).getRgbColorByKey(key);
}

View File

@ -3,14 +3,16 @@ import {
KeyValuePair, KeyValuePair,
Link, Link,
Span, Span,
Trace,
TraceTimelineViewer,
TTraceTimeline,
UIElementsContext,
transformTraceData,
SpanData, SpanData,
ThemeProvider,
ThemeType,
Trace,
TraceData, TraceData,
TracePageHeader, TracePageHeader,
TraceTimelineViewer,
transformTraceData,
TTraceTimeline,
UIElementsContext,
} from '@jaegertracing/jaeger-ui-components'; } from '@jaegertracing/jaeger-ui-components';
import { UIElements } from './uiElements'; import { UIElements } from './uiElements';
import { useViewRange } from './useViewRange'; import { useViewRange } from './useViewRange';
@ -18,6 +20,7 @@ import { useSearch } from './useSearch';
import { useChildrenState } from './useChildrenState'; import { useChildrenState } from './useChildrenState';
import { useDetailState } from './useDetailState'; import { useDetailState } from './useDetailState';
import { useHoverIndentGuide } from './useHoverIndentGuide'; import { useHoverIndentGuide } from './useHoverIndentGuide';
import { useTheme } from '@grafana/ui';
type Props = { type Props = {
trace: TraceData & { spans: SpanData[] }; trace: TraceData & { spans: SpanData[] };
@ -49,8 +52,14 @@ export function TraceView(props: Props) {
const traceProp = transformTraceData(props.trace); const traceProp = transformTraceData(props.trace);
const { search, setSearch, spanFindMatches } = useSearch(traceProp?.spans); const { search, setSearch, spanFindMatches } = useSearch(traceProp?.spans);
const theme = useTheme();
return ( return (
<ThemeProvider
value={{
type: theme.isDark ? ThemeType.Dark : ThemeType.Light,
}}
>
<UIElementsContext.Provider value={UIElements}> <UIElementsContext.Provider value={UIElements}>
<TracePageHeader <TracePageHeader
canCollapse={true} canCollapse={true}
@ -115,5 +124,6 @@ export function TraceView(props: Props) {
uiFind={search} uiFind={search}
/> />
</UIElementsContext.Provider> </UIElementsContext.Provider>
</ThemeProvider>
); );
} }

View File

@ -1,6 +1,9 @@
import React from 'react'; import React from 'react';
import { ButtonProps, Elements } from '@jaegertracing/jaeger-ui-components'; import { ButtonProps, Elements } from '@jaegertracing/jaeger-ui-components';
import { Button, Input } from '@grafana/ui'; import { Button, Input, stylesFactory, useTheme } from '@grafana/ui';
import { css } from 'emotion';
import { GrafanaTheme } from '@grafana/data';
import cx from 'classnames';
/** /**
* Right now Jaeger components need some UI elements to be injected. This is to get rid of AntD UI library that was * Right now Jaeger components need some UI elements to be injected. This is to get rid of AntD UI library that was
@ -29,17 +32,20 @@ export const UIElements: Elements = {
), ),
}; };
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 }) { function Divider({ className }: { className?: string }) {
return ( const styles = getStyles(useTheme());
<div return <div style={{}} className={cx(styles.Divider, className)} />;
style={{
display: 'inline-block',
background: '#e8e8e8',
width: '1px',
height: '0.9em',
margin: '0 8px',
}}
className={className}
/>
);
} }