import {Component} from 'react'; import {browserHistory} from 'react-router'; import {Location} from 'history'; import {Client, RequestOptions} from 'sentry/api'; import DropdownLink from 'sentry/components/dropdownLink'; import MenuItem from 'sentry/components/menuItem'; import Pagination from 'sentry/components/pagination'; import {IconSearch} from 'sentry/icons'; import withApi from 'sentry/utils/withApi'; type Option = [value: string, label: string]; type FilterProps = { location: Location; name: string; options: Option[]; path: string; queryKey: string; value: string; }; class Filter extends Component { getCurrentLabel() { const selected = this.props.options.find( item => item[0] === (this.props.value ?? '') ); if (selected) { return this.props.name + ': ' + selected[1]; } return this.props.name + ': ' + 'Any'; } getDefaultItem() { const query = {...this.props.location.query, cursor: ''}; delete query[this.props.queryKey]; return ( Any ); } getSelector = () => ( {this.getDefaultItem()} {this.props.options.map(([value, label]) => { const filterQuery = { [this.props.queryKey]: value, cursor: '', }; const query = {...this.props.location.query, ...filterQuery}; return ( {label} ); })} ); render() { return (
{this.props.options.length === 1 ? ( {this.getCurrentLabel()} ) : ( this.getSelector() )}
); } } type SortByProps = { location: Location; options: Option[]; path: string; value: string; }; class SortBy extends Component { getCurrentSortLabel() { return this.props.options.find(([value]) => value === this.props.value)?.[1]; } getSortBySelector() { return ( {this.props.options.map(([value, label]) => { const query = {...this.props.location.query, sortBy: value, cursor: ''}; return ( {label} ); })} ); } render() { if (this.props.options.length === 0) { return null; } return (
Showing results sorted by {this.props.options.length === 1 ? ( {this.getCurrentSortLabel()} ) : ( this.getSortBySelector() )}
); } } type FilterConfig = { name: string; options: Option[]; }; // XXX(ts): Using Partial here on the DefaultProps is not really correct, since // defaultProps guarantees they'll be set. But because this component is // wrapped with a HoC, we lose the defaultProps, and users of the component type Props = { api: Client; location: Location; } & Partial; type DefaultProps = { columns: React.ReactNode[]; columnsForRow: (row: any) => React.ReactNode[]; defaultParams: Record; defaultSort: string; endpoint: string; filters: Record; hasPagination: boolean; hasSearch: boolean; keyForRow: (row: any) => string; method: RequestOptions['method']; path: string; sortOptions: Option[]; }; type State = { error: string | boolean; filters: Record; loading: boolean; pageLinks: null | string; query: string; rows: any[]; sortBy: string; }; class ResultGrid extends Component { static defaultProps: DefaultProps = { path: '', endpoint: '', method: 'GET', columns: [], sortOptions: [], filters: {}, defaultSort: '', keyForRow: row => row.id, columnsForRow: () => [], defaultParams: { per_page: 50, }, hasPagination: true, hasSearch: false, }; state: State = this.defaultState; componentWillMount() { this.fetchData(); } componentWillReceiveProps() { const queryParams = this.query; this.setState( { query: queryParams.query ?? '', sortBy: queryParams.sortBy ?? this.props.defaultSort, filters: {...queryParams}, pageLinks: null, loading: true, error: false, }, this.fetchData ); } get defaultState() { const queryParams = this.query; return { rows: [], loading: true, error: false, pageLinks: null, query: queryParams.query ?? '', sortBy: queryParams.sortBy ?? this.props.defaultSort, filters: {...queryParams}, } as State; } get query() { return ((this.props.location ?? {}).query ?? {}) as {[k: string]: string}; } remountComponent() { this.setState(this.defaultState, this.fetchData); } refresh() { this.setState({loading: true}, this.fetchData); } fetchData() { // TODO(dcramer): this should explicitly allow filters/sortBy/cursor/perPage const queryParams = { ...this.props.defaultParams, sortBy: this.state.sortBy, ...this.query, }; this.props.api.request(this.props.endpoint!, { method: this.props.method, data: queryParams, success: (data, _, resp) => { this.setState({ loading: false, error: false, rows: data, pageLinks: resp?.getResponseHeader('Link') ?? null, }); }, error: () => { this.setState({ loading: false, error: true, }); }, }); } onSearch = (e: React.FormEvent) => { const location = this.props.location ?? {}; const {query} = this.state; const targetQueryParams = {...(location.query ?? {}), query, cursor: ''}; e.preventDefault(); browserHistory.push({ pathname: this.props.path, query: targetQueryParams, }); }; onQueryChange = (evt: React.ChangeEvent) => { this.setState({query: evt.target.value}); }; renderLoading() { return (
Hold on to your butts!
); } renderError() { return (
Something bad happened :(
); } renderNoResults() { return ( No results found. ); } renderResults() { return this.state.rows.map(row => ( {this.props.columnsForRow?.(row)} )); } render() { const {filters, sortOptions, path, location} = this.props; return (
{this.props.hasSearch && (
)} {Object.keys(filters ?? {}).map(filterKey => ( ))}
{this.props.columns} {this.state.loading ? this.renderLoading() : this.state.error ? this.renderError() : this.state.rows.length === 0 ? this.renderNoResults() : this.renderResults()}
{this.props.hasPagination && this.state.pageLinks && ( )}
); } } export {ResultGrid}; export default withApi(ResultGrid);