import {MouseEvent} from 'react'; import * as React from 'react'; import {browserHistory} from 'react-router'; import styled from '@emotion/styled'; import classNames from 'classnames'; import {Location, Query} from 'history'; import moment from 'moment'; import {resetGlobalSelection} from 'app/actionCreators/globalSelection'; import {openAddDashboardWidgetModal} from 'app/actionCreators/modal'; import {Client} from 'app/api'; import Feature from 'app/components/acl/feature'; import DropdownMenu from 'app/components/dropdownMenu'; import EmptyStateWarning from 'app/components/emptyStateWarning'; import MenuItem from 'app/components/menuItem'; import Pagination from 'app/components/pagination'; import TimeSince from 'app/components/timeSince'; import {IconEllipsis} from 'app/icons'; import {t} from 'app/locale'; import space from 'app/styles/space'; import {Organization, SavedQuery} from 'app/types'; import {trackAnalyticsEvent} from 'app/utils/analytics'; import EventView from 'app/utils/discover/eventView'; import parseLinkHeader from 'app/utils/parseLinkHeader'; import {decodeList} from 'app/utils/queryString'; import withApi from 'app/utils/withApi'; import {handleCreateQuery, handleDeleteQuery} from './savedQuery/utils'; import MiniGraph from './miniGraph'; import QueryCard from './querycard'; import {getPrebuiltQueries} from './utils'; type Props = { api: Client; organization: Organization; location: Location; savedQueries: SavedQuery[]; renderPrebuilt: boolean; pageLinks: string; onQueryChange: () => void; savedQuerySearchQuery: string; }; class QueryList extends React.Component { componentDidMount() { /** * We need to reset global selection here because the saved queries can define their own projects * in the query. This can lead to mismatched queries for the project */ resetGlobalSelection(); } handleDeleteQuery = (eventView: EventView) => (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); const {api, organization, onQueryChange, location, savedQueries} = this.props; handleDeleteQuery(api, organization, eventView).then(() => { if (savedQueries.length === 1 && location.query.cursor) { browserHistory.push({ pathname: location.pathname, query: {...location.query, cursor: undefined}, }); } else { onQueryChange(); } }); }; handleDuplicateQuery = (eventView: EventView, yAxis: string[]) => (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); const {api, location, organization, onQueryChange} = this.props; eventView = eventView.clone(); eventView.name = `${eventView.name} copy`; handleCreateQuery(api, organization, eventView, yAxis).then(() => { onQueryChange(); browserHistory.push({ pathname: location.pathname, query: {}, }); }); }; handleAddQueryToDashboard = (eventView: EventView, savedQuery?: SavedQuery) => (event: React.MouseEvent) => { const {organization} = this.props; event.preventDefault(); event.stopPropagation(); openAddDashboardWidgetModal({ organization, defaultQuery: eventView.query, start: eventView.start, end: eventView.end, statsPeriod: eventView.statsPeriod, fromDiscover: true, defaultTitle: savedQuery?.name ?? eventView.name, }); }; renderQueries() { const {pageLinks, renderPrebuilt} = this.props; const links = parseLinkHeader(pageLinks || ''); let cards: React.ReactNode[] = []; // If we're on the first page (no-previous page exists) // include the pre-built queries. if (renderPrebuilt && (!links.previous || links.previous.results === false)) { cards = cards.concat(this.renderPrebuiltQueries()); } cards = cards.concat(this.renderSavedQueries()); if (cards.filter(x => x).length === 0) { return (

{t('No saved queries match that filter')}

); } return cards; } renderPrebuiltQueries() { const {location, organization, savedQuerySearchQuery} = this.props; const views = getPrebuiltQueries(organization); const hasSearchQuery = typeof savedQuerySearchQuery === 'string' && savedQuerySearchQuery.length > 0; const needleSearch = hasSearchQuery ? savedQuerySearchQuery.toLowerCase() : ''; const list = views.map((view, index) => { const eventView = EventView.fromNewQueryWithLocation(view, location); // if a search is performed on the list of queries, we filter // on the pre-built queries if ( hasSearchQuery && eventView.name && !eventView.name.toLowerCase().includes(needleSearch) ) { return null; } const recentTimeline = t('Last ') + eventView.statsPeriod; const customTimeline = moment(eventView.start).format('MMM D, YYYY h:mm A') + ' - ' + moment(eventView.end).format('MMM D, YYYY h:mm A'); const to = eventView.getResultsViewUrlTarget(organization.slug); return ( ( )} onEventClick={() => { trackAnalyticsEvent({ eventKey: 'discover_v2.prebuilt_query_click', eventName: 'Discoverv2: Click a pre-built query', organization_id: parseInt(this.props.organization.id, 10), query_name: eventView.name, }); }} renderContextMenu={() => ( {({hasFeature}) => { return ( hasFeature && ( {t('Add to Dashboard')} ) ); }} )} /> ); }); return list; } renderSavedQueries() { const {savedQueries, location, organization} = this.props; if (!savedQueries || !Array.isArray(savedQueries) || savedQueries.length === 0) { return []; } return savedQueries.map((savedQuery, index) => { const eventView = EventView.fromSavedQuery(savedQuery); const recentTimeline = t('Last ') + eventView.statsPeriod; const customTimeline = moment(eventView.start).format('MMM D, YYYY h:mm A') + ' - ' + moment(eventView.end).format('MMM D, YYYY h:mm A'); const to = eventView.getResultsViewShortUrlTarget(organization.slug); const dateStatus = ; const referrer = `api.discover.${eventView.getDisplayMode()}-chart`; return ( { trackAnalyticsEvent({ eventKey: 'discover_v2.saved_query_click', eventName: 'Discoverv2: Click a saved query', organization_id: parseInt(this.props.organization.id, 10), }); }} renderGraph={() => ( )} renderContextMenu={() => ( {({hasFeature}) => hasFeature && ( {t('Add to Dashboard')} ) } {t('Delete Query')} {t('Duplicate Query')} )} /> ); }); } render() { const {pageLinks} = this.props; return ( {this.renderQueries()} { const offset = Number(cursor?.split(':')?.[1] ?? 0); const newQuery: Query & {cursor?: string} = {...query, cursor}; const isPrevious = direction === -1; if (offset <= 0 && isPrevious) { delete newQuery.cursor; } browserHistory.push({ pathname: path, query: newQuery, }); }} /> ); } } const PaginationRow = styled(Pagination)` margin-bottom: 20px; `; const QueryGrid = styled('div')` display: grid; grid-template-columns: minmax(100px, 1fr); grid-gap: ${space(2)}; @media (min-width: ${p => p.theme.breakpoints[1]}) { grid-template-columns: repeat(2, minmax(100px, 1fr)); } @media (min-width: ${p => p.theme.breakpoints[2]}) { grid-template-columns: repeat(3, minmax(100px, 1fr)); } `; const ContextMenu = ({children}) => ( {({isOpen, getRootProps, getActorProps, getMenuProps}) => { const topLevelCx = classNames('dropdown', { 'anchor-right': true, open: isOpen, }); return ( ({ onClick: (event: MouseEvent) => { event.stopPropagation(); event.preventDefault(); }, })} > {isOpen && (
    {children}
)}
); }}
); const MoreOptions = styled('span')` display: flex; color: ${p => p.theme.textColor}; `; const DropdownTarget = styled('div')` display: flex; `; const StyledEmptyStateWarning = styled(EmptyStateWarning)` grid-column: 1 / 4; `; export default withApi(QueryList);