import {Component, Fragment} from 'react'; import styled from '@emotion/styled'; import isEqual from 'lodash/isEqual'; import map from 'lodash/map'; import Input from 'sentry/components/input'; import LoadingIndicator from 'sentry/components/loadingIndicator'; import { joinQuery, ParseResult, parseSearch, Token, TokenResult, } from 'sentry/components/searchSyntax/parser'; import * as SidebarSection from 'sentry/components/sidebarSection'; import {IconClose} from 'sentry/icons/iconClose'; import {t} from 'sentry/locale'; import space from 'sentry/styles/space'; import {Tag, TagCollection} from 'sentry/types'; import IssueListTagFilter from './tagFilter'; import {TagValueLoader} from './types'; type DefaultProps = { onQueryChange: (query: string) => void; query: string; tags: TagCollection; }; type Props = DefaultProps & { parsedQuery: ParseResult; tagValueLoader: TagValueLoader; loading?: boolean; }; type State = { filters: Record>; textFilter: string; }; class IssueListSidebar extends Component { static defaultProps: DefaultProps = { tags: {}, query: '', onQueryChange: function () {}, }; state: State = this.parsedQueryToState(this.props.parsedQuery); componentWillReceiveProps(nextProps: Props) { if (!isEqual(nextProps.query, this.props.query)) { this.setState(this.parsedQueryToState(nextProps.parsedQuery)); } } parsedQueryToState(parsedQuery: ParseResult): State { const parsedFilters = parsedQuery.filter( (p): p is TokenResult => p.type === Token.Filter ); return { filters: Object.fromEntries(parsedFilters.map(p => [p.key.text, p])), textFilter: joinQuery(parsedQuery.filter(p => p.type === Token.FreeText)), }; } onSelectTag = (tag: Tag, value: string | null) => { const parsedResult: TokenResult[] = ( parseSearch(`${tag.key}:${value}`) ?? [] ).filter((p): p is TokenResult => p.type === Token.Filter); if (parsedResult.length !== 1 || parsedResult[0].type !== Token.Filter) { return; } const newEntry = parsedResult[0] as TokenResult; const newFilters = {...this.state.filters}; if (value) { newFilters[tag.key] = newEntry; } else { delete newFilters[tag.key]; } this.setState( { filters: newFilters, }, this.onQueryChange ); }; onTextChange = (evt: React.ChangeEvent) => { this.setState({textFilter: evt.target.value}); }; onQueryChange = () => { const newQuery = [ joinQuery(Object.values(this.state.filters), false, true), this.state.textFilter, ] .filter(f => f) // filter out empty strings .join(' '); this.props.onQueryChange && this.props.onQueryChange(newQuery); }; onClearSearch = () => { this.setState( { textFilter: '', }, this.onQueryChange ); }; render() { const {loading, tagValueLoader, tags} = this.props; // TODO: @taylangocmen: 1. We need to render negated tags better, 2. We need an option to add negated tags to query return ( {loading ? ( ) : ( {t('Text')}
{this.state.textFilter && ( )}
{map(tags, tag => ( ))}
)}
); } } export default IssueListSidebar; const StreamSidebar = styled('div')` display: flex; flex-direction: column; width: 100%; `; const StyledIconClose = styled(IconClose)` cursor: pointer; position: absolute; top: 13px; right: 10px; color: ${p => p.theme.gray200}; &:hover { color: ${p => p.theme.gray300}; } `; const StyledHr = styled('hr')` margin: ${space(2)} 0 0; border-top: solid 1px ${p => p.theme.innerBorder}; `;