Просмотр исходного кода

feat(app-start): Merge samples tables (#64681)

This PR merges the span ops table with the event samples table.

 In doing so I've also made the following changes:
- Add a change column for comparing cold/warm start durations in the
span op table
- Apply the span op, start type, and device class filters to the span op
table and start type and device class for the event samples
    - I had to leave out the span op filter from the event samples table
because it's going to be more difficult to implement and I felt like
this PR was big enough
- Add props to shared components so we can style them differently before
the new style is propagated to the screens module (since it's in prod
currently)
Nar Saynorath 1 год назад
Родитель
Сommit
d31e3ac708

+ 2 - 0
static/app/views/starfish/types.tsx

@@ -35,6 +35,8 @@ export enum SpanMetricsField {
   HTTP_RESPONSE_TRANSFER_SIZE = 'http.response_transfer_size',
   FILE_EXTENSION = 'file_extension',
   OS_NAME = 'os.name',
+  APP_START_TYPE = 'app_start_type',
+  DEVICE_CLASS = 'device.class',
 }
 
 export type SpanNumberFields =

+ 14 - 2
static/app/views/starfish/views/appStartup/screenSummary/eventSamples.tsx

@@ -12,6 +12,11 @@ import {
   SECONDARY_RELEASE_ALIAS,
 } from 'sentry/views/starfish/components/releaseSelector';
 import {useReleaseSelection} from 'sentry/views/starfish/queries/useReleases';
+import {SpanMetricsField} from 'sentry/views/starfish/types';
+import {
+  COLD_START_TYPE,
+  WARM_START_TYPE,
+} from 'sentry/views/starfish/views/appStartup/screenSummary/startTypeSelector';
 import {EventSamplesTable} from 'sentry/views/starfish/views/screens/screenLoadSpans/eventSamplesTable';
 import {useTableQuery} from 'sentry/views/starfish/views/screens/screensTable';
 
@@ -25,6 +30,7 @@ type Props = {
   release: string;
   sortKey: string;
   transaction: string;
+  footerAlignedPagination?: boolean;
   showDeviceClassSelector?: boolean;
 };
 
@@ -34,12 +40,16 @@ export function EventSamples({
   release,
   sortKey,
   showDeviceClassSelector,
+  footerAlignedPagination,
 }: Props) {
   const location = useLocation();
   const {selection} = usePageFilters();
   const {primaryRelease} = useReleaseSelection();
   const cursor = decodeScalar(location.query?.[cursorName]);
 
+  const deviceClass = decodeScalar(location.query[SpanMetricsField.DEVICE_CLASS]) ?? '';
+  const startType = decodeScalar(location.query[SpanMetricsField.APP_START_TYPE]) ?? '';
+
   const searchQuery = new MutableSearch([
     `transaction:${transaction}`,
     `release:${release}`,
@@ -49,10 +59,11 @@ export function EventSamples({
     'OR',
     'span.description:"Warm Start"',
     ')',
+    `${SpanMetricsField.APP_START_TYPE}:${
+      startType || `[${COLD_START_TYPE},${WARM_START_TYPE}]`
+    }`,
   ]);
 
-  const deviceClass = decodeScalar(location.query['device.class']);
-
   if (deviceClass) {
     if (deviceClass === 'Unknown') {
       searchQuery.addFilterValue('!has', 'device.class');
@@ -112,6 +123,7 @@ export function EventSamples({
       showDeviceClassSelector={showDeviceClassSelector}
       columnNameMap={columnNameMap}
       sort={sort}
+      footerAlignedPagination={footerAlignedPagination}
     />
   );
 }

+ 5 - 40
static/app/views/starfish/views/appStartup/screenSummary/index.tsx

@@ -23,13 +23,8 @@ import {
   SECONDARY_RELEASE_ALIAS,
 } from 'sentry/views/starfish/components/releaseSelector';
 import {SpanMetricsField} from 'sentry/views/starfish/types';
-import {EventSamples} from 'sentry/views/starfish/views/appStartup/screenSummary/eventSamples';
-import {SpanOperationTable} from 'sentry/views/starfish/views/appStartup/screenSummary/spanOperationTable';
+import {SamplesTables} from 'sentry/views/starfish/views/appStartup/screenSummary/samples';
 import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
-import {
-  MobileCursors,
-  MobileSortKeys,
-} from 'sentry/views/starfish/views/screens/constants';
 import {MetricsRibbon} from 'sentry/views/starfish/views/screens/screenLoadSpans/metricsRibbon';
 import {ScreenLoadSpanSamples} from 'sentry/views/starfish/views/screens/screenLoadSpans/samples';
 
@@ -194,36 +189,9 @@ function ScreenSummary() {
               <ErrorBoundary mini>
                 <AppStartWidgets additionalFilters={[`transaction:${transactionName}`]} />
               </ErrorBoundary>
-              <EventSamplesContainer>
-                <ErrorBoundary mini>
-                  <div>
-                    <EventSamples
-                      cursorName={MobileCursors.RELEASE_1_EVENT_SAMPLE_TABLE}
-                      sortKey={MobileSortKeys.RELEASE_1_EVENT_SAMPLE_TABLE}
-                      release={primaryRelease}
-                      transaction={transactionName}
-                      showDeviceClassSelector
-                    />
-                  </div>
-                </ErrorBoundary>
-                <ErrorBoundary mini>
-                  <div>
-                    <EventSamples
-                      cursorName={MobileCursors.RELEASE_2_EVENT_SAMPLE_TABLE}
-                      sortKey={MobileSortKeys.RELEASE_2_EVENT_SAMPLE_TABLE}
-                      release={secondaryRelease}
-                      transaction={transactionName}
-                    />
-                  </div>
-                </ErrorBoundary>
-              </EventSamplesContainer>
-              <ErrorBoundary mini>
-                <SpanOperationTable
-                  transaction={transactionName}
-                  primaryRelease={primaryRelease}
-                  secondaryRelease={secondaryRelease}
-                />
-              </ErrorBoundary>
+              <SamplesContainer>
+                <SamplesTables transactionName={transactionName} />
+              </SamplesContainer>
               {spanGroup && spanOp && (
                 <ScreenLoadSpanSamples
                   groupId={spanGroup}
@@ -265,9 +233,6 @@ const Container = styled('div')`
   }
 `;
 
-const EventSamplesContainer = styled('div')`
-  display: grid;
-  grid-template-columns: 1fr 1fr;
+const SamplesContainer = styled('div')`
   margin-top: ${space(2)};
-  gap: ${space(2)};
 `;

+ 112 - 0
static/app/views/starfish/views/appStartup/screenSummary/samples.tsx

@@ -0,0 +1,112 @@
+import {useMemo, useState} from 'react';
+import styled from '@emotion/styled';
+
+import ErrorBoundary from 'sentry/components/errorBoundary';
+import {SegmentedControl} from 'sentry/components/segmentedControl';
+import {t} from 'sentry/locale';
+import {space} from 'sentry/styles/space';
+import {useReleaseSelection} from 'sentry/views/starfish/queries/useReleases';
+import {EventSamples} from 'sentry/views/starfish/views/appStartup/screenSummary/eventSamples';
+import {SpanOperationTable} from 'sentry/views/starfish/views/appStartup/screenSummary/spanOperationTable';
+import {SpanOpSelector} from 'sentry/views/starfish/views/appStartup/screenSummary/spanOpSelector';
+import {StartTypeSelector} from 'sentry/views/starfish/views/appStartup/screenSummary/startTypeSelector';
+import {
+  MobileCursors,
+  MobileSortKeys,
+} from 'sentry/views/starfish/views/screens/constants';
+import {DeviceClassSelector} from 'sentry/views/starfish/views/screens/screenLoadSpans/deviceClassSelector';
+
+const EVENT = 'event';
+const SPANS = 'spans';
+
+export function SamplesTables({transactionName}) {
+  const [sampleType, setSampleType] = useState<typeof EVENT | typeof SPANS>(SPANS);
+  const {primaryRelease, secondaryRelease} = useReleaseSelection();
+
+  const content = useMemo(() => {
+    if (sampleType === EVENT) {
+      return (
+        <EventSplitContainer>
+          <ErrorBoundary mini>
+            {primaryRelease && (
+              <div>
+                <EventSamples
+                  cursorName={MobileCursors.RELEASE_1_EVENT_SAMPLE_TABLE}
+                  sortKey={MobileSortKeys.RELEASE_1_EVENT_SAMPLE_TABLE}
+                  release={primaryRelease}
+                  transaction={transactionName}
+                  footerAlignedPagination
+                />
+              </div>
+            )}
+          </ErrorBoundary>
+          <ErrorBoundary mini>
+            {secondaryRelease && (
+              <div>
+                <EventSamples
+                  cursorName={MobileCursors.RELEASE_2_EVENT_SAMPLE_TABLE}
+                  sortKey={MobileSortKeys.RELEASE_2_EVENT_SAMPLE_TABLE}
+                  release={secondaryRelease}
+                  transaction={transactionName}
+                  footerAlignedPagination
+                />
+              </div>
+            )}
+          </ErrorBoundary>
+        </EventSplitContainer>
+      );
+    }
+
+    return (
+      <ErrorBoundary mini>
+        <SpanOperationTable
+          transaction={transactionName}
+          primaryRelease={primaryRelease}
+          secondaryRelease={secondaryRelease}
+        />
+      </ErrorBoundary>
+    );
+  }, [primaryRelease, sampleType, secondaryRelease, transactionName]);
+
+  return (
+    <div>
+      <Controls>
+        <FiltersContainer>
+          {sampleType === SPANS && (
+            <SpanOpSelector
+              primaryRelease={primaryRelease}
+              transaction={transactionName}
+              secondaryRelease={secondaryRelease}
+            />
+          )}
+          <StartTypeSelector />
+          <DeviceClassSelector size="md" clearSpansTableCursor />
+        </FiltersContainer>
+        <SegmentedControl onChange={value => setSampleType(value)} defaultValue={SPANS}>
+          <SegmentedControl.Item key={SPANS}>{t('By Spans')}</SegmentedControl.Item>
+          <SegmentedControl.Item key={EVENT}>{t('By Event')}</SegmentedControl.Item>
+        </SegmentedControl>
+      </Controls>
+      {content}
+    </div>
+  );
+}
+
+const EventSplitContainer = styled('div')`
+  display: grid;
+  grid-template-columns: 1fr 1fr;
+  gap: ${space(1.5)}
+`;
+
+const Controls = styled('div')`
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: ${space(1)};
+`;
+
+const FiltersContainer = styled('div')`
+  display: flex;
+  gap: ${space(1)};
+  align-items: center;
+`;

+ 2 - 8
static/app/views/starfish/views/appStartup/screenSummary/spanOpSelector.tsx

@@ -1,9 +1,7 @@
 import {browserHistory} from 'react-router';
-import styled from '@emotion/styled';
 
 import {CompactSelect} from 'sentry/components/compactSelect';
 import {t} from 'sentry/locale';
-import {space} from 'sentry/styles/space';
 import type {NewQuery} from 'sentry/types';
 import EventView from 'sentry/utils/discover/eventView';
 import {DiscoverDatasets} from 'sentry/utils/discover/types';
@@ -73,8 +71,8 @@ export function SpanOpSelector({transaction, primaryRelease, secondaryRelease}:
   ];
 
   return (
-    <StyledCompactSelect
-      triggerProps={{prefix: t('Operation'), size: 'xs'}}
+    <CompactSelect
+      triggerProps={{prefix: t('Operation')}}
       value={value}
       options={options ?? []}
       onChange={newValue => {
@@ -90,7 +88,3 @@ export function SpanOpSelector({transaction, primaryRelease, secondaryRelease}:
     />
   );
 }
-
-const StyledCompactSelect = styled(CompactSelect)`
-  margin-bottom: ${space(1)};
-`;

+ 6 - 8
static/app/views/starfish/views/appStartup/screenSummary/spanOperationTable.spec.tsx

@@ -48,7 +48,7 @@ describe('SpanOpSelector', function () {
             'span.description': 'string',
             'span.group': 'string',
             'avg_if(span.self_time,release,release1)': 'duration',
-            'time_spent_percentage()': 'percentage',
+            'avg_compare(span.self_time,release,release1,release2)': 'percent_change',
             'count()': 'integer',
             'avg_if(span.self_time,release,release2)': 'duration',
             'sum(span.self_time)': 'duration',
@@ -61,7 +61,7 @@ describe('SpanOpSelector', function () {
             'span.description': 'Application Init',
             'span.group': '7f4be68f08c0455f',
             'avg_if(span.self_time,release,release1)': 22.549867,
-            'time_spent_percentage()': 0.003017625053431528,
+            'avg_compare(span.self_time,release,release1,release2)': 0.5,
             'count()': 14,
             'avg_if(span.self_time,release,release2)': 12504.931908384617,
             'sum(span.self_time)': 162586.66467600001,
@@ -82,17 +82,15 @@ describe('SpanOpSelector', function () {
 
     expect(await screen.findByRole('link', {name: 'Operation'})).toBeInTheDocument();
     expect(screen.getByRole('link', {name: 'Span Description'})).toBeInTheDocument();
-    expect(screen.getByRole('link', {name: 'Duration (R1)'})).toBeInTheDocument();
-    expect(screen.getByRole('link', {name: 'Duration (R2)'})).toBeInTheDocument();
-    expect(screen.getByRole('link', {name: 'Total Count'})).toBeInTheDocument();
-    expect(screen.getByRole('link', {name: 'Total Time Spent'})).toBeInTheDocument();
+    expect(screen.getByRole('link', {name: 'Avg Duration (R1)'})).toBeInTheDocument();
+    expect(screen.getByRole('link', {name: 'Avg Duration (R2)'})).toBeInTheDocument();
+    expect(screen.getByRole('link', {name: 'Change'})).toBeInTheDocument();
 
     expect(await screen.findByRole('cell', {name: 'app.start.warm'})).toBeInTheDocument();
     expect(screen.getByRole('cell', {name: 'Application Init'})).toBeInTheDocument();
     expect(screen.getByRole('cell', {name: '22.55ms'})).toBeInTheDocument();
     expect(screen.getByRole('cell', {name: '12.50s'})).toBeInTheDocument();
-    expect(screen.getByRole('cell', {name: '14'})).toBeInTheDocument();
-    expect(screen.getByRole('cell', {name: '2.71min'})).toBeInTheDocument();
+    expect(screen.getByRole('cell', {name: '+50%'})).toBeInTheDocument();
 
     expect(screen.getByRole('link', {name: 'Application Init'})).toHaveAttribute(
       'href',

+ 29 - 20
static/app/views/starfish/views/appStartup/screenSummary/spanOperationTable.tsx

@@ -32,13 +32,22 @@ import {OverflowEllipsisTextContainer} from 'sentry/views/starfish/components/te
 import {SpanMetricsField} from 'sentry/views/starfish/types';
 import {STARFISH_CHART_INTERVAL_FIDELITY} from 'sentry/views/starfish/utils/constants';
 import {appendReleaseFilters} from 'sentry/views/starfish/utils/releaseComparison';
-import {SpanOpSelector} from 'sentry/views/starfish/views/appStartup/screenSummary/spanOpSelector';
+import {
+  COLD_START_TYPE,
+  WARM_START_TYPE,
+} from 'sentry/views/starfish/views/appStartup/screenSummary/startTypeSelector';
 import {QueryParameterNames} from 'sentry/views/starfish/views/queryParameters';
 import {MobileCursors} from 'sentry/views/starfish/views/screens/constants';
 import {useTableQuery} from 'sentry/views/starfish/views/screens/screensTable';
 
-const {SPAN_SELF_TIME, SPAN_DESCRIPTION, SPAN_GROUP, SPAN_OP, PROJECT_ID} =
-  SpanMetricsField;
+const {
+  SPAN_SELF_TIME,
+  SPAN_DESCRIPTION,
+  SPAN_GROUP,
+  SPAN_OP,
+  PROJECT_ID,
+  APP_START_TYPE,
+} = SpanMetricsField;
 
 type Props = {
   primaryRelease?: string;
@@ -67,6 +76,8 @@ export function SpanOperationTable({
   const cursor = decodeScalar(location.query?.[MobileCursors.SPANS_TABLE]);
 
   const spanOp = decodeScalar(location.query[SpanMetricsField.SPAN_OP]) ?? '';
+  const startType = decodeScalar(location.query[SpanMetricsField.APP_START_TYPE]) ?? '';
+  const deviceClass = decodeScalar(location.query[SpanMetricsField.DEVICE_CLASS]) ?? '';
 
   const searchQuery = new MutableSearch([
     'transaction.op:ui.load',
@@ -75,9 +86,11 @@ export function SpanOperationTable({
     // Exclude root level spans because they're comprised of nested operations
     '!span.description:"Cold Start"',
     '!span.description:"Warm Start"',
-    ...(spanOp
-      ? [`${SpanMetricsField.SPAN_OP}:${spanOp}`]
-      : [`span.op:[${[...STARTUP_SPANS].join(',')}]`]),
+    `${SpanMetricsField.APP_START_TYPE}:${
+      startType || `[${COLD_START_TYPE},${WARM_START_TYPE}]`
+    }`,
+    `${SpanMetricsField.SPAN_OP}:${spanOp || `[${[...STARTUP_SPANS].join(',')}]`}`,
+    ...(deviceClass ? [`${SpanMetricsField.DEVICE_CLASS}:${deviceClass}`] : []),
   ]);
   const queryStringPrimary = appendReleaseFilters(
     searchQuery,
@@ -89,7 +102,7 @@ export function SpanOperationTable({
     decodeScalar(location.query[QueryParameterNames.SPANS_SORT])
   )[0] ?? {
     kind: 'desc',
-    field: 'time_spent_percentage()',
+    field: `avg_compare(${SPAN_SELF_TIME},release,${primaryRelease},${secondaryRelease})`,
   };
 
   const newQuery: NewQuery = {
@@ -101,8 +114,8 @@ export function SpanOperationTable({
       SPAN_DESCRIPTION,
       `avg_if(${SPAN_SELF_TIME},release,${primaryRelease})`,
       `avg_if(${SPAN_SELF_TIME},release,${secondaryRelease})`,
-      'count()',
-      'time_spent_percentage()',
+      `avg_compare(${SPAN_SELF_TIME},release,${primaryRelease},${secondaryRelease})`,
+      SpanMetricsField.APP_START_TYPE,
       `sum(${SPAN_SELF_TIME})`,
     ],
     query: queryStringPrimary,
@@ -125,16 +138,17 @@ export function SpanOperationTable({
   const columnNameMap = {
     [SPAN_OP]: t('Operation'),
     [SPAN_DESCRIPTION]: t('Span Description'),
-    'count()': t('Total Count'),
     [`avg_if(${SPAN_SELF_TIME},release,${primaryRelease})`]: t(
-      'Duration (%s)',
+      'Avg Duration (%s)',
       PRIMARY_RELEASE_ALIAS
     ),
     [`avg_if(${SPAN_SELF_TIME},release,${secondaryRelease})`]: t(
-      'Duration (%s)',
+      'Avg Duration (%s)',
       SECONDARY_RELEASE_ALIAS
     ),
-    ['time_spent_percentage()']: t('Total Time Spent'),
+    [`avg_compare(${SPAN_SELF_TIME},release,${primaryRelease},${secondaryRelease})`]:
+      t('Change'),
+    [APP_START_TYPE]: t('Start Type'),
   };
 
   function renderBodyCell(column, row): React.ReactNode {
@@ -231,21 +245,16 @@ export function SpanOperationTable({
 
   return (
     <Fragment>
-      <SpanOpSelector
-        primaryRelease={primaryRelease}
-        transaction={transaction}
-        secondaryRelease={secondaryRelease}
-      />
       <GridEditable
         isLoading={isLoading}
         data={data?.data as TableDataRow[]}
         columnOrder={[
+          APP_START_TYPE,
           String(SPAN_OP),
           String(SPAN_DESCRIPTION),
           `avg_if(${SPAN_SELF_TIME},release,${primaryRelease})`,
           `avg_if(${SPAN_SELF_TIME},release,${secondaryRelease})`,
-          'count()',
-          'time_spent_percentage()',
+          `avg_compare(${SPAN_SELF_TIME},release,${primaryRelease},${secondaryRelease})`,
         ].map(col => {
           return {key: col, name: columnNameMap[col] ?? col, width: COL_WIDTH_UNDEFINED};
         })}

+ 43 - 0
static/app/views/starfish/views/appStartup/screenSummary/startTypeSelector.tsx

@@ -0,0 +1,43 @@
+import {browserHistory} from 'react-router';
+
+import {CompactSelect} from 'sentry/components/compactSelect';
+import {t} from 'sentry/locale';
+import {decodeScalar} from 'sentry/utils/queryString';
+import {useLocation} from 'sentry/utils/useLocation';
+import {SpanMetricsField} from 'sentry/views/starfish/types';
+import {MobileCursors} from 'sentry/views/starfish/views/screens/constants';
+
+export const COLD_START_TYPE = 'cold';
+export const WARM_START_TYPE = 'warm';
+
+export function StartTypeSelector() {
+  const location = useLocation();
+
+  const value = decodeScalar(location.query[SpanMetricsField.APP_START_TYPE]) ?? '';
+
+  const options = [
+    {value: '', label: t('All')},
+    {value: COLD_START_TYPE, label: t('Cold')},
+    {value: WARM_START_TYPE, label: t('Warm')},
+  ];
+
+  return (
+    <CompactSelect
+      triggerProps={{prefix: t('Start Type')}}
+      value={value}
+      options={options ?? []}
+      onChange={newValue => {
+        browserHistory.push({
+          ...location,
+          query: {
+            ...location.query,
+            [SpanMetricsField.APP_START_TYPE]: newValue.value,
+            [MobileCursors.RELEASE_1_EVENT_SAMPLE_TABLE]: undefined,
+            [MobileCursors.RELEASE_2_EVENT_SAMPLE_TABLE]: undefined,
+            [MobileCursors.SPANS_TABLE]: undefined,
+          },
+        });
+      }}
+    />
+  );
+}

+ 10 - 2
static/app/views/starfish/views/screens/screenLoadSpans/deviceClassSelector.tsx

@@ -1,3 +1,4 @@
+import type {ComponentProps} from 'react';
 import {browserHistory} from 'react-router';
 
 import {CompactSelect} from 'sentry/components/compactSelect';
@@ -6,7 +7,12 @@ import {decodeScalar} from 'sentry/utils/queryString';
 import {useLocation} from 'sentry/utils/useLocation';
 import {MobileCursors} from 'sentry/views/starfish/views/screens/constants';
 
-export function DeviceClassSelector() {
+interface Props {
+  clearSpansTableCursor?: boolean;
+  size?: ComponentProps<typeof CompactSelect>['size'];
+}
+
+export function DeviceClassSelector({size = 'xs', clearSpansTableCursor}: Props) {
   const location = useLocation();
 
   const value = decodeScalar(location.query['device.class']) ?? '';
@@ -21,7 +27,8 @@ export function DeviceClassSelector() {
 
   return (
     <CompactSelect
-      triggerProps={{prefix: t('Device Class'), size: 'xs'}}
+      size={size}
+      triggerProps={{prefix: t('Device Class')}}
       value={value}
       options={options ?? []}
       onChange={newValue => {
@@ -32,6 +39,7 @@ export function DeviceClassSelector() {
             ['device.class']: newValue.value,
             [MobileCursors.RELEASE_1_EVENT_SAMPLE_TABLE]: undefined,
             [MobileCursors.RELEASE_2_EVENT_SAMPLE_TABLE]: undefined,
+            ...(clearSpansTableCursor ? {[MobileCursors.SPANS_TABLE]: undefined} : {}),
           },
         });
       }}

+ 14 - 4
static/app/views/starfish/views/screens/screenLoadSpans/eventSamplesTable.tsx

@@ -38,6 +38,7 @@ type Props = {
   sort: Sort;
   sortKey: string;
   data?: TableData;
+  footerAlignedPagination?: boolean;
   pageLinks?: string;
   showDeviceClassSelector?: boolean;
 };
@@ -56,6 +57,7 @@ export function EventSamplesTable({
   profileIdKey,
   columnNameMap,
   sort,
+  footerAlignedPagination = false,
 }: Props) {
   const location = useLocation();
   const organization = useOrganization();
@@ -169,10 +171,13 @@ export function EventSamplesTable({
 
   return (
     <Fragment>
-      <Header>
-        {showDeviceClassSelector && <DeviceClassSelector />}
-        <StyledPagination size="xs" pageLinks={pageLinks} onCursor={handleCursor} />
-      </Header>
+      {!footerAlignedPagination && (
+        <Header>
+          {showDeviceClassSelector && <DeviceClassSelector />}
+
+          <StyledPagination size="xs" pageLinks={pageLinks} onCursor={handleCursor} />
+        </Header>
+      )}
       <GridContainer>
         <GridEditable
           isLoading={isLoading}
@@ -190,6 +195,11 @@ export function EventSamplesTable({
           }}
         />
       </GridContainer>
+      <div>
+        {footerAlignedPagination && (
+          <StyledPagination size="xs" pageLinks={pageLinks} onCursor={handleCursor} />
+        )}
+      </div>
     </Fragment>
   );
 }