Skip to content

Commit

Permalink
feat(events-v2): Implement tag distribution / heatmaps UI (#13478)
Browse files Browse the repository at this point in the history
SEN-672
  • Loading branch information
lynnagara authored Jun 4, 2019
1 parent 90a92e3 commit 7c83fff
Show file tree
Hide file tree
Showing 7 changed files with 169 additions and 15 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ export default class TagDistributionMeter extends React.Component {
count: PropTypes.number.isRequired,
name: PropTypes.string.isRequired,
value: PropTypes.string.isRequired,
url: PropTypes.string.isRequired,
url: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
})
).isRequired,
renderEmpty: PropTypes.func,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ const CHART_AXIS_OPTIONS = [

class Events extends AsyncComponent {
static propTypes = {
router: PropTypes.object,
router: PropTypes.object.isRequired,
location: PropTypes.object.isRequired,
organization: SentryTypes.Organization.isRequired,
view: SentryTypes.EventView.isRequired,
};
Expand Down Expand Up @@ -115,7 +116,7 @@ class Events extends AsyncComponent {
/>
<Pagination pageLinks={dataPageLinks} />
</div>
<Tags view={view} />
<Tags view={view} organization={organization} location={location} />
</Container>
</React.Fragment>
);
Expand Down
76 changes: 74 additions & 2 deletions src/sentry/static/sentry/app/views/organizationEventsV2/tags.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,90 @@
import React from 'react';
import PropTypes from 'prop-types';
import styled from 'react-emotion';
import {isEqual} from 'lodash';

import SentryTypes from 'app/sentryTypes';
import TagDistributionMeter from 'app/components/tagDistributionMeter';
import withApi from 'app/utils/withApi';
import withGlobalSelection from 'app/utils/withGlobalSelection';
import {fetchTags, getEventTagSearchUrl} from './utils';

export default class Tags extends React.Component {
class Tags extends React.Component {
static propTypes = {
api: PropTypes.object.isRequired,
organization: SentryTypes.Organization.isRequired,
view: SentryTypes.EventView.isRequired,
selection: SentryTypes.GlobalSelection.isRequired,
location: PropTypes.object.isRequired,
};

state = {
isLoading: true,
tags: {},
};

componentDidMount() {
this.fetchData();
}

componentDidUpdate(prevProps) {
if (
this.props.view.id !== prevProps.view.id ||
!isEqual(this.props.selection, prevProps.selection)
) {
this.fetchData();
}
}

fetchData = async () => {
const {api, organization, view} = this.props;

try {
const tags = await fetchTags(api, organization.slug, view.tags);
this.setState({tags, isLoading: false});
} catch (err) {
this.setState({tags: {}, isLoading: false});
}
};

renderTag(tag) {
return <TagDistributionMeter key={tag} title={tag} segments={[]} />;
const {location} = this.props;
const {isLoading, tags} = this.state;
let segments = [];
let totalValues = 0;
if (!isLoading && tags[tag]) {
totalValues = tags[tag].totalValues;
segments = tags[tag].topValues;
}

segments.forEach(segment => {
segment.url = getEventTagSearchUrl(tag, segment.value, location);
});

return (
<TagDistributionMeter
key={tag}
title={tag}
segments={segments}
totalValues={totalValues}
isLoading={isLoading}
renderLoading={() => <Placeholder />}
/>
);
}

render() {
return <div>{this.props.view.tags.map(tag => this.renderTag(tag))}</div>;
}
}

const Placeholder = styled('div')`
height: 16px;
width: 100%;
display: inline-block;
border-radius: ${p => p.theme.borderRadius};
background-color: #dad9ed;
`;

export {Tags};
export default withApi(withGlobalSelection(Tags));
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {withRouter} from 'react-router';
import Link from 'app/components/links/link';
import {t} from 'app/locale';
import space from 'app/styles/space';
import {eventTagSearchUrl} from './utils';
import {getEventTagSearchUrl} from './utils';

const TagsTable = props => {
return (
Expand All @@ -18,7 +18,9 @@ const TagsTable = props => {
<StyledTr key={tag.key}>
<TagKey>{tag.key}</TagKey>
<TagValue>
<Link to={eventTagSearchUrl(tag, props.location)}>{tag.value}</Link>
<Link to={getEventTagSearchUrl(tag.key, tag.value, props.location)}>
{tag.value}
</Link>
</TagValue>
</StyledTr>
))}
Expand Down
31 changes: 27 additions & 4 deletions src/sentry/static/sentry/app/views/organizationEventsV2/utils.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,17 +58,18 @@ export function getQuery(view, location) {
* Return a location object for the current pathname
* with a query string reflected the provided tag.
*
* @param {Object} tag containing key/value properties
* @param {String} tagKey
* @param {String} tagValue
* @param {Object} browser location object.
* @return {Object} router target
*/
export function eventTagSearchUrl(tag, location) {
export function getEventTagSearchUrl(tagKey, tagValue, location) {
const query = {...location.query};
// Add tag key/value to search
if (query.query) {
query.query += ` ${tag.key}:"${tag.value}"`;
query.query += ` ${tagKey}:"${tagValue}"`;
} else {
query.query = `${tag.key}:"${tag.value}"`;
query.query = `${tagKey}:"${tagValue}"`;
}
// Remove the event slug so the user sees new search results.
delete query.eventSlug;
Expand All @@ -78,3 +79,25 @@ export function eventTagSearchUrl(tag, location) {
query,
};
}

/**
* Fetches tag distributions for heatmaps for an array of tag keys
*
* @param {Object} api
* @param {String} orgSlug
* @param {Array} tagList
* @returns {Promise<Object>}
*/
export function fetchTags(api, orgSlug, tagList) {
return api
.requestPromise(`/organizations/${orgSlug}/events-heatmap/`, {
query: {keys: tagList},
})
.then(resp => {
const tags = {};
resp.forEach(tag => {
tags[tag.key] = tag;
});
return tags;
});
}
56 changes: 56 additions & 0 deletions tests/js/spec/views/organizationEventsV2/tags.spec.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React from 'react';
import {mount} from 'enzyme';

import {Client} from 'app/api';
import {Tags} from 'app/views/organizationEventsV2/tags';

describe('Tags', function() {
const org = TestStubs.Organization();
beforeEach(function() {
Client.addMockResponse({
url: `/organizations/${org.slug}/events-heatmap/`,
body: [
{
key: 'release',
name: 'Release',
totalValues: 2,
topValues: [{count: 2, value: 'abcd123', name: 'abcd123'}],
},
{
key: 'environment',
name: 'Environment',
totalValues: 1,
topValues: [{count: 1, value: 'production', name: 'production'}],
},
],
});
});

afterEach(function() {
Client.clearMockResponses();
});

it('renders', async function() {
const api = new Client();
const view = {
id: 'test',
name: 'Test',
data: {},
tags: ['release', 'environment'],
};
const wrapper = mount(
<Tags
view={view}
api={api}
organization={org}
selection={{projects: [], environments: [], datetime: {}}}
location={{}}
/>
);

expect(wrapper.find('Placeholder')).toHaveLength(2);
await tick();
wrapper.update();
expect(wrapper.find('Placeholder')).toHaveLength(0);
});
});
8 changes: 4 additions & 4 deletions tests/js/spec/views/organizationEventsV2/utils.spec.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
getCurrentView,
getQuery,
eventTagSearchUrl,
getEventTagSearchUrl,
} from 'app/views/organizationEventsV2/utils';
import {ALL_VIEWS} from 'app/views/organizationEventsV2/data';

Expand Down Expand Up @@ -52,23 +52,23 @@ describe('eventTagSearchUrl()', function() {
});

it('adds a query', function() {
expect(eventTagSearchUrl({key: 'browser', value: 'firefox'}, location)).toEqual({
expect(getEventTagSearchUrl('browser', 'firefox', location)).toEqual({
pathname: location.pathname,
query: {query: 'browser:"firefox"'},
});
});

it('removes eventSlug', function() {
location.query.eventSlug = 'project-slug:deadbeef';
expect(eventTagSearchUrl({key: 'browser', value: 'firefox'}, location)).toEqual({
expect(getEventTagSearchUrl('browser', 'firefox', location)).toEqual({
pathname: location.pathname,
query: {query: 'browser:"firefox"'},
});
});

it('appends to an existing query', function() {
location.query.query = 'failure';
expect(eventTagSearchUrl({key: 'browser', value: 'firefox'}, location)).toEqual({
expect(getEventTagSearchUrl('browser', 'firefox', location)).toEqual({
pathname: location.pathname,
query: {query: 'failure browser:"firefox"'},
});
Expand Down

0 comments on commit 7c83fff

Please sign in to comment.