diff --git a/packages/jaeger-ui/src/actions/jaeger-api.js b/packages/jaeger-ui/src/actions/jaeger-api.js
index dfd94ab81d..2e01153104 100644
--- a/packages/jaeger-ui/src/actions/jaeger-api.js
+++ b/packages/jaeger-ui/src/actions/jaeger-api.js
@@ -14,6 +14,7 @@
import { createAction } from 'redux-actions';
import JaegerAPI from '../api/jaeger';
+import fileReader from '../utils/fileReader';
export const fetchTrace = createAction(
'@JAEGER_API/FETCH_TRACE',
@@ -50,3 +51,9 @@ export const fetchServiceOperations = createAction(
export const fetchDependencies = createAction('@JAEGER_API/FETCH_DEPENDENCIES', () =>
JaegerAPI.fetchDependencies()
);
+
+export const uploadTraces = createAction(
+ '@JAEGER_API/UPLOAD_TRACES',
+ fileList => fileReader.readJSONFile(fileList),
+ fileList => ({ fileList })
+);
diff --git a/packages/jaeger-ui/src/actions/jaeger-api.test.js b/packages/jaeger-ui/src/actions/jaeger-api.test.js
index 4565422aba..9255b67514 100644
--- a/packages/jaeger-ui/src/actions/jaeger-api.test.js
+++ b/packages/jaeger-ui/src/actions/jaeger-api.test.js
@@ -26,6 +26,7 @@ import isPromise from 'is-promise';
import * as jaegerApiActions from './jaeger-api';
import JaegerAPI from '../api/jaeger';
+import fileReader from '../utils/fileReader';
it('@JAEGER_API/FETCH_TRACE should fetch the trace by id', () => {
const api = JaegerAPI;
@@ -114,3 +115,22 @@ it('@JAEGER_API/FETCH_SERVICE_OPERATIONS should call the JaegerAPI', () => {
expect(called.verify()).toBeTruthy();
mock.restore();
});
+
+it('uploadTraces should return a promise', () => {
+ const fileList = { data: {}, filename: 'whatever' };
+
+ const { payload } = jaegerApiActions.uploadTraces(fileList);
+ expect(isPromise(payload)).toBeTruthy();
+});
+
+it('uploadTraces should call readJSONFile', () => {
+ const fileList = { data: {}, filename: 'whatever' };
+ const mock = sinon.mock(fileReader);
+ const called = mock
+ .expects('readJSONFile')
+ .once()
+ .withExactArgs(fileList);
+ jaegerApiActions.uploadTraces(fileList);
+ expect(called.verify()).toBeTruthy();
+ mock.restore();
+});
diff --git a/packages/jaeger-ui/src/api/jaeger.js b/packages/jaeger-ui/src/api/jaeger.js
index 2abedb6f1b..cbebe7f482 100644
--- a/packages/jaeger-ui/src/api/jaeger.js
+++ b/packages/jaeger-ui/src/api/jaeger.js
@@ -89,7 +89,7 @@ const JaegerAPI = {
return getJSON(`${this.apiRoot}services`).catch(() => []);
},
fetchServiceOperations(serviceName) {
- return getJSON(`${this.apiRoot}services/${encodeURIComponent(serviceName)}/operations`);
+ return getJSON(`${this.apiRoot}services/${encodeURIComponent(serviceName)}/operations`).catch(() => []);
},
fetchDependencies(endTs = new Date().getTime(), lookback = DEFAULT_DEPENDENCY_LOOKBACK) {
return getJSON(`${this.apiRoot}dependencies`, { query: { endTs, lookback } });
diff --git a/packages/jaeger-ui/src/api/jaeger.test.js b/packages/jaeger-ui/src/api/jaeger.test.js
index 3207cc1597..4a999ab0ab 100644
--- a/packages/jaeger-ui/src/api/jaeger.test.js
+++ b/packages/jaeger-ui/src/api/jaeger.test.js
@@ -88,6 +88,25 @@ it('fetchServices() returns [] on a >= 400 status code', done => {
});
});
+it('fetchOperations() returns [] on a >= 400 status code', done => {
+ const status = 400;
+ const statusText = 'some-status';
+ const msg = 'some-message';
+ const errorData = { errors: [{ msg, code: status }] };
+
+ fetchMock.mockReturnValue(
+ Promise.resolve({
+ status,
+ statusText,
+ text: () => Promise.resolve(JSON.stringify(errorData)),
+ })
+ );
+ JaegerAPI.fetchServiceOperations().then(operations => {
+ expect(operations).toEqual([]);
+ done();
+ });
+});
+
it('fetchTrace() throws an useful error derived from a text payload', done => {
const status = 400;
const statusText = 'some-status';
diff --git a/packages/jaeger-ui/src/components/SearchTracePage/FileUploader.js b/packages/jaeger-ui/src/components/SearchTracePage/FileUploader.js
new file mode 100644
index 0000000000..e7532f4d89
--- /dev/null
+++ b/packages/jaeger-ui/src/components/SearchTracePage/FileUploader.js
@@ -0,0 +1,49 @@
+// Copyright (c) 2017 Uber Technologies, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import * as React from 'react';
+import { Upload, Icon } from 'antd';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { bindActionCreators } from 'redux';
+
+import * as jaegerApiActions from '../../actions/jaeger-api';
+
+const Dragger = Upload.Dragger;
+
+export function FileUploaderImpl(props) {
+ const { uploadTraces } = props;
+ return (
+
+
+
+
+ Click or drag files to this area.
+ Support JSON files containig one or more traces.
+
+ );
+}
+
+FileUploaderImpl.propTypes = {
+ uploadTraces: PropTypes.func.isRequired,
+};
+
+function mapDispatchToProps(dispatch) {
+ const { uploadTraces } = bindActionCreators(jaegerApiActions, dispatch);
+ return {
+ uploadTraces,
+ };
+}
+
+export default connect(null, mapDispatchToProps)(FileUploaderImpl);
diff --git a/packages/jaeger-ui/src/components/SearchTracePage/FileUploader.test.js b/packages/jaeger-ui/src/components/SearchTracePage/FileUploader.test.js
new file mode 100644
index 0000000000..6937682b33
--- /dev/null
+++ b/packages/jaeger-ui/src/components/SearchTracePage/FileUploader.test.js
@@ -0,0 +1,30 @@
+// Copyright (c) 2017 Uber Technologies, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import React from 'react';
+import { mount } from 'enzyme';
+
+import { FileUploaderImpl as FileUploader } from './FileUploader';
+
+describe('', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount();
+ });
+
+ it('does not explode', () => {
+ expect(wrapper).toBeDefined();
+ });
+});
diff --git a/packages/jaeger-ui/src/components/SearchTracePage/index.js b/packages/jaeger-ui/src/components/SearchTracePage/index.js
index f3ab70021d..6f5e46b2b7 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';
@@ -34,10 +34,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 FileUploader from './FileUploader';
import './index.css';
import JaegerLogo from '../../img/jaeger-logo.svg';
+const TabPane = Tabs.TabPane;
+
// export for tests
export class SearchTracePageImpl extends Component {
componentDidMount() {
@@ -95,8 +98,14 @@ export class SearchTracePageImpl extends Component {
{!embedded && (
-
Find Traces
- {!loadingServices && services ? : }
+
+
+ {!loadingServices && services ? : }
+
+
+
+
+
)}
diff --git a/packages/jaeger-ui/src/reducers/trace.js b/packages/jaeger-ui/src/reducers/trace.js
index d8f5199f7b..835f689c4a 100644
--- a/packages/jaeger-ui/src/reducers/trace.js
+++ b/packages/jaeger-ui/src/reducers/trace.js
@@ -15,7 +15,7 @@
import _isEqual from 'lodash/isEqual';
import { handleActions } from 'redux-actions';
-import { fetchTrace, fetchMultipleTraces, searchTraces } from '../actions/jaeger-api';
+import { fetchTrace, fetchMultipleTraces, searchTraces, uploadTraces } from '../actions/jaeger-api';
import { fetchedState } from '../constants';
import transformTraceData from '../model/transform-trace-data';
@@ -124,6 +124,34 @@ function searchErred(state, { meta, payload }) {
return { ...state, search };
}
+function uploadStarted(state) {
+ const search = {
+ results: [].concat(state.search.results),
+ state: fetchedState.LOADING,
+ };
+ return { ...state, search };
+}
+
+function uploadDone(state, { payload }) {
+ const processed = payload.data.map(transformTraceData);
+ const resultTraces = {};
+ const results = [].concat(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.push(id);
+ }
+ const traces = { ...state.traces, ...resultTraces };
+ const search = { ...state.search, results, state: fetchedState.DONE };
+ return { ...state, search, traces };
+}
+
+function uploadErred(state, { payload }) {
+ const search = { error: payload, results: [], state: fetchedState.ERROR };
+ return { ...state, search };
+}
+
export default handleActions(
{
[`${fetchTrace}_PENDING`]: fetchTraceStarted,
@@ -137,6 +165,10 @@ export default handleActions(
[`${searchTraces}_PENDING`]: fetchSearchStarted,
[`${searchTraces}_FULFILLED`]: searchDone,
[`${searchTraces}_REJECTED`]: searchErred,
+
+ [`${uploadTraces}_PENDING`]: uploadStarted,
+ [`${uploadTraces}_FULFILLED`]: uploadDone,
+ [`${uploadTraces}_REJECTED`]: uploadErred,
},
initialState
);
diff --git a/packages/jaeger-ui/src/reducers/trace.test.js b/packages/jaeger-ui/src/reducers/trace.test.js
index 7ed64647f1..527f0aaf0b 100644
--- a/packages/jaeger-ui/src/reducers/trace.test.js
+++ b/packages/jaeger-ui/src/reducers/trace.test.js
@@ -209,3 +209,55 @@ describe('search traces', () => {
});
});
});
+
+describe('upload traces', () => {
+ it('handles a pending upload request', () => {
+ const state = traceReducer(
+ { search: { results: [id] } },
+ {
+ type: `${jaegerApiActions.uploadTraces}${ACTION_POSTFIX_PENDING}`,
+ }
+ );
+ const outcome = {
+ results: [id],
+ state: fetchedState.LOADING,
+ };
+ expect(state.search).toEqual(outcome);
+ });
+
+ it('handles a successful upload request', () => {
+ const state = traceReducer(undefined, {
+ type: `${jaegerApiActions.uploadTraces}${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 upload request', () => {
+ const error = 'some-error';
+ const state = traceReducer(undefined, {
+ type: `${jaegerApiActions.uploadTraces}${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..4b612a0510
--- /dev/null
+++ b/packages/jaeger-ui/src/utils/fileReader.js
@@ -0,0 +1,33 @@
+// Copyright (c) 2017 Uber Technologies, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+const fileReader = {
+ readJSONFile(fileList) {
+ return new Promise((resolve, reject) => {
+ const reader = new FileReader();
+ reader.onloadend = () => {
+ resolve(reader.result);
+ };
+ reader.onerror = () => {
+ reject();
+ };
+ reader.onabort = () => {
+ reject();
+ };
+ reader.readAsText(fileList.file);
+ }).then(result => JSON.parse(result));
+ },
+};
+
+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..4721d605f6
--- /dev/null
+++ b/packages/jaeger-ui/src/utils/fileReader.test.js
@@ -0,0 +1,58 @@
+// Copyright (c) 2017 Uber Technologies, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+import isPromise from 'is-promise';
+import sinon from 'sinon';
+
+import fileReader from './fileReader.js';
+
+it('readJSONFile returns a promise', () => {
+ const fileList = { data: {}, filename: 'whatever' };
+
+ const promise = fileReader.readJSONFile(fileList);
+ expect(isPromise(promise)).toBeTruthy();
+});
+
+it('readJSONFile fails to load a fail', async () => {
+ expect.assertions(1);
+ const fileList = { data: {}, filename: 'whatever' };
+ try {
+ await fileReader.readJSONFile(fileList);
+ } catch (e) {
+ expect(true).toBeTruthy();
+ }
+});
+
+it('readJSONFile fails when fileList is wrong', async () => {
+ expect.assertions(2);
+
+ const mock = sinon.mock(window);
+ const called = mock.expects('FileReader').once();
+ const fileList = {
+ action: '',
+ filename: 'file',
+ file: { uid: '1234' },
+ data: {},
+ headers: {},
+ withCredentials: false,
+ };
+
+ try {
+ await fileReader.readJSONFile(fileList);
+ } catch (e) {
+ expect(true).toBeTruthy();
+ expect(called.verify()).toBeTruthy();
+ }
+ mock.restore();
+});