|
@@ -3,6 +3,7 @@ import isEqual from 'lodash/isEqual';
|
|
|
import isObject from 'lodash/isObject';
|
|
|
import keyBy from 'lodash/keyBy';
|
|
|
import pickBy from 'lodash/pickBy';
|
|
|
+import styled from '@emotion/styled';
|
|
|
|
|
|
import {Client} from 'app/api';
|
|
|
import {addLoadingMessage, clearIndicators} from 'app/actionCreators/indicator';
|
|
@@ -14,7 +15,9 @@ import GroupReleaseStats from 'app/components/group/releaseStats';
|
|
|
import GroupTagDistributionMeter from 'app/components/group/tagDistributionMeter';
|
|
|
import GuideAnchor from 'app/components/assistant/guideAnchor';
|
|
|
import LoadingError from 'app/components/loadingError';
|
|
|
+import Placeholder from 'app/components/placeholder';
|
|
|
import SuggestedOwners from 'app/components/group/suggestedOwners/suggestedOwners';
|
|
|
+import space from 'app/styles/space';
|
|
|
import withApi from 'app/utils/withApi';
|
|
|
import {
|
|
|
Event,
|
|
@@ -25,6 +28,8 @@ import {
|
|
|
TagWithTopValues,
|
|
|
} from 'app/types';
|
|
|
|
|
|
+import SidebarSection from './sidebarSection';
|
|
|
+
|
|
|
type Props = {
|
|
|
api: Client;
|
|
|
organization: Organization;
|
|
@@ -32,6 +37,7 @@ type Props = {
|
|
|
group: Group;
|
|
|
event: Event | null;
|
|
|
environments: Environment[];
|
|
|
+ className?: string;
|
|
|
};
|
|
|
|
|
|
type State = {
|
|
@@ -43,74 +49,73 @@ type State = {
|
|
|
};
|
|
|
|
|
|
class GroupSidebar extends React.Component<Props, State> {
|
|
|
- constructor(props) {
|
|
|
- super(props);
|
|
|
-
|
|
|
- this.state = {
|
|
|
- participants: [],
|
|
|
- environments: props.environments,
|
|
|
- };
|
|
|
- }
|
|
|
+ state: State = {
|
|
|
+ participants: [],
|
|
|
+ environments: this.props.environments,
|
|
|
+ };
|
|
|
|
|
|
- componentDidMount() {
|
|
|
+ async componentDidMount() {
|
|
|
const {group, api} = this.props;
|
|
|
|
|
|
- api.request(`/issues/${group.id}/participants/`, {
|
|
|
- success: data => {
|
|
|
- this.setState({
|
|
|
- participants: data,
|
|
|
- error: false,
|
|
|
- });
|
|
|
- },
|
|
|
- error: () => {
|
|
|
- this.setState({
|
|
|
- error: true,
|
|
|
- });
|
|
|
- },
|
|
|
- });
|
|
|
-
|
|
|
- // Fetch group data for all environments since the one passed in props is filtered for the selected environment
|
|
|
- // The charts rely on having all environment data as well as the data for the selected env
|
|
|
- this.props.api.request(`/issues/${group.id}/`, {
|
|
|
- success: data => {
|
|
|
- this.setState({
|
|
|
- allEnvironmentsGroupData: data,
|
|
|
- });
|
|
|
- },
|
|
|
- error: () => {
|
|
|
- this.setState({
|
|
|
- error: true,
|
|
|
- });
|
|
|
- },
|
|
|
- });
|
|
|
-
|
|
|
+ this.fetchParticipants();
|
|
|
this.fetchTagData();
|
|
|
+
|
|
|
+ try {
|
|
|
+ const allEnvironmentsGroupData = await api.requestPromise(`/issues/${group.id}/`);
|
|
|
+ // eslint-disable-next-line react/no-did-mount-set-state
|
|
|
+ this.setState({
|
|
|
+ allEnvironmentsGroupData,
|
|
|
+ });
|
|
|
+ } catch {
|
|
|
+ // eslint-disable-next-line react/no-did-mount-set-state
|
|
|
+ this.setState({
|
|
|
+ error: true,
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- UNSAFE_componentWillReceiveProps(nextProps) {
|
|
|
+ UNSAFE_componentWillReceiveProps(nextProps: Props) {
|
|
|
if (!isEqual(nextProps.environments, this.props.environments)) {
|
|
|
this.setState({environments: nextProps.environments}, this.fetchTagData);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- fetchTagData() {
|
|
|
+ async fetchParticipants() {
|
|
|
+ const {group, api} = this.props;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const participants = await api.requestPromise(`/issues/${group.id}/participants/`);
|
|
|
+ this.setState({
|
|
|
+ participants,
|
|
|
+ error: false,
|
|
|
+ });
|
|
|
+ return participants;
|
|
|
+ } catch {
|
|
|
+ this.setState({
|
|
|
+ error: true,
|
|
|
+ });
|
|
|
+ return [];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ async fetchTagData() {
|
|
|
const {api, group} = this.props;
|
|
|
|
|
|
- // Fetch the top values for the current group's top tags.
|
|
|
- api.request(`/issues/${group.id}/tags/`, {
|
|
|
- query: pickBy({
|
|
|
- key: group.tags.map(data => data.key),
|
|
|
- environment: this.state.environments.map(env => env.name),
|
|
|
- }),
|
|
|
- success: data => {
|
|
|
- this.setState({tagsWithTopValues: keyBy(data, 'key')});
|
|
|
- },
|
|
|
- error: () => {
|
|
|
- this.setState({
|
|
|
- error: true,
|
|
|
- });
|
|
|
- },
|
|
|
- });
|
|
|
+ try {
|
|
|
+ // Fetch the top values for the current group's top tags.
|
|
|
+ const data = await api.requestPromise(`/issues/${group.id}/tags/`, {
|
|
|
+ query: pickBy({
|
|
|
+ key: group.tags.map(tag => tag.key),
|
|
|
+ environment: this.state.environments.map(env => env.name),
|
|
|
+ }),
|
|
|
+ });
|
|
|
+ this.setState({tagsWithTopValues: keyBy(data, 'key')});
|
|
|
+ } catch {
|
|
|
+ this.setState({
|
|
|
+ tagsWithTopValues: {},
|
|
|
+ error: true,
|
|
|
+ });
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
toggleSubscription() {
|
|
@@ -134,20 +139,9 @@ class GroupSidebar extends React.Component<Props, State> {
|
|
|
},
|
|
|
},
|
|
|
{
|
|
|
- complete: () => {
|
|
|
- api.request(`/issues/${group.id}/participants/`, {
|
|
|
- success: data => {
|
|
|
- this.setState({
|
|
|
- participants: data,
|
|
|
- error: false,
|
|
|
- });
|
|
|
- clearIndicators();
|
|
|
- },
|
|
|
- error: () => {
|
|
|
- this.setState({error: true});
|
|
|
- clearIndicators();
|
|
|
- },
|
|
|
- });
|
|
|
+ complete: async () => {
|
|
|
+ await this.fetchParticipants();
|
|
|
+ clearIndicators();
|
|
|
},
|
|
|
}
|
|
|
);
|
|
@@ -160,28 +154,23 @@ class GroupSidebar extends React.Component<Props, State> {
|
|
|
// # TODO(dcramer): remove plugin.title check in Sentry 8.22+
|
|
|
if (issue) {
|
|
|
issues.push(
|
|
|
- <dl key={plugin.slug}>
|
|
|
- <dt>{`${plugin.shortName || plugin.name || plugin.title}: `}</dt>
|
|
|
- <dd>
|
|
|
- <a href={issue.url}>
|
|
|
- {isObject(issue.label) ? issue.label.id : issue.label}
|
|
|
- </a>
|
|
|
- </dd>
|
|
|
- </dl>
|
|
|
+ <React.Fragment key={plugin.slug}>
|
|
|
+ <span>{`${plugin.shortName || plugin.name || plugin.title}: `}</span>
|
|
|
+ <a href={issue.url}>{isObject(issue.label) ? issue.label.id : issue.label}</a>
|
|
|
+ </React.Fragment>
|
|
|
);
|
|
|
}
|
|
|
});
|
|
|
- if (issues.length) {
|
|
|
- return (
|
|
|
- <div>
|
|
|
- <h6>
|
|
|
- <span>{t('External Issues')}</span>
|
|
|
- </h6>
|
|
|
- {issues}
|
|
|
- </div>
|
|
|
- );
|
|
|
+
|
|
|
+ if (!issues.length) {
|
|
|
+ return null;
|
|
|
}
|
|
|
- return null;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <SidebarSection title={t('External Issues')}>
|
|
|
+ <ExternalIssues>{issues}</ExternalIssues>
|
|
|
+ </SidebarSection>
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
renderParticipantData() {
|
|
@@ -199,13 +188,14 @@ class GroupSidebar extends React.Component<Props, State> {
|
|
|
}
|
|
|
|
|
|
render() {
|
|
|
- const {event, group, organization, project, environments} = this.props;
|
|
|
+ const {className, event, group, organization, project, environments} = this.props;
|
|
|
const {allEnvironmentsGroupData, tagsWithTopValues} = this.state;
|
|
|
const projectId = project.slug;
|
|
|
|
|
|
return (
|
|
|
- <div className="group-stats">
|
|
|
+ <div className={className}>
|
|
|
{event && <SuggestedOwners project={project} group={group} event={event} />}
|
|
|
+
|
|
|
<GroupReleaseStats
|
|
|
organization={organization}
|
|
|
project={project}
|
|
@@ -222,38 +212,48 @@ class GroupSidebar extends React.Component<Props, State> {
|
|
|
|
|
|
{this.renderPluginIssue()}
|
|
|
|
|
|
- <h6>
|
|
|
- <GuideAnchor target="tags" position="bottom">
|
|
|
- <span>{t('Tags')}</span>
|
|
|
- </GuideAnchor>
|
|
|
- </h6>
|
|
|
- {tagsWithTopValues &&
|
|
|
- group.tags.map(tag => {
|
|
|
- const tagWithTopValues = tagsWithTopValues[tag.key];
|
|
|
- const topValues = tagWithTopValues ? tagWithTopValues.topValues : [];
|
|
|
- const topValuesTotal = tagWithTopValues ? tagWithTopValues.totalValues : 0;
|
|
|
-
|
|
|
- return (
|
|
|
- <GroupTagDistributionMeter
|
|
|
- key={tag.key}
|
|
|
- tag={tag.key}
|
|
|
- totalValues={topValuesTotal}
|
|
|
- topValues={topValues}
|
|
|
- name={tag.name}
|
|
|
- data-test-id="group-tag"
|
|
|
- organization={organization}
|
|
|
- projectId={projectId}
|
|
|
- group={group}
|
|
|
- />
|
|
|
- );
|
|
|
- })}
|
|
|
- {group.tags.length === 0 && (
|
|
|
- <p data-test-id="no-tags">
|
|
|
- {environments.length
|
|
|
- ? t('No tags found in the selected environments')
|
|
|
- : t('No tags found')}
|
|
|
- </p>
|
|
|
- )}
|
|
|
+ <SidebarSection
|
|
|
+ title={
|
|
|
+ <GuideAnchor target="tags" position="bottom">
|
|
|
+ {t('Tags')}
|
|
|
+ </GuideAnchor>
|
|
|
+ }
|
|
|
+ >
|
|
|
+ {!tagsWithTopValues ? (
|
|
|
+ <TagPlaceholders>
|
|
|
+ <Placeholder height="40px" />
|
|
|
+ <Placeholder height="40px" />
|
|
|
+ <Placeholder height="40px" />
|
|
|
+ <Placeholder height="40px" />
|
|
|
+ </TagPlaceholders>
|
|
|
+ ) : (
|
|
|
+ group.tags.map(tag => {
|
|
|
+ const tagWithTopValues = tagsWithTopValues[tag.key];
|
|
|
+ const topValues = tagWithTopValues ? tagWithTopValues.topValues : [];
|
|
|
+ const topValuesTotal = tagWithTopValues ? tagWithTopValues.totalValues : 0;
|
|
|
+
|
|
|
+ return (
|
|
|
+ <GroupTagDistributionMeter
|
|
|
+ key={tag.key}
|
|
|
+ tag={tag.key}
|
|
|
+ totalValues={topValuesTotal}
|
|
|
+ topValues={topValues}
|
|
|
+ name={tag.name}
|
|
|
+ organization={organization}
|
|
|
+ projectId={projectId}
|
|
|
+ group={group}
|
|
|
+ />
|
|
|
+ );
|
|
|
+ })
|
|
|
+ )}
|
|
|
+ {group.tags.length === 0 && (
|
|
|
+ <p data-test-id="no-tags">
|
|
|
+ {environments.length
|
|
|
+ ? t('No tags found in the selected environments')
|
|
|
+ : t('No tags found')}
|
|
|
+ </p>
|
|
|
+ )}
|
|
|
+ </SidebarSection>
|
|
|
|
|
|
{this.renderParticipantData()}
|
|
|
</div>
|
|
@@ -261,4 +261,20 @@ class GroupSidebar extends React.Component<Props, State> {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-export default withApi(GroupSidebar);
|
|
|
+const TagPlaceholders = styled('div')`
|
|
|
+ display: grid;
|
|
|
+ gap: ${space(1)};
|
|
|
+ grid-auto-flow: row;
|
|
|
+`;
|
|
|
+
|
|
|
+const StyledGroupSidebar = styled(GroupSidebar)`
|
|
|
+ font-size: ${p => p.theme.fontSizeMedium};
|
|
|
+`;
|
|
|
+
|
|
|
+const ExternalIssues = styled('div')`
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: auto max-content;
|
|
|
+ gap: ${space(2)};
|
|
|
+`;
|
|
|
+
|
|
|
+export default withApi(StyledGroupSidebar);
|