sidebar.tsx 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. import {Component, Fragment} from 'react';
  2. import styled from '@emotion/styled';
  3. import isEqual from 'lodash/isEqual';
  4. import map from 'lodash/map';
  5. import Input from 'sentry/components/forms/controls/input';
  6. import LoadingIndicator from 'sentry/components/loadingIndicator';
  7. import {
  8. ParseResult,
  9. parseSearch,
  10. Token,
  11. TokenResult,
  12. } from 'sentry/components/searchSyntax/parser';
  13. import SidebarSection from 'sentry/components/sidebarSection';
  14. import {IconClose} from 'sentry/icons/iconClose';
  15. import {t} from 'sentry/locale';
  16. import space from 'sentry/styles/space';
  17. import {Tag, TagCollection} from 'sentry/types';
  18. import {objToQuery} from 'sentry/utils/stream';
  19. import IssueListTagFilter from './tagFilter';
  20. import {TagValueLoader} from './types';
  21. type DefaultProps = {
  22. onQueryChange: (query: string) => void;
  23. query: string;
  24. tags: TagCollection;
  25. };
  26. type Props = DefaultProps & {
  27. tagValueLoader: TagValueLoader;
  28. loading?: boolean;
  29. };
  30. type State = {
  31. queryObj: Record<string, string>;
  32. textFilter: string;
  33. };
  34. class IssueListSidebar extends Component<Props, State> {
  35. static defaultProps: DefaultProps = {
  36. tags: {},
  37. query: '',
  38. onQueryChange: function () {},
  39. };
  40. state: State = this.parseQueryToState(this.props.query);
  41. componentWillReceiveProps(nextProps: Props) {
  42. // If query was updated by another source (e.g. SearchBar),
  43. // clobber state of sidebar with new query.
  44. const query = objToQuery(this.state.queryObj);
  45. if (!isEqual(nextProps.query, query)) {
  46. this.setState(this.parseQueryToState(nextProps.query));
  47. }
  48. }
  49. parseQueryToState(query: string): State {
  50. const parsedResult: ParseResult = parseSearch(query) ?? [];
  51. const textFilter = parsedResult
  52. ?.filter(p => p.type === Token.FreeText)
  53. .map(p => p.text)
  54. .join(' ');
  55. const parsedFilers = parsedResult?.filter(
  56. (p): p is TokenResult<Token.Filter> => p.type === Token.Filter
  57. );
  58. const queryObj = Object.fromEntries(
  59. parsedFilers.map((p: TokenResult<Token.Filter>) => [p.key.text, p.value.text])
  60. );
  61. return {
  62. queryObj,
  63. textFilter,
  64. };
  65. }
  66. onSelectTag = (tag: Tag, value: string | null) => {
  67. const newQuery = {...this.state.queryObj};
  68. if (value) {
  69. newQuery[tag.key] = value;
  70. } else {
  71. delete newQuery[tag.key];
  72. }
  73. this.setState(
  74. {
  75. queryObj: newQuery,
  76. },
  77. this.onQueryChange
  78. );
  79. };
  80. onTextChange = (evt: React.ChangeEvent<HTMLInputElement>) => {
  81. this.setState({textFilter: evt.target.value});
  82. };
  83. onTextFilterSubmit = (evt?: React.FormEvent<HTMLFormElement>) => {
  84. evt && evt.preventDefault();
  85. const newQueryObj = {
  86. ...this.state.queryObj,
  87. __text: this.state.textFilter,
  88. };
  89. this.setState(
  90. {
  91. queryObj: newQueryObj,
  92. },
  93. this.onQueryChange
  94. );
  95. };
  96. onQueryChange = () => {
  97. const query = objToQuery(this.state.queryObj);
  98. this.props.onQueryChange && this.props.onQueryChange(query);
  99. };
  100. onClearSearch = () => {
  101. this.setState(
  102. {
  103. textFilter: '',
  104. },
  105. this.onTextFilterSubmit
  106. );
  107. };
  108. render() {
  109. const {loading, tagValueLoader, tags} = this.props;
  110. return (
  111. <StreamSidebar>
  112. {loading ? (
  113. <LoadingIndicator />
  114. ) : (
  115. <Fragment>
  116. <SidebarSection title={t('Text')}>
  117. <form onSubmit={this.onTextFilterSubmit}>
  118. <Input
  119. placeholder={t('Search title and culprit text body')}
  120. onChange={this.onTextChange}
  121. value={this.state.textFilter}
  122. />
  123. {this.state.textFilter && (
  124. <StyledIconClose size="xs" onClick={this.onClearSearch} />
  125. )}
  126. </form>
  127. <StyledHr />
  128. </SidebarSection>
  129. {map(tags, tag => (
  130. <IssueListTagFilter
  131. value={this.state.queryObj[tag.key]}
  132. key={tag.key}
  133. tag={tag}
  134. onSelect={this.onSelectTag}
  135. tagValueLoader={tagValueLoader}
  136. />
  137. ))}
  138. </Fragment>
  139. )}
  140. </StreamSidebar>
  141. );
  142. }
  143. }
  144. export default IssueListSidebar;
  145. const StreamSidebar = styled('div')`
  146. display: flex;
  147. flex-direction: column;
  148. width: 100%;
  149. `;
  150. const StyledIconClose = styled(IconClose)`
  151. cursor: pointer;
  152. position: absolute;
  153. top: 13px;
  154. right: 10px;
  155. color: ${p => p.theme.gray200};
  156. &:hover {
  157. color: ${p => p.theme.gray300};
  158. }
  159. `;
  160. const StyledHr = styled('hr')`
  161. margin: ${space(2)} 0 0;
  162. border-top: solid 1px ${p => p.theme.innerBorder};
  163. `;