diff --git a/src/sentry/static/sentry/app/types/index.tsx b/src/sentry/static/sentry/app/types/index.tsx index aa4c5c4c0afed2..09a09f480d8f14 100644 --- a/src/sentry/static/sentry/app/types/index.tsx +++ b/src/sentry/static/sentry/app/types/index.tsx @@ -1,3 +1,5 @@ +import {SpanEntry} from 'app/views/organizationEventsV2/transactionView/types'; + export type Organization = { id: string; slug: string; @@ -64,19 +66,50 @@ export type EventAttachment = { type: string; }; -// This type is incomplete -export type Event = { +type EntryType = { + data: {[key: string]: any} | any[]; + type: string; +}; + +export type EventTag = {key: string; value: string}; + +type SentryEventBase = { id: string; eventID: string; groupID?: string; - type: string; title: string; culprit: string; metadata: EventMetadata; message: string; platform?: string; + dateCreated?: string; + endTimestamp?: number; + entries: EntryType[]; + + previousEventID?: string; + nextEventID?: string; + projectSlug: string; + + tags: EventTag[]; + + size: number; + + location: string; + + oldestEventID: string | null; + latestEventID: string | null; }; +// This type is incomplete +export type Event = + | ({type: string} & SentryEventBase) + | { + type: 'transaction'; + entries: SpanEntry[]; + startTimestamp: number; + endTimestamp: number; + } & SentryEventBase; + export type EventsStatsData = [number, {count: number}[]][]; export type EventsStats = { @@ -190,3 +223,71 @@ export type Config = { }; distPrefix: string; }; + +type Metadata = { + value: string; + message: string; + directive: string; + type: string; + title: string; + uri: string; +}; + +type EventOrGroupType = [ + 'error', + 'csp', + 'hpkp', + 'expectct', + 'expectstaple', + 'default', + 'transaction' +]; + +// TODO(ts): incomplete +export type Group = { + id: string; + annotations: string[]; + assignedTo: User; + count: string; + culprit: string; + firstSeen: string; + hasSeen: boolean; + isBookmarked: boolean; + isPublic: boolean; + isSubscribed: boolean; + lastSeen: string; + level: string; + logger: string; + metadata: Metadata; + numComments: number; + permalink: string; + project: { + name: string; + slug: string; + }; + shareId: string; + shortId: string; + status: string; + statusDetails: {}; + title: string; + type: EventOrGroupType; + userCount: number; + seenBy: User[]; +}; + +export type EventView = { + id: string; + name: string; + data: { + fields: string[]; + columnNames: string[]; + sort: string[]; + query?: string; + + // TODO: removed as of https://github.com/getsentry/sentry/pull/14321 + // groupby: string[]; + // orderby: string[]; + }; + tags: string[]; + columnWidths: string[]; +}; diff --git a/src/sentry/static/sentry/app/utils.tsx b/src/sentry/static/sentry/app/utils.tsx index 7eed2f0192b55e..29383b3c067d11 100644 --- a/src/sentry/static/sentry/app/utils.tsx +++ b/src/sentry/static/sentry/app/utils.tsx @@ -238,7 +238,7 @@ export function isWebpackChunkLoadingError(error: Error): boolean { ); } -export function deepFreeze(object: {[x: string]: any}) { +export function deepFreeze(object: T) { // Retrieve the property names defined on object const propNames = Object.getOwnPropertyNames(object); // Freeze properties before freezing self diff --git a/src/sentry/static/sentry/app/views/events/utils/eventsRequest.tsx b/src/sentry/static/sentry/app/views/events/utils/eventsRequest.tsx index c6f550b8488502..86d7e715c49ddb 100644 --- a/src/sentry/static/sentry/app/views/events/utils/eventsRequest.tsx +++ b/src/sentry/static/sentry/app/views/events/utils/eventsRequest.tsx @@ -318,7 +318,7 @@ class EventsRequest extends React.PureComponent> = deepFreeze([ { id: 'all', name: t('All Events'), diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/eventDetails.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/eventDetails.tsx similarity index 78% rename from src/sentry/static/sentry/app/views/organizationEventsV2/eventDetails.jsx rename to src/sentry/static/sentry/app/views/organizationEventsV2/eventDetails.tsx index 2f1be0b3034019..37aedff98ea871 100644 --- a/src/sentry/static/sentry/app/views/organizationEventsV2/eventDetails.jsx +++ b/src/sentry/static/sentry/app/views/organizationEventsV2/eventDetails.tsx @@ -1,5 +1,7 @@ import React from 'react'; import {browserHistory} from 'react-router'; +import {Location} from 'history'; +import {Params} from 'react-router/lib/Router'; import PropTypes from 'prop-types'; import {omit} from 'lodash'; import {css} from 'react-emotion'; @@ -11,15 +13,20 @@ import NotFound from 'app/components/errors/notFound'; import withApi from 'app/utils/withApi'; import theme from 'app/utils/theme'; import space from 'app/styles/space'; +import {Organization, EventView, Event} from 'app/types'; import EventModalContent from './eventModalContent'; -import {getQuery} from './utils'; +import {EventQuery, getQuery} from './utils'; -const slugValidator = function(props, propName, componentName) { +const slugValidator = function( + props: {[key: string]: any}, + propName: string, + componentName: string +) { const value = props[propName]; // Accept slugs that look like: // * project-slug:deadbeef - if (value && !/^(?:[^:]+):(?:[a-f0-9]+)$/.test(value)) { + if (value && typeof value === 'string' && !/^(?:[^:]+):(?:[a-f0-9]+)$/.test(value)) { return new Error(`Invalid value for ${propName} provided to ${componentName}.`); } return null; @@ -38,15 +45,27 @@ const modalStyles = css` } `; -class EventDetails extends AsyncComponent { - static propTypes = { +type Props = { + organization: Organization; + location: Location; + eventSlug: string; + view: EventView; + params: Params; +}; + +type State = { + event: Event; +}; + +class EventDetails extends AsyncComponent { + static propTypes: any = { organization: SentryTypes.Organization.isRequired, eventSlug: slugValidator, location: PropTypes.object.isRequired, view: PropTypes.object.isRequired, }; - getEndpoints() { + getEndpoints(): Array<[string, string, {query: EventQuery}]> { const {organization, eventSlug, view, location} = this.props; const query = getQuery(view, location); const url = `/organizations/${organization.slug}/events/${eventSlug}/`; @@ -78,7 +97,6 @@ class EventDetails extends AsyncComponent { return ( { +const ActiveTab = (props: ActiveTabProps) => { const {projectId, event, activeTab} = props; if (!activeTab) { return null; } const entry = event.entries.find(item => item.type === activeTab); - if (INTERFACES[activeTab]) { + if (INTERFACES[activeTab] && entry) { const Component = INTERFACES[activeTab]; return ( { console.error('Unregistered interface: ' + activeTab); return ( - +

{t('There was an error rendering this data.')}

); @@ -62,13 +73,24 @@ ActiveTab.propTypes = { projectId: PropTypes.string.isRequired, }; -class EventInterfaces extends React.Component { +type EventInterfacesProps = { + event: Event; + projectId: string; +}; +type EventInterfacesState = { + activeTab: string; +}; + +class EventInterfaces extends React.Component< + EventInterfacesProps, + EventInterfacesState +> { static propTypes = { event: SentryTypes.Event.isRequired, projectId: PropTypes.string.isRequired, }; - constructor(props) { + constructor(props: EventInterfacesProps) { super(props); this.state = { activeTab: props.event.entries[0].type, @@ -89,7 +111,7 @@ class EventInterfaces extends React.Component { return null; } const type = entry.type; - const classname = type === activeTab ? 'active' : null; + const classname = type === activeTab ? 'active' : undefined; return (
  • { +const EventModalContent = (props: EventModalContentProps) => { const {event, projectId, organization, location, view} = props; // Having an aggregate field means we want to show pagination/graphs @@ -53,7 +64,7 @@ const EventModalContent = props => { {event.type === 'transaction' ? ( - + ) : ( )} @@ -100,7 +111,7 @@ EventHeader.propTypes = { /** * Render metadata about the event and provide a link to the JSON blob */ -const EventMetadata = props => { +const EventMetadata = (props: {event: Event; eventJsonUrl: string}) => { const {event, eventJsonUrl} = props; return ( @@ -109,7 +120,7 @@ const EventMetadata = props => { @@ -166,7 +177,7 @@ const SidebarColumn = styled('div')` grid-column: 2 / 3; `; -const SidebarBlock = styled('div')` +const SidebarBlock = styled('div')<{withSeparator?: boolean; theme?: any}>` margin: 0 0 ${space(2)} 0; padding: 0 0 ${space(2)} 0; ${p => (p.withSeparator ? `border-bottom: 1px solid ${p.theme.borderLight};` : '')} diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/events.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/events.tsx similarity index 84% rename from src/sentry/static/sentry/app/views/organizationEventsV2/events.jsx rename to src/sentry/static/sentry/app/views/organizationEventsV2/events.tsx index f316dfb66fcf31..76e077ddbd71f1 100644 --- a/src/sentry/static/sentry/app/views/organizationEventsV2/events.jsx +++ b/src/sentry/static/sentry/app/views/organizationEventsV2/events.tsx @@ -2,6 +2,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'react-emotion'; import {omit, isEqual} from 'lodash'; +import * as ReactRouter from 'react-router'; +import {Location} from 'history'; + +import {Organization, EventView} from 'app/types'; import SentryTypes from 'app/sentryTypes'; import space from 'app/styles/space'; import SearchBar from 'app/views/events/searchBar'; @@ -15,7 +19,7 @@ import {getParams} from 'app/views/events/utils/getParams'; import Table from './table'; import Tags from './tags'; -import {getQuery} from './utils'; +import {getQuery, EventQuery} from './utils'; import {MODAL_QUERY_KEYS} from './data'; const CHART_AXIS_OPTIONS = [ @@ -23,7 +27,14 @@ const CHART_AXIS_OPTIONS = [ {label: 'Users', value: 'user_count'}, ]; -export default class Events extends React.Component { +type EventsProps = { + router: ReactRouter.InjectedRouter; + location: Location; + organization: Organization; + view: EventView; +}; + +export default class Events extends React.Component { static propTypes = { router: PropTypes.object.isRequired, location: PropTypes.object.isRequired, @@ -81,7 +92,13 @@ export default class Events extends React.Component { } } -class EventsTable extends AsyncComponent { +type EventsTableProps = { + location: Location; + organization: Organization; + view: EventView; +}; + +class EventsTable extends AsyncComponent { static propTypes = { location: PropTypes.object.isRequired, organization: SentryTypes.Organization.isRequired, @@ -90,7 +107,7 @@ class EventsTable extends AsyncComponent { shouldReload = false; - componentDidUpdate(prevProps, prevContext) { + componentDidUpdate(prevProps: EventsTableProps, prevContext) { // Do not update if we are just opening/closing the modal const locationHasChanged = !isEqual( omit(prevProps.location.query, MODAL_QUERY_KEYS), @@ -102,7 +119,7 @@ class EventsTable extends AsyncComponent { } } - getEndpoints() { + getEndpoints(): Array<[string, string, {query: EventQuery}]> { const {location, organization, view} = this.props; return [ [ diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/index.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/index.tsx similarity index 78% rename from src/sentry/static/sentry/app/views/organizationEventsV2/index.jsx rename to src/sentry/static/sentry/app/views/organizationEventsV2/index.tsx index 13434845d7f570..982d3fd8da3ccb 100644 --- a/src/sentry/static/sentry/app/views/organizationEventsV2/index.jsx +++ b/src/sentry/static/sentry/app/views/organizationEventsV2/index.tsx @@ -1,7 +1,11 @@ import React from 'react'; import DocumentTitle from 'react-document-title'; import PropTypes from 'prop-types'; +import * as ReactRouter from 'react-router'; +import {Params} from 'react-router/lib/Router'; +import {Location} from 'history'; +import {Organization} from 'app/types'; import {t} from 'app/locale'; import SentryTypes from 'app/sentryTypes'; import GlobalSelectionHeader from 'app/components/organizations/globalSelectionHeader'; @@ -17,18 +21,26 @@ import withOrganization from 'app/utils/withOrganization'; import Events from './events'; import EventDetails from './eventDetails'; import {ALL_VIEWS} from './data'; -import {getCurrentView} from './utils'; +import {getCurrentView, getFirstQueryString} from './utils'; -class OrganizationEventsV2 extends React.Component { - static propTypes = { +type Props = { + organization: Organization; + location: Location; + router: ReactRouter.InjectedRouter; + params: Params; +}; + +class OrganizationEventsV2 extends React.Component { + static propTypes: any = { organization: SentryTypes.Organization.isRequired, location: PropTypes.object.isRequired, router: PropTypes.object.isRequired, }; - renderTabs() { + renderTabs(): React.ReactNode { const {location} = this.props; - const currentView = getCurrentView(location.query.view); + const firstView = getFirstQueryString(location.query, 'view'); + const currentView = getCurrentView(firstView); return ( @@ -55,8 +67,10 @@ class OrganizationEventsV2 extends React.Component { render() { const {organization, location, router} = this.props; - const {eventSlug} = location.query; - const currentView = getCurrentView(location.query.view); + const eventSlug = getFirstQueryString(location.query, 'eventSlug'); + const view = getFirstQueryString(location.query, 'view'); + + const currentView = getCurrentView(view); return ( diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/linkedIssuePreview.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/linkedIssuePreview.tsx similarity index 91% rename from src/sentry/static/sentry/app/views/organizationEventsV2/linkedIssuePreview.jsx rename to src/sentry/static/sentry/app/views/organizationEventsV2/linkedIssuePreview.tsx index d93f111dd4ea05..e9cce5177c8937 100644 --- a/src/sentry/static/sentry/app/views/organizationEventsV2/linkedIssuePreview.jsx +++ b/src/sentry/static/sentry/app/views/organizationEventsV2/linkedIssuePreview.tsx @@ -13,14 +13,27 @@ import SeenByList from 'app/components/seenByList'; import ShortId from 'app/components/shortId'; import Times from 'app/components/group/times'; import space from 'app/styles/space'; +import {Group} from 'app/types'; -class LinkedIssuePreview extends AsyncComponent { +type Props = { + groupId: string; + eventId: string; +}; + +type State = { + group: Group; +}; + +class LinkedIssuePreview extends AsyncComponent< + Props & AsyncComponent['props'], + State & AsyncComponent['state'] +> { static propTypes = { groupId: PropTypes.string.isRequired, eventId: PropTypes.string.isRequired, }; - getEndpoints() { + getEndpoints(): Array<[string, string]> { const {groupId} = this.props; const groupUrl = `/issues/${groupId}/`; diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/modalLineGraph.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/modalLineGraph.tsx similarity index 92% rename from src/sentry/static/sentry/app/views/organizationEventsV2/modalLineGraph.jsx rename to src/sentry/static/sentry/app/views/organizationEventsV2/modalLineGraph.tsx index 033897af96b996..6f19dfb6a815da 100644 --- a/src/sentry/static/sentry/app/views/organizationEventsV2/modalLineGraph.jsx +++ b/src/sentry/static/sentry/app/views/organizationEventsV2/modalLineGraph.tsx @@ -1,8 +1,10 @@ import React from 'react'; import PropTypes from 'prop-types'; import {browserHistory} from 'react-router'; +import {Location} from 'history'; import {omit} from 'lodash'; +import {Client} from 'app/api'; import {t} from 'app/locale'; import SentryTypes from 'app/sentryTypes'; import {getInterval, useShortInterval} from 'app/components/charts/utils'; @@ -18,6 +20,7 @@ import {Panel} from 'app/components/panels'; import withApi from 'app/utils/withApi'; import withGlobalSelection from 'app/utils/withGlobalSelection'; import theme from 'app/utils/theme'; +import {Event, Organization, EventView} from 'app/types'; import {MODAL_QUERY_KEYS, PIN_ICON} from './data'; import {getQueryString} from './utils'; @@ -26,10 +29,10 @@ import {getQueryString} from './utils'; * Generate the data to display a vertical line for the current * event on the graph. */ -const getCurrentEventMarker = currentEvent => { +const getCurrentEventMarker = (currentEvent: Event) => { const title = t('Current Event'); const eventTime = +new Date( - currentEvent.dateCreated || currentEvent.endTimestamp * 1000 + currentEvent.dateCreated || (currentEvent.endTimestamp || 0) * 1000 ); return { @@ -88,7 +91,7 @@ const handleClick = async function( // Get events that match the clicked timestamp // taking into account the group and current environment & query - const query = { + const query: any = { environment: selection.environments, start: getUtcDateString(value), end: getUtcDateString(value + intervalToMilliseconds(interval)), @@ -127,10 +130,20 @@ const handleClick = async function( }); }; +type ModalLineGraphProps = { + api: Client; + organization: Organization; + location: Location; + currentEvent: Event; + view: EventView; + // TODO(ts): adjust + selection: any; +}; + /** * Render a graph of event volumes for the current group + event. */ -const ModalLineGraph = props => { +const ModalLineGraph = (props: ModalLineGraphProps) => { const {api, organization, location, selection, currentEvent, view} = props; const isUtc = selection.datetime.utc; @@ -215,6 +228,6 @@ ModalLineGraph.propTypes = { organization: SentryTypes.Organization.isRequired, selection: PropTypes.object.isRequired, view: PropTypes.object.isRequired, -}; +} as any; export default withGlobalSelection(withApi(ModalLineGraph)); diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/modalPagination.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/modalPagination.tsx similarity index 83% rename from src/sentry/static/sentry/app/views/organizationEventsV2/modalPagination.jsx rename to src/sentry/static/sentry/app/views/organizationEventsV2/modalPagination.tsx index d506c0f1213782..9400cbd95a5de1 100644 --- a/src/sentry/static/sentry/app/views/organizationEventsV2/modalPagination.jsx +++ b/src/sentry/static/sentry/app/views/organizationEventsV2/modalPagination.tsx @@ -3,30 +3,41 @@ import PropTypes from 'prop-types'; import styled from 'react-emotion'; import isPropValid from '@emotion/is-prop-valid'; import {omit} from 'lodash'; +import {Location} from 'history'; import {t} from 'app/locale'; import Link from 'app/components/links/link'; import SentryTypes from 'app/sentryTypes'; import InlineSvg from 'app/components/inlineSvg'; import space from 'app/styles/space'; +import {Event} from 'app/types'; import {MODAL_QUERY_KEYS} from './data'; +type LinksType = { + oldest: null; + latest: null; + + next: {}; + previous: {}; +}; + /** * Generate a mapping of link names => link targets for pagination */ -function buildTargets(event, location) { +function buildTargets(event: Event, location: Location): LinksType { // Remove slug related keys as we need to create new ones const baseQuery = omit(location.query, MODAL_QUERY_KEYS); - const urlMap = { + const urlMap: {[k in keyof LinksType]: string | undefined | null} = { previous: event.previousEventID, next: event.nextEventID, oldest: event.oldestEventID, latest: event.latestEventID, }; - const links = {}; + const links: {[k in keyof LinksType]?: any} = {}; + Object.entries(urlMap).forEach(([key, value]) => { // If the urlMap has no value we want to skip this link as it is 'disabled'; if (!value) { @@ -42,10 +53,15 @@ function buildTargets(event, location) { } }); - return links; + return links as LinksType; } -const ModalPagination = props => { +type Props = { + event: Event; + location: Location; +}; + +const ModalPagination = (props: Props) => { const {event, location} = props; const links = buildTargets(event, location); @@ -88,9 +104,11 @@ ModalPagination.propTypes = { event: SentryTypes.Event.isRequired, }; -const StyledLink = styled(Link, {shouldForwardProp: isPropValid})` +const StyledLink = styled(Link, { + shouldForwardProp: isPropValid, +})<{theme: any; disabled: boolean; isLast: boolean}>` color: ${p => (p.disabled ? p.theme.disabled : p.theme.gray3)}; - font-size: ${p => p.fontSizeMedium}; + font-size: ${p => p.theme.fontSizeMedium}; text-align: center; padding: ${space(0.5)} ${space(1.5)}; ${p => (p.isLast ? '' : `border-right: 1px solid ${p.theme.borderDark};`)} diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/relatedEvents.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/relatedEvents.tsx similarity index 88% rename from src/sentry/static/sentry/app/views/organizationEventsV2/relatedEvents.jsx rename to src/sentry/static/sentry/app/views/organizationEventsV2/relatedEvents.tsx index f5ba0790afd014..5e8d9eb91f1fe1 100644 --- a/src/sentry/static/sentry/app/views/organizationEventsV2/relatedEvents.jsx +++ b/src/sentry/static/sentry/app/views/organizationEventsV2/relatedEvents.tsx @@ -2,7 +2,9 @@ import React from 'react'; import styled from 'react-emotion'; import PropTypes from 'prop-types'; import {omit} from 'lodash'; +import {Location} from 'history'; +import {Organization, EventView, Event, Project} from 'app/types'; import {t} from 'app/locale'; import SentryTypes from 'app/sentryTypes'; import AsyncComponent from 'app/components/asyncComponent'; @@ -17,16 +19,25 @@ import overflowEllipsis from 'app/styles/overflowEllipsis'; import withProjects from 'app/utils/withProjects'; import {MODAL_QUERY_KEYS} from './data'; - -class RelatedEvents extends AsyncComponent { - static propTypes = { +import {EventQuery} from './utils'; + +type Props = { + location: Location; + organization: Organization; + view: EventView; + event: Event; + projects: Array; +}; + +class RelatedEvents extends AsyncComponent { + static propTypes: any = { event: SentryTypes.Event.isRequired, location: PropTypes.object.isRequired, organization: SentryTypes.Organization.isRequired, projects: PropTypes.arrayOf(SentryTypes.Project), }; - getEndpoints() { + getEndpoints(): Array<[string, string, {query: EventQuery}]> { // TODO what happens when global-views feature is not on the org? const {event, organization} = this.props; const eventsUrl = `/organizations/${organization.slug}/eventsv2/`; @@ -36,7 +47,7 @@ class RelatedEvents extends AsyncComponent { return []; } - const params = { + const params: {query: EventQuery} = { query: { field: [ 'project.name', @@ -106,7 +117,7 @@ const Container = styled('div')` position: relative; `; -const Card = styled('div')` +const Card = styled('div')<{isCurrent?: boolean; theme?: any}>` display: flex; flex-direction: column; border: 1px solid ${p => (p.isCurrent ? p.theme.purpleLight : p.theme.borderLight)}; diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/sortLink.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/sortLink.tsx similarity index 89% rename from src/sentry/static/sentry/app/views/organizationEventsV2/sortLink.jsx rename to src/sentry/static/sentry/app/views/organizationEventsV2/sortLink.tsx index 480c47d59ebb02..6ad1c15d1acd03 100644 --- a/src/sentry/static/sentry/app/views/organizationEventsV2/sortLink.jsx +++ b/src/sentry/static/sentry/app/views/organizationEventsV2/sortLink.tsx @@ -1,11 +1,19 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'react-emotion'; +import {Location} from 'history'; import InlineSvg from 'app/components/inlineSvg'; import Link from 'app/components/links/link'; -class SortLink extends React.Component { +type Props = { + title: string; + sortKey: string; + defaultSort: string | null; + location: Location; +}; + +class SortLink extends React.Component { static propTypes = { title: PropTypes.string.isRequired, sortKey: PropTypes.string.isRequired, diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/styles.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/styles.tsx similarity index 100% rename from src/sentry/static/sentry/app/views/organizationEventsV2/styles.jsx rename to src/sentry/static/sentry/app/views/organizationEventsV2/styles.tsx diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/table.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/table.tsx similarity index 85% rename from src/sentry/static/sentry/app/views/organizationEventsV2/table.jsx rename to src/sentry/static/sentry/app/views/organizationEventsV2/table.tsx index 35b978c71418c1..b05b59819f3242 100644 --- a/src/sentry/static/sentry/app/views/organizationEventsV2/table.jsx +++ b/src/sentry/static/sentry/app/views/organizationEventsV2/table.tsx @@ -2,6 +2,7 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled, {css} from 'react-emotion'; import {omit} from 'lodash'; +import {Location} from 'history'; import SentryTypes from 'app/sentryTypes'; import {Panel, PanelHeader, PanelBody, PanelItem} from 'app/components/panels'; @@ -9,12 +10,22 @@ import EmptyStateWarning from 'app/components/emptyStateWarning'; import LoadingContainer from 'app/components/loading/loadingContainer'; import {t} from 'app/locale'; import space from 'app/styles/space'; +import {EventView, Organization} from 'app/types'; import {FIELD_FORMATTERS, SPECIAL_FIELDS} from './data'; import {getFieldRenderer} from './utils'; import SortLink from './sortLink'; -export default class Table extends React.Component { +type Props = { + view: EventView; + isLoading: boolean; + location: Location; + organization: Organization; + // TODO(ts): adjust this type + data: any; +}; + +export default class Table extends React.Component { static propTypes = { view: SentryTypes.EventView.isRequired, data: PropTypes.object, @@ -61,14 +72,14 @@ export default class Table extends React.Component { {fields.map((field, i) => { const title = columnNames[i] || field; - let sortKey = field; + let sortKey: string | null = field; if (SPECIAL_FIELDS.hasOwnProperty(field)) { - sortKey = SPECIAL_FIELDS[field].sortField; + sortKey = SPECIAL_FIELDS[field].sortField || null; } else if (FIELD_FORMATTERS.hasOwnProperty(field)) { - sortKey = FIELD_FORMATTERS[field].sortField ? field : false; + sortKey = FIELD_FORMATTERS[field].sortField ? field : null; } - if (sortKey === false) { + if (sortKey === null) { return {title}; } @@ -123,9 +134,10 @@ const Cell = styled('div')` overflow: hidden; `; +// TODO(ts): adjust types const StyledPanelBody = styled(props => { const otherProps = omit(props, 'isLoading'); return ; })` - ${p => p.isLoading && 'min-height: 240px;'}; + ${(p: {isLoading: boolean}) => p.isLoading && 'min-height: 240px;'}; `; diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/tags.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/tags.tsx similarity index 85% rename from src/sentry/static/sentry/app/views/organizationEventsV2/tags.jsx rename to src/sentry/static/sentry/app/views/organizationEventsV2/tags.tsx index c377156943228f..d96c9b5b24fa97 100644 --- a/src/sentry/static/sentry/app/views/organizationEventsV2/tags.jsx +++ b/src/sentry/static/sentry/app/views/organizationEventsV2/tags.tsx @@ -2,29 +2,47 @@ import React from 'react'; import PropTypes from 'prop-types'; import styled from 'react-emotion'; import {isEqual, omit} from 'lodash'; +import {Location} from 'history'; import * as Sentry from '@sentry/browser'; +import {Client} from 'app/api'; import SentryTypes from 'app/sentryTypes'; import Placeholder from 'app/components/placeholder'; import TagDistributionMeter from 'app/components/tagDistributionMeter'; import withApi from 'app/utils/withApi'; +import {Organization, EventView} from 'app/types'; + import { fetchTagDistribution, fetchTotalCount, getEventTagSearchUrl, getQuery, + Tag, + TagTopValue, } from './utils'; import {MODAL_QUERY_KEYS} from './data'; -class Tags extends React.Component { - static propTypes = { +type Props = { + api: Client; + organization: Organization; + view: EventView; + location: Location; +}; + +type State = { + tags: {[key: string]: Tag}; + totalValues: null | number; +}; + +class Tags extends React.Component { + static propTypes: any = { api: PropTypes.object.isRequired, organization: SentryTypes.Organization.isRequired, view: SentryTypes.EventView.isRequired, location: PropTypes.object.isRequired, }; - state = { + state: State = { tags: {}, totalValues: null, }; @@ -81,7 +99,8 @@ class Tags extends React.Component { const {location} = this.props; const {tags, totalValues} = this.state; const isLoading = !tags[tag] || totalValues === null; - let segments = []; + + let segments: Array = []; if (!isLoading) { segments = tags[tag].topValues; diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/tagsTable.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/tagsTable.tsx similarity index 88% rename from src/sentry/static/sentry/app/views/organizationEventsV2/tagsTable.jsx rename to src/sentry/static/sentry/app/views/organizationEventsV2/tagsTable.tsx index 9b9b92119a3cb3..4a077600eb9e28 100644 --- a/src/sentry/static/sentry/app/views/organizationEventsV2/tagsTable.jsx +++ b/src/sentry/static/sentry/app/views/organizationEventsV2/tagsTable.tsx @@ -1,16 +1,22 @@ import React from 'react'; import styled from 'react-emotion'; import PropTypes from 'prop-types'; -import {withRouter} from 'react-router'; +import * as ReactRouter from 'react-router'; import Link from 'app/components/links/link'; import Tooltip from 'app/components/tooltip'; import {t} from 'app/locale'; import space from 'app/styles/space'; import overflowEllipsis from 'app/styles/overflowEllipsis'; +import {EventTag} from 'app/types'; + import {getEventTagSearchUrl} from './utils'; -const TagsTable = props => { +type Props = { + tags: Array; +} & ReactRouter.WithRouterProps; + +const TagsTable = (props: Props) => { const {location, tags} = props; return (
    @@ -45,7 +51,7 @@ const TagsTable = props => { TagsTable.propTypes = { tags: PropTypes.array.isRequired, location: PropTypes.object, -}; +} as any; const StyledTable = styled('table')` table-layout: fixed; @@ -77,4 +83,4 @@ const TagValue = styled(TagKey)` ${overflowEllipsis}; `; -export default withRouter(TagsTable); +export default ReactRouter.withRouter(TagsTable); diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/transactionView/index.tsx b/src/sentry/static/sentry/app/views/organizationEventsV2/transactionView/index.tsx index 57d5ae902c0d91..71a531abe3d980 100644 --- a/src/sentry/static/sentry/app/views/organizationEventsV2/transactionView/index.tsx +++ b/src/sentry/static/sentry/app/views/organizationEventsV2/transactionView/index.tsx @@ -5,11 +5,11 @@ import SentryTypes from 'app/sentryTypes'; import {Panel, PanelHeader, PanelBody} from 'app/components/panels'; -import {SentryEvent} from './types'; +import {SentryTransactionEvent} from './types'; import TraceView from './traceView'; type PropType = { - event: SentryEvent; + event: SentryTransactionEvent; }; class TransanctionView extends React.Component { diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/transactionView/traceView.tsx b/src/sentry/static/sentry/app/views/organizationEventsV2/transactionView/traceView.tsx index 36702cb70ff403..bdcfd23c8f842f 100644 --- a/src/sentry/static/sentry/app/views/organizationEventsV2/transactionView/traceView.tsx +++ b/src/sentry/static/sentry/app/views/organizationEventsV2/transactionView/traceView.tsx @@ -6,7 +6,7 @@ import EmptyStateWarning from 'app/components/emptyStateWarning'; import DragManager, {DragManagerChildrenProps} from './dragManager'; import SpanTree from './spanTree'; -import {SpanType, SpanEntry, SentryEvent, ParsedTraceType} from './types'; +import {SpanType, SpanEntry, SentryTransactionEvent, ParsedTraceType} from './types'; import {isValidSpanID} from './utils'; import TraceViewMinimap from './minimap'; import * as CursorGuideHandler from './cursorGuideHandler'; @@ -18,7 +18,7 @@ type TraceContextType = { }; type PropType = { - event: Readonly; + event: Readonly; }; class TraceView extends React.Component { diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/transactionView/types.tsx b/src/sentry/static/sentry/app/views/organizationEventsV2/transactionView/types.tsx index 289e8b8a3e47b8..baa4045b7c3568 100644 --- a/src/sentry/static/sentry/app/views/organizationEventsV2/transactionView/types.tsx +++ b/src/sentry/static/sentry/app/views/organizationEventsV2/transactionView/types.tsx @@ -15,7 +15,7 @@ export type SpanEntry = { data: Array; }; -export type SentryEvent = { +export type SentryTransactionEvent = { entries: Array; startTimestamp: number; endTimestamp: number; diff --git a/src/sentry/static/sentry/app/views/organizationEventsV2/utils.jsx b/src/sentry/static/sentry/app/views/organizationEventsV2/utils.tsx similarity index 54% rename from src/sentry/static/sentry/app/views/organizationEventsV2/utils.jsx rename to src/sentry/static/sentry/app/views/organizationEventsV2/utils.tsx index 35efb27e5b7986..f688c977dd068c 100644 --- a/src/sentry/static/sentry/app/views/organizationEventsV2/utils.jsx +++ b/src/sentry/static/sentry/app/views/organizationEventsV2/utils.tsx @@ -1,5 +1,8 @@ import {partial, pick, get} from 'lodash'; +import {Location} from 'history'; +import {Client} from 'app/api'; +import {EventView} from 'app/types'; import {DEFAULT_PER_PAGE} from 'app/constants'; import {URL_PARAM} from 'app/constants/globalSelectionHeader'; import {ALL_VIEWS, AGGREGATE_ALIASES, SPECIAL_FIELDS, FIELD_FORMATTERS} from './data'; @@ -11,10 +14,18 @@ import {ALL_VIEWS, AGGREGATE_ALIASES, SPECIAL_FIELDS, FIELD_FORMATTERS} from './ * @returns {Object} * */ -export function getCurrentView(requestedView) { +export function getCurrentView(requestedView?: string): EventView { return ALL_VIEWS.find(view => view.id === requestedView) || ALL_VIEWS[0]; } +export type EventQuery = { + field: Array; + project?: string; + sort?: string | string[]; + query: string; + per_page?: number; +}; + /** * Takes a view and determines if there are any aggregate fields in it. * @@ -37,11 +48,21 @@ export function hasAggregateField(view) { * @param {Object} view * @returns {Object} */ -export function getQuery(view, location) { - const groupby = view.data.groupby ? [...view.data.groupby] : []; - const fields = get(view, 'data.fields', []); +export function getQuery(view: EventView, location: Location) { + const fields: Array = get(view, 'data.fields', []); + + type LocationQuery = { + project?: string; + environment?: string; + start?: string; + end?: string; + utc?: string; + statsPeriod?: string; + cursor?: string; + sort?: string; + }; - const data = pick(location.query, [ + const picked = pick(location.query, [ 'project', 'environment', 'start', @@ -52,13 +73,12 @@ export function getQuery(view, location) { 'sort', ]); - data.field = [...new Set(fields)]; - data.groupby = groupby; - if (!data.sort) { - data.sort = view.data.sort; - } - data.per_page = DEFAULT_PER_PAGE; - data.query = getQueryString(view, location); + const data: EventQuery = Object.assign(picked, { + field: [...new Set(fields)], + sort: picked.sort ? picked.sort : view.data.sort, + per_page: DEFAULT_PER_PAGE, + query: getQueryString(view, location), + }); return data; } @@ -72,13 +92,23 @@ export function getQuery(view, location) { * @param {Object} view defaults containing `.data.query` * @param {Location} browser location */ -export function getQueryString(view, location) { - const queryParts = []; +export function getQueryString(view: EventView, location: Location): string { + const queryParts: Array = []; if (view.data.query) { queryParts.push(view.data.query); } if (location.query && location.query.query) { - queryParts.push(location.query.query); + // there may be duplicate query in the query string + // e.g. query=hello&query=world + if (Array.isArray(location.query.query)) { + location.query.query.forEach(query => { + queryParts.push(query); + }); + } + + if (typeof location.query.query === 'string') { + queryParts.push(location.query.query); + } } return queryParts.join(' '); @@ -93,7 +123,11 @@ export function getQueryString(view, location) { * @param {Object} browser location object. * @return {Object} router target */ -export function getEventTagSearchUrl(tagKey, tagValue, location) { +export function getEventTagSearchUrl( + tagKey: string, + tagValue: string, + location: Location +) { const query = {...location.query}; // Add tag key/value to search if (query.query) { @@ -110,20 +144,39 @@ export function getEventTagSearchUrl(tagKey, tagValue, location) { }; } +export type TagTopValue = { + url: { + pathname: string; + query: any; + }; + value: string; +}; + +export type Tag = { + topValues: Array; +}; + /** * Fetches tag distributions for a single tag key * * @param {Object} api * @param {String} orgSlug * @param {String} key - * @param {string} query + * @param {String} query * @returns {Promise} */ -export function fetchTagDistribution(api, orgSlug, key, query) { +export function fetchTagDistribution( + api: Client, + orgSlug: string, + key: string, + query: EventQuery +): Promise { const urlParams = pick(query, Object.values(URL_PARAM)); + const queryOption = {...urlParams, key, query: query.query}; + return api.requestPromise(`/organizations/${orgSlug}/events-distribution/`, { - query: {...urlParams, key, query: query.query}, + query: queryOption, }); } @@ -135,14 +188,24 @@ export function fetchTagDistribution(api, orgSlug, key, query) { * @param {string} query * @returns {Promise} */ -export function fetchTotalCount(api, orgSlug, query) { +export function fetchTotalCount( + api: Client, + orgSlug: String, + query: EventQuery +): Promise { const urlParams = pick(query, Object.values(URL_PARAM)); + const queryOption = {...urlParams, query: query.query}; + + type Response = { + count: number; + }; + return api .requestPromise(`/organizations/${orgSlug}/events-meta/`, { - query: {...urlParams, query: query.query}, + query: queryOption, }) - .then(res => res.count); + .then((res: Response) => res.count); } /** @@ -152,7 +215,7 @@ export function fetchTotalCount(api, orgSlug, query) { * @param {object} metadata mapping. * @returns {Function} */ -export function getFieldRenderer(field, meta) { +export function getFieldRenderer(field: string, meta) { if (SPECIAL_FIELDS.hasOwnProperty(field)) { return SPECIAL_FIELDS[field].renderFunc; } @@ -164,3 +227,30 @@ export function getFieldRenderer(field, meta) { } return partial(FIELD_FORMATTERS.string.renderFunc, fieldName); } + +/** + * Get the first query string of a given name if there are multiple occurrences of it + * e.g. foo=42&foo=bar ==> foo=42 is the first occurrence for 'foo' and "42" will be returned. + * + * @param query query string map + * @param name name of the query string field + */ +export function getFirstQueryString( + query: {[key: string]: string | string[] | null | undefined}, + name: string, + defaultValue?: string +): string | undefined { + const needle = query[name]; + + if (typeof needle === 'string') { + return needle; + } + + if (Array.isArray(needle) && needle.length > 0) { + if (typeof needle[0] === 'string') { + return needle[0]; + } + } + + return defaultValue; +} diff --git a/yarn.lock b/yarn.lock index 4beb37e25837ea..13fcb3e50b1380 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1746,9 +1746,9 @@ "@types/node" "*" "@types/history@^3": - version "3.2.3" - resolved "https://registry.yarnpkg.com/@types/history/-/history-3.2.3.tgz#2416fee5cac641da2d05a905de5af5cb50162f60" - integrity sha512-s4SNWd31cmFP52ilv3LKCh344ayIXmfmcfExsegGspgnk/pQh75Yo6v49uzSE1oFMXp+Sz4GVnesL7rgybX9tQ== + version "3.2.4" + resolved "https://registry.yarnpkg.com/@types/history/-/history-3.2.4.tgz#0b6c62240d1fac020853aa5608758991d9f6ef3d" + integrity sha512-q7x8QeCRk2T6DR2UznwYW//mpN5uNlyajkewH2xd1s1ozCS4oHFRg2WMusxwLFlE57EkUYsd/gCapLBYzV3ffg== "@types/invariant@^2.2.29": version "2.2.29"