diff --git a/package.json b/package.json index f6a4eb1c33..debaf2312e 100644 --- a/package.json +++ b/package.json @@ -72,6 +72,7 @@ "js-yaml": "3.12.0", "json-beautify": "1.0.1", "lodash": "4.17.11", + "logfmt": "^1.2.1", "patternfly": "3.59.1", "patternfly-react": "2.25.3", "patternfly-react-extensions": "2.16.8", diff --git a/src/actions/HelpDropdownThunkActions.ts b/src/actions/HelpDropdownThunkActions.ts index 608be9e934..1c73f71393 100644 --- a/src/actions/HelpDropdownThunkActions.ts +++ b/src/actions/HelpDropdownThunkActions.ts @@ -2,6 +2,7 @@ import { ThunkDispatch } from 'redux-thunk'; import { KialiAppState } from '../store/Store'; import { MessageType } from '../types/MessageCenter'; import { HelpDropdownActions } from './HelpDropdownActions'; +import { JaegerActions } from './JaegerActions'; import { KialiAppAction } from './KialiAppAction'; import { MessageCenterActions } from './MessageCenterActions'; import * as API from '../services/Api'; @@ -18,6 +19,13 @@ const HelpDropdownThunkActions = { status['data']['warningMessages'] ) ); + + // Get the jaeger URL + const hasJaeger = status['data']['externalServices'].filter(item => item['name'] === 'Jaeger'); + if (hasJaeger.length === 1) { + dispatch(JaegerActions.setUrl(hasJaeger[0]['url'])); + } + status['data']['warningMessages'].forEach(wMsg => { dispatch(MessageCenterActions.addMessage(wMsg, 'systemErrors', MessageType.WARNING)); }); diff --git a/src/actions/JaegerActions.ts b/src/actions/JaegerActions.ts new file mode 100644 index 0000000000..3f3c5ce420 --- /dev/null +++ b/src/actions/JaegerActions.ts @@ -0,0 +1,62 @@ +import { ActionType, createAction, createStandardAction } from 'typesafe-actions'; + +enum JaegerActionKeys { + SET_URL = 'SET_URL', + SERVICE_REQUEST_STARTED = 'SERVICE_REQUEST_STARTED', + SERVICE_SUCCESS = 'SERVICE_SUCCESS', + SERVICE_FAILED = 'SERVICE_FAILED', + SET_SERVICE = 'SET_SERVICE', + SET_NAMESPACE = 'SET_NAMESPACE', + SET_LOOKBACK = 'SET_LOOKBACK', + SET_LOOKBACK_CUSTOM = 'SET_LOOKBACK_CUSTOM', + SET_SEARCH_REQUEST = 'SET_SEARCH_REQUEST', + SET_TAGS = 'SET_TAGS', + SET_LIMIT = 'SET_LIMIT', + SET_DURATIONS = 'SET_DURATIONS', + + // RESULTS VISUALZIATION OPTIONS + SET_SEARCH_GRAPH_TO_HIDE = 'SET_SEARCH_GRAPH_TO_HIDE', + + // TRACE VISUALIZATION OPTIONS + SET_TRACE_MINIMAP_TO_SHOW = 'SET_TRACE_MINIMAP_TO_SHOW', + SET_TRACE_DETAILS_TO_SHOW = 'SET_TRACE_DETAILS_TO_SHOW' +} + +// synchronous action creators +export const JaegerActions = { + setUrl: createAction(JaegerActionKeys.SET_URL, resolve => (url: string) => + resolve({ + url: url + }) + ), + requestStarted: createAction(JaegerActionKeys.SERVICE_REQUEST_STARTED), + requestFailed: createAction(JaegerActionKeys.SERVICE_FAILED), + receiveList: createAction(JaegerActionKeys.SERVICE_SUCCESS, resolve => (newList: string[]) => + resolve({ + list: newList + }) + ), + setService: createStandardAction(JaegerActionKeys.SET_SERVICE)(), + setNamespace: createStandardAction(JaegerActionKeys.SET_NAMESPACE)(), + setLookback: createStandardAction(JaegerActionKeys.SET_LOOKBACK)(), + setTags: createStandardAction(JaegerActionKeys.SET_TAGS)(), + setLimit: createStandardAction(JaegerActionKeys.SET_LIMIT)(), + setSearchRequest: createStandardAction(JaegerActionKeys.SET_SEARCH_REQUEST)(), + setSearchGraphToHide: createStandardAction(JaegerActionKeys.SET_SEARCH_GRAPH_TO_HIDE)(), + setMinimapToShow: createStandardAction(JaegerActionKeys.SET_TRACE_MINIMAP_TO_SHOW)(), + setDetailsToShow: createStandardAction(JaegerActionKeys.SET_TRACE_DETAILS_TO_SHOW)(), + setCustomLookback: createAction(JaegerActionKeys.SET_LOOKBACK_CUSTOM, resolve => (start: string, end: string) => + resolve({ + start: start, + end: end + }) + ), + setDurations: createAction(JaegerActionKeys.SET_DURATIONS, resolve => (min: string, max: string) => + resolve({ + min: min, + max: max + }) + ) +}; + +export type JaegerAction = ActionType; diff --git a/src/actions/JaegerThunkActions.ts b/src/actions/JaegerThunkActions.ts new file mode 100644 index 0000000000..83a3e999dc --- /dev/null +++ b/src/actions/JaegerThunkActions.ts @@ -0,0 +1,125 @@ +import { JaegerActions } from './JaegerActions'; + +import * as Api from '../services/Api'; + +import { ServiceOverview } from '../types/ServiceList'; +import { KialiAppState } from '../store/Store'; +import { ThunkDispatch } from 'redux-thunk'; +import { KialiAppAction } from './KialiAppAction'; +import { JAEGER_QUERY } from '../config'; +import logfmtParser from 'logfmt/lib/logfmt_parser'; +import moment from 'moment'; + +export const convTagsLogfmt = (tags: string) => { + if (!tags) { + return null; + } + const data = logfmtParser.parse(tags); + Object.keys(data).forEach(key => { + const value = data[key]; + // make sure all values are strings + // https://github.com/jaegertracing/jaeger/issues/550#issuecomment-352850811 + if (typeof value !== 'string') { + data[key] = String(value); + } + }); + return JSON.stringify(data); +}; + +export class JaegerURLSearch { + url: string; + + constructor(url: string) { + this.url = `${url}${JAEGER_QUERY().PATH}?${JAEGER_QUERY().EMBED.UI_EMBED}=${JAEGER_QUERY().EMBED.VERSION}`; + } + + addQueryParam(param: string, value: string | number) { + this.url += `&${param}=${value}`; + } + + addParam(param: string) { + this.url += `&${param}`; + } +} + +export const getUnixTimeStampInMSFromForm = ( + startDate: string, + startDateTime: string, + endDate: string, + endDateTime: string +) => { + const start = `${startDate} ${startDateTime}`; + const end = `${endDate} ${endDateTime}`; + return { + start: `${moment(start, 'YYYY-MM-DD HH:mm').valueOf()}000`, + end: `${moment(end, 'YYYY-MM-DD HH:mm').valueOf()}000` + }; +}; + +export const JaegerThunkActions = { + asyncFetchServices: (ns: string) => { + return (dispatch: ThunkDispatch, getState: () => KialiAppState) => { + if (getState()['authentication']['token'] === undefined) { + return Promise.resolve(); + } + /** Get the token storage in redux-store */ + const token = getState().authentication.token!.token; + /** generate Token */ + const auth = `Bearer ${token}`; + + // Dispatch a thunk from thunk! + dispatch(JaegerActions.requestStarted()); + return Api.getServices(auth, ns) + .then(response => response['data']) + .then(data => { + const serviceList: string[] = []; + data['services'].forEach((aService: ServiceOverview) => { + serviceList.push(aService.name); + }); + dispatch(JaegerActions.receiveList(serviceList)); + }) + .catch(() => dispatch(JaegerActions.requestFailed())); + }; + }, + getSearchURL: () => { + return (dispatch: ThunkDispatch, getState: () => KialiAppState) => { + const searchOptions = getState().jaegerState.search; + const jaegerOptions = JAEGER_QUERY().OPTIONS; + const urlRequest = new JaegerURLSearch(getState().jaegerState.jaegerURL); + + // Search options + urlRequest.addQueryParam(jaegerOptions.START_TIME, searchOptions.start); + urlRequest.addQueryParam(jaegerOptions.END_TIME, searchOptions.end); + urlRequest.addQueryParam(jaegerOptions.LIMIT_TRACES, searchOptions.limit); + urlRequest.addQueryParam(jaegerOptions.LOOKBACK, searchOptions.lookback); + urlRequest.addQueryParam(jaegerOptions.MAX_DURATION, searchOptions.maxDuration); + urlRequest.addQueryParam(jaegerOptions.MIN_DURATION, searchOptions.minDuration); + urlRequest.addQueryParam( + jaegerOptions.SERVICE_SELECTOR, + searchOptions.serviceSelected + '.' + searchOptions.namespaceSelected + ); + const logfmtTags = convTagsLogfmt(searchOptions.tags); + if (logfmtTags) { + urlRequest.addQueryParam(jaegerOptions.TAGS, logfmtTags); + } + + // Embed Options + const traceOptions = getState().jaegerState.trace; + + // Rename query params for 1.9 Jaeger + urlRequest.addQueryParam(JAEGER_QUERY().EMBED.UI_TRACE_HIDE_MINIMAP, traceOptions.hideMinimap ? '1' : '0'); + urlRequest.addQueryParam(JAEGER_QUERY().EMBED.UI_SEARCH_HIDE_GRAPH, searchOptions.hideGraph ? '1' : '0'); + urlRequest.addQueryParam(JAEGER_QUERY().EMBED.UI_TRACE_HIDE_SUMMARY, traceOptions.hideSummary ? '1' : '0'); + + return dispatch(JaegerActions.setSearchRequest(urlRequest.url)); + }; + }, + setCustomLookback: (startDate: string, startTime: string, endDate: string, endTime: string) => { + return (dispatch: ThunkDispatch, getState: () => KialiAppState) => { + if (getState().jaegerState.search.lookback === 'custom') { + const toTimestamp = getUnixTimeStampInMSFromForm(startDate, startTime, endDate, endTime); + dispatch(JaegerActions.setCustomLookback(toTimestamp.start, toTimestamp.end)); + } + }; + } +}; diff --git a/src/actions/KialiAppAction.ts b/src/actions/KialiAppAction.ts index 8e43fbd0ad..91058cde83 100644 --- a/src/actions/KialiAppAction.ts +++ b/src/actions/KialiAppAction.ts @@ -9,6 +9,7 @@ import { MessageCenterAction } from './MessageCenterActions'; import { NamespaceAction } from './NamespaceAction'; import { UserSettingsAction } from './UserSettingsActions'; import { ServerConfigAction } from './ServerConfigActions'; +import { JaegerAction } from './JaegerActions'; export type KialiAppAction = | GlobalAction @@ -21,4 +22,5 @@ export type KialiAppAction = | MessageCenterAction | NamespaceAction | ServerConfigAction - | UserSettingsAction; + | UserSettingsAction + | JaegerAction; diff --git a/src/actions/__tests__/JaegerActions.test.ts b/src/actions/__tests__/JaegerActions.test.ts new file mode 100644 index 0000000000..a41880ae9c --- /dev/null +++ b/src/actions/__tests__/JaegerActions.test.ts @@ -0,0 +1,43 @@ +import { JaegerActions } from '../JaegerActions'; +import { getType } from 'typesafe-actions'; + +describe('JaegerActions', () => { + it('should "update url" action', () => { + const showAction = JaegerActions.setUrl('jaeger-query-istio-system.127.0.0.1.nip.io'); + expect(showAction.type).toEqual(getType(JaegerActions.setUrl)); + expect(showAction.payload).toEqual({ + url: 'jaeger-query-istio-system.127.0.0.1.nip.io' + }); + }); + + it('should "receive list of services" action', () => { + const services = ['details', 'productpage']; + const showAction = JaegerActions.receiveList(services); + expect(showAction.type).toEqual(getType(JaegerActions.receiveList)); + expect(showAction.payload).toEqual({ + list: services + }); + }); + + it('should "update custom lookback" action', () => { + const start = '1544432675600000'; + const end = '1544432625600000'; + const showAction = JaegerActions.setCustomLookback(start, end); + expect(showAction.type).toEqual(getType(JaegerActions.setCustomLookback)); + expect(showAction.payload).toEqual({ + start: start, + end: end + }); + }); + + it('should "update durations" action', () => { + const min = '10ms'; + const max = '10s'; + const showAction = JaegerActions.setDurations(min, max); + expect(showAction.type).toEqual(getType(JaegerActions.setDurations)); + expect(showAction.payload).toEqual({ + min: min, + max: max + }); + }); +}); diff --git a/src/actions/__tests__/JaegerThunkActions.test.ts b/src/actions/__tests__/JaegerThunkActions.test.ts new file mode 100644 index 0000000000..8b8dda488e --- /dev/null +++ b/src/actions/__tests__/JaegerThunkActions.test.ts @@ -0,0 +1,70 @@ +import { convTagsLogfmt, getUnixTimeStampInMSFromForm, JaegerURLSearch } from '../JaegerThunkActions'; +import { JAEGER_QUERY } from '../../config'; +import moment from 'moment-timezone'; + +describe('JaegerThunkActions', () => { + describe('Methods & Class', () => { + describe('Method convTagsLogfmt', () => { + it('convTagsLogfmt should return null if tags is empty', () => { + expect(convTagsLogfmt('')).toEqual(null); + }); + + it('convTagsLogfmt should JSON stringify of the tags', () => { + expect(convTagsLogfmt('error=true')).toEqual('{"error":"true"}'); + + expect(convTagsLogfmt('http.status_code=200 error=true')).toEqual('{"http.status_code":"200","error":"true"}'); + }); + }); + + describe('Method getUnixTimeStampInMSFromForm', () => { + it('getUnixTimeStampInMSFromForm should return the correct start and end time in MS', () => { + moment.tz.setDefault('UTC'); + const date = '2019-01-01'; + const startTime = '10:30'; + const endTime = '11:30'; + const result = getUnixTimeStampInMSFromForm(date, startTime, date, endTime); + expect(result.start).toEqual('1546338600000000'); + expect(result.end).toEqual('1546342200000000'); + }); + }); + + describe('Class JaegerURLSearch', () => { + const url = 'https://jaeger-query-istio-system.127.0.0.1.nip.io'; + let jaegerURLclass; + + beforeEach(() => { + jaegerURLclass = new JaegerURLSearch(url); + }); + + it('JaegerURLSearch constructor', () => { + expect(jaegerURLclass.url).toEqual( + `${url}${JAEGER_QUERY().PATH}?${JAEGER_QUERY().EMBED.UI_EMBED}=${JAEGER_QUERY().EMBED.VERSION}` + ); + }); + + it('JaegerURLSearch addQueryParam method with number value', () => { + jaegerURLclass.addQueryParam('uiTimelineHideMinimap', 1); + expect(jaegerURLclass.url).toEqual( + `${url}${JAEGER_QUERY().PATH}?${JAEGER_QUERY().EMBED.UI_EMBED}=${ + JAEGER_QUERY().EMBED.VERSION + }&uiTimelineHideMinimap=1` + ); + jaegerURLclass.addQueryParam('uiTimelineHideSummary', '0'); + expect(jaegerURLclass.url).toEqual( + `${url}${JAEGER_QUERY().PATH}?${JAEGER_QUERY().EMBED.UI_EMBED}=${ + JAEGER_QUERY().EMBED.VERSION + }&uiTimelineHideMinimap=1&uiTimelineHideSummary=0` + ); + }); + + it('JaegerURLSearch addParam method', () => { + jaegerURLclass.addParam('uiTimelineHideMinimap'); + expect(jaegerURLclass.url).toEqual( + `${url}${JAEGER_QUERY().PATH}?${JAEGER_QUERY().EMBED.UI_EMBED}=${ + JAEGER_QUERY().EMBED.VERSION + }&uiTimelineHideMinimap` + ); + }); + }); + }); +}); diff --git a/src/components/BreadcrumbView/BreadcrumbView.tsx b/src/components/BreadcrumbView/BreadcrumbView.tsx index 058965d242..9e0fae7dc9 100644 --- a/src/components/BreadcrumbView/BreadcrumbView.tsx +++ b/src/components/BreadcrumbView/BreadcrumbView.tsx @@ -52,6 +52,9 @@ export class BreadcrumbView extends React.Component void; + setGraph: (state: boolean) => void; + setDetails: (state: boolean) => void; + setMinimap: (state: boolean) => void; +} +interface DateTime { + date: string; + time: string; +} + +interface JaegerToolbarState { + tags: string; + limit: number; + dateTimes: { [key: string]: DateTime }; + minDuration: string; + maxDuration: string; +} + +export class JaegerToolbar extends React.Component { + constructor(props: JaegerToolbarProps) { + super(props); + this.state = { + tags: this.props.tagsValue || '', + limit: 20, + minDuration: '', + maxDuration: '', + dateTimes: { start: { date: '', time: '' }, end: { date: '', time: '' } } + }; + } + + onChangeLookBackCustom = (step: string, dateField: string, timeField: string) => { + const current = this.state.dateTimes; + dateField ? (current[step].date = dateField) : (current[step].time = timeField); + this.setState({ dateTimes: current }); + }; + + render() { + const { + disabled, + requestSearchURL, + showGraph, + showSummary, + showMinimap, + setGraph, + setDetails, + setMinimap, + disableSelector, + limit + } = this.props; + + return ( + + + {!disableSelector && ( + + + + )} + + + requestSearchURL(this.state)} + /> + + + this.setState({ tags: e.currentTarget.value })} /> + + + Min Duration + + this.setState({ minDuration: e.currentTarget.value })} + /> + + + + Max Duration + + this.setState({ maxDuration: e.currentTarget.value })} + /> + + + + Limit Results + + this.setState({ limit: e.currentTarget.value })} + /> + + + + ); + } +} + +const mapStateToProps = (state: KialiAppState) => { + return { + limit: state.jaegerState.search.limit, + showGraph: !state.jaegerState.search.hideGraph, + showSummary: !state.jaegerState.trace.hideSummary, + showMinimap: !state.jaegerState.trace.hideMinimap, + disabled: state.jaegerState.toolbar.isFetchingService || !state.jaegerState.search.serviceSelected + }; +}; + +const mapDispatchToProps = (dispatch: ThunkDispatch) => { + return { + requestSearchURL: (state: JaegerToolbarState) => { + dispatch( + JaegerThunkActions.setCustomLookback( + state.dateTimes['start'].date, + state.dateTimes['start'].time, + state.dateTimes['end'].date, + state.dateTimes['end'].time + ) + ); + dispatch(JaegerActions.setTags(state.tags)); + dispatch(JaegerActions.setLimit(state.limit)); + dispatch(JaegerActions.setDurations(state.minDuration, state.maxDuration)); + dispatch(JaegerThunkActions.getSearchURL()); + }, + setGraph: (state: boolean) => { + dispatch(JaegerActions.setSearchGraphToHide(state)); + }, + setMinimap: (state: boolean) => { + dispatch(JaegerActions.setMinimapToShow(state)); + }, + setDetails: (state: boolean) => { + dispatch(JaegerActions.setDetailsToShow(state)); + } + }; +}; + +export const JaegerToolbarContainer = connect( + mapStateToProps, + mapDispatchToProps +)(JaegerToolbar); diff --git a/src/components/JaegerToolbar/LookBack.tsx b/src/components/JaegerToolbar/LookBack.tsx new file mode 100644 index 0000000000..f9305ac643 --- /dev/null +++ b/src/components/JaegerToolbar/LookBack.tsx @@ -0,0 +1,130 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Col, Form, FormGroup, FormControl, FieldLevelHelp } from 'patternfly-react'; +import { KialiAppState } from '../../store/Store'; +import ToolbarDropdown from '../../components/ToolbarDropdown/ToolbarDropdown'; +import { JaegerActions } from '../../actions/JaegerActions'; +import { ThunkDispatch } from 'redux-thunk'; +import { KialiAppAction } from '../../actions/KialiAppAction'; + +interface LookBackProps { + fetching: boolean; + setLookback: (lookback: string) => void; + lookback: string; + onChangeCustom: (when: string, dateField: string, timeField: string) => void; +} + +export class LookBack extends React.PureComponent { + lookBackOptions = { + '1h': 'Last Hour', + '2h': 'Last 2 Hours', + '3h': 'Last 3 Hours', + '6h': 'Last 6 Hours', + '12h': 'Last 12 Hours', + '24h': 'Last 24 Hours', + '2d': 'Last 2 Days', + custom: 'Custom Time Range' + }; + + constructor(props: LookBackProps) { + super(props); + } + + componentDidMount() { + this.props.setLookback(this.props.lookback); + } + + render() { + const { lookback, fetching, setLookback, onChangeCustom } = this.props; + const tz = lookback === 'custom' ? new Date().toTimeString().replace(/^.*?GMT/, 'UTC') : null; + + return ( + + + Lookback + + + {tz && ( +
+ + + Start Time + Times are expressed in {tz}} + placement={'bottom'} + /> + + + onChangeCustom('start', e.target.value, '')} + /> + onChangeCustom('start', '', e.target.value)} + /> + + + + End Time + Times are expressed in {tz}} + placement={'bottom'} + /> + + + onChangeCustom('end', e.target.value, '')} + /> + onChangeCustom('end', '', e.target.value)} + /> + +
+ )} +
+ ); + } +} + +const mapStateToProps = (state: KialiAppState) => { + return { + fetching: state.jaegerState.search.serviceSelected === '', + lookback: state.jaegerState.search.lookback + }; +}; + +const mapDispatchToProps = (dispatch: ThunkDispatch) => { + return { + setLookback: (lookback: string) => { + dispatch(JaegerActions.setLookback(lookback)); + } + }; +}; + +const LookBackContainer = connect( + mapStateToProps, + mapDispatchToProps +)(LookBack); + +export default LookBackContainer; diff --git a/src/components/JaegerToolbar/NamespaceDropdown.tsx b/src/components/JaegerToolbar/NamespaceDropdown.tsx new file mode 100644 index 0000000000..c0a5158dfc --- /dev/null +++ b/src/components/JaegerToolbar/NamespaceDropdown.tsx @@ -0,0 +1,77 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { KialiAppState } from '../../store/Store'; +import Namespace from '../../types/Namespace'; +import ToolbarDropdown from '../../components/ToolbarDropdown/ToolbarDropdown'; +import { JaegerActions } from '../../actions/JaegerActions'; +import { ThunkDispatch } from 'redux-thunk'; +import { KialiAppAction } from '../../actions/KialiAppAction'; +import NamespaceThunkActions from '../../actions/NamespaceThunkActions'; +import { JaegerThunkActions } from '../../actions/JaegerThunkActions'; + +interface NamespaceDropdownProps { + disabled: boolean; + namespace: string; + items: Namespace[]; + refresh: () => void; + setNamespace: (service: string) => void; +} + +export class NamespaceDropdown extends React.PureComponent { + constructor(props: NamespaceDropdownProps) { + super(props); + } + + componentDidMount() { + this.props.refresh(); + } + + render() { + const { disabled, namespace, setNamespace } = this.props; + + const items: { [key: string]: string } = this.props.items.reduce((list, item) => { + list[item.name] = item.name; + return list; + }, {}); + + return ( + + ); + } +} + +const mapStateToProps = (state: KialiAppState) => { + return { + items: state.namespaces.items, + disabled: state.namespaces.isFetching, + namespace: state.jaegerState.search.namespaceSelected + }; +}; + +const mapDispatchToProps = (dispatch: ThunkDispatch) => { + return { + refresh: () => { + dispatch(NamespaceThunkActions.fetchNamespacesIfNeeded()); + }, + setNamespace: (namespace: string) => { + dispatch(JaegerActions.setNamespace(namespace)); + dispatch(JaegerActions.setService('')); + dispatch(JaegerThunkActions.asyncFetchServices(namespace)); + } + }; +}; + +const NamespaceDropdownContainer = connect( + mapStateToProps, + mapDispatchToProps +)(NamespaceDropdown); + +export default NamespaceDropdownContainer; diff --git a/src/components/JaegerToolbar/RightToolbar.tsx b/src/components/JaegerToolbar/RightToolbar.tsx new file mode 100644 index 0000000000..099ebd29c6 --- /dev/null +++ b/src/components/JaegerToolbar/RightToolbar.tsx @@ -0,0 +1,57 @@ +import * as React from 'react'; +import { ToolbarRightContent, Button, Icon } from 'patternfly-react'; + +interface RightToolbarProps { + disabled: boolean; + graph: boolean; + minimap: boolean; + summary: boolean; + onGraphClick: (state: boolean) => void; + onSummaryClick: (state: boolean) => void; + onMinimapClick: (state: boolean) => void; + onSubmit: () => void; +} + +export class RightToolbar extends React.PureComponent { + static active = { color: '#0088ce' }; + + constructor(props: RightToolbarProps) { + super(props); + } + + render() { + const { disabled, graph, minimap, summary, onGraphClick, onSummaryClick, onMinimapClick, onSubmit } = this.props; + return ( + + + + + + + ); + } +} + +export default RightToolbar; diff --git a/src/components/JaegerToolbar/ServiceDropdown.tsx b/src/components/JaegerToolbar/ServiceDropdown.tsx new file mode 100644 index 0000000000..6e23753c78 --- /dev/null +++ b/src/components/JaegerToolbar/ServiceDropdown.tsx @@ -0,0 +1,98 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { KialiAppState } from '../../store/Store'; +import ToolbarDropdown from '../../components/ToolbarDropdown/ToolbarDropdown'; +import { JaegerActions } from '../../actions/JaegerActions'; +import { JaegerThunkActions } from '../../actions/JaegerThunkActions'; +import { ThunkDispatch } from 'redux-thunk'; +import { KialiAppAction } from '../../actions/KialiAppAction'; + +interface ServiceDropdownProps { + disabled: boolean; + activeNamespace: string; + service: string; + items: string[]; + refresh: (ns: string) => void; + setService: (service: string) => void; +} + +export class ServiceDropdown extends React.PureComponent { + constructor(props: ServiceDropdownProps) { + super(props); + } + + componentDidMount() { + if (this.props.activeNamespace) { + this.props.refresh(this.props.activeNamespace); + } + } + + componentDidUpdate(prevProps: ServiceDropdownProps) { + if (this.props.activeNamespace !== prevProps.activeNamespace && this.props.activeNamespace) { + this.props.refresh(this.props.activeNamespace); + } + } + + handleToggle = (isOpen: boolean) => isOpen && this.props.refresh(this.props.activeNamespace); + + labelServiceDropdown = (items: number) => { + if (this.props.activeNamespace && this.props.activeNamespace !== 'all') { + if (items === 0) { + return 'Choose another namespace with services'; + } + return 'Choose a service'; + } + return 'Choose a namespace'; + }; + + render() { + const { disabled } = this.props; + + const items: { [key: string]: string } = this.props.items.reduce((list, item) => { + list[item] = item; + return list; + }, {}); + + return ( + + + + ); + } +} + +const mapStateToProps = (state: KialiAppState) => { + return { + items: state.jaegerState.toolbar.services, + disabled: state.jaegerState.toolbar.isFetchingService, + activeNamespace: state.jaegerState.search.namespaceSelected, + service: state.jaegerState.search.serviceSelected + }; +}; + +const mapDispatchToProps = (dispatch: ThunkDispatch) => { + return { + refresh: (ns: string) => { + dispatch(JaegerThunkActions.asyncFetchServices(ns)); + }, + setService: (service: string) => { + dispatch(JaegerActions.setService(service)); + } + }; +}; + +const ServiceDropdownContainer = connect( + mapStateToProps, + mapDispatchToProps +)(ServiceDropdown); + +export default ServiceDropdownContainer; diff --git a/src/components/JaegerToolbar/TagsControl.tsx b/src/components/JaegerToolbar/TagsControl.tsx new file mode 100644 index 0000000000..4069a27dd2 --- /dev/null +++ b/src/components/JaegerToolbar/TagsControl.tsx @@ -0,0 +1,70 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Col, Form, Card, FormGroup, FormControl, FieldLevelHelp } from 'patternfly-react'; +import { KialiAppState } from '../../store/Store'; + +interface TagsControlProps { + fetching: boolean; + tags: string; + onChange: (event: any) => void; +} + +export class TagsControl extends React.PureComponent { + constructor(props: TagsControlProps) { + super(props); + } + + tagsHelp = () => { + return ( + + + Values should be in the{' '} + + logfmt + {' '} + format. + + +
    +
  • Use space for conjunctions
  • +
  • Values containing whitespace should be enclosed in quotes
  • +
+
+ + error=true db.statement="select * from User" + +
+ ); + }; + + render() { + const { fetching, tags } = this.props; + return ( + + + Tags + + + this.props.onChange(e)} + /> + + ); + } +} + +const mapStateToProps = (state: KialiAppState) => { + return { + fetching: state.jaegerState.search.serviceSelected === '', + tags: state.jaegerState.search.tags + }; +}; + +const TagsControlContainer = connect(mapStateToProps)(TagsControl); + +export default TagsControlContainer; diff --git a/src/components/JaegerToolbar/__tests__/JaegerToolbar.test.tsx b/src/components/JaegerToolbar/__tests__/JaegerToolbar.test.tsx new file mode 100644 index 0000000000..ca0efc6ab6 --- /dev/null +++ b/src/components/JaegerToolbar/__tests__/JaegerToolbar.test.tsx @@ -0,0 +1,127 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { JaegerToolbar } from '../JaegerToolbar'; +import { FormControl } from 'patternfly-react'; +import RightToolbar from '../RightToolbar'; + +describe('LookBack', () => { + let wrapper, requestSearchURL, setGraph, setDetails, setMinimap; + beforeEach(() => { + requestSearchURL = jest.fn(); + setGraph = jest.fn(); + setDetails = jest.fn(); + setMinimap = jest.fn(); + const props = { + disableSelector: false, + tagsValue: '', + showGraph: false, + showSummary: false, + showMinimap: false, + disabled: false, + limit: 0, + requestSearchURL: requestSearchURL, + setGraph: setGraph, + setDetails: setDetails, + setMinimap: setMinimap + }; + wrapper = shallow(); + }); + + it('renders JaegerToolbar correctly', () => { + expect(wrapper).toBeDefined(); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders JaegerToolbar correctly without namespace selector', () => { + wrapper.setProps({ disableSelector: true }); + expect(wrapper).toBeDefined(); + expect(wrapper).toMatchSnapshot(); + }); + + describe('Form', () => { + it('FormControl should be disabled', () => { + wrapper.find(FormControl).forEach(f => { + expect(f.props()['disabled']).toBeFalsy(); + }); + wrapper.setProps({ disabled: true }); + wrapper.find(FormControl).forEach(f => { + expect(f.props()['disabled']).toBeTruthy(); + }); + }); + }); + + describe('RightToolbar', () => { + it('RightToolbar onGraphClick should be setGraph', () => { + expect( + wrapper + .find(RightToolbar) + .first() + .props()['onGraphClick'] + ).toBe(setGraph); + }); + + it('RightToolbar onSummaryClick should be setDetails', () => { + expect( + wrapper + .find(RightToolbar) + .first() + .props()['onSummaryClick'] + ).toBe(setDetails); + }); + + it('RightToolbar onMinimapClick should be setMinimap', () => { + expect( + wrapper + .find(RightToolbar) + .first() + .props()['onMinimapClick'] + ).toBe(setMinimap); + }); + + it('RightToolbar should be disabled', () => { + expect( + wrapper + .find(RightToolbar) + .first() + .props()['disabled'] + ).toBeFalsy(); + wrapper.setProps({ disabled: true }); + expect( + wrapper + .find(RightToolbar) + .first() + .props()['disabled'] + ).toBeTruthy(); + }); + + it('RightToolbar should have the buttons true or false', () => { + const cases = [ + { showGraph: true, showSummary: false, showMinimap: false }, + { showGraph: true, showSummary: false, showMinimap: true }, + { showGraph: true, showSummary: true, showMinimap: false }, + { showGraph: false, showSummary: true, showMinimap: true } + ]; + cases.forEach(useCase => { + wrapper.setProps(useCase); + expect( + wrapper + .find(RightToolbar) + .first() + .props()['graph'] + ).toEqual(useCase.showGraph); + expect( + wrapper + .find(RightToolbar) + .first() + .props()['minimap'] + ).toEqual(useCase.showMinimap); + expect( + wrapper + .find(RightToolbar) + .first() + .props()['summary'] + ).toEqual(useCase.showSummary); + }); + }); + }); +}); diff --git a/src/components/JaegerToolbar/__tests__/LookBack.test.tsx b/src/components/JaegerToolbar/__tests__/LookBack.test.tsx new file mode 100644 index 0000000000..c869bc35dc --- /dev/null +++ b/src/components/JaegerToolbar/__tests__/LookBack.test.tsx @@ -0,0 +1,89 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { LookBack } from '../LookBack'; +import { Form, FormGroup } from 'patternfly-react'; +import ToolbarDropdown from '../../../components/ToolbarDropdown/ToolbarDropdown'; + +const lookBackOptions = { + '1h': 'Last Hour', + '2h': 'Last 2 Hours', + '3h': 'Last 3 Hours', + '6h': 'Last 6 Hours', + '12h': 'Last 12 Hours', + '24h': 'Last 24 Hours', + '2d': 'Last 2 Days', + custom: 'Custom Time Range' +}; + +describe('LookBack', () => { + let wrapper, onChangeCustom, setLookback; + + beforeEach(() => { + onChangeCustom = jest.fn(); + setLookback = jest.fn(); + wrapper = shallow( + + ); + }); + + it('renders LookBack correctly without custom', () => { + expect(wrapper).toBeDefined(); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders Extra forms when lookback is custom', () => { + expect(wrapper.find(Form).length).toEqual(0); + wrapper.setProps({ lookback: 'custom' }); + expect(wrapper.find(Form).length).toEqual(1); + expect(wrapper.find(FormGroup).length).toEqual(2); + }); + + it('LookBack has lookBackOptions options', () => { + expect(wrapper.instance().lookBackOptions).toEqual(lookBackOptions); + }); + + it('disable ToolbarDropwdown if no namespaces', () => { + expect(wrapper.find(ToolbarDropdown).length).toEqual(1); + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['disabled'] + ).toBeFalsy(); + wrapper.setProps({ fetching: true }); + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['disabled'] + ).toBeTruthy(); + }); + + it('ToolbarDropdown has lookBackOptions like options', () => { + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['options'] + ).toEqual(lookBackOptions); + }); + + it('ToolbarDropdown has 1h by defaut', () => { + wrapper.setProps({ lookback: '1h' }); + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['value'] + ).toEqual('1h'); + }); + + it('ToolbarDropdown has setLookback like handleSelect', () => { + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['handleSelect'] + ).toBe(setLookback); + }); +}); diff --git a/src/components/JaegerToolbar/__tests__/NamespaceDropdown.test.tsx b/src/components/JaegerToolbar/__tests__/NamespaceDropdown.test.tsx new file mode 100644 index 0000000000..624d2f977b --- /dev/null +++ b/src/components/JaegerToolbar/__tests__/NamespaceDropdown.test.tsx @@ -0,0 +1,109 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { NamespaceDropdown } from '../NamespaceDropdown'; +import ToolbarDropdown from '../../../components/ToolbarDropdown/ToolbarDropdown'; + +describe('NamespaceDropdown', () => { + let wrapper, refresh, setNamespace; + beforeEach(() => { + refresh = jest.fn(); + setNamespace = jest.fn(); + wrapper = shallow( + + ); + }); + + it('renders NamespaceDropdown correctly without custom', () => { + expect(wrapper).toBeDefined(); + expect(wrapper).toMatchSnapshot(); + }); + + it('renders NamespaceDropdown correctly with custom', () => { + wrapper.setProps({ items: [{ name: 'bookinfo' }, { name: 'istio-system' }] }); + expect(wrapper).toBeDefined(); + expect(wrapper).toMatchSnapshot(); + }); + + it('disable ToolbarDropwdown if no namespaces', () => { + expect(wrapper.find(ToolbarDropdown).length).toEqual(1); + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['disabled'] + ).toBeTruthy(); + wrapper.setProps({ items: [{ name: 'bookinfo' }] }); + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['disabled'] + ).toBeFalsy(); + wrapper.setProps({ disabled: true }); + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['disabled'] + ).toBeTruthy(); + }); + + it('set value if namespace selected', () => { + const ns = 'bookinfo'; + wrapper.setProps({ namespace: ns }); + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['value'] + ).toBe(ns); + }); + + it('set label if namespace selected', () => { + const ns = 'bookinfo'; + const label = 'Select a Namespace'; + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['label'] + ).toBe(label); + wrapper.setProps({ namespace: ns }); + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['label'] + ).toBe(ns); + }); + + it('set items in options in dropdown', () => { + const namespaces = [{ name: 'bookinfo' }, { name: 'istio-system' }]; + const items: { [key: string]: string } = namespaces.reduce((list, item) => { + list[item.name] = item.name; + return list; + }, {}); + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['options'] + ).toEqual({}); + wrapper.setProps({ items: namespaces }); + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['options'] + ).toEqual(items); + }); + + it('set handleSelect in dropdown', () => { + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['handleSelect'] + ).toBe(setNamespace); + }); +}); diff --git a/src/components/JaegerToolbar/__tests__/RightToolbar.test.tsx b/src/components/JaegerToolbar/__tests__/RightToolbar.test.tsx new file mode 100644 index 0000000000..8ff5e99934 --- /dev/null +++ b/src/components/JaegerToolbar/__tests__/RightToolbar.test.tsx @@ -0,0 +1,72 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { RightToolbar } from '../RightToolbar'; + +const active = { color: '#0088ce' }; +describe('RightToolbar', () => { + let wrapper, onGraphClick, onSummaryClick, onMinimapClick, onSubmit; + beforeEach(() => { + onGraphClick = jest.fn(); + onMinimapClick = jest.fn(); + onSummaryClick = jest.fn(); + onSubmit = jest.fn(); + const props = { + disabled: false, + graph: false, + minimap: false, + summary: false, + onGraphClick: onGraphClick, + onSummaryClick: onSummaryClick, + onMinimapClick: onMinimapClick, + onSubmit: onSubmit + }; + wrapper = shallow(); + }); + + it('renders RightToolbar correctly', () => { + expect(wrapper).toBeDefined(); + expect(wrapper).toMatchSnapshot(); + }); + + describe('RightToolbar should have buttons with options', () => { + it('RightToolbar have Graph button', () => { + let buttonProps = wrapper.find({ title: 'Graph' }).props(); + expect(buttonProps).toBeDefined(); + expect(buttonProps['style']).toBeUndefined(); + expect(buttonProps['onClick']).toBeDefined(); + wrapper.setProps({ graph: true }); + buttonProps = wrapper.find({ title: 'Graph' }).props(); + expect(buttonProps['style']).toEqual(active); + }); + + it('RightToolbar have Minimap button', () => { + let buttonProps = wrapper.find({ title: 'Minimap' }).props(); + expect(buttonProps).toBeDefined(); + expect(buttonProps['style']).toBeUndefined(); + expect(buttonProps['onClick']).toBeDefined(); + wrapper.setProps({ minimap: true }); + buttonProps = wrapper.find({ title: 'Minimap' }).props(); + expect(buttonProps['style']).toEqual(active); + }); + + it('RightToolbar have Summary button', () => { + let buttonProps = wrapper.find({ title: 'Summary' }).props(); + expect(buttonProps).toBeDefined(); + expect(buttonProps['style']).toBeUndefined(); + expect(buttonProps['onClick']).toBeDefined(); + wrapper.setProps({ summary: true }); + buttonProps = wrapper.find({ title: 'Summary' }).props(); + expect(buttonProps['style']).toEqual(active); + }); + + it('RightToolbar have Search button', () => { + let buttonProps = wrapper.find({ title: 'Search' }).props(); + expect(buttonProps).toBeDefined(); + expect(buttonProps['onClick']).toBeDefined(); + expect(buttonProps['style']).toEqual({ borderLeft: '1px solid #d1d1d1', marginLeft: '10px' }); + wrapper.setProps({ disabled: true }); + buttonProps = wrapper.find({ title: 'Search' }).props(); + expect(buttonProps['disabled']).toBeTruthy(); + }); + }); +}); diff --git a/src/components/JaegerToolbar/__tests__/ServiceDropdown.test.tsx b/src/components/JaegerToolbar/__tests__/ServiceDropdown.test.tsx new file mode 100644 index 0000000000..1a63fc9389 --- /dev/null +++ b/src/components/JaegerToolbar/__tests__/ServiceDropdown.test.tsx @@ -0,0 +1,95 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { ServiceDropdown } from '../ServiceDropdown'; +import ToolbarDropdown from '../../../components/ToolbarDropdown/ToolbarDropdown'; + +describe('NamespaceDropdown', () => { + let wrapper, refresh, setService; + beforeEach(() => { + refresh = jest.fn(); + setService = jest.fn(); + wrapper = shallow( + + ); + }); + + it('renders ServiceDropdown correctly without custom', () => { + expect(wrapper).toBeDefined(); + expect(wrapper).toMatchSnapshot(); + }); + + it('disable ToolbarDropwdown if no namespaces', () => { + expect(wrapper.find(ToolbarDropdown).length).toEqual(1); + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['disabled'] + ).toBeTruthy(); + wrapper.setProps({ items: ['details', 'productpage'] }); + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['disabled'] + ).toBeFalsy(); + wrapper.setProps({ disabled: true }); + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['disabled'] + ).toBeTruthy(); + }); + + it('value should be empty', () => { + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['value'] + ).toBe(''); + }); + + it('label should be service', () => { + const service = 'details'; + wrapper.setProps({ service: service }); + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['label'] + ).toBe(service); + }); + + it('options should be items', () => { + const services = ['details', 'productpage']; + const items: { [key: string]: string } = services.reduce((list, item) => { + list[item] = item; + return list; + }, {}); + wrapper.setProps({ items: services }); + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['options'] + ).toEqual(items); + }); + + it('handleSelect should be setService', () => { + expect( + wrapper + .find(ToolbarDropdown) + .first() + .props()['handleSelect'] + ).toBe(setService); + }); +}); diff --git a/src/components/JaegerToolbar/__tests__/TagsControl.test.tsx b/src/components/JaegerToolbar/__tests__/TagsControl.test.tsx new file mode 100644 index 0000000000..3e6d2b30c5 --- /dev/null +++ b/src/components/JaegerToolbar/__tests__/TagsControl.test.tsx @@ -0,0 +1,73 @@ +import * as React from 'react'; +import { shallow } from 'enzyme'; +import { TagsControl } from '../TagsControl'; +import { FormControl, FieldLevelHelp } from 'patternfly-react'; + +describe('TagsControls', () => { + let wrapper, onChangeMock; + beforeEach(() => { + onChangeMock = jest.fn(); + wrapper = shallow(); + }); + + it('renders TagsControl correctly', () => { + expect(wrapper).toBeDefined(); + expect(wrapper).toMatchSnapshot(); + }); + + it('TagsControl has a help for logfmt', () => { + expect(wrapper.find(FieldLevelHelp)).toHaveLength(1); + }); + + describe('Actions', () => { + it('FormControl active when the service is selected', () => { + expect(wrapper.find(FormControl)).toHaveLength(1); + expect( + wrapper + .find(FormControl) + .first() + .props()['disabled'] + ).toBeFalsy(); + }); + + it('FormControl disabled when is fetching', () => { + wrapper.setProps({ fetching: true }); + expect(wrapper.find(FormControl)).toHaveLength(1); + expect( + wrapper + .find(FormControl) + .first() + .props()['disabled'] + ).toBeTruthy(); + }); + + it('FormControl tags is empty', () => { + expect(wrapper.find(FormControl)).toHaveLength(1); + expect( + wrapper + .find(FormControl) + .first() + .props()['defaultValue'] + ).toBe(''); + }); + + it('FormControl tags to be {error: true}', () => { + wrapper.setProps({ tags: '{error: true}' }); + expect(wrapper.find(FormControl)).toHaveLength(1); + expect( + wrapper + .find(FormControl) + .first() + .props()['defaultValue'] + ).toBe('{error: true}'); + }); + + it('FormControl call onChange when the value change', () => { + const event = { + target: { value: 'new-value' } + }; + wrapper.find(FormControl).simulate('change', event); + expect(onChangeMock).toBeCalledWith(event); + }); + }); +}); diff --git a/src/components/JaegerToolbar/__tests__/__snapshots__/JaegerToolbar.test.tsx.snap b/src/components/JaegerToolbar/__tests__/__snapshots__/JaegerToolbar.test.tsx.snap new file mode 100644 index 0000000000..2e28ea22fb --- /dev/null +++ b/src/components/JaegerToolbar/__tests__/__snapshots__/JaegerToolbar.test.tsx.snap @@ -0,0 +1,2399 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LookBack renders JaegerToolbar correctly 1`] = ` +ShallowWrapper { + Symbol(enzyme.__root__): [Circular], + Symbol(enzyme.__unrendered__): , + Symbol(enzyme.__renderer__): Object { + "batchedUpdates": [Function], + "getNode": [Function], + "render": [Function], + "simulateError": [Function], + "simulateEvent": [Function], + "unmount": [Function], + }, + Symbol(enzyme.__node__): Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": Array [ + + + + + + + + + , + + + + + Min Duration + + + + + + Max Duration + + + + + + Limit Results + + + + , + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + + + + + , + , + , + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": Array [ + , + " ", + , + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object {}, + "ref": null, + "rendered": null, + "type": [Function], + }, + " ", + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object {}, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": "span", + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "onChangeCustom": [Function], + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "disabled": false, + "graph": false, + "minimap": false, + "onGraphClick": [MockFunction], + "onMinimapClick": [MockFunction], + "onSubmit": [Function], + "onSummaryClick": [MockFunction], + "summary": false, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + , + + + Min Duration + + + , + + + Max Duration + + + , + + + Limit Results + + + , + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "onChange": [Function], + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-group", + "children": Array [ + + Min Duration + , + , + ], + "style": Object { + "display": "inline-flex", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "col", + "children": "Min Duration", + "componentClass": [Function], + "style": Object { + "marginTop": "4px", + }, + }, + "ref": null, + "rendered": "Min Duration", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-control", + "componentClass": "input", + "disabled": false, + "onChange": [Function], + "placeholder": "e.g. 1.2s, 100ms, 500us", + "style": Object { + "marginLeft": "10px", + "width": "200px", + }, + "type": "text", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-group", + "children": Array [ + + Max Duration + , + , + ], + "style": Object { + "display": "inline-flex", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "col", + "children": "Max Duration", + "componentClass": [Function], + "style": Object { + "marginTop": "4px", + }, + }, + "ref": null, + "rendered": "Max Duration", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-control", + "componentClass": "input", + "disabled": false, + "onChange": [Function], + "placeholder": "e.g. 1.1s", + "style": Object { + "marginLeft": "10px", + "width": "200px", + }, + "type": "text", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-group", + "children": Array [ + + Limit Results + , + , + ], + "style": Object { + "display": "inline-flex", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "col", + "children": "Limit Results", + "componentClass": [Function], + "style": Object { + "marginTop": "4px", + }, + }, + "ref": null, + "rendered": "Limit Results", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-control", + "componentClass": "input", + "defaultValue": 0, + "disabled": false, + "onChange": [Function], + "style": Object { + "marginLeft": "10px", + "width": "80px", + }, + "type": "number", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + ], + "type": [Function], + }, + ], + "type": "span", + }, + Symbol(enzyme.__nodes__): Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": Array [ + + + + + + + + + , + + + + + Min Duration + + + + + + Max Duration + + + + + + Limit Results + + + + , + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + + + + + , + , + , + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": Array [ + , + " ", + , + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object {}, + "ref": null, + "rendered": null, + "type": [Function], + }, + " ", + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object {}, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": "span", + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "onChangeCustom": [Function], + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "disabled": false, + "graph": false, + "minimap": false, + "onGraphClick": [MockFunction], + "onMinimapClick": [MockFunction], + "onSubmit": [Function], + "onSummaryClick": [MockFunction], + "summary": false, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + , + + + Min Duration + + + , + + + Max Duration + + + , + + + Limit Results + + + , + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "onChange": [Function], + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-group", + "children": Array [ + + Min Duration + , + , + ], + "style": Object { + "display": "inline-flex", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "col", + "children": "Min Duration", + "componentClass": [Function], + "style": Object { + "marginTop": "4px", + }, + }, + "ref": null, + "rendered": "Min Duration", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-control", + "componentClass": "input", + "disabled": false, + "onChange": [Function], + "placeholder": "e.g. 1.2s, 100ms, 500us", + "style": Object { + "marginLeft": "10px", + "width": "200px", + }, + "type": "text", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-group", + "children": Array [ + + Max Duration + , + , + ], + "style": Object { + "display": "inline-flex", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "col", + "children": "Max Duration", + "componentClass": [Function], + "style": Object { + "marginTop": "4px", + }, + }, + "ref": null, + "rendered": "Max Duration", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-control", + "componentClass": "input", + "disabled": false, + "onChange": [Function], + "placeholder": "e.g. 1.1s", + "style": Object { + "marginLeft": "10px", + "width": "200px", + }, + "type": "text", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-group", + "children": Array [ + + Limit Results + , + , + ], + "style": Object { + "display": "inline-flex", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "col", + "children": "Limit Results", + "componentClass": [Function], + "style": Object { + "marginTop": "4px", + }, + }, + "ref": null, + "rendered": "Limit Results", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-control", + "componentClass": "input", + "defaultValue": 0, + "disabled": false, + "onChange": [Function], + "style": Object { + "marginLeft": "10px", + "width": "80px", + }, + "type": "number", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + ], + "type": [Function], + }, + ], + "type": "span", + }, + ], + Symbol(enzyme.__options__): Object { + "adapter": ReactSixteenAdapter { + "options": Object { + "enableComponentDidUpdateOnSetState": true, + "lifecycles": Object { + "componentDidUpdate": Object { + "onSetState": true, + }, + "getDerivedStateFromProps": true, + "getSnapshotBeforeUpdate": true, + "setState": Object { + "skipsComponentDidUpdateOnNullish": true, + }, + }, + }, + }, + }, +} +`; + +exports[`LookBack renders JaegerToolbar correctly without namespace selector 1`] = ` +ShallowWrapper { + Symbol(enzyme.__root__): [Circular], + Symbol(enzyme.__unrendered__): , + Symbol(enzyme.__renderer__): Object { + "batchedUpdates": [Function], + "getNode": [Function], + "render": [Function], + "simulateError": [Function], + "simulateEvent": [Function], + "unmount": [Function], + }, + Symbol(enzyme.__node__): Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": Array [ + + + + , + + + + + Min Duration + + + + + + Max Duration + + + + + + Limit Results + + + + , + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + false, + , + , + ], + }, + "ref": null, + "rendered": Array [ + false, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "onChangeCustom": [Function], + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "disabled": false, + "graph": false, + "minimap": false, + "onGraphClick": [MockFunction], + "onMinimapClick": [MockFunction], + "onSubmit": [Function], + "onSummaryClick": [MockFunction], + "summary": false, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + , + + + Min Duration + + + , + + + Max Duration + + + , + + + Limit Results + + + , + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "onChange": [Function], + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-group", + "children": Array [ + + Min Duration + , + , + ], + "style": Object { + "display": "inline-flex", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "col", + "children": "Min Duration", + "componentClass": [Function], + "style": Object { + "marginTop": "4px", + }, + }, + "ref": null, + "rendered": "Min Duration", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-control", + "componentClass": "input", + "disabled": false, + "onChange": [Function], + "placeholder": "e.g. 1.2s, 100ms, 500us", + "style": Object { + "marginLeft": "10px", + "width": "200px", + }, + "type": "text", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-group", + "children": Array [ + + Max Duration + , + , + ], + "style": Object { + "display": "inline-flex", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "col", + "children": "Max Duration", + "componentClass": [Function], + "style": Object { + "marginTop": "4px", + }, + }, + "ref": null, + "rendered": "Max Duration", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-control", + "componentClass": "input", + "disabled": false, + "onChange": [Function], + "placeholder": "e.g. 1.1s", + "style": Object { + "marginLeft": "10px", + "width": "200px", + }, + "type": "text", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-group", + "children": Array [ + + Limit Results + , + , + ], + "style": Object { + "display": "inline-flex", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "col", + "children": "Limit Results", + "componentClass": [Function], + "style": Object { + "marginTop": "4px", + }, + }, + "ref": null, + "rendered": "Limit Results", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-control", + "componentClass": "input", + "defaultValue": 0, + "disabled": false, + "onChange": [Function], + "style": Object { + "marginLeft": "10px", + "width": "80px", + }, + "type": "number", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + ], + "type": [Function], + }, + ], + "type": "span", + }, + Symbol(enzyme.__nodes__): Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": Array [ + + + + , + + + + + Min Duration + + + + + + Max Duration + + + + + + Limit Results + + + + , + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + false, + , + , + ], + }, + "ref": null, + "rendered": Array [ + false, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "onChangeCustom": [Function], + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "disabled": false, + "graph": false, + "minimap": false, + "onGraphClick": [MockFunction], + "onMinimapClick": [MockFunction], + "onSubmit": [Function], + "onSummaryClick": [MockFunction], + "summary": false, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "children": Array [ + , + + + Min Duration + + + , + + + Max Duration + + + , + + + Limit Results + + + , + ], + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "onChange": [Function], + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-group", + "children": Array [ + + Min Duration + , + , + ], + "style": Object { + "display": "inline-flex", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "col", + "children": "Min Duration", + "componentClass": [Function], + "style": Object { + "marginTop": "4px", + }, + }, + "ref": null, + "rendered": "Min Duration", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-control", + "componentClass": "input", + "disabled": false, + "onChange": [Function], + "placeholder": "e.g. 1.2s, 100ms, 500us", + "style": Object { + "marginLeft": "10px", + "width": "200px", + }, + "type": "text", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-group", + "children": Array [ + + Max Duration + , + , + ], + "style": Object { + "display": "inline-flex", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "col", + "children": "Max Duration", + "componentClass": [Function], + "style": Object { + "marginTop": "4px", + }, + }, + "ref": null, + "rendered": "Max Duration", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-control", + "componentClass": "input", + "disabled": false, + "onChange": [Function], + "placeholder": "e.g. 1.1s", + "style": Object { + "marginLeft": "10px", + "width": "200px", + }, + "type": "text", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-group", + "children": Array [ + + Limit Results + , + , + ], + "style": Object { + "display": "inline-flex", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "col", + "children": "Limit Results", + "componentClass": [Function], + "style": Object { + "marginTop": "4px", + }, + }, + "ref": null, + "rendered": "Limit Results", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-control", + "componentClass": "input", + "defaultValue": 0, + "disabled": false, + "onChange": [Function], + "style": Object { + "marginLeft": "10px", + "width": "80px", + }, + "type": "number", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + ], + "type": [Function], + }, + ], + "type": "span", + }, + ], + Symbol(enzyme.__options__): Object { + "adapter": ReactSixteenAdapter { + "options": Object { + "enableComponentDidUpdateOnSetState": true, + "lifecycles": Object { + "componentDidUpdate": Object { + "onSetState": true, + }, + "getDerivedStateFromProps": true, + "getSnapshotBeforeUpdate": true, + "setState": Object { + "skipsComponentDidUpdateOnNullish": true, + }, + }, + }, + }, + }, +} +`; diff --git a/src/components/JaegerToolbar/__tests__/__snapshots__/LookBack.test.tsx.snap b/src/components/JaegerToolbar/__tests__/__snapshots__/LookBack.test.tsx.snap new file mode 100644 index 0000000000..16e0fc97f7 --- /dev/null +++ b/src/components/JaegerToolbar/__tests__/__snapshots__/LookBack.test.tsx.snap @@ -0,0 +1,258 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`LookBack renders LookBack correctly without custom 1`] = ` +ShallowWrapper { + Symbol(enzyme.__root__): [Circular], + Symbol(enzyme.__unrendered__): , + Symbol(enzyme.__renderer__): Object { + "batchedUpdates": [Function], + "getNode": [Function], + "render": [Function], + "simulateError": [Function], + "simulateEvent": [Function], + "unmount": [Function], + }, + Symbol(enzyme.__node__): Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": Array [ + + Lookback + , + , + null, + ], + "style": Object { + "marginLeft": "10px", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "col", + "children": "Lookback", + "componentClass": [Function], + "style": Object { + "marginRight": "10px", + }, + }, + "ref": null, + "rendered": "Lookback", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "disabled": false, + "handleSelect": [MockFunction] { + "calls": Array [ + Array [ + "", + ], + ], + }, + "id": "lookback-selector", + "label": undefined, + "options": Object { + "12h": "Last 12 Hours", + "1h": "Last Hour", + "24h": "Last 24 Hours", + "2d": "Last 2 Days", + "2h": "Last 2 Hours", + "3h": "Last 3 Hours", + "6h": "Last 6 Hours", + "custom": "Custom Time Range", + }, + "useName": false, + "value": "", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + null, + ], + "type": "span", + }, + Symbol(enzyme.__nodes__): Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": Array [ + + Lookback + , + , + null, + ], + "style": Object { + "marginLeft": "10px", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "col", + "children": "Lookback", + "componentClass": [Function], + "style": Object { + "marginRight": "10px", + }, + }, + "ref": null, + "rendered": "Lookback", + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "disabled": false, + "handleSelect": [MockFunction] { + "calls": Array [ + Array [ + "", + ], + ], + }, + "id": "lookback-selector", + "label": undefined, + "options": Object { + "12h": "Last 12 Hours", + "1h": "Last Hour", + "24h": "Last 24 Hours", + "2d": "Last 2 Days", + "2h": "Last 2 Hours", + "3h": "Last 3 Hours", + "6h": "Last 6 Hours", + "custom": "Custom Time Range", + }, + "useName": false, + "value": "", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + null, + ], + "type": "span", + }, + ], + Symbol(enzyme.__options__): Object { + "adapter": ReactSixteenAdapter { + "options": Object { + "enableComponentDidUpdateOnSetState": true, + "lifecycles": Object { + "componentDidUpdate": Object { + "onSetState": true, + }, + "getDerivedStateFromProps": true, + "getSnapshotBeforeUpdate": true, + "setState": Object { + "skipsComponentDidUpdateOnNullish": true, + }, + }, + }, + }, + }, +} +`; diff --git a/src/components/JaegerToolbar/__tests__/__snapshots__/NamespaceDropdown.test.tsx.snap b/src/components/JaegerToolbar/__tests__/__snapshots__/NamespaceDropdown.test.tsx.snap new file mode 100644 index 0000000000..b30dedf8c3 --- /dev/null +++ b/src/components/JaegerToolbar/__tests__/__snapshots__/NamespaceDropdown.test.tsx.snap @@ -0,0 +1,176 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NamespaceDropdown renders NamespaceDropdown correctly with custom 1`] = ` +ShallowWrapper { + Symbol(enzyme.__root__): [Circular], + Symbol(enzyme.__unrendered__): , + Symbol(enzyme.__renderer__): Object { + "batchedUpdates": [Function], + "getNode": [Function], + "render": [Function], + "simulateError": [Function], + "simulateEvent": [Function], + "unmount": [Function], + }, + Symbol(enzyme.__node__): Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "disabled": false, + "handleSelect": [MockFunction], + "id": "namespace-selector", + "label": "Select a Namespace", + "options": Object { + "bookinfo": "bookinfo", + "istio-system": "istio-system", + }, + "useName": true, + "value": "", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Symbol(enzyme.__nodes__): Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "disabled": false, + "handleSelect": [MockFunction], + "id": "namespace-selector", + "label": "Select a Namespace", + "options": Object { + "bookinfo": "bookinfo", + "istio-system": "istio-system", + }, + "useName": true, + "value": "", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + Symbol(enzyme.__options__): Object { + "adapter": ReactSixteenAdapter { + "options": Object { + "enableComponentDidUpdateOnSetState": true, + "lifecycles": Object { + "componentDidUpdate": Object { + "onSetState": true, + }, + "getDerivedStateFromProps": true, + "getSnapshotBeforeUpdate": true, + "setState": Object { + "skipsComponentDidUpdateOnNullish": true, + }, + }, + }, + }, + }, +} +`; + +exports[`NamespaceDropdown renders NamespaceDropdown correctly without custom 1`] = ` +ShallowWrapper { + Symbol(enzyme.__root__): [Circular], + Symbol(enzyme.__unrendered__): , + Symbol(enzyme.__renderer__): Object { + "batchedUpdates": [Function], + "getNode": [Function], + "render": [Function], + "simulateError": [Function], + "simulateEvent": [Function], + "unmount": [Function], + }, + Symbol(enzyme.__node__): Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "disabled": true, + "handleSelect": [MockFunction], + "id": "namespace-selector", + "label": "Select a Namespace", + "options": Object {}, + "useName": true, + "value": "", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + Symbol(enzyme.__nodes__): Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "disabled": true, + "handleSelect": [MockFunction], + "id": "namespace-selector", + "label": "Select a Namespace", + "options": Object {}, + "useName": true, + "value": "", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + Symbol(enzyme.__options__): Object { + "adapter": ReactSixteenAdapter { + "options": Object { + "enableComponentDidUpdateOnSetState": true, + "lifecycles": Object { + "componentDidUpdate": Object { + "onSetState": true, + }, + "getDerivedStateFromProps": true, + "getSnapshotBeforeUpdate": true, + "setState": Object { + "skipsComponentDidUpdateOnNullish": true, + }, + }, + }, + }, + }, +} +`; diff --git a/src/components/JaegerToolbar/__tests__/__snapshots__/RightToolbar.test.tsx.snap b/src/components/JaegerToolbar/__tests__/__snapshots__/RightToolbar.test.tsx.snap new file mode 100644 index 0000000000..b08220edb6 --- /dev/null +++ b/src/components/JaegerToolbar/__tests__/__snapshots__/RightToolbar.test.tsx.snap @@ -0,0 +1,472 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`RightToolbar renders RightToolbar correctly 1`] = ` +ShallowWrapper { + Symbol(enzyme.__root__): [Circular], + Symbol(enzyme.__unrendered__): , + Symbol(enzyme.__renderer__): Object { + "batchedUpdates": [Function], + "getNode": [Function], + "render": [Function], + "simulateError": [Function], + "simulateEvent": [Function], + "unmount": [Function], + }, + Symbol(enzyme.__node__): Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object { + "children": Array [ + , + , + , + , + ], + "className": "", + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "active": false, + "block": false, + "bsClass": "btn", + "bsStyle": "default", + "children": , + "disabled": false, + "onClick": [Function], + "style": undefined, + "title": "Graph", + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object { + "name": "th", + "type": "fa", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "active": false, + "block": false, + "bsClass": "btn", + "bsStyle": "default", + "children": , + "disabled": false, + "onClick": [Function], + "style": undefined, + "title": "Minimap", + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object { + "name": "map", + "type": "fa", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "active": false, + "block": false, + "bsClass": "btn", + "bsStyle": "default", + "children": , + "disabled": false, + "onClick": [Function], + "style": undefined, + "title": "Summary", + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object { + "name": "info", + "type": "fa", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "active": false, + "block": false, + "bsClass": "btn", + "bsStyle": "link", + "children": , + "disabled": false, + "onClick": [MockFunction], + "style": Object { + "borderLeft": "1px solid #d1d1d1", + "marginLeft": "10px", + }, + "title": "Search", + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object { + "name": "search", + "type": "pf", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": [Function], + }, + ], + "type": [Function], + }, + Symbol(enzyme.__nodes__): Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object { + "children": Array [ + , + , + , + , + ], + "className": "", + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "active": false, + "block": false, + "bsClass": "btn", + "bsStyle": "default", + "children": , + "disabled": false, + "onClick": [Function], + "style": undefined, + "title": "Graph", + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object { + "name": "th", + "type": "fa", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "active": false, + "block": false, + "bsClass": "btn", + "bsStyle": "default", + "children": , + "disabled": false, + "onClick": [Function], + "style": undefined, + "title": "Minimap", + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object { + "name": "map", + "type": "fa", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "active": false, + "block": false, + "bsClass": "btn", + "bsStyle": "default", + "children": , + "disabled": false, + "onClick": [Function], + "style": undefined, + "title": "Summary", + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object { + "name": "info", + "type": "fa", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "active": false, + "block": false, + "bsClass": "btn", + "bsStyle": "link", + "children": , + "disabled": false, + "onClick": [MockFunction], + "style": Object { + "borderLeft": "1px solid #d1d1d1", + "marginLeft": "10px", + }, + "title": "Search", + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object { + "name": "search", + "type": "pf", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": [Function], + }, + ], + "type": [Function], + }, + ], + Symbol(enzyme.__options__): Object { + "adapter": ReactSixteenAdapter { + "options": Object { + "enableComponentDidUpdateOnSetState": true, + "lifecycles": Object { + "componentDidUpdate": Object { + "onSetState": true, + }, + "getDerivedStateFromProps": true, + "getSnapshotBeforeUpdate": true, + "setState": Object { + "skipsComponentDidUpdateOnNullish": true, + }, + }, + }, + }, + }, +} +`; diff --git a/src/components/JaegerToolbar/__tests__/__snapshots__/ServiceDropdown.test.tsx.snap b/src/components/JaegerToolbar/__tests__/__snapshots__/ServiceDropdown.test.tsx.snap new file mode 100644 index 0000000000..1eca0204ac --- /dev/null +++ b/src/components/JaegerToolbar/__tests__/__snapshots__/ServiceDropdown.test.tsx.snap @@ -0,0 +1,122 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`NamespaceDropdown renders ServiceDropdown correctly without custom 1`] = ` +ShallowWrapper { + Symbol(enzyme.__root__): [Circular], + Symbol(enzyme.__unrendered__): , + Symbol(enzyme.__renderer__): Object { + "batchedUpdates": [Function], + "getNode": [Function], + "render": [Function], + "simulateError": [Function], + "simulateEvent": [Function], + "unmount": [Function], + }, + Symbol(enzyme.__node__): Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": , + "style": Object { + "marginLeft": "10px", + }, + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "disabled": true, + "handleSelect": [MockFunction], + "id": "namespace-selector", + "label": "Choose a namespace", + "onToggle": [Function], + "options": Object {}, + "useName": true, + "value": "", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": "span", + }, + Symbol(enzyme.__nodes__): Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "host", + "props": Object { + "children": , + "style": Object { + "marginLeft": "10px", + }, + }, + "ref": null, + "rendered": Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "disabled": true, + "handleSelect": [MockFunction], + "id": "namespace-selector", + "label": "Choose a namespace", + "onToggle": [Function], + "options": Object {}, + "useName": true, + "value": "", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + "type": "span", + }, + ], + Symbol(enzyme.__options__): Object { + "adapter": ReactSixteenAdapter { + "options": Object { + "enableComponentDidUpdateOnSetState": true, + "lifecycles": Object { + "componentDidUpdate": Object { + "onSetState": true, + }, + "getDerivedStateFromProps": true, + "getSnapshotBeforeUpdate": true, + "setState": Object { + "skipsComponentDidUpdateOnNullish": true, + }, + }, + }, + }, + }, +} +`; diff --git a/src/components/JaegerToolbar/__tests__/__snapshots__/TagsControl.test.tsx.snap b/src/components/JaegerToolbar/__tests__/__snapshots__/TagsControl.test.tsx.snap new file mode 100644 index 0000000000..5135e2f8e6 --- /dev/null +++ b/src/components/JaegerToolbar/__tests__/__snapshots__/TagsControl.test.tsx.snap @@ -0,0 +1,505 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`TagsControls renders TagsControl correctly 1`] = ` +ShallowWrapper { + Symbol(enzyme.__root__): [Circular], + Symbol(enzyme.__unrendered__): , + Symbol(enzyme.__renderer__): Object { + "batchedUpdates": [Function], + "getNode": [Function], + "render": [Function], + "simulateError": [Function], + "simulateEvent": [Function], + "unmount": [Function], + }, + Symbol(enzyme.__node__): Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-group", + "children": Array [ + + Tags + + + Values should be in the + + + logfmt + + + format. + + +
    +
  • + Use space for conjunctions +
  • +
  • + Values containing whitespace should be enclosed in quotes +
  • +
+
+ + + error=true db.statement="select * from User" + + + + } + placement="bottom" + rootClose={true} + /> + , + , + ], + "style": Object { + "display": "inline-flex", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "col", + "children": Array [ + "Tags", + + + Values should be in the + + + logfmt + + + format. + + +
    +
  • + Use space for conjunctions +
  • +
  • + Values containing whitespace should be enclosed in quotes +
  • +
+
+ + + error=true db.statement="select * from User" + + + + } + placement="bottom" + rootClose={true} + />, + ], + "componentClass": [Function], + }, + "ref": null, + "rendered": Array [ + "Tags", + Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object { + "buttonClass": "", + "children": null, + "close": undefined, + "content": + + Values should be in the + + + logfmt + + + format. + + +
    +
  • + Use space for conjunctions +
  • +
  • + Values containing whitespace should be enclosed in quotes +
  • +
+
+ + + error=true db.statement="select * from User" + + +
, + "placement": "bottom", + "rootClose": true, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-control", + "componentClass": "input", + "defaultValue": "", + "disabled": false, + "onChange": [Function], + "placeholder": "e.g. http.status_code=200 error=true", + "style": Object { + "marginLeft": "10px", + "width": "400px", + }, + "type": "text", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Symbol(enzyme.__nodes__): Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-group", + "children": Array [ + + Tags + + + Values should be in the + + + logfmt + + + format. + + +
    +
  • + Use space for conjunctions +
  • +
  • + Values containing whitespace should be enclosed in quotes +
  • +
+
+ + + error=true db.statement="select * from User" + + + + } + placement="bottom" + rootClose={true} + /> + , + , + ], + "style": Object { + "display": "inline-flex", + }, + }, + "ref": null, + "rendered": Array [ + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "col", + "children": Array [ + "Tags", + + + Values should be in the + + + logfmt + + + format. + + +
    +
  • + Use space for conjunctions +
  • +
  • + Values containing whitespace should be enclosed in quotes +
  • +
+
+ + + error=true db.statement="select * from User" + + + + } + placement="bottom" + rootClose={true} + />, + ], + "componentClass": [Function], + }, + "ref": null, + "rendered": Array [ + "Tags", + Object { + "instance": null, + "key": undefined, + "nodeType": "function", + "props": Object { + "buttonClass": "", + "children": null, + "close": undefined, + "content": + + Values should be in the + + + logfmt + + + format. + + +
    +
  • + Use space for conjunctions +
  • +
  • + Values containing whitespace should be enclosed in quotes +
  • +
+
+ + + error=true db.statement="select * from User" + + +
, + "placement": "bottom", + "rootClose": true, + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + Object { + "instance": null, + "key": undefined, + "nodeType": "class", + "props": Object { + "bsClass": "form-control", + "componentClass": "input", + "defaultValue": "", + "disabled": false, + "onChange": [Function], + "placeholder": "e.g. http.status_code=200 error=true", + "style": Object { + "marginLeft": "10px", + "width": "400px", + }, + "type": "text", + }, + "ref": null, + "rendered": null, + "type": [Function], + }, + ], + "type": [Function], + }, + ], + Symbol(enzyme.__options__): Object { + "adapter": ReactSixteenAdapter { + "options": Object { + "enableComponentDidUpdateOnSetState": true, + "lifecycles": Object { + "componentDidUpdate": Object { + "onSetState": true, + }, + "getDerivedStateFromProps": true, + "getSnapshotBeforeUpdate": true, + "setState": Object { + "skipsComponentDidUpdateOnNullish": true, + }, + }, + }, + }, + }, +} +`; diff --git a/src/components/JaegerToolbar/index.tsx b/src/components/JaegerToolbar/index.tsx new file mode 100644 index 0000000000..5d17362103 --- /dev/null +++ b/src/components/JaegerToolbar/index.tsx @@ -0,0 +1,3 @@ +import { JaegerToolbarContainer } from './JaegerToolbar'; + +export { JaegerToolbarContainer as JaegerToolbar }; diff --git a/src/components/Nav/Navigation.tsx b/src/components/Nav/Navigation.tsx index 93c0b962a0..8829ef55a3 100644 --- a/src/components/Nav/Navigation.tsx +++ b/src/components/Nav/Navigation.tsx @@ -71,21 +71,9 @@ class Navigation extends React.Component { return isRoute; }); return navItems.map(item => { - if (item.title === 'Distributed Tracing') { - if (this.props.jaegerUrl === '') { - return ''; - } - return ( - this.goTojaeger()} - /> - ); - } - return ( + return item.title === 'Distributed Tracing' && this.props.jaegerUrl === '' ? ( + '' + ) : ( { + return deepFreeze(jaegerQuery) as typeof jaegerQuery; +}; diff --git a/src/pages/ServiceDetails/ServiceDetailsPage.tsx b/src/pages/ServiceDetails/ServiceDetailsPage.tsx index cf701f15ea..5d745993ac 100644 --- a/src/pages/ServiceDetails/ServiceDetailsPage.tsx +++ b/src/pages/ServiceDetails/ServiceDetailsPage.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { RouteComponentProps } from 'react-router-dom'; -import { Nav, NavItem, TabContainer, TabContent, TabPane } from 'patternfly-react'; +import { Nav, NavItem, TabContainer, TabContent, TabPane, Icon } from 'patternfly-react'; import ServiceId from '../../types/ServiceId'; import * as API from '../../services/Api'; import * as MessageCenter from '../../utils/MessageCenter'; @@ -8,6 +8,7 @@ import { ServiceDetailsInfo } from '../../types/ServiceInfo'; import { ObjectValidation, Validations } from '../../types/IstioObjects'; import { authentication } from '../../utils/Authentication'; import ServiceMetricsContainer from '../../containers/ServiceMetricsContainer'; +import ServiceTracesContainer from './ServiceTraces'; import ServiceInfo from './ServiceInfo'; import { MetricsObjectTypes } from '../../types/Metrics'; import { default as DestinationRuleValidator } from './ServiceInfo/types/DestinationRuleValidator'; @@ -20,6 +21,7 @@ type ServiceDetailsState = { interface ServiceDetailsProps extends RouteComponentProps { jaegerUrl: string; + getErrorTraces: (service: string) => void; } interface ParsedSearch { @@ -157,6 +159,7 @@ class ServiceDetails extends React.Component @@ -169,9 +172,20 @@ class ServiceDetails extends React.Component
Inbound Metrics
- -
Traces
-
+ {this.props.jaegerUrl && ( + +
+ Error Traces{' '} + + ({errorTraces} + {errorTraces > 0 && ( + + )} + ) + +
+
+ )} @@ -193,6 +207,14 @@ class ServiceDetails extends React.Component + {this.props.jaegerUrl && ( + + + + )} diff --git a/src/pages/ServiceDetails/ServiceTraces.tsx b/src/pages/ServiceDetails/ServiceTraces.tsx new file mode 100644 index 0000000000..a48bba699a --- /dev/null +++ b/src/pages/ServiceDetails/ServiceTraces.tsx @@ -0,0 +1,65 @@ +import * as React from 'react'; +import Iframe from 'react-iframe'; +import { connect } from 'react-redux'; +import { KialiAppState } from '../../store/Store'; +import { ThunkDispatch } from 'redux-thunk'; +import { KialiAppAction } from '../../actions/KialiAppAction'; +import { JaegerToolbar } from '../../components/JaegerToolbar'; +import { JaegerActions } from '../../actions/JaegerActions'; +import { JaegerThunkActions } from '../../actions/JaegerThunkActions'; + +interface ServiceTracesProps { + namespace: string; + service: string; + url: string; + setOptions: (ns: string, service: string) => void; +} + +class ServiceTraces extends React.Component { + constructor(props: ServiceTracesProps) { + super(props); + } + + componentDidMount = () => { + this.props.setOptions(this.props.namespace, this.props.service); + }; + + render() { + const { url } = this.props; + + return ( + <> + +
+