Skip to content

Commit

Permalink
Add file uploader
Browse files Browse the repository at this point in the history
Signed-off-by: Yuri Roncella <[email protected]>
  • Loading branch information
Yuri Roncella committed Feb 25, 2019
1 parent 5d0e712 commit 5331fa2
Show file tree
Hide file tree
Showing 10 changed files with 347 additions and 3 deletions.
25 changes: 25 additions & 0 deletions packages/jaeger-ui/src/actions/file-reader-api.js
Original file line number Diff line number Diff line change
@@ -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 })
);
38 changes: 38 additions & 0 deletions packages/jaeger-ui/src/actions/file-reader-api.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// 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();
});

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();
});
36 changes: 36 additions & 0 deletions packages/jaeger-ui/src/components/SearchTracePage/FileLoader.js
Original file line number Diff line number Diff line change
@@ -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 (
<Dragger accept=".json" customRequest={props.loadJsonTraces} multiple>
<p className="ant-upload-drag-icon">
<Icon type="file-add" />
</p>
<p className="ant-upload-text">Click or drag files to this area.</p>
<p className="ant-upload-hint">Support JSON files containig one or more traces.</p>
</Dragger>
);
}
Original file line number Diff line number Diff line change
@@ -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('<FileLoader />', () => {
let wrapper;

beforeEach(() => {
wrapper = shallow(<FileLoader loadJsonTrace={jest.fn()} />);
});

it('matches the snapshot', () => {
expect(wrapper).toMatchSnapshot();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`<FileLoader /> matches the snapshot 1`] = `
<Dragger
accept=".json"
multiple={true}
>
<p
className="ant-upload-drag-icon"
>
<Icon
type="file-add"
/>
</p>
<p
className="ant-upload-text"
>
Click or drag files to this area.
</p>
<p
className="ant-upload-hint"
>
Support JSON files containig one or more traces.
</p>
</Dragger>
`;
20 changes: 17 additions & 3 deletions packages/jaeger-ui/src/components/SearchTracePage/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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';
Expand All @@ -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() {
Expand Down Expand Up @@ -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;
Expand All @@ -95,8 +100,14 @@ export class SearchTracePageImpl extends Component {
{!embedded && (
<Col span={6} className="SearchTracePage--column">
<div className="SearchTracePage--find">
<h2>Find Traces</h2>
{!loadingServices && services ? <SearchForm services={services} /> : <LoadingIndicator />}
<Tabs size="large">
<TabPane tab="Find Traces" key="searchForm">
{!loadingServices && services ? <SearchForm services={services} /> : <LoadingIndicator />}
</TabPane>
<TabPane tab="JSON File" key="fileLoader">
<FileLoader loadJsonTraces={loadJsonTraces} />
</TabPane>
</Tabs>
</div>
</Col>
)}
Expand Down Expand Up @@ -177,6 +188,7 @@ SearchTracePageImpl.propTypes = {
message: PropTypes.string,
})
),
loadJsonTraces: PropTypes.func,
};

const stateTraceXformer = getLastXformCacher(stateTrace => {
Expand Down Expand Up @@ -257,6 +269,7 @@ function mapDispatchToProps(dispatch) {
jaegerApiActions,
dispatch
);
const { loadJsonTraces } = bindActionCreators(fileReaderActions, dispatch);
const { cohortAddTrace, cohortRemoveTrace } = bindActionCreators(traceDiffActions, dispatch);
return {
cohortAddTrace,
Expand All @@ -265,6 +278,7 @@ function mapDispatchToProps(dispatch) {
fetchServiceOperations,
fetchServices,
searchTraces,
loadJsonTraces,
};
}

Expand Down
30 changes: 30 additions & 0 deletions packages/jaeger-ui/src/reducers/trace.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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,
Expand All @@ -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
);
53 changes: 53 additions & 0 deletions packages/jaeger-ui/src/reducers/trace.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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);
});
});
34 changes: 34 additions & 0 deletions packages/jaeger-ui/src/utils/fileReader.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// @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') {
return resolve(JSON.parse(reader.result));
}
return reject('Invalid result type');
};
reader.onerror = reject;
reader.onabort = reject;
reader.readAsText(fileList.file);
});
},
};

export default fileReader;
Loading

0 comments on commit 5331fa2

Please sign in to comment.