Skip to content

Commit 89c3e17

Browse files
committed
feat(ui): Do not load Global Selection values from local storage in Issue Details
This is for the case where an Issue is accessed from a direct URL. Previously it would load the last accessed project/env from local storage. This is not desired behavior when accessing an Issue directly. This will look for a route to Issue details and avoid loading the store. However we also want to initialize the store if we leave the issue details page. Otherwise (this is exacerbated in the single project case), you could have last accessed project `Foo`, open url to issue in `Bar`, and then navigating from Issue to Issues list could result in a project that is neither `Foo` nor `Bar`. Fixes SEN-709 Fixes SEN-658
1 parent 1df80ec commit 89c3e17

File tree

10 files changed

+243
-34
lines changed

10 files changed

+243
-34
lines changed

src/sentry/static/sentry/app/components/organizations/globalSelectionHeader/index.jsx

+38-4
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
updateProjects,
1818
} from 'app/actionCreators/globalSelection';
1919
import BackToIssues from 'app/components/organizations/backToIssues';
20+
import ConfigStore from 'app/stores/configStore';
2021
import Header from 'app/components/organizations/header';
2122
import HeaderItemPosition from 'app/components/organizations/headerItemPosition';
2223
import HeaderSeparator from 'app/components/organizations/headerSeparator';
@@ -26,10 +27,9 @@ import MultipleProjectSelector from 'app/components/organizations/multipleProjec
2627
import SentryTypes from 'app/sentryTypes';
2728
import TimeRangeSelector from 'app/components/organizations/timeRangeSelector';
2829
import Tooltip from 'app/components/tooltip';
30+
import space from 'app/styles/space';
2931
import withGlobalSelection from 'app/utils/withGlobalSelection';
30-
import ConfigStore from 'app/stores/configStore';
3132
import withProjects from 'app/utils/withProjects';
32-
import space from 'app/styles/space';
3333

3434
import {getStateFromQuery} from './utils';
3535

@@ -75,6 +75,15 @@ class GlobalSelectionHeader extends React.Component {
7575
*/
7676
showRelative: PropTypes.bool,
7777

78+
// GlobalSelectionStore is not always initialized (e.g. Group Details) before this is rendered
79+
//
80+
// This component intentionally attempts to sync store --> URL Parameter
81+
// only when mounted, except when this prop changes.
82+
//
83+
// XXX: This comes from GlobalSelectionStore and currently does not reset,
84+
// so it happens at most once. Can add a reset as needed.
85+
forceUrlSync: PropTypes.bool,
86+
7887
// Callbacks //
7988
onChangeProjects: PropTypes.func,
8089
onUpdateProjects: PropTypes.func,
@@ -186,7 +195,7 @@ class GlobalSelectionHeader extends React.Component {
186195
return true;
187196
}
188197

189-
//update if any projects are starred or reordered
198+
// update if any projects are starred or reordered
190199
if (
191200
this.props.projects &&
192201
nextProps.projects &&
@@ -198,14 +207,39 @@ class GlobalSelectionHeader extends React.Component {
198207
return true;
199208
}
200209

210+
// Update if `forceUrlSync` changes
211+
if (this.props.forceUrlSync !== nextProps.forceUrlSync && !this.props.forceUrlSync) {
212+
return true;
213+
}
214+
201215
return false;
202216
}
203217

204218
componentDidUpdate(prevProps) {
205-
if (this.props.hasCustomRouting) {
219+
const {hasCustomRouting, location, forceUrlSync, selection} = this.props;
220+
221+
if (hasCustomRouting) {
206222
return;
207223
}
208224

225+
// Kind of gross
226+
if (forceUrlSync !== prevProps.forceUrlSync && !prevProps.forceUrlSync) {
227+
const {project, environment} = getStateFromQuery(location.query);
228+
229+
if (
230+
!isEqual(project, selection.projects) ||
231+
!isEqual(environment, selection.environments)
232+
) {
233+
updateParamsWithoutHistory(
234+
{
235+
project: selection.projects,
236+
environment: selection.environments,
237+
},
238+
this.getRouter()
239+
);
240+
}
241+
}
242+
209243
// If component has updated (e.g. due to re-render from a router action),
210244
// update store values with values from router. Router should be source of truth
211245
this.updateStoreIfChange(prevProps, this.props);

src/sentry/static/sentry/app/routes.jsx

+2
Original file line numberDiff line numberDiff line change
@@ -954,6 +954,8 @@ function routes() {
954954
}
955955
component={errorHandler(LazyLoad)}
956956
>
957+
{/* XXX: if we change the path for group details, we *must* update `OrganizationContext`.
958+
There is behavior that depends on this path and unfortunately no great way to test for this contract */}
957959
<IndexRoute
958960
componentPromise={() =>
959961
import(/* webpackChunkName: "OrganizationGroupEventDetails" */ './views/organizationGroupDetails/groupEventDetails')

src/sentry/static/sentry/app/stores/globalSelectionStore.jsx

+13-2
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,21 @@ const GlobalSelectionStore = Reflux.createStore({
4949
},
5050

5151
reset(state) {
52+
this._hasLoaded = false;
5253
this.selection = state || getDefaultSelection();
5354
},
5455

5556
/**
5657
* Initializes the global selection store
5758
* If there are query params apply these, otherwise check local storage
5859
*/
59-
loadInitialData(organization, queryParams) {
60+
loadInitialData(organization, queryParams, {forceUrlSync, onlyIfNeverLoaded} = {}) {
61+
// If this option is true, only load if it has never been loaded before
62+
if (onlyIfNeverLoaded && this._hasLoaded) {
63+
return;
64+
}
65+
66+
this._hasLoaded = true;
6067
this.organization = organization;
6168
const query = pick(queryParams, Object.values(URL_PARAM));
6269
const hasQuery = Object.keys(query).length > 0;
@@ -87,12 +94,16 @@ const GlobalSelectionStore = Reflux.createStore({
8794
globalSelection = {datetime: defaultDateTime, ...JSON.parse(storedValue)};
8895
}
8996
} catch (ex) {
97+
console.error(ex); // eslint-disable-line no-console
9098
// use default if invalid
9199
}
92100
}
93101

94102
if (isValidSelection(globalSelection, organization)) {
95-
this.selection = globalSelection;
103+
this.selection = {
104+
...globalSelection,
105+
...(forceUrlSync ? {forceUrlSync: true} : {}),
106+
};
96107
this.trigger(this.selection);
97108
}
98109
},

src/sentry/static/sentry/app/utils/withGlobalSelection.jsx

+25-5
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import React from 'react';
22
import Reflux from 'reflux';
33
import createReactClass from 'create-react-class';
44

5-
import getDisplayName from 'app/utils/getDisplayName';
65
import GlobalSelectionStore from 'app/stores/globalSelectionStore';
6+
import getDisplayName from 'app/utils/getDisplayName';
77

88
/**
99
* Higher order component that uses GlobalSelectionStore and provides the
@@ -19,13 +19,33 @@ const withGlobalSelection = WrappedComponent =>
1919
};
2020
},
2121

22+
componentDidMount() {
23+
this.updateSelection();
24+
},
25+
2226
onUpdate() {
23-
this.setState({
24-
selection: GlobalSelectionStore.get(),
25-
});
27+
this.updateSelection();
2628
},
29+
30+
updateSelection() {
31+
const selection = GlobalSelectionStore.get();
32+
33+
if (this.state.selection !== selection) {
34+
this.setState({
35+
selection,
36+
});
37+
}
38+
},
39+
2740
render() {
28-
return <WrappedComponent selection={this.state.selection} {...this.props} />;
41+
const {forceUrlSync, ...selection} = this.state.selection;
42+
return (
43+
<WrappedComponent
44+
forceUrlSync={forceUrlSync}
45+
selection={selection}
46+
{...this.props}
47+
/>
48+
);
2949
},
3050
});
3151

src/sentry/static/sentry/app/views/organizationContext.jsx

+12-1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const OrganizationContext = createReactClass({
3434

3535
propTypes: {
3636
api: PropTypes.object,
37+
routes: PropTypes.arrayOf(PropTypes.object),
3738
includeSidebar: PropTypes.bool,
3839
useLastOrganization: PropTypes.bool,
3940
organizationsLoading: PropTypes.bool,
@@ -136,7 +137,17 @@ const OrganizationContext = createReactClass({
136137

137138
TeamStore.loadInitialData(data.teams);
138139
ProjectsStore.loadInitialData(data.projects);
139-
GlobalSelectionStore.loadInitialData(data, this.props.location.query);
140+
141+
// Make an exception for issue details in the case where it is accessed directly (e.g. from email)
142+
// We do not want to load the user's last used env/project in this case, otherwise will
143+
// lead to very confusing behavior.
144+
if (
145+
!this.props.routes.find(
146+
({path}) => path && path.includes('/organizations/:orgId/issues/:groupId/')
147+
)
148+
) {
149+
GlobalSelectionStore.loadInitialData(data, this.props.location.query);
150+
}
140151
OrganizationEnvironmentsStore.loadInitialData(environments);
141152

142153
this.setState({

src/sentry/static/sentry/app/views/organizationGroupDetails/groupEventDetails.jsx

+29-11
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,26 @@
1-
import React from 'react';
2-
import PropTypes from 'prop-types';
3-
import {isEqual} from 'lodash';
41
import {browserHistory} from 'react-router';
2+
import {isEqual} from 'lodash';
3+
import PropTypes from 'prop-types';
4+
import React from 'react';
55

6-
import SentryTypes from 'app/sentryTypes';
6+
import {fetchSentryAppComponents} from 'app/actionCreators/sentryAppComponents';
77
import {withMeta} from 'app/components/events/meta/metaProxy';
88
import EventEntries from 'app/components/events/eventEntries';
9+
import GlobalSelectionStore from 'app/stores/globalSelectionStore';
910
import GroupEventDetailsLoadingError from 'app/components/errors/groupEventDetailsLoadingError';
1011
import GroupSidebar from 'app/components/group/sidebar';
1112
import LoadingIndicator from 'app/components/loadingIndicator';
12-
import ResolutionBox from 'app/components/resolutionBox';
1313
import MutedBox from 'app/components/mutedBox';
14+
import OrganizationEnvironmentsStore from 'app/stores/organizationEnvironmentsStore';
15+
import ResolutionBox from 'app/components/resolutionBox';
16+
import SentryTypes from 'app/sentryTypes';
17+
import fetchSentryAppInstallations from 'app/utils/fetchSentryAppInstallations';
1418
import withApi from 'app/utils/withApi';
15-
import withOrganization from 'app/utils/withOrganization';
1619
import withGlobalSelection from 'app/utils/withGlobalSelection';
17-
import fetchSentryAppInstallations from 'app/utils/fetchSentryAppInstallations';
18-
import {fetchSentryAppComponents} from 'app/actionCreators/sentryAppComponents';
19-
import OrganizationEnvironmentsStore from 'app/stores/organizationEnvironmentsStore';
20+
import withOrganization from 'app/utils/withOrganization';
2021

21-
import GroupEventToolbar from './eventToolbar';
2222
import {fetchGroupEventAndMarkSeen, getEventEnvironment} from './utils';
23+
import GroupEventToolbar from './eventToolbar';
2324

2425
class GroupEventDetails extends React.Component {
2526
static propTypes = {
@@ -77,7 +78,24 @@ class GroupEventDetails extends React.Component {
7778
}
7879

7980
componentWillUnmount() {
80-
this.props.api.clear();
81+
const {api, organization} = this.props;
82+
83+
// Note: We do not load global selection store with any data when this component is used
84+
// This is handled in `<OrganizationContext>` by examining the routes.
85+
//
86+
// When this view gets unmounted, attempt to load initial data so that projects/envs
87+
// gets loaded with the last used one (via local storage). `forceUrlSync` will make
88+
// `<GlobalSelectionHeader>` sync values from store to the URL (if they are different),
89+
// otherwise they can out of sync because the component only syncs in `DidMount`, and
90+
// the timing for that is not guaranteed.
91+
//
92+
// TBD: if this behavior is actually desired
93+
GlobalSelectionStore.loadInitialData(organization, this.props.location.query, {
94+
onlyIfNeverLoaded: true,
95+
forceUrlSync: true,
96+
});
97+
98+
api.clear();
8199
}
82100

83101
fetchData = () => {

src/sentry/static/sentry/app/views/organizationGroupDetails/index.jsx

+2-3
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,13 @@ class OrganizationGroupDetails extends React.Component {
2121
}
2222

2323
render() {
24-
// eslint-disable-next-line no-unused-vars
2524
const {selection, ...props} = this.props;
2625

2726
return (
2827
<GroupDetails
2928
environments={selection.environments}
30-
enableSnuba={true}
31-
showGlobalHeader={true}
29+
enableSnuba
30+
showGlobalHeader
3231
{...props}
3332
/>
3433
);

0 commit comments

Comments
 (0)