diff --git a/CHANGELOG.md b/CHANGELOG.md
index 043235a2b3..d302c750dd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,8 @@
* **Trace detail:** Log Markers on Spans ([Fix #119](https://github.com/jaegertracing/jaeger-ui/issues/119)) ([@sfriberg](https://github.com/sfriberg) in [#309](https://github.com/jaegertracing/jaeger-ui/pull/309))
+* **Search:** Load trace(s) from a JSON file ([Fix #214](https://github.com/jaegertracing/jaeger-ui/issues/214)) ([@yuribit](https://github.com/yuribit) in [#327](https://github.com/jaegertracing/jaeger-ui/pull/327))
+
## v1.0.1 (February 15, 2019)
### Fixes
diff --git a/packages/jaeger-ui/src/actions/file-reader-api.js b/packages/jaeger-ui/src/actions/file-reader-api.js
new file mode 100644
index 0000000000..8cdbd10bda
--- /dev/null
+++ b/packages/jaeger-ui/src/actions/file-reader-api.js
@@ -0,0 +1,25 @@
+// @flow
+
+// Copyright (c) 2019 Uber Technologies, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import { createAction } from 'redux-actions';
+import fileReader from '../utils/fileReader';
+
+// eslint-disable-next-line import/prefer-default-export
+export const loadJsonTraces = createAction(
+ '@FILE_READER_API/LOAD_JSON',
+ fileList => fileReader.readJsonFile(fileList),
+ fileList => ({ fileList })
+);
diff --git a/packages/jaeger-ui/src/actions/file-reader-api.test.js b/packages/jaeger-ui/src/actions/file-reader-api.test.js
new file mode 100644
index 0000000000..d45bade5b0
--- /dev/null
+++ b/packages/jaeger-ui/src/actions/file-reader-api.test.js
@@ -0,0 +1,40 @@
+// Copyright (c) 2019 Uber Technologies, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import sinon from 'sinon';
+import isPromise from 'is-promise';
+
+import * as fileReaderActions from './file-reader-api';
+import fileReader from '../utils/fileReader';
+
+it('loadJsonTraces should return a promise', () => {
+ const fileList = { data: {}, filename: 'whatever' };
+
+ const { payload } = fileReaderActions.loadJsonTraces(fileList);
+ expect(isPromise(payload)).toBeTruthy();
+ // prevent the unhandled rejection warnings
+ payload.catch(() => {});
+});
+
+it('loadJsonTraces should call readJsonFile', () => {
+ const fileList = { data: {}, filename: 'whatever' };
+ const mock = sinon.mock(fileReader);
+ const called = mock
+ .expects('readJsonFile')
+ .once()
+ .withExactArgs(fileList);
+ fileReaderActions.loadJsonTraces(fileList);
+ expect(called.verify()).toBeTruthy();
+ mock.restore();
+});
diff --git a/packages/jaeger-ui/src/components/SearchTracePage/FileLoader.js b/packages/jaeger-ui/src/components/SearchTracePage/FileLoader.js
new file mode 100644
index 0000000000..19e70eb1e4
--- /dev/null
+++ b/packages/jaeger-ui/src/components/SearchTracePage/FileLoader.js
@@ -0,0 +1,36 @@
+// @flow
+
+// Copyright (c) 2019 Uber Technologies, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as React from 'react';
+import { Upload, Icon } from 'antd';
+
+const Dragger = Upload.Dragger;
+
+type FileLoaderProps = {
+ loadJsonTraces: (fileList: FileList) => void,
+};
+
+export default function FileLoader(props: FileLoaderProps) {
+ return (
+
+
+
+
+ Click or drag files to this area.
+ Support JSON files containig one or more traces.
+
+ );
+}
diff --git a/packages/jaeger-ui/src/components/SearchTracePage/FileLoader.test.js b/packages/jaeger-ui/src/components/SearchTracePage/FileLoader.test.js
new file mode 100644
index 0000000000..7c12ab0f9e
--- /dev/null
+++ b/packages/jaeger-ui/src/components/SearchTracePage/FileLoader.test.js
@@ -0,0 +1,30 @@
+// Copyright (c) 2019 Uber Technologies, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import React from 'react';
+import { shallow } from 'enzyme';
+
+import FileLoader from './FileLoader';
+
+describe('', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = shallow();
+ });
+
+ it('matches the snapshot', () => {
+ expect(wrapper).toMatchSnapshot();
+ });
+});
diff --git a/packages/jaeger-ui/src/components/SearchTracePage/__snapshots__/FileLoader.test.js.snap b/packages/jaeger-ui/src/components/SearchTracePage/__snapshots__/FileLoader.test.js.snap
new file mode 100644
index 0000000000..0ea82b4d4c
--- /dev/null
+++ b/packages/jaeger-ui/src/components/SearchTracePage/__snapshots__/FileLoader.test.js.snap
@@ -0,0 +1,26 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[` matches the snapshot 1`] = `
+
+
+
+
+
+ Click or drag files to this area.
+
+
+ Support JSON files containig one or more traces.
+
+
+`;
diff --git a/packages/jaeger-ui/src/components/SearchTracePage/index.js b/packages/jaeger-ui/src/components/SearchTracePage/index.js
index f3ab70021d..95d12f64f3 100644
--- a/packages/jaeger-ui/src/components/SearchTracePage/index.js
+++ b/packages/jaeger-ui/src/components/SearchTracePage/index.js
@@ -15,7 +15,7 @@
/* eslint-disable react/require-default-props */
import React, { Component } from 'react';
-import { Col, Row } from 'antd';
+import { Col, Row, Tabs } from 'antd';
import PropTypes from 'prop-types';
import queryString from 'query-string';
import { connect } from 'react-redux';
@@ -26,6 +26,7 @@ import SearchForm from './SearchForm';
import SearchResults, { sortFormSelector } from './SearchResults';
import { isSameQuery, getUrl } from './url';
import * as jaegerApiActions from '../../actions/jaeger-api';
+import * as fileReaderActions from '../../actions/file-reader-api';
import ErrorMessage from '../common/ErrorMessage';
import LoadingIndicator from '../common/LoadingIndicator';
import { getLocation as getTraceLocation } from '../TracePage/url';
@@ -34,10 +35,13 @@ import { fetchedState } from '../../constants';
import { sortTraces } from '../../model/search';
import getLastXformCacher from '../../utils/get-last-xform-cacher';
import { stripEmbeddedState } from '../../utils/embedded-url';
+import FileLoader from './FileLoader';
import './index.css';
import JaegerLogo from '../../img/jaeger-logo.svg';
+const TabPane = Tabs.TabPane;
+
// export for tests
export class SearchTracePageImpl extends Component {
componentDidMount() {
@@ -85,6 +89,7 @@ export class SearchTracePageImpl extends Component {
services,
traceResults,
queryOfResults,
+ loadJsonTraces,
} = this.props;
const hasTraceResults = traceResults && traceResults.length > 0;
const showErrors = errors && !loadingTraces;
@@ -95,8 +100,14 @@ export class SearchTracePageImpl extends Component {
{!embedded && (
-
Find Traces
- {!loadingServices && services ? : }
+
+
+ {!loadingServices && services ? : }
+
+
+
+
+
)}
@@ -177,6 +188,7 @@ SearchTracePageImpl.propTypes = {
message: PropTypes.string,
})
),
+ loadJsonTraces: PropTypes.func,
};
const stateTraceXformer = getLastXformCacher(stateTrace => {
@@ -257,6 +269,7 @@ function mapDispatchToProps(dispatch) {
jaegerApiActions,
dispatch
);
+ const { loadJsonTraces } = bindActionCreators(fileReaderActions, dispatch);
const { cohortAddTrace, cohortRemoveTrace } = bindActionCreators(traceDiffActions, dispatch);
return {
cohortAddTrace,
@@ -265,6 +278,7 @@ function mapDispatchToProps(dispatch) {
fetchServiceOperations,
fetchServices,
searchTraces,
+ loadJsonTraces,
};
}
diff --git a/packages/jaeger-ui/src/reducers/trace.js b/packages/jaeger-ui/src/reducers/trace.js
index d8f5199f7b..2aa8d8c84a 100644
--- a/packages/jaeger-ui/src/reducers/trace.js
+++ b/packages/jaeger-ui/src/reducers/trace.js
@@ -16,6 +16,7 @@ import _isEqual from 'lodash/isEqual';
import { handleActions } from 'redux-actions';
import { fetchTrace, fetchMultipleTraces, searchTraces } from '../actions/jaeger-api';
+import { loadJsonTraces } from '../actions/file-reader-api';
import { fetchedState } from '../constants';
import transformTraceData from '../model/transform-trace-data';
@@ -124,6 +125,31 @@ function searchErred(state, { meta, payload }) {
return { ...state, search };
}
+function loadJsonStarted(state) {
+ const { search } = state;
+ return { ...state, search: { ...search, state: fetchedState.LOADING } };
+}
+
+function loadJsonDone(state, { payload }) {
+ const processed = payload.data.map(transformTraceData);
+ const resultTraces = {};
+ const results = new Set(state.search.results);
+ for (let i = 0; i < processed.length; i++) {
+ const data = processed[i];
+ const id = data.traceID;
+ resultTraces[id] = { data, id, state: fetchedState.DONE };
+ results.add(id);
+ }
+ const traces = { ...state.traces, ...resultTraces };
+ const search = { ...state.search, results: Array.from(results), state: fetchedState.DONE };
+ return { ...state, search, traces };
+}
+
+function loadJsonErred(state, { payload }) {
+ const search = { error: payload, results: [], state: fetchedState.ERROR };
+ return { ...state, search };
+}
+
export default handleActions(
{
[`${fetchTrace}_PENDING`]: fetchTraceStarted,
@@ -137,6 +163,10 @@ export default handleActions(
[`${searchTraces}_PENDING`]: fetchSearchStarted,
[`${searchTraces}_FULFILLED`]: searchDone,
[`${searchTraces}_REJECTED`]: searchErred,
+
+ [`${loadJsonTraces}_PENDING`]: loadJsonStarted,
+ [`${loadJsonTraces}_FULFILLED`]: loadJsonDone,
+ [`${loadJsonTraces}_REJECTED`]: loadJsonErred,
},
initialState
);
diff --git a/packages/jaeger-ui/src/reducers/trace.test.js b/packages/jaeger-ui/src/reducers/trace.test.js
index 7ed64647f1..8f1d4621f2 100644
--- a/packages/jaeger-ui/src/reducers/trace.test.js
+++ b/packages/jaeger-ui/src/reducers/trace.test.js
@@ -13,6 +13,7 @@
// limitations under the License.
import * as jaegerApiActions from '../actions/jaeger-api';
+import * as fileReaderActions from '../actions/file-reader-api';
import { fetchedState } from '../constants';
import traceGenerator from '../demo/trace-generators';
import transformTraceData from '../model/transform-trace-data';
@@ -209,3 +210,55 @@ describe('search traces', () => {
});
});
});
+
+describe('load json traces', () => {
+ it('handles a pending load json request', () => {
+ const state = traceReducer(
+ { search: { results: [id] } },
+ {
+ type: `${fileReaderActions.loadJsonTraces}${ACTION_POSTFIX_PENDING}`,
+ }
+ );
+ const outcome = {
+ results: [id],
+ state: fetchedState.LOADING,
+ };
+ expect(state.search).toEqual(outcome);
+ });
+
+ it('handles a successful load json request', () => {
+ const state = traceReducer(undefined, {
+ type: `${fileReaderActions.loadJsonTraces}${ACTION_POSTFIX_FULFILLED}`,
+ payload: { data: [trace] },
+ });
+ const outcome = {
+ traces: {
+ [id]: {
+ id,
+ data: transformTraceData(trace),
+ state: fetchedState.DONE,
+ },
+ },
+ search: {
+ query: null,
+ state: fetchedState.DONE,
+ results: [id],
+ },
+ };
+ expect(state).toEqual(outcome);
+ });
+
+ it('handles a failed load json request', () => {
+ const error = 'some-error';
+ const state = traceReducer(undefined, {
+ type: `${fileReaderActions.loadJsonTraces}${ACTION_POSTFIX_REJECTED}`,
+ payload: error,
+ });
+ const outcome = {
+ error,
+ results: [],
+ state: fetchedState.ERROR,
+ };
+ expect(state.search).toEqual(outcome);
+ });
+});
diff --git a/packages/jaeger-ui/src/utils/fileReader.js b/packages/jaeger-ui/src/utils/fileReader.js
new file mode 100644
index 0000000000..e6ecbeb8e0
--- /dev/null
+++ b/packages/jaeger-ui/src/utils/fileReader.js
@@ -0,0 +1,51 @@
+// @flow
+
+// Copyright (c) 2019 Uber Technologies, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+const fileReader = {
+ readJsonFile(fileList: { file: File }) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onload = () => {
+ if (typeof reader.result !== 'string') {
+ reject(new Error('Invalid result type'));
+ return;
+ }
+ try {
+ resolve(JSON.parse(reader.result));
+ } catch (error) {
+ reject(new Error(`Error parsing JSON: ${error.message}`));
+ }
+ };
+ reader.onerror = () => {
+ // eslint-disable-next-line no-console
+ const errMessage = reader.error ? `: ${String(reader.error)}` : '';
+ reject(new Error(`Error reading the JSON file${errMessage}`));
+ };
+ reader.onabort = () => {
+ // eslint-disable-next-line no-console
+ reject(new Error(`Reading the JSON file has been aborted`));
+ };
+ try {
+ reader.readAsText(fileList.file);
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ reject(new Error(`Error reading the JSON file: ${error.message}`));
+ }
+ });
+ },
+};
+
+export default fileReader;
diff --git a/packages/jaeger-ui/src/utils/fileReader.test.js b/packages/jaeger-ui/src/utils/fileReader.test.js
new file mode 100644
index 0000000000..67904cc24e
--- /dev/null
+++ b/packages/jaeger-ui/src/utils/fileReader.test.js
@@ -0,0 +1,56 @@
+// Copyright (c) 2019 Uber Technologies, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import isPromise from 'is-promise';
+
+import fileReader from './fileReader.js';
+
+describe('fileReader.readJsonFile', () => {
+ it('returns a promise', () => {
+ const promise = fileReader.readJsonFile({ rando: true });
+ // prevent the unhandled rejection warning
+ promise.catch(() => {});
+ expect(isPromise(promise)).toBeTruthy();
+ });
+
+ it('rejects when given an invalid file', () => {
+ const p = fileReader.readJsonFile({ rando: true });
+ return expect(p).rejects.toMatchObject(expect.any(Error));
+ });
+
+ it('does not throw when given an invalid file', () => {
+ let threw = false;
+ try {
+ const p = fileReader.readJsonFile({ rando: true });
+ // prevent the unhandled rejection warning
+ p.catch(() => {});
+ } catch (_) {
+ threw = true;
+ }
+ return expect(threw).toBe(false);
+ });
+
+ it('loads JSON data, successfully', () => {
+ const obj = { ok: true };
+ const file = new File([JSON.stringify(obj)], 'foo.json');
+ const p = fileReader.readJsonFile({ file });
+ return expect(p).resolves.toMatchObject(obj);
+ });
+
+ it('rejects on malform JSON', () => {
+ const file = new File(['not-json'], 'foo.json');
+ const p = fileReader.readJsonFile({ file });
+ return expect(p).rejects.toMatchObject(expect.any(Error));
+ });
+});