diff --git a/src/sentry/static/sentry/app/stores/environmentStore.jsx b/src/sentry/static/sentry/app/stores/environmentStore.jsx index c1e431a58a6f13..7cb16dac683faa 100644 --- a/src/sentry/static/sentry/app/stores/environmentStore.jsx +++ b/src/sentry/static/sentry/app/stores/environmentStore.jsx @@ -65,12 +65,7 @@ const EnvironmentStore = Reflux.createStore({ let prodEnvs = allEnvs.filter(e => PRODUCTION_ENV_NAMES.has(e.name)); - return ( - defaultEnv || - (prodEnvs.length && prodEnvs[0]) || - (allEnvs.length && allEnvs[0]) || - null - ); + return defaultEnv || (prodEnvs.length && prodEnvs[0]) || null; }, }); diff --git a/src/sentry/static/sentry/app/views/stream.jsx b/src/sentry/static/sentry/app/views/stream.jsx index ab4d202bf9dc68..b6e3011f1b987d 100644 --- a/src/sentry/static/sentry/app/views/stream.jsx +++ b/src/sentry/static/sentry/app/views/stream.jsx @@ -10,6 +10,9 @@ import _ from 'lodash'; import ApiMixin from '../mixins/apiMixin'; import GroupStore from '../stores/groupStore'; +import LatestContextStore from '../stores/latestContextStore'; +import StreamTagStore from '../stores/streamTagStore'; +import EnvironmentStore from '../stores/environmentStore'; import LoadingError from '../components/loadingError'; import LoadingIndicator from '../components/loadingIndicator'; import ProjectState from '../mixins/projectState'; @@ -18,7 +21,6 @@ import StreamGroup from '../components/stream/group'; import StreamActions from './stream/actions'; import StreamTagActions from '../actions/streamTagActions'; import AlertActions from '../actions/alertActions'; -import StreamTagStore from '../stores/streamTagStore'; import StreamFilters from './stream/filters'; import StreamSidebar from './stream/sidebar'; import TimeSince from '../components/timeSince'; @@ -27,6 +29,10 @@ import {logAjaxError} from '../utils/logging'; import parseLinkHeader from '../utils/parseLinkHeader'; import {t, tn, tct} from '../locale'; +import {objToQuery, queryToObj} from '../utils/stream'; + +import {setActiveEnvironment} from '../actionCreators/environments'; + const MAX_TAGS = 500; const Stream = createReactClass({ @@ -42,6 +48,7 @@ const Stream = createReactClass({ mixins: [ Reflux.listenTo(GroupStore, 'onGroupChange'), Reflux.listenTo(StreamTagStore, 'onStreamTagChange'), + Reflux.listenTo(LatestContextStore, 'onLatestContextChange'), ApiMixin, ProjectState, ], @@ -63,6 +70,10 @@ const Stream = createReactClass({ ? project && !project.firstEvent : realtimeActiveCookie === 'true'; + let hasEnvironmentsFeature = new Set(this.getOrganization().features).has( + 'environments' + ); + return { groupIds: [], isDefaultSearch: null, @@ -86,6 +97,11 @@ const Stream = createReactClass({ tagsLoading: true, isSidebarVisible: false, processingIssues: null, + activeEnvironment: hasEnvironmentsFeature + ? LatestContextStore.getInitialState().environment + : null, + // TODO(lyn): remove when feature is rolled out + hasEnvironmentsFeature, ...this.getQueryState(), }; }, @@ -101,6 +117,9 @@ const Stream = createReactClass({ this.fetchSavedSearches(); this.fetchProcessingIssues(); this.fetchTags(); + + // Make sure it gets called on mount + this.onLatestContextChange(LatestContextStore.getInitialState()); }, componentWillReceiveProps(nextProps) { @@ -109,15 +128,18 @@ const Stream = createReactClass({ } // Do not make new API request if props haven't actually changed - if (!_.isEqual(this.props, nextProps)) { + // Unless no request has been performed yet + if (!_.isEqual(this.props, nextProps) || !this.lastRequest) { this.fetchData(); } // you cannot apply both a query and a saved search (our routes do not // support it), so the searchId takes priority + let nextSearchId = nextProps.params.searchId || null; + let searchIdChanged = this.state.isDefaultSearch - ? nextProps.params.searchId - : nextProps.params.searchId !== this.state.searchId; + ? nextSearchId + : nextSearchId !== this.state.searchId; if (searchIdChanged || nextProps.location.search !== this.props.location.search) { // TODO(dcramer): handle 404 from popState on searchId @@ -159,8 +181,8 @@ const Stream = createReactClass({ savedSearchList: data, loading: false, }; - let needsData = this.state.loading; let searchId = this.state.searchId; + let needsData = this.state.loading; if (searchId) { let match = data.filter(search => { return search.id === searchId; @@ -182,17 +204,20 @@ const Stream = createReactClass({ let defaultResults = data.filter(search => { return search.isUserDefault; }); + if (!defaultResults.length) { defaultResults = data.filter(search => { return search.isDefault; }); } + if (defaultResults.length) { newState.searchId = defaultResults[0].id; newState.query = defaultResults[0].query; newState.isDefaultSearch = true; } } + return void this.setState(newState, needsData ? this.fetchData : null); }, error: error => { @@ -345,14 +370,40 @@ const Stream = createReactClass({ let url = this.getGroupListEndpoint(); + // Remove leading and trailing whitespace + let query = this.state.query.replace(/^\s+|\s+$/g, ''); + + let activeEnvironment = this.state.activeEnvironment; + let activeEnvName = activeEnvironment ? activeEnvironment.name : null; + let requestParams = { - query: this.state.query.replace(/^\s+|\s+$/g, ''), + query, limit: this.props.maxItems, sort: this.state.sort, statsPeriod: this.state.statsPeriod, shortIdLookup: '1', }; + // Always keep the global active environment in sync with the queried environment + // The global environment wins unless there one is specified by the saved search + let queryObj = queryToObj(query); + if ('environment' in queryObj) { + // Set the global environment to the one specified by the saved search + if (queryObj.environment !== activeEnvName) { + let env = EnvironmentStore.getByName(queryObj.environment); + setActiveEnvironment(env); + } + requestParams.environment = queryObj.environment; + } else if (activeEnvironment) { + // Set the environment of the query to match the global settings + query = objToQuery({ + ...queryObj, + environment: activeEnvironment.name, + }); + requestParams.query = query; + requestParams.environment = activeEnvironment.name; + } + let currentQuery = this.props.location.query || {}; if ('cursor' in currentQuery) { requestParams.cursor = currentQuery.cursor; @@ -392,6 +443,7 @@ const Stream = createReactClass({ return void this.setState({ error: false, dataLoading: false, + query, queryCount: typeof queryCount !== 'undefined' ? parseInt(queryCount, 10) || 0 : 0, queryMaxCount: @@ -478,6 +530,39 @@ const Stream = createReactClass({ }); }, + onLatestContextChange(context) { + if (this.state.hasEnvironmentsFeature) { + // Always query the currently active environment selection unless + // the environment parameter is part of the saved search + let environment = context.environment; + + let query = this.state.query; + + if (environment) { + // Environment has changed: append the new environment to the query + query = objToQuery({ + ...queryToObj(this.state.query), + environment: environment.name, + }); + } else { + // Environment is null, remove it from the query + let queryObj = queryToObj(this.state.query); + delete queryObj.environment; + query = objToQuery({ + ...queryObj, + }); + } + + this.setState( + { + activeEnvironment: environment, + query, + }, + this.fetchData + ); + } + }, + onSearch(query) { if (query === this.state.query) { // if query is the same, just re-fetch data @@ -534,6 +619,7 @@ const Stream = createReactClass({ } let params = this.props.params; + let path = this.state.searchId ? `/${params.orgId}/${params.projectId}/searches/${this.state.searchId}/` : `/${params.orgId}/${params.projectId}/`; @@ -635,11 +721,7 @@ const Stream = createReactClass({ /> ); }); - return ( -