From 6ac4dc1c3f146b9aeaf1ae0aca58e438b0eaff77 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Wed, 18 Mar 2020 12:20:51 -0400 Subject: [PATCH 01/31] WIP: Action and types for decorations Signed-off-by: Everett Ross --- .../src/actions/path-agnostic-decorations.tsx | 102 ++++++++++++++++++ .../src/components/DeepDependencies/index.tsx | 2 + .../src/components/DeepDependencies/url.tsx | 4 + packages/jaeger-ui/src/model/ddg/types.tsx | 1 + .../model/path-agnostic-decorations/types.tsx | 40 +++++++ .../reducers/path-agnostic-decorations.tsx | 0 packages/jaeger-ui/src/types/TDdgState.tsx | 10 ++ .../types/TPathAgnosticDecorationsState.tsx | 33 ++++++ packages/jaeger-ui/src/types/config.tsx | 6 +- 9 files changed, 197 insertions(+), 1 deletion(-) create mode 100644 packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx create mode 100644 packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx create mode 100644 packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx create mode 100644 packages/jaeger-ui/src/types/TPathAgnosticDecorationsState.tsx diff --git a/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx b/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx new file mode 100644 index 0000000000..1d14a06eb6 --- /dev/null +++ b/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx @@ -0,0 +1,102 @@ +// Copyright (c) 2017 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import _get from 'lodash/get'; +import _identity from 'lodash/identity'; +import _memoize from 'lodash/memoize'; +import _set from 'lodash/set'; +import { createActions, ActionFunctionAny, Action } from 'redux-actions'; + +import { TNewData, TPathAgnosticDecorationSchema } from '../model/path-agnostic-decorations/types'; +import { getConfigValue } from '../utils/config/get-config'; +import generateActionTypes from '../utils/generate-action-types'; + +export const actionTypes = generateActionTypes('@jaeger-ui/PATH_AGNOSTIC_DECORATIONS', [ + 'GET_DECORATION', +]); + +// TODO new home +export const getDecorationSchema = _memoize((id: string): TPathAgnosticDecorationSchema | undefined => { + const schemas = getConfigValue('pathAgnosticDecorations') as TPathAgnosticDecorationSchema[]; + if (!schemas) return undefined; + return schemas.find(s => s.id === id); +}); + +let doneCount: undefined | number; +let pendingCount: undefined | number; +let pendingData: undefined | TNewData; +let pendingPromise: undefined | Promise; +let resolve: undefined | ((arg: TNewData) => void); + +function getDecoration(id: string, service: string, operation?: string) { + const returnPromise = !resolve || !pendingPromise; + if (returnPromise) { + pendingPromise = new Promise(res => { + resolve = res; + }); + } + + const schema = getDecorationSchema(id); + if (!schema) return; + + pendingCount = pendingCount ? pendingCount + 1 : 1; + const { url, opUrl, valuePath, opValuePath } = schema; + let promise: Promise>; + let getPath: string; + let setPath: string; + if (opValuePath && opUrl && operation) { + // const promise = fetch(stringSupplant(opUrl, { service, operation })); + const arbitraryNum = operation.length + service.length; + // getPath = stringSupplant(opValuePath, ({ service, operation })); + getPath = opValuePath; + setPath = `withoutOp.${service}.${operation}`; + promise = new Promise(res => setTimeout(() => res({ opVal: arbitraryNum }), arbitraryNum * 100)); + // .then(res => _get(res, getPath, `${getPath} not found in response`)); + } else { + // const promise = fetch(stringSupplant(url, { service })); + const arbitraryNum = service.length; + // getPath = stringSupplant(valuePath, ({ service })); + getPath = valuePath; + setPath = `withOp.${service}`; + promise = new Promise(res => setTimeout(() => res({ val: arbitraryNum }), arbitraryNum * 100)); + // .then(res => _get(res, getPath, `${getPath} not found in response`)); + } + + promise.then(res => { + return _get(res, getPath, `${getPath} not found in response`); + }).catch(err => { + return `Unable to fetch data, statusCode: ${err.statusCode}`; + }).then(value => { + if (!pendingData) pendingData = {}; + _set(pendingData, setPath, value); + doneCount = doneCount ? doneCount + 1 : 1; + if (doneCount === pendingCount) { + if (resolve) resolve(pendingData); + else throw new Error('`resolve` unexpectedly undefined'); + + doneCount = pendingCount = pendingData = pendingPromise = resolve = undefined; + }; + }); + + if (returnPromise) return pendingPromise; +} + +const fullActions = createActions | undefined>({ + [actionTypes.GET_DECORATION]: getDecoration, +}); + +export default (fullActions as any).jaegerUi.pathAgnosticDecorations as Record< + string, + ActionFunctionAny | undefined>> +>; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx index 19bf5f30be..380cf9dcb3 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx @@ -157,6 +157,8 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { this.updateUrlState({ visEncoding }); }; + setDecoration = (decoration: string | undefined) => this.updateUrlState({ decoration }); + setDensity = (density: EDdgDensity) => this.updateUrlState({ density }); setDistance = (distance: number, direction: EDirection) => { diff --git a/packages/jaeger-ui/src/components/DeepDependencies/url.tsx b/packages/jaeger-ui/src/components/DeepDependencies/url.tsx index 69c25284df..60af0cf308 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/url.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/url.tsx @@ -54,6 +54,7 @@ function firstParam(arg: string | string[]): string { export const getUrlState = memoizeOne(function getUrlState(search: string): TDdgSparseUrlState { const { density = EDdgDensity.PreventPathEntanglement, + decoration, end, hash, operation, @@ -65,6 +66,9 @@ export const getUrlState = memoizeOne(function getUrlState(search: string): TDdg const rv: TDdgSparseUrlState = { density: firstParam(density) as EDdgDensity, }; + if (decoration) { + rv.decoration = firstParam(decoration); + } if (end) { rv.end = Number.parseInt(firstParam(end), 10); } diff --git a/packages/jaeger-ui/src/model/ddg/types.tsx b/packages/jaeger-ui/src/model/ddg/types.tsx index 8ea7311ec0..abea39415f 100644 --- a/packages/jaeger-ui/src/model/ddg/types.tsx +++ b/packages/jaeger-ui/src/model/ddg/types.tsx @@ -101,6 +101,7 @@ export type TDdgVertex = TVertex<{ export type TDdgSparseUrlState = { density: EDdgDensity; + decoration?: string; end?: number; hash?: string; operation?: string; diff --git a/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx new file mode 100644 index 0000000000..6d64b92eeb --- /dev/null +++ b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx @@ -0,0 +1,40 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +export type TPathAgnosticDecorationSchema = { + id: string; + label: string; + icon?: string; + url: string; + opUrl?: string; + valuePath: string; + opValuePath?: string; + gradient: { + startPercentage: number; + inclusiveStart: boolean; + endPercentage: number; + inclusiveEnd: boolean; + }[]; +}; + +export type TNewData = Record; + withOp: Record>; +}>; diff --git a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/jaeger-ui/src/types/TDdgState.tsx b/packages/jaeger-ui/src/types/TDdgState.tsx index 4540714a79..4c3953c0ed 100644 --- a/packages/jaeger-ui/src/types/TDdgState.tsx +++ b/packages/jaeger-ui/src/types/TDdgState.tsx @@ -30,6 +30,16 @@ export type TDdgStateEntry = viewModifiers: Map; }; +/* +type TDdgState = { + entries: Record; + pathAgnosticDecorations: Record; + }>; +}; + */ type TDdgState = Record; // eslint-disable-next-line no-undef diff --git a/packages/jaeger-ui/src/types/TPathAgnosticDecorationsState.tsx b/packages/jaeger-ui/src/types/TPathAgnosticDecorationsState.tsx new file mode 100644 index 0000000000..f148646b07 --- /dev/null +++ b/packages/jaeger-ui/src/types/TPathAgnosticDecorationsState.tsx @@ -0,0 +1,33 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +type TPathAgnosticDecorations = Record; + }; + withOp: { + max: number; + entries: Record>; + }; +}>; + +// eslint-disable-next-line no-undef +export default TPathAgnosticDecorations; diff --git a/packages/jaeger-ui/src/types/config.tsx b/packages/jaeger-ui/src/types/config.tsx index d0fc4a3f5e..fd0866e811 100644 --- a/packages/jaeger-ui/src/types/config.tsx +++ b/packages/jaeger-ui/src/types/config.tsx @@ -12,6 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { TPathAgnosticDecorationSchema } from '../model/path-agnostic-decorations/types'; import { TNil } from '.'; export type ConfigMenuItem = { @@ -39,9 +40,12 @@ export type LinkPatternsConfig = { export type Config = { archiveEnabled?: boolean; - deepDependencies?: { menuEnabled?: boolean }; + deepDependencies?: { + menuEnabled?:boolean; + }; dependencies?: { dagMaxServicesLen?: number; menuEnabled?: boolean }; menu: (ConfigMenuGroup | ConfigMenuItem)[]; + pathAgnosticDecorations?: TPathAgnosticDecorationSchema[]; search?: { maxLookback: { label: string; value: string }; maxLimit: number }; scripts?: TScript[]; topTagPrefixes?: string[]; From 76e814960f386098c2eeb257192f41246a4c9702 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Wed, 18 Mar 2020 17:46:13 -0400 Subject: [PATCH 02/31] Add PAD reducer, fix types, fix year Signed-off-by: Everett Ross --- .../model/path-agnostic-decorations/types.tsx | 17 ++-- packages/jaeger-ui/src/reducers/ddg.tsx | 2 +- .../reducers/path-agnostic-decorations.tsx | 87 +++++++++++++++++++ .../types/TPathAgnosticDecorationsState.tsx | 20 ++--- 4 files changed, 103 insertions(+), 23 deletions(-) diff --git a/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx index 6d64b92eeb..7fbc4bf699 100644 --- a/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx +++ b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx @@ -20,21 +20,22 @@ export type TPathAgnosticDecorationSchema = { opUrl?: string; valuePath: string; opValuePath?: string; + /* gradient: { startPercentage: number; inclusiveStart: boolean; endPercentage: number; inclusiveEnd: boolean; }[]; + */ +}; + +export type TPadEntry = { + value: number | string; // string or other type is for data unavailable + // renderData: unknown; }; export type TNewData = Record; - withOp: Record>; + withoutOp: Record; + withOp?: Record>; }>; diff --git a/packages/jaeger-ui/src/reducers/ddg.tsx b/packages/jaeger-ui/src/reducers/ddg.tsx index 8e36924446..61ccd776d2 100644 --- a/packages/jaeger-ui/src/reducers/ddg.tsx +++ b/packages/jaeger-ui/src/reducers/ddg.tsx @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Uber Technologies, Inc. +// Copyright (c) 2019 Uber Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx index e69de29bb2..1a9a7189a3 100644 --- a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx +++ b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx @@ -0,0 +1,87 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { handleActions } from 'redux-actions'; + +import { actionTypes } from '../actions/path-agnostic-decorations'; + +import { TNewData } from '../model/path-agnostic-decorations/types'; +import TPathAgnosticDecorationsState from '../types/TPathAgnosticDecorationsState'; +import guardReducer from '../utils/guardReducer'; + +function getDecorationDone(state: TPathAgnosticDecorationsState, payload?: TNewData) { + if (!payload) return state; + return Object.keys(payload).reduce((newState, decorationID) => { + const { withOp, withoutOp } = payload[decorationID]; + const newWithOpValues: number[] = []; + Object.keys(withoutOp).forEach(service => { + const { value } = withoutOp[service]; + if (typeof value === 'number') newWithoutOpValues.push(value); + }); + + const newWithoutOpValues: number[] = []; + if (withOp) { + Object.keys(withOp).forEach(service => { + Object.keys(withOp[service]).forEach(operation => { + const { value } = withOp[service][operation]; + if (typeof value === 'number') newWithOpValues.push(value); + }); + }); + } + + if (newState[decorationID]) { + const { withOpMax: currWithOpMax, withoutOpMax: currWithoutOpMax } = newState[decorationID]; + if (typeof currWithOpMax === 'number') newWithOpValues.push(currWithOpMax); + if (typeof currWithoutOpMax === 'number') newWithoutOpValues.push(currWithoutOpMax); + const withOpMax = Math.max(...newWithOpValues); + const withoutOpMax = Math.max(...newWithoutOpValues); + return { + ...newState, + [decorationID]: { + withOp: withOp + ? Object.keys(withOp).reduce((newWithOp, service) => ({ + ...newWithOp, + [service]: Object.assign({}, newWithOp[service], withoutOp[service]), + }), newState[decorationID].withOp || {}) + : newState[decorationID].withOp, + withOpMax, + withoutOp: withOp + ? Object.assign({}, newState[decorationID].withoutOp, withoutOp) + : newState[decorationID].withoutOp, + withoutOpMax, + }, + }; + + } else { + const withOpMax = Math.max(...newWithOpValues); + const withoutOpMax = Math.max(...newWithoutOpValues); + return { + ...newState, + [decorationID]: { + withOp, + withOpMax, + withoutOp, + withoutOpMax, + }, + }; + } + }, state); +} + +export default handleActions( + { + [actionTypes.GET_DECORATION]: guardReducer(getDecorationDone), + }, + {} +); diff --git a/packages/jaeger-ui/src/types/TPathAgnosticDecorationsState.tsx b/packages/jaeger-ui/src/types/TPathAgnosticDecorationsState.tsx index f148646b07..4c914cb1f2 100644 --- a/packages/jaeger-ui/src/types/TPathAgnosticDecorationsState.tsx +++ b/packages/jaeger-ui/src/types/TPathAgnosticDecorationsState.tsx @@ -12,21 +12,13 @@ // See the License for the specific language governing permissions and // limitations under the License. +import { TPadEntry } from '../model/path-agnostic-decorations/types'; + type TPathAgnosticDecorations = Record; - }; - withOp: { - max: number; - entries: Record>; - }; + withOpMax?: number; + withoutOpMax: number; + withoutOp: Record; + withOp?: Record>; }>; // eslint-disable-next-line no-undef From d2747c0c899024ae527de28041a35a1465f6d33d Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Wed, 18 Mar 2020 19:14:14 -0400 Subject: [PATCH 03/31] Fix and test reducer, fix types, fix another year Signed-off-by: Everett Ross --- .../model/path-agnostic-decorations/types.tsx | 2 +- packages/jaeger-ui/src/reducers/ddg.test.js | 2 +- .../path-agnostic-decorations.test.js | 153 ++++++++++++++++++ .../reducers/path-agnostic-decorations.tsx | 20 +-- .../types/TPathAgnosticDecorationsState.tsx | 4 +- 5 files changed, 168 insertions(+), 13 deletions(-) create mode 100644 packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js diff --git a/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx index 7fbc4bf699..03e757c547 100644 --- a/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx +++ b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx @@ -36,6 +36,6 @@ export type TPadEntry = { }; export type TNewData = Record; + withoutOp?: Record; withOp?: Record>; }>; diff --git a/packages/jaeger-ui/src/reducers/ddg.test.js b/packages/jaeger-ui/src/reducers/ddg.test.js index 04006c8e5f..f1c4b09141 100644 --- a/packages/jaeger-ui/src/reducers/ddg.test.js +++ b/packages/jaeger-ui/src/reducers/ddg.test.js @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Uber Technologies, Inc. +// Copyright (c) 2019 Uber Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js new file mode 100644 index 0000000000..7f862e249b --- /dev/null +++ b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js @@ -0,0 +1,153 @@ +// Copyright (c) 2019 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import _merge from 'lodash/merge'; + +import { getDecorationDone } from './path-agnostic-decorations'; + +describe('pathAgnosticDecoration reducers', () => { + const decorationID = { + 0: 'decoration id 0', + 1: 'decoration id 1', + }; + const op = { + 0: 'op 0', + 1: 'op 1', + }; + const svc = { + 0: 'svc 0', + 1: 'svc 1', + }; + + function genStateAndPayload(id, service, operation, value) { + const isWithOp = typeof operation === 'number'; + const valueObj = { + value, + }; + + const payloadKey = isWithOp ? 'withOp' : 'withoutOp'; + const payload = { + [decorationID[id]]: { + [payloadKey]: { + [svc[service]]: isWithOp + ? { + [op[operation]]: valueObj, + } + : valueObj, + }, + }, + }; + + const stateKey = isWithOp ? 'withOpMax' : 'withoutOpMax'; + const state = { + [decorationID[id]]: { + ...payload[decorationID[id]], + [stateKey]: typeof value === 'number' ? value : -Infinity, + }, + }; + return { payload, state }; + } + + // variable names are type(payload|state)+decoration(0|1)+service(0|1)+operation(0|1|u)+value(0|1|s) + // u for undefined, or opless + // s for string, or errored request + const { payload: payload00u0, state: state00u0 } = genStateAndPayload(0, 0, undefined, 0); + const { payload: payload00us, state: state00us } = genStateAndPayload(0, 0, undefined, 'Error: 404'); + const { payload: payload01u1, state: state01u1 } = genStateAndPayload(0, 1, undefined, 1); + const { payload: payload10u1, state: state10u1 } = genStateAndPayload(1, 0, undefined, 1); + + const { payload: payload0000, state: state0000 } = genStateAndPayload(0, 0, 0, 0); + const { payload: payload000s, state: state000s } = genStateAndPayload(0, 0, 0, 'Error: 404'); + const { payload: payload0011, state: state0011 } = genStateAndPayload(0, 0, 1, 1); + const { payload: payload0101, state: state0101 } = genStateAndPayload(0, 1, 0, 1); + const { payload: payload1001, state: state1001 } = genStateAndPayload(1, 0, 0, 1); + + function mergeAndObjectContaining(...args) { + const merged = _merge({}, ...args); + const objectContaining = {}; + Object.keys(merged).forEach(key => { + objectContaining[key] = expect.objectContaining(merged[key]); + }); + return objectContaining; + } + + it('returns existing state if payload is undefined', () => { + const priorState = {}; + expect(getDecorationDone(priorState)).toBe(priorState); + }); + + describe('withoutOp', () => { + it('adds service decoration to empty state', () => { + expect(getDecorationDone({}, payload00u0)).toEqual(mergeAndObjectContaining(state00u0)); + }); + + it('adds service decoration error to empty state', () => { + expect(getDecorationDone({}, payload00us)).toEqual(mergeAndObjectContaining(state00us)); + }); + + it('adds service decoration to state with different decoration', () => { + expect(getDecorationDone(state00u0, payload10u1)).toEqual(mergeAndObjectContaining(state00u0, state10u1)); + }); + + it('adds service decoration to state with existing decoration and updates withoutOpMax', () => { + expect(getDecorationDone(state00u0, payload01u1)).toEqual(mergeAndObjectContaining(state00u0, state01u1)); + }); + + it('adds service decoration to state with existing decoration without overriding higher withoutOpMax', () => { + expect(getDecorationDone(state01u1, payload00u0)).toEqual(mergeAndObjectContaining(state00u0, state01u1)); + }); + + it('adds service decoration error to state with existing decoration without overriding existing withoutOpMax', () => { + expect(getDecorationDone(state01u1, payload00us)).toEqual(mergeAndObjectContaining(state00us, state01u1)); + }); + }); + + describe('withOp', () => { + it('adds operation decoration to empty state', () => { + expect(getDecorationDone({}, payload0000)).toEqual(mergeAndObjectContaining(state0000)); + }); + + it('adds operation decoration error to empty state', () => { + expect(getDecorationDone({}, payload000s)).toEqual(mergeAndObjectContaining(state000s)); + }); + + it('adds operation decoration to state with different decoration', () => { + expect(getDecorationDone(state0000, payload1001)).toEqual(mergeAndObjectContaining(state0000, state1001)); + }); + + it('adds operation decoration to state with same decoration but different service and updates withOpMax', () => { + expect(getDecorationDone(state0000, payload0101)).toEqual(mergeAndObjectContaining(state0000, state0101)); + }); + + it('adds operation decoration to state with same decoration but different service without overriding higher withoutOpMax', () => { + expect(getDecorationDone(state0101, payload0000)).toEqual(mergeAndObjectContaining(state0000, state0101)); + }); + + it('adds operation decoration to state with existing decoration and same service and updates withoutOpMax', () => { + expect(getDecorationDone(state0000, payload0011)).toEqual(mergeAndObjectContaining(state0000, state0011)); + }); + + it('adds operation decoration to state with existing decoration and same service without overriding higher withoutOpMax', () => { + expect(getDecorationDone(state0011, payload0000)).toEqual(mergeAndObjectContaining(state0000, state0011)); + }); + + it('adds operation decoration error to state with existing decoration without overriding existing withoutOpMax', () => { + expect(getDecorationDone(state0011, payload000s)).toEqual(mergeAndObjectContaining(state000s, state0011)); + }); + + it('adds operation decoration to state with only service decorations', () => { + expect(getDecorationDone(state01u1, payload0000)).toEqual(mergeAndObjectContaining(state0000, state01u1)); + }); + }); +}); diff --git a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx index 1a9a7189a3..74c4b720a8 100644 --- a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx +++ b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx @@ -20,17 +20,19 @@ import { TNewData } from '../model/path-agnostic-decorations/types'; import TPathAgnosticDecorationsState from '../types/TPathAgnosticDecorationsState'; import guardReducer from '../utils/guardReducer'; -function getDecorationDone(state: TPathAgnosticDecorationsState, payload?: TNewData) { +export function getDecorationDone(state: TPathAgnosticDecorationsState, payload?: TNewData) { if (!payload) return state; return Object.keys(payload).reduce((newState, decorationID) => { const { withOp, withoutOp } = payload[decorationID]; - const newWithOpValues: number[] = []; - Object.keys(withoutOp).forEach(service => { - const { value } = withoutOp[service]; - if (typeof value === 'number') newWithoutOpValues.push(value); - }); - const newWithoutOpValues: number[] = []; + if (withoutOp) { + Object.keys(withoutOp).forEach(service => { + const { value } = withoutOp[service]; + if (typeof value === 'number') newWithoutOpValues.push(value); + }); + } + + const newWithOpValues: number[] = []; if (withOp) { Object.keys(withOp).forEach(service => { Object.keys(withOp[service]).forEach(operation => { @@ -52,11 +54,11 @@ function getDecorationDone(state: TPathAgnosticDecorationsState, payload?: TNewD withOp: withOp ? Object.keys(withOp).reduce((newWithOp, service) => ({ ...newWithOp, - [service]: Object.assign({}, newWithOp[service], withoutOp[service]), + [service]: Object.assign({}, newWithOp[service], withOp[service]), }), newState[decorationID].withOp || {}) : newState[decorationID].withOp, withOpMax, - withoutOp: withOp + withoutOp: withoutOp ? Object.assign({}, newState[decorationID].withoutOp, withoutOp) : newState[decorationID].withoutOp, withoutOpMax, diff --git a/packages/jaeger-ui/src/types/TPathAgnosticDecorationsState.tsx b/packages/jaeger-ui/src/types/TPathAgnosticDecorationsState.tsx index 4c914cb1f2..3320b7eec6 100644 --- a/packages/jaeger-ui/src/types/TPathAgnosticDecorationsState.tsx +++ b/packages/jaeger-ui/src/types/TPathAgnosticDecorationsState.tsx @@ -16,8 +16,8 @@ import { TPadEntry } from '../model/path-agnostic-decorations/types'; type TPathAgnosticDecorations = Record; + withoutOpMax?: number; + withoutOp?: Record; withOp?: Record>; }>; From 21a40d412df5cdfb8cbe4cc69c2c1cbd8f1763ef Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Wed, 18 Mar 2020 19:16:39 -0400 Subject: [PATCH 04/31] Add another pad reducer test Signed-off-by: Everett Ross --- .../jaeger-ui/src/reducers/path-agnostic-decorations.test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js index 7f862e249b..785e2c03ef 100644 --- a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js +++ b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js @@ -111,6 +111,10 @@ describe('pathAgnosticDecoration reducers', () => { it('adds service decoration error to state with existing decoration without overriding existing withoutOpMax', () => { expect(getDecorationDone(state01u1, payload00us)).toEqual(mergeAndObjectContaining(state00us, state01u1)); }); + + it('adds service decoration to state with only operation decorations', () => { + expect(getDecorationDone(state0011, payload00u0)).toEqual(mergeAndObjectContaining(state00u0, state0011)); + }); }); describe('withOp', () => { From da3b0d2399b8c22be25d83fd6224b9cd5b7480d5 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Fri, 20 Mar 2020 16:05:36 -0400 Subject: [PATCH 05/31] WIP: Begin testing action Signed-off-by: Everett Ross --- .../actions/path-agnostic-decorations.test.js | 125 ++++++++++++++++++ .../src/actions/path-agnostic-decorations.tsx | 44 +++--- packages/jaeger-ui/src/api/jaeger.js | 3 + .../path-agnostic-decorations.test.js | 102 ++++++++------ 4 files changed, 216 insertions(+), 58 deletions(-) create mode 100644 packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js diff --git a/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js b/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js new file mode 100644 index 0000000000..93879a3b4b --- /dev/null +++ b/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js @@ -0,0 +1,125 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import _get from 'lodash/get'; +import _set from 'lodash/set'; + +import JaegerAPI from '../api/jaeger'; + +import * as getConfig from '../utils/config/get-config'; +import { getDecoration as getDecorationImpl, stringSupplant } from './path-agnostic-decorations'; + +describe('getDecoration', () => { + let getConfigValueSpy; + let fetchDecorationSpy; + let resolves; + let rejects; + // let mockDecorations; + + const opUrl = 'opUrl?service=#service&operation=#operation'; + const url = 'opUrl?service=#service'; + const valuePath = 'withoutOpPath.#service'; + const opValuePath = 'opPath.#service.#operation'; + const withOpID = 'decoration id with op url and op path'; + const partialID = 'decoration id with op url without op path'; + const withoutOpID = 'decoration id with only url'; + + // wrapper is necessary to prevent cross pollution between tests + let couldBePending = []; + const getDecoration = (...args) => { + const promise = getDecorationImpl(...args); + if (promise) couldBePending.push(promise); + return promise; + }; + + beforeAll(() => { + getConfigValueSpy = jest + .spyOn(getConfig, 'getConfigValue') + .mockReturnValue([ + { + id: withOpID, + url, + opUrl, + valuePath, + opValuePath, + }, + { + id: partialID, + url, + opUrl, + valuePath, + },{ + id: withoutOpID, + url, + valuePath, + } + ]); + fetchDecorationSpy = jest + .spyOn(JaegerAPI, 'fetchDecoration') + .mockImplementation(() => new Promise((res, rej) => { + resolves.push(res); + rejects.push(rej); + })); + }); + + beforeEach(() => { + resolves = []; + rejects = []; + // mockDecorations = []; + }); + + afterEach(async () => { + resolves.forEach(resolve => resolve()); + await Promise.all(couldBePending); + couldBePending = []; + }); + + it('returns undefined if schema is not found for id', () => { + expect(getDecoration('missing id', 'b', 'c')).toBeUndefined(); + }); + + it('returns a promise for its first call', () => { + expect(getDecoration(withOpID, 'b', 'c')).toEqual(expect.any(Promise)); + }); + + it('returns undefined if invoked before previous invocation is resolved', () => { + getDecoration(withOpID, 'b', 'c'); + expect(getDecoration(withoutOpID, 'b')).toBeUndefined(); + }); + + it('resolves to include single response', async () => { + }); + + it('handles error responses', () => { + }); + + it('defaults value if valuePath not found in response', () => { + }); + + it('resolves to include responses for all concurrent requests', () => { + }); + + it('returns new promise if invoked after previous invocation is resolved', async () => { + const promise = getDecoration(withOpID, 'b', 'c'); + resolves[0](); + await promise; + expect(getDecoration(withoutOpID, 'b')).toEqual(expect.any(Promise)); + }); + + it('scopes promises to not include previous promise results', () => { + }); + + it('no-ops for already processed id, service, and operation', () => { + }); +}); diff --git a/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx b/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx index 1d14a06eb6..5f412d08a9 100644 --- a/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx +++ b/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx @@ -1,4 +1,4 @@ -// Copyright (c) 2017 Uber Technologies, Inc. +// Copyright (c) 2020 Uber Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import _memoize from 'lodash/memoize'; import _set from 'lodash/set'; import { createActions, ActionFunctionAny, Action } from 'redux-actions'; +import JaegerAPI from '../api/jaeger'; import { TNewData, TPathAgnosticDecorationSchema } from '../model/path-agnostic-decorations/types'; import { getConfigValue } from '../utils/config/get-config'; import generateActionTypes from '../utils/generate-action-types'; @@ -26,6 +27,16 @@ export const actionTypes = generateActionTypes('@jaeger-ui/PATH_AGNOSTIC_DECORAT 'GET_DECORATION', ]); +// this should probable be in a util file somewhere, with the ability to bind an enconding to it +const parameterRegExp = /#\{([^{}]*)\}/g; + +export function stringSupplant(str: string, map: Record) { + return str.replace(parameterRegExp, (_, name) => { + const value = map[name]; + return value == null ? '' : `${value}`; + }); +} + // TODO new home export const getDecorationSchema = _memoize((id: string): TPathAgnosticDecorationSchema | undefined => { const schemas = getConfigValue('pathAgnosticDecorations') as TPathAgnosticDecorationSchema[]; @@ -39,7 +50,11 @@ let pendingData: undefined | TNewData; let pendingPromise: undefined | Promise; let resolve: undefined | ((arg: TNewData) => void); -function getDecoration(id: string, service: string, operation?: string) { +export function getDecoration(id: string, service: string, operation?: string) { + const schema = getDecorationSchema(id); + console.log(schema); + if (!schema) return; + const returnPromise = !resolve || !pendingPromise; if (returnPromise) { pendingPromise = new Promise(res => { @@ -47,40 +62,39 @@ function getDecoration(id: string, service: string, operation?: string) { }); } - const schema = getDecorationSchema(id); - if (!schema) return; - pendingCount = pendingCount ? pendingCount + 1 : 1; const { url, opUrl, valuePath, opValuePath } = schema; let promise: Promise>; let getPath: string; let setPath: string; if (opValuePath && opUrl && operation) { - // const promise = fetch(stringSupplant(opUrl, { service, operation })); - const arbitraryNum = operation.length + service.length; - // getPath = stringSupplant(opValuePath, ({ service, operation })); - getPath = opValuePath; + promise = JaegerAPI.fetchDecoration(stringSupplant(opUrl, { service, operation })); + // const arbitraryNum = operation.length + service.length; + getPath = stringSupplant(opValuePath, ({ service, operation })); + // getPath = opValuePath; setPath = `withoutOp.${service}.${operation}`; - promise = new Promise(res => setTimeout(() => res({ opVal: arbitraryNum }), arbitraryNum * 100)); + // promise = new Promise(res => setTimeout(() => res({ opVal: arbitraryNum }), arbitraryNum * 100)); // .then(res => _get(res, getPath, `${getPath} not found in response`)); } else { - // const promise = fetch(stringSupplant(url, { service })); - const arbitraryNum = service.length; - // getPath = stringSupplant(valuePath, ({ service })); + console.log(schema, url); + promise = JaegerAPI.fetchDecoration(stringSupplant(url, { service })); + // const arbitraryNum = service.length; + getPath = stringSupplant(valuePath, ({ service })); getPath = valuePath; setPath = `withOp.${service}`; - promise = new Promise(res => setTimeout(() => res({ val: arbitraryNum }), arbitraryNum * 100)); + // promise = new Promise(res => setTimeout(() => res({ val: arbitraryNum }), arbitraryNum * 100)); // .then(res => _get(res, getPath, `${getPath} not found in response`)); } promise.then(res => { return _get(res, getPath, `${getPath} not found in response`); }).catch(err => { - return `Unable to fetch data, statusCode: ${err.statusCode}`; + return `Unable to fetch decoration: ${err.message || err}`; }).then(value => { if (!pendingData) pendingData = {}; _set(pendingData, setPath, value); doneCount = doneCount ? doneCount + 1 : 1; + if (doneCount === pendingCount) { if (resolve) resolve(pendingData); else throw new Error('`resolve` unexpectedly undefined'); diff --git a/packages/jaeger-ui/src/api/jaeger.js b/packages/jaeger-ui/src/api/jaeger.js index a1a115dd96..daaec18525 100644 --- a/packages/jaeger-ui/src/api/jaeger.js +++ b/packages/jaeger-ui/src/api/jaeger.js @@ -80,6 +80,9 @@ const JaegerAPI = { archiveTrace(id) { return getJSON(`${this.apiRoot}archive/${id}`, { method: 'POST' }); }, + fetchDecoration(url) { + return getJSON(url); + }, fetchDeepDependencyGraph(query) { return getJSON(`${ANALYTICS_ROOT}v1/dependencies`, { query }); }, diff --git a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js index 785e2c03ef..df197c33d3 100644 --- a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js +++ b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js @@ -16,54 +16,60 @@ import _merge from 'lodash/merge'; import { getDecorationDone } from './path-agnostic-decorations'; -describe('pathAgnosticDecoration reducers', () => { - const decorationID = { - 0: 'decoration id 0', - 1: 'decoration id 1', - }; - const op = { - 0: 'op 0', - 1: 'op 1', - }; - const svc = { - 0: 'svc 0', - 1: 'svc 1', +const decorationID = { + 0: 'decoration id 0', + 1: 'decoration id 1', +}; +const op = { + 0: 'op 0', + 1: 'op 1', +}; +const svc = { + 0: 'svc 0', + 1: 'svc 1', +}; + +// eslint-disable-next-line import/prefer-default-export +export function genStateAndPayload(_id, _service, _operation, value) { + const id = decorationID[_id]; + const operation = op[_operation]; + const service = svc[_service]; + + const isWithOp = typeof operation === 'number'; + const valueObj = { + value, }; - function genStateAndPayload(id, service, operation, value) { - const isWithOp = typeof operation === 'number'; - const valueObj = { - value, - }; - - const payloadKey = isWithOp ? 'withOp' : 'withoutOp'; - const payload = { - [decorationID[id]]: { - [payloadKey]: { - [svc[service]]: isWithOp - ? { - [op[operation]]: valueObj, - } - : valueObj, - }, + const payloadKey = isWithOp ? 'withOp' : 'withoutOp'; + const payload = { + [id]: { + [payloadKey]: { + [service]: isWithOp + ? { + [operation]: valueObj, + } + : valueObj, }, - }; + }, + }; - const stateKey = isWithOp ? 'withOpMax' : 'withoutOpMax'; - const state = { - [decorationID[id]]: { - ...payload[decorationID[id]], - [stateKey]: typeof value === 'number' ? value : -Infinity, - }, - }; - return { payload, state }; - } + const stateKey = isWithOp ? 'withOpMax' : 'withoutOpMax'; + const state = { + [id]: { + ...payload[id], + [stateKey]: typeof value === 'number' ? value : -Infinity, + }, + }; + return { id, operation, service, payload, state }; +} +describe('pathAgnosticDecoration reducers', () => { // variable names are type(payload|state)+decoration(0|1)+service(0|1)+operation(0|1|u)+value(0|1|s) // u for undefined, or opless // s for string, or errored request const { payload: payload00u0, state: state00u0 } = genStateAndPayload(0, 0, undefined, 0); const { payload: payload00us, state: state00us } = genStateAndPayload(0, 0, undefined, 'Error: 404'); + const { state: state00u1 } = genStateAndPayload(0, 0, undefined, 1); const { payload: payload01u1, state: state01u1 } = genStateAndPayload(0, 1, undefined, 1); const { payload: payload10u1, state: state10u1 } = genStateAndPayload(1, 0, undefined, 1); @@ -111,10 +117,6 @@ describe('pathAgnosticDecoration reducers', () => { it('adds service decoration error to state with existing decoration without overriding existing withoutOpMax', () => { expect(getDecorationDone(state01u1, payload00us)).toEqual(mergeAndObjectContaining(state00us, state01u1)); }); - - it('adds service decoration to state with only operation decorations', () => { - expect(getDecorationDone(state0011, payload00u0)).toEqual(mergeAndObjectContaining(state00u0, state0011)); - }); }); describe('withOp', () => { @@ -149,9 +151,23 @@ describe('pathAgnosticDecoration reducers', () => { it('adds operation decoration error to state with existing decoration without overriding existing withoutOpMax', () => { expect(getDecorationDone(state0011, payload000s)).toEqual(mergeAndObjectContaining(state000s, state0011)); }); + }); + + describe('mixed', () => { + it('adds service decoration to state with only operation decorations', () => { + expect(getDecorationDone(state0011, payload00u0)).toEqual(mergeAndObjectContaining(state00u0, state0011)); + }); it('adds operation decoration to state with only service decorations', () => { - expect(getDecorationDone(state01u1, payload0000)).toEqual(mergeAndObjectContaining(state0000, state01u1)); + expect(getDecorationDone(state00u1, payload0000)).toEqual(mergeAndObjectContaining(state0000, state00u1)); + }); + + it('adds multiple operation and service decortions to state with multiple operation and service decorations', () => { + const initialState = _merge({}, state00us, state00u0, state1001); + const payload = _merge({}, payload01u1, payload10u1, payload10u1, payload000s, payload0101); + const expectedState = mergeAndObjectContaining({}, state000s, initialState, state01u1, state10u1, state0101); + + expect(getDecorationDone(initialState, payload)).toEqual(expectedState); }); }); }); From d11f86615cca474e3c2f353740644b75d508e222 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Mon, 23 Mar 2020 18:11:52 -0400 Subject: [PATCH 06/31] WIP: Finish action tests TODO: Move stringSupplant Signed-off-by: Everett Ross --- .../actions/path-agnostic-decorations.test.js | 255 ++++++++++++++---- .../src/actions/path-agnostic-decorations.tsx | 86 +++--- .../model/path-agnostic-decorations/types.tsx | 11 +- .../path-agnostic-decorations.test.js | 69 +++-- .../reducers/path-agnostic-decorations.tsx | 39 +-- .../types/TPathAgnosticDecorationsState.tsx | 15 +- packages/jaeger-ui/src/types/config.tsx | 2 +- 7 files changed, 347 insertions(+), 130 deletions(-) diff --git a/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js b/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js index 93879a3b4b..f60a77675e 100644 --- a/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js +++ b/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js @@ -12,20 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -import _get from 'lodash/get'; import _set from 'lodash/set'; import JaegerAPI from '../api/jaeger'; import * as getConfig from '../utils/config/get-config'; -import { getDecoration as getDecorationImpl, stringSupplant } from './path-agnostic-decorations'; +import { processed, getDecoration as getDecorationImpl, stringSupplant } from './path-agnostic-decorations'; describe('getDecoration', () => { let getConfigValueSpy; let fetchDecorationSpy; let resolves; let rejects; - // let mockDecorations; const opUrl = 'opUrl?service=#service&operation=#operation'; const url = 'opUrl?service=#service'; @@ -34,6 +32,9 @@ describe('getDecoration', () => { const withOpID = 'decoration id with op url and op path'; const partialID = 'decoration id with op url without op path'; const withoutOpID = 'decoration id with only url'; + const service = 'svc'; + const operation = 'op'; + const testVal = 42; // wrapper is necessary to prevent cross pollution between tests let couldBePending = []; @@ -44,39 +45,40 @@ describe('getDecoration', () => { }; beforeAll(() => { - getConfigValueSpy = jest - .spyOn(getConfig, 'getConfigValue') - .mockReturnValue([ - { - id: withOpID, - url, - opUrl, - valuePath, - opValuePath, - }, - { - id: partialID, - url, - opUrl, - valuePath, - },{ - id: withoutOpID, - url, - valuePath, - } - ]); - fetchDecorationSpy = jest - .spyOn(JaegerAPI, 'fetchDecoration') - .mockImplementation(() => new Promise((res, rej) => { - resolves.push(res); - rejects.push(rej); - })); + getConfigValueSpy = jest.spyOn(getConfig, 'getConfigValue').mockReturnValue([ + { + id: withOpID, + url, + opUrl, + valuePath, + opValuePath, + }, + { + id: partialID, + url, + opUrl, + valuePath, + }, + { + id: withoutOpID, + url, + valuePath, + }, + ]); + fetchDecorationSpy = jest.spyOn(JaegerAPI, 'fetchDecoration').mockImplementation( + () => + new Promise((res, rej) => { + resolves.push(res); + rejects.push(rej); + }) + ); }); beforeEach(() => { + fetchDecorationSpy.mockClear(); + processed.clear(); resolves = []; rejects = []; - // mockDecorations = []; }); afterEach(async () => { @@ -85,41 +87,202 @@ describe('getDecoration', () => { couldBePending = []; }); + it('returns undefined if no schemas exist in config', () => { + getConfigValueSpy.mockReturnValueOnce(); + expect(getDecoration('foo', service, operation)).toBeUndefined(); + }); + it('returns undefined if schema is not found for id', () => { - expect(getDecoration('missing id', 'b', 'c')).toBeUndefined(); + expect(getDecoration('missing id', service, operation)).toBeUndefined(); }); it('returns a promise for its first call', () => { - expect(getDecoration(withOpID, 'b', 'c')).toEqual(expect.any(Promise)); + expect(getDecoration(withOpID, service, operation)).toEqual(expect.any(Promise)); }); - it('returns undefined if invoked before previous invocation is resolved', () => { - getDecoration(withOpID, 'b', 'c'); - expect(getDecoration(withoutOpID, 'b')).toBeUndefined(); + it('resolves to include single response for op decoration given op', async () => { + const promise = getDecoration(withOpID, service, operation); + resolves[0](_set({}, stringSupplant(opValuePath, { service, operation }), testVal)); + const res = await promise; + expect(res).toEqual(_set({}, `${withOpID}.withOp.${service}.${operation}`, testVal)); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(opUrl, { service, operation })); }); - it('resolves to include single response', async () => { + it('resolves to include single response for op decoration not given op', async () => { + const promise = getDecoration(withOpID, service); + resolves[0](_set({}, stringSupplant(valuePath, { service }), testVal)); + const res = await promise; + expect(res).toEqual(_set({}, `${withOpID}.withoutOp.${service}`, testVal)); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service })); }); - it('handles error responses', () => { + it('resolves to include single response for malformed op decoration given op', async () => { + const promise = getDecoration(partialID, service, operation); + resolves[0](_set({}, stringSupplant(valuePath, { service }), testVal)); + const res = await promise; + expect(res).toEqual(_set({}, `${partialID}.withoutOp.${service}`, testVal)); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service })); }); - it('defaults value if valuePath not found in response', () => { + it('resolves to include single response for svc decoration given op', async () => { + const promise = getDecoration(withoutOpID, service, operation); + resolves[0](_set({}, stringSupplant(valuePath, { service }), testVal)); + const res = await promise; + expect(res).toEqual(_set({}, `${withoutOpID}.withoutOp.${service}`, testVal)); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service })); }); - it('resolves to include responses for all concurrent requests', () => { + it('resolves to include single response for svc decoration not given op', async () => { + const promise = getDecoration(withoutOpID, service); + resolves[0](_set({}, stringSupplant(valuePath, { service }), testVal)); + const res = await promise; + expect(res).toEqual(_set({}, `${withoutOpID}.withoutOp.${service}`, testVal)); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service })); + }); + + it('handles error responses', async () => { + const message = 'foo error message'; + const promise0 = getDecoration(withoutOpID, service); + rejects[0]({ message }); + const res0 = await promise0; + expect(res0).toEqual( + _set({}, `${withoutOpID}.withoutOp.${service}`, `Unable to fetch decoration: ${message}`) + ); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service })); + + const err = 'foo error without message'; + const promise1 = getDecoration(withOpID, service, operation); + rejects[1](err); + const res1 = await promise1; + expect(res1).toEqual( + _set({}, `${withOpID}.withOp.${service}.${operation}`, `Unable to fetch decoration: ${err}`) + ); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(opUrl, { service, operation })); }); - it('returns new promise if invoked after previous invocation is resolved', async () => { - const promise = getDecoration(withOpID, 'b', 'c'); + it('defaults value if valuePath not found in response', async () => { + const promise = getDecoration(withoutOpID, service); resolves[0](); - await promise; - expect(getDecoration(withoutOpID, 'b')).toEqual(expect.any(Promise)); + const res = await promise; + expect(res).toEqual( + _set( + {}, + `${withoutOpID}.withoutOp.${service}`, + `${stringSupplant(valuePath, { service })} not found in response` + ) + ); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service })); }); - it('scopes promises to not include previous promise results', () => { + it('returns undefined if invoked before previous invocation is resolved', () => { + getDecoration(withOpID, service, operation); + expect(getDecoration(withoutOpID, service)).toBeUndefined(); }); - it('no-ops for already processed id, service, and operation', () => { + it('resolves to include responses for all concurrent requests', async () => { + const otherOp = 'other op'; + const promise = getDecoration(withOpID, service, operation); + resolves[0](_set({}, stringSupplant(opValuePath, { service, operation }), testVal)); + getDecoration(partialID, service, operation); + resolves[1](_set({}, stringSupplant(valuePath, { service }), testVal)); + getDecoration(withOpID, service); + resolves[2](_set({}, stringSupplant(valuePath, { service }), testVal)); + getDecoration(withoutOpID, service); + resolves[3](_set({}, stringSupplant(valuePath, { service }), testVal)); + const message = 'foo error message'; + getDecoration(withOpID, service, otherOp); + rejects[4]({ message }); + const res = await promise; + + expect(res).toEqual({ + [withOpID]: { + withOp: { + [service]: { + [operation]: testVal, + [otherOp]: `Unable to fetch decoration: ${message}`, + }, + }, + withoutOp: { + [service]: testVal, + }, + }, + [partialID]: { + withoutOp: { + [service]: testVal, + }, + }, + [withoutOpID]: { + withoutOp: { + [service]: testVal, + }, + }, + }); + }); + + it('scopes promises to not include previous promise results', async () => { + const otherOp = 'other op'; + const promise0 = getDecoration(withOpID, service, operation); + resolves[0](_set({}, stringSupplant(opValuePath, { service, operation }), testVal)); + getDecoration(partialID, service, operation); + resolves[1](_set({}, stringSupplant(valuePath, { service }), testVal)); + const res0 = await promise0; + + const promise1 = getDecoration(withOpID, service); + resolves[2](_set({}, stringSupplant(valuePath, { service }), testVal)); + getDecoration(withoutOpID, service); + resolves[3](_set({}, stringSupplant(valuePath, { service }), testVal)); + const message = 'foo error message'; + getDecoration(withOpID, service, otherOp); + rejects[4]({ message }); + const res1 = await promise1; + + expect(res0).toEqual({ + [withOpID]: { + withOp: { + [service]: { + [operation]: testVal, + }, + }, + }, + [partialID]: { + withoutOp: { + [service]: testVal, + }, + }, + }); + + expect(res1).toEqual({ + [withOpID]: { + withOp: { + [service]: { + [otherOp]: `Unable to fetch decoration: ${message}`, + }, + }, + withoutOp: { + [service]: testVal, + }, + }, + [withoutOpID]: { + withoutOp: { + [service]: testVal, + }, + }, + }); + }); + + it('no-ops for already processed id, service, and operation', async () => { + const promise0 = getDecoration(withOpID, service, operation); + resolves[0](_set({}, stringSupplant(opValuePath, { service, operation }), testVal)); + const res0 = await promise0; + expect(res0).toEqual(_set({}, `${withOpID}.withOp.${service}.${operation}`, testVal)); + + const promise1 = getDecoration(withOpID, service, operation); + expect(promise1).toBeUndefined(); + + const promise2 = getDecoration(withoutOpID, service); + resolves[1](_set({}, stringSupplant(valuePath, { service }), testVal)); + const res1 = await promise2; + expect(res1).toEqual(_set({}, `${withoutOpID}.withoutOp.${service}`, testVal)); + expect(fetchDecorationSpy).toHaveBeenCalledTimes(2); }); }); diff --git a/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx b/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx index 5f412d08a9..6faf9952f9 100644 --- a/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx +++ b/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx @@ -13,7 +13,6 @@ // limitations under the License. import _get from 'lodash/get'; -import _identity from 'lodash/identity'; import _memoize from 'lodash/memoize'; import _set from 'lodash/set'; import { createActions, ActionFunctionAny, Action } from 'redux-actions'; @@ -23,23 +22,23 @@ import { TNewData, TPathAgnosticDecorationSchema } from '../model/path-agnostic- import { getConfigValue } from '../utils/config/get-config'; import generateActionTypes from '../utils/generate-action-types'; -export const actionTypes = generateActionTypes('@jaeger-ui/PATH_AGNOSTIC_DECORATIONS', [ - 'GET_DECORATION', -]); +export const actionTypes = generateActionTypes('@jaeger-ui/PATH_AGNOSTIC_DECORATIONS', ['GET_DECORATION']); // this should probable be in a util file somewhere, with the ability to bind an enconding to it const parameterRegExp = /#\{([^{}]*)\}/g; export function stringSupplant(str: string, map: Record) { return str.replace(parameterRegExp, (_, name) => { + // istanbul ignore next : Will test in new file const value = map[name]; + // istanbul ignore next : Will test in new file return value == null ? '' : `${value}`; }); } // TODO new home export const getDecorationSchema = _memoize((id: string): TPathAgnosticDecorationSchema | undefined => { - const schemas = getConfigValue('pathAgnosticDecorations') as TPathAgnosticDecorationSchema[]; + const schemas = getConfigValue('pathAgnosticDecorations') as TPathAgnosticDecorationSchema[] | undefined; if (!schemas) return undefined; return schemas.find(s => s.id === id); }); @@ -50,10 +49,27 @@ let pendingData: undefined | TNewData; let pendingPromise: undefined | Promise; let resolve: undefined | ((arg: TNewData) => void); -export function getDecoration(id: string, service: string, operation?: string) { +// Bespoke memoization-adjacent solution necessary as this should return `undefined`, not an old promise, on +// duplicate calls +export const processed = new Map>>(); + +export function getDecoration( + id: string, + service: string, + operation?: string +): Promise | undefined { + const processedID = processed.get(id); + if (!processedID) { + processed.set(id, new Map>([[service, new Set([operation])]])); + } else { + const processedService = processedID.get(service); + if (!processedService) processedID.set(service, new Set([operation])); + else if (!processedService.has(operation)) processedService.add(operation); + else return undefined; + } + const schema = getDecorationSchema(id); - console.log(schema); - if (!schema) return; + if (!schema) return undefined; const returnPromise = !resolve || !pendingPromise; if (returnPromise) { @@ -69,41 +85,39 @@ export function getDecoration(id: string, service: string, operation?: string) { let setPath: string; if (opValuePath && opUrl && operation) { promise = JaegerAPI.fetchDecoration(stringSupplant(opUrl, { service, operation })); - // const arbitraryNum = operation.length + service.length; - getPath = stringSupplant(opValuePath, ({ service, operation })); - // getPath = opValuePath; - setPath = `withoutOp.${service}.${operation}`; - // promise = new Promise(res => setTimeout(() => res({ opVal: arbitraryNum }), arbitraryNum * 100)); - // .then(res => _get(res, getPath, `${getPath} not found in response`)); + getPath = stringSupplant(opValuePath, { service, operation }); + setPath = `${id}.withOp.${service}.${operation}`; } else { - console.log(schema, url); promise = JaegerAPI.fetchDecoration(stringSupplant(url, { service })); - // const arbitraryNum = service.length; - getPath = stringSupplant(valuePath, ({ service })); + getPath = stringSupplant(valuePath, { service }); getPath = valuePath; - setPath = `withOp.${service}`; - // promise = new Promise(res => setTimeout(() => res({ val: arbitraryNum }), arbitraryNum * 100)); - // .then(res => _get(res, getPath, `${getPath} not found in response`)); + setPath = `${id}.withoutOp.${service}`; } - promise.then(res => { - return _get(res, getPath, `${getPath} not found in response`); - }).catch(err => { - return `Unable to fetch decoration: ${err.message || err}`; - }).then(value => { - if (!pendingData) pendingData = {}; - _set(pendingData, setPath, value); - doneCount = doneCount ? doneCount + 1 : 1; - - if (doneCount === pendingCount) { - if (resolve) resolve(pendingData); - else throw new Error('`resolve` unexpectedly undefined'); - - doneCount = pendingCount = pendingData = pendingPromise = resolve = undefined; - }; - }); + promise + .then(res => { + return _get(res, getPath, `${getPath} not found in response`); + }) + .catch(err => { + return `Unable to fetch decoration: ${err.message || err}`; + }) + .then(value => { + if (!pendingData) pendingData = {}; + _set(pendingData, setPath, value); + doneCount = doneCount ? doneCount + 1 : 1; + + if (doneCount === pendingCount) { + if (resolve) resolve(pendingData); + // istanbul ignore next : Unreachable error to appease TS, resolve made to exist at top at function + else throw new Error('`resolve` unexpectedly undefined'); + + // eslint-disable-next-line no-multi-assign + doneCount = pendingCount = pendingData = pendingPromise = resolve = undefined; + } + }); if (returnPromise) return pendingPromise; + return undefined; } const fullActions = createActions | undefined>({ diff --git a/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx index 03e757c547..0993f44018 100644 --- a/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx +++ b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx @@ -35,7 +35,10 @@ export type TPadEntry = { // renderData: unknown; }; -export type TNewData = Record; - withOp?: Record>; -}>; +export type TNewData = Record< + string, + { + withoutOp?: Record; + withOp?: Record>; + } +>; diff --git a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js index df197c33d3..4624b7832a 100644 --- a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js +++ b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js @@ -35,7 +35,7 @@ export function genStateAndPayload(_id, _service, _operation, value) { const operation = op[_operation]; const service = svc[_service]; - const isWithOp = typeof operation === 'number'; + const isWithOp = Boolean(operation); const valueObj = { value, }; @@ -45,10 +45,10 @@ export function genStateAndPayload(_id, _service, _operation, value) { [id]: { [payloadKey]: { [service]: isWithOp - ? { - [operation]: valueObj, - } - : valueObj, + ? { + [operation]: valueObj, + } + : valueObj, }, }, }; @@ -103,19 +103,27 @@ describe('pathAgnosticDecoration reducers', () => { }); it('adds service decoration to state with different decoration', () => { - expect(getDecorationDone(state00u0, payload10u1)).toEqual(mergeAndObjectContaining(state00u0, state10u1)); + expect(getDecorationDone(state00u0, payload10u1)).toEqual( + mergeAndObjectContaining(state00u0, state10u1) + ); }); it('adds service decoration to state with existing decoration and updates withoutOpMax', () => { - expect(getDecorationDone(state00u0, payload01u1)).toEqual(mergeAndObjectContaining(state00u0, state01u1)); + expect(getDecorationDone(state00u0, payload01u1)).toEqual( + mergeAndObjectContaining(state00u0, state01u1) + ); }); it('adds service decoration to state with existing decoration without overriding higher withoutOpMax', () => { - expect(getDecorationDone(state01u1, payload00u0)).toEqual(mergeAndObjectContaining(state00u0, state01u1)); + expect(getDecorationDone(state01u1, payload00u0)).toEqual( + mergeAndObjectContaining(state00u0, state01u1) + ); }); it('adds service decoration error to state with existing decoration without overriding existing withoutOpMax', () => { - expect(getDecorationDone(state01u1, payload00us)).toEqual(mergeAndObjectContaining(state00us, state01u1)); + expect(getDecorationDone(state01u1, payload00us)).toEqual( + mergeAndObjectContaining(state00us, state01u1) + ); }); }); @@ -129,44 +137,67 @@ describe('pathAgnosticDecoration reducers', () => { }); it('adds operation decoration to state with different decoration', () => { - expect(getDecorationDone(state0000, payload1001)).toEqual(mergeAndObjectContaining(state0000, state1001)); + expect(getDecorationDone(state0000, payload1001)).toEqual( + mergeAndObjectContaining(state0000, state1001) + ); }); it('adds operation decoration to state with same decoration but different service and updates withOpMax', () => { - expect(getDecorationDone(state0000, payload0101)).toEqual(mergeAndObjectContaining(state0000, state0101)); + expect(getDecorationDone(state0000, payload0101)).toEqual( + mergeAndObjectContaining(state0000, state0101) + ); }); it('adds operation decoration to state with same decoration but different service without overriding higher withoutOpMax', () => { - expect(getDecorationDone(state0101, payload0000)).toEqual(mergeAndObjectContaining(state0000, state0101)); + expect(getDecorationDone(state0101, payload0000)).toEqual( + mergeAndObjectContaining(state0000, state0101) + ); }); it('adds operation decoration to state with existing decoration and same service and updates withoutOpMax', () => { - expect(getDecorationDone(state0000, payload0011)).toEqual(mergeAndObjectContaining(state0000, state0011)); + expect(getDecorationDone(state0000, payload0011)).toEqual( + mergeAndObjectContaining(state0000, state0011) + ); }); it('adds operation decoration to state with existing decoration and same service without overriding higher withoutOpMax', () => { - expect(getDecorationDone(state0011, payload0000)).toEqual(mergeAndObjectContaining(state0000, state0011)); + expect(getDecorationDone(state0011, payload0000)).toEqual( + mergeAndObjectContaining(state0000, state0011) + ); }); it('adds operation decoration error to state with existing decoration without overriding existing withoutOpMax', () => { - expect(getDecorationDone(state0011, payload000s)).toEqual(mergeAndObjectContaining(state000s, state0011)); + expect(getDecorationDone(state0011, payload000s)).toEqual( + mergeAndObjectContaining(state000s, state0011) + ); }); }); describe('mixed', () => { it('adds service decoration to state with only operation decorations', () => { - expect(getDecorationDone(state0011, payload00u0)).toEqual(mergeAndObjectContaining(state00u0, state0011)); + expect(getDecorationDone(state0011, payload00u0)).toEqual( + mergeAndObjectContaining(state00u0, state0011) + ); }); it('adds operation decoration to state with only service decorations', () => { - expect(getDecorationDone(state00u1, payload0000)).toEqual(mergeAndObjectContaining(state0000, state00u1)); + expect(getDecorationDone(state00u1, payload0000)).toEqual( + mergeAndObjectContaining(state0000, state00u1) + ); }); it('adds multiple operation and service decortions to state with multiple operation and service decorations', () => { const initialState = _merge({}, state00us, state00u0, state1001); const payload = _merge({}, payload01u1, payload10u1, payload10u1, payload000s, payload0101); - const expectedState = mergeAndObjectContaining({}, state000s, initialState, state01u1, state10u1, state0101); - + const expectedState = mergeAndObjectContaining( + {}, + state000s, + initialState, + state01u1, + state10u1, + state0101 + ); + expect(getDecorationDone(initialState, payload)).toEqual(expectedState); }); }); diff --git a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx index 74c4b720a8..d639c25e22 100644 --- a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx +++ b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx @@ -52,10 +52,13 @@ export function getDecorationDone(state: TPathAgnosticDecorationsState, payload? ...newState, [decorationID]: { withOp: withOp - ? Object.keys(withOp).reduce((newWithOp, service) => ({ - ...newWithOp, - [service]: Object.assign({}, newWithOp[service], withOp[service]), - }), newState[decorationID].withOp || {}) + ? Object.keys(withOp).reduce( + (newWithOp, service) => ({ + ...newWithOp, + [service]: Object.assign({}, newWithOp[service], withOp[service]), + }), + newState[decorationID].withOp || {} + ) : newState[decorationID].withOp, withOpMax, withoutOp: withoutOp @@ -64,26 +67,26 @@ export function getDecorationDone(state: TPathAgnosticDecorationsState, payload? withoutOpMax, }, }; - - } else { - const withOpMax = Math.max(...newWithOpValues); - const withoutOpMax = Math.max(...newWithoutOpValues); - return { - ...newState, - [decorationID]: { - withOp, - withOpMax, - withoutOp, - withoutOpMax, - }, - }; } + const withOpMax = Math.max(...newWithOpValues); + const withoutOpMax = Math.max(...newWithoutOpValues); + return { + ...newState, + [decorationID]: { + withOp, + withOpMax, + withoutOp, + withoutOpMax, + }, + }; }, state); } export default handleActions( { - [actionTypes.GET_DECORATION]: guardReducer(getDecorationDone), + [actionTypes.GET_DECORATION]: guardReducer( + getDecorationDone + ), }, {} ); diff --git a/packages/jaeger-ui/src/types/TPathAgnosticDecorationsState.tsx b/packages/jaeger-ui/src/types/TPathAgnosticDecorationsState.tsx index 3320b7eec6..f9244f88de 100644 --- a/packages/jaeger-ui/src/types/TPathAgnosticDecorationsState.tsx +++ b/packages/jaeger-ui/src/types/TPathAgnosticDecorationsState.tsx @@ -14,12 +14,15 @@ import { TPadEntry } from '../model/path-agnostic-decorations/types'; -type TPathAgnosticDecorations = Record; - withOp?: Record>; -}>; +type TPathAgnosticDecorations = Record< + string, + { + withOpMax?: number; + withoutOpMax?: number; + withoutOp?: Record; + withOp?: Record>; + } +>; // eslint-disable-next-line no-undef export default TPathAgnosticDecorations; diff --git a/packages/jaeger-ui/src/types/config.tsx b/packages/jaeger-ui/src/types/config.tsx index fd0866e811..abd6b5e57c 100644 --- a/packages/jaeger-ui/src/types/config.tsx +++ b/packages/jaeger-ui/src/types/config.tsx @@ -41,7 +41,7 @@ export type LinkPatternsConfig = { export type Config = { archiveEnabled?: boolean; deepDependencies?: { - menuEnabled?:boolean; + menuEnabled?: boolean; }; dependencies?: { dagMaxServicesLen?: number; menuEnabled?: boolean }; menu: (ConfigMenuGroup | ConfigMenuItem)[]; From c6192478681685c22c257be04014544fb510d429 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Tue, 24 Mar 2020 15:45:18 -0400 Subject: [PATCH 07/31] Move and test stringSupplant Signed-off-by: Everett Ross --- .../actions/path-agnostic-decorations.test.js | 6 +- .../src/actions/path-agnostic-decorations.tsx | 16 +----- .../jaeger-ui/src/model/link-patterns.tsx | 22 +------- .../reducers/path-agnostic-decorations.tsx | 1 - .../src/utils/stringSupplant.test.js | 56 +++++++++++++++++++ .../jaeger-ui/src/utils/stringSupplant.tsx | 40 +++++++++++++ 6 files changed, 104 insertions(+), 37 deletions(-) create mode 100644 packages/jaeger-ui/src/utils/stringSupplant.test.js create mode 100644 packages/jaeger-ui/src/utils/stringSupplant.tsx diff --git a/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js b/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js index f60a77675e..60ca164011 100644 --- a/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js +++ b/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js @@ -14,10 +14,10 @@ import _set from 'lodash/set'; -import JaegerAPI from '../api/jaeger'; - +import { processed, getDecoration as getDecorationImpl } from './path-agnostic-decorations'; import * as getConfig from '../utils/config/get-config'; -import { processed, getDecoration as getDecorationImpl, stringSupplant } from './path-agnostic-decorations'; +import stringSupplant from '../utils/stringSupplant'; +import JaegerAPI from '../api/jaeger'; describe('getDecoration', () => { let getConfigValueSpy; diff --git a/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx b/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx index 6faf9952f9..5b09d616d1 100644 --- a/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx +++ b/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx @@ -21,23 +21,11 @@ import JaegerAPI from '../api/jaeger'; import { TNewData, TPathAgnosticDecorationSchema } from '../model/path-agnostic-decorations/types'; import { getConfigValue } from '../utils/config/get-config'; import generateActionTypes from '../utils/generate-action-types'; +import stringSupplant from '../utils/stringSupplant'; export const actionTypes = generateActionTypes('@jaeger-ui/PATH_AGNOSTIC_DECORATIONS', ['GET_DECORATION']); -// this should probable be in a util file somewhere, with the ability to bind an enconding to it -const parameterRegExp = /#\{([^{}]*)\}/g; - -export function stringSupplant(str: string, map: Record) { - return str.replace(parameterRegExp, (_, name) => { - // istanbul ignore next : Will test in new file - const value = map[name]; - // istanbul ignore next : Will test in new file - return value == null ? '' : `${value}`; - }); -} - -// TODO new home -export const getDecorationSchema = _memoize((id: string): TPathAgnosticDecorationSchema | undefined => { +const getDecorationSchema = _memoize((id: string): TPathAgnosticDecorationSchema | undefined => { const schemas = getConfigValue('pathAgnosticDecorations') as TPathAgnosticDecorationSchema[] | undefined; if (!schemas) return undefined; return schemas.find(s => s.id === id); diff --git a/packages/jaeger-ui/src/model/link-patterns.tsx b/packages/jaeger-ui/src/model/link-patterns.tsx index 8ab8975ea5..60f76011cb 100644 --- a/packages/jaeger-ui/src/model/link-patterns.tsx +++ b/packages/jaeger-ui/src/model/link-patterns.tsx @@ -14,13 +14,13 @@ import _uniq from 'lodash/uniq'; import memoize from 'lru-memoize'; + import { getConfigValue } from '../utils/config/get-config'; +import { encodedStringSupplant, getParamNames } from '../utils/stringSupplant'; import { getParent } from './span'; import { TNil } from '../types'; import { Span, Link, KeyValuePair, Trace } from '../types/trace'; -const parameterRegExp = /#\{([^{}]*)\}/g; - type ProcessedTemplate = { parameters: string[]; template: (template: { [key: string]: any }) => string; @@ -38,22 +38,6 @@ type ProcessedLinkPattern = { type TLinksRV = { url: string; text: string }[]; -function getParamNames(str: string) { - const names = new Set(); - str.replace(parameterRegExp, (match, name) => { - names.add(name); - return match; - }); - return Array.from(names); -} - -function stringSupplant(str: string, encodeFn: (unencoded: any) => string, map: Record) { - return str.replace(parameterRegExp, (_, name) => { - const value = map[name]; - return value == null ? '' : encodeFn(value); - }); -} - export function processTemplate(template: any, encodeFn: (unencoded: any) => string): ProcessedTemplate { if (typeof template !== 'string') { /* @@ -68,7 +52,7 @@ export function processTemplate(template: any, encodeFn: (unencoded: any) => str } return { parameters: getParamNames(template), - template: stringSupplant.bind(null, template, encodeFn), + template: encodedStringSupplant.bind(null, template, encodeFn), }; } diff --git a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx index d639c25e22..12f4e6cf52 100644 --- a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx +++ b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx @@ -15,7 +15,6 @@ import { handleActions } from 'redux-actions'; import { actionTypes } from '../actions/path-agnostic-decorations'; - import { TNewData } from '../model/path-agnostic-decorations/types'; import TPathAgnosticDecorationsState from '../types/TPathAgnosticDecorationsState'; import guardReducer from '../utils/guardReducer'; diff --git a/packages/jaeger-ui/src/utils/stringSupplant.test.js b/packages/jaeger-ui/src/utils/stringSupplant.test.js new file mode 100644 index 0000000000..744dcb39df --- /dev/null +++ b/packages/jaeger-ui/src/utils/stringSupplant.test.js @@ -0,0 +1,56 @@ +// Copyright (c) 2020 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import stringSupplant, { encodedStringSupplant, getParamNames } from './stringSupplant'; + +describe('stringSupplant', () => { + const value0 = 'val0'; + const value1 = 'val1'; + + it('replaces values', () => { + expect(stringSupplant('key0: #{value0}; key1: #{value1}', { value0, value1 })).toBe( + `key0: ${value0}; key1: ${value1}` + ); + }); + + it('omits missing values', () => { + expect(stringSupplant('key0: #{value0}; key1: #{value1}', { value0 })).toBe(`key0: ${value0}; key1: `); + }); + + describe('encodedStringSupplant', () => { + it('encodes present values', () => { + const reverse = str => + str + .split('') + .reverse() + .join(''); + const encodeFn = jest.fn(reverse); + expect(encodedStringSupplant('key0: #{value0}; key1: #{value1}', encodeFn, { value0, value1 })).toBe( + `key0: ${reverse(value0)}; key1: ${reverse(value1)}` + ); + + const callCount = encodeFn.mock.calls.length; + encodedStringSupplant('key0: #{value0}; key1: #{value1}', encodeFn, { value0 }); + expect(encodeFn.mock.calls.length).toBe(callCount + 1); + }); + }); +}); + +describe('getParamNames', () => { + it('gets unique names', () => { + const name0 = 'name 0'; + const name1 = 'name 1'; + expect(getParamNames(`foo #{${name0}} bar #{${name1}} baz #{${name0}}`)).toEqual([name0, name1]); + }); +}); diff --git a/packages/jaeger-ui/src/utils/stringSupplant.tsx b/packages/jaeger-ui/src/utils/stringSupplant.tsx new file mode 100644 index 0000000000..8b1ae7b23f --- /dev/null +++ b/packages/jaeger-ui/src/utils/stringSupplant.tsx @@ -0,0 +1,40 @@ +// Copyright (c) 2020 The Jaeger Authors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +const PARAMETER_REG_EXP = /#\{([^{}]*)\}/g; + +export function encodedStringSupplant( + str: string, + encodeFn: null | ((unencoded: string | number) => string), + map: Record +) { + return str.replace(PARAMETER_REG_EXP, (_, name) => { + const mapValue = map[name]; + const value = mapValue != null && encodeFn ? encodeFn(mapValue) : mapValue; + return value == null ? '' : `${value}`; + }); +} + +export default function stringSupplant(str: string, map: Record) { + return encodedStringSupplant(str, null, map); +} + +export function getParamNames(str: string) { + const names = new Set(); + str.replace(PARAMETER_REG_EXP, (match, name) => { + names.add(name); + return match; + }); + return Array.from(names); +} From 74bc7e058bcd66498abd0a956738b5dd0086640a Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Tue, 24 Mar 2020 16:11:56 -0400 Subject: [PATCH 08/31] Cleanup Signed-off-by: Everett Ross --- .../path-agnostic-decorations.test.js | 85 +++++++++---------- packages/jaeger-ui/src/types/TDdgState.tsx | 10 --- 2 files changed, 42 insertions(+), 53 deletions(-) diff --git a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js index 4624b7832a..f870fd4fa1 100644 --- a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js +++ b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js @@ -16,54 +16,53 @@ import _merge from 'lodash/merge'; import { getDecorationDone } from './path-agnostic-decorations'; -const decorationID = { - 0: 'decoration id 0', - 1: 'decoration id 1', -}; -const op = { - 0: 'op 0', - 1: 'op 1', -}; -const svc = { - 0: 'svc 0', - 1: 'svc 1', -}; - -// eslint-disable-next-line import/prefer-default-export -export function genStateAndPayload(_id, _service, _operation, value) { - const id = decorationID[_id]; - const operation = op[_operation]; - const service = svc[_service]; - - const isWithOp = Boolean(operation); - const valueObj = { - value, +describe('pathAgnosticDecoration reducers', () => { + const decorationID = { + 0: 'decoration id 0', + 1: 'decoration id 1', + }; + const op = { + 0: 'op 0', + 1: 'op 1', + }; + const svc = { + 0: 'svc 0', + 1: 'svc 1', }; - const payloadKey = isWithOp ? 'withOp' : 'withoutOp'; - const payload = { - [id]: { - [payloadKey]: { - [service]: isWithOp - ? { - [operation]: valueObj, - } - : valueObj, + function genStateAndPayload(_id, _service, _operation, value) { + const id = decorationID[_id]; + const operation = op[_operation]; + const service = svc[_service]; + + const isWithOp = Boolean(operation); + const valueObj = { + value, + }; + + const payloadKey = isWithOp ? 'withOp' : 'withoutOp'; + const payload = { + [id]: { + [payloadKey]: { + [service]: isWithOp + ? { + [operation]: valueObj, + } + : valueObj, + }, }, - }, - }; + }; - const stateKey = isWithOp ? 'withOpMax' : 'withoutOpMax'; - const state = { - [id]: { - ...payload[id], - [stateKey]: typeof value === 'number' ? value : -Infinity, - }, - }; - return { id, operation, service, payload, state }; -} + const stateKey = isWithOp ? 'withOpMax' : 'withoutOpMax'; + const state = { + [id]: { + ...payload[id], + [stateKey]: typeof value === 'number' ? value : -Infinity, + }, + }; + return { id, operation, service, payload, state }; + } -describe('pathAgnosticDecoration reducers', () => { // variable names are type(payload|state)+decoration(0|1)+service(0|1)+operation(0|1|u)+value(0|1|s) // u for undefined, or opless // s for string, or errored request diff --git a/packages/jaeger-ui/src/types/TDdgState.tsx b/packages/jaeger-ui/src/types/TDdgState.tsx index 4c3953c0ed..4540714a79 100644 --- a/packages/jaeger-ui/src/types/TDdgState.tsx +++ b/packages/jaeger-ui/src/types/TDdgState.tsx @@ -30,16 +30,6 @@ export type TDdgStateEntry = viewModifiers: Map; }; -/* -type TDdgState = { - entries: Record; - pathAgnosticDecorations: Record; - }>; -}; - */ type TDdgState = Record; // eslint-disable-next-line no-undef From a42355050251b80de28b401b0da82e1d1b959182 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Fri, 27 Mar 2020 18:25:16 -0400 Subject: [PATCH 09/31] WIP: Decorate nodes, selector/detail side panel Signed-off-by: Everett Ross --- packages/jaeger-ui/package.json | 1 + .../src/actions/path-agnostic-decorations.tsx | 4 +- packages/jaeger-ui/src/api/jaeger.js | 10 +- .../Graph/DdgNodeContent/constants.tsx | 1 + .../Graph/DdgNodeContent/index.tsx | 195 ++++++++++++------ .../DeepDependencies/Graph/index.tsx | 9 +- .../SidePanel/DetailsPanel.tsx | 60 ++++++ .../DeepDependencies/SidePanel/index.tsx | 120 +++++++++++ .../src/components/DeepDependencies/index.css | 6 + .../src/components/DeepDependencies/index.tsx | 65 ++++-- .../src/constants/default-config.tsx | 22 ++ .../model/path-agnostic-decorations/index.tsx | 57 +++++ .../model/path-agnostic-decorations/types.tsx | 4 +- packages/jaeger-ui/src/reducers/index.js | 2 + .../reducers/path-agnostic-decorations.tsx | 2 +- yarn.lock | 27 ++- 16 files changed, 486 insertions(+), 99 deletions(-) create mode 100644 packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx create mode 100644 packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx create mode 100644 packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx diff --git a/packages/jaeger-ui/package.json b/packages/jaeger-ui/package.json index 9c9f6ed587..6935b7ac4b 100644 --- a/packages/jaeger-ui/package.json +++ b/packages/jaeger-ui/package.json @@ -78,6 +78,7 @@ "query-string": "^6.3.0", "raven-js": "^3.22.1", "react": "^16.3.2", + "react-circular-progressbar": "^2.0.3", "react-dimensions": "^1.3.0", "react-dom": "^16.3.2", "react-ga": "^2.4.1", diff --git a/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx b/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx index 5b09d616d1..f968736754 100644 --- a/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx +++ b/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx @@ -74,12 +74,12 @@ export function getDecoration( if (opValuePath && opUrl && operation) { promise = JaegerAPI.fetchDecoration(stringSupplant(opUrl, { service, operation })); getPath = stringSupplant(opValuePath, { service, operation }); - setPath = `${id}.withOp.${service}.${operation}`; + setPath = `${id}.withOp.${service}.${operation}.value`; } else { promise = JaegerAPI.fetchDecoration(stringSupplant(url, { service })); getPath = stringSupplant(valuePath, { service }); getPath = valuePath; - setPath = `${id}.withoutOp.${service}`; + setPath = `${id}.withoutOp.${service}.value`; } promise diff --git a/packages/jaeger-ui/src/api/jaeger.js b/packages/jaeger-ui/src/api/jaeger.js index daaec18525..9e37bed835 100644 --- a/packages/jaeger-ui/src/api/jaeger.js +++ b/packages/jaeger-ui/src/api/jaeger.js @@ -81,7 +81,15 @@ const JaegerAPI = { return getJSON(`${this.apiRoot}archive/${id}`, { method: 'POST' }); }, fetchDecoration(url) { - return getJSON(url); + // return getJSON(url); + if (url.startsWith('neapolitan')) { + return new Promise((res, rej) => setTimeout(() => { + if (url.length % 4) res({ val: url.length ** 2 }); + else if (url.length % 2) res('No val here'); + else rej(new Error(`One of the unlucky third: ${url.length}`)); + }, 150)); + } + return new Promise(res => setTimeout(() => res({ val: url.length ** 2 }), 150)); }, fetchDeepDependencyGraph(query) { return getJSON(`${ANALYTICS_ROOT}v1/dependencies`, { query }); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/constants.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/constants.tsx index 2f733fd272..9583fc70bb 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/constants.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/constants.tsx @@ -19,6 +19,7 @@ export const FONT_SIZE = 14; export const FONT = `${FONT_SIZE}px Helvetica Nueue`; export const LINE_HEIGHT = 1.5; export const OP_PADDING_TOP = 5; +export const PROGRESS_BAR_STROKE_WIDTH = 15; export const RADIUS = 75; export const WORD_RX = /\W*\w+\W*/g; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx index e901a5d88e..5567c7df5b 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx @@ -16,8 +16,14 @@ import * as React from 'react'; import { Checkbox, Popover } from 'antd'; import cx from 'classnames'; import { TLayoutVertex } from '@jaegertracing/plexus/lib/types'; +import { CircularProgressbar } from 'react-circular-progressbar'; import IoAndroidLocate from 'react-icons/lib/io/android-locate'; import MdVisibilityOff from 'react-icons/lib/md/visibility-off'; +import { connect } from 'react-redux'; +import { RouteComponentProps, withRouter } from 'react-router-dom'; +import { bindActionCreators, Dispatch } from 'redux'; + +import 'react-circular-progressbar/dist/styles.css'; import calcPositioning from './calc-positioning'; import { @@ -26,6 +32,7 @@ import { MIN_LENGTH, OP_PADDING_TOP, PARAM_NAME_LENGTH, + PROGRESS_BAR_STROKE_WIDTH, RADIUS, WORD_RX, } from './constants'; @@ -36,6 +43,7 @@ import BreakableText from '../../../common/BreakableText'; import FilteredList from '../../../common/FilteredList'; import NewWindowIcon from '../../../common/NewWindowIcon'; import { getUrl as getSearchUrl } from '../../../SearchTracePage/url'; +import padActions from '../../../../actions/path-agnostic-decorations'; import { ECheckedStatus, EDdgDensity, @@ -44,10 +52,16 @@ import { TDdgVertex, PathElem, } from '../../../../model/ddg/types'; +import extractDecorationFromState, { TDecorationFromState } from '../../../../model/path-agnostic-decorations'; +import { ReduxState } from '../../../../types/index'; import './index.css'; -type TProps = { +type TDispatchProps = { + getDecoration: (id: string, svc: string, op?: string) => void; +}; + +type TProps = RouteComponentProps & TDispatchProps & TDecorationFromState & { focalNodeUrl: string | null; focusPathsThroughVertex: (vertexKey: string) => void; getGenerationVisibility: (vertexKey: string, direction: EDirection) => ECheckedStatus | null; @@ -56,79 +70,98 @@ type TProps = { isFocalNode: boolean; isPositioned: boolean; operation: string | string[] | null; + selectVertex: (selectedVertex: TDdgVertex) => void; service: string; setOperation: (operation: string) => void; setViewModifier: (visIndices: number[], viewModifier: EViewModifier, isEnabled: boolean) => void; updateGenerationVisibility: (vertexKey: string, direction: EDirection) => void; + vertex: TDdgVertex; vertexKey: string; -}; +} /* & { + history: RouterHistory; + location: Location; + match: any; +}*/; type TState = { childrenVisibility?: ECheckedStatus | null; parentVisibility?: ECheckedStatus | null; +} + +export function getNodeRenderer({ + baseUrl, + density, + extraUrlArgs, + focusPathsThroughVertex, + getGenerationVisibility, + getVisiblePathElems, + hideVertex, + selectVertex, + setOperation, + setViewModifier, + updateGenerationVisibility, +}: { + baseUrl: string; + density: EDdgDensity; + extraUrlArgs?: { [key: string]: unknown }; + focusPathsThroughVertex: (vertexKey: string) => void; + getGenerationVisibility: (vertexKey: string, direction: EDirection) => ECheckedStatus | null; + getVisiblePathElems: (vertexKey: string) => PathElem[] | undefined; + hideVertex: (vertexKey: string) => void; + selectVertex: (selectedVertex: TDdgVertex) => void; + setOperation: (operation: string) => void; + setViewModifier: (visIndices: number[], viewModifier: EViewModifier, enable: boolean) => void; + updateGenerationVisibility: (vertexKey: string, direction: EDirection) => void; +}) { + return function renderNode(vertex: TDdgVertex, _: unknown, lv: TLayoutVertex | null) { + const { isFocalNode, key, operation, service } = vertex; + return ( + + ); + }; }; -export default class DdgNodeContent extends React.PureComponent { +export function measureNode() { + const diameter = 2 * (RADIUS + 1); + + return { + height: diameter, + width: diameter, + }; +} + +export class UnconnectedDdgNodeContent extends React.PureComponent { state: TState = {}; + hoveredIndices: Set = new Set(); - static measureNode() { - const diameter = 2 * (RADIUS + 1); + constructor(props: TProps) { + super(props); - return { - height: diameter, - width: diameter, - }; + this.getDecoration(); } - static getNodeRenderer({ - baseUrl, - density, - extraUrlArgs, - focusPathsThroughVertex, - getGenerationVisibility, - getVisiblePathElems, - hideVertex, - setOperation, - setViewModifier, - updateGenerationVisibility, - }: { - baseUrl: string; - density: EDdgDensity; - extraUrlArgs?: { [key: string]: unknown }; - focusPathsThroughVertex: (vertexKey: string) => void; - getGenerationVisibility: (vertexKey: string, direction: EDirection) => ECheckedStatus | null; - getVisiblePathElems: (vertexKey: string) => PathElem[] | undefined; - hideVertex: (vertexKey: string) => void; - setOperation: (operation: string) => void; - setViewModifier: (visIndices: number[], viewModifier: EViewModifier, enable: boolean) => void; - updateGenerationVisibility: (vertexKey: string, direction: EDirection) => void; - }) { - return function renderNode(vertex: TDdgVertex, _: unknown, lv: TLayoutVertex | null) { - const { isFocalNode, key, operation, service } = vertex; - return ( - - ); - }; + componentDidUpdate(prevProps: TProps) { + if (prevProps.decorationID !== this.props.decorationID) this.getDecoration(); } - hoveredIndices: Set = new Set(); - componentWillUnmount() { if (this.hoveredIndices.size) { this.props.setViewModifier(Array.from(this.hoveredIndices), EViewModifier.Hovered, false); @@ -136,11 +169,22 @@ export default class DdgNodeContent extends React.PureComponent } } + private getDecoration() { + const { decorationID, getDecoration, operation, service } = this.props; + + if (decorationID) getDecoration(decorationID, service, typeof operation === 'string' ? operation : undefined); + } + private focusPaths = () => { const { focusPathsThroughVertex, vertexKey } = this.props; focusPathsThroughVertex(vertexKey); }; + private handleClick = () => { + const { selectVertex, vertex } = this.props; + selectVertex(vertex); + } + private hideVertex = () => { const { hideVertex, vertexKey } = this.props; hideVertex(vertexKey); @@ -214,20 +258,38 @@ export default class DdgNodeContent extends React.PureComponent render() { const { childrenVisibility, parentVisibility } = this.state; - const { focalNodeUrl, isFocalNode, isPositioned, operation, service } = this.props; + const { decorationColor, decorationMax, decorationValue, focalNodeUrl, isFocalNode, isPositioned, operation, service } = this.props; const { radius, svcWidth, opWidth, svcMarginTop } = calcPositioning(service, operation); - const scaleFactor = RADIUS / radius; + const trueRadius = typeof decorationValue === 'number' ? RADIUS - PROGRESS_BAR_STROKE_WIDTH : RADIUS; + const scaleFactor = trueRadius / radius; const transform = `translate(${RADIUS - radius}px, ${RADIUS - radius}px) scale(${scaleFactor})`; + let backgroundColor: string | undefined; + if (typeof decorationValue === 'string') { + backgroundColor = 'lightblue'; + } + return (
+ {typeof decorationValue === 'number' && }

); } } + +export function mapDispatchToProps(dispatch: Dispatch): TDispatchProps { + const { getDecoration } = bindActionCreators( + padActions, + dispatch + ); + + return { + getDecoration, + }; +} + +const DdgNodeContent = withRouter(connect(extractDecorationFromState, mapDispatchToProps)(UnconnectedDdgNodeContent)); + +export default DdgNodeContent; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx index 1269737189..b4681a4802 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx @@ -19,7 +19,7 @@ import { TSetProps, TFromGraphStateFn, TDefEntry } from '@jaegertracing/plexus/l import { TEdge } from '@jaegertracing/plexus/lib/types'; import TNonEmptyArray from '@jaegertracing/plexus/lib/types/TNonEmptyArray'; -import DdgNodeContent from './DdgNodeContent'; +import { getNodeRenderer, measureNode } from './DdgNodeContent'; import getNodeRenderers from './getNodeRenderers'; import getSetOnEdge from './getSetOnEdge'; import { @@ -43,6 +43,7 @@ type TProps = { getGenerationVisibility: (vertexKey: string, direction: EDirection) => ECheckedStatus | null; getVisiblePathElems: (vertexKey: string) => PathElem[] | undefined; hideVertex: (vertexKey: string) => void; + selectVertex: (selectedVertex: TDdgVertex) => void; setOperation: (operation: string) => void; setViewModifier: (visIndices: number[], viewModifier: EViewModifier, enable: boolean) => void; uiFindMatches: Set | undefined; @@ -73,7 +74,7 @@ const edgesDefs: TNonEmptyArray> = [ export default class Graph extends PureComponent { private getNodeRenderers = memoize(getNodeRenderers); - private getNodeContentRenderer = memoize(DdgNodeContent.getNodeRenderer); + private getNodeContentRenderer = memoize(getNodeRenderer); private getSetOnEdge = memoize(getSetOnEdge); private layoutManager: LayoutManager = new LayoutManager({ @@ -102,6 +103,7 @@ export default class Graph extends PureComponent { getGenerationVisibility, getVisiblePathElems, hideVertex, + selectVertex, setOperation, setViewModifier, uiFindMatches, @@ -154,7 +156,7 @@ export default class Graph extends PureComponent { key: 'nodes/content', layerType: 'html', measurable: true, - measureNode: DdgNodeContent.measureNode, + measureNode: measureNode, renderNode: this.getNodeContentRenderer({ baseUrl, density, @@ -163,6 +165,7 @@ export default class Graph extends PureComponent { getGenerationVisibility, getVisiblePathElems, hideVertex, + selectVertex, setOperation, setViewModifier, updateGenerationVisibility, diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx new file mode 100644 index 0000000000..bdcd827bf1 --- /dev/null +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx @@ -0,0 +1,60 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react'; +import { CircularProgressbar } from 'react-circular-progressbar'; +import { connect } from 'react-redux'; + +import 'react-circular-progressbar/dist/styles.css'; + +import extractDecorationFromState, { TDecorationFromState } from '../../../model/path-agnostic-decorations'; +import { TPathAgnosticDecorationSchema } from '../../../model/path-agnostic-decorations/types'; + +type TProps = TDecorationFromState & { + decorationSchema: TPathAgnosticDecorationSchema; + service: string; + operation?: string | string[] | null; +}; + +export class UnconnectedDetailsPanel extends React.PureComponent { + render() { + const { decorationColor, decorationMax, decorationValue, operation, service } = this.props; + return ( +
+
+ {service}{operation && ( + ::{operation} + )} +
+ {typeof decorationValue === 'number' && typeof decorationMax === 'number' && } +
+ ) + } +} + +export default connect(extractDecorationFromState)(UnconnectedDetailsPanel); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx new file mode 100644 index 0000000000..99380b633f --- /dev/null +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx @@ -0,0 +1,120 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react'; +import { CircularProgressbar } from 'react-circular-progressbar'; +import 'react-circular-progressbar/dist/styles.css'; + +import { + TDdgVertex, +} from '../../../model/ddg/types'; +import { TPathAgnosticDecorationSchema } from '../../../model/path-agnostic-decorations/types'; +import { getConfigValue } from '../../../utils/config/get-config'; +import DetailsPanel from './DetailsPanel'; + + +type TProps = { + clearSelected: () => void; + selectDecoration: (decoration?: string) => void; + selectedDecoration?: string; + selectedVertex?: TDdgVertex; +}; + +type TState = { + collapsed: boolean; +}; + +export default class SidePanel extends React.PureComponent { + state = { + collapsed: false, + }; + + decorations: TPathAgnosticDecorationSchema[] | undefined; + + constructor(props: TProps) { + super(props); + + this.decorations = getConfigValue('pathAgnosticDecorations'); + } + + render() { + if (!this.decorations) return null; + + const { + clearSelected, + selectDecoration, + selectedDecoration, + selectedVertex, + } = this.props; + + const selectedSchema = this.decorations.find(({ id }) => id === selectedDecoration); + + return ( +
+
+ {this.decorations.map(({ acronym, id, name }) => ( + + ))} + +
+
+ {selectedVertex && selectedSchema && } +
+
+ ); + } +} + +/* +export function mapStateToProps(state: ReduxState, ownProps: TOwnProps): TReduxProps { + const { services: stServices } = state; + const { services, serverOpsForService } = stServices; + const urlState = getUrlState(ownProps.location.search); + const { density, operation, service, showOp: urlStateShowOp } = urlState; + const showOp = urlStateShowOp !== undefined ? urlStateShowOp : operation !== undefined; + let graphState: TDdgStateEntry | undefined; + if (service) { + graphState = _get(state.ddg, getStateEntryKey({ service, operation, start: 0, end: 0 })); + } + let graph: GraphModel | undefined; + if (graphState && graphState.state === fetchedState.DONE) { + graph = makeGraph(graphState.model, showOp, density); + } + return { + graph, + graphState, + serverOpsForService, + services, + showOp, + urlState: sanitizeUrlState(urlState, _get(graphState, 'model.hash')), + ...extractUiFindFromState(state), + }; +} +*/ diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.css b/packages/jaeger-ui/src/components/DeepDependencies/index.css index 2f99030ed9..c70e0d123d 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.css @@ -30,6 +30,12 @@ limitations under the License. } .Ddg--graphWrapper { + display: flex; flex: 1; + flex-direction: column; overflow: hidden; } + +.Ddg--graphWrapper.is-horizontal { + flex-direction: row; +} diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx index 380cf9dcb3..6823b7f18e 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx @@ -19,8 +19,9 @@ import { bindActionCreators, Dispatch } from 'redux'; import { connect } from 'react-redux'; import { trackClearOperation, trackFocusPaths, trackHide, trackSetService, trackShow } from './index.track'; -import Header from './Header'; import Graph from './Graph'; +import Header from './Header'; +import SidePanel from './SidePanel'; import { getUrl, getUrlState, sanitizeUrlState, ROUTE_PATH } from './url'; import ErrorMessage from '../common/ErrorMessage'; import LoadingIndicator from '../common/LoadingIndicator'; @@ -38,6 +39,7 @@ import { EViewModifier, TDdgModelParams, TDdgSparseUrlState, + TDdgVertex, } from '../../model/ddg/types'; import { encode, encodeDistance } from '../../model/ddg/visibility-codec'; import { getConfigValue } from '../../utils/config/get-config'; @@ -75,8 +77,12 @@ export type TOwnProps = { export type TProps = TDispatchProps & TReduxProps & TOwnProps; +type TState = { + selectedVertex?: TDdgVertex; +}; + // export for tests -export class DeepDependencyGraphPageImpl extends React.PureComponent { +export class DeepDependencyGraphPageImpl extends React.PureComponent { static defaultProps = { showSvcOpsHeader: true, baseUrl: ROUTE_PATH, @@ -207,6 +213,10 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { }); }; + selectVertex = (selectedVertex?: TDdgVertex) => { + this.setState({ selectedVertex }); + } + showVertices = (vertexKeys: string[]) => { const { graph, urlState } = this.props; const { visEncoding } = urlState; @@ -238,6 +248,7 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { }; render() { + const { selectedVertex = undefined } = this.state || {}; const { baseUrl, extraUrlArgs, @@ -257,6 +268,7 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { const hiddenUiFindMatches = graph && graph.getHiddenUiFindMatches(uiFind, visEncoding); let content: React.ReactElement | null = null; + let wrapperClassName: string = ''; if (!graphState) { content =

Enter query above

; } else if (graphState.state === fetchedState.DONE && graph) { @@ -267,26 +279,37 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { viewModifiers ); if (vertices.length > 1) { + const { density, showOp, service, operation, visEncoding } = urlState; + wrapperClassName = 'is-horizontal'; // TODO: using `key` here is a hack, debug digraph to fix the underlying issue content = ( - + <> + + + ); } else if ( graphState.model.distanceToPathElems.has(-1) || @@ -362,7 +385,7 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { visEncoding={visEncoding} />

-
{content}
+
{content}
); } diff --git a/packages/jaeger-ui/src/constants/default-config.tsx b/packages/jaeger-ui/src/constants/default-config.tsx index 198b84f9a3..5ff6fbd1a7 100644 --- a/packages/jaeger-ui/src/constants/default-config.tsx +++ b/packages/jaeger-ui/src/constants/default-config.tsx @@ -56,6 +56,28 @@ export default deepFreeze( ], }, ], + pathAgnosticDecorations: [{ + acronym: 'N', + id: 'n', + name: 'Neapolitan mix of success/error/no path', + url: 'neapolitan #{service}', + valuePath: 'val', + }, { + acronym: 'AR', + id: 'ar', + name: 'All should resolve', + url: 'all should res #{service}', + valuePath: 'val', + }, { + acronym: 'RDT', + id: 'rdt', + name: 'all should resolve details too #{service}', + url: 'details too #{service}', + detailPath: 'deets', + valuePath: 'val', + }/*, { + TODO: op example too + }*/], search: { maxLookback: { label: '2 Days', diff --git a/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx b/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx new file mode 100644 index 0000000000..c48a98dd7c --- /dev/null +++ b/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx @@ -0,0 +1,57 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import _get from 'lodash/get'; +import queryString from 'query-string'; + +import { ReduxState } from '../../types/index'; +import { + TDdgVertex, +} from '../ddg/types'; + +export type TDecorationFromState = { + decorationColor?: string; + decorationID?: string; + decorationValue?: string | number; + decorationMax?: number; +}; + +export default function extractDecorationFromState(state: ReduxState, { service, operation }: { service: string, operation?: string | string[] | null }): TDecorationFromState { + const { decoration } = queryString.parse(state.router.location.search); + const decorationID = Array.isArray(decoration) ? decoration[0] : decoration; + + if (!decorationID) return {}; + + let decorationValue = _get(state, `pathAgnosticDecorations.${decorationID}.withOp.${service}.${operation}.value`); + let decorationMax = _get(state, `pathAgnosticDecorations.${decorationID}.withOpMax`); + if (!decorationValue) { + decorationValue = _get(state, `pathAgnosticDecorations.${decorationID}.withoutOp.${service}.value`); + decorationMax = _get(state, `pathAgnosticDecorations.${decorationID}.withoutOpMax`); + } + + /* + const red = Math.ceil(decorationValue / decorationMax * 255); + const decorationColor = `rgb(${red}, 0, 0)` + */ + // const saturation = Math.ceil(Math.pow(decorationValue / decorationMax, 1 / 2) * 100); + // const decorationColor = `hsl(0, ${saturation}%, 50%)`; + const scale = Math.pow(decorationValue / decorationMax, 1 / 4); + const saturation = Math.ceil(scale * 100); + const light = 50 + Math.ceil((1 - scale) * 50); + const decorationColor = `hsl(0, ${saturation}%, ${light}%)`; + // const decorationColor = `hsl(0, 100%, ${light}%)`; + + + return { decorationColor, decorationID, decorationValue, decorationMax }; +} diff --git a/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx index 0993f44018..57bbbc7885 100644 --- a/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx +++ b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx @@ -13,9 +13,9 @@ // limitations under the License. export type TPathAgnosticDecorationSchema = { + acronym: string; id: string; - label: string; - icon?: string; + name: string; url: string; opUrl?: string; valuePath: string; diff --git a/packages/jaeger-ui/src/reducers/index.js b/packages/jaeger-ui/src/reducers/index.js index e37f6e41a7..7b23c2ab32 100644 --- a/packages/jaeger-ui/src/reducers/index.js +++ b/packages/jaeger-ui/src/reducers/index.js @@ -17,6 +17,7 @@ import { reducer as formReducer } from 'redux-form'; import config from './config'; import dependencies from './dependencies'; import ddg from './ddg'; +import pathAgnosticDecorations from './path-agnostic-decorations'; import embedded from './embedded'; import services from './services'; import trace from './trace'; @@ -26,6 +27,7 @@ export default { dependencies, ddg, embedded, + pathAgnosticDecorations, services, trace, form: formReducer, diff --git a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx index 12f4e6cf52..08216d8e4d 100644 --- a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx +++ b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx @@ -83,7 +83,7 @@ export function getDecorationDone(state: TPathAgnosticDecorationsState, payload? export default handleActions( { - [actionTypes.GET_DECORATION]: guardReducer( + [`${actionTypes.GET_DECORATION}_FULFILLED`]: guardReducer( getDecorationDone ), }, diff --git a/yarn.lock b/yarn.lock index 6dc3dbcc47..3fe19d2da8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1543,6 +1543,8 @@ version "1.3.0" resolved "https://registry.yarnpkg.com/@types%2fobject-hash/-/object-hash-1.3.0.tgz#b20db2074129f71829d61ff404e618c4ac3d73cf" integrity sha1-sg2yB0Ep9xgp1h/0BOYYxKw9c88= + dependencies: + "@types/node" "*" "@types/prop-types@*": version "15.7.0" @@ -5573,8 +5575,8 @@ fsevents@^1.2.3, fsevents@^1.2.7: fstream@1.0.12, fstream@^1.0.0, fstream@^1.0.12: version "1.0.12" - resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" - integrity sha512-WvJ193OHa0GHPEL+AycEJgxvBEwyfRkN1vhjca23OaPVMCaLCXTd5qAu82AjTcgP1UJmytkOKb63Ypde7raDIg== + resolved "https://unpm.uberinternal.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + integrity sha1-Touo7i1Ivk99DeUFRVVI6uWTIEU= dependencies: graceful-fs "^4.1.2" inherits "~2.0.0" @@ -5897,8 +5899,8 @@ handle-thing@^2.0.0: handlebars@4.1.2, handlebars@^4.0.3, handlebars@^4.1.0: version "4.1.2" - resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" - integrity sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw== + resolved "https://unpm.uberinternal.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" + integrity sha1-trN8HO0DBrIh4JT8eso+wjsTG2c= dependencies: neo-async "^2.6.0" optimist "^0.6.1" @@ -7235,8 +7237,8 @@ js-tokens@^3.0.2: js-yaml@3.13.1, js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0: version "3.13.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + resolved "https://unpm.uberinternal.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + integrity sha1-r/FRswv9+o5J4F2iLnQV6d+jeEc= dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -7793,8 +7795,8 @@ lodash.uniq@^4.5.0: lodash@4.17.11, lodash@4.x, "lodash@>=3.5 <5", lodash@^3.10.0, lodash@^4.15.0, lodash@^4.16.5, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: version "4.17.11" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" - integrity sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg== + resolved "https://unpm.uberinternal.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + integrity sha1-s56mIp72B+zYniyN8SU2iRysm40= logfmt@^1.2.0: version "1.2.1" @@ -10431,6 +10433,11 @@ react-app-rewired@2.0.1: dotenv "^6.2.0" semver "^5.6.0" +react-circular-progressbar@^2.0.3: + version "2.0.3" + resolved "https://unpm.uberinternal.com/react-circular-progressbar/-/react-circular-progressbar-2.0.3.tgz#fa8eb59f8db168d2904bae4590641792c80f5991" + integrity sha1-+o61n42xaNKQS65FkGQXksgPWZE= + react-dev-utils@^7.0.0: version "7.0.5" resolved "https://registry.yarnpkg.com/react-dev-utils/-/react-dev-utils-7.0.5.tgz#cb95375d01ae71ca27b3c7616006ef7a77d14e8e" @@ -12126,8 +12133,8 @@ tapable@^1.0.0, tapable@^1.1.0: tar@2.2.2, tar@^2.0.0: version "2.2.2" - resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" - integrity sha512-FCEhQ/4rE1zYv9rYXJw/msRqsnmlje5jHP6huWeBZ704jUTy02c5AZyWujpMR1ax6mVw9NyJMfuK2CMDWVIfgA== + resolved "https://unpm.uberinternal.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" + integrity sha1-DKiEhWLHKZuLRG/2pNYM27I+3EA= dependencies: block-stream "*" fstream "^1.0.12" From 4b2374e44d15521dbcb430a7a20e417795d9c969 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Mon, 30 Mar 2020 17:49:43 -0400 Subject: [PATCH 10/31] WIP: Style side panel Signed-off-by: Everett Ross --- packages/jaeger-ui/src/api/jaeger.js | 7 ++-- .../Graph/DdgNodeContent/index.tsx | 20 ++------- .../SidePanel/DetailsPanel.css | 42 +++++++++++++++++++ .../SidePanel/DetailsPanel.tsx | 30 ++++++------- .../DeepDependencies/SidePanel/index.css | 38 +++++++++++++++++ .../DeepDependencies/SidePanel/index.tsx | 6 ++- .../model/path-agnostic-decorations/index.tsx | 40 ++++++++++++++++-- 7 files changed, 139 insertions(+), 44 deletions(-) create mode 100644 packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css create mode 100644 packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.css diff --git a/packages/jaeger-ui/src/api/jaeger.js b/packages/jaeger-ui/src/api/jaeger.js index 9e37bed835..fb46dd5f03 100644 --- a/packages/jaeger-ui/src/api/jaeger.js +++ b/packages/jaeger-ui/src/api/jaeger.js @@ -82,11 +82,10 @@ const JaegerAPI = { }, fetchDecoration(url) { // return getJSON(url); - if (url.startsWith('neapolitan')) { + if (url.length % 2 && url.startsWith('neapolitan')) { return new Promise((res, rej) => setTimeout(() => { - if (url.length % 4) res({ val: url.length ** 2 }); - else if (url.length % 2) res('No val here'); - else rej(new Error(`One of the unlucky third: ${url.length}`)); + if (url.length % 4 === 1) res('No val here'); + else rej(new Error(`One of the unlucky quarter: ${url.length}`)); }, 150)); } return new Promise(res => setTimeout(() => res({ val: url.length ** 2 }), 150)); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx index 5567c7df5b..2b3fb8b780 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx @@ -77,11 +77,7 @@ type TProps = RouteComponentProps & TDispatchProps & TDecorationFromState & updateGenerationVisibility: (vertexKey: string, direction: EDirection) => void; vertex: TDdgVertex; vertexKey: string; -} /* & { - history: RouterHistory; - location: Location; - match: any; -}*/; +}; type TState = { childrenVisibility?: ECheckedStatus | null; @@ -258,7 +254,7 @@ export class UnconnectedDdgNodeContent extends React.PureComponent - {typeof decorationValue === 'number' && } + {decorationProgressbar}
span { + border-bottom: solid 1px #bbb; + /* box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.3); */ +} diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx index bdcd827bf1..599e4618fc 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx @@ -20,6 +20,10 @@ import 'react-circular-progressbar/dist/styles.css'; import extractDecorationFromState, { TDecorationFromState } from '../../../model/path-agnostic-decorations'; import { TPathAgnosticDecorationSchema } from '../../../model/path-agnostic-decorations/types'; +import stringSupplant from '../../../utils/stringSupplant'; + +import './DetailsPanel.css'; + type TProps = TDecorationFromState & { decorationSchema: TPathAgnosticDecorationSchema; @@ -29,29 +33,19 @@ type TProps = TDecorationFromState & { export class UnconnectedDetailsPanel extends React.PureComponent { render() { - const { decorationColor, decorationMax, decorationValue, operation, service } = this.props; + const { decorationProgressbar, decorationColor, decorationMax, decorationSchema, decorationValue, operation: _op, service } = this.props; + const operation = _op && !Array.isArray(_op) ? _op : undefined; return ( -
-
+
+
{service}{operation && ( ::{operation} )}
- {typeof decorationValue === 'number' && typeof decorationMax === 'number' && } +
+ {stringSupplant(decorationSchema.name, { service, operation })} +
+ {decorationProgressbar || ({decorationValue})}
) } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.css new file mode 100644 index 0000000000..da654cdd6f --- /dev/null +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.css @@ -0,0 +1,38 @@ +/* +Copyright (c) 2020 The Jaeger Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.Ddg--SidePanel { + background-color: #f6f6f6; + border-left: solid 1px #ddd; + box-shadow: -3px 0 3px rgba(0, 0, 0, 0.1); + display: flex; +} + +.Ddg--SidePanel--Btns { + display: flex; + flex-direction: column; + justify-content: center; +} + +.Ddg--SidePanel--decorationBtn { + border-radius: 100%; + padding-bottom: 25%; + padding-top: 25%; +} + +.Ddg--SidePanel--decorationBtn.is-selected { + background-color: yellow; +} diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx index 99380b633f..64e3f6ad59 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx @@ -23,6 +23,8 @@ import { TPathAgnosticDecorationSchema } from '../../../model/path-agnostic-deco import { getConfigValue } from '../../../utils/config/get-config'; import DetailsPanel from './DetailsPanel'; +import './index.css'; + type TProps = { clearSelected: () => void; @@ -77,10 +79,10 @@ export default class SidePanel extends React.PureComponent { className="Ddg--SidePanel--decorationBtn" onClick={() => selectDecoration()} > - X + Clear
-
+
{selectedVertex && selectedSchema && + ) : undefined; - - return { decorationColor, decorationID, decorationValue, decorationMax }; + return { decorationProgressbar, decorationBackgroundColor, decorationColor, decorationID, decorationValue, decorationMax }; } From 3ed45899a7291c4ad525cf7b25864f8af330db8d Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Tue, 31 Mar 2020 17:21:14 -0400 Subject: [PATCH 11/31] WIP: Continue styling side panel, fetch details in details panel, render details in details card TODO: Style table, handle list, handle styled values Signed-off-by: Everett Ross --- .../src/actions/path-agnostic-decorations.tsx | 18 +-- packages/jaeger-ui/src/api/jaeger.js | 26 ++++ .../Graph/DdgNodeContent/index.css | 4 + .../Graph/DdgNodeContent/index.tsx | 13 +- .../SidePanel/DetailsCard.tsx | 127 ++++++++++++++++++ .../SidePanel/DetailsPanel.css | 4 + .../SidePanel/DetailsPanel.tsx | 127 ++++++++++++++++-- .../DeepDependencies/SidePanel/index.css | 48 ++++++- .../DeepDependencies/SidePanel/index.tsx | 16 +++ .../TimelineColumnResizer.css | 4 + .../TimelineColumnResizer.tsx | 13 +- .../src/constants/default-config.tsx | 16 ++- .../model/path-agnostic-decorations/types.tsx | 42 ++++-- 13 files changed, 402 insertions(+), 56 deletions(-) create mode 100644 packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard.tsx diff --git a/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx b/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx index f968736754..6e3bd5d532 100644 --- a/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx +++ b/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx @@ -67,24 +67,26 @@ export function getDecoration( } pendingCount = pendingCount ? pendingCount + 1 : 1; - const { url, opUrl, valuePath, opValuePath } = schema; + const { summaryUrl, opSummaryUrl, summaryPath, opSummaryPath } = schema; let promise: Promise>; let getPath: string; let setPath: string; - if (opValuePath && opUrl && operation) { - promise = JaegerAPI.fetchDecoration(stringSupplant(opUrl, { service, operation })); - getPath = stringSupplant(opValuePath, { service, operation }); + if (opSummaryPath && opSummaryUrl && operation) { + // TODO: lru memoize it up if chrome doesn't cache + promise = JaegerAPI.fetchDecoration(stringSupplant(opSummaryUrl, { service, operation })); + getPath = stringSupplant(opSummaryPath, { service, operation }); setPath = `${id}.withOp.${service}.${operation}.value`; } else { - promise = JaegerAPI.fetchDecoration(stringSupplant(url, { service })); - getPath = stringSupplant(valuePath, { service }); - getPath = valuePath; + // TODO: lru memoize it up if chrome doesn't cache + promise = JaegerAPI.fetchDecoration(stringSupplant(summaryUrl, { service })); + getPath = stringSupplant(summaryPath, { service }); + getPath = summaryPath; setPath = `${id}.withoutOp.${service}.value`; } promise .then(res => { - return _get(res, getPath, `${getPath} not found in response`); + return _get(res, getPath, `\`${getPath}\` not found in response`); }) .catch(err => { return `Unable to fetch decoration: ${err.message || err}`; diff --git a/packages/jaeger-ui/src/api/jaeger.js b/packages/jaeger-ui/src/api/jaeger.js index fb46dd5f03..ee3440677a 100644 --- a/packages/jaeger-ui/src/api/jaeger.js +++ b/packages/jaeger-ui/src/api/jaeger.js @@ -88,6 +88,32 @@ const JaegerAPI = { else rej(new Error(`One of the unlucky quarter: ${url.length}`)); }, 150)); } + if (url === 'get deets') { + return new Promise(res => setTimeout(() => res({ + deets: { + here: [ + { + count: 0, + value: 'first', + foo: 'bar', + bar: 'baz', + }, + { + count: 1, + value: 'second', + foo: 'bar too', + }, + ], + }, + defs: { + here: [ + 'count', + 'value', + 'foo', + ] + }, + }), 750)); + } return new Promise(res => setTimeout(() => res({ val: url.length ** 2 }), 150)); }, fetchDeepDependencyGraph(query) { diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css index 0442107f79..c272f1ba87 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css @@ -38,6 +38,10 @@ limitations under the License. display: flex; } +.DdgNodeContent--core.is-missingDecoration { + background-color: #E58C33; +} + .DdgNodeContent--core.is-positioned { bottom: 1px; left: 1px; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx index 2b3fb8b780..832262004e 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx @@ -16,15 +16,12 @@ import * as React from 'react'; import { Checkbox, Popover } from 'antd'; import cx from 'classnames'; import { TLayoutVertex } from '@jaegertracing/plexus/lib/types'; -import { CircularProgressbar } from 'react-circular-progressbar'; import IoAndroidLocate from 'react-icons/lib/io/android-locate'; import MdVisibilityOff from 'react-icons/lib/md/visibility-off'; import { connect } from 'react-redux'; import { RouteComponentProps, withRouter } from 'react-router-dom'; import { bindActionCreators, Dispatch } from 'redux'; -import 'react-circular-progressbar/dist/styles.css'; - import calcPositioning from './calc-positioning'; import { MAX_LENGTH, @@ -254,28 +251,24 @@ export class UnconnectedDdgNodeContent extends React.PureComponent {decorationProgressbar}

{ + /* + static defaultProps = { + columnDef: [], + }; + */ + renderList(details: string[]) { + return Not yet implement; + } + + renderTable(details: TPadRow[]) { + const { columnDefs: _columnDefs } = this.props; + const columnDefs = _columnDefs ? _columnDefs.slice() : []; + + const knownColumns = new Set((columnDefs || []).map(keyOrObj => { + if (typeof keyOrObj === 'string') return keyOrObj; + return keyOrObj.key; + })); + details.forEach((row: any) => { // TODO def not any + Object.keys(row).forEach((col: string) => { + if (!knownColumns.has(col)) { + knownColumns.add(col); + columnDefs.push(col); + }; + }); + }); + console.log(knownColumns); + console.log(columnDefs); + + return ( + + {columnDefs.map((fdef: any) => { + const def = fdef as unknown as any as (string | TPadColumnDef); + if (typeof def === 'string') { + const defString = def as string; // absolutely should not be necessary + return ( + {defString}}*/} + /> + ); + } + const defObj = def as TPadColumnDef; + return ( + {defString}}*/} + /> + ); + })} +
+ ); + } + + renderDetails() { + const { columnDefs: _columnDefs, details } = this.props; + const columnDefs = _columnDefs ? _columnDefs.slice() : []; + + let isTable = false; + + if (Array.isArray(details)) { + if (details.length === 0) return null; + + if (typeof details[0] === 'string') return this.renderList(details as string[]); + + return this.renderTable(details as TPadRow[]); + } + + return details; + } + + render() { + return ( +
+
+ {this.props.header} +
+

{this.props.description}

+ {this.renderDetails()} +
+ ); + } +} diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css index 8f041bbbc5..dfd8c5f8d9 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css @@ -40,3 +40,7 @@ limitations under the License. border-bottom: solid 1px #bbb; /* box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.3); */ } + +.Ddg-DetailsPanel--errorMsg { + color: #E58C33; +} diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx index 599e4618fc..dbaa0781ec 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx @@ -13,14 +13,16 @@ // limitations under the License. import * as React from 'react'; -import { CircularProgressbar } from 'react-circular-progressbar'; +import _get from 'lodash/get'; import { connect } from 'react-redux'; -import 'react-circular-progressbar/dist/styles.css'; - +import BreakableText from '../../../components/common/BreakableText'; +import ColumnResizer from '../../../components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer'; +import JaegerAPI from '../../../api/jaeger'; import extractDecorationFromState, { TDecorationFromState } from '../../../model/path-agnostic-decorations'; -import { TPathAgnosticDecorationSchema } from '../../../model/path-agnostic-decorations/types'; +import { TPathAgnosticDecorationSchema, TPadColumnDefs, TPadDetails } from '../../../model/path-agnostic-decorations/types'; import stringSupplant from '../../../utils/stringSupplant'; +import DetailsCard from './DetailsCard'; import './DetailsPanel.css'; @@ -31,21 +33,120 @@ type TProps = TDecorationFromState & { operation?: string | string[] | null; }; -export class UnconnectedDetailsPanel extends React.PureComponent { +type TState = { + columnDefs?: TPadColumnDefs; + details?: TPadDetails; + detailsErred?: boolean; + detailsLoading?: boolean; + width?: number; +}; + +export class UnconnectedDetailsPanel extends React.PureComponent { + state = {} as TState; + + componentDidMount() { + this.fetchDetails(); + } + + componentDidUpdate(prevProps: TProps) { + if (prevProps.operation !== this.props.operation + || prevProps.service !== this.props.service + || prevProps.decorationSchema !== this.props.decorationSchema + ) { + console.log('clearing state'); + this.setState({ + details: undefined, + detailsErred: undefined, + detailsLoading: undefined, + }); + this.fetchDetails(); + } + } + + fetchDetails() { + const { + decorationSchema: { + detailUrl, + detailPath, + detailColumnDefPath, + opDetailUrl, + opDetailPath, + opDetailColumnDefPath, + }, + operation: _op, + service, + }= this.props; + + const operation = _op && !Array.isArray(_op) ? _op : undefined; + + let fetchUrl: string | undefined; + let getDetailPath: string | undefined; + let getDefPath: string | undefined; + if (opDetailUrl && opDetailPath) { + fetchUrl = stringSupplant(opDetailUrl, { service, operation }); + getDetailPath = stringSupplant(opDetailPath, { service, operation }); + getDefPath = opDetailColumnDefPath && stringSupplant(opDetailColumnDefPath, { service, operation }); + } else if (detailUrl && detailPath) { + fetchUrl = stringSupplant(detailUrl, { service }); + getDetailPath = stringSupplant(detailPath, { service }); + getDefPath = detailColumnDefPath && stringSupplant(detailColumnDefPath, { service }); + } + + if (!fetchUrl || !getDetailPath) return; + + this.setState({ detailsLoading: true }); + + JaegerAPI.fetchDecoration(fetchUrl) + .then(res => { + const details = _get(res, (getDetailPath as string), `\`${getDetailPath}\` not found in response`); + const columnDefs: TPadColumnDefs = getDefPath + ? _get(res, getDefPath, []) + : []; + this.setState({ details, columnDefs }) + + + }) + .catch(err => { + this.setState({ + details: `Unable to fetch decoration: ${err.message || err}`, + detailsErred: true, + }); + }) + } + + onResize = (width: number) => { + this.setState({ width }); + } + render() { const { decorationProgressbar, decorationColor, decorationMax, decorationSchema, decorationValue, operation: _op, service } = this.props; + const { width = 0.3 } = this.state; const operation = _op && !Array.isArray(_op) ? _op : undefined; return ( -
-
- {service}{operation && ( - ::{operation} +
+
+
+ {operation && } +
+
+ {stringSupplant(decorationSchema.name, { service, operation })} +
+ {decorationProgressbar || ({decorationValue})} + {this.state.details && ( + )}
-
- {stringSupplant(decorationSchema.name, { service, operation })} -
- {decorationProgressbar || ({decorationValue})} +
) } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.css index da654cdd6f..e4340f07a4 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.css @@ -21,14 +21,55 @@ limitations under the License. display: flex; } -.Ddg--SidePanel--Btns { +.Ddg--SidePanel--Btns, .Ddg--SidePanel--DecorationBtns { display: flex; flex-direction: column; +} + +.Ddg--SidePanel--Btns { + justify-content: space-between; +} + +.Ddg--SidePanel--Btns button { + pointer: click; +} + + +.Ddg--SidePanel--closeBtn, .Ddg--SidePanel--infoBtn { + background: transparent; + border: none; + margin: 5px 0px; +} + +.Ddg--SidePanel--closeBtn > svg, +.Ddg--SidePanel--infoBtn > svg { + height: 80%; + width: 80%; +} + +.Ddg--SidePanel--closeBtn:focus > svg, +.Ddg--SidePanel--infoBtn:focus > svg { + filter: drop-shadow(0 0 2px blue); +} + +.Ddg--SidePanel--closeBtn:focus, +.Ddg--SidePanel--infoBtn:focus{ + outline: none; +} + +.Ddg--SidePanel--closeBtn.is-hidden { + visibility: hidden; + pointer-events: none; +} + +.Ddg--SidePanel--DecorationBtns { + flex-grow: 1; justify-content: center; } .Ddg--SidePanel--decorationBtn { border-radius: 100%; + margin: 3px; padding-bottom: 25%; padding-top: 25%; } @@ -36,3 +77,8 @@ limitations under the License. .Ddg--SidePanel--decorationBtn.is-selected { background-color: yellow; } + +.Ddg--SidePanel--decorationBtn:focus { + box-shadow: 0 0 5px 2px blue; + outline: none; +} diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx index 64e3f6ad59..133b6504ef 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx @@ -14,8 +14,12 @@ import * as React from 'react'; import { CircularProgressbar } from 'react-circular-progressbar'; +import MdExitToApp from 'react-icons/lib/md/exit-to-app'; +import MdInfoOutline from 'react-icons/lib/md/info-outline'; + import 'react-circular-progressbar/dist/styles.css'; + import { TDdgVertex, } from '../../../model/ddg/types'; @@ -50,6 +54,10 @@ export default class SidePanel extends React.PureComponent { this.decorations = getConfigValue('pathAgnosticDecorations'); } + clearSelected = () => { + this.props.clearSelected(); + } + render() { if (!this.decorations) return null; @@ -65,6 +73,10 @@ export default class SidePanel extends React.PureComponent { return (
+ +
{this.decorations.map(({ acronym, id, name }) => ( +
+
{selectedVertex && selectedSchema && void; position: number; + rightSide?: boolean; }; type TimelineColumnResizerState = { @@ -77,20 +78,22 @@ export default class TimelineColumnResizer extends React.PureComponent< }; _handleDragUpdate = ({ value }: DraggingUpdate) => { - this.setState({ dragPosition: value }); + const dragPosition = this.props.rightSide ? 1 - value : value; + this.setState({ dragPosition }); }; _handleDragEnd = ({ manager, value }: DraggingUpdate) => { manager.resetBounds(); this.setState({ dragPosition: null }); - this.props.onChange(value); + const dragPosition = this.props.rightSide ? 1 - value : value; + this.props.onChange(dragPosition); }; render() { let left; let draggerStyle; let isDraggingCls = ''; - const { position } = this.props; + const { position, rightSide } = this.props; const { dragPosition } = this.state; left = `${position * 100}%`; const gripStyle = { left }; @@ -113,7 +116,7 @@ export default class TimelineColumnResizer extends React.PureComponent< draggerStyle = gripStyle; } return ( -
+
[]; +export type TPadRow = Record; + +export type TPadDetails = string | string[] | TPadRow[]; + export type TPadEntry = { value: number | string; // string or other type is for data unavailable // renderData: unknown; From 177066b247e6802ad8c1bf6288e6691762796158 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Tue, 31 Mar 2020 18:15:21 -0400 Subject: [PATCH 12/31] WIP: Improve TS handling of union of arrays TODO: Style table, handle list, handle styled values Signed-off-by: Everett Ross --- .../SidePanel/DetailsCard.tsx | 41 ++++++++----------- .../SidePanel/DetailsPanel.tsx | 2 +- .../model/path-agnostic-decorations/types.tsx | 3 +- 3 files changed, 21 insertions(+), 25 deletions(-) diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard.tsx index 677cf4c8e7..4c4926ef15 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard.tsx @@ -32,6 +32,10 @@ type TProps = { header: string; }; +function isList(arr: string[] | TPadRow[]): arr is string[] { + return typeof arr[0] === 'string'; +} + export default class DetailsCard extends React.PureComponent { /* static defaultProps = { @@ -44,13 +48,12 @@ export default class DetailsCard extends React.PureComponent { renderTable(details: TPadRow[]) { const { columnDefs: _columnDefs } = this.props; - const columnDefs = _columnDefs ? _columnDefs.slice() : []; - - const knownColumns = new Set((columnDefs || []).map(keyOrObj => { + const columnDefs: TPadColumnDefs = _columnDefs ? _columnDefs.slice() : []; + const knownColumns = new Set(columnDefs.map(keyOrObj => { if (typeof keyOrObj === 'string') return keyOrObj; return keyOrObj.key; })); - details.forEach((row: any) => { // TODO def not any + details.forEach(row => { Object.keys(row).forEach((col: string) => { if (!knownColumns.has(col)) { knownColumns.add(col); @@ -58,8 +61,6 @@ export default class DetailsCard extends React.PureComponent { }; }); }); - console.log(knownColumns); - console.log(columnDefs); return ( { rowKey="id" pagination={false} > - {columnDefs.map((fdef: any) => { - const def = fdef as unknown as any as (string | TPadColumnDef); + {columnDefs.map(def => { if (typeof def === 'string') { - const defString = def as string; // absolutely should not be necessary return ( {defString}}*/} /> ); } - const defObj = def as TPadColumnDef; return ( {defString}}*/} + key={def.key} + title={def.label || def.key} + dataIndex={def.key} + {...{} /*render={() => {defString}}*/} /> ); })} @@ -100,17 +98,14 @@ export default class DetailsCard extends React.PureComponent { const { columnDefs: _columnDefs, details } = this.props; const columnDefs = _columnDefs ? _columnDefs.slice() : []; - let isTable = false; - if (Array.isArray(details)) { if (details.length === 0) return null; - if (typeof details[0] === 'string') return this.renderList(details as string[]); - - return this.renderTable(details as TPadRow[]); + if (isList(details)) return this.renderList(details); + return this.renderTable(details); } - return details; + return {details}; } render() { diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx index dbaa0781ec..38b72d36be 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx @@ -42,7 +42,7 @@ type TState = { }; export class UnconnectedDetailsPanel extends React.PureComponent { - state = {} as TState; + state: TState = {}; componentDidMount() { this.fetchDetails(); diff --git a/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx index 66249e1a7d..d3c1d0e968 100644 --- a/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx +++ b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx @@ -41,7 +41,8 @@ export type TPadColumnDef = { label?: string; styling?: React.CSSProperties; }; -export type TPadColumnDefs = (string | (TStyledValue & { key: string }))[]; + +export type TPadColumnDefs = (string | TPadColumnDef)[]; // export type TPadDetails = string | TStyledValue | string[] | TStyledValue[] | Record[]; export type TPadRow = Record; From dd649942687c1fbb0c9961d99acc943313a9bc7b Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Wed, 1 Apr 2020 15:39:16 -0400 Subject: [PATCH 13/31] WIP: Limit % circle size, update cursor for clickable ddg nodes, fix resizer height css, make top offset css var TODO: Style table, handle list, handle styled values Signed-off-by: Everett Ross --- .../jaeger-ui/src/components/App/Page.css | 4 +-- .../Graph/DdgNodeContent/index.css | 4 +++ .../Graph/DdgNodeContent/index.tsx | 5 ++-- .../SidePanel/DetailsCard/index.css | 22 +++++++++++++++ .../index.tsx} | 10 ++++--- .../SidePanel/DetailsPanel.css | 8 +++++- .../SidePanel/DetailsPanel.tsx | 19 ++++++++----- .../DeepDependencies/SidePanel/index.css | 14 +++++++--- .../DeepDependencies/SidePanel/index.tsx | 27 ------------------- .../TimelineColumnResizer.css | 2 +- .../TimelineColumnResizer.tsx | 3 ++- .../jaeger-ui/src/components/common/utils.css | 4 +++ 12 files changed, 74 insertions(+), 48 deletions(-) create mode 100644 packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css rename packages/jaeger-ui/src/components/DeepDependencies/SidePanel/{DetailsCard.tsx => DetailsCard/index.tsx} (93%) diff --git a/packages/jaeger-ui/src/components/App/Page.css b/packages/jaeger-ui/src/components/App/Page.css index 620189931c..85e2558672 100644 --- a/packages/jaeger-ui/src/components/App/Page.css +++ b/packages/jaeger-ui/src/components/App/Page.css @@ -26,8 +26,8 @@ limitations under the License. display: flex; flex-direction: column; left: 0; - min-height: calc(100% - 46px); + min-height: calc(100% - var(--nav-height)); position: absolute; right: 0; - top: 46px; + top: var(--nav-height); } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css index c272f1ba87..959c6106ee 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css @@ -38,6 +38,10 @@ limitations under the License. display: flex; } +.DdgNodeContent--core.is-decorated { + cursor: pointer; +} + .DdgNodeContent--core.is-missingDecoration { background-color: #E58C33; } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx index 832262004e..1e52a4815e 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx @@ -174,8 +174,8 @@ export class UnconnectedDdgNodeContent extends React.PureComponent { - const { selectVertex, vertex } = this.props; - selectVertex(vertex); + const { decorationValue, selectVertex, vertex } = this.props; + if (decorationValue) selectVertex(vertex); } private hideVertex = () => { @@ -263,6 +263,7 @@ export class UnconnectedDdgNodeContent extends React.PureComponent { } render() { + const { description, header } = this.props; + return (
- {this.props.header} + {header}
-

{this.props.description}

+

{description}

{this.renderDetails()}
); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css index dfd8c5f8d9..d27af4becb 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css @@ -38,9 +38,15 @@ limitations under the License. .Ddg--DetailsPanel--DecorationHeader > span { border-bottom: solid 1px #bbb; - /* box-shadow: 0 1px 0.5px rgba(0, 0, 0, 0.3); */ + box-shadow: 0px 3px 3px -3px rgba(0, 0, 0, 0.1); } .Ddg-DetailsPanel--errorMsg { color: #E58C33; } + +.Ddg--DetailsPanel--PercentCircleWrapper { + margin: auto; + max-width: 20vh; + padding: 0 3%; +} diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx index 38b72d36be..61f7ab6885 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx @@ -131,7 +131,14 @@ export class UnconnectedDetailsPanel extends React.PureComponent
{stringSupplant(decorationSchema.name, { service, operation })}
- {decorationProgressbar || ({decorationValue})} + {decorationProgressbar + ? ( +
+ {decorationProgressbar} +
+ ) + : {decorationValue} + } {this.state.details && ( )} + /> ) } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.css index e4340f07a4..487c2a0873 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.css @@ -31,7 +31,7 @@ limitations under the License. } .Ddg--SidePanel--Btns button { - pointer: click; + cursor: pointer; } @@ -49,7 +49,7 @@ limitations under the License. .Ddg--SidePanel--closeBtn:focus > svg, .Ddg--SidePanel--infoBtn:focus > svg { - filter: drop-shadow(0 0 2px blue); + filter: drop-shadow(0 0 2px #006c99); } .Ddg--SidePanel--closeBtn:focus, @@ -75,10 +75,16 @@ limitations under the License. } .Ddg--SidePanel--decorationBtn.is-selected { - background-color: yellow; + box-shadow: 0px 0px 4px 2px #006c99; } .Ddg--SidePanel--decorationBtn:focus { - box-shadow: 0 0 5px 2px blue; + box-shadow: inset 0 0 5px 2px #00b4ff; outline: none; } + +.Ddg--SidePanel--decorationBtn.is-selected:focus { + border: none; + margin: 4px; + box-shadow: inset 0 0 5px 2px #00b4ff, 0px 0px 5px 3px #006c99; +} diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx index 133b6504ef..797021f75c 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx @@ -109,30 +109,3 @@ export default class SidePanel extends React.PureComponent { ); } } - -/* -export function mapStateToProps(state: ReduxState, ownProps: TOwnProps): TReduxProps { - const { services: stServices } = state; - const { services, serverOpsForService } = stServices; - const urlState = getUrlState(ownProps.location.search); - const { density, operation, service, showOp: urlStateShowOp } = urlState; - const showOp = urlStateShowOp !== undefined ? urlStateShowOp : operation !== undefined; - let graphState: TDdgStateEntry | undefined; - if (service) { - graphState = _get(state.ddg, getStateEntryKey({ service, operation, start: 0, end: 0 })); - } - let graph: GraphModel | undefined; - if (graphState && graphState.state === fetchedState.DONE) { - graph = makeGraph(graphState.model, showOp, density); - } - return { - graph, - graphState, - serverOpsForService, - services, - showOp, - urlState: sanitizeUrlState(urlState, _get(graphState, 'model.hash')), - ...extractUiFindFromState(state), - }; -} -*/ diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.css b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.css index 2d30dd30df..6e00ed0cc7 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.css +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.css @@ -34,7 +34,7 @@ limitations under the License. .TimelineColumnResizer--dragger { border-left: 2px solid transparent; cursor: col-resize; - height: 5000px; + height: calc(100vh - var(--nav-height)); margin-left: -1px; position: absolute; top: 0; diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx index 2d36ea49c6..e0cc829866 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx @@ -68,7 +68,8 @@ export default class TimelineColumnResizer extends React.PureComponent< throw new Error('invalid state'); } const { left: clientXLeft, width } = this._rootElm.getBoundingClientRect(); - const { min, max } = this.props; + let { rightSide, min, max } = this.props; + if (rightSide) [min, max] = [1 - max, 1 - min]; return { clientXLeft, width, diff --git a/packages/jaeger-ui/src/components/common/utils.css b/packages/jaeger-ui/src/components/common/utils.css index 0cf06bc0cc..d56333a1b6 100644 --- a/packages/jaeger-ui/src/components/common/utils.css +++ b/packages/jaeger-ui/src/components/common/utils.css @@ -14,6 +14,10 @@ See the License for the specific language governing permissions and limitations under the License. */ +:root { + --nav-height: 46px; +} + .u-width-100 { width: 100%; } From c98e43493ef3ed1f4b062f85a62faaf67a2b8ae7 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Wed, 1 Apr 2020 20:06:53 -0400 Subject: [PATCH 14/31] WIP: Handle list, begin overflow management TODO: Style table overflow, handle styled values Signed-off-by: Everett Ross --- packages/jaeger-ui/src/api/jaeger.js | 112 +++++++++++++++++- .../SidePanel/DetailsCard/index.css | 21 ++++ .../SidePanel/DetailsCard/index.tsx | 28 ++++- .../SidePanel/DetailsPanel.css | 4 +- .../SidePanel/DetailsPanel.tsx | 51 ++++---- .../src/constants/default-config.tsx | 24 +++- 6 files changed, 202 insertions(+), 38 deletions(-) diff --git a/packages/jaeger-ui/src/api/jaeger.js b/packages/jaeger-ui/src/api/jaeger.js index ee3440677a..0d3d353901 100644 --- a/packages/jaeger-ui/src/api/jaeger.js +++ b/packages/jaeger-ui/src/api/jaeger.js @@ -88,7 +88,7 @@ const JaegerAPI = { else rej(new Error(`One of the unlucky quarter: ${url.length}`)); }, 150)); } - if (url === 'get deets') { + if (url === 'get graph') { return new Promise(res => setTimeout(() => res({ deets: { here: [ @@ -103,6 +103,102 @@ const JaegerAPI = { value: 'second', foo: 'bar too', }, + { + count: 2, + value: 'third', + foo: 'bar three', + }, + { + count: 3, + value: 'second', + foo: 'bar too', + }, + { + count: 4, + value: 'third', + foo: 'bar three', + }, + { + count: 5, + value: 'second', + foo: 'bar too', + }, + { + count: 6, + value: 'third', + foo: 'bar three', + }, + { + count: 7, + value: 'second', + foo: 'bar too', + }, + { + count: 8, + value: 'third', + foo: 'bar three', + }, + { + count: 9, + value: 'second', + foo: 'bar too', + }, + { + count: 10, + value: 'third', + foo: 'bar three', + }, + { + count: 11, + value: 'second', + foo: 'bar too', + }, + { + count: 12, + value: 'third', + foo: 'bar three', + }, + { + count: 13, + value: 'second', + foo: 'bar too', + }, + { + count: 14, + value: 'third', + foo: 'bar three', + }, + { + count: 15, + value: 'second', + foo: 'bar too', + }, + { + count: 16, + value: 'third', + foo: 'bar three', + }, + { + count: 17, + value: 'second', + foo: 'bar too', + }, + { + count: 18, + value: 'third', + foo: 'bar three', + }, + { + count: 19, + value: 'second', + foo: 'bar too', + }, + { + count: 20, + value: 'third', + foo: 'bar three', + }, + ], }, defs: { @@ -114,6 +210,20 @@ const JaegerAPI = { }, }), 750)); } + if (url === 'get string') { + return new Promise(res => setTimeout(() => res({ + deets: { + here: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + }, + }), 750)); + } + if (url === 'get list') { + return new Promise(res => setTimeout(() => res({ + deets: { + here: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'.split(' '), + }, + }), 750)); + } return new Promise(res => setTimeout(() => res({ val: url.length ** 2 }), 150)); }, fetchDeepDependencyGraph(query) { diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css index 65fa4351a9..57802fee20 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css @@ -14,9 +14,30 @@ See the License for the specific language governing permissions and limitations under the License. */ +.DetailsCard { + display: flex; + flex-direction: column; +} + .DetailsCard--Header { border-bottom: solid 1px #ddd; box-shadow: 0px 5px 5px -5px rgba(0, 0, 0, 0.3); font-size: 1.5em; padding: .1em .3em; } + +.DetailsCard--DetailsWrapper { + /* overflow: scroll; */ + display: flex; + flex-direction: column; +} + +.DetailsCard--DetailsWrapper div:only-of-type { + height: 100%; + display: flex; + flex-direction: column; +} + +.DetailsCard--DetailsWrapper div:not(:only-of-type):last-of-type { + overflow: scroll; +} diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx index 4b68ea324d..ffc1725b5c 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx @@ -13,7 +13,7 @@ // limitations under the License. import * as React from 'react'; -import { Table } from 'antd'; +import { List, Table } from 'antd'; // import _get from 'lodash/get'; import { TPadColumnDef, TPadColumnDefs, TPadDetails, TPadRow } from '../../../../model/path-agnostic-decorations/types'; @@ -23,6 +23,7 @@ import './index.css'; // // const { Column } = Table; +const { Item } = List; type TProps = { @@ -43,7 +44,13 @@ export default class DetailsCard extends React.PureComponent { }; */ renderList(details: string[]) { - return Not yet implement; + console.log(details); + return ( + {s}} + /> + ); } renderTable(details: TPadRow[]) { @@ -65,6 +72,9 @@ export default class DetailsCard extends React.PureComponent { return (
{ return ( { + return (a[def] < b[def] ? 1 : b[def] < a[def] ? -1 : 0); + }} title={def} dataIndex={def} {...{} /*render={() => {defString}}*/} @@ -84,6 +97,9 @@ export default class DetailsCard extends React.PureComponent { return ( { + return a[def.key] < b[def.key] ? 1 : b[def.key] < a[def.key] ? -1 : 0; + }} title={def.label || def.key} dataIndex={def.key} {...{} /*render={() => {defString}}*/} @@ -112,12 +128,14 @@ export default class DetailsCard extends React.PureComponent { const { description, header } = this.props; return ( -
+
{header} + {description &&

{description}

} +
+
+ {this.renderDetails()}
-

{description}

- {this.renderDetails()}
); } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css index d27af4becb..50f83f44c6 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css @@ -19,6 +19,8 @@ limitations under the License. background-color: #fafafa; border-left: solid 1px #ddd; box-shadow: -3px 0 3px rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: column; height: 100%; } @@ -46,7 +48,7 @@ limitations under the License. } .Ddg--DetailsPanel--PercentCircleWrapper { - margin: auto; + margin: 0 auto; max-width: 20vh; padding: 0 3%; } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx index 61f7ab6885..042ef94b44 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx @@ -102,9 +102,8 @@ export class UnconnectedDetailsPanel extends React.PureComponent const columnDefs: TPadColumnDefs = getDefPath ? _get(res, getDefPath, []) : []; - this.setState({ details, columnDefs }) - + this.setState({ details, columnDefs }) }) .catch(err => { this.setState({ @@ -124,34 +123,32 @@ export class UnconnectedDetailsPanel extends React.PureComponent const operation = _op && !Array.isArray(_op) ? _op : undefined; return (
-
-
- {operation && } -
-
- {stringSupplant(decorationSchema.name, { service, operation })} -
- {decorationProgressbar - ? ( -
- {decorationProgressbar} -
- ) - : {decorationValue} - } - {this.state.details && ( - - )} +
+ {operation && } +
+
+ {stringSupplant(decorationSchema.name, { service, operation })} +
+ {decorationProgressbar + ? ( +
+ {decorationProgressbar}
+ ) + : {decorationValue} + } + {this.state.details && ( + + )}
diff --git a/packages/jaeger-ui/src/constants/default-config.tsx b/packages/jaeger-ui/src/constants/default-config.tsx index 69a75484dc..daa5782b9b 100644 --- a/packages/jaeger-ui/src/constants/default-config.tsx +++ b/packages/jaeger-ui/src/constants/default-config.tsx @@ -69,14 +69,30 @@ export default deepFreeze( summaryUrl: 'all should res #{service}', summaryPath: 'val', }, { - acronym: 'RDT', - id: 'rdt', - name: 'all should resolve details too #{service}', + acronym: 'RGT', + id: 'rgt', + name: 'all should resolve details graph too #{service}', summaryUrl: 'details too #{service}', - detailUrl: 'get deets', + detailUrl: 'get graph', detailPath: 'deets.here', detailColumnDefPath: 'defs.here', summaryPath: 'val', + }, { + acronym: 'RST', + id: 'rst', + name: 'all should resolve details string too #{service}', + summaryUrl: 'details too #{service}', + detailUrl: 'get string', + detailPath: 'deets.here', + summaryPath: 'val', + }, { + acronym: 'RLT', + id: 'rlt', + name: 'all should resolve details list too #{service}', + summaryUrl: 'details too #{service}', + detailUrl: 'get list', + detailPath: 'deets.here', + summaryPath: 'val', }/*, { TODO: op example too }*/], From 0160f790f321a4cf9dafa3f9cedbb4b73396c3ce Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Wed, 1 Apr 2020 20:57:25 -0400 Subject: [PATCH 15/31] WIP: Manage overflow, begin handling styled values TODO: Handle styled values, loading&err render, modal, beautification Signed-off-by: Everett Ross --- packages/jaeger-ui/src/api/jaeger.js | 7 +++- .../SidePanel/DetailsCard/index.css | 9 ++++- .../SidePanel/DetailsCard/index.tsx | 35 +++++++++++++++++-- 3 files changed, 46 insertions(+), 5 deletions(-) diff --git a/packages/jaeger-ui/src/api/jaeger.js b/packages/jaeger-ui/src/api/jaeger.js index 0d3d353901..f1faddd0bf 100644 --- a/packages/jaeger-ui/src/api/jaeger.js +++ b/packages/jaeger-ui/src/api/jaeger.js @@ -93,7 +93,12 @@ const JaegerAPI = { deets: { here: [ { - count: 0, + count: { + value: 0, + styling: { + backgroundColor: 'red', + }, + }, value: 'first', foo: 'bar', bar: 'baz', diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css index 57802fee20..6d44382bf0 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css @@ -27,7 +27,8 @@ limitations under the License. } .DetailsCard--DetailsWrapper { - /* overflow: scroll; */ + overflow: scroll; + /* display: flex; flex-direction: column; } @@ -41,3 +42,9 @@ limitations under the License. .DetailsCard--DetailsWrapper div:not(:only-of-type):last-of-type { overflow: scroll; } +*/ +} + +.DetailsCard--DetailsWrapper th { + min-width: 65px; +} diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx index ffc1725b5c..13080319cc 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx @@ -69,22 +69,51 @@ export default class DetailsCard extends React.PureComponent { }); }); + const onCellObj = { + onCell: (...args: any[]) => { + console.log('on cell'); + console.log(...args); + return ({ + style: { + backgroundColor: 'red', + color: 'white', + }, + }) + }, + render: (...args: any[]) => { + console.log('render'); + console.log(...args); + return args[0]; + }, + }; + return (
{ + console.log(args); + return ( + + ); + } + } + }} */ } > {columnDefs.map(def => { if (typeof def === 'string') { return ( { return (a[def] < b[def] ? 1 : b[def] < a[def] ? -1 : 0); }} From 10c2e41de82ad893e194f3951ab268245f5cfee9 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Thu, 2 Apr 2020 17:30:04 -0400 Subject: [PATCH 16/31] WIP: Handle styled values, render loading&err, style card TODO: Add info modal Signed-off-by: Everett Ross --- packages/jaeger-ui/src/api/jaeger.js | 15 ++- .../SidePanel/DetailsCard/index.css | 17 +-- .../SidePanel/DetailsCard/index.tsx | 123 ++++++++---------- .../SidePanel/DetailsPanel.css | 27 +++- .../SidePanel/DetailsPanel.tsx | 32 +++-- .../src/constants/default-config.tsx | 24 ++++ .../model/path-agnostic-decorations/types.tsx | 2 +- 7 files changed, 140 insertions(+), 100 deletions(-) diff --git a/packages/jaeger-ui/src/api/jaeger.js b/packages/jaeger-ui/src/api/jaeger.js index f1faddd0bf..5b3ecfc514 100644 --- a/packages/jaeger-ui/src/api/jaeger.js +++ b/packages/jaeger-ui/src/api/jaeger.js @@ -97,6 +97,7 @@ const JaegerAPI = { value: 0, styling: { backgroundColor: 'red', + color: 'white', }, }, value: 'first', @@ -209,11 +210,18 @@ const JaegerAPI = { defs: { here: [ 'count', - 'value', + { + key: 'value', + label: 'The value column', + styling: { + backgroundColor: 'blue', + color: 'lightgrey', + }, + }, 'foo', ] }, - }), 750)); + }), 2750)); } if (url === 'get string') { return new Promise(res => setTimeout(() => res({ @@ -229,6 +237,9 @@ const JaegerAPI = { }, }), 750)); } + if (url === 'infinite load') return new Promise(res => setTimeout(() => res('you are patient, eh?'), 1500000)); + if (url === 'deets err') return new Promise((res, rej) => setTimeout(() => rej(new Error('you knew this would happen')), 600)); + if (url === 'deets 404') return new Promise(res => res({})); return new Promise(res => setTimeout(() => res({ val: url.length ** 2 }), 150)); }, fetchDeepDependencyGraph(query) { diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css index 6d44382bf0..48a2504721 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css @@ -17,6 +17,7 @@ limitations under the License. .DetailsCard { display: flex; flex-direction: column; + padding: .5em; } .DetailsCard--Header { @@ -28,21 +29,7 @@ limitations under the License. .DetailsCard--DetailsWrapper { overflow: scroll; - /* - display: flex; - flex-direction: column; -} - -.DetailsCard--DetailsWrapper div:only-of-type { - height: 100%; - display: flex; - flex-direction: column; -} - -.DetailsCard--DetailsWrapper div:not(:only-of-type):last-of-type { - overflow: scroll; -} -*/ + margin-top: .5em; } .DetailsCard--DetailsWrapper th { diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx index 13080319cc..fcb5546ff9 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx @@ -14,19 +14,18 @@ import * as React from 'react'; import { List, Table } from 'antd'; -// import _get from 'lodash/get'; +import _isEmpty from 'lodash/isEmpty'; -import { TPadColumnDef, TPadColumnDefs, TPadDetails, TPadRow } from '../../../../model/path-agnostic-decorations/types'; -// import stringSupplant from '../../../utils/stringSupplant'; +import { TPadColumnDef, TPadColumnDefs, TPadDetails, TPadRow, TStyledValue } from '../../../../model/path-agnostic-decorations/types'; import './index.css'; -// -// + const { Column } = Table; const { Item } = List; type TProps = { + className?: string; columnDefs?: TPadColumnDefs; description?: string; details: TPadDetails; @@ -38,13 +37,7 @@ function isList(arr: string[] | TPadRow[]): arr is string[] { } export default class DetailsCard extends React.PureComponent { - /* - static defaultProps = { - columnDef: [], - }; - */ renderList(details: string[]) { - console.log(details); return ( { ); } + renderColumn(def: TPadColumnDef | string) { + let dataIndex: string; + let key: string; + let style: React.CSSProperties | undefined; + let title: string; + if (typeof def === 'string') { + key = title = dataIndex = def; + } else { + key = title = dataIndex = def.key; + if (def.label) title = def.label; + if (def.styling) style = def.styling; + } + + const props = { + dataIndex, + key, + title, + onCell: (row: TPadRow) => { + const cellData = row[dataIndex]; + if (!cellData || typeof cellData !== 'object') return null; + const { styling } = cellData; + if (_isEmpty(styling)) return null; + return ({ + style: styling, + }) + }, + onHeaderCell: () => ({ + style, + }), + render: (cellData: undefined | string | TStyledValue) => { + if (!cellData || typeof cellData !== 'object') return cellData; + return cellData.value; + }, + sorter: (a: TPadRow, b: TPadRow) => { + const aData = a[dataIndex]; + const aValue = typeof aData === 'object' && aData.value !== undefined ? aData.value : aData; + const bData = b[dataIndex]; + const bValue = typeof bData === 'object' && bData.value !== undefined ? bData.value : bData; + return (aValue < bValue ? 1 : bValue < aValue ? -1 : 0); + }, + }; + + return ; + } + renderTable(details: TPadRow[]) { const { columnDefs: _columnDefs } = this.props; const columnDefs: TPadColumnDefs = _columnDefs ? _columnDefs.slice() : []; @@ -69,24 +107,6 @@ export default class DetailsCard extends React.PureComponent { }); }); - const onCellObj = { - onCell: (...args: any[]) => { - console.log('on cell'); - console.log(...args); - return ({ - style: { - backgroundColor: 'red', - color: 'white', - }, - }) - }, - render: (...args: any[]) => { - console.log('render'); - console.log(...args); - return args[0]; - }, - }; - return (
+ hello + {args[0].children[2] && (args[0].children[2].value !== undefined && args[0].children[2].value) || args[0].children[2]} +
{ dataSource={details} rowKey="id" pagination={false} - {...{} /* components={{ - body: { - cell: (...args: any[]) => { - console.log(args); - return ( - - ); - } - } - }} */ } > - {columnDefs.map(def => { - if (typeof def === 'string') { - return ( - { - return (a[def] < b[def] ? 1 : b[def] < a[def] ? -1 : 0); - }} - title={def} - dataIndex={def} - {...{} /*render={() => {defString}}*/} - /> - ); - } - return ( - { - return a[def.key] < b[def.key] ? 1 : b[def.key] < a[def.key] ? -1 : 0; - }} - title={def.label || def.key} - dataIndex={def.key} - {...{} /*render={() => {defString}}*/} - /> - ); - })} + {columnDefs.map(this.renderColumn)}
- hello - {args[0].children[2] && (args[0].children[2].value !== undefined && args[0].children[2].value) || args[0].children[2]} -
); } @@ -154,10 +135,10 @@ export default class DetailsCard extends React.PureComponent { } render() { - const { description, header } = this.props; + const { className, description, header } = this.props; return ( -
+
{header} {description &&

{description}

} diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css index 50f83f44c6..c50eeca12f 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css @@ -43,10 +43,35 @@ limitations under the License. box-shadow: 0px 3px 3px -3px rgba(0, 0, 0, 0.1); } -.Ddg-DetailsPanel--errorMsg { +.Ddg--DetailsPanel--errorMsg { color: #E58C33; } +.Ddg--DetailsPanel--DetailsCard { + background-color: #ffffff; + border: solid 2px rgba(0, 0, 0, 0.3); + margin: 0.5em; +} + +.Ddg--DetailsPanel--DetailsCard.is-error { + color: #E58C33; +} + +.Ddg--DetailsPanel--LoadingIndicator { + display: block; + margin: auto; + position: absolute; + left: 0; + right: 0; + top: 50%; + line-height: 0; +} + +.Ddg--DetailsPanel--LoadingWrapper { + flex-grow: 1; + position: relative; +} + .Ddg--DetailsPanel--PercentCircleWrapper { margin: 0 auto; max-width: 20vh; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx index 042ef94b44..cfa2157587 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx @@ -16,7 +16,8 @@ import * as React from 'react'; import _get from 'lodash/get'; import { connect } from 'react-redux'; -import BreakableText from '../../../components/common/BreakableText'; +import BreakableText from '../..//common/BreakableText'; +import LoadingIndicator from '../../common/LoadingIndicator'; import ColumnResizer from '../../../components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer'; import JaegerAPI from '../../../api/jaeger'; import extractDecorationFromState, { TDecorationFromState } from '../../../model/path-agnostic-decorations'; @@ -53,7 +54,6 @@ export class UnconnectedDetailsPanel extends React.PureComponent || prevProps.service !== this.props.service || prevProps.decorationSchema !== this.props.decorationSchema ) { - console.log('clearing state'); this.setState({ details: undefined, detailsErred: undefined, @@ -98,17 +98,23 @@ export class UnconnectedDetailsPanel extends React.PureComponent JaegerAPI.fetchDecoration(fetchUrl) .then(res => { - const details = _get(res, (getDetailPath as string), `\`${getDetailPath}\` not found in response`); + let detailsErred = false; + let details = _get(res, (getDetailPath as string)); + if (details === undefined) { + details = `\`${getDetailPath}\` not found in response`; + detailsErred = true; + } const columnDefs: TPadColumnDefs = getDefPath ? _get(res, getDefPath, []) : []; - this.setState({ details, columnDefs }) + this.setState({ columnDefs, details, detailsErred, detailsLoading: false }) }) .catch(err => { this.setState({ details: `Unable to fetch decoration: ${err.message || err}`, detailsErred: true, + detailsLoading: false, }); }) } @@ -135,15 +141,21 @@ export class UnconnectedDetailsPanel extends React.PureComponent {decorationProgressbar}
) - : {decorationValue} + : {decorationValue} } + {this.state.detailsLoading && ( +
+ +
+ )} {this.state.details && ( - - )} + )} []; -export type TPadRow = Record; +export type TPadRow = Record; export type TPadDetails = string | string[] | TPadRow[]; From ecc8dd0bceb3158a4783f59156a57cf77febe536 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Thu, 2 Apr 2020 18:41:27 -0400 Subject: [PATCH 17/31] Add info modal, begin clean up TODO clean up&test Signed-off-by: Everett Ross --- packages/jaeger-ui/src/api/jaeger.js | 320 ++++++++++-------- .../Graph/DdgNodeContent/index.css | 2 +- .../Graph/DdgNodeContent/index.tsx | 76 +++-- .../SidePanel/DetailsCard/index.css | 6 +- .../SidePanel/DetailsCard/index.tsx | 49 +-- .../SidePanel/DetailsPanel.css | 9 +- .../SidePanel/DetailsPanel.tsx | 66 ++-- .../DeepDependencies/SidePanel/index.css | 9 +- .../DeepDependencies/SidePanel/index.tsx | 93 ++--- .../src/components/DeepDependencies/index.tsx | 2 +- .../TimelineColumnResizer.tsx | 5 +- .../src/constants/default-config.tsx | 135 ++++---- .../model/path-agnostic-decorations/index.tsx | 73 ++-- .../reducers/path-agnostic-decorations.tsx | 7 +- 14 files changed, 467 insertions(+), 385 deletions(-) diff --git a/packages/jaeger-ui/src/api/jaeger.js b/packages/jaeger-ui/src/api/jaeger.js index 5b3ecfc514..2a400a2ac4 100644 --- a/packages/jaeger-ui/src/api/jaeger.js +++ b/packages/jaeger-ui/src/api/jaeger.js @@ -83,164 +83,188 @@ const JaegerAPI = { fetchDecoration(url) { // return getJSON(url); if (url.length % 2 && url.startsWith('neapolitan')) { - return new Promise((res, rej) => setTimeout(() => { - if (url.length % 4 === 1) res('No val here'); - else rej(new Error(`One of the unlucky quarter: ${url.length}`)); - }, 150)); + return new Promise((res, rej) => + setTimeout(() => { + if (url.length % 4 === 1) res('No val here'); + else rej(new Error(`One of the unlucky quarter: ${url.length}`)); + }, 150) + ); } if (url === 'get graph') { - return new Promise(res => setTimeout(() => res({ - deets: { - here: [ - { - count: { - value: 0, - styling: { - backgroundColor: 'red', - color: 'white', - }, + return new Promise(res => + setTimeout( + () => + res({ + deets: { + here: [ + { + count: { + value: 0, + styling: { + backgroundColor: 'red', + color: 'white', + }, + }, + value: 'first', + foo: 'bar', + bar: 'baz', + }, + { + count: 1, + value: 'second', + foo: 'bar too', + }, + { + count: 2, + value: 'third', + foo: 'bar three', + }, + { + count: 3, + value: 'second', + foo: 'bar too', + }, + { + count: 4, + value: 'third', + foo: 'bar three', + }, + { + count: 5, + value: 'second', + foo: 'bar too', + }, + { + count: 6, + value: 'third', + foo: 'bar three', + }, + { + count: 7, + value: 'second', + foo: 'bar too', + }, + { + count: 8, + value: 'third', + foo: 'bar three', + }, + { + count: 9, + value: 'second', + foo: 'bar too', + }, + { + count: 10, + value: 'third', + foo: 'bar three', + }, + { + count: 11, + value: 'second', + foo: 'bar too', + }, + { + count: 12, + value: 'third', + foo: 'bar three', + }, + { + count: 13, + value: 'second', + foo: 'bar too', + }, + { + count: 14, + value: 'third', + foo: 'bar three', + }, + { + count: 15, + value: 'second', + foo: 'bar too', + }, + { + count: 16, + value: 'third', + foo: 'bar three', + }, + { + count: 17, + value: 'second', + foo: 'bar too', + }, + { + count: 18, + value: 'third', + foo: 'bar three', + }, + { + count: 19, + value: 'second', + foo: 'bar too', + }, + { + count: 20, + value: 'third', + foo: 'bar three', + }, + ], }, - value: 'first', - foo: 'bar', - bar: 'baz', - }, - { - count: 1, - value: 'second', - foo: 'bar too', - }, - { - count: 2, - value: 'third', - foo: 'bar three', - }, - { - count: 3, - value: 'second', - foo: 'bar too', - }, - { - count: 4, - value: 'third', - foo: 'bar three', - }, - { - count: 5, - value: 'second', - foo: 'bar too', - }, - { - count: 6, - value: 'third', - foo: 'bar three', - }, - { - count: 7, - value: 'second', - foo: 'bar too', - }, - { - count: 8, - value: 'third', - foo: 'bar three', - }, - { - count: 9, - value: 'second', - foo: 'bar too', - }, - { - count: 10, - value: 'third', - foo: 'bar three', - }, - { - count: 11, - value: 'second', - foo: 'bar too', - }, - { - count: 12, - value: 'third', - foo: 'bar three', - }, - { - count: 13, - value: 'second', - foo: 'bar too', - }, - { - count: 14, - value: 'third', - foo: 'bar three', - }, - { - count: 15, - value: 'second', - foo: 'bar too', - }, - { - count: 16, - value: 'third', - foo: 'bar three', - }, - { - count: 17, - value: 'second', - foo: 'bar too', - }, - { - count: 18, - value: 'third', - foo: 'bar three', - }, - { - count: 19, - value: 'second', - foo: 'bar too', - }, - { - count: 20, - value: 'third', - foo: 'bar three', - }, - - ], - }, - defs: { - here: [ - 'count', - { - key: 'value', - label: 'The value column', - styling: { - backgroundColor: 'blue', - color: 'lightgrey', + defs: { + here: [ + 'count', + { + key: 'value', + label: 'The value column', + styling: { + backgroundColor: 'blue', + color: 'lightgrey', + }, + }, + 'foo', + ], }, - }, - 'foo', - ] - }, - }), 2750)); + }), + 2750 + ) + ); } if (url === 'get string') { - return new Promise(res => setTimeout(() => res({ - deets: { - here: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', - }, - }), 750)); + return new Promise(res => + setTimeout( + () => + res({ + deets: { + here: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + }, + }), + 750 + ) + ); } if (url === 'get list') { - return new Promise(res => setTimeout(() => res({ - deets: { - here: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'.split(' '), - }, - }), 750)); + return new Promise(res => + setTimeout( + () => + res({ + deets: { + here: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'.split( + ' ' + ), + }, + }), + 750 + ) + ); } - if (url === 'infinite load') return new Promise(res => setTimeout(() => res('you are patient, eh?'), 1500000)); - if (url === 'deets err') return new Promise((res, rej) => setTimeout(() => rej(new Error('you knew this would happen')), 600)); + if (url === 'infinite load') + return new Promise(res => setTimeout(() => res('you are patient, eh?'), 1500000)); + if (url === 'deets err') + return new Promise((res, rej) => setTimeout(() => rej(new Error('you knew this would happen')), 600)); if (url === 'deets 404') return new Promise(res => res({})); - return new Promise(res => setTimeout(() => res({ val: url.length ** 2 }), 150)); + return new Promise(res => setTimeout(() => res({ val: url.length ** 2 }), 150)); }, fetchDeepDependencyGraph(query) { return getJSON(`${ANALYTICS_ROOT}v1/dependencies`, { query }); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css index 959c6106ee..72616b8535 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.css @@ -43,7 +43,7 @@ limitations under the License. } .DdgNodeContent--core.is-missingDecoration { - background-color: #E58C33; + background-color: #e58c33; } .DdgNodeContent--core.is-positioned { diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx index 1e52a4815e..f839d944d3 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx @@ -49,7 +49,9 @@ import { TDdgVertex, PathElem, } from '../../../../model/ddg/types'; -import extractDecorationFromState, { TDecorationFromState } from '../../../../model/path-agnostic-decorations'; +import extractDecorationFromState, { + TDecorationFromState, +} from '../../../../model/path-agnostic-decorations'; import { ReduxState } from '../../../../types/index'; import './index.css'; @@ -58,28 +60,30 @@ type TDispatchProps = { getDecoration: (id: string, svc: string, op?: string) => void; }; -type TProps = RouteComponentProps & TDispatchProps & TDecorationFromState & { - focalNodeUrl: string | null; - focusPathsThroughVertex: (vertexKey: string) => void; - getGenerationVisibility: (vertexKey: string, direction: EDirection) => ECheckedStatus | null; - getVisiblePathElems: (vertexKey: string) => PathElem[] | undefined; - hideVertex: (vertexKey: string) => void; - isFocalNode: boolean; - isPositioned: boolean; - operation: string | string[] | null; - selectVertex: (selectedVertex: TDdgVertex) => void; - service: string; - setOperation: (operation: string) => void; - setViewModifier: (visIndices: number[], viewModifier: EViewModifier, isEnabled: boolean) => void; - updateGenerationVisibility: (vertexKey: string, direction: EDirection) => void; - vertex: TDdgVertex; - vertexKey: string; -}; +type TProps = RouteComponentProps & + TDispatchProps & + TDecorationFromState & { + focalNodeUrl: string | null; + focusPathsThroughVertex: (vertexKey: string) => void; + getGenerationVisibility: (vertexKey: string, direction: EDirection) => ECheckedStatus | null; + getVisiblePathElems: (vertexKey: string) => PathElem[] | undefined; + hideVertex: (vertexKey: string) => void; + isFocalNode: boolean; + isPositioned: boolean; + operation: string | string[] | null; + selectVertex: (selectedVertex: TDdgVertex) => void; + service: string; + setOperation: (operation: string) => void; + setViewModifier: (visIndices: number[], viewModifier: EViewModifier, isEnabled: boolean) => void; + updateGenerationVisibility: (vertexKey: string, direction: EDirection) => void; + vertex: TDdgVertex; + vertexKey: string; + }; type TState = { childrenVisibility?: ECheckedStatus | null; parentVisibility?: ECheckedStatus | null; -} +}; export function getNodeRenderer({ baseUrl, @@ -110,9 +114,7 @@ export function getNodeRenderer({ const { isFocalNode, key, operation, service } = vertex; return ( ); }; -}; +} export function measureNode() { const diameter = 2 * (RADIUS + 1); @@ -165,7 +167,9 @@ export class UnconnectedDdgNodeContent extends React.PureComponent { @@ -176,7 +180,7 @@ export class UnconnectedDdgNodeContent extends React.PureComponent { const { decorationValue, selectVertex, vertex } = this.props; if (decorationValue) selectVertex(vertex); - } + }; private hideVertex = () => { const { hideVertex, vertexKey } = this.props; @@ -251,7 +255,15 @@ export class UnconnectedDdgNodeContent extends React.PureComponent): TDispatchProps { - const { getDecoration } = bindActionCreators( - padActions, - dispatch - ); + const { getDecoration } = bindActionCreators(padActions, dispatch); return { getDecoration, }; } -const DdgNodeContent = withRouter(connect(extractDecorationFromState, mapDispatchToProps)(UnconnectedDdgNodeContent)); +const DdgNodeContent = withRouter( + connect( + extractDecorationFromState, + mapDispatchToProps + )(UnconnectedDdgNodeContent) +); export default DdgNodeContent; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css index 48a2504721..cdc7deb58e 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css @@ -17,19 +17,19 @@ limitations under the License. .DetailsCard { display: flex; flex-direction: column; - padding: .5em; + padding: 0.5em; } .DetailsCard--Header { border-bottom: solid 1px #ddd; box-shadow: 0px 5px 5px -5px rgba(0, 0, 0, 0.3); font-size: 1.5em; - padding: .1em .3em; + padding: 0.1em 0.3em; } .DetailsCard--DetailsWrapper { overflow: scroll; - margin-top: .5em; + margin-top: 0.5em; } .DetailsCard--DetailsWrapper th { diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx index fcb5546ff9..22ea07dd8b 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx @@ -16,14 +16,19 @@ import * as React from 'react'; import { List, Table } from 'antd'; import _isEmpty from 'lodash/isEmpty'; -import { TPadColumnDef, TPadColumnDefs, TPadDetails, TPadRow, TStyledValue } from '../../../../model/path-agnostic-decorations/types'; +import { + TPadColumnDef, + TPadColumnDefs, + TPadDetails, + TPadRow, + TStyledValue, +} from '../../../../model/path-agnostic-decorations/types'; import './index.css'; const { Column } = Table; const { Item } = List; - type TProps = { className?: string; columnDefs?: TPadColumnDefs; @@ -41,7 +46,11 @@ export default class DetailsCard extends React.PureComponent { return ( {s}} + renderItem={(s: string) => ( + + {s} + + )} /> ); } @@ -68,9 +77,9 @@ export default class DetailsCard extends React.PureComponent { if (!cellData || typeof cellData !== 'object') return null; const { styling } = cellData; if (_isEmpty(styling)) return null; - return ({ + return { style: styling, - }) + }; }, onHeaderCell: () => ({ style, @@ -84,7 +93,7 @@ export default class DetailsCard extends React.PureComponent { const aValue = typeof aData === 'object' && aData.value !== undefined ? aData.value : aData; const bData = b[dataIndex]; const bValue = typeof bData === 'object' && bData.value !== undefined ? bData.value : bData; - return (aValue < bValue ? 1 : bValue < aValue ? -1 : 0); + return aValue < bValue ? 1 : bValue < aValue ? -1 : 0; }, }; @@ -94,30 +103,26 @@ export default class DetailsCard extends React.PureComponent { renderTable(details: TPadRow[]) { const { columnDefs: _columnDefs } = this.props; const columnDefs: TPadColumnDefs = _columnDefs ? _columnDefs.slice() : []; - const knownColumns = new Set(columnDefs.map(keyOrObj => { - if (typeof keyOrObj === 'string') return keyOrObj; - return keyOrObj.key; - })); + const knownColumns = new Set( + columnDefs.map(keyOrObj => { + if (typeof keyOrObj === 'string') return keyOrObj; + return keyOrObj.key; + }) + ); details.forEach(row => { Object.keys(row).forEach((col: string) => { if (!knownColumns.has(col)) { knownColumns.add(col); columnDefs.push(col); - }; + } }); }); return ( - +
{columnDefs.map(this.renderColumn)} -
- ); + + ); } renderDetails() { @@ -143,9 +148,7 @@ export default class DetailsCard extends React.PureComponent { {header} {description &&

{description}

}
-
- {this.renderDetails()} -
+
{this.renderDetails()}
); } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css index c50eeca12f..32d08856c8 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css @@ -14,7 +14,6 @@ See the License for the specific language governing permissions and limitations under the License. */ - .Ddg--DetailsPanel { background-color: #fafafa; border-left: solid 1px #ddd; @@ -29,13 +28,13 @@ limitations under the License. box-shadow: 0px 3px 3px rgba(0, 0, 0, 0.1); font-size: 1.5em; text-align: right; - padding: .1em .3em; + padding: 0.1em 0.3em; } .Ddg--DetailsPanel--DecorationHeader { font-size: 1.1em; text-align: center; - padding: .3em; + padding: 0.3em; } .Ddg--DetailsPanel--DecorationHeader > span { @@ -44,7 +43,7 @@ limitations under the License. } .Ddg--DetailsPanel--errorMsg { - color: #E58C33; + color: #e58c33; } .Ddg--DetailsPanel--DetailsCard { @@ -54,7 +53,7 @@ limitations under the License. } .Ddg--DetailsPanel--DetailsCard.is-error { - color: #E58C33; + color: #e58c33; } .Ddg--DetailsPanel--LoadingIndicator { diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx index cfa2157587..0cd22c8486 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx @@ -21,13 +21,16 @@ import LoadingIndicator from '../../common/LoadingIndicator'; import ColumnResizer from '../../../components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer'; import JaegerAPI from '../../../api/jaeger'; import extractDecorationFromState, { TDecorationFromState } from '../../../model/path-agnostic-decorations'; -import { TPathAgnosticDecorationSchema, TPadColumnDefs, TPadDetails } from '../../../model/path-agnostic-decorations/types'; +import { + TPathAgnosticDecorationSchema, + TPadColumnDefs, + TPadDetails, +} from '../../../model/path-agnostic-decorations/types'; import stringSupplant from '../../../utils/stringSupplant'; import DetailsCard from './DetailsCard'; import './DetailsPanel.css'; - type TProps = TDecorationFromState & { decorationSchema: TPathAgnosticDecorationSchema; service: string; @@ -43,16 +46,17 @@ type TState = { }; export class UnconnectedDetailsPanel extends React.PureComponent { - state: TState = {}; + state: TState = {}; componentDidMount() { this.fetchDetails(); } componentDidUpdate(prevProps: TProps) { - if (prevProps.operation !== this.props.operation - || prevProps.service !== this.props.service - || prevProps.decorationSchema !== this.props.decorationSchema + if ( + prevProps.operation !== this.props.operation || + prevProps.service !== this.props.service || + prevProps.decorationSchema !== this.props.decorationSchema ) { this.setState({ details: undefined, @@ -75,7 +79,7 @@ export class UnconnectedDetailsPanel extends React.PureComponent }, operation: _op, service, - }= this.props; + } = this.props; const operation = _op && !Array.isArray(_op) ? _op : undefined; @@ -99,16 +103,14 @@ export class UnconnectedDetailsPanel extends React.PureComponent JaegerAPI.fetchDecoration(fetchUrl) .then(res => { let detailsErred = false; - let details = _get(res, (getDetailPath as string)); + let details = _get(res, getDetailPath as string); if (details === undefined) { details = `\`${getDetailPath}\` not found in response`; detailsErred = true; } - const columnDefs: TPadColumnDefs = getDefPath - ? _get(res, getDefPath, []) - : []; + const columnDefs: TPadColumnDefs = getDefPath ? _get(res, getDefPath, []) : []; - this.setState({ columnDefs, details, detailsErred, detailsLoading: false }) + this.setState({ columnDefs, details, detailsErred, detailsLoading: false }); }) .catch(err => { this.setState({ @@ -116,33 +118,39 @@ export class UnconnectedDetailsPanel extends React.PureComponent detailsErred: true, detailsLoading: false, }); - }) + }); } onResize = (width: number) => { this.setState({ width }); - } + }; render() { - const { decorationProgressbar, decorationColor, decorationMax, decorationSchema, decorationValue, operation: _op, service } = this.props; + const { + decorationProgressbar, + decorationColor, + decorationMax, + decorationSchema, + decorationValue, + operation: _op, + service, + } = this.props; const { width = 0.3 } = this.state; const operation = _op && !Array.isArray(_op) ? _op : undefined; return (
- {operation && } + + {operation && }
{stringSupplant(decorationSchema.name, { service, operation })}
- {decorationProgressbar - ? ( -
- {decorationProgressbar} -
- ) - : {decorationValue} - } + {decorationProgressbar ? ( +
{decorationProgressbar}
+ ) : ( + {decorationValue} + )} {this.state.detailsLoading && (
@@ -156,15 +164,9 @@ export class UnconnectedDetailsPanel extends React.PureComponent header="Details" /> )} - +
- ) + ); } } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.css index 487c2a0873..6c4e1dba2f 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.css @@ -21,7 +21,8 @@ limitations under the License. display: flex; } -.Ddg--SidePanel--Btns, .Ddg--SidePanel--DecorationBtns { +.Ddg--SidePanel--Btns, +.Ddg--SidePanel--DecorationBtns { display: flex; flex-direction: column; } @@ -34,8 +35,8 @@ limitations under the License. cursor: pointer; } - -.Ddg--SidePanel--closeBtn, .Ddg--SidePanel--infoBtn { +.Ddg--SidePanel--closeBtn, +.Ddg--SidePanel--infoBtn { background: transparent; border: none; margin: 5px 0px; @@ -53,7 +54,7 @@ limitations under the License. } .Ddg--SidePanel--closeBtn:focus, -.Ddg--SidePanel--infoBtn:focus{ +.Ddg--SidePanel--infoBtn:focus { outline: none; } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx index 797021f75c..3ead80c503 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx @@ -13,23 +13,17 @@ // limitations under the License. import * as React from 'react'; -import { CircularProgressbar } from 'react-circular-progressbar'; +import { Modal, Table } from 'antd'; import MdExitToApp from 'react-icons/lib/md/exit-to-app'; import MdInfoOutline from 'react-icons/lib/md/info-outline'; -import 'react-circular-progressbar/dist/styles.css'; - - -import { - TDdgVertex, -} from '../../../model/ddg/types'; +import { TDdgVertex } from '../../../model/ddg/types'; import { TPathAgnosticDecorationSchema } from '../../../model/path-agnostic-decorations/types'; import { getConfigValue } from '../../../utils/config/get-config'; import DetailsPanel from './DetailsPanel'; import './index.css'; - type TProps = { clearSelected: () => void; selectDecoration: (decoration?: string) => void; @@ -37,15 +31,7 @@ type TProps = { selectedVertex?: TDdgVertex; }; -type TState = { - collapsed: boolean; -}; - -export default class SidePanel extends React.PureComponent { - state = { - collapsed: false, - }; - +export default class SidePanel extends React.PureComponent { decorations: TPathAgnosticDecorationSchema[] | undefined; constructor(props: TProps) { @@ -56,54 +42,79 @@ export default class SidePanel extends React.PureComponent { clearSelected = () => { this.props.clearSelected(); - } + }; + + openInfoModal = () => { + Modal.info({ + content: ( + + ), + maskClosable: true, + title: 'Decoration Options', + width: '60vw', + }); + }; render() { if (!this.decorations) return null; - const { - clearSelected, - selectDecoration, - selectedDecoration, - selectedVertex, - } = this.props; + const { clearSelected, selectDecoration, selectedDecoration, selectedVertex } = this.props; const selectedSchema = this.decorations.find(({ id }) => id === selectedDecoration); return (
-
- {this.decorations.map(({ acronym, id, name }) => ( - - ))} + {this.decorations.map(({ acronym, id, name }) => ( + + ))}
-
- {selectedVertex && selectedSchema && } + {selectedVertex && selectedSchema && ( + + )}
); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx index 6823b7f18e..7154c8da9f 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx @@ -215,7 +215,7 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent { this.setState({ selectedVertex }); - } + }; showVertices = (vertexKeys: string[]) => { const { graph, urlState } = this.props; diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx index e0cc829866..4ca8af365d 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx @@ -117,7 +117,10 @@ export default class TimelineColumnResizer extends React.PureComponent< draggerStyle = gripStyle; } return ( -
+
- ) : undefined; + }, + }} + maxValue={decorationMax} + strokeWidth={(PROGRESS_BAR_STROKE_WIDTH / RADIUS) * 50} + text={`${decorationValue}`} + value={decorationValue} + /> + ) : ( + undefined + ); - return { decorationProgressbar, decorationBackgroundColor, decorationColor, decorationID, decorationValue, decorationMax }; + return { + decorationProgressbar, + decorationBackgroundColor, + decorationColor, + decorationID, + decorationValue, + decorationMax, + }; } diff --git a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx index 08216d8e4d..5237741106 100644 --- a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx +++ b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx @@ -83,9 +83,10 @@ export function getDecorationDone(state: TPathAgnosticDecorationsState, payload? export default handleActions( { - [`${actionTypes.GET_DECORATION}_FULFILLED`]: guardReducer( - getDecorationDone - ), + [`${actionTypes.GET_DECORATION}_FULFILLED`]: guardReducer< + TPathAgnosticDecorationsState, + TNewData | undefined + >(getDecorationDone), }, {} ); From 276f638b1c4e2660722cbdcd812615441e31021d Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Mon, 6 Apr 2020 16:22:20 -0400 Subject: [PATCH 18/31] Fix: rowKeys, setViewModifier argument name, decoration header size, destructured variable order, DeepDependencies/index initial state, stale comments, yarn.lock @types/node, yarn.lock registry urls, 'value' in paths, existing tests, op-specific details Add: linking row cells TODO: New tests Signed-off-by: Everett Ross --- .../actions/path-agnostic-decorations.test.js | 77 ++--- .../src/actions/path-agnostic-decorations.tsx | 4 +- .../__snapshots__/index.test.js.snap | 264 ++---------------- .../Graph/DdgNodeContent/index.test.js | 18 +- .../Graph/DdgNodeContent/index.tsx | 11 +- .../SidePanel/DetailsCard/index.tsx | 11 +- .../SidePanel/DetailsPanel.css | 2 +- .../SidePanel/DetailsPanel.tsx | 4 +- .../DeepDependencies/SidePanel/index.tsx | 1 + .../src/components/DeepDependencies/index.tsx | 4 +- .../TimelineColumnResizer.tsx | 2 +- .../src/constants/default-config.tsx | 147 +++++----- .../model/path-agnostic-decorations/index.tsx | 12 +- .../model/path-agnostic-decorations/types.tsx | 6 +- .../path-agnostic-decorations.test.js | 6 +- .../reducers/path-agnostic-decorations.tsx | 4 +- yarn.lock | 14 +- 17 files changed, 196 insertions(+), 391 deletions(-) diff --git a/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js b/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js index 60ca164011..c2ca1b3a61 100644 --- a/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js +++ b/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js @@ -25,10 +25,10 @@ describe('getDecoration', () => { let resolves; let rejects; - const opUrl = 'opUrl?service=#service&operation=#operation'; - const url = 'opUrl?service=#service'; - const valuePath = 'withoutOpPath.#service'; - const opValuePath = 'opPath.#service.#operation'; + const opSummaryUrl = 'opSummaryUrl?service=#service&operation=#operation'; + const summaryUrl = 'summaryUrl?service=#service'; + const summaryPath = 'withoutOpPath.#service'; + const opSummaryPath = 'opPath.#service.#operation'; const withOpID = 'decoration id with op url and op path'; const partialID = 'decoration id with op url without op path'; const withoutOpID = 'decoration id with only url'; @@ -48,21 +48,22 @@ describe('getDecoration', () => { getConfigValueSpy = jest.spyOn(getConfig, 'getConfigValue').mockReturnValue([ { id: withOpID, - url, - opUrl, - valuePath, - opValuePath, + summaryUrl, + opSummaryUrl, + summaryPath, + opSummaryPath, }, { id: partialID, - url, - opUrl, - valuePath, + summaryUrl, + opSummaryUrl, + summaryPath, }, { id: withoutOpID, - url, - valuePath, + summaryUrl, + opSummaryUrl, + summaryPath, }, ]); fetchDecorationSpy = jest.spyOn(JaegerAPI, 'fetchDecoration').mockImplementation( @@ -102,42 +103,42 @@ describe('getDecoration', () => { it('resolves to include single response for op decoration given op', async () => { const promise = getDecoration(withOpID, service, operation); - resolves[0](_set({}, stringSupplant(opValuePath, { service, operation }), testVal)); + resolves[0](_set({}, stringSupplant(opSummaryPath, { service, operation }), testVal)); const res = await promise; expect(res).toEqual(_set({}, `${withOpID}.withOp.${service}.${operation}`, testVal)); - expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(opUrl, { service, operation })); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(opSummaryUrl, { service, operation })); }); it('resolves to include single response for op decoration not given op', async () => { const promise = getDecoration(withOpID, service); - resolves[0](_set({}, stringSupplant(valuePath, { service }), testVal)); + resolves[0](_set({}, stringSupplant(summaryPath, { service }), testVal)); const res = await promise; expect(res).toEqual(_set({}, `${withOpID}.withoutOp.${service}`, testVal)); - expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service })); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(summaryUrl, { service })); }); it('resolves to include single response for malformed op decoration given op', async () => { const promise = getDecoration(partialID, service, operation); - resolves[0](_set({}, stringSupplant(valuePath, { service }), testVal)); + resolves[0](_set({}, stringSupplant(summaryPath, { service }), testVal)); const res = await promise; expect(res).toEqual(_set({}, `${partialID}.withoutOp.${service}`, testVal)); - expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service })); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(summaryUrl, { service })); }); it('resolves to include single response for svc decoration given op', async () => { const promise = getDecoration(withoutOpID, service, operation); - resolves[0](_set({}, stringSupplant(valuePath, { service }), testVal)); + resolves[0](_set({}, stringSupplant(summaryPath, { service }), testVal)); const res = await promise; expect(res).toEqual(_set({}, `${withoutOpID}.withoutOp.${service}`, testVal)); - expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service })); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(summaryUrl, { service })); }); it('resolves to include single response for svc decoration not given op', async () => { const promise = getDecoration(withoutOpID, service); - resolves[0](_set({}, stringSupplant(valuePath, { service }), testVal)); + resolves[0](_set({}, stringSupplant(summaryPath, { service }), testVal)); const res = await promise; expect(res).toEqual(_set({}, `${withoutOpID}.withoutOp.${service}`, testVal)); - expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service })); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(summaryUrl, { service })); }); it('handles error responses', async () => { @@ -148,7 +149,7 @@ describe('getDecoration', () => { expect(res0).toEqual( _set({}, `${withoutOpID}.withoutOp.${service}`, `Unable to fetch decoration: ${message}`) ); - expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service })); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(summaryUrl, { service })); const err = 'foo error without message'; const promise1 = getDecoration(withOpID, service, operation); @@ -157,10 +158,10 @@ describe('getDecoration', () => { expect(res1).toEqual( _set({}, `${withOpID}.withOp.${service}.${operation}`, `Unable to fetch decoration: ${err}`) ); - expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(opUrl, { service, operation })); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(opSummaryUrl, { service, operation })); }); - it('defaults value if valuePath not found in response', async () => { + it('defaults value if summaryPath not found in response', async () => { const promise = getDecoration(withoutOpID, service); resolves[0](); const res = await promise; @@ -168,10 +169,10 @@ describe('getDecoration', () => { _set( {}, `${withoutOpID}.withoutOp.${service}`, - `${stringSupplant(valuePath, { service })} not found in response` + `\`${stringSupplant(summaryPath, { service })}\` not found in response` ) ); - expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(url, { service })); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(summaryUrl, { service })); }); it('returns undefined if invoked before previous invocation is resolved', () => { @@ -182,13 +183,13 @@ describe('getDecoration', () => { it('resolves to include responses for all concurrent requests', async () => { const otherOp = 'other op'; const promise = getDecoration(withOpID, service, operation); - resolves[0](_set({}, stringSupplant(opValuePath, { service, operation }), testVal)); + resolves[0](_set({}, stringSupplant(opSummaryPath, { service, operation }), testVal)); getDecoration(partialID, service, operation); - resolves[1](_set({}, stringSupplant(valuePath, { service }), testVal)); + resolves[1](_set({}, stringSupplant(summaryPath, { service }), testVal)); getDecoration(withOpID, service); - resolves[2](_set({}, stringSupplant(valuePath, { service }), testVal)); + resolves[2](_set({}, stringSupplant(summaryPath, { service }), testVal)); getDecoration(withoutOpID, service); - resolves[3](_set({}, stringSupplant(valuePath, { service }), testVal)); + resolves[3](_set({}, stringSupplant(summaryPath, { service }), testVal)); const message = 'foo error message'; getDecoration(withOpID, service, otherOp); rejects[4]({ message }); @@ -222,15 +223,15 @@ describe('getDecoration', () => { it('scopes promises to not include previous promise results', async () => { const otherOp = 'other op'; const promise0 = getDecoration(withOpID, service, operation); - resolves[0](_set({}, stringSupplant(opValuePath, { service, operation }), testVal)); + resolves[0](_set({}, stringSupplant(opSummaryPath, { service, operation }), testVal)); getDecoration(partialID, service, operation); - resolves[1](_set({}, stringSupplant(valuePath, { service }), testVal)); + resolves[1](_set({}, stringSupplant(summaryPath, { service }), testVal)); const res0 = await promise0; const promise1 = getDecoration(withOpID, service); - resolves[2](_set({}, stringSupplant(valuePath, { service }), testVal)); + resolves[2](_set({}, stringSupplant(summaryPath, { service }), testVal)); getDecoration(withoutOpID, service); - resolves[3](_set({}, stringSupplant(valuePath, { service }), testVal)); + resolves[3](_set({}, stringSupplant(summaryPath, { service }), testVal)); const message = 'foo error message'; getDecoration(withOpID, service, otherOp); rejects[4]({ message }); @@ -272,7 +273,7 @@ describe('getDecoration', () => { it('no-ops for already processed id, service, and operation', async () => { const promise0 = getDecoration(withOpID, service, operation); - resolves[0](_set({}, stringSupplant(opValuePath, { service, operation }), testVal)); + resolves[0](_set({}, stringSupplant(opSummaryPath, { service, operation }), testVal)); const res0 = await promise0; expect(res0).toEqual(_set({}, `${withOpID}.withOp.${service}.${operation}`, testVal)); @@ -280,7 +281,7 @@ describe('getDecoration', () => { expect(promise1).toBeUndefined(); const promise2 = getDecoration(withoutOpID, service); - resolves[1](_set({}, stringSupplant(valuePath, { service }), testVal)); + resolves[1](_set({}, stringSupplant(summaryPath, { service }), testVal)); const res1 = await promise2; expect(res1).toEqual(_set({}, `${withoutOpID}.withoutOp.${service}`, testVal)); expect(fetchDecorationSpy).toHaveBeenCalledTimes(2); diff --git a/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx b/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx index 6e3bd5d532..84a78c23cc 100644 --- a/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx +++ b/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx @@ -75,13 +75,13 @@ export function getDecoration( // TODO: lru memoize it up if chrome doesn't cache promise = JaegerAPI.fetchDecoration(stringSupplant(opSummaryUrl, { service, operation })); getPath = stringSupplant(opSummaryPath, { service, operation }); - setPath = `${id}.withOp.${service}.${operation}.value`; + setPath = `${id}.withOp.${service}.${operation}`; } else { // TODO: lru memoize it up if chrome doesn't cache promise = JaegerAPI.fetchDecoration(stringSupplant(summaryUrl, { service })); getPath = stringSupplant(summaryPath, { service }); getPath = summaryPath; - setPath = `${id}.withoutOp.${service}.value`; + setPath = `${id}.withoutOp.${service}`; } promise diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap index 2fcb38b9c8..9cae7d85fc 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap @@ -1,240 +1,28 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[` DdgNodeContent.getNodeRenderer() returns a 1`] = ` - -`; - -exports[` DdgNodeContent.getNodeRenderer() returns a focal 1`] = ` -
-
-
-

- -

-
- -
-
-
- -
+exports[` getNodeRenderer() returns a 1`] = ` +Object { + "focalNodeUrl": "/deep-dependencies?operation=the-operation&service=the-service", + "focusPathsThroughVertex": undefined, + "getGenerationVisibility": undefined, + "getVisiblePathElems": undefined, + "hideVertex": undefined, + "isFocalNode": false, + "isPositioned": false, + "operation": "the-operation", + "selectVertex": undefined, + "service": "the-service", + "setOperation": undefined, + "setViewModifier": undefined, + "updateGenerationVisibility": undefined, + "vertex": Object { + "isFocalNode": false, + "key": "some-key", + "operation": "the-operation", + "service": "the-service", + }, + "vertexKey": "some-key", +} `; exports[` omits the operation if it is null 1`] = ` @@ -245,6 +33,7 @@ exports[` omits the operation if it is null 1`] = ` >
omits the operation if it is null 2`] = ` >
renders correctly when isFocalNode = true and focalNod >
renders correctly when isFocalNode = true and focalNod >
renders the number of operations if there are multiple >
renders the number of operations if there are multiple >
', () => { describe('measureNode', () => { it('returns twice the RADIUS with a buffer for svg border', () => { const diameterWithBuffer = 2 * RADIUS + 2; - expect(DdgNodeContent.measureNode()).toEqual({ + expect(measureNode()).toEqual({ height: diameterWithBuffer, width: diameterWithBuffer, }); @@ -487,7 +487,7 @@ describe('', () => { }); }); - describe('DdgNodeContent.getNodeRenderer()', () => { + describe('getNodeRenderer()', () => { const ddgVertex = { isFocalNode: false, key: 'some-key', @@ -496,8 +496,8 @@ describe('', () => { }; const noOp = () => {}; - it('returns a ', () => { - const ddgNode = DdgNodeContent.getNodeRenderer( + fit('returns a ', () => { + const ddgNode = getNodeRenderer( noOp, noOp, EDdgDensity.PreventPathEntanglement, @@ -506,18 +506,16 @@ describe('', () => { { maxDuration: '100ms' } )(ddgVertex); expect(ddgNode).toBeDefined(); - expect(shallow(ddgNode)).toMatchSnapshot(); - expect(ddgNode.type).toBe(DdgNodeContent); + expect(ddgNode.props).toMatchSnapshot(); }); it('returns a focal ', () => { - const focalNode = DdgNodeContent.getNodeRenderer(noOp, noOp)({ + const focalNode = getNodeRenderer(noOp, noOp)({ ...ddgVertex, isFocalNode: true, }); expect(focalNode).toBeDefined(); - expect(shallow(focalNode)).toMatchSnapshot(); - expect(focalNode.type).toBe(DdgNodeContent); + expect(ddgNode.props).toMatchSnapshot(); }); }); }); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx index f839d944d3..e8e801ff3a 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx @@ -60,7 +60,7 @@ type TDispatchProps = { getDecoration: (id: string, svc: string, op?: string) => void; }; -type TProps = RouteComponentProps & +type TProps = /* RouteComponentProps & */ TDispatchProps & TDecorationFromState & { focalNodeUrl: string | null; @@ -107,7 +107,7 @@ export function getNodeRenderer({ hideVertex: (vertexKey: string) => void; selectVertex: (selectedVertex: TDdgVertex) => void; setOperation: (operation: string) => void; - setViewModifier: (visIndices: number[], viewModifier: EViewModifier, enable: boolean) => void; + setViewModifier: (visIndices: number[], viewModifier: EViewModifier, isEnabled: boolean) => void; updateGenerationVisibility: (vertexKey: string, direction: EDirection) => void; }) { return function renderNode(vertex: TDdgVertex, _: unknown, lv: TLayoutVertex | null) { @@ -382,11 +382,18 @@ export function mapDispatchToProps(dispatch: Dispatch): TDispatchPro }; } + /* const DdgNodeContent = withRouter( connect( extractDecorationFromState, mapDispatchToProps )(UnconnectedDdgNodeContent) ); + */ +const DdgNodeContent = + connect( + extractDecorationFromState, + mapDispatchToProps + )(UnconnectedDdgNodeContent); export default DdgNodeContent; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx index 22ea07dd8b..12c43f67d4 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx @@ -86,7 +86,8 @@ export default class DetailsCard extends React.PureComponent { }), render: (cellData: undefined | string | TStyledValue) => { if (!cellData || typeof cellData !== 'object') return cellData; - return cellData.value; + if (!cellData.linkTo) return cellData.value; + return cellData.value; }, sorter: (a: TPadRow, b: TPadRow) => { const aData = a[dataIndex]; @@ -119,7 +120,13 @@ export default class DetailsCard extends React.PureComponent { }); return ( -
+
JSON.stringify(row)} + > {columnDefs.map(this.renderColumn)}
); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css index 32d08856c8..417afcccf6 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css @@ -32,7 +32,7 @@ limitations under the License. } .Ddg--DetailsPanel--DecorationHeader { - font-size: 1.1em; + font-size: 1.3em; text-align: center; padding: 0.3em; } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx index 0cd22c8486..0621de4077 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx @@ -86,7 +86,7 @@ export class UnconnectedDetailsPanel extends React.PureComponent let fetchUrl: string | undefined; let getDetailPath: string | undefined; let getDefPath: string | undefined; - if (opDetailUrl && opDetailPath) { + if (opDetailUrl && opDetailPath && operation) { fetchUrl = stringSupplant(opDetailUrl, { service, operation }); getDetailPath = stringSupplant(opDetailPath, { service, operation }); getDefPath = opDetailColumnDefPath && stringSupplant(opDetailColumnDefPath, { service, operation }); @@ -164,7 +164,7 @@ export class UnconnectedDetailsPanel extends React.PureComponent header="Details" /> )} - +
); } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx index 3ead80c503..199b07d2be 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx @@ -61,6 +61,7 @@ export default class SidePanel extends React.PureComponent { }, ]} dataSource={this.decorations} + rowKey={(schema: TPathAgnosticDecorationSchema) => schema.id} /> ), maskClosable: true, diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx index 7154c8da9f..602968dce9 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx @@ -96,6 +96,8 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent; export type TPadDetails = string | string[] | TPadRow[]; -export type TPadEntry = { - value: number | string; // string or other type is for data unavailable -}; +export type TPadEntry = number | string; // string or other type is for data unavailable export type TNewData = Record< string, diff --git a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js index f870fd4fa1..bbd508b80a 100644 --- a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js +++ b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js @@ -36,9 +36,11 @@ describe('pathAgnosticDecoration reducers', () => { const service = svc[_service]; const isWithOp = Boolean(operation); + /* const valueObj = { value, }; + */ const payloadKey = isWithOp ? 'withOp' : 'withoutOp'; const payload = { @@ -46,9 +48,9 @@ describe('pathAgnosticDecoration reducers', () => { [payloadKey]: { [service]: isWithOp ? { - [operation]: valueObj, + [operation]: value, } - : valueObj, + : value, }, }, }; diff --git a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx index 5237741106..4c0f2d8cda 100644 --- a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx +++ b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.tsx @@ -26,7 +26,7 @@ export function getDecorationDone(state: TPathAgnosticDecorationsState, payload? const newWithoutOpValues: number[] = []; if (withoutOp) { Object.keys(withoutOp).forEach(service => { - const { value } = withoutOp[service]; + const value = withoutOp[service]; if (typeof value === 'number') newWithoutOpValues.push(value); }); } @@ -35,7 +35,7 @@ export function getDecorationDone(state: TPathAgnosticDecorationsState, payload? if (withOp) { Object.keys(withOp).forEach(service => { Object.keys(withOp[service]).forEach(operation => { - const { value } = withOp[service][operation]; + const value = withOp[service][operation]; if (typeof value === 'number') newWithOpValues.push(value); }); }); diff --git a/yarn.lock b/yarn.lock index 3fe19d2da8..5683b1ab6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1543,8 +1543,6 @@ version "1.3.0" resolved "https://registry.yarnpkg.com/@types%2fobject-hash/-/object-hash-1.3.0.tgz#b20db2074129f71829d61ff404e618c4ac3d73cf" integrity sha1-sg2yB0Ep9xgp1h/0BOYYxKw9c88= - dependencies: - "@types/node" "*" "@types/prop-types@*": version "15.7.0" @@ -5575,7 +5573,7 @@ fsevents@^1.2.3, fsevents@^1.2.7: fstream@1.0.12, fstream@^1.0.0, fstream@^1.0.12: version "1.0.12" - resolved "https://unpm.uberinternal.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" + resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" integrity sha1-Touo7i1Ivk99DeUFRVVI6uWTIEU= dependencies: graceful-fs "^4.1.2" @@ -5899,7 +5897,7 @@ handle-thing@^2.0.0: handlebars@4.1.2, handlebars@^4.0.3, handlebars@^4.1.0: version "4.1.2" - resolved "https://unpm.uberinternal.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" + resolved "https://registry.yarnpkg.com/handlebars/-/handlebars-4.1.2.tgz#b6b37c1ced0306b221e094fc7aca3ec23b131b67" integrity sha1-trN8HO0DBrIh4JT8eso+wjsTG2c= dependencies: neo-async "^2.6.0" @@ -7237,7 +7235,7 @@ js-tokens@^3.0.2: js-yaml@3.13.1, js-yaml@^3.12.0, js-yaml@^3.7.0, js-yaml@^3.9.0: version "3.13.1" - resolved "https://unpm.uberinternal.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" integrity sha1-r/FRswv9+o5J4F2iLnQV6d+jeEc= dependencies: argparse "^1.0.7" @@ -7795,7 +7793,7 @@ lodash.uniq@^4.5.0: lodash@4.17.11, lodash@4.x, "lodash@>=3.5 <5", lodash@^3.10.0, lodash@^4.15.0, lodash@^4.16.5, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.3, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1: version "4.17.11" - resolved "https://unpm.uberinternal.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.11.tgz#b39ea6229ef607ecd89e2c8df12536891cac9b8d" integrity sha1-s56mIp72B+zYniyN8SU2iRysm40= logfmt@^1.2.0: @@ -10435,7 +10433,7 @@ react-app-rewired@2.0.1: react-circular-progressbar@^2.0.3: version "2.0.3" - resolved "https://unpm.uberinternal.com/react-circular-progressbar/-/react-circular-progressbar-2.0.3.tgz#fa8eb59f8db168d2904bae4590641792c80f5991" + resolved "https://registry.yarnpkg.com/react-circular-progressbar/-/react-circular-progressbar-2.0.3.tgz#fa8eb59f8db168d2904bae4590641792c80f5991" integrity sha1-+o61n42xaNKQS65FkGQXksgPWZE= react-dev-utils@^7.0.0: @@ -12133,7 +12131,7 @@ tapable@^1.0.0, tapable@^1.1.0: tar@2.2.2, tar@^2.0.0: version "2.2.2" - resolved "https://unpm.uberinternal.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" + resolved "https://registry.yarnpkg.com/tar/-/tar-2.2.2.tgz#0ca8848562c7299b8b446ff6a4d60cdbb23edc40" integrity sha1-DKiEhWLHKZuLRG/2pNYM27I+3EA= dependencies: block-stream "*" From 1856a14d9fcfe470465bff044696009a876eff29 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Tue, 7 Apr 2020 14:07:53 -0400 Subject: [PATCH 19/31] Handle linked cells, fix cell sort order Signed-off-by: Everett Ross --- .../DeepDependencies/SidePanel/DetailsCard/index.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx index 12c43f67d4..c3bfdc05c4 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx @@ -87,14 +87,18 @@ export default class DetailsCard extends React.PureComponent { render: (cellData: undefined | string | TStyledValue) => { if (!cellData || typeof cellData !== 'object') return cellData; if (!cellData.linkTo) return cellData.value; - return cellData.value; + return {cellData.value}; }, sorter: (a: TPadRow, b: TPadRow) => { const aData = a[dataIndex]; const aValue = typeof aData === 'object' && aData.value !== undefined ? aData.value : aData; const bData = b[dataIndex]; const bValue = typeof bData === 'object' && bData.value !== undefined ? bData.value : bData; - return aValue < bValue ? 1 : bValue < aValue ? -1 : 0; + return aValue < bValue ? -1 : bValue < aValue ? 1 : 0; }, }; From 0d9dc944c74ad9db92a054b76282242cf617ae6a Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Tue, 7 Apr 2020 17:53:52 -0400 Subject: [PATCH 20/31] Test existing files, track decorations viewed, memoize summary requests TODO: Test SidePanel/ index, index.track, DetailsPanel Signed-off-by: Everett Ross --- .../actions/path-agnostic-decorations.test.js | 7 +- .../src/actions/path-agnostic-decorations.tsx | 20 +- packages/jaeger-ui/src/api/jaeger.js | 14 +- .../__snapshots__/index.test.js.snap | 628 ++++++++++++++++++ .../Graph/DdgNodeContent/index.test.js | 89 ++- .../Graph/DdgNodeContent/index.tsx | 11 +- .../SidePanel/DetailsPanel.tsx | 6 +- .../SidePanel/index.track.tsx | 34 + .../DeepDependencies/SidePanel/index.tsx | 21 +- .../components/DeepDependencies/index.test.js | 52 ++ .../DeepDependencies/index.track.test.js | 9 + .../components/DeepDependencies/url.test.js | 7 +- .../src/constants/default-config.tsx | 9 + packages/jaeger-ui/src/setupProxy.js | 11 + 14 files changed, 883 insertions(+), 35 deletions(-) create mode 100644 packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.track.tsx diff --git a/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js b/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js index c2ca1b3a61..f364b08052 100644 --- a/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js +++ b/packages/jaeger-ui/src/actions/path-agnostic-decorations.test.js @@ -14,11 +14,13 @@ import _set from 'lodash/set'; -import { processed, getDecoration as getDecorationImpl } from './path-agnostic-decorations'; +import { _processed, getDecoration as getDecorationImpl } from './path-agnostic-decorations'; import * as getConfig from '../utils/config/get-config'; import stringSupplant from '../utils/stringSupplant'; import JaegerAPI from '../api/jaeger'; +jest.mock('lru-memoize', () => () => x => x); + describe('getDecoration', () => { let getConfigValueSpy; let fetchDecorationSpy; @@ -62,7 +64,6 @@ describe('getDecoration', () => { { id: withoutOpID, summaryUrl, - opSummaryUrl, summaryPath, }, ]); @@ -77,7 +78,7 @@ describe('getDecoration', () => { beforeEach(() => { fetchDecorationSpy.mockClear(); - processed.clear(); + _processed.clear(); resolves = []; rejects = []; }); diff --git a/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx b/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx index 84a78c23cc..880163a7fa 100644 --- a/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx +++ b/packages/jaeger-ui/src/actions/path-agnostic-decorations.tsx @@ -15,6 +15,7 @@ import _get from 'lodash/get'; import _memoize from 'lodash/memoize'; import _set from 'lodash/set'; +import memoize from 'lru-memoize'; import { createActions, ActionFunctionAny, Action } from 'redux-actions'; import JaegerAPI from '../api/jaeger'; @@ -23,9 +24,12 @@ import { getConfigValue } from '../utils/config/get-config'; import generateActionTypes from '../utils/generate-action-types'; import stringSupplant from '../utils/stringSupplant'; +// wrapping JaegerAPI.fetchDecoration is necessary for tests to properly mock inside memoization +const fetchDecoration = memoize(10)((url: string) => JaegerAPI.fetchDecoration(url)); + export const actionTypes = generateActionTypes('@jaeger-ui/PATH_AGNOSTIC_DECORATIONS', ['GET_DECORATION']); -const getDecorationSchema = _memoize((id: string): TPathAgnosticDecorationSchema | undefined => { +export const getDecorationSchema = _memoize((id: string): TPathAgnosticDecorationSchema | undefined => { const schemas = getConfigValue('pathAgnosticDecorations') as TPathAgnosticDecorationSchema[] | undefined; if (!schemas) return undefined; return schemas.find(s => s.id === id); @@ -39,16 +43,17 @@ let resolve: undefined | ((arg: TNewData) => void); // Bespoke memoization-adjacent solution necessary as this should return `undefined`, not an old promise, on // duplicate calls -export const processed = new Map>>(); +// exported for tests +export const _processed = new Map>>(); export function getDecoration( id: string, service: string, operation?: string ): Promise | undefined { - const processedID = processed.get(id); + const processedID = _processed.get(id); if (!processedID) { - processed.set(id, new Map>([[service, new Set([operation])]])); + _processed.set(id, new Map>([[service, new Set([operation])]])); } else { const processedService = processedID.get(service); if (!processedService) processedID.set(service, new Set([operation])); @@ -72,15 +77,12 @@ export function getDecoration( let getPath: string; let setPath: string; if (opSummaryPath && opSummaryUrl && operation) { - // TODO: lru memoize it up if chrome doesn't cache - promise = JaegerAPI.fetchDecoration(stringSupplant(opSummaryUrl, { service, operation })); + promise = fetchDecoration(stringSupplant(opSummaryUrl, { service, operation })); getPath = stringSupplant(opSummaryPath, { service, operation }); setPath = `${id}.withOp.${service}.${operation}`; } else { - // TODO: lru memoize it up if chrome doesn't cache - promise = JaegerAPI.fetchDecoration(stringSupplant(summaryUrl, { service })); + promise = fetchDecoration(stringSupplant(summaryUrl, { service })); getPath = stringSupplant(summaryPath, { service }); - getPath = summaryPath; setPath = `${id}.withoutOp.${service}`; } diff --git a/packages/jaeger-ui/src/api/jaeger.js b/packages/jaeger-ui/src/api/jaeger.js index 6bcebf7761..ad9eac907f 100644 --- a/packages/jaeger-ui/src/api/jaeger.js +++ b/packages/jaeger-ui/src/api/jaeger.js @@ -81,7 +81,12 @@ const JaegerAPI = { return getJSON(`${this.apiRoot}archive/${id}`, { method: 'POST' }); }, fetchDecoration(url) { - // return getJSON(url); + console.log('calling url: ', url); + return getJSON(url); + // eslint-disable-next-line no-unreachable + if (url.startsWith('/analytics') || url.startsWith('/api/serviceedges')) { + return getJSON(url); + } if (url.length % 2 && url.startsWith('neapolitan')) { return new Promise((res, rej) => setTimeout(() => { @@ -90,6 +95,7 @@ const JaegerAPI = { }, 150) ); } + // eslint-disable-next-line no-unreachable if (url === 'get graph') { return new Promise(res => setTimeout( @@ -230,6 +236,7 @@ const JaegerAPI = { ) ); } + // eslint-disable-next-line no-unreachable if (url === 'get string') { return new Promise(res => setTimeout( @@ -244,6 +251,7 @@ const JaegerAPI = { ) ); } + // eslint-disable-next-line no-unreachable if (url === 'get list') { return new Promise(res => setTimeout( @@ -259,11 +267,15 @@ const JaegerAPI = { ) ); } + // eslint-disable-next-line no-unreachable if (url === 'infinite load') return new Promise(res => setTimeout(() => res('you are patient, eh?'), 1500000)); + // eslint-disable-next-line no-unreachable if (url === 'deets err') return new Promise((res, rej) => setTimeout(() => rej(new Error('you knew this would happen')), 600)); + // eslint-disable-next-line no-unreachable if (url === 'deets 404') return new Promise(res => res({})); + // eslint-disable-next-line no-unreachable return new Promise(res => setTimeout(() => res({ val: url.length ** 2 }), 150)); // return getJSON(url); }, diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap index 9cae7d85fc..80f10f081d 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap @@ -25,6 +25,31 @@ Object { } `; +exports[` getNodeRenderer() returns a focal 1`] = ` +Object { + "focalNodeUrl": null, + "focusPathsThroughVertex": undefined, + "getGenerationVisibility": undefined, + "getVisiblePathElems": undefined, + "hideVertex": undefined, + "isFocalNode": true, + "isPositioned": false, + "operation": "the-operation", + "selectVertex": undefined, + "service": "the-service", + "setOperation": undefined, + "setViewModifier": undefined, + "updateGenerationVisibility": undefined, + "vertex": Object { + "isFocalNode": true, + "key": "some-key", + "operation": "the-operation", + "service": "the-service", + }, + "vertexKey": "some-key", +} +`; + exports[` omits the operation if it is null 1`] = `
omits the operation if it is null 2`] = `
`; +exports[` renders correctly when decorationValue is a string 1`] = ` + +`; + +exports[` renders correctly when decorationValue is a string 2`] = ` + +`; + +exports[` renders correctly when given decorationProgressbar 1`] = ` + +`; + +exports[` renders correctly when given decorationProgressbar 2`] = ` + +`; + exports[` renders correctly when isFocalNode = true and focalNodeUrl = null 1`] = `
', () => { - const vertexKey = 'some-key'; - const service = 'some-service'; + const decorationID = 'test decorationID' + const decorationValue = 42; const operation = 'some-operation'; const operationArray = ['op0', 'op1', 'op2', 'op3']; + const service = 'some-service'; + const vertexKey = 'some-key'; const props = { focalNodeUrl: 'some-url', focusPathsThroughVertex: jest.fn(), + getDecoration: jest.fn(), getGenerationVisibility: jest.fn(), getVisiblePathElems: jest.fn(), hideVertex: jest.fn(), isFocalNode: false, operation, + selectVertex: jest.fn(), setOperation: jest.fn(), setViewModifier: jest.fn(), service, updateGenerationVisibility: jest.fn(), + vertex: { + key: vertexKey, + }, vertexKey, }; let wrapper; beforeEach(() => { + props.getDecoration.mockClear(); props.getGenerationVisibility.mockReturnValue(null).mockClear(); props.getVisiblePathElems.mockReset(); + props.selectVertex.mockReset(); props.setViewModifier.mockReset(); props.updateGenerationVisibility.mockReset(); wrapper = shallow(); @@ -84,6 +93,61 @@ describe('', () => { expect(wrapper).toMatchSnapshot(); }); + it('renders correctly when given decorationProgressbar', () => { + expect(wrapper).toMatchSnapshot(); + + const decorationProgressbar = Test progressbar; + wrapper.setProps({ decorationProgressbar, decorationValue }); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders correctly when decorationValue is a string', () => { + expect(wrapper).toMatchSnapshot(); + + wrapper.setProps({ decorationValue: 'Error: Status Code 418' }); + expect(wrapper).toMatchSnapshot(); + }); + + describe('getDecoration', () => { + it('gets decoration on mount or change of props.decorationID iff props.decorationID is truthy', () => { + expect(props.getDecoration).not.toHaveBeenCalled(); + + wrapper.setProps({ decorationID }); + expect(props.getDecoration).toHaveBeenCalledTimes(1); + expect(props.getDecoration).toHaveBeenLastCalledWith(decorationID, service, operation); + + wrapper.setProps({ decorationID }); + expect(props.getDecoration).toHaveBeenCalledTimes(1); + + const newDecorationID = `new ${decorationID}`; + wrapper.setProps({ decorationID: newDecorationID }); + expect(props.getDecoration).toHaveBeenCalledTimes(2); + expect(props.getDecoration).toHaveBeenLastCalledWith(newDecorationID, service, operation); + + wrapper.setProps({ decorationID, operation: operationArray }); + expect(props.getDecoration).toHaveBeenCalledTimes(3); + expect(props.getDecoration).toHaveBeenLastCalledWith(decorationID, service, undefined); + + shallow(); + expect(props.getDecoration).toHaveBeenCalledTimes(4); + expect(props.getDecoration).toHaveBeenLastCalledWith(decorationID, service, operation); + }); + }); + + describe('handleClick', () => { + it('calls props.selectVertex iff props.decorationValue is truthy', () => { + expect(props.selectVertex).not.toHaveBeenCalled(); + + wrapper.find('.DdgNodeContent--core').simulate('click'); + expect(props.selectVertex).not.toHaveBeenCalled(); + + wrapper.setProps({ decorationValue }); + wrapper.find('.DdgNodeContent--core').simulate('click'); + expect(props.selectVertex).toHaveBeenCalledTimes(1); + expect(props.selectVertex).toHaveBeenLastCalledWith(props.vertex); + }); + }); + describe('measureNode', () => { it('returns twice the RADIUS with a buffer for svg border', () => { const diameterWithBuffer = 2 * RADIUS + 2; @@ -174,6 +238,13 @@ describe('', () => { }); expect(props.setViewModifier).toHaveBeenCalledWith([], EViewModifier.Hovered, true); }); + + it('clears hoveredIndices on mouse out', () => { + wrapper.simulate('mouseover', { type: 'mouseover' }); + expect(wrapper.instance().hoveredIndices).not.toEqual(new Set()); + wrapper.simulate('mouseout', { type: 'mouseout' }); + expect(wrapper.instance().hoveredIndices).toEqual(new Set()); + }); }); describe('node interactions', () => { @@ -496,7 +567,7 @@ describe('', () => { }; const noOp = () => {}; - fit('returns a ', () => { + it('returns a ', () => { const ddgNode = getNodeRenderer( noOp, noOp, @@ -515,7 +586,15 @@ describe('', () => { isFocalNode: true, }); expect(focalNode).toBeDefined(); - expect(ddgNode.props).toMatchSnapshot(); + expect(focalNode.props).toMatchSnapshot(); + }); + }); + + describe('mapDispatchToProps()', () => { + it('creates the actions correctly', () => { + expect(mapDispatchToProps(() => {})).toEqual({ + getDecoration: expect.any(Function), + }); }); }); }); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx index e8e801ff3a..8a64c53b60 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx @@ -19,7 +19,6 @@ import { TLayoutVertex } from '@jaegertracing/plexus/lib/types'; import IoAndroidLocate from 'react-icons/lib/io/android-locate'; import MdVisibilityOff from 'react-icons/lib/md/visibility-off'; import { connect } from 'react-redux'; -import { RouteComponentProps, withRouter } from 'react-router-dom'; import { bindActionCreators, Dispatch } from 'redux'; import calcPositioning from './calc-positioning'; @@ -266,7 +265,7 @@ export class UnconnectedDdgNodeContent extends React.PureComponent): TDispatchPro }; } - /* -const DdgNodeContent = withRouter( - connect( - extractDecorationFromState, - mapDispatchToProps - )(UnconnectedDdgNodeContent) -); - */ const DdgNodeContent = connect( extractDecorationFromState, diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx index 0621de4077..8b5b3e2502 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx @@ -101,7 +101,8 @@ export class UnconnectedDetailsPanel extends React.PureComponent this.setState({ detailsLoading: true }); JaegerAPI.fetchDecoration(fetchUrl) - .then(res => { + // TODO: type + .then((res: any) => { let detailsErred = false; let details = _get(res, getDetailPath as string); if (details === undefined) { @@ -112,7 +113,8 @@ export class UnconnectedDetailsPanel extends React.PureComponent this.setState({ columnDefs, details, detailsErred, detailsLoading: false }); }) - .catch(err => { + // TODO: type + .catch((err: any) => { this.setState({ details: `Unable to fetch decoration: ${err.message || err}`, detailsErred: true, diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.track.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.track.tsx new file mode 100644 index 0000000000..d9618e3c26 --- /dev/null +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.track.tsx @@ -0,0 +1,34 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { trackEvent } from '../../../utils/tracking'; +import getTrackFilter from '../../../utils/tracking/getTrackFilter'; + +// export for tests +export const CATEGORY_DECORATION_SELECTION = 'jaeger/ux/ddg/decoration-selection'; +export const CATEGORY_DECORATION_VIEW_DETAILS = 'jaeger/ux/ddg/decoration-view-details'; + +// export for tests +export const ACTION_CLEAR = 'clear'; +export const ACTION_SET = 'set'; + +export function trackDecorationSelected(decoration?: string) { + if (decoration) trackEvent(CATEGORY_DECORATION_SELECTION, ACTION_SET, decoration); + else trackEvent(CATEGORY_DECORATION_SELECTION, ACTION_CLEAR); +} + +export function trackDecorationViewDetails(value?: unknown) { + if (value) trackEvent(CATEGORY_DECORATION_VIEW_DETAILS, ACTION_SET); + else trackEvent(CATEGORY_DECORATION_VIEW_DETAILS, ACTION_CLEAR); +} diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx index 199b07d2be..08c975a17e 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx @@ -21,6 +21,7 @@ import { TDdgVertex } from '../../../model/ddg/types'; import { TPathAgnosticDecorationSchema } from '../../../model/path-agnostic-decorations/types'; import { getConfigValue } from '../../../utils/config/get-config'; import DetailsPanel from './DetailsPanel'; +import * as track from './index.track'; import './index.css'; @@ -37,13 +38,27 @@ export default class SidePanel extends React.PureComponent { constructor(props: TProps) { super(props); + const { selectedDecoration, selectedVertex } = props; + if (selectedDecoration) track.trackDecorationSelected(selectedDecoration); + if (selectedVertex) track.trackDecorationViewDetails(selectedVertex); + this.decorations = getConfigValue('pathAgnosticDecorations'); } + componentDidUpdate(prevProps: TProps) { + if (prevProps.selectedVertex !== this.props.selectedVertex) track.trackDecorationViewDetails(this.props.selectedVertex); + } + clearSelected = () => { + track.trackDecorationViewDetails(); this.props.clearSelected(); }; + selectDecoration = (decoration?: string) => { + track.trackDecorationSelected(decoration); + this.props.selectDecoration(decoration); + } + openInfoModal = () => { Modal.info({ content: ( @@ -73,7 +88,7 @@ export default class SidePanel extends React.PureComponent { render() { if (!this.decorations) return null; - const { clearSelected, selectDecoration, selectedDecoration, selectedVertex } = this.props; + const { selectedDecoration, selectedVertex } = this.props; const selectedSchema = this.decorations.find(({ id }) => id === selectedDecoration); @@ -91,7 +106,7 @@ export default class SidePanel extends React.PureComponent { @@ -99,7 +114,7 @@ export default class SidePanel extends React.PureComponent { diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.test.js b/packages/jaeger-ui/src/components/DeepDependencies/index.test.js index 67a6ca24c2..aa44e25823 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.test.js @@ -270,6 +270,26 @@ describe('DeepDependencyGraphPage', () => { }); }); + describe('setDecoration', () => { + it('updates url with provided density', () => { + const decoration = 'decoration-id'; + ddgPageImpl.setDecoration(decoration); + expect(getUrlSpy).toHaveBeenLastCalledWith( + Object.assign({}, props.urlState, { decoration }), + undefined + ); + }); + + it('clears density from url', () => { + const decoration = undefined; + ddgPageImpl.setDecoration(decoration); + expect(getUrlSpy).toHaveBeenLastCalledWith( + Object.assign({}, props.urlState, { decoration }), + undefined + ); + }); + }); + describe('setDensity', () => { it('updates url with provided density', () => { const density = EDdgDensity.PreventPathEntanglement; @@ -492,6 +512,28 @@ describe('DeepDependencyGraphPage', () => { }); }); + describe('select vertex', () => { + let wrapper; + const selectedVertex = { key: 'test vertex' }; + + beforeEach(() => { + wrapper = shallow(); + }); + + it('selects a vertex', () => { + expect(wrapper.state('selectedVertex')).toBeUndefined(); + wrapper.instance().selectVertex(selectedVertex); + expect(wrapper.state('selectedVertex')).toEqual(selectedVertex); + }); + + it('clears a vertex', () => { + wrapper.setState({ selectedVertex }); + expect(wrapper.state('selectedVertex')).toEqual(selectedVertex); + wrapper.instance().selectVertex(); + expect(wrapper.state('selectedVertex')).toBeUndefined(); + }); + }); + describe('view modifiers', () => { const visibilityIndices = ['visId0', 'visId1', 'visId2']; const targetVM = EViewModifier.Emphasized; @@ -643,6 +685,16 @@ describe('DeepDependencyGraphPage', () => { expect(errorComponent.prop('error')).toBe(error); }); + xit('selects a vertex', () => { + const wrapper = shallow( + + ); + const vertex = { key: 'test vertex' }; + expect(wrapper.instance.state('selectedVertex')).toBeUndefined(); + wrapper.instance().selectVertex(vertex); + expect(wrapper.instance.state('selectedVertex')).toEqual(vertex); + }); + describe('graphState.state === fetchedState.DONE', () => { function makeGraphState(specifiedDistance, vertexCount = 1) { graph.getVisible.mockReturnValueOnce({ diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.track.test.js b/packages/jaeger-ui/src/components/DeepDependencies/index.track.test.js index cbd2ad98d3..31cf161b73 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.track.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.track.test.js @@ -29,6 +29,7 @@ import { ACTION_HIDE_PARENTS, ACTION_INCREASE, ACTION_SET_FOCUS, + ACTION_SET_OPERATION, ACTION_SHOW, ACTION_SHOW_CHILDREN, ACTION_SHOW_PARENTS, @@ -41,6 +42,7 @@ import { trackSetFocus, trackShowMatches, trackToggleShowOp, + trackVertexSetOperation, trackViewTraces, } from './index.track'; import { EDdgDensity, EDirection } from '../../model/ddg/types'; @@ -235,6 +237,13 @@ describe('DeepDependencies tracking', () => { ); }); + describe('trackVertexSetOperation', () => { + it('calls trackEvent with the match category and show action', () => { + trackVertexSetOperation(); + expect(trackEvent).toHaveBeenCalledWith(CATEGORY_VERTEX_INTERACTIONS, ACTION_SET_OPERATION); + }); + }); + describe('trackViewTraces', () => { it('calls trackViewTraces with the vertex category and view traces action', () => { trackViewTraces(); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/url.test.js b/packages/jaeger-ui/src/components/DeepDependencies/url.test.js index 0386c22949..9fa0695219 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/url.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/url.test.js @@ -70,6 +70,7 @@ describe('DeepDependencyGraph/url', () => { }); describe('getUrlState', () => { + const decoration = 'test decoration'; const density = 'test density'; const end = '900'; const hash = 'test hash'; @@ -79,6 +80,7 @@ describe('DeepDependencyGraph/url', () => { const start = '400'; const visEncoding = 'vis encoding'; const acceptableParams = { + decoration, density, end, hash, @@ -89,6 +91,7 @@ describe('DeepDependencyGraph/url', () => { visEncoding, }; const expectedParams = { + decoration, density, end: Number.parseInt(end, 10), hash, @@ -152,7 +155,7 @@ describe('DeepDependencyGraph/url', () => { }); it('omits falsy values', () => { - ['end', 'hash', 'operation', 'service', 'start', 'visEncoding'].forEach(param => { + ['decoration', 'end', 'hash', 'operation', 'service', 'start', 'visEncoding'].forEach(param => { [null, undefined, ''].forEach(falsyPossibility => { parseSpy.mockReturnValue({ ...expectedParams, [param]: falsyPossibility }); expect(Reflect.has(getUrlState(getSearch()), param)).toBe(false); @@ -161,7 +164,7 @@ describe('DeepDependencyGraph/url', () => { }); it('handles and warns on duplicate values', () => { - ['end', 'hash', 'operation', 'service', 'showOp', 'start', 'visEncoding'].forEach(param => { + ['decoration', 'end', 'hash', 'operation', 'service', 'showOp', 'start', 'visEncoding'].forEach(param => { const secondParam = `second ${acceptableParams[param]}`; parseSpy.mockReturnValue({ ...acceptableParams, diff --git a/packages/jaeger-ui/src/constants/default-config.tsx b/packages/jaeger-ui/src/constants/default-config.tsx index 7b4f042f93..af8e5d050b 100644 --- a/packages/jaeger-ui/src/constants/default-config.tsx +++ b/packages/jaeger-ui/src/constants/default-config.tsx @@ -57,6 +57,15 @@ export default deepFreeze( }, ], pathAgnosticDecorations: [{ + acronym: 'ME', + id: 'me', + name: 'Missing Edges', + summaryUrl: '/analytics/v1/serviceedges/missing/summary?source=connmon', + summaryPath: '#{service}.count', + detailUrl: 'serviceedges/missing?source=connmon&service=#{service}', + detailPath: 'missingEdges', + detailColumnDefPath: 'columnDefs', + }, { acronym: 'N', id: 'n', name: 'Neapolitan mix of success/error/no path', diff --git a/packages/jaeger-ui/src/setupProxy.js b/packages/jaeger-ui/src/setupProxy.js index 34bb0a779b..9035954655 100644 --- a/packages/jaeger-ui/src/setupProxy.js +++ b/packages/jaeger-ui/src/setupProxy.js @@ -36,4 +36,15 @@ module.exports = function setupProxy(app) { xfwd: true, }) ); + // TODO: remove + app.use( + proxy('/serviceedges', { + target: 'http://localhost:16686', + logLevel: 'silent', + secure: false, + changeOrigin: true, + ws: true, + xfwd: true, + }) + ) }; From d6ddfb3f2f6296b0e6f9e2b651954b4e64dbb81b Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Wed, 8 Apr 2020 18:11:15 -0400 Subject: [PATCH 21/31] Test SidePanel/ index&track WIP test DetailsPanel Signed-off-by: Everett Ross --- .../SidePanel/DetailsPanel.test.js | 173 ++++++++++ .../SidePanel/DetailsPanel.tsx | 6 +- .../__snapshots__/index.test.js.snap | 298 ++++++++++++++++++ .../DeepDependencies/SidePanel/index.test.js | 210 ++++++++++++ .../SidePanel/index.track.test.js | 61 ++++ 5 files changed, 744 insertions(+), 4 deletions(-) create mode 100644 packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js create mode 100644 packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/index.test.js.snap create mode 100644 packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.test.js create mode 100644 packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.track.test.js diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js new file mode 100644 index 0000000000..fdd1c14cc0 --- /dev/null +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js @@ -0,0 +1,173 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* eslint-disable import/first */ +import React from 'react'; +import { shallow } from 'enzyme'; +import _get from 'lodash/get'; +import _set from 'lodash/set'; + +import stringSupplant from '../../../utils/stringSupplant'; +import JaegerAPI from '../../../api/jaeger'; +import { UnconnectedDetailsPanel as DetailsPanel } from './DetailsPanel'; + +describe('', () => { + describe('componentDidMount', () => { + it('fetches details', () => { + }); + }); + + describe('componentDidUpdate', () => { + it('fetches details and clears relevant state if decorationID changes', () => { + }); + + it('fetches details and clears relevant state if operation changes', () => { + }); + + it('fetches details and clears relevant state if service changes', () => { + }); + + it('does nothing if decorationID, operation, and service are unchanged', () => { + }); + }); + + describe('fetchDetails', () => { + const service = 'test svc'; + // const operation = 'test op'; + // const operationArray = ['test op0', 'test op1']; + let fetchDecorationSpy; + let promise; + let res; + let rej; + // const decorationID = 'test decoration ID'; + + beforeAll(() => { + fetchDecorationSpy = jest.spyOn(JaegerAPI, 'fetchDecoration').mockImplementation( + () => { + promise = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); + return promise; + } + ); + }); + + beforeEach(() => { + fetchDecorationSpy.mockClear(); + // resolves = []; + // rejects = []; + }); + + describe('fetches correct details url, perferring op-scoped details, or does not fetch at all', () => { + const details = 'test details'; + const columnDefs = ['test', 'column', 'defs']; + + ['detailUrl#{service}', undefined].forEach(detailUrl => { + ['detail.path.#{service}', undefined].forEach(detailPath => { + ['detail.column.def.path.#{service}', undefined].forEach(detailColumnDefPath => { + ['opDetailUrl#{service}#{operation}', undefined].forEach(opDetailUrl => { + ['op.detail.path.#{service}.#{operation}', undefined].forEach(opDetailPath => { + ['op', ['op0', 'op1'], undefined].forEach(operation => { + ['op.detail.column.def.path.#{service}', undefined].forEach(opDetailColumnDefPath => { + [{ message: 'Err obj with message' }, 'error message' , false].forEach(error => { + [true, false].forEach(hasDetails => { + [true, false].forEach(hasColumnDefPath => { + + it( + 'fetchs details for an operation iff operation, opDetailUrl, and opDetailPath are all strings', + async () => { + const detailsPanel = new DetailsPanel({ + operation, + service, + decorationSchema: { + detailUrl, + detailPath, + detailColumnDefPath, + opDetailUrl, + opDetailPath, + opDetailColumnDefPath, + }, + }); + const setStateSpy = jest.spyOn(detailsPanel, 'setState'); + detailsPanel.fetchDetails(); + + if (typeof opDetailUrl === 'string' && typeof opDetailPath === 'string' && typeof operation === 'string') { + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(opDetailUrl, { service, operation })); + expect(setStateSpy).toHaveBeenLastCalledWith({ detailsLoading: true }); + const expectedSetStateArg = { + detailsLoading: false, + }; + if (!error) { + const result = {}; + const supplantedDetailsPath = stringSupplant(opDetailPath, { service, operation }); + + if (hasDetails && opDetailPath) { + _set(result, supplantedDetailsPath, details); + expectedSetStateArg.details = details; + expectedSetStateArg.detailsErred = false; + } else { + expectedSetStateArg.details = `\`${supplantedDetailsPath}\` not found in response`; + expectedSetStateArg.detailsErred = true; + } + if (hasColumnDefPath && opDetailColumnDefPath) { + _set(result, stringSupplant(opDetailColumnDefPath, { service, operation }), columnDefs); + expectedSetStateArg.columnDefs = columnDefs; + } else { + expectedSetStateArg.columnDefs = []; + } + res(result); + await promise; + expect(setStateSpy).toHaveBeenLastCalledWith(expectedSetStateArg); + } else { + expectedSetStateArg.detailsErred = true; + const errorMessage = error.message || error; + expectedSetStateArg.details = `Unable to fetch decoration: ${errorMessage}`; + rej(error); + await promise.catch(() => {}); + expect(setStateSpy).toHaveBeenLastCalledWith(expectedSetStateArg); + } + } else if (typeof detailUrl === 'string' && typeof detailPath === 'string') { + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(detailUrl, { service })); + } else { + expect(fetchDecorationSpy).not.toHaveBeenCalled(); + } + } + ); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + }); + + + it( + 'fetchs details for a service iff service, detailsUrl, and detailsPath are all strings and any of operation, opDetailsUrl, and opDetailsPath are not strings', + () => { + } + ); + }); + + describe('render', () => { + }); + + describe('onResize', () => { + }); +}); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx index 8b5b3e2502..193273dc05 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx @@ -101,8 +101,7 @@ export class UnconnectedDetailsPanel extends React.PureComponent this.setState({ detailsLoading: true }); JaegerAPI.fetchDecoration(fetchUrl) - // TODO: type - .then((res: any) => { + .then((res: unknown) => { let detailsErred = false; let details = _get(res, getDetailPath as string); if (details === undefined) { @@ -113,8 +112,7 @@ export class UnconnectedDetailsPanel extends React.PureComponent this.setState({ columnDefs, details, detailsErred, detailsLoading: false }); }) - // TODO: type - .catch((err: any) => { + .catch((err: Error) => { this.setState({ details: `Unable to fetch decoration: ${err.message || err}`, detailsErred: true, diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/index.test.js.snap b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/index.test.js.snap new file mode 100644 index 0000000000..a4a023136c --- /dev/null +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/index.test.js.snap @@ -0,0 +1,298 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` info button opens info modal 1`] = ` +Object { + "content": , + "maskClosable": true, + "title": "Decoration Options", + "width": "60vw", +} +`; + +exports[` render ignores selectedVertex without selected decoration 1`] = ` +
+
+ +
+ + + + +
+ +
+
+
+`; + +exports[` render renders config decorations with clear button 1`] = ` +
+
+ +
+ + + + +
+ +
+
+
+`; + +exports[` render renders selected decoration 1`] = ` +
+
+ +
+ + + + +
+ +
+
+
+`; + +exports[` render renders sidePanel and closeBtn when vertex and decoration are both selected 1`] = ` +
+
+ +
+ + + + +
+ +
+
+ +
+
+`; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.test.js b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.test.js new file mode 100644 index 0000000000..3defe17350 --- /dev/null +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.test.js @@ -0,0 +1,210 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +/* eslint-disable import/first */ +import React from 'react'; +import { shallow } from 'enzyme'; +import { Modal } from 'antd'; + +import SidePanel from '.'; +import * as track from './index.track'; +import * as getConfig from '../../../utils/config/get-config'; + +describe('', () => { + let getConfigValueSpy; + const testAcronym = 'TA'; + const testID = 'test ID'; + const mockConfig = [ + { + id: 'first', + acronym: '1st', + name: 'First Decoration', + }, + { + id: testID, + acronym: testAcronym, + name: 'Decoration to test interactions', + }, + { + id: 'last', + acronym: 'LO', + name: 'The last one', + }, + ]; + const testVertex = { service: 'svc', operation: 'op' }; + + let trackDecorationSelectedSpy; + let trackDecorationViewDetailsSpy; + + beforeAll(() => { + getConfigValueSpy = jest.spyOn(getConfig, 'getConfigValue').mockReturnValue(mockConfig); + trackDecorationSelectedSpy = jest.spyOn(track, 'trackDecorationSelected'); + trackDecorationViewDetailsSpy = jest.spyOn(track, 'trackDecorationViewDetails'); + }); + + beforeEach(() => { + trackDecorationSelectedSpy.mockReset(); + trackDecorationViewDetailsSpy.mockReset(); + }); + + describe('constructor', () => { + it('inits decorations', () => { + const wrapper = shallow(); + expect(wrapper.instance().decorations).toBe(mockConfig); + }); + + it('tracks initial selection', () => { + expect(trackDecorationSelectedSpy).toHaveBeenCalledTimes(0); + + shallow(); + expect(trackDecorationSelectedSpy).toHaveBeenCalledTimes(1); + expect(trackDecorationSelectedSpy).toHaveBeenLastCalledWith(testID); + expect(trackDecorationViewDetailsSpy).toHaveBeenCalledTimes(0); + + shallow(); + expect(trackDecorationViewDetailsSpy).toHaveBeenCalledTimes(1); + expect(trackDecorationViewDetailsSpy).toHaveBeenLastCalledWith(testVertex); + expect(trackDecorationSelectedSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('render', () => { + it('renders null if there are no decorations', () => { + getConfigValueSpy.mockReturnValueOnce(undefined); + const wrapper = shallow(); + expect(wrapper.getElement()).toBe(null); + }); + + it('renders config decorations with clear button', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders selected decoration', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('ignores selectedVertex without selected decoration', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders sidePanel and closeBtn when vertex and decoration are both selected', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + }); + + describe('componentDidUpdate', () => { + it('tracks change in vertex from absent to present', () => { + const wrapper = shallow(); + expect(trackDecorationViewDetailsSpy).toHaveBeenCalledTimes(0); + + wrapper.setProps({ selectedVertex: testVertex }); + expect(trackDecorationViewDetailsSpy).toHaveBeenCalledTimes(1); + expect(trackDecorationViewDetailsSpy).toHaveBeenLastCalledWith(testVertex); + }); + + it('tracks change in vertex from present to absent', () => { + const wrapper = shallow(); + expect(trackDecorationViewDetailsSpy).toHaveBeenCalledTimes(1); + + wrapper.setProps({ selectedVertex: undefined }); + expect(trackDecorationViewDetailsSpy).toHaveBeenCalledTimes(2); + expect(trackDecorationViewDetailsSpy).toHaveBeenLastCalledWith(undefined); + }); + + it('tracks change in vertex between different vertexes', () => { + const wrapper = shallow(); + expect(trackDecorationViewDetailsSpy).toHaveBeenCalledTimes(1); + expect(trackDecorationViewDetailsSpy).toHaveBeenLastCalledWith(testVertex); + + const newVertex = { ...testVertex }; + wrapper.setProps({ selectedVertex: newVertex }); + expect(trackDecorationViewDetailsSpy).toHaveBeenCalledTimes(2); + expect(trackDecorationViewDetailsSpy).toHaveBeenLastCalledWith(newVertex); + }); + + it('does not track unchanged vertex', () => { + const wrapper = shallow(); + expect(trackDecorationViewDetailsSpy).toHaveBeenCalledTimes(1); + expect(trackDecorationViewDetailsSpy).toHaveBeenLastCalledWith(testVertex); + + wrapper.setProps({ selectedDecoration: testID }); + expect(trackDecorationViewDetailsSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('clearSelected', () => { + it('clears selected and tracks clearing', () => { + const clearSelected = jest.fn(); + const wrapper = shallow(); + expect(clearSelected).toHaveBeenCalledTimes(0); + expect(trackDecorationViewDetailsSpy).toHaveBeenCalledTimes(1); + + wrapper.find('button').at(0).simulate('click'); + expect(clearSelected).toHaveBeenCalledTimes(1); + expect(trackDecorationViewDetailsSpy).toHaveBeenCalledTimes(2); + expect(trackDecorationViewDetailsSpy).toHaveBeenLastCalledWith(); + }); + }); + + describe('selectDecoration', () => { + const selectDecoration = jest.fn(); + + beforeEach(() => { + selectDecoration.mockReset(); + }); + + it('selects decoration and tracks selection', () => { + const wrapper = shallow(); + expect(selectDecoration).toHaveBeenCalledTimes(0); + expect(trackDecorationSelectedSpy).toHaveBeenCalledTimes(0); + + wrapper.find(`button[children="${testAcronym}"]`).simulate('click'); + expect(selectDecoration).toHaveBeenCalledTimes(1); + expect(trackDecorationSelectedSpy).toHaveBeenCalledTimes(1); + expect(trackDecorationSelectedSpy).toHaveBeenLastCalledWith(testID); + }); + + it('clears decoration and tracks clear', () => { + const wrapper = shallow(); + expect(selectDecoration).toHaveBeenCalledTimes(0); + expect(trackDecorationSelectedSpy).toHaveBeenCalledTimes(1); + + wrapper.find('.Ddg--SidePanel--DecorationBtns > button').last().simulate('click'); + expect(selectDecoration).toHaveBeenCalledTimes(1); + expect(trackDecorationSelectedSpy).toHaveBeenCalledTimes(2); + expect(trackDecorationSelectedSpy).toHaveBeenLastCalledWith(undefined); + }); + }); + + describe('info button ', () => { + let modalInfoSpy; + + beforeAll(() => { + modalInfoSpy = jest.spyOn(Modal, 'info'); + }); + + it('opens info modal', () => { + const wrapper = shallow(); + expect(modalInfoSpy).toHaveBeenCalledTimes(0); + + wrapper.find('button').last().simulate('click'); + expect(modalInfoSpy).toHaveBeenCalledTimes(1); + expect(modalInfoSpy.mock.calls[0][0]).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.track.test.js b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.track.test.js new file mode 100644 index 0000000000..7d1694996e --- /dev/null +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.track.test.js @@ -0,0 +1,61 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as trackingUtils from '../../../utils/tracking'; +import { + CATEGORY_DECORATION_SELECTION, + CATEGORY_DECORATION_VIEW_DETAILS, + ACTION_CLEAR, + ACTION_SET, + trackDecorationSelected, + trackDecorationViewDetails, +} from './index.track'; + +describe('PAD SidePanel tracking', () => { + let trackEvent; + + beforeAll(() => { + trackEvent = jest.spyOn(trackingUtils, 'trackEvent').mockImplementation(); + }); + + beforeEach(() => { + trackEvent.mockClear(); + }); + + describe('trackDecorationSelected', () => { + it('tracks decoration selection with label', () => { + const decoration = 'test decoration ID'; + trackDecorationSelected(decoration); + expect(trackEvent).toHaveBeenCalledWith(CATEGORY_DECORATION_SELECTION, ACTION_SET, decoration); + }); + + it('tracks decoration cleared', () => { + trackDecorationSelected(); + expect(trackEvent).toHaveBeenCalledWith(CATEGORY_DECORATION_SELECTION, ACTION_CLEAR); + }); + }); + + describe('trackDecorationViewDetails', () => { + it('tracks details viewed', () => { + const truthyArg = { service: 'svc', operation: 'op' }; + trackDecorationViewDetails(truthyArg); + expect(trackEvent).toHaveBeenCalledWith(CATEGORY_DECORATION_VIEW_DETAILS, ACTION_SET); + }); + + it('tracks details closed', () => { + trackDecorationViewDetails(); + expect(trackEvent).toHaveBeenCalledWith(CATEGORY_DECORATION_VIEW_DETAILS, ACTION_CLEAR); + }); + }); +}); From 412c050280e9995d4bdb9dcccf63979038875bad Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Wed, 8 Apr 2020 18:38:10 -0400 Subject: [PATCH 22/31] WIP test DetailsPanel Signed-off-by: Everett Ross --- .../SidePanel/DetailsPanel.test.js | 88 +++++++++++-------- 1 file changed, 51 insertions(+), 37 deletions(-) diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js index fdd1c14cc0..aabac2d7c5 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js @@ -74,13 +74,14 @@ describe('', () => { const details = 'test details'; const columnDefs = ['test', 'column', 'defs']; + // TODO: one `it` if all pass, separate `it`s if any fail ['detailUrl#{service}', undefined].forEach(detailUrl => { ['detail.path.#{service}', undefined].forEach(detailPath => { ['detail.column.def.path.#{service}', undefined].forEach(detailColumnDefPath => { ['opDetailUrl#{service}#{operation}', undefined].forEach(opDetailUrl => { ['op.detail.path.#{service}.#{operation}', undefined].forEach(opDetailPath => { - ['op', ['op0', 'op1'], undefined].forEach(operation => { - ['op.detail.column.def.path.#{service}', undefined].forEach(opDetailColumnDefPath => { + ['op.detail.column.def.path.#{service}', undefined].forEach(opDetailColumnDefPath => { + ['op', ['op0', 'op1'], undefined].forEach(operation => { [{ message: 'Err obj with message' }, 'error message' , false].forEach(error => { [true, false].forEach(hasDetails => { [true, false].forEach(hasColumnDefPath => { @@ -103,45 +104,58 @@ describe('', () => { const setStateSpy = jest.spyOn(detailsPanel, 'setState'); detailsPanel.fetchDetails(); + let supplantedUrl; + let supplantedColumnDefPath; + let supplantedDetailsPath; + if (typeof opDetailUrl === 'string' && typeof opDetailPath === 'string' && typeof operation === 'string') { - expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(opDetailUrl, { service, operation })); - expect(setStateSpy).toHaveBeenLastCalledWith({ detailsLoading: true }); - const expectedSetStateArg = { - detailsLoading: false, - }; - if (!error) { - const result = {}; - const supplantedDetailsPath = stringSupplant(opDetailPath, { service, operation }); - - if (hasDetails && opDetailPath) { - _set(result, supplantedDetailsPath, details); - expectedSetStateArg.details = details; - expectedSetStateArg.detailsErred = false; - } else { - expectedSetStateArg.details = `\`${supplantedDetailsPath}\` not found in response`; - expectedSetStateArg.detailsErred = true; - } - if (hasColumnDefPath && opDetailColumnDefPath) { - _set(result, stringSupplant(opDetailColumnDefPath, { service, operation }), columnDefs); - expectedSetStateArg.columnDefs = columnDefs; - } else { - expectedSetStateArg.columnDefs = []; - } - res(result); - await promise; - expect(setStateSpy).toHaveBeenLastCalledWith(expectedSetStateArg); - } else { - expectedSetStateArg.detailsErred = true; - const errorMessage = error.message || error; - expectedSetStateArg.details = `Unable to fetch decoration: ${errorMessage}`; - rej(error); - await promise.catch(() => {}); - expect(setStateSpy).toHaveBeenLastCalledWith(expectedSetStateArg); - } + supplantedUrl = stringSupplant(opDetailUrl, { service, operation }); + supplantedDetailsPath = stringSupplant(opDetailPath, { service, operation }); + if (opDetailColumnDefPath) supplantedColumnDefPath = stringSupplant(opDetailColumnDefPath, { service, operation }); } else if (typeof detailUrl === 'string' && typeof detailPath === 'string') { - expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(detailUrl, { service })); + supplantedUrl = stringSupplant(detailUrl, { service }); + supplantedDetailsPath = stringSupplant(detailPath, { service }); + if (detailColumnDefPath) supplantedColumnDefPath = stringSupplant(detailColumnDefPath, { service }); } else { expect(fetchDecorationSpy).not.toHaveBeenCalled(); + return; + } + + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(supplantedUrl); + expect(setStateSpy).toHaveBeenLastCalledWith({ detailsLoading: true }); + + const expectedSetStateArg = { + detailsLoading: false, + detailsErred: Boolean(error || !hasDetails), + }; + + if (!error) { + const result = {}; + + if (hasDetails) { + _set(result, supplantedDetailsPath, details); + expectedSetStateArg.details = details; + } else { + expectedSetStateArg.details = `\`${supplantedDetailsPath}\` not found in response`; + } + + if (hasColumnDefPath && supplantedColumnDefPath) { + _set(result, supplantedColumnDefPath, columnDefs); + expectedSetStateArg.columnDefs = columnDefs; + } else { + expectedSetStateArg.columnDefs = []; + } + + res(result); + await promise; + expect(setStateSpy).toHaveBeenLastCalledWith(expectedSetStateArg); + + } else { + const errorMessage = error.message || error; + expectedSetStateArg.details = `Unable to fetch decoration: ${errorMessage}`; + rej(error); + await promise.catch(() => {}); + expect(setStateSpy).toHaveBeenLastCalledWith(expectedSetStateArg); } } ); From 25b5b0637e3eb130f9cecfd637130f1dbd14bfd4 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Thu, 9 Apr 2020 13:56:13 -0400 Subject: [PATCH 23/31] Finish DetailsPanel tests Signed-off-by: Everett Ross --- .../SidePanel/DetailsPanel.test.js | 217 +++++++++---- .../SidePanel/DetailsPanel.tsx | 10 +- .../__snapshots__/DetailsPanel.test.js.snap | 307 ++++++++++++++++++ .../model/path-agnostic-decorations/index.tsx | 6 - 4 files changed, 469 insertions(+), 71 deletions(-) create mode 100644 packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/DetailsPanel.test.js.snap diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js index aabac2d7c5..2b9434b75f 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js @@ -12,69 +12,60 @@ // See the License for the specific language governing permissions and // limitations under the License. -/* eslint-disable import/first */ import React from 'react'; import { shallow } from 'enzyme'; -import _get from 'lodash/get'; import _set from 'lodash/set'; import stringSupplant from '../../../utils/stringSupplant'; import JaegerAPI from '../../../api/jaeger'; +import ColumnResizer from '../../TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer'; import { UnconnectedDetailsPanel as DetailsPanel } from './DetailsPanel'; describe('', () => { - describe('componentDidMount', () => { - it('fetches details', () => { - }); - }); + const service = 'test svc'; + const opString = 'test op'; + const props = { + decorationSchema: { + detailUrl: 'http://test.detail.url?someParam=#{service}', + detailPath: 'test.detail.path', + opDetailUrl: 'http://test.opDetail.url?someParam=#{service}&otherParam=#{operation}', + opDetailPath: 'test.opDetail.path', + name: 'Decorating #{service}', + }, + decorationValue: 'test decorationValue', + service, + }; + const supplantedUrl = stringSupplant(props.decorationSchema.detailUrl, { service }); + const supplantedOpUrl = stringSupplant(props.decorationSchema.opDetailUrl, { operation: opString, service }); + let fetchDecorationSpy; + let promise; + let res; + let rej; - describe('componentDidUpdate', () => { - it('fetches details and clears relevant state if decorationID changes', () => { - }); - - it('fetches details and clears relevant state if operation changes', () => { - }); - - it('fetches details and clears relevant state if service changes', () => { - }); + beforeAll(() => { + fetchDecorationSpy = jest.spyOn(JaegerAPI, 'fetchDecoration').mockImplementation( + () => { + promise = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); + return promise; + } + ); + }); - it('does nothing if decorationID, operation, and service are unchanged', () => { - }); + beforeEach(() => { + fetchDecorationSpy.mockClear(); }); describe('fetchDetails', () => { - const service = 'test svc'; - // const operation = 'test op'; - // const operationArray = ['test op0', 'test op1']; - let fetchDecorationSpy; - let promise; - let res; - let rej; - // const decorationID = 'test decoration ID'; - - beforeAll(() => { - fetchDecorationSpy = jest.spyOn(JaegerAPI, 'fetchDecoration').mockImplementation( - () => { - promise = new Promise((resolve, reject) => { - res = resolve; - rej = reject; - }); - return promise; - } - ); - }); - beforeEach(() => { - fetchDecorationSpy.mockClear(); - // resolves = []; - // rejects = []; - }); - - describe('fetches correct details url, perferring op-scoped details, or does not fetch at all', () => { + it('fetches correct details url, perferring op-scoped details, or does not fetch at all', async () => { const details = 'test details'; const columnDefs = ['test', 'column', 'defs']; - // TODO: one `it` if all pass, separate `it`s if any fail + const tests = []; + ['detailUrl#{service}', undefined].forEach(detailUrl => { ['detail.path.#{service}', undefined].forEach(detailPath => { ['detail.column.def.path.#{service}', undefined].forEach(detailColumnDefPath => { @@ -86,9 +77,8 @@ describe('', () => { [true, false].forEach(hasDetails => { [true, false].forEach(hasColumnDefPath => { - it( - 'fetchs details for an operation iff operation, opDetailUrl, and opDetailPath are all strings', - async () => { + tests.push(async () => { + fetchDecorationSpy.mockClear(); const detailsPanel = new DetailsPanel({ operation, service, @@ -101,19 +91,20 @@ describe('', () => { opDetailColumnDefPath, }, }); - const setStateSpy = jest.spyOn(detailsPanel, 'setState'); + + const setStateSpy = jest.spyOn(detailsPanel, 'setState').mockImplementation(); detailsPanel.fetchDetails(); - let supplantedUrl; + let supplantedFetchUrl; let supplantedColumnDefPath; let supplantedDetailsPath; if (typeof opDetailUrl === 'string' && typeof opDetailPath === 'string' && typeof operation === 'string') { - supplantedUrl = stringSupplant(opDetailUrl, { service, operation }); + supplantedFetchUrl = stringSupplant(opDetailUrl, { service, operation }); supplantedDetailsPath = stringSupplant(opDetailPath, { service, operation }); if (opDetailColumnDefPath) supplantedColumnDefPath = stringSupplant(opDetailColumnDefPath, { service, operation }); } else if (typeof detailUrl === 'string' && typeof detailPath === 'string') { - supplantedUrl = stringSupplant(detailUrl, { service }); + supplantedFetchUrl = stringSupplant(detailUrl, { service }); supplantedDetailsPath = stringSupplant(detailPath, { service }); if (detailColumnDefPath) supplantedColumnDefPath = stringSupplant(detailColumnDefPath, { service }); } else { @@ -121,7 +112,7 @@ describe('', () => { return; } - expect(fetchDecorationSpy).toHaveBeenLastCalledWith(supplantedUrl); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(supplantedFetchUrl); expect(setStateSpy).toHaveBeenLastCalledWith({ detailsLoading: true }); const expectedSetStateArg = { @@ -169,19 +160,127 @@ describe('', () => { }); }); }); + + const errors = []; + await Promise.all(tests.map(test => test().catch(err => errors.push(err)))); + if (errors.length) throw errors; }); + }); + describe('render', () => { + it('renders', () => { + const wrapper = shallow(); + wrapper.setState({ detailsLoading: false }); + expect(wrapper).toMatchSnapshot(); + }); - it( - 'fetchs details for a service iff service, detailsUrl, and detailsPath are all strings and any of operation, opDetailsUrl, and opDetailsPath are not strings', - () => { - } - ); + it('renders with operation', () => { + const wrapper = shallow(); + wrapper.setState({ detailsLoading: false }); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders omitted array of operations', () => { + const wrapper = shallow(); + wrapper.setState({ detailsLoading: false }); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders with progressbar', () => { + const progressbar =
stand-in progressbar
; + const wrapper = shallow(); + wrapper.setState({ detailsLoading: false }); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders while loading', () => { + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders details', () => { + const wrapper = shallow(); + wrapper.setState({ detailsLoading: false, details: 'details string' }); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders details error', () => { + const wrapper = shallow(); + wrapper.setState({ detailsLoading: false, details: 'details error', detailsErred: true }); + expect(wrapper).toMatchSnapshot(); + }); }); - describe('render', () => { + describe('componentDidMount', () => { + it('fetches details', () => { + expect(fetchDecorationSpy).not.toHaveBeenCalled(); + + shallow(); + expect(fetchDecorationSpy).toHaveBeenCalled(); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(supplantedUrl); + }); + }); + + describe('componentDidUpdate', () => { + const expectedState = expect.objectContaining({ + details: undefined, + detailsErred: false, + detailsLoading: true, + }); + let wrapper; + + beforeEach(() => { + wrapper = shallow(); + wrapper.setState({ + details: 'test details', + detailsErred: false, + detailsLoading: false, + }); + expect(fetchDecorationSpy).toHaveBeenCalledTimes(1); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(supplantedUrl); + }); + + it('fetches details and clears relevant state if decorationSchema changes', () => { + const detailUrl = 'http://new.schema.detailsUrl?service=#{service}'; + const newSchema = { + ...props.decorationSchema, + detailUrl, + }; + wrapper.setProps({ decorationSchema: newSchema }); + expect(fetchDecorationSpy).toHaveBeenCalledTimes(2); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(detailUrl, { service })); + expect(wrapper.state()).toEqual(expectedState); + }); + + it('fetches details and clears relevant state if operation changes', () => { + wrapper.setProps({ operation: opString }); + expect(fetchDecorationSpy).toHaveBeenCalledTimes(2); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(supplantedOpUrl); + expect(wrapper.state()).toEqual(expectedState); + }); + + it('fetches details and clears relevant state if service changes', () => { + const newService = 'different test service'; + wrapper.setProps({ service: newService }); + expect(fetchDecorationSpy).toHaveBeenCalledTimes(2); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(props.decorationSchema.detailUrl, { service: newService })); + expect(wrapper.state()).toEqual(expectedState); + }); + + it('does nothing if decorationSchema, operation, and service are unchanged', () => { + wrapper.setProps({ decorationValue: `not_${props.decorationValue}` }); + expect(fetchDecorationSpy).toHaveBeenCalledTimes(1); + }); }); describe('onResize', () => { + it('updates state', () => { + const width = 60; + const wrapper = shallow(); + expect(wrapper.state('width')).not.toBe(width); + + wrapper.find(ColumnResizer).prop('onChange')(width); + expect(wrapper.state('width')).toBe(width); + }); }); }); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx index 193273dc05..5e7016f73d 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx @@ -16,10 +16,10 @@ import * as React from 'react'; import _get from 'lodash/get'; import { connect } from 'react-redux'; -import BreakableText from '../..//common/BreakableText'; +import BreakableText from '../../common/BreakableText'; import LoadingIndicator from '../../common/LoadingIndicator'; -import ColumnResizer from '../../../components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer'; import JaegerAPI from '../../../api/jaeger'; +import ColumnResizer from '../../TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer'; import extractDecorationFromState, { TDecorationFromState } from '../../../model/path-agnostic-decorations'; import { TPathAgnosticDecorationSchema, @@ -60,8 +60,8 @@ export class UnconnectedDetailsPanel extends React.PureComponent ) { this.setState({ details: undefined, - detailsErred: undefined, - detailsLoading: undefined, + detailsErred: false, + detailsLoading: false, }); this.fetchDetails(); } @@ -128,8 +128,6 @@ export class UnconnectedDetailsPanel extends React.PureComponent render() { const { decorationProgressbar, - decorationColor, - decorationMax, decorationSchema, decorationValue, operation: _op, diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/DetailsPanel.test.js.snap b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/DetailsPanel.test.js.snap new file mode 100644 index 0000000000..38bdad6ae9 --- /dev/null +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/DetailsPanel.test.js.snap @@ -0,0 +1,307 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` render renders 1`] = ` +
+
+ +
+
+ + Decorating test svc + +
+ + test decorationValue + + +
+`; + +exports[` render renders details 1`] = ` +
+
+ +
+
+ + Decorating test svc + +
+ + test decorationValue + + + +
+`; + +exports[` render renders details error 1`] = ` +
+
+ +
+
+ + Decorating test svc + +
+ + test decorationValue + + + +
+`; + +exports[` render renders omitted array of operations 1`] = ` +
+
+ +
+
+ + Decorating test svc + +
+ + test decorationValue + + +
+`; + +exports[` render renders while loading 1`] = ` +
+
+ +
+
+ + Decorating test svc + +
+ + test decorationValue + +
+ +
+ +
+`; + +exports[` render renders with operation 1`] = ` +
+
+ + +
+
+ + Decorating test svc + +
+ + test decorationValue + + +
+`; + +exports[` render renders with progressbar 1`] = ` +
+
+ +
+
+ + Decorating test svc + +
+
+
+ stand-in progressbar +
+
+ +
+`; diff --git a/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx b/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx index 7fdb1a6790..ab732e0c9d 100644 --- a/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx +++ b/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx @@ -27,11 +27,8 @@ import { TDdgVertex } from '../ddg/types'; import 'react-circular-progressbar/dist/styles.css'; export type TDecorationFromState = { - decorationBackgroundColor?: string; - decorationColor?: string; decorationID?: string; decorationProgressbar?: React.ReactNode; - decorationMax?: number; decorationValue?: string | number; }; @@ -91,10 +88,7 @@ export default function extractDecorationFromState( return { decorationProgressbar, - decorationBackgroundColor, - decorationColor, decorationID, decorationValue, - decorationMax, }; } From e41e1126d3b01d70da7df67491e00d3cd0dbe6ff Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Thu, 9 Apr 2020 14:29:24 -0400 Subject: [PATCH 24/31] Clean up Signed-off-by: Everett Ross --- packages/jaeger-ui/src/api/jaeger.js | 196 ------------------ .../__snapshots__/index.test.js.snap | 10 + .../Graph/DdgNodeContent/index.test.js | 20 +- .../Graph/DdgNodeContent/index.tsx | 13 +- .../DeepDependencies/Graph/index.tsx | 2 +- .../SidePanel/DetailsCard/index.tsx | 26 +-- .../SidePanel/DetailsPanel.test.js | 168 ++++++++------- .../SidePanel/DetailsPanel.tsx | 11 +- .../__snapshots__/index.test.js.snap | 24 +++ .../DeepDependencies/SidePanel/index.test.js | 15 +- .../SidePanel/index.track.tsx | 1 - .../DeepDependencies/SidePanel/index.tsx | 13 +- .../components/DeepDependencies/index.test.js | 12 +- .../src/components/DeepDependencies/index.tsx | 1 - .../components/DeepDependencies/url.test.js | 18 +- .../TimelineColumnResizer.tsx | 3 +- .../src/constants/default-config.tsx | 85 -------- .../model/path-agnostic-decorations/index.tsx | 10 +- packages/jaeger-ui/src/setupProxy.js | 2 +- 19 files changed, 194 insertions(+), 436 deletions(-) diff --git a/packages/jaeger-ui/src/api/jaeger.js b/packages/jaeger-ui/src/api/jaeger.js index ad9eac907f..daaec18525 100644 --- a/packages/jaeger-ui/src/api/jaeger.js +++ b/packages/jaeger-ui/src/api/jaeger.js @@ -81,203 +81,7 @@ const JaegerAPI = { return getJSON(`${this.apiRoot}archive/${id}`, { method: 'POST' }); }, fetchDecoration(url) { - console.log('calling url: ', url); return getJSON(url); - // eslint-disable-next-line no-unreachable - if (url.startsWith('/analytics') || url.startsWith('/api/serviceedges')) { - return getJSON(url); - } - if (url.length % 2 && url.startsWith('neapolitan')) { - return new Promise((res, rej) => - setTimeout(() => { - if (url.length % 4 === 1) res('No val here'); - else rej(new Error(`One of the unlucky quarter: ${url.length}`)); - }, 150) - ); - } - // eslint-disable-next-line no-unreachable - if (url === 'get graph') { - return new Promise(res => - setTimeout( - () => - res({ - deets: { - here: [ - { - count: { - value: 0, - styling: { - backgroundColor: 'red', - color: 'white', - }, - }, - value: 'first', - foo: 'bar', - bar: 'baz', - }, - { - count: 1, - value: 'second', - foo: 'bar too', - }, - { - count: 2, - value: 'third', - foo: 'bar three', - }, - { - count: 3, - value: 'second', - foo: 'bar too', - }, - { - count: 4, - value: 'third', - foo: 'bar three', - }, - { - count: 5, - value: 'second', - foo: 'bar too', - }, - { - count: 6, - value: 'third', - foo: 'bar three', - }, - { - count: 7, - value: 'second', - foo: 'bar too', - }, - { - count: 8, - value: 'third', - foo: 'bar three', - }, - { - count: 9, - value: 'second', - foo: 'bar too', - }, - { - count: 10, - value: 'third', - foo: 'bar three', - }, - { - count: 11, - value: 'second', - foo: 'bar too', - }, - { - count: 12, - value: 'third', - foo: 'bar three', - }, - { - count: 13, - value: 'second', - foo: 'bar too', - }, - { - count: 14, - value: 'third', - foo: 'bar three', - }, - { - count: 15, - value: 'second', - foo: 'bar too', - }, - { - count: 16, - value: 'third', - foo: 'bar three', - }, - { - count: 17, - value: 'second', - foo: 'bar too', - }, - { - count: 18, - value: 'third', - foo: 'bar three', - }, - { - count: 19, - value: 'second', - foo: 'bar too', - }, - { - count: 20, - value: 'third', - foo: 'bar three', - }, - ], - }, - defs: { - here: [ - 'count', - { - key: 'value', - label: 'The value column', - styling: { - backgroundColor: 'blue', - color: 'lightgrey', - }, - }, - 'foo', - ], - }, - }), - 2750 - ) - ); - } - // eslint-disable-next-line no-unreachable - if (url === 'get string') { - return new Promise(res => - setTimeout( - () => - res({ - deets: { - here: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', - }, - }), - 750 - ) - ); - } - // eslint-disable-next-line no-unreachable - if (url === 'get list') { - return new Promise(res => - setTimeout( - () => - res({ - deets: { - here: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'.split( - ' ' - ), - }, - }), - 750 - ) - ); - } - // eslint-disable-next-line no-unreachable - if (url === 'infinite load') - return new Promise(res => setTimeout(() => res('you are patient, eh?'), 1500000)); - // eslint-disable-next-line no-unreachable - if (url === 'deets err') - return new Promise((res, rej) => setTimeout(() => rej(new Error('you knew this would happen')), 600)); - // eslint-disable-next-line no-unreachable - if (url === 'deets 404') return new Promise(res => res({})); - // eslint-disable-next-line no-unreachable - return new Promise(res => setTimeout(() => res({ val: url.length ** 2 }), 150)); - // return getJSON(url); }, fetchDeepDependencyGraph(query) { return getJSON(`${ANALYTICS_ROOT}v1/dependencies`, { query }); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap index 80f10f081d..6052a4ab34 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/__snapshots__/index.test.js.snap @@ -59,6 +59,7 @@ exports[` omits the operation if it is null 1`] = `
omits the operation if it is null 2`] = `
renders correctly when decorationValue is a string 1`]
renders correctly when decorationValue is a string 2`]
renders correctly when given decorationProgressbar 1`]
renders correctly when given decorationProgressbar 2`]
renders correctly when isFocalNode = true and focalNod
renders correctly when isFocalNode = true and focalNod
renders the number of operations if there are multiple
renders the number of operations if there are multiple
', () => { - const decorationID = 'test decorationID' + const decorationID = 'test decorationID'; const decorationValue = 42; const operation = 'some-operation'; const operationArray = ['op0', 'op1', 'op2', 'op3']; @@ -568,14 +573,9 @@ describe('', () => { const noOp = () => {}; it('returns a ', () => { - const ddgNode = getNodeRenderer( - noOp, - noOp, - EDdgDensity.PreventPathEntanglement, - true, - 'testBaseUrl', - { maxDuration: '100ms' } - )(ddgVertex); + const ddgNode = getNodeRenderer(noOp, noOp, EDdgDensity.PreventPathEntanglement, true, 'testBaseUrl', { + maxDuration: '100ms', + })(ddgVertex); expect(ddgNode).toBeDefined(); expect(ddgNode.props).toMatchSnapshot(); }); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx index 8a64c53b60..8687ce7e9a 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/index.tsx @@ -59,8 +59,7 @@ type TDispatchProps = { getDecoration: (id: string, svc: string, op?: string) => void; }; -type TProps = /* RouteComponentProps & */ - TDispatchProps & +type TProps = TDispatchProps & TDecorationFromState & { focalNodeUrl: string | null; focusPathsThroughVertex: (vertexKey: string) => void; @@ -280,6 +279,7 @@ export class UnconnectedDdgNodeContent extends React.PureComponent
@@ -381,10 +381,9 @@ export function mapDispatchToProps(dispatch: Dispatch): TDispatchPro }; } -const DdgNodeContent = - connect( - extractDecorationFromState, - mapDispatchToProps - )(UnconnectedDdgNodeContent); +const DdgNodeContent = connect( + extractDecorationFromState, + mapDispatchToProps +)(UnconnectedDdgNodeContent); export default DdgNodeContent; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx index b4681a4802..39a0b80b01 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/index.tsx @@ -156,7 +156,7 @@ export default class Graph extends PureComponent { key: 'nodes/content', layerType: 'html', measurable: true, - measureNode: measureNode, + measureNode, renderNode: this.getNodeContentRenderer({ baseUrl, density, diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx index c3bfdc05c4..ca8157bd35 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx @@ -42,7 +42,7 @@ function isList(arr: string[] | TPadRow[]): arr is string[] { } export default class DetailsCard extends React.PureComponent { - renderList(details: string[]) { + static renderList(details: string[]) { return ( { ); } - renderColumn(def: TPadColumnDef | string) { + static renderColumn(def: TPadColumnDef | string) { let dataIndex: string; let key: string; let style: React.CSSProperties | undefined; let title: string; if (typeof def === 'string') { + // eslint-disable-next-line no-multi-assign key = title = dataIndex = def; } else { + // eslint-disable-next-line no-multi-assign key = title = dataIndex = def.key; if (def.label) title = def.label; if (def.styling) style = def.styling; @@ -87,18 +89,19 @@ export default class DetailsCard extends React.PureComponent { render: (cellData: undefined | string | TStyledValue) => { if (!cellData || typeof cellData !== 'object') return cellData; if (!cellData.linkTo) return cellData.value; - return {cellData.value}; + return ( + + {cellData.value} + + ); }, sorter: (a: TPadRow, b: TPadRow) => { const aData = a[dataIndex]; const aValue = typeof aData === 'object' && aData.value !== undefined ? aData.value : aData; const bData = b[dataIndex]; const bValue = typeof bData === 'object' && bData.value !== undefined ? bData.value : bData; - return aValue < bValue ? -1 : bValue < aValue ? 1 : 0; + if (aValue < bValue) return -1; + return bValue < aValue ? 1 : 0; }, }; @@ -131,19 +134,18 @@ export default class DetailsCard extends React.PureComponent { pagination={false} rowKey={(row: TPadRow) => JSON.stringify(row)} > - {columnDefs.map(this.renderColumn)} + {columnDefs.map(DetailsCard.renderColumn)}
); } renderDetails() { - const { columnDefs: _columnDefs, details } = this.props; - const columnDefs = _columnDefs ? _columnDefs.slice() : []; + const { details } = this.props; if (Array.isArray(details)) { if (details.length === 0) return null; - if (isList(details)) return this.renderList(details); + if (isList(details)) return DetailsCard.renderList(details); return this.renderTable(details); } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js index 2b9434b75f..1370eca530 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js @@ -36,22 +36,23 @@ describe('', () => { service, }; const supplantedUrl = stringSupplant(props.decorationSchema.detailUrl, { service }); - const supplantedOpUrl = stringSupplant(props.decorationSchema.opDetailUrl, { operation: opString, service }); + const supplantedOpUrl = stringSupplant(props.decorationSchema.opDetailUrl, { + operation: opString, + service, + }); let fetchDecorationSpy; let promise; let res; let rej; beforeAll(() => { - fetchDecorationSpy = jest.spyOn(JaegerAPI, 'fetchDecoration').mockImplementation( - () => { - promise = new Promise((resolve, reject) => { - res = resolve; - rej = reject; - }); - return promise; - } - ); + fetchDecorationSpy = jest.spyOn(JaegerAPI, 'fetchDecoration').mockImplementation(() => { + promise = new Promise((resolve, reject) => { + res = resolve; + rej = reject; + }); + return promise; + }); }); beforeEach(() => { @@ -59,7 +60,6 @@ describe('', () => { }); describe('fetchDetails', () => { - it('fetches correct details url, perferring op-scoped details, or does not fetch at all', async () => { const details = 'test details'; const columnDefs = ['test', 'column', 'defs']; @@ -73,83 +73,89 @@ describe('', () => { ['op.detail.path.#{service}.#{operation}', undefined].forEach(opDetailPath => { ['op.detail.column.def.path.#{service}', undefined].forEach(opDetailColumnDefPath => { ['op', ['op0', 'op1'], undefined].forEach(operation => { - [{ message: 'Err obj with message' }, 'error message' , false].forEach(error => { + [{ message: 'Err obj with message' }, 'error message', false].forEach(error => { [true, false].forEach(hasDetails => { [true, false].forEach(hasColumnDefPath => { + tests.push(async () => { + fetchDecorationSpy.mockClear(); + const detailsPanel = new DetailsPanel({ + operation, + service, + decorationSchema: { + detailUrl, + detailPath, + detailColumnDefPath, + opDetailUrl, + opDetailPath, + opDetailColumnDefPath, + }, + }); + + const setStateSpy = jest.spyOn(detailsPanel, 'setState').mockImplementation(); + detailsPanel.fetchDetails(); + + let supplantedFetchUrl; + let supplantedColumnDefPath; + let supplantedDetailsPath; + + if ( + typeof opDetailUrl === 'string' && + typeof opDetailPath === 'string' && + typeof operation === 'string' + ) { + supplantedFetchUrl = stringSupplant(opDetailUrl, { service, operation }); + supplantedDetailsPath = stringSupplant(opDetailPath, { service, operation }); + if (opDetailColumnDefPath) + supplantedColumnDefPath = stringSupplant(opDetailColumnDefPath, { + service, + operation, + }); + } else if (typeof detailUrl === 'string' && typeof detailPath === 'string') { + supplantedFetchUrl = stringSupplant(detailUrl, { service }); + supplantedDetailsPath = stringSupplant(detailPath, { service }); + if (detailColumnDefPath) + supplantedColumnDefPath = stringSupplant(detailColumnDefPath, { service }); + } else { + expect(fetchDecorationSpy).not.toHaveBeenCalled(); + return; + } - tests.push(async () => { - fetchDecorationSpy.mockClear(); - const detailsPanel = new DetailsPanel({ - operation, - service, - decorationSchema: { - detailUrl, - detailPath, - detailColumnDefPath, - opDetailUrl, - opDetailPath, - opDetailColumnDefPath, - }, - }); - - const setStateSpy = jest.spyOn(detailsPanel, 'setState').mockImplementation(); - detailsPanel.fetchDetails(); - - let supplantedFetchUrl; - let supplantedColumnDefPath; - let supplantedDetailsPath; - - if (typeof opDetailUrl === 'string' && typeof opDetailPath === 'string' && typeof operation === 'string') { - supplantedFetchUrl = stringSupplant(opDetailUrl, { service, operation }); - supplantedDetailsPath = stringSupplant(opDetailPath, { service, operation }); - if (opDetailColumnDefPath) supplantedColumnDefPath = stringSupplant(opDetailColumnDefPath, { service, operation }); - } else if (typeof detailUrl === 'string' && typeof detailPath === 'string') { - supplantedFetchUrl = stringSupplant(detailUrl, { service }); - supplantedDetailsPath = stringSupplant(detailPath, { service }); - if (detailColumnDefPath) supplantedColumnDefPath = stringSupplant(detailColumnDefPath, { service }); - } else { - expect(fetchDecorationSpy).not.toHaveBeenCalled(); - return; - } - - expect(fetchDecorationSpy).toHaveBeenLastCalledWith(supplantedFetchUrl); - expect(setStateSpy).toHaveBeenLastCalledWith({ detailsLoading: true }); - - const expectedSetStateArg = { - detailsLoading: false, - detailsErred: Boolean(error || !hasDetails), - }; - - if (!error) { - const result = {}; + expect(fetchDecorationSpy).toHaveBeenLastCalledWith(supplantedFetchUrl); + expect(setStateSpy).toHaveBeenLastCalledWith({ detailsLoading: true }); - if (hasDetails) { - _set(result, supplantedDetailsPath, details); - expectedSetStateArg.details = details; - } else { - expectedSetStateArg.details = `\`${supplantedDetailsPath}\` not found in response`; - } + const expectedSetStateArg = { + detailsLoading: false, + detailsErred: Boolean(error || !hasDetails), + }; - if (hasColumnDefPath && supplantedColumnDefPath) { - _set(result, supplantedColumnDefPath, columnDefs); - expectedSetStateArg.columnDefs = columnDefs; - } else { - expectedSetStateArg.columnDefs = []; - } + if (!error) { + const result = {}; - res(result); - await promise; - expect(setStateSpy).toHaveBeenLastCalledWith(expectedSetStateArg); + if (hasDetails) { + _set(result, supplantedDetailsPath, details); + expectedSetStateArg.details = details; + } else { + expectedSetStateArg.details = `\`${supplantedDetailsPath}\` not found in response`; + } + if (hasColumnDefPath && supplantedColumnDefPath) { + _set(result, supplantedColumnDefPath, columnDefs); + expectedSetStateArg.columnDefs = columnDefs; } else { - const errorMessage = error.message || error; - expectedSetStateArg.details = `Unable to fetch decoration: ${errorMessage}`; - rej(error); - await promise.catch(() => {}); - expect(setStateSpy).toHaveBeenLastCalledWith(expectedSetStateArg); + expectedSetStateArg.columnDefs = []; } + + res(result); + await promise; + expect(setStateSpy).toHaveBeenLastCalledWith(expectedSetStateArg); + } else { + const errorMessage = error.message || error; + expectedSetStateArg.details = `Unable to fetch decoration: ${errorMessage}`; + rej(error); + await promise.catch(() => {}); + expect(setStateSpy).toHaveBeenLastCalledWith(expectedSetStateArg); } - ); + }); }); }); }); @@ -244,7 +250,7 @@ describe('', () => { const detailUrl = 'http://new.schema.detailsUrl?service=#{service}'; const newSchema = { ...props.decorationSchema, - detailUrl, + detailUrl, }; wrapper.setProps({ decorationSchema: newSchema }); expect(fetchDecorationSpy).toHaveBeenCalledTimes(2); @@ -263,7 +269,9 @@ describe('', () => { const newService = 'different test service'; wrapper.setProps({ service: newService }); expect(fetchDecorationSpy).toHaveBeenCalledTimes(2); - expect(fetchDecorationSpy).toHaveBeenLastCalledWith(stringSupplant(props.decorationSchema.detailUrl, { service: newService })); + expect(fetchDecorationSpy).toHaveBeenLastCalledWith( + stringSupplant(props.decorationSchema.detailUrl, { service: newService }) + ); expect(wrapper.state()).toEqual(expectedState); }); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx index 5e7016f73d..6c237ac462 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx @@ -58,6 +58,7 @@ export class UnconnectedDetailsPanel extends React.PureComponent prevProps.service !== this.props.service || prevProps.decorationSchema !== this.props.decorationSchema ) { + // eslint-disable-next-line react/no-did-update-set-state this.setState({ details: undefined, detailsErred: false, @@ -126,13 +127,7 @@ export class UnconnectedDetailsPanel extends React.PureComponent }; render() { - const { - decorationProgressbar, - decorationSchema, - decorationValue, - operation: _op, - service, - } = this.props; + const { decorationProgressbar, decorationSchema, decorationValue, operation: _op, service } = this.props; const { width = 0.3 } = this.state; const operation = _op && !Array.isArray(_op) ? _op : undefined; return ( @@ -162,7 +157,7 @@ export class UnconnectedDetailsPanel extends React.PureComponent header="Details" /> )} - +
); } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/index.test.js.snap b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/index.test.js.snap index a4a023136c..64999374e4 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/index.test.js.snap +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/index.test.js.snap @@ -63,6 +63,7 @@ exports[` render ignores selectedVertex without selected decoration 1 @@ -73,6 +74,7 @@ exports[` render ignores selectedVertex without selected decoration 1 className="Ddg--SidePanel--decorationBtn " key="first" onClick={[Function]} + type="button" > 1st @@ -80,6 +82,7 @@ exports[` render ignores selectedVertex without selected decoration 1 className="Ddg--SidePanel--decorationBtn " key="test ID" onClick={[Function]} + type="button" > TA @@ -87,6 +90,7 @@ exports[` render ignores selectedVertex without selected decoration 1 className="Ddg--SidePanel--decorationBtn " key="last" onClick={[Function]} + type="button" > LO @@ -94,6 +98,7 @@ exports[` render ignores selectedVertex without selected decoration 1 className="Ddg--SidePanel--decorationBtn" key="clearBtn" onClick={[Function]} + type="button" > Clear @@ -101,6 +106,7 @@ exports[` render ignores selectedVertex without selected decoration 1 @@ -121,6 +127,7 @@ exports[` render renders config decorations with clear button 1`] = ` @@ -131,6 +138,7 @@ exports[` render renders config decorations with clear button 1`] = ` className="Ddg--SidePanel--decorationBtn " key="first" onClick={[Function]} + type="button" > 1st @@ -138,6 +146,7 @@ exports[` render renders config decorations with clear button 1`] = ` className="Ddg--SidePanel--decorationBtn " key="test ID" onClick={[Function]} + type="button" > TA @@ -145,6 +154,7 @@ exports[` render renders config decorations with clear button 1`] = ` className="Ddg--SidePanel--decorationBtn " key="last" onClick={[Function]} + type="button" > LO @@ -152,6 +162,7 @@ exports[` render renders config decorations with clear button 1`] = ` className="Ddg--SidePanel--decorationBtn" key="clearBtn" onClick={[Function]} + type="button" > Clear @@ -159,6 +170,7 @@ exports[` render renders config decorations with clear button 1`] = ` @@ -179,6 +191,7 @@ exports[` render renders selected decoration 1`] = ` @@ -189,6 +202,7 @@ exports[` render renders selected decoration 1`] = ` className="Ddg--SidePanel--decorationBtn " key="first" onClick={[Function]} + type="button" > 1st @@ -196,6 +210,7 @@ exports[` render renders selected decoration 1`] = ` className="Ddg--SidePanel--decorationBtn is-selected" key="test ID" onClick={[Function]} + type="button" > TA @@ -203,6 +218,7 @@ exports[` render renders selected decoration 1`] = ` className="Ddg--SidePanel--decorationBtn " key="last" onClick={[Function]} + type="button" > LO @@ -210,6 +226,7 @@ exports[` render renders selected decoration 1`] = ` className="Ddg--SidePanel--decorationBtn" key="clearBtn" onClick={[Function]} + type="button" > Clear @@ -217,6 +234,7 @@ exports[` render renders selected decoration 1`] = ` @@ -237,6 +255,7 @@ exports[` render renders sidePanel and closeBtn when vertex and decor @@ -247,6 +266,7 @@ exports[` render renders sidePanel and closeBtn when vertex and decor className="Ddg--SidePanel--decorationBtn " key="first" onClick={[Function]} + type="button" > 1st @@ -254,6 +274,7 @@ exports[` render renders sidePanel and closeBtn when vertex and decor className="Ddg--SidePanel--decorationBtn is-selected" key="test ID" onClick={[Function]} + type="button" > TA @@ -261,6 +282,7 @@ exports[` render renders sidePanel and closeBtn when vertex and decor className="Ddg--SidePanel--decorationBtn " key="last" onClick={[Function]} + type="button" > LO @@ -268,6 +290,7 @@ exports[` render renders sidePanel and closeBtn when vertex and decor className="Ddg--SidePanel--decorationBtn" key="clearBtn" onClick={[Function]} + type="button" > Clear @@ -275,6 +298,7 @@ exports[` render renders sidePanel and closeBtn when vertex and decor diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.test.js b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.test.js index 3defe17350..920090fc34 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.test.js @@ -154,7 +154,10 @@ describe('', () => { expect(clearSelected).toHaveBeenCalledTimes(0); expect(trackDecorationViewDetailsSpy).toHaveBeenCalledTimes(1); - wrapper.find('button').at(0).simulate('click'); + wrapper + .find('button') + .at(0) + .simulate('click'); expect(clearSelected).toHaveBeenCalledTimes(1); expect(trackDecorationViewDetailsSpy).toHaveBeenCalledTimes(2); expect(trackDecorationViewDetailsSpy).toHaveBeenLastCalledWith(); @@ -184,7 +187,10 @@ describe('', () => { expect(selectDecoration).toHaveBeenCalledTimes(0); expect(trackDecorationSelectedSpy).toHaveBeenCalledTimes(1); - wrapper.find('.Ddg--SidePanel--DecorationBtns > button').last().simulate('click'); + wrapper + .find('.Ddg--SidePanel--DecorationBtns > button') + .last() + .simulate('click'); expect(selectDecoration).toHaveBeenCalledTimes(1); expect(trackDecorationSelectedSpy).toHaveBeenCalledTimes(2); expect(trackDecorationSelectedSpy).toHaveBeenLastCalledWith(undefined); @@ -202,7 +208,10 @@ describe('', () => { const wrapper = shallow(); expect(modalInfoSpy).toHaveBeenCalledTimes(0); - wrapper.find('button').last().simulate('click'); + wrapper + .find('button') + .last() + .simulate('click'); expect(modalInfoSpy).toHaveBeenCalledTimes(1); expect(modalInfoSpy.mock.calls[0][0]).toMatchSnapshot(); }); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.track.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.track.tsx index d9618e3c26..41be6d9123 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.track.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.track.tsx @@ -13,7 +13,6 @@ // limitations under the License. import { trackEvent } from '../../../utils/tracking'; -import getTrackFilter from '../../../utils/tracking/getTrackFilter'; // export for tests export const CATEGORY_DECORATION_SELECTION = 'jaeger/ux/ddg/decoration-selection'; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx index 08c975a17e..5daae1e527 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/index.tsx @@ -46,7 +46,9 @@ export default class SidePanel extends React.PureComponent { } componentDidUpdate(prevProps: TProps) { - if (prevProps.selectedVertex !== this.props.selectedVertex) track.trackDecorationViewDetails(this.props.selectedVertex); + if (prevProps.selectedVertex !== this.props.selectedVertex) { + track.trackDecorationViewDetails(this.props.selectedVertex); + } } clearSelected = () => { @@ -57,7 +59,7 @@ export default class SidePanel extends React.PureComponent { selectDecoration = (decoration?: string) => { track.trackDecorationSelected(decoration); this.props.selectDecoration(decoration); - } + }; openInfoModal = () => { Modal.info({ @@ -97,15 +99,17 @@ export default class SidePanel extends React.PureComponent {
- {this.decorations.map(({ acronym, id, name }) => ( + {this.decorations.map(({ acronym, id }) => (
-
diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.test.js b/packages/jaeger-ui/src/components/DeepDependencies/index.test.js index aa44e25823..e1bccf5d25 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.test.js @@ -517,7 +517,7 @@ describe('DeepDependencyGraphPage', () => { const selectedVertex = { key: 'test vertex' }; beforeEach(() => { - wrapper = shallow(); + wrapper = shallow(); }); it('selects a vertex', () => { @@ -685,16 +685,6 @@ describe('DeepDependencyGraphPage', () => { expect(errorComponent.prop('error')).toBe(error); }); - xit('selects a vertex', () => { - const wrapper = shallow( - - ); - const vertex = { key: 'test vertex' }; - expect(wrapper.instance.state('selectedVertex')).toBeUndefined(); - wrapper.instance().selectVertex(vertex); - expect(wrapper.instance.state('selectedVertex')).toEqual(vertex); - }); - describe('graphState.state === fetchedState.DONE', () => { function makeGraphState(specifiedDistance, vertexCount = 1) { graph.getVisible.mockReturnValueOnce({ diff --git a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx index 602968dce9..8df2a3525f 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/index.tsx @@ -281,7 +281,6 @@ export class DeepDependencyGraphPageImpl extends React.PureComponent 1) { - const { density, showOp, service, operation, visEncoding } = urlState; wrapperClassName = 'is-horizontal'; // TODO: using `key` here is a hack, debug digraph to fix the underlying issue content = ( diff --git a/packages/jaeger-ui/src/components/DeepDependencies/url.test.js b/packages/jaeger-ui/src/components/DeepDependencies/url.test.js index 9fa0695219..81ab2f25f8 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/url.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/url.test.js @@ -164,14 +164,16 @@ describe('DeepDependencyGraph/url', () => { }); it('handles and warns on duplicate values', () => { - ['decoration', 'end', 'hash', 'operation', 'service', 'showOp', 'start', 'visEncoding'].forEach(param => { - const secondParam = `second ${acceptableParams[param]}`; - parseSpy.mockReturnValue({ - ...acceptableParams, - [param]: [acceptableParams[param], secondParam], - }); - expect(getUrlState(getSearch())[param]).toBe(expectedParams[param]); - }); + ['decoration', 'end', 'hash', 'operation', 'service', 'showOp', 'start', 'visEncoding'].forEach( + param => { + const secondParam = `second ${acceptableParams[param]}`; + parseSpy.mockReturnValue({ + ...acceptableParams, + [param]: [acceptableParams[param], secondParam], + }); + expect(getUrlState(getSearch())[param]).toBe(expectedParams[param]); + } + ); }); it('memoizes correctly', () => { diff --git a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx index cd9a29b13a..246e3a8600 100644 --- a/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx +++ b/packages/jaeger-ui/src/components/TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer.tsx @@ -68,7 +68,8 @@ export default class TimelineColumnResizer extends React.PureComponent< throw new Error('invalid state'); } const { left: clientXLeft, width } = this._rootElm.getBoundingClientRect(); - let { min, max, rightSide } = this.props; + const { rightSide } = this.props; + let { min, max } = this.props; if (rightSide) [min, max] = [1 - max, 1 - min]; return { clientXLeft, diff --git a/packages/jaeger-ui/src/constants/default-config.tsx b/packages/jaeger-ui/src/constants/default-config.tsx index af8e5d050b..198b84f9a3 100644 --- a/packages/jaeger-ui/src/constants/default-config.tsx +++ b/packages/jaeger-ui/src/constants/default-config.tsx @@ -56,91 +56,6 @@ export default deepFreeze( ], }, ], - pathAgnosticDecorations: [{ - acronym: 'ME', - id: 'me', - name: 'Missing Edges', - summaryUrl: '/analytics/v1/serviceedges/missing/summary?source=connmon', - summaryPath: '#{service}.count', - detailUrl: 'serviceedges/missing?source=connmon&service=#{service}', - detailPath: 'missingEdges', - detailColumnDefPath: 'columnDefs', - }, { - acronym: 'N', - id: 'n', - name: 'Neapolitan mix of success/error/no path', - summaryUrl: 'neapolitan #{service}', - summaryPath: 'val', - }, { - acronym: 'AR', - id: 'ar', - name: 'All should resolve', - summaryUrl: 'all should res #{service}', - summaryPath: 'val', - }, { - acronym: 'RGT', - id: 'rgt', - name: 'all should resolve details graph too #{service}', - summaryUrl: 'details too #{service}', - detailUrl: 'get graph', - detailPath: 'deets.here', - detailColumnDefPath: 'defs.here', - summaryPath: 'val', - }, { - acronym: 'RST', - id: 'rst', - name: 'all should resolve details string too #{service}', - summaryUrl: 'details too #{service}', - detailUrl: 'get string', - detailPath: 'deets.here', - summaryPath: 'val', - }, { - acronym: 'RLT', - id: 'rlt', - name: 'all should resolve details list too #{service}', - summaryUrl: 'details too #{service}', - detailUrl: 'get list', - detailPath: 'deets.here', - summaryPath: 'val', - }, { - acronym: 'RIL', - id: 'ril', - name: 'all should resolve, but infinite load details', - summaryUrl: 'details too #{service}', - detailUrl: 'infinite load', - detailPath: 'deets.here', - summaryPath: 'val', - }, { - acronym: 'RDE', - id: 'rde', - name: 'all should resolve, but details err', - summaryUrl: 'details too #{service}', - detailUrl: 'deets err', - detailPath: 'deets.here', - summaryPath: 'val', - }, { - acronym: 'RD4', - id: 'rd4', - name: 'all should resolve, but details not found', - summaryUrl: 'details too #{service}', - detailUrl: 'deets 404', - detailPath: 'deets.here', - summaryPath: 'val', - }, { - acronym: 'OPs', - id: 'ops', - name: 'got dem ops', - summaryUrl: 'neapolitan #{service}', - summaryPath: 'val', - opSummaryUrl: 'just #{service}', - opSummaryPath: 'val', - detailUrl: 'deets 404', - detailPath: 'deets.here', - opDetailUrl: 'get list', - opDetailPath: 'deets.here', - }/*, { - TODO: op example too - }*/], search: { maxLookback: { label: '2 Days', diff --git a/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx b/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx index ab732e0c9d..b1296f84d7 100644 --- a/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx +++ b/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx @@ -22,7 +22,6 @@ import { RADIUS, } from '../../components/DeepDependencies/Graph/DdgNodeContent/constants'; import { ReduxState } from '../../types/index'; -import { TDdgVertex } from '../ddg/types'; import 'react-circular-progressbar/dist/styles.css'; @@ -41,21 +40,18 @@ export default function extractDecorationFromState( if (!decorationID) return {}; - let decorationValue = _get( - state, - `pathAgnosticDecorations.${decorationID}.withOp.${service}.${operation}` - ); + let decorationValue = _get(state, `pathAgnosticDecorations.${decorationID}.withOp.${service}.${operation}`); let decorationMax = _get(state, `pathAgnosticDecorations.${decorationID}.withOpMax`); if (!decorationValue) { decorationValue = _get(state, `pathAgnosticDecorations.${decorationID}.withoutOp.${service}`); decorationMax = _get(state, `pathAgnosticDecorations.${decorationID}.withoutOpMax`); } - const scale = Math.pow(decorationValue / decorationMax, 1 / 4); + const scale = (decorationValue / decorationMax) ** (1 / 4); const saturation = Math.ceil(scale * 100); const light = 50 + Math.ceil((1 - scale) * 50); const decorationColor = `hsl(0, ${saturation}%, ${light}%)`; - const backgroundScale = Math.pow((decorationMax - decorationValue) / decorationMax, 1 / 4); + const backgroundScale = ((decorationMax - decorationValue) / decorationMax) ** (1 / 4); const backgroundSaturation = Math.ceil(backgroundScale * 100); const backgroundLight = 50 + Math.ceil((1 - backgroundScale) * 50); const decorationBackgroundColor = `hsl(120, ${backgroundSaturation}%, ${backgroundLight}%)`; diff --git a/packages/jaeger-ui/src/setupProxy.js b/packages/jaeger-ui/src/setupProxy.js index 9035954655..4bf0572aa8 100644 --- a/packages/jaeger-ui/src/setupProxy.js +++ b/packages/jaeger-ui/src/setupProxy.js @@ -46,5 +46,5 @@ module.exports = function setupProxy(app) { ws: true, xfwd: true, }) - ) + ); }; From ca8c7fb75936d02de58047ded54554f4384f93a8 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Thu, 9 Apr 2020 19:04:58 -0400 Subject: [PATCH 25/31] Add skeleton components and fetch quality metrics Signed-off-by: Everett Ross --- packages/jaeger-ui/src/api/jaeger.js | 3 + .../jaeger-ui/src/components/App/index.js | 3 + .../components/QualityMetrics/MetricCard.tsx | 149 ++++++++++++++++++ .../components/QualityMetrics/ScoreCard.tsx | 149 ++++++++++++++++++ .../src/components/QualityMetrics/index.tsx | 149 ++++++++++++++++++ .../src/components/QualityMetrics/types.tsx | 69 ++++++++ .../src/components/QualityMetrics/url.tsx | 29 ++++ packages/jaeger-ui/src/setupProxy.js | 10 ++ 8 files changed, 561 insertions(+) create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.tsx create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/index.tsx create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/types.tsx create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/url.tsx diff --git a/packages/jaeger-ui/src/api/jaeger.js b/packages/jaeger-ui/src/api/jaeger.js index daaec18525..e1ad20a8dc 100644 --- a/packages/jaeger-ui/src/api/jaeger.js +++ b/packages/jaeger-ui/src/api/jaeger.js @@ -89,6 +89,9 @@ const JaegerAPI = { fetchDependencies(endTs = new Date().getTime(), lookback = DEFAULT_DEPENDENCY_LOOKBACK) { return getJSON(`${this.apiRoot}dependencies`, { query: { endTs, lookback } }); }, + fetchQualityMetrics(service /* , lookback = DEFAULT_QUALITY_METRICS_LOOKBACK */) { + return getJSON(`/qualitymetrics-v2`, { query: { service /* , lookback */ } }); + }, fetchServiceOperations(serviceName) { return getJSON(`${this.apiRoot}services/${encodeURIComponent(serviceName)}/operations`); }, diff --git a/packages/jaeger-ui/src/components/App/index.js b/packages/jaeger-ui/src/components/App/index.js index 78493b617c..d09bdcbbc9 100644 --- a/packages/jaeger-ui/src/components/App/index.js +++ b/packages/jaeger-ui/src/components/App/index.js @@ -24,6 +24,8 @@ import DependencyGraph from '../DependencyGraph'; import { ROUTE_PATH as dependenciesPath } from '../DependencyGraph/url'; import DeepDependencies from '../DeepDependencies'; import { ROUTE_PATH as deepDependenciesPath } from '../DeepDependencies/url'; +import QualityMetrics from '../QualityMetrics'; +import { ROUTE_PATH as qualityMetricsPath } from '../QualityMetrics/url'; import SearchTracePage from '../SearchTracePage'; import { ROUTE_PATH as searchPath } from '../SearchTracePage/url'; import TraceDiff from '../TraceDiff'; @@ -60,6 +62,7 @@ export default class JaegerUIApp extends Component { + diff --git a/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx new file mode 100644 index 0000000000..46cedc6c68 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx @@ -0,0 +1,149 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react'; +import { History as RouterHistory, Location } from 'history'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import queryString from 'query-string'; + +import * as jaegerApiActions from '../../actions/jaeger-api'; +import JaegerAPI from '../../api/jaeger'; +import BreakableText from '../common/BreakableText'; + +import { ReduxState } from '../../types'; +import { TQualityMetrics } from './types'; + +type TOwnProps = { + history: RouterHistory; + location: Location; +} + +type TDispatchProps = { + fetchServices: () => void; +}; + +type TReduxProps = { + service?: string; + services?: string[] | null; +} + +export type TProps = TDispatchProps & TReduxProps & TOwnProps; + +type TState = { + qualityMetrics?: TQualityMetrics; + error?: Error; + loading?: boolean; +}; + +export class UnconnectedQualityMetrics extends React.PureComponent { + state: TState = {}; + + componentDidMount() { + this.fetchQualityMetrics(); + } + + componentDidUpdate(prevProps: TProps) { + if (prevProps.service !== this.props.service) { + // eslint-disable-next-line react/no-did-update-set-state + this.setState({ + qualityMetrics: undefined, + error: undefined, + loading: false, + }); + this.fetchQualityMetrics(); + } + } + + fetchQualityMetrics() { + const { + service, + } = this.props; + if (!service) return; + + this.setState({ loading: true }); + + JaegerAPI.fetchQualityMetrics(service) + .then((qualityMetrics: TQualityMetrics) => { + + this.setState({ qualityMetrics, loading: false }); + }) + .catch((error: Error) => { + this.setState({ + error, + loading: false, + }); + }); + } + + render() { + const { service } = this.props; + const { qualityMetrics, error, loading } = this.state; + return ( +
+
+ Quality Metrics +
+ {/* +
+ {stringSupplant(decorationSchema.name, { service, operation })} +
+ {decorationProgressbar ? ( +
{decorationProgressbar}
+ ) : ( + {decorationValue} + )} + {this.state.detailsLoading && ( +
+ +
+ )} + {this.state.details && ( + + )} + + */} +
+ ); + } +} + +export function mapStateToProps(state: ReduxState, ownProps: TOwnProps): TReduxProps { + const { services: stServices } = state; + const { services } = stServices; + const { service: serviceFromUrl } = queryString.parse(ownProps.location.search); + const service = Array.isArray(serviceFromUrl) ? serviceFromUrl[0] : serviceFromUrl; + return { + service, + services, + }; +} + +export function mapDispatchToProps(dispatch: Dispatch): TDispatchProps { + const { fetchServices } = bindActionCreators( + jaegerApiActions, + dispatch + ); + + return { + fetchServices, + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(UnconnectedQualityMetrics); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.tsx b/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.tsx new file mode 100644 index 0000000000..46cedc6c68 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.tsx @@ -0,0 +1,149 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react'; +import { History as RouterHistory, Location } from 'history'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import queryString from 'query-string'; + +import * as jaegerApiActions from '../../actions/jaeger-api'; +import JaegerAPI from '../../api/jaeger'; +import BreakableText from '../common/BreakableText'; + +import { ReduxState } from '../../types'; +import { TQualityMetrics } from './types'; + +type TOwnProps = { + history: RouterHistory; + location: Location; +} + +type TDispatchProps = { + fetchServices: () => void; +}; + +type TReduxProps = { + service?: string; + services?: string[] | null; +} + +export type TProps = TDispatchProps & TReduxProps & TOwnProps; + +type TState = { + qualityMetrics?: TQualityMetrics; + error?: Error; + loading?: boolean; +}; + +export class UnconnectedQualityMetrics extends React.PureComponent { + state: TState = {}; + + componentDidMount() { + this.fetchQualityMetrics(); + } + + componentDidUpdate(prevProps: TProps) { + if (prevProps.service !== this.props.service) { + // eslint-disable-next-line react/no-did-update-set-state + this.setState({ + qualityMetrics: undefined, + error: undefined, + loading: false, + }); + this.fetchQualityMetrics(); + } + } + + fetchQualityMetrics() { + const { + service, + } = this.props; + if (!service) return; + + this.setState({ loading: true }); + + JaegerAPI.fetchQualityMetrics(service) + .then((qualityMetrics: TQualityMetrics) => { + + this.setState({ qualityMetrics, loading: false }); + }) + .catch((error: Error) => { + this.setState({ + error, + loading: false, + }); + }); + } + + render() { + const { service } = this.props; + const { qualityMetrics, error, loading } = this.state; + return ( +
+
+ Quality Metrics +
+ {/* +
+ {stringSupplant(decorationSchema.name, { service, operation })} +
+ {decorationProgressbar ? ( +
{decorationProgressbar}
+ ) : ( + {decorationValue} + )} + {this.state.detailsLoading && ( +
+ +
+ )} + {this.state.details && ( + + )} + + */} +
+ ); + } +} + +export function mapStateToProps(state: ReduxState, ownProps: TOwnProps): TReduxProps { + const { services: stServices } = state; + const { services } = stServices; + const { service: serviceFromUrl } = queryString.parse(ownProps.location.search); + const service = Array.isArray(serviceFromUrl) ? serviceFromUrl[0] : serviceFromUrl; + return { + service, + services, + }; +} + +export function mapDispatchToProps(dispatch: Dispatch): TDispatchProps { + const { fetchServices } = bindActionCreators( + jaegerApiActions, + dispatch + ); + + return { + fetchServices, + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(UnconnectedQualityMetrics); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/index.tsx b/packages/jaeger-ui/src/components/QualityMetrics/index.tsx new file mode 100644 index 0000000000..46cedc6c68 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/index.tsx @@ -0,0 +1,149 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react'; +import { History as RouterHistory, Location } from 'history'; +import { connect } from 'react-redux'; +import { bindActionCreators, Dispatch } from 'redux'; +import queryString from 'query-string'; + +import * as jaegerApiActions from '../../actions/jaeger-api'; +import JaegerAPI from '../../api/jaeger'; +import BreakableText from '../common/BreakableText'; + +import { ReduxState } from '../../types'; +import { TQualityMetrics } from './types'; + +type TOwnProps = { + history: RouterHistory; + location: Location; +} + +type TDispatchProps = { + fetchServices: () => void; +}; + +type TReduxProps = { + service?: string; + services?: string[] | null; +} + +export type TProps = TDispatchProps & TReduxProps & TOwnProps; + +type TState = { + qualityMetrics?: TQualityMetrics; + error?: Error; + loading?: boolean; +}; + +export class UnconnectedQualityMetrics extends React.PureComponent { + state: TState = {}; + + componentDidMount() { + this.fetchQualityMetrics(); + } + + componentDidUpdate(prevProps: TProps) { + if (prevProps.service !== this.props.service) { + // eslint-disable-next-line react/no-did-update-set-state + this.setState({ + qualityMetrics: undefined, + error: undefined, + loading: false, + }); + this.fetchQualityMetrics(); + } + } + + fetchQualityMetrics() { + const { + service, + } = this.props; + if (!service) return; + + this.setState({ loading: true }); + + JaegerAPI.fetchQualityMetrics(service) + .then((qualityMetrics: TQualityMetrics) => { + + this.setState({ qualityMetrics, loading: false }); + }) + .catch((error: Error) => { + this.setState({ + error, + loading: false, + }); + }); + } + + render() { + const { service } = this.props; + const { qualityMetrics, error, loading } = this.state; + return ( +
+
+ Quality Metrics +
+ {/* +
+ {stringSupplant(decorationSchema.name, { service, operation })} +
+ {decorationProgressbar ? ( +
{decorationProgressbar}
+ ) : ( + {decorationValue} + )} + {this.state.detailsLoading && ( +
+ +
+ )} + {this.state.details && ( + + )} + + */} +
+ ); + } +} + +export function mapStateToProps(state: ReduxState, ownProps: TOwnProps): TReduxProps { + const { services: stServices } = state; + const { services } = stServices; + const { service: serviceFromUrl } = queryString.parse(ownProps.location.search); + const service = Array.isArray(serviceFromUrl) ? serviceFromUrl[0] : serviceFromUrl; + return { + service, + services, + }; +} + +export function mapDispatchToProps(dispatch: Dispatch): TDispatchProps { + const { fetchServices } = bindActionCreators( + jaegerApiActions, + dispatch + ); + + return { + fetchServices, + }; +} + +export default connect(mapStateToProps, mapDispatchToProps)(UnconnectedQualityMetrics); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/types.tsx b/packages/jaeger-ui/src/components/QualityMetrics/types.tsx new file mode 100644 index 0000000000..c61044a87a --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/types.tsx @@ -0,0 +1,69 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react'; + + +import { TPadColumnDef, TPadRow } from '../../model/path-agnostic-decorations/types'; + +export type TQualityMetrics = { + traceQualityDocumentationLink: string; + bannerText?: string | { + value: string; + styling: React.CSSProperties; // typedef + }; + scores: { + key: string; + label: string; + max: number; + value: number; + }[]; + metrics: { + name: string; + category: string; // matching some scores.key; + description: string; + metricDocumentationLink: string; + metricWeight: number; + passCount: number; + passExamples?: { + spanIDs?: string[]; + traceID: string; + }[]; + failureCount: number; + failureExamples?: { + spanIDs?: string[]; + traceID: string; + }[]; + exemptionCount?: number; + exemptionExamples?: { + spanIDs?: string[]; + traceID: string; + }[]; + details?: { + description?: string; + tableHeader?: string; + columns: TPadColumnDef[]; + rows: TPadRow[]; + }[]; + }[]; + clients: { + version: string; + minVersion: string; + count: number; + examples: { + spanIDs?: string[]; + traceID: string; + }[]; + }; +} diff --git a/packages/jaeger-ui/src/components/QualityMetrics/url.tsx b/packages/jaeger-ui/src/components/QualityMetrics/url.tsx new file mode 100644 index 0000000000..7382cff858 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/url.tsx @@ -0,0 +1,29 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { matchPath } from 'react-router-dom'; + +import prefixUrl from '../../utils/prefix-url'; + +export const ROUTE_PATH = prefixUrl('/quality-metrics'); + +const ROUTE_MATCHER = { path: ROUTE_PATH, strict: true, exact: true }; + +export function matches(path: string) { + return Boolean(matchPath(path, ROUTE_MATCHER)); +} + +export function getUrl() { + return ROUTE_PATH; +} diff --git a/packages/jaeger-ui/src/setupProxy.js b/packages/jaeger-ui/src/setupProxy.js index 4bf0572aa8..df4e026110 100644 --- a/packages/jaeger-ui/src/setupProxy.js +++ b/packages/jaeger-ui/src/setupProxy.js @@ -47,4 +47,14 @@ module.exports = function setupProxy(app) { xfwd: true, }) ); + app.use( + proxy('/qualitymetrics-v2', { + target: 'http://localhost:16686', + logLevel: 'silent', + secure: false, + changeOrigin: true, + ws: true, + xfwd: true, + }) + ); }; From 140014ccd4dc907f2f0aed3647897800c94aa1a3 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Mon, 13 Apr 2020 17:57:03 -0400 Subject: [PATCH 26/31] WIP: Render all data and dropdowns except banner TODO: Render banner text, style components, test Signed-off-by: Everett Ross --- packages/jaeger-ui/src/api/jaeger.js | 202 +++++++++++++++++- .../SidePanel/DetailsCard/index.tsx | 22 +- .../components/QualityMetrics/CountCard.tsx | 47 ++++ .../QualityMetrics/ExamplesLink.tsx | 64 ++++++ .../src/components/QualityMetrics/Header.tsx | 57 +++++ .../components/QualityMetrics/MetricCard.tsx | 168 +++++---------- .../components/QualityMetrics/ScoreCard.tsx | 143 +++---------- .../src/components/QualityMetrics/index.css | 20 ++ .../src/components/QualityMetrics/index.tsx | 90 +++++--- .../src/components/QualityMetrics/types.tsx | 32 ++- .../src/components/QualityMetrics/url.tsx | 15 +- .../components/common/CircularProgressbar.tsx | 72 +++++++ .../src/constants/default-config.tsx | 96 +++++++++ .../model/path-agnostic-decorations/index.tsx | 28 +-- .../model/path-agnostic-decorations/types.tsx | 3 +- 15 files changed, 738 insertions(+), 321 deletions(-) create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/CountCard.tsx create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/ExamplesLink.tsx create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/Header.tsx create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/index.css create mode 100644 packages/jaeger-ui/src/components/common/CircularProgressbar.tsx diff --git a/packages/jaeger-ui/src/api/jaeger.js b/packages/jaeger-ui/src/api/jaeger.js index e1ad20a8dc..5b0c91ddf9 100644 --- a/packages/jaeger-ui/src/api/jaeger.js +++ b/packages/jaeger-ui/src/api/jaeger.js @@ -80,8 +80,207 @@ const JaegerAPI = { archiveTrace(id) { return getJSON(`${this.apiRoot}archive/${id}`, { method: 'POST' }); }, + fetchQualityMetrics(service /* , lookback = DEFAULT_QUALITY_METRICS_LOOKBACK */) { + return getJSON(`/qualitymetrics-v2`, { query: { service /* , lookback */ } }); + }, fetchDecoration(url) { + console.log('calling url: ', url); return getJSON(url); + // eslint-disable-next-line no-unreachable + if (url.startsWith('/analytics') || url.startsWith('/api/serviceedges')) { + return getJSON(url); + } + if (url.length % 2 && url.startsWith('neapolitan')) { + return new Promise((res, rej) => + setTimeout(() => { + if (url.length % 4 === 1) res('No val here'); + else rej(new Error(`One of the unlucky quarter: ${url.length}`)); + }, 150) + ); + } + // eslint-disable-next-line no-unreachable + if (url === 'get graph') { + return new Promise(res => + setTimeout( + () => + res({ + deets: { + here: [ + { + count: { + value: 0, + styling: { + backgroundColor: 'red', + color: 'white', + }, + }, + value: 'first', + foo: 'bar', + bar: 'baz', + }, + { + count: 1, + value: 'second', + foo: 'bar too', + }, + { + count: 2, + value: 'third', + foo: 'bar three', + }, + { + count: 3, + value: 'second', + foo: 'bar too', + }, + { + count: 4, + value: 'third', + foo: 'bar three', + }, + { + count: 5, + value: 'second', + foo: 'bar too', + }, + { + count: 6, + value: 'third', + foo: 'bar three', + }, + { + count: 7, + value: 'second', + foo: 'bar too', + }, + { + count: 8, + value: 'third', + foo: 'bar three', + }, + { + count: 9, + value: 'second', + foo: 'bar too', + }, + { + count: 10, + value: 'third', + foo: 'bar three', + }, + { + count: 11, + value: 'second', + foo: 'bar too', + }, + { + count: 12, + value: 'third', + foo: 'bar three', + }, + { + count: 13, + value: 'second', + foo: 'bar too', + }, + { + count: 14, + value: 'third', + foo: 'bar three', + }, + { + count: 15, + value: 'second', + foo: 'bar too', + }, + { + count: 16, + value: 'third', + foo: 'bar three', + }, + { + count: 17, + value: 'second', + foo: 'bar too', + }, + { + count: 18, + value: 'third', + foo: 'bar three', + }, + { + count: 19, + value: 'second', + foo: 'bar too', + }, + { + count: 20, + value: 'third', + foo: 'bar three', + }, + ], + }, + defs: { + here: [ + 'count', + { + key: 'value', + label: 'The value column', + styling: { + backgroundColor: 'blue', + color: 'lightgrey', + }, + }, + 'foo', + ], + }, + }), + 2750 + ) + ); + } + // eslint-disable-next-line no-unreachable + if (url === 'get string') { + return new Promise(res => + setTimeout( + () => + res({ + deets: { + here: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', + }, + }), + 750 + ) + ); + } + // eslint-disable-next-line no-unreachable + if (url === 'get list') { + return new Promise(res => + setTimeout( + () => + res({ + deets: { + here: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'.split( + ' ' + ), + }, + }), + 750 + ) + ); + } + // eslint-disable-next-line no-unreachable + if (url === 'infinite load') + return new Promise(res => setTimeout(() => res('you are patient, eh?'), 1500000)); + // eslint-disable-next-line no-unreachable + if (url === 'deets err') + return new Promise((res, rej) => setTimeout(() => rej(new Error('you knew this would happen')), 600)); + // eslint-disable-next-line no-unreachable + if (url === 'deets 404') return new Promise(res => res({})); + // eslint-disable-next-line no-unreachable + return new Promise(res => setTimeout(() => res({ val: url.length ** 2 }), 150)); + // return getJSON(url); }, fetchDeepDependencyGraph(query) { return getJSON(`${ANALYTICS_ROOT}v1/dependencies`, { query }); @@ -89,9 +288,6 @@ const JaegerAPI = { fetchDependencies(endTs = new Date().getTime(), lookback = DEFAULT_DEPENDENCY_LOOKBACK) { return getJSON(`${this.apiRoot}dependencies`, { query: { endTs, lookback } }); }, - fetchQualityMetrics(service /* , lookback = DEFAULT_QUALITY_METRICS_LOOKBACK */) { - return getJSON(`/qualitymetrics-v2`, { query: { service /* , lookback */ } }); - }, fetchServiceOperations(serviceName) { return getJSON(`${this.apiRoot}services/${encodeURIComponent(serviceName)}/operations`); }, diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx index ca8157bd35..6c0c9c12bf 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx @@ -58,6 +58,7 @@ export default class DetailsCard extends React.PureComponent { static renderColumn(def: TPadColumnDef | string) { let dataIndex: string; let key: string; + let sortable: boolean = true; let style: React.CSSProperties | undefined; let title: string; if (typeof def === 'string') { @@ -68,6 +69,7 @@ export default class DetailsCard extends React.PureComponent { key = title = dataIndex = def.key; if (def.label) title = def.label; if (def.styling) style = def.styling; + if (def.preventSort) sortable = false; } const props = { @@ -95,14 +97,14 @@ export default class DetailsCard extends React.PureComponent { ); }, - sorter: (a: TPadRow, b: TPadRow) => { + sorter: sortable && ((a: TPadRow, b: TPadRow) => { const aData = a[dataIndex]; - const aValue = typeof aData === 'object' && aData.value !== undefined ? aData.value : aData; + const aValue = typeof aData === 'object' && typeof aData.value === 'string' ? aData.value : aData; const bData = b[dataIndex]; - const bValue = typeof bData === 'object' && bData.value !== undefined ? bData.value : bData; + const bValue = typeof bData === 'object' && typeof bData.value === 'string' ? bData.value : bData; if (aValue < bValue) return -1; return bValue < aValue ? 1 : 0; - }, + }), }; return ; @@ -132,7 +134,16 @@ export default class DetailsCard extends React.PureComponent { size="middle" dataSource={details} pagination={false} - rowKey={(row: TPadRow) => JSON.stringify(row)} + rowKey={(row: TPadRow) => JSON.stringify(row, function replacer(key: string, value: TPadRow | string | number | TStyledValue) { + function isRow(v: typeof value): v is TPadRow { return v === row }; + if (isRow(value)) return value; + if (typeof value === 'object') { + if (typeof value.value === 'string') return value.value; + const { key = "Unknown" } = value.value; + return key; + } + return value; + })} > {columnDefs.map(DetailsCard.renderColumn)} @@ -155,6 +166,7 @@ export default class DetailsCard extends React.PureComponent { render() { const { className, description, header } = this.props; + // TODO: Collapsible return (
diff --git a/packages/jaeger-ui/src/components/QualityMetrics/CountCard.tsx b/packages/jaeger-ui/src/components/QualityMetrics/CountCard.tsx new file mode 100644 index 0000000000..17e4af0bb2 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/CountCard.tsx @@ -0,0 +1,47 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react'; + +import ExamplesLink from './ExamplesLink'; + +import { TExample } from './types'; + +export type TProps = { + count?: number; + title?: string; + examples?: TExample[]; +} + +export default class ScoreCard extends React.PureComponent { + render() { + const { + count, + title, + examples, + } = this.props; + + if (count === undefined || title === undefined) return null; + + return ( +
+
+ {title} +
+ {count /* maybe antd number thing, those decent */} + +
+ ); + } +} diff --git a/packages/jaeger-ui/src/components/QualityMetrics/ExamplesLink.tsx b/packages/jaeger-ui/src/components/QualityMetrics/ExamplesLink.tsx new file mode 100644 index 0000000000..c44ba064ab --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/ExamplesLink.tsx @@ -0,0 +1,64 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react'; + +import NewWindowIcon from '../common/NewWindowIcon'; +import { getUrl } from '../SearchTracePage/url'; + +import { TExample } from './types'; + +export type TProps = { + examples?: TExample[]; + includeText?: boolean; +} + +type TExampleWithSpans = { + traceID: string; + spanIDs: string[]; +}; + +function hasSpans(example: TExample | TExampleWithSpans): example is TExampleWithSpans { + return Boolean(example.spanIDs && example.spanIDs.length); +} + +function getGetUrlArg(examples: TExample[]): { spanLinks: Record, traceID: string[] } { + const spanLinks: Record = {}; + const traceID: string[] = []; + examples.forEach((example: TExample) => { + if (hasSpans(example)) spanLinks[example.traceID] = example.spanIDs.join('.'); + else traceID.push(example.traceID); + }) + return { + spanLinks, + traceID, + }; +} + +export default class ExamplesLink extends React.PureComponent { + render() { + const { + examples, + includeText, + } = this.props; + + if (!examples || !examples.length) return null; + + return ( + + {includeText && 'Examples '} + + ); + } +} diff --git a/packages/jaeger-ui/src/components/QualityMetrics/Header.tsx b/packages/jaeger-ui/src/components/QualityMetrics/Header.tsx new file mode 100644 index 0000000000..800d8f970b --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/Header.tsx @@ -0,0 +1,57 @@ +// Copyright (c) 2019 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react'; +import { Icon, Input, Tooltip } from 'antd'; +import MdVisibility from 'react-icons/lib/md/visibility'; +import MdVisibilityOff from 'react-icons/lib/md/visibility-off'; + +// import HopsSelector from './HopsSelector'; +import NameSelector from '../DeepDependencies/Header/NameSelector'; +// import LayoutSettings from './LayoutSettings'; +// import { trackFilter, trackHeaderSetOperation, trackShowMatches } from '../index.track'; +// import UiFindInput from '../../common/UiFindInput'; +// import { EDirection, TDdgDistanceToPathElems, EDdgDensity } from '../../../model/ddg/types'; + +// import './index.css'; + +type TProps = { + service?: string; + services?: string[] | null; + setService: (service: string) => void; +}; +export default class Header extends React.PureComponent { + render() { + const { + service, + services, + setService, + } = this.props; + + return ( +
+
+ +
+
+ ); + } +} diff --git a/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx index 46cedc6c68..c75a74ddec 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx @@ -13,137 +13,67 @@ // limitations under the License. import * as React from 'react'; -import { History as RouterHistory, Location } from 'history'; -import { connect } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import queryString from 'query-string'; -import * as jaegerApiActions from '../../actions/jaeger-api'; -import JaegerAPI from '../../api/jaeger'; -import BreakableText from '../common/BreakableText'; +import CircularProgressbar from '../common/CircularProgressbar'; +import NewWindowIcon from '../common/NewWindowIcon'; +import DetailsCard from '../DeepDependencies/SidePanel/DetailsCard'; +import CountCard from './CountCard'; -import { ReduxState } from '../../types'; import { TQualityMetrics } from './types'; -type TOwnProps = { - history: RouterHistory; - location: Location; +export type TProps = { + // link: string; + metric: TQualityMetrics["metrics"][0]; } -type TDispatchProps = { - fetchServices: () => void; -}; - -type TReduxProps = { - service?: string; - services?: string[] | null; -} - -export type TProps = TDispatchProps & TReduxProps & TOwnProps; - -type TState = { - qualityMetrics?: TQualityMetrics; - error?: Error; - loading?: boolean; -}; - -export class UnconnectedQualityMetrics extends React.PureComponent { - state: TState = {}; - - componentDidMount() { - this.fetchQualityMetrics(); - } - - componentDidUpdate(prevProps: TProps) { - if (prevProps.service !== this.props.service) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ - qualityMetrics: undefined, - error: undefined, - loading: false, - }); - this.fetchQualityMetrics(); - } - } - - fetchQualityMetrics() { +export default class MetricCard extends React.PureComponent { + render() { const { - service, + // link, + metric: { + name, + category, + description, + metricDocumentationLink, + // metricWeight, + passCount, + passExamples, + failureCount, + failureExamples, + exemptionCount, + exemptionExamples, + details, + } } = this.props; - if (!service) return; - - this.setState({ loading: true }); - - JaegerAPI.fetchQualityMetrics(service) - .then((qualityMetrics: TQualityMetrics) => { - - this.setState({ qualityMetrics, loading: false }); - }) - .catch((error: Error) => { - this.setState({ - error, - loading: false, - }); - }); - } - - render() { - const { service } = this.props; - const { qualityMetrics, error, loading } = this.state; return ( -
-
- Quality Metrics -
- {/* -
- {stringSupplant(decorationSchema.name, { service, operation })} +
+
+
- {decorationProgressbar ? ( -
{decorationProgressbar}
- ) : ( - {decorationValue} - )} - {this.state.detailsLoading && ( -
- +
+
+ {name}
- )} - {this.state.details && ( - - )} - - */} +

{description}

+
+ + + +
+ {details && details.map(detail=> Boolean(detail.rows && detail.rows.length) && ( + + ))} +
); } } - -export function mapStateToProps(state: ReduxState, ownProps: TOwnProps): TReduxProps { - const { services: stServices } = state; - const { services } = stServices; - const { service: serviceFromUrl } = queryString.parse(ownProps.location.search); - const service = Array.isArray(serviceFromUrl) ? serviceFromUrl[0] : serviceFromUrl; - return { - service, - services, - }; -} - -export function mapDispatchToProps(dispatch: Dispatch): TDispatchProps { - const { fetchServices } = bindActionCreators( - jaegerApiActions, - dispatch - ); - - return { - fetchServices, - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(UnconnectedQualityMetrics); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.tsx b/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.tsx index 46cedc6c68..4fbb587e04 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.tsx @@ -13,137 +13,44 @@ // limitations under the License. import * as React from 'react'; -import { History as RouterHistory, Location } from 'history'; -import { connect } from 'react-redux'; -import { bindActionCreators, Dispatch } from 'redux'; -import queryString from 'query-string'; -import * as jaegerApiActions from '../../actions/jaeger-api'; -import JaegerAPI from '../../api/jaeger'; -import BreakableText from '../common/BreakableText'; +import CircularProgressbar from '../common/CircularProgressbar'; +import NewWindowIcon from '../common/NewWindowIcon'; -import { ReduxState } from '../../types'; import { TQualityMetrics } from './types'; -type TOwnProps = { - history: RouterHistory; - location: Location; +export type TProps = { + link: string; + score: TQualityMetrics["scores"][0]; } -type TDispatchProps = { - fetchServices: () => void; -}; - -type TReduxProps = { - service?: string; - services?: string[] | null; -} - -export type TProps = TDispatchProps & TReduxProps & TOwnProps; - -type TState = { - qualityMetrics?: TQualityMetrics; - error?: Error; - loading?: boolean; -}; - -export class UnconnectedQualityMetrics extends React.PureComponent { - state: TState = {}; - - componentDidMount() { - this.fetchQualityMetrics(); - } - - componentDidUpdate(prevProps: TProps) { - if (prevProps.service !== this.props.service) { - // eslint-disable-next-line react/no-did-update-set-state - this.setState({ - qualityMetrics: undefined, - error: undefined, - loading: false, - }); - this.fetchQualityMetrics(); - } - } - - fetchQualityMetrics() { +export default class ScoreCard extends React.PureComponent { + render() { const { - service, + link, + score: { + label, + max: maxValue, + value, + } } = this.props; - if (!service) return; - - this.setState({ loading: true }); - - JaegerAPI.fetchQualityMetrics(service) - .then((qualityMetrics: TQualityMetrics) => { - - this.setState({ qualityMetrics, loading: false }); - }) - .catch((error: Error) => { - this.setState({ - error, - loading: false, - }); - }); - } - - render() { - const { service } = this.props; - const { qualityMetrics, error, loading } = this.state; return ( -
-
- Quality Metrics -
- {/* -
- {stringSupplant(decorationSchema.name, { service, operation })} +
+
+ {label}
- {decorationProgressbar ? ( -
{decorationProgressbar}
- ) : ( - {decorationValue} - )} - {this.state.detailsLoading && ( -
- -
- )} - {this.state.details && ( - + +
+ {value < maxValue && ( + How to improve )} - - */}
); } } - -export function mapStateToProps(state: ReduxState, ownProps: TOwnProps): TReduxProps { - const { services: stServices } = state; - const { services } = stServices; - const { service: serviceFromUrl } = queryString.parse(ownProps.location.search); - const service = Array.isArray(serviceFromUrl) ? serviceFromUrl[0] : serviceFromUrl; - return { - service, - services, - }; -} - -export function mapDispatchToProps(dispatch: Dispatch): TDispatchProps { - const { fetchServices } = bindActionCreators( - jaegerApiActions, - dispatch - ); - - return { - fetchServices, - }; -} - -export default connect(mapStateToProps, mapDispatchToProps)(UnconnectedQualityMetrics); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/index.css b/packages/jaeger-ui/src/components/QualityMetrics/index.css new file mode 100644 index 0000000000..26c05f6d31 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/index.css @@ -0,0 +1,20 @@ +/* +Copyright (c) 2020 The Jaeger Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.QualityMetrics--ScoreCards { + display: flex; + justify-content: space-around; +} diff --git a/packages/jaeger-ui/src/components/QualityMetrics/index.tsx b/packages/jaeger-ui/src/components/QualityMetrics/index.tsx index 46cedc6c68..9ad02dcca7 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/index.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/index.tsx @@ -16,15 +16,22 @@ import * as React from 'react'; import { History as RouterHistory, Location } from 'history'; import { connect } from 'react-redux'; import { bindActionCreators, Dispatch } from 'redux'; -import queryString from 'query-string'; import * as jaegerApiActions from '../../actions/jaeger-api'; import JaegerAPI from '../../api/jaeger'; import BreakableText from '../common/BreakableText'; +import DetailsCard from '../DeepDependencies/SidePanel/DetailsCard'; +import ExamplesLink from './ExamplesLink'; +import Header from './Header'; +import MetricCard from './MetricCard'; +import ScoreCard from './ScoreCard'; +import { getUrl, getUrlState } from './url'; import { ReduxState } from '../../types'; import { TQualityMetrics } from './types'; +import './index.css'; + type TOwnProps = { history: RouterHistory; location: Location; @@ -50,6 +57,15 @@ type TState = { export class UnconnectedQualityMetrics extends React.PureComponent { state: TState = {}; + constructor(props: TProps) { + super(props); + + const { fetchServices, services } = props; + if (!services) { + fetchServices(); + } + } + componentDidMount() { this.fetchQualityMetrics(); } @@ -87,38 +103,58 @@ export class UnconnectedQualityMetrics extends React.PureComponent { + const { history } = this.props; + history.push(getUrl({ service })); + } + render() { - const { service } = this.props; + const { service, services } = this.props; const { qualityMetrics, error, loading } = this.state; return (
+
Quality Metrics
- {/* -
- {stringSupplant(decorationSchema.name, { service, operation })} -
- {decorationProgressbar ? ( -
{decorationProgressbar}
- ) : ( - {decorationValue} - )} - {this.state.detailsLoading && ( -
- -
- )} - {this.state.details && ( - + {qualityMetrics && ( + <> +
+ {qualityMetrics.scores.map(score => ( + + ))} + +
+
+ {qualityMetrics.metrics.map(metric => ( + + ))} +
+ ({ + ...clientRow, + examples: { + value: , + }, + }))} + header="Client Versions" + /> + )} - - */}
); } @@ -127,10 +163,8 @@ export class UnconnectedQualityMetrics extends React.PureComponent) { + if (!queryParams) return ROUTE_PATH; + + return `${ROUTE_PATH}?${queryString.stringify(queryParams)}`; } + +export const getUrlState = memoizeOne(function getUrlState(search: string): { service?: string } { + const { service: serviceFromUrl } = queryString.parse(search); + const service = Array.isArray(serviceFromUrl) ? serviceFromUrl[0] : serviceFromUrl; + if (service) return { service }; + return {}; +}); diff --git a/packages/jaeger-ui/src/components/common/CircularProgressbar.tsx b/packages/jaeger-ui/src/components/common/CircularProgressbar.tsx new file mode 100644 index 0000000000..8c978a436b --- /dev/null +++ b/packages/jaeger-ui/src/components/common/CircularProgressbar.tsx @@ -0,0 +1,72 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react'; +import { CircularProgressbar as CircularProgressbarImpl } from 'react-circular-progressbar'; + +import 'react-circular-progressbar/dist/styles.css'; + +type TProps = { + // key: string; + backgroundHue?: number; + decorationHue?: number; + maxValue: number; + strokeWidth?: number; + text?: string; + value: number; +} + +export default class CircularProgressbar extends React.PureComponent { + render() { + const { + // key, + backgroundHue, + decorationHue = 0, + maxValue, + strokeWidth, + text, + value, + } = this.props; + const scale = (value / maxValue) ** (1 / 4); + const saturation = Math.ceil(scale * 100); + const light = 50 + Math.ceil((1 - scale) * 50); + const decorationColor = `hsl(${decorationHue}, ${saturation}%, ${light}%)`; + const backgroundScale = ((maxValue - value) / maxValue) ** (1 / 4); + const backgroundSaturation = Math.ceil(backgroundScale * 100); + const backgroundLight = 50 + Math.ceil((1 - backgroundScale) * 50); + const decorationBackgroundColor = `hsl(${backgroundHue}, ${backgroundSaturation}%, ${backgroundLight}%)`; + + return ( + + ); + } +} diff --git a/packages/jaeger-ui/src/constants/default-config.tsx b/packages/jaeger-ui/src/constants/default-config.tsx index 198b84f9a3..6ffc24ef9c 100644 --- a/packages/jaeger-ui/src/constants/default-config.tsx +++ b/packages/jaeger-ui/src/constants/default-config.tsx @@ -56,6 +56,102 @@ export default deepFreeze( ], }, ], + pathAgnosticDecorations: [ + { + acronym: 'ME', + id: 'me', + name: 'Missing Edges', + summaryUrl: '/analytics/v1/serviceedges/missing/summary?source=connmon', + summaryPath: '#{service}.count', + detailUrl: 'serviceedges/missing?source=connmon&service=#{service}', + detailPath: 'missingEdges', + detailColumnDefPath: 'columnDefs', + }, + { + acronym: 'N', + id: 'n', + name: 'Neapolitan mix of success/error/no path', + summaryUrl: 'neapolitan #{service}', + summaryPath: 'val', + }, + { + acronym: 'AR', + id: 'ar', + name: 'All should resolve', + summaryUrl: 'all should res #{service}', + summaryPath: 'val', + }, + { + acronym: 'RGT', + id: 'rgt', + name: 'all should resolve details graph too #{service}', + summaryUrl: 'details too #{service}', + detailUrl: 'get graph', + detailPath: 'deets.here', + detailColumnDefPath: 'defs.here', + summaryPath: 'val', + }, + { + acronym: 'RST', + id: 'rst', + name: 'all should resolve details string too #{service}', + summaryUrl: 'details too #{service}', + detailUrl: 'get string', + detailPath: 'deets.here', + summaryPath: 'val', + }, + { + acronym: 'RLT', + id: 'rlt', + name: 'all should resolve details list too #{service}', + summaryUrl: 'details too #{service}', + detailUrl: 'get list', + detailPath: 'deets.here', + summaryPath: 'val', + }, + { + acronym: 'RIL', + id: 'ril', + name: 'all should resolve, but infinite load details', + summaryUrl: 'details too #{service}', + detailUrl: 'infinite load', + detailPath: 'deets.here', + summaryPath: 'val', + }, + { + acronym: 'RDE', + id: 'rde', + name: 'all should resolve, but details err', + summaryUrl: 'details too #{service}', + detailUrl: 'deets err', + detailPath: 'deets.here', + summaryPath: 'val', + }, + { + acronym: 'RD4', + id: 'rd4', + name: 'all should resolve, but details not found', + summaryUrl: 'details too #{service}', + detailUrl: 'deets 404', + detailPath: 'deets.here', + summaryPath: 'val', + }, + { + acronym: 'OPs', + id: 'ops', + name: 'got dem ops', + summaryUrl: 'neapolitan #{service}', + summaryPath: 'val', + opSummaryUrl: 'just #{service}', + opSummaryPath: 'val', + detailUrl: 'deets 404', + detailPath: 'deets.here', + opDetailUrl: 'get list', + opDetailPath: 'deets.here', + } /*, { + TODO: op example too + }*/, + ], search: { maxLookback: { label: '2 Days', diff --git a/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx b/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx index b1296f84d7..4df28b52a1 100644 --- a/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx +++ b/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx @@ -15,7 +15,7 @@ import * as React from 'react'; import _get from 'lodash/get'; import queryString from 'query-string'; -import { CircularProgressbar } from 'react-circular-progressbar'; +import CircularProgressbar from '../../components/common/CircularProgressbar'; import { PROGRESS_BAR_STROKE_WIDTH, @@ -23,8 +23,6 @@ import { } from '../../components/DeepDependencies/Graph/DdgNodeContent/constants'; import { ReduxState } from '../../types/index'; -import 'react-circular-progressbar/dist/styles.css'; - export type TDecorationFromState = { decorationID?: string; decorationProgressbar?: React.ReactNode; @@ -47,32 +45,12 @@ export default function extractDecorationFromState( decorationMax = _get(state, `pathAgnosticDecorations.${decorationID}.withoutOpMax`); } - const scale = (decorationValue / decorationMax) ** (1 / 4); - const saturation = Math.ceil(scale * 100); - const light = 50 + Math.ceil((1 - scale) * 50); - const decorationColor = `hsl(0, ${saturation}%, ${light}%)`; - const backgroundScale = ((decorationMax - decorationValue) / decorationMax) ** (1 / 4); - const backgroundSaturation = Math.ceil(backgroundScale * 100); - const backgroundLight = 50 + Math.ceil((1 - backgroundScale) * 50); - const decorationBackgroundColor = `hsl(120, ${backgroundSaturation}%, ${backgroundLight}%)`; - const decorationProgressbar = typeof decorationValue === 'number' ? ( Date: Tue, 14 Apr 2020 19:27:51 -0400 Subject: [PATCH 27/31] WIP: Style components, implement lookback TODO: Render banner text, render weight, test Signed-off-by: Everett Ross --- packages/jaeger-ui/src/api/jaeger.js | 4 +- .../SidePanel/DetailsCard/index.css | 35 +++++++++++ .../SidePanel/DetailsCard/index.tsx | 41 +++++++++++-- .../SidePanel/DetailsPanel.css | 1 + .../components/QualityMetrics/CountCard.css | 36 +++++++++++ .../components/QualityMetrics/CountCard.tsx | 10 +++- .../src/components/QualityMetrics/Header.css | 41 +++++++++++++ .../src/components/QualityMetrics/Header.tsx | 32 ++++++---- .../components/QualityMetrics/MetricCard.css | 59 +++++++++++++++++++ .../components/QualityMetrics/MetricCard.tsx | 23 ++++++-- .../components/QualityMetrics/ScoreCard.css | 33 +++++++++++ .../components/QualityMetrics/ScoreCard.tsx | 14 +++-- .../src/components/QualityMetrics/index.css | 11 ++++ .../src/components/QualityMetrics/index.tsx | 37 ++++++++---- .../src/components/QualityMetrics/url.tsx | 21 +++++-- .../components/common/CircularProgressbar.tsx | 10 ++-- 16 files changed, 351 insertions(+), 57 deletions(-) create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/CountCard.css create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/Header.css create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/MetricCard.css create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.css diff --git a/packages/jaeger-ui/src/api/jaeger.js b/packages/jaeger-ui/src/api/jaeger.js index 5b0c91ddf9..5489bfbcbb 100644 --- a/packages/jaeger-ui/src/api/jaeger.js +++ b/packages/jaeger-ui/src/api/jaeger.js @@ -80,8 +80,8 @@ const JaegerAPI = { archiveTrace(id) { return getJSON(`${this.apiRoot}archive/${id}`, { method: 'POST' }); }, - fetchQualityMetrics(service /* , lookback = DEFAULT_QUALITY_METRICS_LOOKBACK */) { - return getJSON(`/qualitymetrics-v2`, { query: { service /* , lookback */ } }); + fetchQualityMetrics(service, lookback) { + return getJSON(`/qualitymetrics-v2`, { query: { service, lookback } }); }, fetchDecoration(url) { console.log('calling url: ', url); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css index cdc7deb58e..4f766271a1 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css @@ -20,6 +20,30 @@ limitations under the License. padding: 0.5em; } +.DetailsCard--ButtonHeaderWrapper { + display: flex; +} + +.DetailsCard--Collapser { + border: none; + background: transparent; + flex: 0; + font-size: 1.5rem; + transition: transform 0.25s ease-out; +} + +.DetailsCard--Collapser:focus { + outline: none; +} + +.DetailsCard--Collapser.is-collapsed { + transform: rotate(-90deg); +} + +.DetailsCard--HeaderWrapper { + flex-grow: 1; +} + .DetailsCard--Header { border-bottom: solid 1px #ddd; box-shadow: 0px 5px 5px -5px rgba(0, 0, 0, 0.3); @@ -27,9 +51,20 @@ limitations under the License. padding: 0.1em 0.3em; } +.DetailsCard--Description { + margin-bottom: 0; + max-width: 90%; +} + .DetailsCard--DetailsWrapper { overflow: scroll; margin-top: 0.5em; + transition: max-height 0.25s ease-out; + max-height: 60vh; +} + +.DetailsCard--DetailsWrapper.is-collapsed { + max-height: 0px; } .DetailsCard--DetailsWrapper th { diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx index 6c0c9c12bf..03663ee4f2 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx @@ -14,7 +14,9 @@ import * as React from 'react'; import { List, Table } from 'antd'; +import cx from 'classnames'; import _isEmpty from 'lodash/isEmpty'; +import MdKeyboardArrowDown from 'react-icons/lib/md/keyboard-arrow-down'; import { TPadColumnDef, @@ -31,17 +33,30 @@ const { Item } = List; type TProps = { className?: string; + collapsible?: boolean; columnDefs?: TPadColumnDefs; description?: string; details: TPadDetails; header: string; }; +type TState = { + collapsed: boolean; +} + function isList(arr: string[] | TPadRow[]): arr is string[] { return typeof arr[0] === 'string'; } export default class DetailsCard extends React.PureComponent { + state: TState; + + constructor(props: TProps) { + super(props); + + this.state = { collapsed: Boolean(props.collapsible) }; + } + static renderList(details: string[]) { return ( { return {details}; } + toggleCollapse = () => { + this.setState({ collapsed: !this.state.collapsed }); + } + render() { - const { className, description, header } = this.props; + const { collapsed } = this.state; + const { className, collapsible, description, header } = this.props; // TODO: Collapsible return ( -
-
- {header} - {description &&

{description}

} +
+
+ {collapsible && } +
+ {header} + {description &&

{description}

} +
-
{this.renderDetails()}
+
{this.renderDetails()}
); } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css index 417afcccf6..908660c0a1 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css @@ -50,6 +50,7 @@ limitations under the License. background-color: #ffffff; border: solid 2px rgba(0, 0, 0, 0.3); margin: 0.5em; + overflow: hidden; } .Ddg--DetailsPanel--DetailsCard.is-error { diff --git a/packages/jaeger-ui/src/components/QualityMetrics/CountCard.css b/packages/jaeger-ui/src/components/QualityMetrics/CountCard.css new file mode 100644 index 0000000000..40ec01f890 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/CountCard.css @@ -0,0 +1,36 @@ +/* +Copyright (c) 2020 The Jaeger Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.CountCard { + display: flex; + flex-direction: column; + /* padding: 0 5px; */ + margin: 5px 5%; + padding: 5px; + text-align: center; +} + +.CountCard--Count { + font-size: 1.6em; + padding: 0.3em; +} + +.CountCard--TitleHeader { + border-bottom: solid 1px #ddd; + box-shadow: 0px 5px 5px -5px rgba(0, 0, 0, 0.3); + font-size: 1.1em; + margin: 0 auto; +} diff --git a/packages/jaeger-ui/src/components/QualityMetrics/CountCard.tsx b/packages/jaeger-ui/src/components/QualityMetrics/CountCard.tsx index 17e4af0bb2..80799960b3 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/CountCard.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/CountCard.tsx @@ -24,6 +24,8 @@ export type TProps = { examples?: TExample[]; } +import './CountCard.css'; + export default class ScoreCard extends React.PureComponent { render() { const { @@ -36,10 +38,12 @@ export default class ScoreCard extends React.PureComponent { return (
-
+ {title} -
- {count /* maybe antd number thing, those decent */} + + + {count} +
); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/Header.css b/packages/jaeger-ui/src/components/QualityMetrics/Header.css new file mode 100644 index 0000000000..1a603efbc8 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/Header.css @@ -0,0 +1,41 @@ +/* +Copyright (c) 2020 Uber Technologies, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.QualityMetrics--Header { + align-items: center; + background: #fafafa; + border-bottom: 1px solid #ddd; + box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1); + display: flex; + flex-direction: row; + padding: 0.75rem 1.25rem 0.75rem 1.25rem; + position: relative; + z-index: 10; +} + +.QualityMetrics--Header--LookbackLabel { + color: var(--tx-color-muted); + cursor: pointer; + font-size: 1.5em; + margin: 0 0.6em 0 0; + outline: 4px solid transparent; +} + +.QualityMetrics--Header--LookbackSuffix { + color: var(--tx-color-muted); + margin: 0 0 0 0.6em; + outline: 4px solid transparent; +} diff --git a/packages/jaeger-ui/src/components/QualityMetrics/Header.tsx b/packages/jaeger-ui/src/components/QualityMetrics/Header.tsx index 800d8f970b..afc218f2a9 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/Header.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/Header.tsx @@ -13,7 +13,7 @@ // limitations under the License. import * as React from 'react'; -import { Icon, Input, Tooltip } from 'antd'; +import { InputNumber } from 'antd'; import MdVisibility from 'react-icons/lib/md/visibility'; import MdVisibilityOff from 'react-icons/lib/md/visibility-off'; @@ -24,33 +24,39 @@ import NameSelector from '../DeepDependencies/Header/NameSelector'; // import UiFindInput from '../../common/UiFindInput'; // import { EDirection, TDdgDistanceToPathElems, EDdgDensity } from '../../../model/ddg/types'; -// import './index.css'; +import './Header.css'; type TProps = { + lookback: number; service?: string; services?: string[] | null; + setLookback: (lookback: number | string | undefined) => void; setService: (service: string) => void; }; + export default class Header extends React.PureComponent { render() { const { + lookback, service, services, setService, + setLookback, } = this.props; return ( -
-
- -
+
+ + + + (in hours)
); } diff --git a/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.css b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.css new file mode 100644 index 0000000000..db5da1529c --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.css @@ -0,0 +1,59 @@ +/* +Copyright (c) 2020 The Jaeger Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.MetricCard { + border-radius: 7px; + border: 1px rgba(0, 0, 0, 0.5) solid; + display: flex; + margin: 5px 5%; +} + +.MetricCard--Body { + border-left: solid 1px rgba(0, 0, 0, 0.3); + box-shadow: -3px 0 3px rgba(0, 0, 0, 0.1); + flex-grow: 1; + padding: 5px; + text-align: center; +} + +.MetricCard--CircularProgressbarWrapper { + flex-shrink: 0; + max-width: 180px; + padding: 5px; + width: 25%; +} + +.MetricCard--CountsWrapper { + display: flex; + flex-wrap: wrap; + justify-content: space-around; +} + +.MetricCard--Description { + margin: 0 auto; + width: 60%; +} + +.MetricCard--Details { + text-align: left; + padding: 0.3em; +} + +.MetricCard--TitleHeader { + border-bottom: solid 1px #ddd; + box-shadow: 0px 5px 5px -5px rgba(0, 0, 0, 0.3); + font-size: 1.3em; +} diff --git a/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx index c75a74ddec..7a0e4c8dd1 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx @@ -21,11 +21,18 @@ import CountCard from './CountCard'; import { TQualityMetrics } from './types'; +import './MetricCard.css'; + export type TProps = { // link: string; metric: TQualityMetrics["metrics"][0]; } +const dividToFixedFloorPercentage = (pass: number, fail: number) => { + const str = `${pass/(pass+fail)*100}.0` + return `${str.substring(0, str.indexOf('.') + 2)}%`; +} + export default class MetricCard extends React.PureComponent { render() { const { @@ -49,25 +56,29 @@ export default class MetricCard extends React.PureComponent {
-
+ {name} -
-

{description}

+ +

{description}

- {details && details.map(detail=> Boolean(detail.rows && detail.rows.length) && ( + {details && details.map(detail => Boolean(detail.rows && detail.rows.length) && ( diff --git a/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.css b/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.css new file mode 100644 index 0000000000..17412aa571 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.css @@ -0,0 +1,33 @@ +/* +Copyright (c) 2020 The Jaeger Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.ScoreCard { + margin: 10px; + max-width: 25%; + min-width: 180px; + text-align: center; +} + +.ScoreCard--CircularProgressbarWrapper { + padding: 3px; +} + +.ScoreCard--TitleHeader { + border-bottom: solid 1px #ddd; + box-shadow: 0px 5px 5px -5px rgba(0, 0, 0, 0.3); + font-size: 1.8em; + /* margin: 0 auto; */ +} diff --git a/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.tsx b/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.tsx index 4fbb587e04..a24f8c87a8 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.tsx @@ -19,6 +19,8 @@ import NewWindowIcon from '../common/NewWindowIcon'; import { TQualityMetrics } from './types'; +import './ScoreCard.css'; + export type TProps = { link: string; score: TQualityMetrics["scores"][0]; @@ -34,22 +36,24 @@ export default class ScoreCard extends React.PureComponent { value, } } = this.props; + const linkText = value < maxValue + ? 'How to improve ' + : 'Great! What does this mean '; return (
-
+ {label} -
+
- {value < maxValue && ( - How to improve - )} + {linkText}
); } diff --git a/packages/jaeger-ui/src/components/QualityMetrics/index.css b/packages/jaeger-ui/src/components/QualityMetrics/index.css index 26c05f6d31..c139ef855d 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/index.css +++ b/packages/jaeger-ui/src/components/QualityMetrics/index.css @@ -14,7 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ +.QualityMetrics { + display: flex; + flex-direction: column; + height: calc(100vh - var(--nav-height)); +} + .QualityMetrics--ScoreCards { display: flex; + flex-wrap: wrap; justify-content: space-around; } + +.QualityMetrics--Body { + overflow: scroll; +} diff --git a/packages/jaeger-ui/src/components/QualityMetrics/index.tsx b/packages/jaeger-ui/src/components/QualityMetrics/index.tsx index 9ad02dcca7..733b31c8f0 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/index.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/index.tsx @@ -42,6 +42,7 @@ type TDispatchProps = { }; type TReduxProps = { + lookback: number; service?: string; services?: string[] | null; } @@ -71,7 +72,7 @@ export class UnconnectedQualityMetrics extends React.PureComponent { this.setState({ qualityMetrics, loading: false }); @@ -103,27 +105,37 @@ export class UnconnectedQualityMetrics extends React.PureComponent { + if (!lookback || typeof lookback === 'string') return; + if (lookback < 1 || lookback !== Math.floor(lookback)) return; + + const { history, service = "" } = this.props; + history.push(getUrl({ lookback, service })); + } + setService = (service: string) => { - const { history } = this.props; - history.push(getUrl({ service })); + const { history, lookback } = this.props; + history.push(getUrl({ lookback, service })); } render() { - const { service, services } = this.props; + const { lookback, service, services } = this.props; const { qualityMetrics, error, loading } = this.state; return (
-
-
- Quality Metrics -
+
{qualityMetrics && ( - <> +
{qualityMetrics.scores.map(score => ( ))} -
{qualityMetrics.metrics.map(metric => ( @@ -131,6 +143,7 @@ export class UnconnectedQualityMetrics extends React.PureComponent - +
)}
); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/url.tsx b/packages/jaeger-ui/src/components/QualityMetrics/url.tsx index ec911ea0fb..02bb4270f0 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/url.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/url.tsx @@ -26,15 +26,26 @@ export function matches(path: string) { return Boolean(matchPath(path, ROUTE_MATCHER)); } -export function getUrl(queryParams?: Record) { +export function getUrl(queryParams?: Record) { if (!queryParams) return ROUTE_PATH; return `${ROUTE_PATH}?${queryString.stringify(queryParams)}`; } -export const getUrlState = memoizeOne(function getUrlState(search: string): { service?: string } { - const { service: serviceFromUrl } = queryString.parse(search); +type TReturnValue = { + lookback: number; + service?: string +} + +export const getUrlState = memoizeOne(function getUrlState(search: string): TReturnValue { + const { lookback: lookbackFromUrl, service: serviceFromUrl } = queryString.parse(search); const service = Array.isArray(serviceFromUrl) ? serviceFromUrl[0] : serviceFromUrl; - if (service) return { service }; - return {}; + const lookbackStr = Array.isArray(lookbackFromUrl) ? lookbackFromUrl[0] : lookbackFromUrl; + const lookback = lookbackStr && Number.parseInt(lookbackStr); + const rv: TReturnValue = { + lookback: 1, + }; + if (service) rv.service = service; + if (lookback) rv.lookback = lookback; + return rv; }); diff --git a/packages/jaeger-ui/src/components/common/CircularProgressbar.tsx b/packages/jaeger-ui/src/components/common/CircularProgressbar.tsx index 8c978a436b..886ddd3232 100644 --- a/packages/jaeger-ui/src/components/common/CircularProgressbar.tsx +++ b/packages/jaeger-ui/src/components/common/CircularProgressbar.tsx @@ -39,12 +39,12 @@ export default class CircularProgressbar extends React.PureComponent { value, } = this.props; const scale = (value / maxValue) ** (1 / 4); - const saturation = Math.ceil(scale * 100); - const light = 50 + Math.ceil((1 - scale) * 50); + const saturation = 20 + Math.ceil(scale * 80); + const light = 50 + Math.ceil((1 - scale) * 30); const decorationColor = `hsl(${decorationHue}, ${saturation}%, ${light}%)`; const backgroundScale = ((maxValue - value) / maxValue) ** (1 / 4); - const backgroundSaturation = Math.ceil(backgroundScale * 100); - const backgroundLight = 50 + Math.ceil((1 - backgroundScale) * 50); + const backgroundSaturation = 20 + Math.ceil(backgroundScale * 80); + const backgroundLight = 50 + Math.ceil((1 - backgroundScale) * 30); const decorationBackgroundColor = `hsl(${backgroundHue}, ${backgroundSaturation}%, ${backgroundLight}%)`; return ( @@ -58,7 +58,7 @@ export default class CircularProgressbar extends React.PureComponent { fill: decorationColor, }, trail: { - stroke: backgroundHue ? decorationBackgroundColor : 'transparent', + stroke: backgroundHue !== undefined ? decorationBackgroundColor : 'transparent', strokeLinecap: 'butt', }, }} From 0928275504cceb73fcc98535592b19bf2740b304 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Wed, 15 Apr 2020 19:11:03 -0400 Subject: [PATCH 28/31] Debounce InputNumber, limit search url length, add metric documentation tooltip, tweak styles, add BannerText, handle loading, handle error TODO: test, cleanup Signed-off-by: Everett Ross --- .../Graph/DdgNodeContent/constants.tsx | 4 +- .../SidePanel/DetailsCard/index.css | 1 + .../components/QualityMetrics/BannerText.css | 22 ++++++++++ .../components/QualityMetrics/BannerText.tsx | 44 +++++++++++++++++++ .../QualityMetrics/ExamplesLink.tsx | 2 +- .../src/components/QualityMetrics/Header.tsx | 36 ++++++++++----- .../components/QualityMetrics/MetricCard.css | 2 +- .../components/QualityMetrics/MetricCard.tsx | 8 +++- .../components/QualityMetrics/ScoreCard.css | 1 - .../src/components/QualityMetrics/index.css | 15 +++++-- .../src/components/QualityMetrics/index.tsx | 14 +++++- .../src/components/SearchTracePage/url.tsx | 10 ++++- 12 files changed, 137 insertions(+), 22 deletions(-) create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/BannerText.css create mode 100644 packages/jaeger-ui/src/components/QualityMetrics/BannerText.tsx diff --git a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/constants.tsx b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/constants.tsx index 9583fc70bb..8c0d741301 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/constants.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/Graph/DdgNodeContent/constants.tsx @@ -24,7 +24,9 @@ export const RADIUS = 75; export const WORD_RX = /\W*\w+\W*/g; // While browsers suport URLs of unlimited length, many server clients do not handle more than this max -export const MAX_LENGTH = 2083; +// export const MAX_LENGTH = 2083; +// get unpreditable, unreproducable results past 7400 characters +export const MAX_LENGTH = 7000; export const MAX_LINKED_TRACES = 35; export const MIN_LENGTH = getSearchUrl().length; export const PARAM_NAME_LENGTH = '&traceID='.length; diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css index 4f766271a1..8259c3c353 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.css @@ -27,6 +27,7 @@ limitations under the License. .DetailsCard--Collapser { border: none; background: transparent; + cursor: pointer; flex: 0; font-size: 1.5rem; transition: transform 0.25s ease-out; diff --git a/packages/jaeger-ui/src/components/QualityMetrics/BannerText.css b/packages/jaeger-ui/src/components/QualityMetrics/BannerText.css new file mode 100644 index 0000000000..83e86a6adf --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/BannerText.css @@ -0,0 +1,22 @@ +/* +Copyright (c) 2020 Uber Technologies, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.BannerText { + font-size: 1.5em; + font-weight: heavy; + padding: 0.5rem 1rem 0.5rem 1rem; + text-align: center; +} diff --git a/packages/jaeger-ui/src/components/QualityMetrics/BannerText.tsx b/packages/jaeger-ui/src/components/QualityMetrics/BannerText.tsx new file mode 100644 index 0000000000..28ceadc366 --- /dev/null +++ b/packages/jaeger-ui/src/components/QualityMetrics/BannerText.tsx @@ -0,0 +1,44 @@ +// Copyright (c) 2020 Uber Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import * as React from 'react'; + +import CircularProgressbar from '../common/CircularProgressbar'; +import NewWindowIcon from '../common/NewWindowIcon'; + +import { TQualityMetrics } from './types'; + +import './BannerText.css'; + +export type TProps = { + bannerText: TQualityMetrics["bannerText"]; +} + +export default class BannerText extends React.PureComponent { + render() { + const { + bannerText + } = this.props; + if (!bannerText) return null; + + const { styling = undefined } = typeof bannerText === 'object' ? bannerText : {}; + const text = typeof bannerText === 'object' ? bannerText.value : bannerText; + + return ( +
+ {text} +
+ ); + } +} diff --git a/packages/jaeger-ui/src/components/QualityMetrics/ExamplesLink.tsx b/packages/jaeger-ui/src/components/QualityMetrics/ExamplesLink.tsx index c44ba064ab..947003389f 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/ExamplesLink.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/ExamplesLink.tsx @@ -37,7 +37,7 @@ function getGetUrlArg(examples: TExample[]): { spanLinks: Record const spanLinks: Record = {}; const traceID: string[] = []; examples.forEach((example: TExample) => { - if (hasSpans(example)) spanLinks[example.traceID] = example.spanIDs.join('.'); + if (hasSpans(example)) spanLinks[example.traceID] = example.spanIDs.join(' '); else traceID.push(example.traceID); }) return { diff --git a/packages/jaeger-ui/src/components/QualityMetrics/Header.tsx b/packages/jaeger-ui/src/components/QualityMetrics/Header.tsx index afc218f2a9..2e172c1906 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/Header.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/Header.tsx @@ -1,4 +1,4 @@ -// Copyright (c) 2019 Uber Technologies, Inc. +// Copyright (c) 2020 Uber Technologies, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,15 +14,9 @@ import * as React from 'react'; import { InputNumber } from 'antd'; -import MdVisibility from 'react-icons/lib/md/visibility'; -import MdVisibilityOff from 'react-icons/lib/md/visibility-off'; +import _debounce from 'lodash/debounce'; -// import HopsSelector from './HopsSelector'; import NameSelector from '../DeepDependencies/Header/NameSelector'; -// import LayoutSettings from './LayoutSettings'; -// import { trackFilter, trackHeaderSetOperation, trackShowMatches } from '../index.track'; -// import UiFindInput from '../../common/UiFindInput'; -// import { EDirection, TDdgDistanceToPathElems, EDdgDensity } from '../../../model/ddg/types'; import './Header.css'; @@ -34,15 +28,35 @@ type TProps = { setService: (service: string) => void; }; -export default class Header extends React.PureComponent { +type TState = { + ownInputValue: number | undefined; +}; + +export default class Header extends React.PureComponent { + state: TState = { + ownInputValue: undefined, + }; + + setLookback = _debounce((lookback: number | string | undefined) => { + this.setState({ ownInputValue: undefined }); + this.props.setLookback(lookback); + }, 350); + + handleInputChange = (value: string | number | undefined) => { + if (typeof value === 'string') return; + this.setState({ ownInputValue: value }); + this.setLookback(value); + }; + render() { const { lookback, service, services, setService, - setLookback, } = this.props; + const { ownInputValue } = this.state; + const lookbackValue = ownInputValue !== undefined ? ownInputValue : lookback; return (
@@ -55,7 +69,7 @@ export default class Header extends React.PureComponent { options={services || []} /> - + (in hours)
); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.css b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.css index db5da1529c..de4705d2ba 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.css +++ b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.css @@ -55,5 +55,5 @@ limitations under the License. .MetricCard--TitleHeader { border-bottom: solid 1px #ddd; box-shadow: 0px 5px 5px -5px rgba(0, 0, 0, 0.3); - font-size: 1.3em; + font-size: 1.5em; } diff --git a/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx index 7a0e4c8dd1..164ae8d6d5 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx @@ -13,6 +13,7 @@ // limitations under the License. import * as React from 'react'; +import { Tooltip } from 'antd'; import CircularProgressbar from '../common/CircularProgressbar'; import NewWindowIcon from '../common/NewWindowIcon'; @@ -65,7 +66,12 @@ export default class MetricCard extends React.PureComponent {
- {name} + {name} + +

{description}

diff --git a/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.css b/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.css index 17412aa571..171748f9d6 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.css +++ b/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.css @@ -29,5 +29,4 @@ limitations under the License. border-bottom: solid 1px #ddd; box-shadow: 0px 5px 5px -5px rgba(0, 0, 0, 0.3); font-size: 1.8em; - /* margin: 0 auto; */ } diff --git a/packages/jaeger-ui/src/components/QualityMetrics/index.css b/packages/jaeger-ui/src/components/QualityMetrics/index.css index c139ef855d..c77383e12f 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/index.css +++ b/packages/jaeger-ui/src/components/QualityMetrics/index.css @@ -20,12 +20,19 @@ limitations under the License. height: calc(100vh - var(--nav-height)); } +.QualityMetrics--Body { + overflow: scroll; +} + +.QualityMetrics--Error { + font-size: 1.7em; + margin: 0 auto; + max-width: 80%; + padding: 1em; +} + .QualityMetrics--ScoreCards { display: flex; flex-wrap: wrap; justify-content: space-around; } - -.QualityMetrics--Body { - overflow: scroll; -} diff --git a/packages/jaeger-ui/src/components/QualityMetrics/index.tsx b/packages/jaeger-ui/src/components/QualityMetrics/index.tsx index 733b31c8f0..1691d744f9 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/index.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/index.tsx @@ -20,7 +20,9 @@ import { bindActionCreators, Dispatch } from 'redux'; import * as jaegerApiActions from '../../actions/jaeger-api'; import JaegerAPI from '../../api/jaeger'; import BreakableText from '../common/BreakableText'; +import LoadingIndicator from '../common/LoadingIndicator'; import DetailsCard from '../DeepDependencies/SidePanel/DetailsCard'; +import BannerText from './BannerText'; import ExamplesLink from './ExamplesLink'; import Header from './Header'; import MetricCard from './MetricCard'; @@ -94,7 +96,6 @@ export class UnconnectedQualityMetrics extends React.PureComponent { - this.setState({ qualityMetrics, loading: false }); }) .catch((error: Error) => { @@ -131,6 +132,8 @@ export class UnconnectedQualityMetrics extends React.PureComponent {qualityMetrics && ( + <> +
{qualityMetrics.scores.map(score => ( @@ -167,6 +170,15 @@ export class UnconnectedQualityMetrics extends React.PureComponent
+ + )} + {loading && ( + + )} + {error && ( +
+ {error.message} +
)}
); diff --git a/packages/jaeger-ui/src/components/SearchTracePage/url.tsx b/packages/jaeger-ui/src/components/SearchTracePage/url.tsx index f1567d2b48..756bf0396d 100644 --- a/packages/jaeger-ui/src/components/SearchTracePage/url.tsx +++ b/packages/jaeger-ui/src/components/SearchTracePage/url.tsx @@ -17,6 +17,7 @@ import queryString from 'query-string'; import { matchPath } from 'react-router-dom'; import prefixUrl from '../../utils/prefix-url'; +import { MAX_LENGTH } from '../DeepDependencies/Graph/DdgNodeContent/constants'; import { SearchQuery } from '../../types/search'; @@ -55,7 +56,14 @@ export function getUrl(query?: TUrlState) { }, []), traceID: ids && ids.length ? ids : undefined, }; - return `${searchUrl}?${queryString.stringify(stringifyArg)}`; + + const fullUrl = `${searchUrl}?${queryString.stringify(stringifyArg)}`; + if (fullUrl.length <= MAX_LENGTH) return fullUrl; + + const truncated = fullUrl.slice(0, MAX_LENGTH + 1); + if (truncated[MAX_LENGTH] === '&') return truncated.slice(0, -1); + + return truncated.slice(0, truncated.lastIndexOf('&')); } export const getUrlState: (search: string) => TUrlState = memoizeOne(function getUrlState( From 52f48d780c3d8f25d52046a507ffdcfe65356b50 Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Wed, 15 Apr 2020 19:36:49 -0400 Subject: [PATCH 29/31] Cleanup Signed-off-by: Everett Ross --- packages/jaeger-ui/src/api/jaeger.js | 196 ------------------ .../SidePanel/DetailsCard/index.tsx | 84 ++++---- .../components/QualityMetrics/BannerText.tsx | 11 +- .../components/QualityMetrics/CountCard.tsx | 20 +- .../QualityMetrics/ExamplesLink.tsx | 16 +- .../src/components/QualityMetrics/Header.tsx | 11 +- .../components/QualityMetrics/MetricCard.tsx | 54 ++--- .../components/QualityMetrics/ScoreCard.tsx | 25 +-- .../src/components/QualityMetrics/index.tsx | 121 +++++------ .../src/components/QualityMetrics/types.tsx | 12 +- .../src/components/QualityMetrics/url.tsx | 8 +- .../components/common/CircularProgressbar.tsx | 2 +- .../src/constants/default-config.tsx | 96 --------- 13 files changed, 179 insertions(+), 477 deletions(-) diff --git a/packages/jaeger-ui/src/api/jaeger.js b/packages/jaeger-ui/src/api/jaeger.js index 5489bfbcbb..c18bae3a96 100644 --- a/packages/jaeger-ui/src/api/jaeger.js +++ b/packages/jaeger-ui/src/api/jaeger.js @@ -84,203 +84,7 @@ const JaegerAPI = { return getJSON(`/qualitymetrics-v2`, { query: { service, lookback } }); }, fetchDecoration(url) { - console.log('calling url: ', url); return getJSON(url); - // eslint-disable-next-line no-unreachable - if (url.startsWith('/analytics') || url.startsWith('/api/serviceedges')) { - return getJSON(url); - } - if (url.length % 2 && url.startsWith('neapolitan')) { - return new Promise((res, rej) => - setTimeout(() => { - if (url.length % 4 === 1) res('No val here'); - else rej(new Error(`One of the unlucky quarter: ${url.length}`)); - }, 150) - ); - } - // eslint-disable-next-line no-unreachable - if (url === 'get graph') { - return new Promise(res => - setTimeout( - () => - res({ - deets: { - here: [ - { - count: { - value: 0, - styling: { - backgroundColor: 'red', - color: 'white', - }, - }, - value: 'first', - foo: 'bar', - bar: 'baz', - }, - { - count: 1, - value: 'second', - foo: 'bar too', - }, - { - count: 2, - value: 'third', - foo: 'bar three', - }, - { - count: 3, - value: 'second', - foo: 'bar too', - }, - { - count: 4, - value: 'third', - foo: 'bar three', - }, - { - count: 5, - value: 'second', - foo: 'bar too', - }, - { - count: 6, - value: 'third', - foo: 'bar three', - }, - { - count: 7, - value: 'second', - foo: 'bar too', - }, - { - count: 8, - value: 'third', - foo: 'bar three', - }, - { - count: 9, - value: 'second', - foo: 'bar too', - }, - { - count: 10, - value: 'third', - foo: 'bar three', - }, - { - count: 11, - value: 'second', - foo: 'bar too', - }, - { - count: 12, - value: 'third', - foo: 'bar three', - }, - { - count: 13, - value: 'second', - foo: 'bar too', - }, - { - count: 14, - value: 'third', - foo: 'bar three', - }, - { - count: 15, - value: 'second', - foo: 'bar too', - }, - { - count: 16, - value: 'third', - foo: 'bar three', - }, - { - count: 17, - value: 'second', - foo: 'bar too', - }, - { - count: 18, - value: 'third', - foo: 'bar three', - }, - { - count: 19, - value: 'second', - foo: 'bar too', - }, - { - count: 20, - value: 'third', - foo: 'bar three', - }, - ], - }, - defs: { - here: [ - 'count', - { - key: 'value', - label: 'The value column', - styling: { - backgroundColor: 'blue', - color: 'lightgrey', - }, - }, - 'foo', - ], - }, - }), - 2750 - ) - ); - } - // eslint-disable-next-line no-unreachable - if (url === 'get string') { - return new Promise(res => - setTimeout( - () => - res({ - deets: { - here: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.', - }, - }), - 750 - ) - ); - } - // eslint-disable-next-line no-unreachable - if (url === 'get list') { - return new Promise(res => - setTimeout( - () => - res({ - deets: { - here: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.'.split( - ' ' - ), - }, - }), - 750 - ) - ); - } - // eslint-disable-next-line no-unreachable - if (url === 'infinite load') - return new Promise(res => setTimeout(() => res('you are patient, eh?'), 1500000)); - // eslint-disable-next-line no-unreachable - if (url === 'deets err') - return new Promise((res, rej) => setTimeout(() => rej(new Error('you knew this would happen')), 600)); - // eslint-disable-next-line no-unreachable - if (url === 'deets 404') return new Promise(res => res({})); - // eslint-disable-next-line no-unreachable - return new Promise(res => setTimeout(() => res({ val: url.length ** 2 }), 150)); - // return getJSON(url); }, fetchDeepDependencyGraph(query) { return getJSON(`${ANALYTICS_ROOT}v1/dependencies`, { query }); diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx index 03663ee4f2..2b1f58952a 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx @@ -42,20 +42,14 @@ type TProps = { type TState = { collapsed: boolean; -} +}; function isList(arr: string[] | TPadRow[]): arr is string[] { return typeof arr[0] === 'string'; } export default class DetailsCard extends React.PureComponent { - state: TState; - - constructor(props: TProps) { - super(props); - - this.state = { collapsed: Boolean(props.collapsible) }; - } + state: TState; static renderList(details: string[]) { return ( @@ -112,19 +106,27 @@ export default class DetailsCard extends React.PureComponent { ); }, - sorter: sortable && ((a: TPadRow, b: TPadRow) => { - const aData = a[dataIndex]; - const aValue = typeof aData === 'object' && typeof aData.value === 'string' ? aData.value : aData; - const bData = b[dataIndex]; - const bValue = typeof bData === 'object' && typeof bData.value === 'string' ? bData.value : bData; - if (aValue < bValue) return -1; - return bValue < aValue ? 1 : 0; - }), + sorter: + sortable && + ((a: TPadRow, b: TPadRow) => { + const aData = a[dataIndex]; + const aValue = typeof aData === 'object' && typeof aData.value === 'string' ? aData.value : aData; + const bData = b[dataIndex]; + const bValue = typeof bData === 'object' && typeof bData.value === 'string' ? bData.value : bData; + if (aValue < bValue) return -1; + return bValue < aValue ? 1 : 0; + }), }; return ; } + constructor(props: TProps) { + super(props); + + this.state = { collapsed: Boolean(props.collapsible) }; + } + renderTable(details: TPadRow[]) { const { columnDefs: _columnDefs } = this.props; const columnDefs: TPadColumnDefs = _columnDefs ? _columnDefs.slice() : []; @@ -149,16 +151,22 @@ export default class DetailsCard extends React.PureComponent { size="middle" dataSource={details} pagination={false} - rowKey={(row: TPadRow) => JSON.stringify(row, function replacer(key: string, value: TPadRow | string | number | TStyledValue) { - function isRow(v: typeof value): v is TPadRow { return v === row }; - if (isRow(value)) return value; - if (typeof value === 'object') { - if (typeof value.value === 'string') return value.value; - const { key = "Unknown" } = value.value; - return key; - } - return value; - })} + rowKey={(row: TPadRow) => + JSON.stringify(row, function replacer( + key: string, + value: TPadRow | string | number | TStyledValue + ) { + function isRow(v: typeof value): v is TPadRow { + return v === row; + } + if (isRow(value)) return value; + if (typeof value === 'object') { + if (typeof value.value === 'string') return value.value; + return value.value.key || 'Unknown'; + } + return value; + }) + } > {columnDefs.map(DetailsCard.renderColumn)} @@ -179,8 +187,10 @@ export default class DetailsCard extends React.PureComponent { } toggleCollapse = () => { - this.setState({ collapsed: !this.state.collapsed }); - } + this.setState((prevState: TState) => ({ + collapsed: !prevState.collapsed, + })); + }; render() { const { collapsed } = this.state; @@ -190,19 +200,23 @@ export default class DetailsCard extends React.PureComponent { return (
- {collapsible && } + + + )}
{header} {description &&

{description}

}
-
{this.renderDetails()}
+
+ {this.renderDetails()} +
); } diff --git a/packages/jaeger-ui/src/components/QualityMetrics/BannerText.tsx b/packages/jaeger-ui/src/components/QualityMetrics/BannerText.tsx index 28ceadc366..27fd9f0e54 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/BannerText.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/BannerText.tsx @@ -14,22 +14,17 @@ import * as React from 'react'; -import CircularProgressbar from '../common/CircularProgressbar'; -import NewWindowIcon from '../common/NewWindowIcon'; - import { TQualityMetrics } from './types'; import './BannerText.css'; export type TProps = { - bannerText: TQualityMetrics["bannerText"]; -} + bannerText: TQualityMetrics['bannerText']; +}; export default class BannerText extends React.PureComponent { render() { - const { - bannerText - } = this.props; + const { bannerText } = this.props; if (!bannerText) return null; const { styling = undefined } = typeof bannerText === 'object' ? bannerText : {}; diff --git a/packages/jaeger-ui/src/components/QualityMetrics/CountCard.tsx b/packages/jaeger-ui/src/components/QualityMetrics/CountCard.tsx index 80799960b3..64700c8218 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/CountCard.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/CountCard.tsx @@ -18,32 +18,24 @@ import ExamplesLink from './ExamplesLink'; import { TExample } from './types'; +import './CountCard.css'; + export type TProps = { count?: number; title?: string; examples?: TExample[]; -} - -import './CountCard.css'; +}; export default class ScoreCard extends React.PureComponent { render() { - const { - count, - title, - examples, - } = this.props; + const { count, title, examples } = this.props; if (count === undefined || title === undefined) return null; return (
- - {title} - - - {count} - + {title} + {count}
); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/ExamplesLink.tsx b/packages/jaeger-ui/src/components/QualityMetrics/ExamplesLink.tsx index 947003389f..14e91a8ee3 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/ExamplesLink.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/ExamplesLink.tsx @@ -22,7 +22,7 @@ import { TExample } from './types'; export type TProps = { examples?: TExample[]; includeText?: boolean; -} +}; type TExampleWithSpans = { traceID: string; @@ -33,13 +33,13 @@ function hasSpans(example: TExample | TExampleWithSpans): example is TExampleWit return Boolean(example.spanIDs && example.spanIDs.length); } -function getGetUrlArg(examples: TExample[]): { spanLinks: Record, traceID: string[] } { +function getGetUrlArg(examples: TExample[]): { spanLinks: Record; traceID: string[] } { const spanLinks: Record = {}; const traceID: string[] = []; examples.forEach((example: TExample) => { if (hasSpans(example)) spanLinks[example.traceID] = example.spanIDs.join(' '); else traceID.push(example.traceID); - }) + }); return { spanLinks, traceID, @@ -48,16 +48,14 @@ function getGetUrlArg(examples: TExample[]): { spanLinks: Record export default class ExamplesLink extends React.PureComponent { render() { - const { - examples, - includeText, - } = this.props; + const { examples, includeText } = this.props; if (!examples || !examples.length) return null; return ( - - {includeText && 'Examples '} + + {includeText && 'Examples '} + ); } diff --git a/packages/jaeger-ui/src/components/QualityMetrics/Header.tsx b/packages/jaeger-ui/src/components/QualityMetrics/Header.tsx index 2e172c1906..51ad574043 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/Header.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/Header.tsx @@ -49,12 +49,7 @@ export default class Header extends React.PureComponent { }; render() { - const { - lookback, - service, - services, - setService, - } = this.props; + const { lookback, service, services, setService } = this.props; const { ownInputValue } = this.state; const lookbackValue = ownInputValue !== undefined ? ownInputValue : lookback; @@ -68,7 +63,9 @@ export default class Header extends React.PureComponent { required options={services || []} /> - + (in hours)
diff --git a/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx index 164ae8d6d5..82dad7c4d4 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/MetricCard.tsx @@ -25,25 +25,21 @@ import { TQualityMetrics } from './types'; import './MetricCard.css'; export type TProps = { - // link: string; - metric: TQualityMetrics["metrics"][0]; -} + metric: TQualityMetrics['metrics'][0]; +}; const dividToFixedFloorPercentage = (pass: number, fail: number) => { - const str = `${pass/(pass+fail)*100}.0` + const str = `${(pass / (pass + fail)) * 100}.0`; return `${str.substring(0, str.indexOf('.') + 2)}%`; -} +}; export default class MetricCard extends React.PureComponent { render() { const { - // link, metric: { name, - category, description, metricDocumentationLink, - // metricWeight, passCount, passExamples, failureCount, @@ -51,7 +47,7 @@ export default class MetricCard extends React.PureComponent { exemptionCount, exemptionExamples, details, - } + }, } = this.props; return (
@@ -66,29 +62,33 @@ export default class MetricCard extends React.PureComponent {
- {name} - + {name}{' '} + + + +

{description}

- - - + + +
- {details && details.map(detail => Boolean(detail.rows && detail.rows.length) && ( - - ))} + {details && + details.map( + detail => + Boolean(detail.rows && detail.rows.length) && ( + + ) + )}
); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.tsx b/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.tsx index a24f8c87a8..bc1be005df 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/ScoreCard.tsx @@ -23,37 +23,32 @@ import './ScoreCard.css'; export type TProps = { link: string; - score: TQualityMetrics["scores"][0]; -} + score: TQualityMetrics['scores'][0]; +}; export default class ScoreCard extends React.PureComponent { render() { const { link, - score: { - label, - max: maxValue, - value, - } + score: { label, max: maxValue, value }, } = this.props; - const linkText = value < maxValue - ? 'How to improve ' - : 'Great! What does this mean '; + const linkText = value < maxValue ? 'How to improve ' : 'Great! What does this mean '; return (
- - {label} - + {label}
- {linkText} + + {linkText} + +
); } diff --git a/packages/jaeger-ui/src/components/QualityMetrics/index.tsx b/packages/jaeger-ui/src/components/QualityMetrics/index.tsx index 1691d744f9..bb5766d556 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/index.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/index.tsx @@ -19,7 +19,6 @@ import { bindActionCreators, Dispatch } from 'redux'; import * as jaegerApiActions from '../../actions/jaeger-api'; import JaegerAPI from '../../api/jaeger'; -import BreakableText from '../common/BreakableText'; import LoadingIndicator from '../common/LoadingIndicator'; import DetailsCard from '../DeepDependencies/SidePanel/DetailsCard'; import BannerText from './BannerText'; @@ -37,7 +36,7 @@ import './index.css'; type TOwnProps = { history: RouterHistory; location: Location; -} +}; type TDispatchProps = { fetchServices: () => void; @@ -47,7 +46,7 @@ type TReduxProps = { lookback: number; service?: string; services?: string[] | null; -} +}; export type TProps = TDispatchProps & TReduxProps & TOwnProps; @@ -86,10 +85,7 @@ export class UnconnectedQualityMetrics extends React.PureComponent { const { history, lookback } = this.props; history.push(getUrl({ lookback, service })); - } + }; render() { const { lookback, service, services } = this.props; @@ -133,53 +129,58 @@ export class UnconnectedQualityMetrics extends React.PureComponent {qualityMetrics && ( <> - -
-
- {qualityMetrics.scores.map(score => ( - - ))} -
-
- {qualityMetrics.metrics.map(metric => ( - - ))} + +
+
+ {qualityMetrics.scores.map(score => ( + + ))} +
+
+ {qualityMetrics.metrics.map(metric => ( + + ))} +
+ ({ + ...clientRow, + examples: { + value: ( + + ), + }, + }))} + header="Client Versions" + />
- ({ - ...clientRow, - examples: { - value: , - }, - }))} - header="Client Versions" - /> -
- - )} - {loading && ( - - )} - {error && ( -
- {error.message} -
+ )} + {loading && } + {error &&
{error.message}
}
); } @@ -195,14 +196,14 @@ export function mapStateToProps(state: ReduxState, ownProps: TOwnProps): TReduxP } export function mapDispatchToProps(dispatch: Dispatch): TDispatchProps { - const { fetchServices } = bindActionCreators( - jaegerApiActions, - dispatch - ); + const { fetchServices } = bindActionCreators(jaegerApiActions, dispatch); return { fetchServices, }; } -export default connect(mapStateToProps, mapDispatchToProps)(UnconnectedQualityMetrics); +export default connect( + mapStateToProps, + mapDispatchToProps +)(UnconnectedQualityMetrics); diff --git a/packages/jaeger-ui/src/components/QualityMetrics/types.tsx b/packages/jaeger-ui/src/components/QualityMetrics/types.tsx index 007955c723..37efcacea7 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/types.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/types.tsx @@ -23,10 +23,12 @@ export type TExample = { export type TQualityMetrics = { traceQualityDocumentationLink: string; - bannerText?: string | { - value: string; - styling: React.CSSProperties; - }; + bannerText?: + | string + | { + value: string; + styling: React.CSSProperties; + }; scores: { key: string; label: string; @@ -58,4 +60,4 @@ export type TQualityMetrics = { count: number; examples: TExample[]; }[]; -} +}; diff --git a/packages/jaeger-ui/src/components/QualityMetrics/url.tsx b/packages/jaeger-ui/src/components/QualityMetrics/url.tsx index 02bb4270f0..71031db32c 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/url.tsx +++ b/packages/jaeger-ui/src/components/QualityMetrics/url.tsx @@ -32,16 +32,16 @@ export function getUrl(queryParams?: Record) { return `${ROUTE_PATH}?${queryString.stringify(queryParams)}`; } -type TReturnValue = { +type TReturnValue = { lookback: number; - service?: string -} + service?: string; +}; export const getUrlState = memoizeOne(function getUrlState(search: string): TReturnValue { const { lookback: lookbackFromUrl, service: serviceFromUrl } = queryString.parse(search); const service = Array.isArray(serviceFromUrl) ? serviceFromUrl[0] : serviceFromUrl; const lookbackStr = Array.isArray(lookbackFromUrl) ? lookbackFromUrl[0] : lookbackFromUrl; - const lookback = lookbackStr && Number.parseInt(lookbackStr); + const lookback = lookbackStr && Number.parseInt(lookbackStr, 10); const rv: TReturnValue = { lookback: 1, }; diff --git a/packages/jaeger-ui/src/components/common/CircularProgressbar.tsx b/packages/jaeger-ui/src/components/common/CircularProgressbar.tsx index 886ddd3232..df5a8302a3 100644 --- a/packages/jaeger-ui/src/components/common/CircularProgressbar.tsx +++ b/packages/jaeger-ui/src/components/common/CircularProgressbar.tsx @@ -25,7 +25,7 @@ type TProps = { strokeWidth?: number; text?: string; value: number; -} +}; export default class CircularProgressbar extends React.PureComponent { render() { diff --git a/packages/jaeger-ui/src/constants/default-config.tsx b/packages/jaeger-ui/src/constants/default-config.tsx index 6ffc24ef9c..198b84f9a3 100644 --- a/packages/jaeger-ui/src/constants/default-config.tsx +++ b/packages/jaeger-ui/src/constants/default-config.tsx @@ -56,102 +56,6 @@ export default deepFreeze( ], }, ], - pathAgnosticDecorations: [ - { - acronym: 'ME', - id: 'me', - name: 'Missing Edges', - summaryUrl: '/analytics/v1/serviceedges/missing/summary?source=connmon', - summaryPath: '#{service}.count', - detailUrl: 'serviceedges/missing?source=connmon&service=#{service}', - detailPath: 'missingEdges', - detailColumnDefPath: 'columnDefs', - }, - { - acronym: 'N', - id: 'n', - name: 'Neapolitan mix of success/error/no path', - summaryUrl: 'neapolitan #{service}', - summaryPath: 'val', - }, - { - acronym: 'AR', - id: 'ar', - name: 'All should resolve', - summaryUrl: 'all should res #{service}', - summaryPath: 'val', - }, - { - acronym: 'RGT', - id: 'rgt', - name: 'all should resolve details graph too #{service}', - summaryUrl: 'details too #{service}', - detailUrl: 'get graph', - detailPath: 'deets.here', - detailColumnDefPath: 'defs.here', - summaryPath: 'val', - }, - { - acronym: 'RST', - id: 'rst', - name: 'all should resolve details string too #{service}', - summaryUrl: 'details too #{service}', - detailUrl: 'get string', - detailPath: 'deets.here', - summaryPath: 'val', - }, - { - acronym: 'RLT', - id: 'rlt', - name: 'all should resolve details list too #{service}', - summaryUrl: 'details too #{service}', - detailUrl: 'get list', - detailPath: 'deets.here', - summaryPath: 'val', - }, - { - acronym: 'RIL', - id: 'ril', - name: 'all should resolve, but infinite load details', - summaryUrl: 'details too #{service}', - detailUrl: 'infinite load', - detailPath: 'deets.here', - summaryPath: 'val', - }, - { - acronym: 'RDE', - id: 'rde', - name: 'all should resolve, but details err', - summaryUrl: 'details too #{service}', - detailUrl: 'deets err', - detailPath: 'deets.here', - summaryPath: 'val', - }, - { - acronym: 'RD4', - id: 'rd4', - name: 'all should resolve, but details not found', - summaryUrl: 'details too #{service}', - detailUrl: 'deets 404', - detailPath: 'deets.here', - summaryPath: 'val', - }, - { - acronym: 'OPs', - id: 'ops', - name: 'got dem ops', - summaryUrl: 'neapolitan #{service}', - summaryPath: 'val', - opSummaryUrl: 'just #{service}', - opSummaryPath: 'val', - detailUrl: 'deets 404', - detailPath: 'deets.here', - opDetailUrl: 'get list', - opDetailPath: 'deets.here', - } /*, { - TODO: op example too - }*/, - ], search: { maxLookback: { label: '2 Days', From af66ff9d7498fe00274f65544f656b58ff83a62b Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Fri, 24 Apr 2020 12:37:33 -0400 Subject: [PATCH 30/31] Add support for decoration links Signed-off-by: Everett Ross --- .../DeepDependencies/SidePanel/DetailsPanel.css | 4 ++++ .../DeepDependencies/SidePanel/DetailsPanel.tsx | 15 +++++++++++++++ .../src/model/path-agnostic-decorations/types.tsx | 1 + 3 files changed, 20 insertions(+) diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css index 908660c0a1..348c23a21e 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.css @@ -42,6 +42,10 @@ limitations under the License. box-shadow: 0px 3px 3px -3px rgba(0, 0, 0, 0.1); } +.Ddg--DetailsPanel--DetailLink { + padding-left: 0.2em; +} + .Ddg--DetailsPanel--errorMsg { color: #e58c33; } diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx index 6c237ac462..18451fa3b3 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.tsx @@ -13,11 +13,13 @@ // limitations under the License. import * as React from 'react'; +import { Tooltip } from 'antd'; import _get from 'lodash/get'; import { connect } from 'react-redux'; import BreakableText from '../../common/BreakableText'; import LoadingIndicator from '../../common/LoadingIndicator'; +import NewWindowIcon from '../../common/NewWindowIcon'; import JaegerAPI from '../../../api/jaeger'; import ColumnResizer from '../../TracePage/TraceTimelineViewer/TimelineHeaderRow/TimelineColumnResizer'; import extractDecorationFromState, { TDecorationFromState } from '../../../model/path-agnostic-decorations'; @@ -128,6 +130,7 @@ export class UnconnectedDetailsPanel extends React.PureComponent render() { const { decorationProgressbar, decorationSchema, decorationValue, operation: _op, service } = this.props; + const { detailLink } = decorationSchema; const { width = 0.3 } = this.state; const operation = _op && !Array.isArray(_op) ? _op : undefined; return ( @@ -138,6 +141,18 @@ export class UnconnectedDetailsPanel extends React.PureComponent
{stringSupplant(decorationSchema.name, { service, operation })} + {detailLink && ( + + + + + + )}
{decorationProgressbar ? (
{decorationProgressbar}
diff --git a/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx index 4853f5f47b..cefb7d67a0 100644 --- a/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx +++ b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx @@ -22,6 +22,7 @@ export type TPathAgnosticDecorationSchema = { opSummaryUrl?: string; summaryPath: string; opSummaryPath?: string; + detailLink?: string; detailUrl?: string; detailPath?: string; detailColumnDefPath?: string; From dc285401bbafaa79e3da637b171734fc16af986b Mon Sep 17 00:00:00 2001 From: Everett Ross Date: Fri, 24 Apr 2020 14:49:59 -0400 Subject: [PATCH 31/31] Clean up and add quality-metrics top nav link Signed-off-by: Everett Ross --- .../jaeger-ui/src/components/App/TopNav.tsx | 9 +++ .../SidePanel/DetailsCard/index.tsx | 1 - .../SidePanel/DetailsPanel.test.js | 9 +++ .../__snapshots__/DetailsPanel.test.js.snap | 70 +++++++++++++++++++ .../DeepDependencies/SidePanel/index.test.js | 1 - .../components/QualityMetrics/BannerText.tsx | 4 +- .../components/QualityMetrics/CountCard.css | 1 - .../components/common/CircularProgressbar.tsx | 11 +-- .../model/path-agnostic-decorations/index.tsx | 2 +- .../model/path-agnostic-decorations/types.tsx | 2 +- .../path-agnostic-decorations.test.js | 5 -- packages/jaeger-ui/src/setupProxy.js | 1 - packages/jaeger-ui/src/types/config.tsx | 3 + 13 files changed, 96 insertions(+), 23 deletions(-) diff --git a/packages/jaeger-ui/src/components/App/TopNav.tsx b/packages/jaeger-ui/src/components/App/TopNav.tsx index 411a933e07..4cf3b78b6c 100644 --- a/packages/jaeger-ui/src/components/App/TopNav.tsx +++ b/packages/jaeger-ui/src/components/App/TopNav.tsx @@ -21,6 +21,7 @@ import { RouteComponentProps, Link, withRouter } from 'react-router-dom'; import TraceIDSearchInput from './TraceIDSearchInput'; import * as dependencyGraph from '../DependencyGraph/url'; import * as deepDependencies from '../DeepDependencies/url'; +import * as qualityMetrics from '../QualityMetrics/url'; import * as searchUrl from '../SearchTracePage/url'; import * as diffUrl from '../TraceDiff/url'; import { ReduxState } from '../../types'; @@ -59,6 +60,14 @@ if (getConfigValue('deepDependencies.menuEnabled')) { }); } +if (getConfigValue('qualityMetrics.menuEnabled')) { + NAV_LINKS.push({ + to: qualityMetrics.getUrl(), + matches: qualityMetrics.matches, + text: 'Quality Metrics', + }); +} + function getItemLink(item: ConfigMenuItem) { const { label, anchorTarget, url } = item; return ( diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx index 2b1f58952a..b41fef0929 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsCard/index.tsx @@ -196,7 +196,6 @@ export default class DetailsCard extends React.PureComponent { const { collapsed } = this.state; const { className, collapsible, description, header } = this.props; - // TODO: Collapsible return (
diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js index 1370eca530..a349d4fb59 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/DetailsPanel.test.js @@ -215,6 +215,15 @@ describe('', () => { wrapper.setState({ detailsLoading: false, details: 'details error', detailsErred: true }); expect(wrapper).toMatchSnapshot(); }); + + it('renders detailLink', () => { + const schemaWithLink = { + ...props.decorationSchema, + detailLink: 'test details link', + }; + const wrapper = shallow(); + expect(wrapper).toMatchSnapshot(); + }); }); describe('componentDidMount', () => { diff --git a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/DetailsPanel.test.js.snap b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/DetailsPanel.test.js.snap index 38bdad6ae9..64143fcf26 100644 --- a/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/DetailsPanel.test.js.snap +++ b/packages/jaeger-ui/src/components/DeepDependencies/SidePanel/__snapshots__/DetailsPanel.test.js.snap @@ -40,6 +40,76 @@ exports[` render renders 1`] = `
`; +exports[` render renders detailLink 1`] = ` +
+
+ +
+
+ + Decorating test svc + + + + + + +
+ + test decorationValue + +
+ +
+ +
+`; + exports[` render renders details 1`] = `
{ const { bannerText } = this.props; if (!bannerText) return null; - const { styling = undefined } = typeof bannerText === 'object' ? bannerText : {}; - const text = typeof bannerText === 'object' ? bannerText.value : bannerText; + const { styling = undefined, value: text } = + typeof bannerText === 'object' ? bannerText : { value: bannerText }; return (
diff --git a/packages/jaeger-ui/src/components/QualityMetrics/CountCard.css b/packages/jaeger-ui/src/components/QualityMetrics/CountCard.css index 40ec01f890..78703c24dc 100644 --- a/packages/jaeger-ui/src/components/QualityMetrics/CountCard.css +++ b/packages/jaeger-ui/src/components/QualityMetrics/CountCard.css @@ -17,7 +17,6 @@ limitations under the License. .CountCard { display: flex; flex-direction: column; - /* padding: 0 5px; */ margin: 5px 5%; padding: 5px; text-align: center; diff --git a/packages/jaeger-ui/src/components/common/CircularProgressbar.tsx b/packages/jaeger-ui/src/components/common/CircularProgressbar.tsx index df5a8302a3..540762780d 100644 --- a/packages/jaeger-ui/src/components/common/CircularProgressbar.tsx +++ b/packages/jaeger-ui/src/components/common/CircularProgressbar.tsx @@ -18,7 +18,6 @@ import { CircularProgressbar as CircularProgressbarImpl } from 'react-circular-p import 'react-circular-progressbar/dist/styles.css'; type TProps = { - // key: string; backgroundHue?: number; decorationHue?: number; maxValue: number; @@ -29,15 +28,7 @@ type TProps = { export default class CircularProgressbar extends React.PureComponent { render() { - const { - // key, - backgroundHue, - decorationHue = 0, - maxValue, - strokeWidth, - text, - value, - } = this.props; + const { backgroundHue, decorationHue = 0, maxValue, strokeWidth, text, value } = this.props; const scale = (value / maxValue) ** (1 / 4); const saturation = 20 + Math.ceil(scale * 80); const light = 50 + Math.ceil((1 - scale) * 30); diff --git a/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx b/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx index 4df28b52a1..b9e971cc49 100644 --- a/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx +++ b/packages/jaeger-ui/src/model/path-agnostic-decorations/index.tsx @@ -15,8 +15,8 @@ import * as React from 'react'; import _get from 'lodash/get'; import queryString from 'query-string'; -import CircularProgressbar from '../../components/common/CircularProgressbar'; +import CircularProgressbar from '../../components/common/CircularProgressbar'; import { PROGRESS_BAR_STROKE_WIDTH, RADIUS, diff --git a/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx index cefb7d67a0..f691ea6b81 100644 --- a/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx +++ b/packages/jaeger-ui/src/model/path-agnostic-decorations/types.tsx @@ -50,7 +50,7 @@ export type TPadRow = Record; export type TPadDetails = string | string[] | TPadRow[]; -export type TPadEntry = number | string; // string or other type is for data unavailable +export type TPadEntry = number | string; export type TNewData = Record< string, diff --git a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js index bbd508b80a..dc0a6e075c 100644 --- a/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js +++ b/packages/jaeger-ui/src/reducers/path-agnostic-decorations.test.js @@ -36,11 +36,6 @@ describe('pathAgnosticDecoration reducers', () => { const service = svc[_service]; const isWithOp = Boolean(operation); - /* - const valueObj = { - value, - }; - */ const payloadKey = isWithOp ? 'withOp' : 'withoutOp'; const payload = { diff --git a/packages/jaeger-ui/src/setupProxy.js b/packages/jaeger-ui/src/setupProxy.js index df4e026110..8a3ce75c4f 100644 --- a/packages/jaeger-ui/src/setupProxy.js +++ b/packages/jaeger-ui/src/setupProxy.js @@ -36,7 +36,6 @@ module.exports = function setupProxy(app) { xfwd: true, }) ); - // TODO: remove app.use( proxy('/serviceedges', { target: 'http://localhost:16686', diff --git a/packages/jaeger-ui/src/types/config.tsx b/packages/jaeger-ui/src/types/config.tsx index abd6b5e57c..a34a461f46 100644 --- a/packages/jaeger-ui/src/types/config.tsx +++ b/packages/jaeger-ui/src/types/config.tsx @@ -46,6 +46,9 @@ export type Config = { dependencies?: { dagMaxServicesLen?: number; menuEnabled?: boolean }; menu: (ConfigMenuGroup | ConfigMenuItem)[]; pathAgnosticDecorations?: TPathAgnosticDecorationSchema[]; + qualityMetrics?: { + menuEnabled?: boolean; + }; search?: { maxLookback: { label: string; value: string }; maxLimit: number }; scripts?: TScript[]; topTagPrefixes?: string[];