Skip to content

feat: Add click + sidepanel support to items within surrounding context #989

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
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
5 changes: 5 additions & 0 deletions .changeset/sharp-snails-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---

feat: Add click + sidepanel support to items within surrounding context
238 changes: 158 additions & 80 deletions packages/app/src/components/ContextSidePanel.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useCallback, useMemo, useState } from 'react';
import { sq } from 'date-fns/locale';
import ms from 'ms';
import { parseAsString, useQueryState } from 'nuqs';
import { useForm } from 'react-hook-form';
import { tcFromSource } from '@hyperdx/common-utils/dist/metadata';
import {
Expand All @@ -13,8 +14,11 @@ import { useDebouncedValue } from '@mantine/hooks';
import { SQLInlineEditorControlled } from '@/components/SQLInlineEditor';
import WhereLanguageControlled from '@/components/WhereLanguageControlled';
import SearchInputV2 from '@/SearchInputV2';
import { useSource } from '@/source';
import { formatAttributeClause } from '@/utils';

import DBRowSidePanel from './DBRowSidePanel';
import { BreadcrumbPath } from './DBRowSidePanelHeader';
import { DBSqlRowTable } from './DBRowTable';

enum ContextBy {
Expand All @@ -31,13 +35,40 @@ interface ContextSubpanelProps {
dbSqlRowTableConfig: ChartConfigWithDateRange | undefined;
rowData: Record<string, any>;
rowId: string | undefined;
breadcrumbPath?: BreadcrumbPath;
}

// Custom hook to manage nested panel state
function useNestedPanelState(isNested: boolean) {
// Query state (URL-based) for root level
const queryState = {
contextRowId: useQueryState('contextRowId', parseAsString),
contextRowSource: useQueryState('contextRowSource', parseAsString),
};

// Local state for nested levels
const localState = {
contextRowId: useState<string | null>(null),
contextRowSource: useState<string | null>(null),
};

// Choose which state to use based on nesting level
const activeState = isNested ? localState : queryState;

return {
contextRowId: activeState.contextRowId[0],
contextRowSource: activeState.contextRowSource[0],
setContextRowId: activeState.contextRowId[1],
setContextRowSource: activeState.contextRowSource[1],
};
}

export default function ContextSubpanel({
source,
dbSqlRowTableConfig,
rowData,
rowId,
breadcrumbPath = [],
}: ContextSubpanelProps) {
const QUERY_KEY_PREFIX = 'context';
const { Timestamp: origTimestamp } = rowData;
Expand All @@ -55,6 +86,33 @@ export default function ContextSubpanel({
const formWhere = watch('where');
const [debouncedWhere] = useDebouncedValue(formWhere, 1000);

// State management for nested panels
const isNested = breadcrumbPath.length > 0;

const {
contextRowId,
contextRowSource,
setContextRowId,
setContextRowSource,
} = useNestedPanelState(isNested);

const { data: contextRowSidePanelSource } = useSource({
id: contextRowSource || '',
});

const handleContextSidePanelClose = useCallback(() => {
setContextRowId(null);
setContextRowSource(null);
}, [setContextRowId, setContextRowSource]);

const handleRowExpandClick = useCallback(
(rowWhere: string) => {
setContextRowId(rowWhere);
setContextRowSource(source.id);
},
[source.id, setContextRowId, setContextRowSource],
);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TL;DR: URL state is used for the side panel. This allows us to expand into local state for nested panels as to not add additional complexity into url state

const date = useMemo(() => new Date(origTimestamp), [origTimestamp]);

const newDateRange = useMemo(
Expand Down Expand Up @@ -175,89 +233,109 @@ export default function ContextSubpanel({
contextBy,
]);

return (
config && (
<Flex direction="column" mih="0px" style={{ flexGrow: 1 }}>
<Group justify="space-between" p="sm">
<SegmentedControl
bg="dark.7"
color="dark.5"
size="xs"
data={generateSegmentedControlData()}
value={contextBy}
onChange={v => setContextBy(v as ContextBy)}
/>
{contextBy === ContextBy.Custom && (
<WhereLanguageControlled
name="whereLanguage"
control={control}
sqlInput={
originalLanguage === 'lucene' ? null : (
<SQLInlineEditorControlled
tableConnections={tcFromSource(source)}
control={control}
name="where"
placeholder="SQL WHERE clause (ex. column = 'foo')"
language="sql"
enableHotkey
size="sm"
/>
)
}
luceneInput={
originalLanguage === 'sql' ? null : (
<SearchInputV2
tableConnections={tcFromSource(source)}
control={control}
name="where"
language="lucene"
placeholder="Lucene where clause (ex. column:value)"
enableHotkey
size="sm"
/>
)
}
/>
)}
<SegmentedControl
bg="dark.7"
color="dark.5"
size="xs"
data={[
{ label: '100ms', value: ms('100ms').toString() },
{ label: '500ms', value: ms('500ms').toString() },
{ label: '1s', value: ms('1s').toString() },
{ label: '5s', value: ms('5s').toString() },
{ label: '30s', value: ms('30s').toString() },
{ label: '1m', value: ms('1m').toString() },
{ label: '5m', value: ms('5m').toString() },
{ label: '15m', value: ms('15m').toString() },
]}
value={range.toString()}
onChange={value => setRange(Number(value))}
const contextComponent = config && (
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just moving this into its own component for readability

<Flex direction="column" mih="0px" style={{ flexGrow: 1 }}>
<Group justify="space-between" p="sm">
<SegmentedControl
bg="dark.7"
color="dark.5"
size="xs"
data={generateSegmentedControlData()}
value={contextBy}
onChange={v => setContextBy(v as ContextBy)}
/>
{contextBy === ContextBy.Custom && (
<WhereLanguageControlled
name="whereLanguage"
control={control}
sqlInput={
originalLanguage === 'lucene' ? null : (
<SQLInlineEditorControlled
tableConnections={tcFromSource(source)}
control={control}
name="where"
placeholder="SQL WHERE clause (ex. column = 'foo')"
language="sql"
enableHotkey
size="sm"
/>
)
}
luceneInput={
originalLanguage === 'sql' ? null : (
<SearchInputV2
tableConnections={tcFromSource(source)}
control={control}
name="where"
language="lucene"
placeholder="Lucene where clause (ex. column:value)"
enableHotkey
size="sm"
/>
)
}
/>
</Group>
<Group p="sm">
<div>
{contextBy !== ContextBy.All && (
<Badge size="md" variant="default">
{contextBy}:{CONTEXT_MAPPING[contextBy].value}
</Badge>
)}
)}
<SegmentedControl
bg="dark.7"
color="dark.5"
size="xs"
data={[
{ label: '100ms', value: ms('100ms').toString() },
{ label: '500ms', value: ms('500ms').toString() },
{ label: '1s', value: ms('1s').toString() },
{ label: '5s', value: ms('5s').toString() },
{ label: '30s', value: ms('30s').toString() },
{ label: '1m', value: ms('1m').toString() },
{ label: '5m', value: ms('5m').toString() },
{ label: '15m', value: ms('15m').toString() },
]}
value={range.toString()}
onChange={value => setRange(Number(value))}
/>
</Group>
<Group p="sm">
<div>
{contextBy !== ContextBy.All && (
<Badge size="md" variant="default">
Time range: ±{ms(range / 2)}
{contextBy}:{CONTEXT_MAPPING[contextBy].value}
</Badge>
</div>
</Group>
<div style={{ height: '100%', overflow: 'auto' }}>
<DBSqlRowTable
highlightedLineId={rowId}
isLive={false}
config={config}
queryKeyPrefix={QUERY_KEY_PREFIX}
/>
)}
<Badge size="md" variant="default">
Time range: ±{ms(range / 2)}
</Badge>
</div>
</Flex>
)
</Group>
<div style={{ height: '100%', overflow: 'auto' }}>
<DBSqlRowTable
highlightedLineId={rowId}
isLive={false}
config={config}
queryKeyPrefix={QUERY_KEY_PREFIX}
onRowExpandClick={handleRowExpandClick}
/>
</div>
</Flex>
);

return (
<>
{contextComponent}
{contextRowId && contextRowSidePanelSource && (
<DBRowSidePanel
source={contextRowSidePanelSource}
rowId={contextRowId}
onClose={handleContextSidePanelClose}
isNestedPanel={true}
breadcrumbPath={[
...breadcrumbPath,
{
label: `Surrounding Context`,
rowData,
},
]}
/>
)}
</>
);
}
12 changes: 11 additions & 1 deletion packages/app/src/components/DBRowSidePanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types';
import { Box, Stack } from '@mantine/core';
import { useClickOutside } from '@mantine/hooks';

import DBRowSidePanelHeader from '@/components/DBRowSidePanelHeader';
import DBRowSidePanelHeader, {
BreadcrumbPath,
} from '@/components/DBRowSidePanelHeader';
import useResizable from '@/hooks/useResizable';
import { LogSidePanelKbdShortcuts } from '@/LogSidePanelElements';
import { getEventBody } from '@/source';
Expand Down Expand Up @@ -71,13 +73,16 @@ type DBRowSidePanelProps = {
rowId: string | undefined;
onClose: () => void;
isNestedPanel?: boolean;
breadcrumbPath?: BreadcrumbPath;
};

const DBRowSidePanel = ({
rowId: rowId,
source,
isNestedPanel = false,
setSubDrawerOpen,
onClose,
breadcrumbPath = [],
}: DBRowSidePanelProps & {
setSubDrawerOpen: Dispatch<SetStateAction<boolean>>;
}) => {
Expand Down Expand Up @@ -230,6 +235,8 @@ const DBRowSidePanel = ({
mainContent={mainContent}
mainContentHeader={mainContentColumn}
severityText={severityText}
breadcrumbPath={breadcrumbPath}
onBreadcrumbClick={onClose}
/>
</Box>
{/* <SidePanelHeader
Expand Down Expand Up @@ -349,6 +356,7 @@ const DBRowSidePanel = ({
dbSqlRowTableConfig={dbSqlRowTableConfig}
rowData={normalizedRow}
rowId={rowId}
breadcrumbPath={breadcrumbPath}
/>
</ErrorBoundary>
)}
Expand Down Expand Up @@ -405,6 +413,7 @@ export default function DBRowSidePanelErrorBoundary({
rowId,
source,
isNestedPanel,
breadcrumbPath = [],
}: DBRowSidePanelProps) {
const contextZIndex = useZIndex();
const drawerZIndex = contextZIndex + 10;
Expand Down Expand Up @@ -474,6 +483,7 @@ export default function DBRowSidePanelErrorBoundary({
rowId={rowId}
onClose={_onClose}
isNestedPanel={isNestedPanel}
breadcrumbPath={breadcrumbPath}
setSubDrawerOpen={setSubDrawerOpen}
/>
</ErrorBoundary>
Expand Down
Loading