Skip to content

Commit

Permalink
feat(explore): Add view samples expansion to explore (#83545)
Browse files Browse the repository at this point in the history
This adds a discover style expand stack button to the start of every row
in the aggregation mode that can be used to drill down and view samples
with the attributes of the row.
  • Loading branch information
Zylphrex authored Jan 16, 2025
1 parent f28eda5 commit 4d3d547
Show file tree
Hide file tree
Showing 4 changed files with 242 additions and 41 deletions.
47 changes: 30 additions & 17 deletions static/app/views/explore/components/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
HeaderTitle,
} from 'sentry/components/gridEditable/styles';
import {space} from 'sentry/styles/space';
import {defined} from 'sentry/utils';
import {Actions} from 'sentry/views/discover/table/cellAction';

interface TableProps extends React.ComponentProps<typeof _TableWrapper> {}
Expand Down Expand Up @@ -55,8 +56,17 @@ const MINIMUM_COLUMN_WIDTH = COL_WIDTH_MINIMUM;
export function useTableStyles(
fields: string[],
tableRef: React.RefObject<HTMLDivElement>,
minimumColumnWidth = MINIMUM_COLUMN_WIDTH
options?: {
minimumColumnWidth?: number;
prefixColumnWidth?: 'min-content' | number;
}
) {
const minimumColumnWidth = options?.minimumColumnWidth ?? MINIMUM_COLUMN_WIDTH;
const prefixColumnWidth =
defined(options?.prefixColumnWidth) && typeof options.prefixColumnWidth === 'number'
? `${options.prefixColumnWidth}px`
: options?.prefixColumnWidth;

const resizingColumnIndex = useRef<number | null>(null);
const columnWidthsRef = useRef<(number | null)[]>(fields.map(() => null));

Expand All @@ -66,14 +76,15 @@ export function useTableStyles(
);
}, [fields]);

const initialTableStyles = useMemo(
() => ({
gridTemplateColumns: fields
.map(() => `minmax(${minimumColumnWidth}px, auto)`)
.join(' '),
}),
[fields, minimumColumnWidth]
);
const initialTableStyles = useMemo(() => {
const gridTemplateColumns = fields.map(() => `minmax(${minimumColumnWidth}px, auto)`);
if (defined(prefixColumnWidth)) {
gridTemplateColumns.unshift(prefixColumnWidth);
}
return {
gridTemplateColumns: gridTemplateColumns.join(' '),
};
}, [fields, minimumColumnWidth, prefixColumnWidth]);

const onResizeMouseDown = useCallback(
(event: React.MouseEvent<HTMLDivElement>, index: number) => {
Expand Down Expand Up @@ -105,13 +116,15 @@ export function useTableStyles(
columnWidthsRef.current[index] = newWidth;

// Updating the grid's `gridTemplateColumns` directly
gridElement.style.gridTemplateColumns = columnWidthsRef.current
.map(width => {
return typeof width === 'number'
? `${width}px`
: `minmax(${minimumColumnWidth}px, auto)`;
})
.join(' ');
const gridTemplateColumns = columnWidthsRef.current.map(width => {
return typeof width === 'number'
? `${width}px`
: `minmax(${minimumColumnWidth}px, auto)`;
});
if (defined(prefixColumnWidth)) {
gridTemplateColumns.unshift(prefixColumnWidth);
}
gridElement.style.gridTemplateColumns = gridTemplateColumns.join(' ');
}

function onMouseUp() {
Expand All @@ -125,7 +138,7 @@ export function useTableStyles(
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
},
[tableRef, minimumColumnWidth]
[tableRef, minimumColumnWidth, prefixColumnWidth]
);

return {initialTableStyles, onResizeMouseDown};
Expand Down
73 changes: 50 additions & 23 deletions static/app/views/explore/tables/aggregatesTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@ import styled from '@emotion/styled';

import EmptyStateWarning from 'sentry/components/emptyStateWarning';
import {GridResizer} from 'sentry/components/gridEditable/styles';
import Link from 'sentry/components/links/link';
import LoadingIndicator from 'sentry/components/loadingIndicator';
import Pagination from 'sentry/components/pagination';
import {Tooltip} from 'sentry/components/tooltip';
import {CHART_PALETTE} from 'sentry/constants/chartPalette';
import {IconArrow} from 'sentry/icons/iconArrow';
import {IconStack} from 'sentry/icons/iconStack';
import {IconWarning} from 'sentry/icons/iconWarning';
import {t} from 'sentry/locale';
import type {Confidence} from 'sentry/types/organization';
Expand All @@ -16,7 +19,9 @@ import {
parseFunction,
prettifyParsedFunction,
} from 'sentry/utils/discover/fields';
import {useLocation} from 'sentry/utils/useLocation';
import useOrganization from 'sentry/utils/useOrganization';
import useProjects from 'sentry/utils/useProjects';
import {
Table,
TableBody,
Expand All @@ -41,6 +46,7 @@ import {useSpanTags} from 'sentry/views/explore/contexts/spanTagsContext';
import {useAnalytics} from 'sentry/views/explore/hooks/useAnalytics';
import type {AggregatesTableResult} from 'sentry/views/explore/hooks/useExploreAggregatesTable';
import {TOP_EVENTS_LIMIT, useTopEvents} from 'sentry/views/explore/hooks/useTopEvents';
import {viewSamplesTarget} from 'sentry/views/explore/utils';

import {FieldRenderer} from './fieldRenderer';

Expand All @@ -53,8 +59,11 @@ export function AggregatesTable({
aggregatesTableResult,
confidences,
}: AggregatesTableProps) {
const topEvents = useTopEvents();
const location = useLocation();
const organization = useOrganization();
const {projects} = useProjects();

const topEvents = useTopEvents();
const title = useExploreTitle();
const dataset = useExploreDataset();
const groupBys = useExploreGroupBys();
Expand Down Expand Up @@ -82,7 +91,9 @@ export function AggregatesTable({
});

const tableRef = useRef<HTMLTableElement>(null);
const {initialTableStyles, onResizeMouseDown} = useTableStyles(fields, tableRef);
const {initialTableStyles, onResizeMouseDown} = useTableStyles(fields, tableRef, {
prefixColumnWidth: 'min-content',
});

const meta = result.meta ?? {};

Expand All @@ -94,6 +105,9 @@ export function AggregatesTable({
<Table ref={tableRef} styles={initialTableStyles}>
<TableHead>
<TableRow>
<TableHeadCell isFirst={false}>
<TableHeadCellContent />
</TableHeadCell>
{fields.map((field, i) => {
// Hide column names before alignment is determined
if (result.isPending) {
Expand Down Expand Up @@ -163,25 +177,35 @@ export function AggregatesTable({
<IconWarning data-test-id="error-indicator" color="gray300" size="lg" />
</TableStatus>
) : result.isFetched && result.data?.length ? (
result.data?.map((row, i) => (
<TableRow key={i}>
{fields.map((field, j) => {
return (
<TableBodyCell key={j}>
{topEvents && i < topEvents && j === 0 && (
<TopResultsIndicator index={i} />
)}
<FieldRenderer
column={columns[j]!}
data={row}
unit={meta?.units?.[field]}
meta={meta}
/>
</TableBodyCell>
);
})}
</TableRow>
))
result.data?.map((row, i) => {
const target = viewSamplesTarget(location, query, groupBys, row, {
projects,
});
return (
<TableRow key={i}>
<TableBodyCell>
{topEvents && i < topEvents && <TopResultsIndicator index={i} />}
<Tooltip title={t('View Samples')} containerDisplayMode="flex">
<StyledLink to={target}>
<IconStack />
</StyledLink>
</Tooltip>
</TableBodyCell>
{fields.map((field, j) => {
return (
<TableBodyCell key={j}>
<FieldRenderer
column={columns[j]!}
data={row}
unit={meta?.units?.[field]}
meta={meta}
/>
</TableBodyCell>
);
})}
</TableRow>
);
})
) : (
<TableStatus>
<EmptyStateWarning>
Expand All @@ -199,12 +223,15 @@ export function AggregatesTable({
const TopResultsIndicator = styled('div')<{index: number}>`
position: absolute;
left: -1px;
margin-top: 4.5px;
width: 9px;
height: 15px;
height: 16px;
border-radius: 0 3px 3px 0;
background-color: ${p => {
return CHART_PALETTE[TOP_EVENTS_LIMIT - 1]![p.index];
}};
`;

const StyledLink = styled(Link)`
display: flex;
`;
122 changes: 122 additions & 0 deletions static/app/views/explore/utils.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import {LocationFixture} from 'sentry-fixture/locationFixture';
import {ProjectFixture} from 'sentry-fixture/project';

import {viewSamplesTarget} from 'sentry/views/explore/utils';

describe('viewSamplesTarget', function () {
const project = ProjectFixture();
const extras = {projects: [project]};

it('simple drill down with no group bys', function () {
const location = LocationFixture();
const target = viewSamplesTarget(location, '', [], {}, extras);
expect(target).toMatchObject({
query: {
mode: 'samples',
query: '',
},
});
});

it('simple drill down with single group by', function () {
const location = LocationFixture();
const target = viewSamplesTarget(
location,
'',
['foo'],
{foo: 'foo', 'count()': 10},
extras
);
expect(target).toMatchObject({
query: {
mode: 'samples',
query: 'foo:foo',
},
});
});

it('simple drill down with multiple group bys', function () {
const location = LocationFixture();
const target = viewSamplesTarget(
location,
'',
['foo', 'bar', 'baz'],
{
foo: 'foo',
bar: 'bar',
baz: 'baz',
'count()': 10,
},
extras
);
expect(target).toMatchObject({
query: {
mode: 'samples',
query: 'foo:foo bar:bar baz:baz',
},
});
});

it('simple drill down with on environment', function () {
const location = LocationFixture();
const target = viewSamplesTarget(
location,
'',
['environment'],
{
environment: 'prod',
'count()': 10,
},
extras
);
expect(target).toMatchObject({
query: {
mode: 'samples',
query: '',
environment: 'prod',
},
});
});

it('simple drill down with on project id', function () {
const location = LocationFixture();
const target = viewSamplesTarget(
location,
'',
['project.id'],
{
'project.id': 1,
'count()': 10,
},
extras
);
expect(target).toMatchObject({
query: {
mode: 'samples',
query: '',
project: '1',
},
});
});

it('simple drill down with on project slug', function () {
const location = LocationFixture();
const target = viewSamplesTarget(
location,
'',
['project'],
{
project: project.slug,
'count()': 10,
},
extras
);
expect(target).toMatchObject({
query: {
mode: 'samples',
query: '',
project: String(project.id),
},
});
});
});
Loading

0 comments on commit 4d3d547

Please sign in to comment.