Browse Source

feat(discover): Implement saved query redesign (#10220)

Implement new design for discover saved queries. Read mode displays a
query in an inbox-like view to allow quick navigation between queries.
Treat query name as a standard field in the edit query sidebar, autocreating
the name on initial save and immediately navigating the user to edit mode.
Edit query is a slide over component.
Lyn Nagara 6 years ago
parent
commit
532ac8fc4c

+ 59 - 100
src/sentry/static/sentry/app/views/organizationDiscover/discover.jsx

@@ -1,7 +1,6 @@
 import PropTypes from 'prop-types';
 import React from 'react';
 import {browserHistory} from 'react-router';
-import {isEqual} from 'lodash';
 
 import {
   addErrorMessage,
@@ -18,8 +17,9 @@ import Result from './result';
 import Intro from './intro';
 import EarlyAdopterMessage from './earlyAdopterMessage';
 import NewQuery from './sidebar/newQuery';
-import QueryRead from './sidebar/queryRead';
+import EditSavedQuery from './sidebar/editSavedQuery';
 import SavedQueryList from './sidebar/savedQueryList';
+import QueryPanel from './sidebar/queryPanel';
 
 import createResultManager from './resultManager';
 import {
@@ -27,7 +27,6 @@ import {
   getQueryFromQueryString,
   deleteSavedQuery,
   updateSavedQuery,
-  parseSavedQuery,
 } from './utils';
 import {isValidCondition} from './conditions/utils';
 import {isValidAggregation} from './aggregations/utils';
@@ -38,11 +37,8 @@ import {
   TopBar,
   Sidebar,
   SidebarTabs,
-  SavedQueryTitle,
-  SavedQueryAction,
   PageTitle,
-  EditableName,
-  BackToQueryList,
+  SavedQueryWrapper,
 } from './styles';
 
 import {trackQuery} from './analytics';
@@ -51,9 +47,12 @@ export default class OrganizationDiscover extends React.Component {
   static propTypes = {
     organization: SentryTypes.Organization.isRequired,
     queryBuilder: PropTypes.object.isRequired,
-    savedQuery: SentryTypes.DiscoverSavedQuery, // Provided if it's a saved search
+    // savedQuery and isEditingSavedQuery are provided if it's a saved query
+    savedQuery: SentryTypes.DiscoverSavedQuery,
+    isEditingSavedQuery: PropTypes.bool,
     updateSavedQueryData: PropTypes.func.isRequired,
     view: PropTypes.oneOf(['query', 'saved']),
+    toggleEditMode: PropTypes.func.isRequired,
   };
 
   constructor(props) {
@@ -63,7 +62,7 @@ export default class OrganizationDiscover extends React.Component {
       resultManager,
       data: resultManager.getAll(),
       isFetchingQuery: false,
-      isEditingSavedQuery: false,
+      isEditingSavedQuery: props.isEditingSavedQuery,
       savedQueryName: null,
       view: props.view || 'query',
     };
@@ -76,7 +75,7 @@ export default class OrganizationDiscover extends React.Component {
   }
 
   componentWillReceiveProps(nextProps) {
-    const {queryBuilder, location: {search}, savedQuery} = nextProps;
+    const {queryBuilder, location: {search}, savedQuery, isEditingSavedQuery} = nextProps;
     const currentSearch = this.props.location.search;
     const {resultManager} = this.state;
 
@@ -85,6 +84,11 @@ export default class OrganizationDiscover extends React.Component {
       this.runQuery();
     }
 
+    if (isEditingSavedQuery !== this.props.isEditingSavedQuery) {
+      this.setState({isEditingSavedQuery});
+      return;
+    }
+
     if (currentSearch === search) {
       return;
     }
@@ -169,7 +173,7 @@ export default class OrganizationDiscover extends React.Component {
       });
   };
 
-  onFetchPage(nextOrPrev) {
+  onFetchPage = nextOrPrev => {
     this.setState({isFetchingQuery: true});
     return this.state.resultManager
       .fetchPage(nextOrPrev)
@@ -181,7 +185,7 @@ export default class OrganizationDiscover extends React.Component {
         addErrorMessage(message);
         this.setState({isFetchingQuery: false});
       });
-  }
+  };
 
   toggleSidebar = view => {
     if (view !== this.state.view) {
@@ -193,16 +197,6 @@ export default class OrganizationDiscover extends React.Component {
     }
   };
 
-  toggleEditMode = () => {
-    this.setState(state => {
-      const isEditMode = !state.isEditingSavedQuery;
-      return {
-        isEditingSavedQuery: isEditMode,
-        savedQueryName: isEditMode ? this.props.savedQuery.name : null,
-      };
-    });
-  };
-
   loadSavedQueries = () => {
     browserHistory.push({
       pathname: `/organizations/${this.props.organization.slug}/discover/`,
@@ -226,6 +220,8 @@ export default class OrganizationDiscover extends React.Component {
 
   deleteSavedQuery = () => {
     const {organization, savedQuery} = this.props;
+    const {resultManager} = this.state;
+
     deleteSavedQuery(organization, savedQuery.id)
       .then(() => {
         addSuccessMessage(
@@ -233,6 +229,7 @@ export default class OrganizationDiscover extends React.Component {
             name: savedQuery.name,
           })
         );
+        resultManager.reset();
         this.loadSavedQueries();
       })
       .catch(() => {
@@ -241,38 +238,25 @@ export default class OrganizationDiscover extends React.Component {
       });
   };
 
-  updateSavedQueryName = savedQueryName => {
-    this.setState({savedQueryName});
-  };
-
-  updateSavedQuery = () => {
-    const {queryBuilder, savedQuery, organization} = this.props;
+  updateSavedQuery = name => {
+    const {queryBuilder, savedQuery, organization, toggleEditMode} = this.props;
     const query = queryBuilder.getInternal();
-    const hasChanged =
-      !isEqual(query, parseSavedQuery(savedQuery)) ||
-      savedQuery.name !== this.state.savedQueryName;
-
-    const data = {...query, name: this.state.savedQueryName};
-
-    if (hasChanged) {
-      updateSavedQuery(organization, savedQuery.id, data)
-        .then(resp => {
-          addSuccessMessage(t('Updated query'));
-          this.toggleEditMode(); // Return to read-only mode
-          this.props.updateSavedQueryData(resp);
-          this.runQuery();
-        })
-        .catch(() => {
-          addErrorMessage(t('Could not update query'));
-        });
-    } else {
-      this.toggleEditMode(); // Return to read-only mode
-    }
+
+    const data = {...query, name};
+
+    updateSavedQuery(organization, savedQuery.id, data)
+      .then(resp => {
+        addSuccessMessage(t('Updated query'));
+        toggleEditMode(); // Return to read-only mode
+        this.props.updateSavedQueryData(resp);
+      })
+      .catch(() => {
+        addErrorMessage(t('Could not update query'));
+      });
   };
 
   renderSidebarNav() {
-    const {view, isEditingSavedQuery} = this.state;
-    const {savedQuery} = this.props;
+    const {view} = this.state;
     const views = [
       {id: 'query', title: t('Query')},
       {id: 'saved', title: t('Saved queries')},
@@ -287,55 +271,18 @@ export default class OrganizationDiscover extends React.Component {
             </li>
           ))}
         </SidebarTabs>
-        {savedQuery && (
-          <React.Fragment>
-            <BackToQueryList>
-              <a onClick={this.loadSavedQueries}>
-                {tct('[arr] Back to saved query list', {arr: '←'})}
-              </a>
-            </BackToQueryList>
-            <SavedQueryTitle>
-              {!isEditingSavedQuery && (
-                <React.Fragment>
-                  {savedQuery.name}
-                  <SavedQueryAction onClick={this.toggleEditMode}>
-                    {t('Edit query')}
-                  </SavedQueryAction>
-                </React.Fragment>
-              )}
-              {isEditingSavedQuery && (
-                <React.Fragment>
-                  <EditableName
-                    value={savedQuery.name}
-                    onChange={this.updateSavedQueryName}
-                  />
-                  <SavedQueryAction onClick={this.updateSavedQuery}>
-                    {t('Save changes')}
-                  </SavedQueryAction>
-                  <SavedQueryAction onClick={this.deleteSavedQuery}>
-                    {t('Delete')}
-                  </SavedQueryAction>
-                </React.Fragment>
-              )}
-            </SavedQueryTitle>
-          </React.Fragment>
-        )}
       </React.Fragment>
     );
   }
 
   render() {
     const {data, isFetchingQuery, view, resultManager, isEditingSavedQuery} = this.state;
-    const {queryBuilder, organization, savedQuery} = this.props;
+
+    const {queryBuilder, organization, savedQuery, toggleEditMode} = this.props;
 
     const currentQuery = queryBuilder.getInternal();
 
     const shouldDisplayResult = resultManager.shouldDisplayResult();
-    const shouldRenderSavedList = view === 'saved' && !savedQuery;
-    const shouldRenderReadMode = view === 'saved' && savedQuery && !isEditingSavedQuery;
-    const shouldRenderEditMode =
-      (view === 'saved' && savedQuery && isEditingSavedQuery) ||
-      (view === 'query' && !savedQuery);
 
     const projects = organization.projects.filter(project => project.isMember);
 
@@ -344,15 +291,14 @@ export default class OrganizationDiscover extends React.Component {
         <Sidebar>
           <PageTitle>{t('Discover')}</PageTitle>
           {this.renderSidebarNav()}
-          {shouldRenderReadMode && (
-            <QueryRead
-              queryBuilder={queryBuilder}
-              isFetchingQuery={isFetchingQuery}
-              onRunQuery={this.runQuery}
-            />
+          {view === 'saved' && (
+            <SavedQueryWrapper isEditing={isEditingSavedQuery}>
+              <SavedQueryList organization={organization} savedQuery={savedQuery} />
+            </SavedQueryWrapper>
           )}
-          {shouldRenderEditMode && (
+          {view === 'query' && (
             <NewQuery
+              organization={organization}
               queryBuilder={queryBuilder}
               isFetchingQuery={isFetchingQuery}
               onUpdateField={this.updateField}
@@ -360,7 +306,21 @@ export default class OrganizationDiscover extends React.Component {
               onReset={this.reset}
             />
           )}
-          {shouldRenderSavedList && <SavedQueryList organization={organization} />}
+          {isEditingSavedQuery &&
+            savedQuery && (
+              <QueryPanel title={t('Edit Query')} onClose={toggleEditMode}>
+                <EditSavedQuery
+                  savedQuery={savedQuery}
+                  queryBuilder={queryBuilder}
+                  isFetchingQuery={isFetchingQuery}
+                  onUpdateField={this.updateField}
+                  onRunQuery={this.runQuery}
+                  onReset={this.reset}
+                  onDeleteQuery={this.deleteSavedQuery}
+                  onSaveQuery={this.updateSavedQuery}
+                />
+              </QueryPanel>
+            )}
         </Sidebar>
         <Body>
           <TopBar>
@@ -385,10 +345,9 @@ export default class OrganizationDiscover extends React.Component {
             {shouldDisplayResult && (
               <Result
                 data={data}
-                organization={organization}
                 savedQuery={savedQuery}
-                queryBuilder={queryBuilder}
-                onFetchPage={this.onFetchPage.bind(this)}
+                onToggleEdit={toggleEditMode}
+                onFetchPage={this.onFetchPage}
               />
             )}
             {!shouldDisplayResult && <Intro updateQuery={this.updateFields} />}

+ 21 - 0
src/sentry/static/sentry/app/views/organizationDiscover/index.jsx

@@ -101,6 +101,24 @@ const OrganizationDiscoverContainer = createReactClass({
     this.setState({savedQuery});
   },
 
+  toggleEditMode: function() {
+    const {organization} = this.context;
+    const {savedQuery} = this.state;
+    const isEditingSavedQuery = this.props.location.query.editing === 'true';
+
+    const newQuery = {...this.props.location.query};
+    if (!isEditingSavedQuery) {
+      newQuery.editing = 'true';
+    } else {
+      delete newQuery.editing;
+    }
+
+    browserHistory.push({
+      pathname: `/organizations/${organization.slug}/discover/saved/${savedQuery.id}/`,
+      query: newQuery,
+    });
+  },
+
   renderComingSoon: function() {
     return (
       <Flex className="organization-home" justify="center" align="center">
@@ -127,6 +145,7 @@ const OrganizationDiscoverContainer = createReactClass({
 
   render() {
     const {isLoading, savedQuery, view} = this.state;
+
     const {location, params} = this.props;
     const hasFeature = this.getFeatures().has('discover');
 
@@ -143,8 +162,10 @@ const OrganizationDiscoverContainer = createReactClass({
             location={location}
             params={params}
             savedQuery={savedQuery}
+            isEditingSavedQuery={this.props.location.query.editing === 'true'}
             updateSavedQueryData={this.updateSavedQuery}
             view={view}
+            toggleEditMode={this.toggleEditMode}
           />
         )}
       </DiscoverWrapper>

+ 13 - 79
src/sentry/static/sentry/app/views/organizationDiscover/result/index.jsx

@@ -1,25 +1,21 @@
 import React from 'react';
-import {browserHistory} from 'react-router';
 import PropTypes from 'prop-types';
 import classNames from 'classnames';
 import {Box, Flex} from 'grid-emotion';
 
 import SentryTypes from 'app/sentryTypes';
-import {t, tct} from 'app/locale';
+import {t} from 'app/locale';
 import Link from 'app/components/link';
 import BarChart from 'app/components/charts/barChart';
 import LineChart from 'app/components/charts/lineChart';
 import space from 'app/styles/space';
+import InlineSvg from 'app/components/inlineSvg';
 
-import {addSuccessMessage, addErrorMessage} from 'app/actionCreators/indicator';
-
-import {getChartData, getChartDataByDay, downloadAsCsv, generateQueryName} from './utils';
-import {createSavedQuery} from '../utils';
-import Pagination from './pagination';
+import {getChartData, getChartDataByDay, downloadAsCsv} from './utils';
 import Table from './table';
+import Pagination from './pagination';
 import {
   Heading,
-  EditableName,
   ResultSummary,
   ChartWrapper,
   ChartNote,
@@ -29,19 +25,16 @@ import {NUMBER_OF_SERIES_BY_DAY} from '../data';
 
 export default class Result extends React.Component {
   static propTypes = {
-    organization: SentryTypes.Organization.isRequired,
     data: PropTypes.object.isRequired,
-    queryBuilder: PropTypes.object.isRequired,
     savedQuery: SentryTypes.DiscoverSavedQuery, // Provided if it's a saved search
     onFetchPage: PropTypes.func.isRequired,
+    onToggleEdit: PropTypes.func,
   };
 
   constructor() {
     super();
     this.state = {
       view: 'table',
-      isEditMode: false,
-      savedQueryName: null,
     };
   }
 
@@ -64,48 +57,10 @@ export default class Result extends React.Component {
     }
 
     this.setState({
-      isEditMode: false,
       savedQueryName: null,
     });
   }
 
-  toggleEditMode = () => {
-    const {savedQuery} = this.props;
-    this.setState(state => {
-      const isEditMode = !state.isEditMode;
-      return {
-        isEditMode,
-        savedQueryName: isEditMode
-          ? savedQuery ? savedQuery.name : generateQueryName()
-          : null,
-      };
-    });
-  };
-
-  confirmSave = () => {
-    const {organization, queryBuilder} = this.props;
-    const {savedQueryName} = this.state;
-    const data = {...queryBuilder.getInternal(), name: savedQueryName};
-
-    createSavedQuery(organization, data)
-      .then(savedQuery => {
-        addSuccessMessage(
-          tct('Successfully saved query [name]', {name: savedQuery.name})
-        );
-        browserHistory.push({
-          pathname: `/organizations/${organization.slug}/discover/saved/${savedQuery.id}/`,
-        });
-      })
-      .catch(err => {
-        const message = (err && err.detail) || t('Could not save query');
-        addErrorMessage(message);
-      });
-  };
-
-  updateSavedQueryName = val => {
-    this.setState({savedQueryName: val});
-  };
-
   renderToggle() {
     const {baseQuery, byDayQuery} = this.props.data;
 
@@ -173,46 +128,25 @@ export default class Result extends React.Component {
 
   renderSavedQueryHeader() {
     return (
-      <Flex>
+      <Flex align="center">
         <Heading>{this.props.savedQuery.name}</Heading>
+        <SavedQueryAction onClick={this.props.onToggleEdit}>
+          <InlineSvg src="icon-edit" />
+        </SavedQueryAction>
       </Flex>
     );
   }
 
   renderQueryResultHeader() {
-    const {isEditMode, savedQueryName} = this.state;
-
     return (
-      <React.Fragment>
-        {!isEditMode && (
-          <Flex>
-            <Heading>{t('Result')}</Heading>
-            <SavedQueryAction data-test-id="save" onClick={this.toggleEditMode}>
-              {t('Save')}
-            </SavedQueryAction>
-          </Flex>
-        )}
-        {isEditMode && (
-          <Flex>
-            <EditableName value={savedQueryName} onChange={this.updateSavedQueryName} />
-            <SavedQueryAction data-test-id="confirm" onClick={this.confirmSave}>
-              {t('Confirm save')}
-            </SavedQueryAction>
-            <SavedQueryAction data-test-id="cancel" onClick={this.toggleEditMode}>
-              {t('Cancel')}
-            </SavedQueryAction>
-          </Flex>
-        )}
-      </React.Fragment>
+      <Flex>
+        <Heading>{t('Result')}</Heading>
+      </Flex>
     );
   }
 
   render() {
-    const {
-      data: {baseQuery, byDayQuery},
-      savedQuery,
-      onFetchPage,
-    } = this.props;
+    const {data: {baseQuery, byDayQuery}, savedQuery, onFetchPage} = this.props;
 
     const {view} = this.state;
 

+ 0 - 9
src/sentry/static/sentry/app/views/organizationDiscover/result/utils.jsx

@@ -254,12 +254,3 @@ function disableMacros(value) {
 
   return value;
 }
-
-/**
- * Generate a saved query name based on the current timestamp
- *
- * @returns {String}
- */
-export function generateQueryName() {
-  return `Result - ${moment.utc().format('MMM DD HH:mm:ss')}`;
-}

+ 100 - 0
src/sentry/static/sentry/app/views/organizationDiscover/sidebar/editSavedQuery.jsx

@@ -0,0 +1,100 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import {Flex, Box} from 'grid-emotion';
+import {isEqual} from 'lodash';
+
+import SentryTypes from 'app/sentryTypes';
+import Button from 'app/components/button';
+import {t} from 'app/locale';
+import InlineSvg from 'app/components/inlineSvg';
+
+import QueryFields from './queryFields';
+import {parseSavedQuery} from '../utils';
+import {ButtonSpinner, SavedQueryAction} from '../styles';
+
+export default class EditSavedQuery extends React.Component {
+  static propTypes = {
+    queryBuilder: PropTypes.object.isRequired,
+    onRunQuery: PropTypes.func.isRequired,
+    savedQuery: SentryTypes.DiscoverSavedQuery,
+    onUpdateField: PropTypes.func.isRequired,
+    onDeleteQuery: PropTypes.func.isRequired,
+    onSaveQuery: PropTypes.func.isRequired,
+    isFetchingQuery: PropTypes.bool.isRequired,
+  };
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      savedQueryName: props.savedQuery.name,
+    };
+  }
+
+  handleUpdateName(savedQueryName) {
+    this.setState({savedQueryName});
+  }
+
+  hasChanges() {
+    const {queryBuilder, savedQuery} = this.props;
+
+    const hasChanged =
+      !isEqual(parseSavedQuery(savedQuery), queryBuilder.getInternal()) ||
+      this.state.savedQueryName !== savedQuery.name;
+    return hasChanged;
+  }
+
+  render() {
+    const {
+      queryBuilder,
+      savedQuery,
+      isFetchingQuery,
+      onUpdateField,
+      onRunQuery,
+      onDeleteQuery,
+      onSaveQuery,
+    } = this.props;
+
+    const {savedQueryName} = this.state;
+
+    return (
+      <QueryFields
+        queryBuilder={queryBuilder}
+        onUpdateField={onUpdateField}
+        savedQuery={savedQuery}
+        savedQueryName={this.state.savedQueryName}
+        onUpdateName={name => this.handleUpdateName(name)}
+        actions={
+          <Flex justify="space-between">
+            <Flex>
+              <Box mr={1}>
+                <Button
+                  size="xsmall"
+                  onClick={onRunQuery}
+                  priority="primary"
+                  busy={isFetchingQuery}
+                >
+                  {t('Run')}
+                  {isFetchingQuery && <ButtonSpinner />}
+                </Button>
+              </Box>
+              <Box>
+                <Button
+                  size="xsmall"
+                  onClick={() => onSaveQuery(savedQueryName)}
+                  disabled={!this.hasChanges()}
+                >
+                  {t('Save')}
+                </Button>
+              </Box>
+            </Flex>
+            <Box>
+              <SavedQueryAction data-test-id="delete" onClick={onDeleteQuery}>
+                <InlineSvg src="icon-trash" />
+              </SavedQueryAction>
+            </Box>
+          </Flex>
+        }
+      />
+    );
+  }
+}

+ 55 - 15
src/sentry/static/sentry/app/views/organizationDiscover/sidebar/newQuery.jsx

@@ -1,38 +1,78 @@
 import PropTypes from 'prop-types';
 import React from 'react';
 import {Flex, Box} from 'grid-emotion';
+import {browserHistory} from 'react-router';
 
 import Button from 'app/components/button';
-import {t} from 'app/locale';
+import {t, tct} from 'app/locale';
+import {addSuccessMessage, addErrorMessage} from 'app/actionCreators/indicator';
+import SentryTypes from 'app/sentryTypes';
 
 import QueryFields from './queryFields';
+import {createSavedQuery, generateQueryName} from '../utils';
 import {ButtonSpinner} from '../styles';
 
 export default class NewQuery extends React.Component {
   static propTypes = {
+    organization: SentryTypes.Organization,
+    queryBuilder: PropTypes.object.isRequired,
     onRunQuery: PropTypes.func.isRequired,
     onReset: PropTypes.func.isRequired,
+    onUpdateField: PropTypes.func.isRequired,
     isFetchingQuery: PropTypes.bool.isRequired,
   };
 
+  saveQuery() {
+    const {organization, queryBuilder} = this.props;
+    const savedQueryName = generateQueryName();
+    const data = {...queryBuilder.getInternal(), name: savedQueryName};
+
+    createSavedQuery(organization, data)
+      .then(savedQuery => {
+        addSuccessMessage(tct('Successfully saved query [name]', {name: savedQueryName}));
+        browserHistory.push({
+          pathname: `/organizations/${organization.slug}/discover/saved/${savedQuery.id}/`,
+          query: {editing: true},
+        });
+      })
+      .catch(err => {
+        const message = (err && err.detail) || t('Could not save query');
+        addErrorMessage(message);
+      });
+  }
+
   render() {
-    const {onRunQuery, onReset, isFetchingQuery, ...props} = this.props;
+    const {
+      queryBuilder,
+      onRunQuery,
+      onReset,
+      isFetchingQuery,
+      onUpdateField,
+    } = this.props;
     return (
       <QueryFields
-        {...props}
+        queryBuilder={queryBuilder}
+        onUpdateField={onUpdateField}
         actions={
-          <Flex>
-            <Box mr={1}>
-              <Button
-                size="xsmall"
-                onClick={onRunQuery}
-                priority="primary"
-                busy={isFetchingQuery}
-              >
-                {t('Run Query')}
-                {isFetchingQuery && <ButtonSpinner />}
-              </Button>
-            </Box>
+          <Flex justify="space-between">
+            <Flex>
+              <Box mr={1}>
+                <Button
+                  size="xsmall"
+                  onClick={onRunQuery}
+                  priority="primary"
+                  busy={isFetchingQuery}
+                >
+                  {t('Run')}
+                  {isFetchingQuery && <ButtonSpinner />}
+                </Button>
+              </Box>
+              <Box>
+                <Button size="xsmall" onClick={() => this.saveQuery()}>
+                  {t('Save')}
+                </Button>
+              </Box>
+            </Flex>
             <Box>
               <Button size="xsmall" onClick={onReset}>
                 {t('Reset')}

+ 29 - 1
src/sentry/static/sentry/app/views/organizationDiscover/sidebar/queryFields.jsx

@@ -1,7 +1,9 @@
 import PropTypes from 'prop-types';
 import React from 'react';
 
+import SentryTypes from 'app/sentryTypes';
 import {t} from 'app/locale';
+import TextField from 'app/components/forms/textField';
 import NumberField from 'app/components/forms/numberField';
 import SelectControl from 'app/components/forms/selectControl';
 
@@ -15,6 +17,10 @@ export default class QueryFields extends React.Component {
     queryBuilder: PropTypes.object.isRequired,
     onUpdateField: PropTypes.func.isRequired,
     actions: PropTypes.node.isRequired,
+    // savedQuery, savedQueryName, and onUpdateName are provided only when it's a saved search
+    savedQuery: SentryTypes.DiscoverSavedQuery,
+    savedQueryName: PropTypes.string,
+    onUpdateName: PropTypes.func,
   };
 
   getSummarizePlaceholder = () => {
@@ -28,7 +34,14 @@ export default class QueryFields extends React.Component {
   };
 
   render() {
-    const {queryBuilder, onUpdateField, actions} = this.props;
+    const {
+      queryBuilder,
+      onUpdateField,
+      actions,
+      savedQuery,
+      savedQueryName,
+      onUpdateName,
+    } = this.props;
 
     const currentQuery = queryBuilder.getInternal();
     const columns = queryBuilder.getColumns();
@@ -44,6 +57,21 @@ export default class QueryFields extends React.Component {
 
     return (
       <React.Fragment>
+        {savedQuery && (
+          <Fieldset>
+            <React.Fragment>
+              <SidebarLabel htmlFor="name" className="control-label">
+                {t('Name')}
+              </SidebarLabel>
+              <TextField
+                name="name"
+                value={savedQueryName}
+                placeholder={t('Saved search name')}
+                onChange={val => onUpdateName(val)}
+              />
+            </React.Fragment>
+          </Fieldset>
+        )}
         <Fieldset>
           <SidebarLabel htmlFor="fields" className="control-label">
             {t('Summarize')}

+ 33 - 0
src/sentry/static/sentry/app/views/organizationDiscover/sidebar/queryPanel.jsx

@@ -0,0 +1,33 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import InlineSvg from 'app/components/inlineSvg';
+
+import {
+  QueryPanelContainer,
+  QueryPanelTitle,
+  QueryPanelCloseLink,
+  Heading,
+} from '../styles';
+
+export default class QueryPanel extends React.Component {
+  static propTypes = {
+    title: PropTypes.node.isRequired,
+    onClose: PropTypes.func.isRequired,
+  };
+  render() {
+    const {title, onClose} = this.props;
+    return (
+      <QueryPanelContainer>
+        <QueryPanelTitle>
+          <Heading>{title}</Heading>
+
+          <QueryPanelCloseLink onClick={onClose}>
+            <InlineSvg src="icon-close" height="38px" />
+          </QueryPanelCloseLink>
+        </QueryPanelTitle>
+        {this.props.children}
+      </QueryPanelContainer>
+    );
+  }
+}

+ 0 - 93
src/sentry/static/sentry/app/views/organizationDiscover/sidebar/queryRead.jsx

@@ -1,93 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import {Flex} from 'grid-emotion';
-
-import {t} from 'app/locale';
-import Button from 'app/components/button';
-import * as conditionsUtils from '../conditions/utils';
-import * as aggregationsUtils from '../aggregations/utils';
-
-import {Fieldset, PlaceholderText, SidebarLabel, ButtonSpinner} from '../styles';
-
-export default class QueryRead extends React.Component {
-  static propTypes = {
-    queryBuilder: PropTypes.object.isRequired,
-    isFetchingQuery: PropTypes.bool.isRequired,
-    onRunQuery: PropTypes.func.isRequired,
-  };
-
-  renderAggregations(aggregations) {
-    if (!aggregations.length) {
-      return <PlaceholderText>{t('No aggregations selected')}</PlaceholderText>;
-    }
-
-    return aggregations.map((aggregation, index) => (
-      <PlaceholderText key={index}>
-        {aggregationsUtils.getInternal(aggregation)}
-      </PlaceholderText>
-    ));
-  }
-
-  renderConditions(conditions) {
-    if (!conditions.length) {
-      return <PlaceholderText>{t('No conditions selected')}</PlaceholderText>;
-    }
-
-    return conditions.map((condition, index) => (
-      <PlaceholderText key={index}>
-        {conditionsUtils.getInternal(condition)}
-      </PlaceholderText>
-    ));
-  }
-
-  render() {
-    const {queryBuilder, onRunQuery, isFetchingQuery} = this.props;
-
-    const currentQuery = queryBuilder.getInternal();
-
-    return (
-      <React.Fragment>
-        <Fieldset>
-          <SidebarLabel className="control-label">{t('Summarize')}</SidebarLabel>
-          <PlaceholderText>
-            {currentQuery.fields.join(', ') || t('No fields selected')}
-          </PlaceholderText>
-        </Fieldset>
-        <Fieldset>
-          <SidebarLabel className="control-label">{t('Aggregations')}</SidebarLabel>
-          {this.renderAggregations(currentQuery.aggregations)}
-        </Fieldset>
-        <Fieldset>
-          <SidebarLabel className="control-label">{t('Conditions')}</SidebarLabel>
-          {this.renderConditions(currentQuery.conditions)}
-        </Fieldset>
-        <Fieldset>
-          <SidebarLabel className="control-label">{t('Order by')}</SidebarLabel>
-          <PlaceholderText>
-            {currentQuery.orderby || t('No orderby value selected')}
-          </PlaceholderText>
-        </Fieldset>
-        <Fieldset>
-          <SidebarLabel className="control-label">{t('Limit')}</SidebarLabel>
-          <PlaceholderText>
-            {currentQuery.limit || t('No limit provided')}
-          </PlaceholderText>
-        </Fieldset>
-
-        <Fieldset>
-          <Flex>
-            <Button
-              size="xsmall"
-              onClick={onRunQuery}
-              priority="primary"
-              busy={isFetchingQuery}
-            >
-              {t('Run')}
-              {isFetchingQuery && <ButtonSpinner />}
-            </Button>
-          </Flex>
-        </Fieldset>
-      </React.Fragment>
-    );
-  }
-}

+ 57 - 26
src/sentry/static/sentry/app/views/organizationDiscover/sidebar/savedQueryList.jsx

@@ -10,7 +10,6 @@ import {t, tct} from 'app/locale';
 import {fetchSavedQueries} from '../utils';
 import {
   Fieldset,
-  SavedQuery,
   SavedQueryList,
   SavedQueryListItem,
   SavedQueryLink,
@@ -20,14 +19,37 @@ import {
 export default class SavedQueries extends React.Component {
   static propTypes = {
     organization: SentryTypes.Organization.isRequired,
+    // provided if it's a saved query
+    savedQuery: SentryTypes.DiscoverSavedQuery,
   };
 
-  constructor() {
-    super();
-    this.state = {isLoading: true, data: []};
+  constructor(props) {
+    super(props);
+    this.state = {isLoading: true, data: [], topSavedQuery: props.savedQuery};
   }
 
   componentDidMount() {
+    this.fetchAll();
+  }
+
+  componentWillReceiveProps(nextProps) {
+    // Refetch on deletion
+    if (!nextProps.savedQuery && this.props.savedQuery !== nextProps.savedQuery) {
+      this.fetchAll();
+    }
+    // Update saved query if any name / details have been updated
+    if (
+      nextProps.savedQuery &&
+      this.state.topSavedQuery &&
+      nextProps.savedQuery.id === this.state.topSavedQuery.id
+    ) {
+      this.setState({
+        topSavedQuery: nextProps.savedQuery,
+      });
+    }
+  }
+
+  fetchAll() {
     fetchSavedQueries(this.props.organization)
       .then(data => {
         this.setState({isLoading: false, data});
@@ -51,38 +73,47 @@ export default class SavedQueries extends React.Component {
     return <Fieldset>{t('No saved queries')}</Fieldset>;
   }
 
-  renderList() {
+  renderListItem(query) {
+    const {savedQuery} = this.props;
+
+    const {id, name, dateUpdated} = query;
     const {organization} = this.props;
-    const {data} = this.state;
+    return (
+      <SavedQueryListItem key={id} isActive={savedQuery && savedQuery.id === id}>
+        <SavedQueryLink to={`/organizations/${organization.slug}/discover/saved/${id}/`}>
+          {name}
+        </SavedQueryLink>
+        <SavedQueryUpdated>
+          {tct('Updated [date] (UTC)', {
+            date: moment.utc(dateUpdated).format('MMM DD HH:mm:ss'),
+          })}
+        </SavedQueryUpdated>
+      </SavedQueryListItem>
+    );
+  }
+
+  renderList() {
+    const {data, topSavedQuery} = this.state;
+
+    const savedQueryId = topSavedQuery ? topSavedQuery.id : null;
 
     if (!data.length) {
       return this.renderEmpty();
     }
 
-    return (
-      <SavedQueryList>
-        {data.map(({id, name, dateUpdated}) => (
-          <SavedQueryListItem key={id}>
-            <SavedQueryLink
-              to={`/organizations/${organization.slug}/discover/saved/${id}/`}
-            >
-              {name}
-            </SavedQueryLink>
-            <SavedQueryUpdated>
-              {tct('Updated [date] (UTC)', {
-                date: moment.utc(dateUpdated).format('MMM DD HH:mm:ss'),
-              })}
-            </SavedQueryUpdated>
-          </SavedQueryListItem>
-        ))}
-      </SavedQueryList>
-    );
+    return data.map(query => {
+      return query.id !== savedQueryId ? this.renderListItem(query) : null;
+    });
   }
 
   render() {
-    const {isLoading} = this.state;
+    const {topSavedQuery, isLoading} = this.state;
+
     return (
-      <SavedQuery>{isLoading ? this.renderLoading() : this.renderList()}</SavedQuery>
+      <SavedQueryList>
+        {topSavedQuery && this.renderListItem(topSavedQuery)}
+        {isLoading ? this.renderLoading() : this.renderList()}
+      </SavedQueryList>
     );
   }
 }

Some files were not shown because too many files changed in this diff