import {Fragment, PureComponent} from 'react'; import type {InjectedRouter} from 'react-router'; import {browserHistory} from 'react-router'; import {useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {FocusScope} from '@react-aria/focus'; import {AnimatePresence} from 'framer-motion'; import type {Location} from 'history'; import isEqual from 'lodash/isEqual'; import type {Client} from 'sentry/api'; import Feature from 'sentry/components/acl/feature'; import FeatureDisabled from 'sentry/components/acl/featureDisabled'; import GuideAnchor from 'sentry/components/assistant/guideAnchor'; import Banner from 'sentry/components/banner'; import {Button} from 'sentry/components/button'; import ButtonBar from 'sentry/components/buttonBar'; import {CreateAlertFromViewButton} from 'sentry/components/createAlertButton'; import type {MenuItemProps} from 'sentry/components/dropdownMenu'; import {DropdownMenu} from 'sentry/components/dropdownMenu'; import {Hovercard} from 'sentry/components/hovercard'; import InputControl from 'sentry/components/input'; import {Overlay, PositionWrapper} from 'sentry/components/overlay'; import {IconBookmark, IconDelete, IconEllipsis, IconStar} from 'sentry/icons'; import {t} from 'sentry/locale'; import {space} from 'sentry/styles/space'; import type {Organization, Project, SavedQuery} from 'sentry/types'; import {trackAnalytics} from 'sentry/utils/analytics'; import EventView from 'sentry/utils/discover/eventView'; import {getDiscoverQueriesUrl} from 'sentry/utils/discover/urls'; import useOverlay from 'sentry/utils/useOverlay'; import withApi from 'sentry/utils/withApi'; import {normalizeUrl} from 'sentry/utils/withDomainRequired'; import withProjects from 'sentry/utils/withProjects'; import {handleAddQueryToDashboard} from 'sentry/views/discover/utils'; import {DEFAULT_EVENT_VIEW} from '../data'; import { handleCreateQuery, handleDeleteQuery, handleResetHomepageQuery, handleUpdateHomepageQuery, handleUpdateQuery, } from './utils'; const renderDisabled = p => ( } > {p.children(p)} ); type SaveAsDropdownProps = { disabled: boolean; modifiedHandleCreateQuery: (e: React.MouseEvent) => void; onChangeInput: (e: React.FormEvent) => void; queryName: string; }; function SaveAsDropdown({ queryName, disabled, onChangeInput, modifiedHandleCreateQuery, }: SaveAsDropdownProps) { const {isOpen, triggerProps, overlayProps, arrowProps} = useOverlay(); const theme = useTheme(); return (
{isOpen && ( {t('Save for Org')} )}
); } type DefaultProps = { disabled: boolean; }; type Props = DefaultProps & { api: Client; eventView: EventView; /** * DO NOT USE `Location` TO GENERATE `EventView` IN THIS COMPONENT. * * In this component, state is generated from EventView and SavedQueriesStore. * Using Location to rebuild EventView will break the tests. `Location` is * passed down only because it is needed for navigation. */ location: Location; organization: Organization; projects: Project[]; queryDataLoading: boolean; router: InjectedRouter; savedQuery: SavedQuery | undefined; setHomepageQuery: (homepageQuery?: SavedQuery) => void; setSavedQuery: (savedQuery: SavedQuery) => void; updateCallback: () => void; yAxis: string[]; homepageQuery?: SavedQuery; isHomepage?: boolean; }; type State = { isEditingQuery: boolean; isNewQuery: boolean; queryName: string; }; class SavedQueryButtonGroup extends PureComponent { static getDerivedStateFromProps(nextProps: Readonly, prevState: State): State { const {eventView: nextEventView, savedQuery, queryDataLoading, yAxis} = nextProps; // For a new unsaved query if (!savedQuery) { return { isNewQuery: true, isEditingQuery: false, queryName: prevState.queryName || '', }; } if (queryDataLoading) { return prevState; } const savedEventView = EventView.fromSavedQuery(savedQuery); // Switching from a SavedQuery to another SavedQuery if (savedEventView.id !== nextEventView.id) { return { isNewQuery: false, isEditingQuery: false, queryName: '', }; } // For modifying a SavedQuery const isEqualQuery = nextEventView.isEqualTo(savedEventView); // undefined saved yAxis defaults to count() and string values are converted to array const isEqualYAxis = isEqual( yAxis, !savedQuery.yAxis ? ['count()'] : typeof savedQuery.yAxis === 'string' ? [savedQuery.yAxis] : savedQuery.yAxis ); return { isNewQuery: false, isEditingQuery: !isEqualQuery || !isEqualYAxis, // HACK(leedongwei): See comment at SavedQueryButtonGroup.onFocusInput queryName: prevState.queryName || '', }; } /** * Stop propagation for the input and container so people can interact with * the inputs in the dropdown. */ static stopEventPropagation = (event: React.MouseEvent) => { const capturedElements = ['LI', 'INPUT']; if ( event.target instanceof Element && capturedElements.includes(event.target.nodeName) ) { event.preventDefault(); event.stopPropagation(); } }; static defaultProps: DefaultProps = { disabled: false, }; state: State = { isNewQuery: true, isEditingQuery: false, queryName: '', }; onChangeInput = (event: React.FormEvent) => { const target = event.target as HTMLInputElement; this.setState({queryName: target.value}); }; /** * There are two ways to create a query * 1) Creating a query from scratch and saving it * 2) Modifying an existing query and saving it */ handleCreateQuery = (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); const {api, organization, eventView, yAxis} = this.props; if (!this.state.queryName) { return; } const nextEventView = eventView.clone(); nextEventView.name = this.state.queryName; // Checks if "Save as" button is clicked from a clean state, or it is // clicked while modifying an existing query const isNewQuery = !eventView.id; handleCreateQuery(api, organization, nextEventView, yAxis, isNewQuery).then( (savedQuery: SavedQuery) => { const view = EventView.fromSavedQuery(savedQuery); Banner.dismiss('discover'); this.setState({queryName: ''}); browserHistory.push( normalizeUrl(view.getResultsViewUrlTarget(organization.slug)) ); } ); }; handleUpdateQuery = (event: React.MouseEvent) => { event.preventDefault(); event.stopPropagation(); const {api, organization, eventView, updateCallback, yAxis, setSavedQuery} = this.props; handleUpdateQuery(api, organization, eventView, yAxis).then( (savedQuery: SavedQuery) => { const view = EventView.fromSavedQuery(savedQuery); setSavedQuery(savedQuery); this.setState({queryName: ''}); browserHistory.push(view.getResultsViewShortUrlTarget(organization.slug)); updateCallback(); } ); }; handleDeleteQuery = (event?: React.MouseEvent) => { event?.preventDefault(); event?.stopPropagation(); const {api, organization, eventView} = this.props; handleDeleteQuery(api, organization, eventView).then(() => { browserHistory.push( normalizeUrl({ pathname: getDiscoverQueriesUrl(organization), query: {}, }) ); }); }; handleCreateAlertSuccess = () => { const {organization} = this.props; trackAnalytics('discover_v2.create_alert_clicked', { organization, status: 'success', }); }; renderButtonViewSaved(disabled: boolean) { const {organization} = this.props; return ( ); } renderButtonSaveAs(disabled: boolean) { const {queryName} = this.state; return ( ); } renderButtonSave(disabled: boolean) { const {isNewQuery, isEditingQuery} = this.state; if (!isNewQuery && !isEditingQuery) { return null; } // Existing query with edits, show save and save as. if (!isNewQuery && isEditingQuery) { return ( {this.renderButtonSaveAs(disabled)} ); } // Is a new query enable saveas return this.renderButtonSaveAs(disabled); } renderButtonDelete(disabled: boolean) { const {isNewQuery} = this.state; if (isNewQuery) { return null; } return ( ); } renderSaveAsHomepage(disabled: boolean) { const { api, organization, eventView, location, isHomepage, setHomepageQuery, homepageQuery, queryDataLoading, } = this.props; const buttonDisabled = disabled || queryDataLoading; const analyticsEventSource = isHomepage ? 'homepage' : eventView.id ? 'saved-query' : 'prebuilt-query'; if ( homepageQuery && eventView.isEqualTo(EventView.fromSavedQuery(homepageQuery), ['id', 'name']) ) { return ( ); } return ( ); } renderQueryButton(renderFunc: (disabled: boolean) => React.ReactNode) { const {organization} = this.props; return ( {({hasFeature}) => renderFunc(!hasFeature || this.props.disabled)} ); } render() { const {organization, eventView, savedQuery, yAxis, router, location, isHomepage} = this.props; const contextMenuItems: MenuItemProps[] = []; if (organization.features.includes('dashboards-edit')) { contextMenuItems.push({ key: 'add-to-dashboard', label: t('Add to Dashboard'), onAction: () => { handleAddQueryToDashboard({ organization, location, eventView, query: savedQuery, yAxis, router, }); }, }); } if (!isHomepage && savedQuery) { contextMenuItems.push({ key: 'delete-saved-query', label: t('Delete Saved Query'), onAction: () => this.handleDeleteQuery(), }); } const contextMenu = ( (