|
@@ -1,6 +1,7 @@
|
|
|
import {useEffect, useState} from 'react';
|
|
|
import styled from '@emotion/styled';
|
|
|
|
|
|
+import {Button, LinkButton} from 'sentry/components/button';
|
|
|
import {CompactSelect} from 'sentry/components/compactSelect';
|
|
|
import DateTime from 'sentry/components/dateTime';
|
|
|
import EmptyStateWarning from 'sentry/components/emptyStateWarning';
|
|
@@ -16,9 +17,11 @@ import OpsBreakdown from 'sentry/components/events/opsBreakdown';
|
|
|
import Link from 'sentry/components/links/link';
|
|
|
import LoadingIndicator from 'sentry/components/loadingIndicator';
|
|
|
import TextOverflow from 'sentry/components/textOverflow';
|
|
|
+import {Tooltip} from 'sentry/components/tooltip';
|
|
|
+import {IconChevron, IconOpen} from 'sentry/icons';
|
|
|
import {t} from 'sentry/locale';
|
|
|
import {space} from 'sentry/styles/space';
|
|
|
-import {EventTransaction, Group, Project} from 'sentry/types';
|
|
|
+import {EventTransaction, Project} from 'sentry/types';
|
|
|
import {defined} from 'sentry/utils';
|
|
|
import {useDiscoverQuery} from 'sentry/utils/discover/discoverQuery';
|
|
|
import EventView from 'sentry/utils/discover/eventView';
|
|
@@ -28,7 +31,9 @@ import {getShortEventId} from 'sentry/utils/events';
|
|
|
import {useApiQuery} from 'sentry/utils/queryClient';
|
|
|
import {useLocation} from 'sentry/utils/useLocation';
|
|
|
import useOrganization from 'sentry/utils/useOrganization';
|
|
|
-import {GroupEventActions} from 'sentry/views/issueDetails/groupEventCarousel';
|
|
|
+
|
|
|
+const BUTTON_ICON_SIZE = 'sm';
|
|
|
+const BUTTON_SIZE = 'sm';
|
|
|
|
|
|
export function getSampleEventQuery({
|
|
|
transaction,
|
|
@@ -92,19 +97,41 @@ function useFetchSampleEvents({
|
|
|
eventView,
|
|
|
location,
|
|
|
orgSlug: organization.slug,
|
|
|
- limit: 5,
|
|
|
+ limit: 20,
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-type EventDisplayProps = {
|
|
|
+interface NavButtonProps {
|
|
|
+ disabled: boolean;
|
|
|
+ icon: React.ReactNode;
|
|
|
+ onPaginate: () => void;
|
|
|
+ title: string;
|
|
|
+}
|
|
|
+
|
|
|
+function NavButton({title, disabled, icon, onPaginate}: NavButtonProps) {
|
|
|
+ return (
|
|
|
+ <Tooltip title={title} disabled={disabled} skipWrapper>
|
|
|
+ <div>
|
|
|
+ <StyledNavButton
|
|
|
+ size={BUTTON_SIZE}
|
|
|
+ aria-label={title}
|
|
|
+ icon={icon}
|
|
|
+ disabled={disabled}
|
|
|
+ onClick={onPaginate}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </Tooltip>
|
|
|
+ );
|
|
|
+}
|
|
|
+
|
|
|
+interface EventDisplayProps {
|
|
|
durationBaseline: number;
|
|
|
end: number;
|
|
|
eventSelectLabel: string;
|
|
|
- group: Group;
|
|
|
project: Project;
|
|
|
start: number;
|
|
|
transaction: string;
|
|
|
-};
|
|
|
+}
|
|
|
|
|
|
function EventDisplay({
|
|
|
eventSelectLabel,
|
|
@@ -113,7 +140,6 @@ function EventDisplay({
|
|
|
end,
|
|
|
transaction,
|
|
|
durationBaseline,
|
|
|
- group,
|
|
|
}: EventDisplayProps) {
|
|
|
const location = useLocation();
|
|
|
const organization = useOrganization();
|
|
@@ -140,6 +166,12 @@ function EventDisplay({
|
|
|
}
|
|
|
}, [eventIds, selectedEventId]);
|
|
|
|
|
|
+ const eventIdIndex =
|
|
|
+ eventIds && eventIds.findIndex(eventId => eventId === selectedEventId);
|
|
|
+ const hasNext =
|
|
|
+ defined(eventIdIndex) && defined(eventIds) && eventIdIndex + 1 < eventIds.length;
|
|
|
+ const hasPrev = defined(eventIdIndex) && eventIdIndex - 1 >= 0;
|
|
|
+
|
|
|
if (isError) {
|
|
|
return null;
|
|
|
}
|
|
@@ -162,30 +194,65 @@ function EventDisplay({
|
|
|
return (
|
|
|
<EventDisplayContainer>
|
|
|
<div>
|
|
|
- <StyledEventSelectorControlBar>
|
|
|
- <CompactSelect
|
|
|
- size="sm"
|
|
|
- disabled={false}
|
|
|
- options={eventIds.map(id => ({
|
|
|
- value: id,
|
|
|
- label: id,
|
|
|
- details: <DateTime date={data?.data.find(d => d.id === id)?.timestamp} />,
|
|
|
- }))}
|
|
|
- value={selectedEventId}
|
|
|
- onChange={({value}) => setSelectedEventId(value)}
|
|
|
- triggerLabel={
|
|
|
- <ButtonLabelWrapper>
|
|
|
- <TextOverflow>
|
|
|
- {eventSelectLabel}:{' '}
|
|
|
- <SelectionTextWrapper>
|
|
|
- {getShortEventId(selectedEventId)}
|
|
|
- </SelectionTextWrapper>
|
|
|
- </TextOverflow>
|
|
|
- </ButtonLabelWrapper>
|
|
|
- }
|
|
|
- />
|
|
|
- <GroupEventActions event={eventData} group={group} projectSlug={project.slug} />
|
|
|
- </StyledEventSelectorControlBar>
|
|
|
+ <StyledControlBar>
|
|
|
+ <StyledEventControls>
|
|
|
+ <CompactSelect
|
|
|
+ size="sm"
|
|
|
+ disabled={false}
|
|
|
+ options={eventIds.map(id => ({
|
|
|
+ value: id,
|
|
|
+ label: id,
|
|
|
+ details: <DateTime date={data?.data.find(d => d.id === id)?.timestamp} />,
|
|
|
+ }))}
|
|
|
+ value={selectedEventId}
|
|
|
+ onChange={({value}) => setSelectedEventId(value)}
|
|
|
+ triggerLabel={
|
|
|
+ <ButtonLabelWrapper>
|
|
|
+ <TextOverflow>
|
|
|
+ {eventSelectLabel}:{' '}
|
|
|
+ <SelectionTextWrapper>
|
|
|
+ {getShortEventId(selectedEventId)}
|
|
|
+ </SelectionTextWrapper>
|
|
|
+ </TextOverflow>
|
|
|
+ </ButtonLabelWrapper>
|
|
|
+ }
|
|
|
+ />
|
|
|
+ <LinkButton
|
|
|
+ title={t('Full Event Details')}
|
|
|
+ size={BUTTON_SIZE}
|
|
|
+ to={eventDetailsRoute({
|
|
|
+ eventSlug: generateEventSlug({project: project.slug, id: eventData.id}),
|
|
|
+ orgSlug: organization.slug,
|
|
|
+ })}
|
|
|
+ aria-label={t('Full Event Details')}
|
|
|
+ icon={<IconOpen />}
|
|
|
+ />
|
|
|
+ </StyledEventControls>
|
|
|
+ <div>
|
|
|
+ <NavButtons>
|
|
|
+ <NavButton
|
|
|
+ title={t('Previous Event')}
|
|
|
+ disabled={!hasPrev}
|
|
|
+ icon={<IconChevron direction="left" size={BUTTON_ICON_SIZE} />}
|
|
|
+ onPaginate={() => {
|
|
|
+ if (hasPrev) {
|
|
|
+ setSelectedEventId(eventIds[eventIdIndex - 1]);
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ <NavButton
|
|
|
+ title={t('Next Event')}
|
|
|
+ disabled={!hasNext}
|
|
|
+ icon={<IconChevron direction="right" size={BUTTON_ICON_SIZE} />}
|
|
|
+ onPaginate={() => {
|
|
|
+ if (hasNext) {
|
|
|
+ setSelectedEventId(eventIds[eventIdIndex + 1]);
|
|
|
+ }
|
|
|
+ }}
|
|
|
+ />
|
|
|
+ </NavButtons>
|
|
|
+ </div>
|
|
|
+ </StyledControlBar>
|
|
|
<ComparisonContentWrapper>
|
|
|
<Link
|
|
|
to={eventDetailsRoute({
|
|
@@ -241,7 +308,12 @@ const ButtonLabelWrapper = styled('span')`
|
|
|
grid-template-columns: 1fr auto;
|
|
|
`;
|
|
|
|
|
|
-const StyledEventSelectorControlBar = styled('div')`
|
|
|
+const StyledControlBar = styled('div')`
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+`;
|
|
|
+
|
|
|
+const StyledEventControls = styled('div')`
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
gap: 8px;
|
|
@@ -282,3 +354,31 @@ const EmptyStateWrapper = styled('div')`
|
|
|
const SelectionTextWrapper = styled('span')`
|
|
|
font-weight: normal;
|
|
|
`;
|
|
|
+
|
|
|
+const StyledNavButton = styled(Button)`
|
|
|
+ border-radius: 0;
|
|
|
+`;
|
|
|
+
|
|
|
+const NavButtons = styled('div')`
|
|
|
+ display: flex;
|
|
|
+
|
|
|
+ > * {
|
|
|
+ &:not(:last-child) {
|
|
|
+ ${StyledNavButton} {
|
|
|
+ border-right: none;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &:first-child {
|
|
|
+ ${StyledNavButton} {
|
|
|
+ border-radius: ${p => p.theme.borderRadius} 0 0 ${p => p.theme.borderRadius};
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ ${StyledNavButton} {
|
|
|
+ border-radius: 0 ${p => p.theme.borderRadius} ${p => p.theme.borderRadius} 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+`;
|