Skip to content

Commit 7c83fff

Browse files
authored
feat(events-v2): Implement tag distribution / heatmaps UI (#13478)
SEN-672
1 parent 90a92e3 commit 7c83fff

File tree

7 files changed

+169
-15
lines changed

7 files changed

+169
-15
lines changed

src/sentry/static/sentry/app/components/tagDistributionMeter/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ export default class TagDistributionMeter extends React.Component {
1919
count: PropTypes.number.isRequired,
2020
name: PropTypes.string.isRequired,
2121
value: PropTypes.string.isRequired,
22-
url: PropTypes.string.isRequired,
22+
url: PropTypes.oneOfType([PropTypes.string, PropTypes.object]).isRequired,
2323
})
2424
).isRequired,
2525
renderEmpty: PropTypes.func,

src/sentry/static/sentry/app/views/organizationEventsV2/events.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ const CHART_AXIS_OPTIONS = [
2525

2626
class Events extends AsyncComponent {
2727
static propTypes = {
28-
router: PropTypes.object,
28+
router: PropTypes.object.isRequired,
29+
location: PropTypes.object.isRequired,
2930
organization: SentryTypes.Organization.isRequired,
3031
view: SentryTypes.EventView.isRequired,
3132
};
@@ -115,7 +116,7 @@ class Events extends AsyncComponent {
115116
/>
116117
<Pagination pageLinks={dataPageLinks} />
117118
</div>
118-
<Tags view={view} />
119+
<Tags view={view} organization={organization} location={location} />
119120
</Container>
120121
</React.Fragment>
121122
);
Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,90 @@
11
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
import styled from 'react-emotion';
4+
import {isEqual} from 'lodash';
25

36
import SentryTypes from 'app/sentryTypes';
47
import TagDistributionMeter from 'app/components/tagDistributionMeter';
8+
import withApi from 'app/utils/withApi';
9+
import withGlobalSelection from 'app/utils/withGlobalSelection';
10+
import {fetchTags, getEventTagSearchUrl} from './utils';
511

6-
export default class Tags extends React.Component {
12+
class Tags extends React.Component {
713
static propTypes = {
14+
api: PropTypes.object.isRequired,
15+
organization: SentryTypes.Organization.isRequired,
816
view: SentryTypes.EventView.isRequired,
17+
selection: SentryTypes.GlobalSelection.isRequired,
18+
location: PropTypes.object.isRequired,
19+
};
20+
21+
state = {
22+
isLoading: true,
23+
tags: {},
24+
};
25+
26+
componentDidMount() {
27+
this.fetchData();
28+
}
29+
30+
componentDidUpdate(prevProps) {
31+
if (
32+
this.props.view.id !== prevProps.view.id ||
33+
!isEqual(this.props.selection, prevProps.selection)
34+
) {
35+
this.fetchData();
36+
}
37+
}
38+
39+
fetchData = async () => {
40+
const {api, organization, view} = this.props;
41+
42+
try {
43+
const tags = await fetchTags(api, organization.slug, view.tags);
44+
this.setState({tags, isLoading: false});
45+
} catch (err) {
46+
this.setState({tags: {}, isLoading: false});
47+
}
948
};
1049

1150
renderTag(tag) {
12-
return <TagDistributionMeter key={tag} title={tag} segments={[]} />;
51+
const {location} = this.props;
52+
const {isLoading, tags} = this.state;
53+
let segments = [];
54+
let totalValues = 0;
55+
if (!isLoading && tags[tag]) {
56+
totalValues = tags[tag].totalValues;
57+
segments = tags[tag].topValues;
58+
}
59+
60+
segments.forEach(segment => {
61+
segment.url = getEventTagSearchUrl(tag, segment.value, location);
62+
});
63+
64+
return (
65+
<TagDistributionMeter
66+
key={tag}
67+
title={tag}
68+
segments={segments}
69+
totalValues={totalValues}
70+
isLoading={isLoading}
71+
renderLoading={() => <Placeholder />}
72+
/>
73+
);
1374
}
1475

1576
render() {
1677
return <div>{this.props.view.tags.map(tag => this.renderTag(tag))}</div>;
1778
}
1879
}
80+
81+
const Placeholder = styled('div')`
82+
height: 16px;
83+
width: 100%;
84+
display: inline-block;
85+
border-radius: ${p => p.theme.borderRadius};
86+
background-color: #dad9ed;
87+
`;
88+
89+
export {Tags};
90+
export default withApi(withGlobalSelection(Tags));

src/sentry/static/sentry/app/views/organizationEventsV2/tagsTable.jsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {withRouter} from 'react-router';
66
import Link from 'app/components/links/link';
77
import {t} from 'app/locale';
88
import space from 'app/styles/space';
9-
import {eventTagSearchUrl} from './utils';
9+
import {getEventTagSearchUrl} from './utils';
1010

1111
const TagsTable = props => {
1212
return (
@@ -18,7 +18,9 @@ const TagsTable = props => {
1818
<StyledTr key={tag.key}>
1919
<TagKey>{tag.key}</TagKey>
2020
<TagValue>
21-
<Link to={eventTagSearchUrl(tag, props.location)}>{tag.value}</Link>
21+
<Link to={getEventTagSearchUrl(tag.key, tag.value, props.location)}>
22+
{tag.value}
23+
</Link>
2224
</TagValue>
2325
</StyledTr>
2426
))}

src/sentry/static/sentry/app/views/organizationEventsV2/utils.jsx

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,17 +58,18 @@ export function getQuery(view, location) {
5858
* Return a location object for the current pathname
5959
* with a query string reflected the provided tag.
6060
*
61-
* @param {Object} tag containing key/value properties
61+
* @param {String} tagKey
62+
* @param {String} tagValue
6263
* @param {Object} browser location object.
6364
* @return {Object} router target
6465
*/
65-
export function eventTagSearchUrl(tag, location) {
66+
export function getEventTagSearchUrl(tagKey, tagValue, location) {
6667
const query = {...location.query};
6768
// Add tag key/value to search
6869
if (query.query) {
69-
query.query += ` ${tag.key}:"${tag.value}"`;
70+
query.query += ` ${tagKey}:"${tagValue}"`;
7071
} else {
71-
query.query = `${tag.key}:"${tag.value}"`;
72+
query.query = `${tagKey}:"${tagValue}"`;
7273
}
7374
// Remove the event slug so the user sees new search results.
7475
delete query.eventSlug;
@@ -78,3 +79,25 @@ export function eventTagSearchUrl(tag, location) {
7879
query,
7980
};
8081
}
82+
83+
/**
84+
* Fetches tag distributions for heatmaps for an array of tag keys
85+
*
86+
* @param {Object} api
87+
* @param {String} orgSlug
88+
* @param {Array} tagList
89+
* @returns {Promise<Object>}
90+
*/
91+
export function fetchTags(api, orgSlug, tagList) {
92+
return api
93+
.requestPromise(`/organizations/${orgSlug}/events-heatmap/`, {
94+
query: {keys: tagList},
95+
})
96+
.then(resp => {
97+
const tags = {};
98+
resp.forEach(tag => {
99+
tags[tag.key] = tag;
100+
});
101+
return tags;
102+
});
103+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import React from 'react';
2+
import {mount} from 'enzyme';
3+
4+
import {Client} from 'app/api';
5+
import {Tags} from 'app/views/organizationEventsV2/tags';
6+
7+
describe('Tags', function() {
8+
const org = TestStubs.Organization();
9+
beforeEach(function() {
10+
Client.addMockResponse({
11+
url: `/organizations/${org.slug}/events-heatmap/`,
12+
body: [
13+
{
14+
key: 'release',
15+
name: 'Release',
16+
totalValues: 2,
17+
topValues: [{count: 2, value: 'abcd123', name: 'abcd123'}],
18+
},
19+
{
20+
key: 'environment',
21+
name: 'Environment',
22+
totalValues: 1,
23+
topValues: [{count: 1, value: 'production', name: 'production'}],
24+
},
25+
],
26+
});
27+
});
28+
29+
afterEach(function() {
30+
Client.clearMockResponses();
31+
});
32+
33+
it('renders', async function() {
34+
const api = new Client();
35+
const view = {
36+
id: 'test',
37+
name: 'Test',
38+
data: {},
39+
tags: ['release', 'environment'],
40+
};
41+
const wrapper = mount(
42+
<Tags
43+
view={view}
44+
api={api}
45+
organization={org}
46+
selection={{projects: [], environments: [], datetime: {}}}
47+
location={{}}
48+
/>
49+
);
50+
51+
expect(wrapper.find('Placeholder')).toHaveLength(2);
52+
await tick();
53+
wrapper.update();
54+
expect(wrapper.find('Placeholder')).toHaveLength(0);
55+
});
56+
});

tests/js/spec/views/organizationEventsV2/utils.spec.jsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {
22
getCurrentView,
33
getQuery,
4-
eventTagSearchUrl,
4+
getEventTagSearchUrl,
55
} from 'app/views/organizationEventsV2/utils';
66
import {ALL_VIEWS} from 'app/views/organizationEventsV2/data';
77

@@ -52,23 +52,23 @@ describe('eventTagSearchUrl()', function() {
5252
});
5353

5454
it('adds a query', function() {
55-
expect(eventTagSearchUrl({key: 'browser', value: 'firefox'}, location)).toEqual({
55+
expect(getEventTagSearchUrl('browser', 'firefox', location)).toEqual({
5656
pathname: location.pathname,
5757
query: {query: 'browser:"firefox"'},
5858
});
5959
});
6060

6161
it('removes eventSlug', function() {
6262
location.query.eventSlug = 'project-slug:deadbeef';
63-
expect(eventTagSearchUrl({key: 'browser', value: 'firefox'}, location)).toEqual({
63+
expect(getEventTagSearchUrl('browser', 'firefox', location)).toEqual({
6464
pathname: location.pathname,
6565
query: {query: 'browser:"firefox"'},
6666
});
6767
});
6868

6969
it('appends to an existing query', function() {
7070
location.query.query = 'failure';
71-
expect(eventTagSearchUrl({key: 'browser', value: 'firefox'}, location)).toEqual({
71+
expect(getEventTagSearchUrl('browser', 'firefox', location)).toEqual({
7272
pathname: location.pathname,
7373
query: {query: 'failure browser:"firefox"'},
7474
});

0 commit comments

Comments
 (0)