Skip to content

Commit d951a9f

Browse files
committed
Update UI to use new API data and generic column formatting.
1 parent fa5ca51 commit d951a9f

File tree

6 files changed

+133
-73
lines changed

6 files changed

+133
-73
lines changed

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

Lines changed: 66 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,72 @@ export const ALL_VIEWS = deepFreeze([
8989
},
9090
]);
9191

92+
/**
93+
* A mapping of field types to their rendering function.
94+
* This mapping is used when a field is not defined in SPECIAL_FIELDS
95+
* This mapping should match the output sentry.utils.snuba:get_json_type
96+
*/
97+
export const FIELD_FORMATTERS = {
98+
boolean: {
99+
sortField: true,
100+
renderFunc: (field, data, {organization, location}) => {
101+
const target = {
102+
pathname: `/organizations/${organization.slug}/events/`,
103+
query: {
104+
...location.query,
105+
query: `${field}:${data[field]}`,
106+
},
107+
};
108+
const value = data[field] ? t('yes') : t('no');
109+
return <QueryLink to={target}>{value}</QueryLink>;
110+
},
111+
},
112+
integer: {
113+
sortField: true,
114+
renderFunc: (field, data) => (
115+
<NumberContainer>
116+
{typeof data[field] === 'number' ? <Count value={data[field]} /> : null}
117+
</NumberContainer>
118+
),
119+
},
120+
number: {
121+
sortField: true,
122+
renderFunc: (field, data) => (
123+
<NumberContainer>
124+
{typeof data[field] === 'number' ? <Count value={data[field]} /> : null}
125+
</NumberContainer>
126+
),
127+
},
128+
date: {
129+
sortField: true,
130+
renderFunc: (field, data) => (
131+
<Container>
132+
{data[field] ? (
133+
getDynamicText({
134+
value: <StyledDateTime date={data[field]} />,
135+
fixed: 'timestamp',
136+
})
137+
) : (
138+
<span>t('n/a')</span>
139+
)}
140+
</Container>
141+
),
142+
},
143+
string: {
144+
sortField: false,
145+
renderFunc: (field, data, {organization, location}) => {
146+
const target = {
147+
pathname: `/organizations/${organization.slug}/events/`,
148+
query: {
149+
...location.query,
150+
query: `${field}:${data[field]}`,
151+
},
152+
};
153+
return <QueryLink to={target}>{data[field]}</QueryLink>;
154+
},
155+
},
156+
};
157+
92158
/**
93159
* "Special fields" do not map 1:1 to an single column in the event database,
94160
* they are a UI concept that combines the results of multiple fields and
@@ -189,19 +255,6 @@ export const SPECIAL_FIELDS = {
189255
return <QueryLink to={target}>{badge}</QueryLink>;
190256
},
191257
},
192-
timestamp: {
193-
sortField: 'timestamp',
194-
renderFunc: data => (
195-
<Container>
196-
{data.timestamp
197-
? getDynamicText({
198-
value: <StyledDateTime date={data.timestamp} />,
199-
fixed: 'time',
200-
})
201-
: null}
202-
</Container>
203-
),
204-
},
205258
issue_title: {
206259
sortField: 'issue_title',
207260
renderFunc: (data, {organization, location}) => {
@@ -221,25 +274,6 @@ export const SPECIAL_FIELDS = {
221274
);
222275
},
223276
},
224-
// TODO generalize this.
225-
'count(id)': {
226-
sortField: 'count_id',
227-
renderFunc: data => (
228-
<NumberContainer>
229-
{typeof data.count_id === 'number' ? <Count value={data.count_id} /> : null}
230-
</NumberContainer>
231-
),
232-
},
233-
'count_unique(user)': {
234-
sortField: 'unique_count_user',
235-
renderFunc: data => (
236-
<NumberContainer>
237-
{typeof data.count_unique_user === 'number' ? (
238-
<Count value={data.count_unique_user} />
239-
) : null}
240-
</NumberContainer>
241-
),
242-
},
243277
last_seen: {
244278
sortField: 'last_seen',
245279
renderFunc: data => {

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ class RelatedEvents extends AsyncComponent {
6262
renderBody() {
6363
const {location, projects, event} = this.props;
6464
const {events} = this.state;
65-
if (!events) {
65+
if (!events || !events.data) {
6666
return null;
6767
}
6868

@@ -71,10 +71,10 @@ class RelatedEvents extends AsyncComponent {
7171
<Title>
7272
<InlineSvg src="icon-link" size="12px" /> {t('Related Events')}
7373
</Title>
74-
{events.length < 1 ? (
74+
{events.data.length < 1 ? (
7575
<Card>{t('No related events found.')}</Card>
7676
) : (
77-
events.map(item => {
77+
events.data.map(item => {
7878
const eventUrl = {
7979
pathname: location.pathname,
8080
query: {

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

Lines changed: 12 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,26 +10,27 @@ import LoadingContainer from 'app/components/loading/loadingContainer';
1010
import {t} from 'app/locale';
1111
import space from 'app/styles/space';
1212

13-
import {SPECIAL_FIELDS} from './data';
14-
import {QueryLink} from './styles';
13+
import {FIELD_FORMATTERS, SPECIAL_FIELDS} from './data';
14+
import {getFieldRenderer} from './utils';
1515
import SortLink from './sortLink';
1616

1717
export default class Table extends React.Component {
1818
static propTypes = {
1919
view: SentryTypes.EventView.isRequired,
20-
data: PropTypes.arrayOf(PropTypes.object),
20+
data: PropTypes.object,
2121
isLoading: PropTypes.bool.isRequired,
2222
organization: SentryTypes.Organization.isRequired,
2323
location: PropTypes.object,
2424
};
2525

2626
renderBody() {
27-
const {view, data, organization, location, isLoading} = this.props;
28-
const {fields} = view.data;
27+
const {view, organization, location, isLoading} = this.props;
2928

30-
if (!data || isLoading) {
29+
if (!this.props.data || isLoading) {
3130
return null;
3231
}
32+
const {fields} = view.data;
33+
const {data, meta} = this.props.data;
3334

3435
if (data.length === 0) {
3536
return (
@@ -42,22 +43,8 @@ export default class Table extends React.Component {
4243
return data.map((row, idx) => (
4344
<Row key={idx} className={getGridStyle(view)}>
4445
{fields.map(field => {
45-
const target = {
46-
pathname: `/organizations/${organization.slug}/events/`,
47-
query: {
48-
...location.query,
49-
query: `${field}:${row[field]}`,
50-
},
51-
};
52-
return (
53-
<Cell key={field}>
54-
{SPECIAL_FIELDS.hasOwnProperty(field) ? (
55-
SPECIAL_FIELDS[field].renderFunc(row, {organization, location})
56-
) : (
57-
<QueryLink to={target}>{row[field]}</QueryLink>
58-
)}
59-
</Cell>
60-
);
46+
const fieldRenderer = getFieldRenderer(field, meta);
47+
return <Cell key={field}>{fieldRenderer(row, {organization, location})}</Cell>;
6148
})}
6249
</Row>
6350
));
@@ -73,9 +60,12 @@ export default class Table extends React.Component {
7360
<TableHeader className={getGridStyle(view)}>
7461
{fields.map((field, i) => {
7562
const title = columnNames[i] || field;
63+
7664
let sortKey = field;
7765
if (SPECIAL_FIELDS.hasOwnProperty(field)) {
7866
sortKey = SPECIAL_FIELDS[field].sortField;
67+
} else if (FIELD_FORMATTERS.hasOwnProperty(field)) {
68+
sortKey = FIELD_FORMATTERS[field].sortField ? field : false;
7969
}
8070

8171
if (sortKey === false) {

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import {pick, get} from 'lodash';
1+
import {partial, pick, get} from 'lodash';
22

33
import {DEFAULT_PER_PAGE} from 'app/constants';
44
import {URL_PARAM} from 'app/constants/globalSelectionHeader';
5-
import {ALL_VIEWS} from './data';
5+
import {ALL_VIEWS, SPECIAL_FIELDS, FIELD_FORMATTERS} from './data';
66

77
/**
88
* Given a view id, return the corresponding view object
@@ -134,3 +134,23 @@ export function fetchTotalCount(api, orgSlug, query) {
134134
})
135135
.then(res => res.count);
136136
}
137+
138+
/**
139+
* Get the field renderer for the named field and metadata
140+
*
141+
* @param {String} field name
142+
* @param {object} metadata mapping.
143+
* @returns {Function}
144+
*/
145+
export function getFieldRenderer(field, meta) {
146+
if (SPECIAL_FIELDS.hasOwnProperty(field)) {
147+
return SPECIAL_FIELDS[field].renderFunc;
148+
}
149+
// Inflect the field name so it will match the property in the result set.
150+
const fieldName = field.replace(/^([^\(]+)\(([a-z\._+]+)\)$/, '$1_$2');
151+
const fieldType = meta[fieldName];
152+
if (FIELD_FORMATTERS.hasOwnProperty(fieldType)) {
153+
return partial(FIELD_FORMATTERS[fieldType].renderFunc, fieldName);
154+
}
155+
return partial(FIELD_FORMATTERS.string.renderFunc, fieldName);
156+
}

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,22 @@ describe('OrganizationEventsV2 > EventDetails', function() {
1313
beforeEach(function() {
1414
MockApiClient.addMockResponse({
1515
url: '/organizations/org-slug/events/',
16-
body: [
17-
{
18-
id: 'deadbeef',
19-
title: 'Oh no something bad',
20-
'project.name': 'project-slug',
21-
timestamp: '2019-05-23T22:12:48+00:00',
16+
body: {
17+
meta: {
18+
id: 'string',
19+
title: 'string',
20+
'project.name': 'string',
21+
timestamp: 'date',
2222
},
23-
],
23+
data: [
24+
{
25+
id: 'deadbeef',
26+
title: 'Oh no something bad',
27+
'project.name': 'project-slug',
28+
timestamp: '2019-05-23T22:12:48+00:00',
29+
},
30+
],
31+
},
2432
});
2533
MockApiClient.addMockResponse({
2634
url: '/organizations/org-slug/events/project-slug:deadbeef/',

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,14 +8,22 @@ describe('OrganizationEventsV2', function() {
88
beforeEach(function() {
99
MockApiClient.addMockResponse({
1010
url: '/organizations/org-slug/events/',
11-
body: [
12-
{
13-
id: 'deadbeef',
14-
title: eventTitle,
15-
'project.name': 'project-slug',
16-
timestamp: '2019-05-23T22:12:48+00:00',
11+
body: {
12+
meta: {
13+
id: 'string',
14+
title: 'string',
15+
'project.name': 'string',
16+
timestamp: 'date',
1717
},
18-
],
18+
data: [
19+
{
20+
id: 'deadbeef',
21+
title: eventTitle,
22+
'project.name': 'project-slug',
23+
timestamp: '2019-05-23T22:12:48+00:00',
24+
},
25+
],
26+
},
1927
});
2028
MockApiClient.addMockResponse({
2129
url: '/organizations/org-slug/events/project-slug:deadbeef/',

0 commit comments

Comments
 (0)