diff --git a/package.json b/package.json index 52371e074f..2b4571ef86 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "@kiali/kiali-ui", "version": "0.12.0", + "proxy": "https://kiali-istio-system.127.0.0.1.nip.io", "description": "React UI for [Kiali](https://github.com/kiali/kiali).", "keywords": [ "istio service mesh", @@ -69,6 +70,7 @@ "deep-freeze": "0.0.1", "js-yaml": "3.12.0", "lodash": "4.17.11", + "logfmt": "^1.2.1", "patternfly": "3.48.3", "patternfly-react": "2.20.3", "react": "16.5.2", 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..58a0167b17 --- /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: 'http://localhost:3001' + }) + ), + 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..3a6909b2af --- /dev/null +++ b/src/actions/JaegerThunkActions.ts @@ -0,0 +1,155 @@ +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'; +import { HTTP_VERBS } from '../types/Common'; +import { converToTimestamp } from '../reducers/JaegerState'; + +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); +}; + +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}`; + } +} + +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 => { + let 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; + let 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); + 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'); + + /* + if (!traceOptions.showMinimap) { + urlRequest.addParam(JAEGER_QUERY().EMBED.UI_TRACE_HIDE_MINIMAP); + } + + if (searchOptions.hideGraph) { + urlRequest.addParam(JAEGER_QUERY().EMBED.UI_SEARCH_HIDE_GRAPH); + } + + if (traceOptions.showDetails) { + urlRequest.addParam(JAEGER_QUERY().EMBED.UI_TRACE_SHOW_DETAILS); + } + */ + 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)); + } + }; + }, + getErrorTraces: (service: string) => { + return (dispatch: ThunkDispatch, getState: () => KialiAppState) => { + const nowTime = Date.now() * 1000; + const endTime = `${nowTime}`; + const startTime = `${nowTime - converToTimestamp('1h')}`; + let url = + `${getState().jaegerState.jaegerURL}/api/traces?` + + `service=${service}&` + + `end=${endTime}&start=${startTime}&` + + `tags=%7B"error"%3A"true"%7D`; + + Api.newRequest(HTTP_VERBS.GET, url, {}, {}) + .then(response => { + console.log(response); + }) + .catch(() => dispatch(JaegerActions.requestFailed())); + }; + } +}; diff --git a/src/actions/KialiAppAction.ts b/src/actions/KialiAppAction.ts index 4f52f986e6..9415978c46 100644 --- a/src/actions/KialiAppAction.ts +++ b/src/actions/KialiAppAction.ts @@ -8,6 +8,7 @@ import { LoginAction } from './LoginActions'; import { MessageCenterAction } from './MessageCenterActions'; import { NamespaceAction } from './NamespaceAction'; import { UserSettingsAction } from './UserSettingsActions'; +import { JaegerAction } from './JaegerActions'; export type KialiAppAction = | GlobalAction @@ -19,4 +20,5 @@ export type KialiAppAction = | LoginAction | MessageCenterAction | NamespaceAction - | UserSettingsAction; + | UserSettingsAction + | JaegerAction; diff --git a/src/components/JaegerToolbar/LookBack.tsx b/src/components/JaegerToolbar/LookBack.tsx new file mode 100644 index 0000000000..161d449bcf --- /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 ( + + + Lookack + + + {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..cd5be0f3f5 --- /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.toolbar.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..443846e8f0 --- /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.toolbar.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..35359cd108 --- /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/index.tsx b/src/components/JaegerToolbar/index.tsx new file mode 100644 index 0000000000..82b2e52e78 --- /dev/null +++ b/src/components/JaegerToolbar/index.tsx @@ -0,0 +1,180 @@ +import * as React from 'react'; +import { connect } from 'react-redux'; +import { Col, Form, FormGroup, FormControl, Toolbar } from 'patternfly-react'; +import NamespaceDropdownContainer from './NamespaceDropdown'; +import ServiceDropdown from './ServiceDropdown'; +import LookBack from './LookBack'; +import RightToolbar from './RightToolbar'; +import TagsControl from './TagsControl'; +import { ThunkDispatch } from 'redux-thunk'; +import { KialiAppState } from '../../store/Store'; +import { KialiAppAction } from '../../actions/KialiAppAction'; +import { JaegerThunkActions } from '../../actions/JaegerThunkActions'; +import { JaegerActions } from '../../actions/JaegerActions'; + +interface JaegerToolbarProps { + disableSelector?: boolean; + showGraph: boolean; + showSummary: boolean; + showMinimap: boolean; + disabled: boolean; + limit: number; + requestSearchURL: (state: JaegerToolbarState) => 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; +} + +class JaegerToolbar extends React.Component { + constructor(props: JaegerToolbarProps) { + super(props); + this.state = { + tags: '', + limit: 20, + minDuration: '', + maxDuration: '', + dateTimes: { start: { date: '', time: '' }, end: { date: '', time: '' } } + }; + } + + onChangeLookBackCustom = (step: string, dateField: string, timeField: string) => { + let 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)); + } + }; +}; + +const JaegerToolbarContainer = connect( + mapStateToProps, + mapDispatchToProps +)(JaegerToolbar); + +export default JaegerToolbarContainer; diff --git a/src/components/Nav/Navigation.tsx b/src/components/Nav/Navigation.tsx index c8fd740142..9ce8163336 100644 --- a/src/components/Nav/Navigation.tsx +++ b/src/components/Nav/Navigation.tsx @@ -66,20 +66,6 @@ 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 deepFreeze(jaegerQuery) as typeof jaegerQuery; +}; diff --git a/src/containers/NavigationContainer.ts b/src/containers/NavigationContainer.ts index c9dfe075f1..e8d3adb6ad 100644 --- a/src/containers/NavigationContainer.ts +++ b/src/containers/NavigationContainer.ts @@ -1,20 +1,15 @@ import { connect } from 'react-redux'; import { ThunkDispatch } from 'redux-thunk'; import Navigation from '../components/Nav/Navigation'; -import { KialiAppState, Component } from '../store/Store'; +import { KialiAppState } from '../store/Store'; import { KialiAppAction } from '../actions/KialiAppAction'; import LoginThunkActions from '../actions/LoginThunkActions'; import UserSettingsThunkActions from '../actions/UserSettingsThunkActions'; -const getJaegerUrl = (components: Component[]) => { - const jaegerinfo = components.find(comp => comp.name === 'Jaeger'); - return jaegerinfo ? jaegerinfo.url : ''; -}; - const mapStateToProps = (state: KialiAppState) => ({ authenticated: state.authentication.logged, navCollapsed: state.userSettings.interface.navCollapse, - jaegerUrl: getJaegerUrl(state.statusState.components) + jaegerUrl: state.jaegerState.jaegerURL }); const mapDispatchToProps = (dispatch: ThunkDispatch) => ({ diff --git a/src/pages/ServiceDetails/ServiceDetailsPage.tsx b/src/pages/ServiceDetails/ServiceDetailsPage.tsx index 3e8dd645e4..60a8261d9f 100644 --- a/src/pages/ServiceDetails/ServiceDetailsPage.tsx +++ b/src/pages/ServiceDetails/ServiceDetailsPage.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import { Link, RouteComponentProps } from 'react-router-dom'; -import { Breadcrumb, Nav, NavItem, TabContainer, TabContent, TabPane } from 'patternfly-react'; +import { Breadcrumb, Nav, NavItem, TabContainer, TabContent, TabPane, Icon } from 'patternfly-react'; import history from '../../app/History'; import ServiceId from '../../types/ServiceId'; import * as API from '../../services/Api'; @@ -10,6 +10,7 @@ import { Validations } from '../../types/IstioObjects'; import { authentication } from '../../utils/Authentication'; import IstioObjectDetails from './IstioObjectDetails'; import ServiceMetricsContainer from '../../containers/ServiceMetricsContainer'; +import ServiceTracesContainer from './ServiceTraces'; import ServiceInfo from './ServiceInfo'; import { TargetPage, ListPageLink } from '../../components/ListPage/ListPageLink'; import { MetricsObjectTypes, MetricsDirection } from '../../types/Metrics'; @@ -212,6 +213,7 @@ class ServiceDetails extends React.Component
Inbound Metrics
- -
Traces
+ +
+ Traces{' '} + {errorTraces > 0 && ( + + ({errorTraces} + ) + + )} +
@@ -271,6 +281,12 @@ class ServiceDetails extends React.Component + + + diff --git a/src/pages/ServiceDetails/ServiceTraces.tsx b/src/pages/ServiceDetails/ServiceTraces.tsx new file mode 100644 index 0000000000..e34994bda7 --- /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 ( + <> + +
+