Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 34 additions & 1 deletion static/app/views/discover/table/cellAction.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@ import {render, screen, userEvent} from 'sentry-test/reactTestingLibrary';
import type {TableDataRow} from 'sentry/utils/discover/discoverQuery';
import {EventView} from 'sentry/utils/discover/eventView';
import {MutableSearch} from 'sentry/utils/tokenizeSearch';
import {Actions, CellAction, updateQuery} from 'sentry/views/discover/table/cellAction';
import {
Actions,
ActionTriggerType,
CellAction,
updateQuery,
} from 'sentry/views/discover/table/cellAction';
import type {TableColumn} from 'sentry/views/discover/table/types';

const defaultData: TableDataRow = {
Expand All @@ -32,6 +37,8 @@ function renderComponent({
handleCellAction = jest.fn(),
columnIndex = 0,
data = defaultData,
pin,
triggerType,
}: {
eventView: EventView;
columnIndex?: number;
Expand All @@ -40,12 +47,16 @@ function renderComponent({
action: Actions,
value: string | number | null[] | string[] | null
) => void;
pin?: React.ReactNode;
triggerType?: ActionTriggerType;
}) {
return render(
<CellAction
dataRow={data}
column={eventView.getColumns()[columnIndex]!}
handleCellAction={handleCellAction}
pin={pin}
triggerType={triggerType}
>
<strong>some content</strong>
</CellAction>
Expand Down Expand Up @@ -389,6 +400,28 @@ describe('Discover -> CellAction', () => {
expect(screen.queryByRole('button', {name: 'Actions'})).not.toBeInTheDocument();
});
});

describe('pin prop', () => {
it('renders the pin element with the bold hover trigger', () => {
renderComponent({
eventView: view,
triggerType: ActionTriggerType.BOLD_HOVER,
pin: <button type="button">pin me</button>,
});

expect(screen.getByRole('button', {name: 'pin me'})).toBeInTheDocument();
});

it('renders the pin element with the ellipsis trigger', () => {
renderComponent({
eventView: view,
triggerType: ActionTriggerType.ELLIPSIS,
pin: <button type="button">pin me</button>,
});

expect(screen.getByRole('button', {name: 'pin me'})).toBeInTheDocument();
});
});
});

describe('updateQuery()', () => {
Expand Down
18 changes: 15 additions & 3 deletions static/app/views/discover/table/cellAction.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -348,11 +348,13 @@ export enum ActionTriggerType {
}

type Props = React.PropsWithoutRef<Omit<CellActionsOpts, 'to'>> & {
pin?: React.ReactNode;
triggerType?: ActionTriggerType;
usePortalOnDropdown?: boolean;
};

export function CellAction({
pin,
triggerType = ActionTriggerType.BOLD_HOVER,
allowActions,
usePortalOnDropdown,
Expand All @@ -373,6 +375,7 @@ export function CellAction({
if (triggerType === ActionTriggerType.BOLD_HOVER) {
return (
<Container
containsPin={!!pin}
data-test-id={cellActions === null ? undefined : 'cell-action-container'}
>
{cellActions?.length ? (
Expand Down Expand Up @@ -428,12 +431,16 @@ export function CellAction({
) : (
children
)}
{pin}
Comment thread
JoshuaKGoldberg marked this conversation as resolved.
</Container>
);
}

return (
<Container data-test-id={cellActions === null ? undefined : 'cell-action-container'}>
<Container
containsPin={!!pin}
data-test-id={cellActions === null ? undefined : 'cell-action-container'}
>
{children}
{cellActions?.length && (
<DropdownMenu
Expand Down Expand Up @@ -462,13 +469,18 @@ export function CellAction({
)}
/>
)}
{pin}
</Container>
);
}

const Container = styled('div')`
const Container = styled('div')<{containsPin?: boolean}>`
--logsPinButtonArea: 2rem;
position: relative;
width: 100%;
width: ${p =>
p.containsPin
? `calc(100% - var(--logsPinButtonArea) + ${p.theme.space.md})`
: `100%`};
height: 100%;
display: flex;
flex-direction: column;
Expand Down
135 changes: 135 additions & 0 deletions static/app/views/explore/logs/pinning/PinnedLogs.spec.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import {LogFixture} from 'sentry-fixture/log';
import {OrganizationFixture} from 'sentry-fixture/organization';

import {
render,
screen,
userEvent,
type RenderOptions,
} from 'sentry-test/reactTestingLibrary';

import {PinnedLogs} from 'sentry/views/explore/logs/pinning/PinnedLogs';
import {LogsPinningProvider} from 'sentry/views/explore/logs/pinning/useLogsPinning';
import {OurLogKnownFieldKey} from 'sentry/views/explore/logs/types';
import type {LogTableRowItem} from 'sentry/views/explore/logs/utils';

const allRows: LogTableRowItem[] = [
LogFixture({
[OurLogKnownFieldKey.ID]: 'log-1',
[OurLogKnownFieldKey.PROJECT_ID]: '1',
[OurLogKnownFieldKey.ORGANIZATION_ID]: 1,
[OurLogKnownFieldKey.MESSAGE]: 'first pinned log',
}),
LogFixture({
[OurLogKnownFieldKey.ID]: 'log-2',
[OurLogKnownFieldKey.PROJECT_ID]: '1',
[OurLogKnownFieldKey.ORGANIZATION_ID]: 1,
[OurLogKnownFieldKey.MESSAGE]: 'second pinned log',
}),
];

const renderRow = (dataRow: LogTableRowItem) => (
<tr data-test-id={`pinned-row-${dataRow[OurLogKnownFieldKey.ID]}`}>
<td>{dataRow[OurLogKnownFieldKey.MESSAGE]}</td>
</tr>
);

function renderPinnedLogs(options: RenderOptions = {}) {
return render(
<table>
<LogsPinningProvider>
<PinnedLogs allRows={allRows} renderRow={renderRow} />
</LogsPinningProvider>
</table>,
{
organization: OrganizationFixture({features: ['ourlogs-pinning']}),
...options,
}
);
}

describe('PinnedLogs', () => {
it('renders nothing when the feature is disabled even if rows are pinned', () => {
renderPinnedLogs({
organization: OrganizationFixture({features: []}),
initialRouterConfig: {
location: {pathname: '/', query: {logsPinned: 'log-1'}},
},
});

expect(screen.queryByRole('toolbar')).not.toBeInTheDocument();
});

it('renders nothing when no rows are pinned', () => {
renderPinnedLogs();

expect(screen.queryByRole('toolbar')).not.toBeInTheDocument();
});

it('renders the pinned row when its id is present in allRows', () => {
renderPinnedLogs({
initialRouterConfig: {
location: {pathname: '/', query: {logsPinned: 'log-1'}},
},
});

expect(screen.getByTestId('pinned-row-log-1')).toBeInTheDocument();
});

it('does not render a row when the pinned id is missing from allRows', () => {
renderPinnedLogs({
initialRouterConfig: {
location: {pathname: '/', query: {logsPinned: 'missing-log'}},
},
});

expect(screen.queryByTestId('pinned-row-missing-log')).not.toBeInTheDocument();
});

it('shows the count of pinned rows in the collapse toggle label', () => {
renderPinnedLogs({
initialRouterConfig: {
location: {pathname: '/', query: {logsPinned: ['log-1', 'log-2']}},
},
});

expect(screen.getByRole('button', {name: 'Collapse 2 pinned'})).toBeInTheDocument();
});

it('hides the rendered pinned rows when the collapse button is clicked', async () => {
renderPinnedLogs({
initialRouterConfig: {
location: {pathname: '/', query: {logsPinned: 'log-1'}},
},
});

await userEvent.click(screen.getByRole('button', {name: 'Collapse 1 pinned'}));

expect(screen.queryByTestId('pinned-row-log-1')).not.toBeInTheDocument();
});

it('shows the rendered pinned rows again when the toggle button is clicked twice', async () => {
renderPinnedLogs({
initialRouterConfig: {
location: {pathname: '/', query: {logsPinned: 'log-1'}},
},
});

await userEvent.click(screen.getByRole('button', {name: 'Collapse 1 pinned'}));
await userEvent.click(screen.getByRole('button', {name: 'Expand 1 pinned'}));

expect(screen.getByTestId('pinned-row-log-1')).toBeInTheDocument();
});

it('removes the rendered pinned rows when the Clear all button is clicked', async () => {
renderPinnedLogs({
initialRouterConfig: {
location: {pathname: '/', query: {logsPinned: 'log-1'}},
},
});

await userEvent.click(screen.getByRole('button', {name: 'Clear all pins'}));

expect(screen.queryByTestId('pinned-row-log-1')).not.toBeInTheDocument();
});
});
89 changes: 89 additions & 0 deletions static/app/views/explore/logs/pinning/PinnedLogs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import {Fragment, useEffect, useState} from 'react';
import styled from '@emotion/styled';

import {Button} from '@sentry/scraps/button';
import {Flex} from '@sentry/scraps/layout';

import {GridRow} from 'sentry/components/tables/gridEditable/styles';
import {IconChevron, IconClose} from 'sentry/icons';
import {t} from 'sentry/locale';
import {TableBody} from 'sentry/views/explore/components/table';
import {useLogsPinning} from 'sentry/views/explore/logs/pinning/useLogsPinning';
import {OurLogKnownFieldKey} from 'sentry/views/explore/logs/types';
import type {LogTableRowItem} from 'sentry/views/explore/logs/utils';

interface Props {
allRows: LogTableRowItem[];
renderRow: (dataRow: LogTableRowItem) => React.ReactNode;
}

export function PinnedLogs({allRows, renderRow}: Props) {
const logsPinning = useLogsPinning();
const [expanded, setExpanded] = useState(true);
const pinsCount = logsPinning?.pinnedRows.size;

useEffect(() => {
if (!pinsCount) {
setExpanded(true);
}
}, [pinsCount]);

if (!logsPinning || !pinsCount) {
return null;
}

return (
<PinnedTableBody>
{expanded &&
Array.from(logsPinning.pinnedRows).map(rowId => {
const dataRow = allRows.find(datum => datum[OurLogKnownFieldKey.ID] === rowId);

// TODO(LOGS-781): this is not correct yet because the virtualizer might not have found it yet.
// Will have to manually re-fetch data.
if (!dataRow) {
Comment thread
JoshuaKGoldberg marked this conversation as resolved.
return null;
}

return <Fragment key={rowId}>{renderRow(dataRow)}</Fragment>;
})}
<GridRow role="toolbar">
<PinnedGridBodyCell>
<Flex justify="end">
<Button
size="xs"
icon={<IconChevron size="xs" direction={expanded ? 'up' : 'down'} />}
onClick={() => setExpanded(previous => !previous)}
>
{expanded
? t('Collapse %s pinned', pinsCount)
: t('Expand %s pinned', pinsCount)}
</Button>
<Button
aria-label={t('Clear all pins')}
icon={<IconClose size="xs" />}
onClick={logsPinning.clearPinnedRows}
size="xs"
variant="transparent"
>
{t('Clear all')}
</Button>
</Flex>
</PinnedGridBodyCell>
</GridRow>
</PinnedTableBody>
);
}

const PinnedTableBody = styled(TableBody)`
border-bottom: 1px solid ${p => p.theme.tokens.border.primary};
height: max-content;
overflow-y: auto;
overflow-x: hidden;
scrollbar-gutter: stable;
scrollbar-width: thin;
`;

const PinnedGridBodyCell = styled('td')`
grid-column: 1 / -1;
padding: ${p => p.theme.space.sm};
`;
Loading
Loading