Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(workflow): Sort group tags alphabetically #29005

Merged
merged 3 commits into from
Oct 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 101 additions & 116 deletions static/app/views/organizationGroupDetails/groupTags.tsx
Original file line number Diff line number Diff line change
@@ -1,159 +1,132 @@
import * as React from 'react';
import {RouteComponentProps} from 'react-router';
import styled from '@emotion/styled';
import isEqual from 'lodash/isEqual';

import {Client} from 'app/api';
import Alert from 'app/components/alert';
import AsyncComponent from 'app/components/asyncComponent';
import Button from 'app/components/button';
import Count from 'app/components/count';
import DeviceName from 'app/components/deviceName';
import GlobalSelectionLink from 'app/components/globalSelectionLink';
import LoadingError from 'app/components/loadingError';
import LoadingIndicator from 'app/components/loadingIndicator';
import ExternalLink from 'app/components/links/externalLink';
import {extractSelectionParameters} from 'app/components/organizations/globalSelectionHeader/utils';
import {Panel, PanelBody, PanelHeader} from 'app/components/panels';
import Version from 'app/components/version';
import {t, tct} from 'app/locale';
import overflowEllipsis from 'app/styles/overflowEllipsis';
import space from 'app/styles/space';
import {Group, TagWithTopValues} from 'app/types';
import {percent} from 'app/utils';
import withApi from 'app/utils/withApi';

type Props = {
type Props = AsyncComponent['props'] & {
baseUrl: string;
group: Group;
api: Client;
environments: string[];
};
} & RouteComponentProps<{}, {}>;

type State = {
type State = AsyncComponent['state'] & {
tagList: null | TagWithTopValues[];
loading: boolean;
error: boolean;
};

class GroupTags extends React.Component<Props, State> {
state: State = {
tagList: null,
loading: true,
error: false,
};
class GroupTags extends AsyncComponent<Props, State> {
getDefaultState(): State {
return {
...super.getDefaultState(),
tagList: null,
};
}

componentDidMount() {
this.fetchData();
getEndpoints(): ReturnType<AsyncComponent['getEndpoints']> {
const {group, environments} = this.props;
return [
[
'tagList',
`/issues/${group.id}/tags/`,
{
query: {environment: environments},
},
],
];
}

componentDidUpdate(prevProps: Props) {
if (!isEqual(prevProps.environments, this.props.environments)) {
this.fetchData();
this.remountComponent();
}
}

fetchData = () => {
const {api, group, environments} = this.props;
this.setState({
loading: true,
error: false,
});

api.request(`/issues/${group.id}/tags/`, {
query: {environment: environments},
success: data => {
this.setState({
tagList: data,
error: false,
loading: false,
});
},
error: () => {
this.setState({
error: true,
loading: false,
});
},
});
};

getTagsDocsUrl() {
return 'https://docs.sentry.io/platform-redirect/?next=/enriching-events/tags';
}

render() {
const {baseUrl} = this.props;

let children: React.ReactNode[] = [];
renderTags() {
const {baseUrl, location} = this.props;
const {tagList} = this.state;

if (this.state.loading) {
return <LoadingIndicator />;
} else if (this.state.error) {
return <LoadingError onRetry={this.fetchData} />;
}
const alphabeticalTags = (tagList ?? []).sort((a, b) => a.key.localeCompare(b.key));

if (this.state.tagList) {
children = this.state.tagList.map((tag, tagIdx) => {
const valueChildren = tag.topValues.map((tagValue, tagValueIdx) => {
let label: React.ReactNode = null;
const pct = percent(tagValue.count, tag.totalValues);
const query = tagValue.query || `${tag.key}:"${tagValue.value}"`;

switch (tag.key) {
case 'release':
label = <Version version={tagValue.name} anchor={false} />;
break;
default:
label = <DeviceName value={tagValue.name} />;
}

return (
<li key={tagValueIdx} data-test-id={tag.key}>
<TagBarGlobalSelectionLink
to={{
pathname: `${baseUrl}events/`,
query: {query},
}}
>
<TagBarBackground style={{width: pct + '%'}} />
<TagBarLabel>{label}</TagBarLabel>
<TagBarCount>
<Count value={tagValue.count} />
</TagBarCount>
</TagBarGlobalSelectionLink>
</li>
);
});

return (
return (
<Container>
{alphabeticalTags.map((tag, tagIdx) => (
<TagItem key={tagIdx}>
<Panel>
<PanelHeader hasButtons style={{textTransform: 'none'}}>
<div style={{fontSize: 16}}>{tag.key}</div>
<DetailsLinkWrapper>
<GlobalSelectionLink
className="btn btn-default btn-sm"
to={`${baseUrl}tags/${tag.key}/`}
>
{t('More Details')}
</GlobalSelectionLink>
</DetailsLinkWrapper>
</PanelHeader>
<StyledPanelHeader hasButtons>
<TagHeading>{tag.key}</TagHeading>
<Button
size="small"
to={{
pathname: `${baseUrl}tags/${tag.key}/`,
query: extractSelectionParameters(location.query),
}}
>
{t('More Details')}
</Button>
</StyledPanelHeader>
<PanelBody withPadding>
<ul style={{listStyleType: 'none', padding: 0, margin: 0}}>
{valueChildren}
</ul>
<UnstyledUnorderedList>
{tag.topValues.map((tagValue, tagValueIdx) => (
<li key={tagValueIdx} data-test-id={tag.key}>
<TagBarGlobalSelectionLink
to={{
pathname: `${baseUrl}events/`,
query: {
query: tagValue.query || `${tag.key}:"${tagValue.value}"`,
},
}}
>
<TagBarBackground
widthPercent={percent(tagValue.count, tag.totalValues) + '%'}
/>
<TagBarLabel>
{tag.key === 'release' ? (
<Version version={tagValue.name} anchor={false} />
) : (
<DeviceName value={tagValue.name} />
)}
</TagBarLabel>
<TagBarCount>
<Count value={tagValue.count} />
</TagBarCount>
</TagBarGlobalSelectionLink>
</li>
))}
</UnstyledUnorderedList>
</PanelBody>
</Panel>
</TagItem>
);
});
}
))}
</Container>
);
}

renderBody() {
return (
<div>
<Container>{children}</Container>
{this.renderTags()}
<Alert type="info">
{tct(
'Tags are automatically indexed for searching and breakdown charts. Learn how to [link: add custom tags to issues]',
{
link: <a href={this.getTagsDocsUrl()} />,
link: (
<ExternalLink href="https://docs.sentry.io/platform-redirect/?next=/enriching-events/tags" />
),
}
)}
</Alert>
Expand All @@ -162,27 +135,39 @@ class GroupTags extends React.Component<Props, State> {
}
}

const DetailsLinkWrapper = styled('div')`
display: flex;
`;

const Container = styled('div')`
display: flex;
flex-wrap: wrap;
`;

const StyledPanelHeader = styled(PanelHeader)`
text-transform: none;
`;

const TagHeading = styled('h5')`
font-size: ${p => p.theme.fontSizeLarge};
margin-bottom: 0;
`;

const UnstyledUnorderedList = styled('ul')`
list-style: none;
padding-left: 0;
margin-bottom: 0;
`;

const TagItem = styled('div')`
padding: 0 ${space(1)};
width: 50%;
`;

const TagBarBackground = styled('div')`
const TagBarBackground = styled('div')<{widthPercent: string}>`
position: absolute;
top: 0;
bottom: 0;
left: 0;
background: ${p => p.theme.tagBar};
border-radius: ${p => p.theme.borderRadius};
width: ${p => p.widthPercent};
`;

const TagBarGlobalSelectionLink = styled(GlobalSelectionLink)`
Expand Down Expand Up @@ -217,4 +202,4 @@ const TagBarCount = styled('div')`
font-variant-numeric: tabular-nums;
`;

export default withApi(GroupTags);
export default GroupTags;
13 changes: 8 additions & 5 deletions tests/js/spec/views/organizationGroupDetails/groupTags.spec.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {mountWithTheme} from 'sentry-test/enzyme';
import {initializeOrg} from 'sentry-test/initializeOrg';
import {fireEvent, mountWithTheme} from 'sentry-test/reactTestingLibrary';

import GroupTags from 'app/views/organizationGroupDetails/groupTags';

Expand All @@ -18,12 +18,11 @@ describe('GroupTags', function () {
const wrapper = mountWithTheme(
<GroupTags
group={group}
query={{}}
environments={['dev']}
params={{orgId: 'org-slug', groupId: group.id}}
location={{}}
baseUrl={`/organizations/${organization.slug}/issues/${group.id}/`}
/>,
routerContext
{context: routerContext}
);

expect(tagsMock).toHaveBeenCalledWith(
Expand All @@ -33,7 +32,11 @@ describe('GroupTags', function () {
})
);

wrapper.find('li[data-test-id="user"] Link').first().simulate('click', {button: 0});
const headers = wrapper.getAllByRole('heading').map(header => header.innerHTML);
// Check headers have been sorted alphabetically
expect(headers).toEqual(['browser', 'device', 'environment', 'url', 'user']);

fireEvent.click(wrapper.getByText('david'));

expect(router.push).toHaveBeenCalledWith({
pathname: '/organizations/org-slug/issues/1/events/',
Expand Down