Reusable DBSqlRowTableWithSideBar Component (#1171)

Across the app, we are inconsistent with when we can open the sidebar and expand functionality. This is because the sidebar and logic was managed by the parent component.

Additionally, the expand logic was set to assume a certain structure that some places in the application could not support (ex clickhouse dashboard doesn't have a 'source'). 

As a result, I have created the `DBSqlRowTableWithSideBar` component which will manage a lot of the common use cases for us. This PR introduces that new component, and updates all references (that could be easily upgraded) to use the new component when applciable.

The result: a lot less duplicate code (see # of lines removed) and the ability to more easily maintain the components down the road.

This PR also fixes several bugs I found as I tested these flows, especially around sidebars opening subpanels.

Fixes: HDX-2341
This commit is contained in:
Brandon Pereira 2025-09-17 13:58:45 -06:00 committed by GitHub
parent 970c0027b8
commit 7a0580590d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 369 additions and 334 deletions

View file

@ -0,0 +1,5 @@
---
"@hyperdx/app": patch
---
Reusable DBSqlRowTableWithSideBar Component

View file

@ -7,6 +7,8 @@ import {
useQueryStates,
} from 'nuqs';
import { useForm } from 'react-hook-form';
import { sql } from '@codemirror/lang-sql';
import { format as formatSql } from '@hyperdx/common-utils/dist/sqlFormatter';
import { DisplayType } from '@hyperdx/common-utils/dist/types';
import {
Box,
@ -19,6 +21,7 @@ import {
Tabs,
Text,
} from '@mantine/core';
import ReactCodeMirror from '@uiw/react-codemirror';
import { ConnectionSelectControlled } from '@/components/ConnectionSelect';
import { DBTimeChart } from '@/components/DBTimeChart';
@ -702,9 +705,18 @@ function ClickhousePage() {
Slowest Queries
</Text>
<DBSqlRowTable
highlightedLineId={undefined}
onRowExpandClick={() => {}}
showExpandButton={false}
renderRowDetails={row => {
return (
<ReactCodeMirror
extensions={[sql()]}
editable={false}
value={formatSql(row.query)}
theme="dark"
lang="sql"
maxHeight="200px"
/>
);
}}
config={{
select: `event_time, query_kind,
read_rows,
@ -735,7 +747,6 @@ function ClickhousePage() {
],
limit: { limit: 100 },
}}
onScroll={() => {}}
/>
</ChartBox>
</Grid.Col>

View file

@ -49,12 +49,10 @@ import {
} from '@mantine/core';
import { useHover, usePrevious } from '@mantine/hooks';
import { notifications } from '@mantine/notifications';
import { useQueryClient } from '@tanstack/react-query';
import { ContactSupportText } from '@/components/ContactSupportText';
import EditTimeChartForm from '@/components/DBEditTimeChartForm';
import DBNumberChart from '@/components/DBNumberChart';
import { DBSqlRowTable } from '@/components/DBRowTable';
import DBTableChart from '@/components/DBTableChart';
import { DBTimeChart } from '@/components/DBTimeChart';
import { SQLInlineEditorControlled } from '@/components/SQLInlineEditor';
@ -66,11 +64,10 @@ import {
useDeleteDashboard,
} from '@/dashboard';
import DBRowSidePanel from './components/DBRowSidePanel';
import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar';
import OnboardingModal from './components/OnboardingModal';
import { Tags } from './components/Tags';
import { useDashboardRefresh } from './hooks/useDashboardRefresh';
import { useAllFields } from './hooks/useMetadata';
import api from './api';
import { DEFAULT_CHART_CONFIG } from './ChartUtils';
import { IS_LOCAL_MODE } from './config';
@ -89,7 +86,7 @@ import {
import { parseTimeQuery, useNewTimeQuery } from './timeQuery';
import { useConfirm } from './useConfirm';
import { getMetricTableName, hashCode, omit } from './utils';
import { ZIndexContext } from './zIndex';
import { useZIndex, ZIndexContext } from './zIndex';
import 'react-grid-layout/css/styles.css';
import 'react-resizable/css/styles.css';
@ -197,17 +194,6 @@ const Tile = forwardRef(
const [hovered, setHovered] = useState(false);
// Search tile
const [rowId, setRowId] = useQueryState('rowWhere');
const [_, setRowSource] = useQueryState('rowSource');
const handleRowExpandClick = useCallback(
(rowWhere: string) => {
setRowId(rowWhere);
setRowSource(chart.config.source);
},
[chart.config.source, setRowId, setRowSource],
);
const alert = chart.config.alert;
const alertIndicatorColor = useMemo(() => {
if (!alert) {
@ -351,7 +337,7 @@ const Tile = forwardRef(
<HDXMarkdownChart config={queriedConfig} />
)}
{queriedConfig?.displayType === DisplayType.Search && (
<DBSqlRowTable
<DBSqlRowTableWithSideBar
enabled
sourceId={chart.config.source}
config={{
@ -372,9 +358,6 @@ const Tile = forwardRef(
groupBy: undefined,
granularity: undefined,
}}
onRowExpandClick={handleRowExpandClick}
highlightedLineId={rowId ?? undefined}
onScroll={() => {}}
isLive={false}
queryKeyPrefix={'search'}
/>
@ -402,6 +385,8 @@ const EditTileModal = ({
isSaving?: boolean;
onSave: (chart: Tile) => void;
}) => {
const contextZIndex = useZIndex();
const modalZIndex = contextZIndex + 10;
return (
<Modal
opened={chart != null}
@ -410,22 +395,25 @@ const EditTileModal = ({
centered
size="90%"
padding="xs"
zIndex={modalZIndex}
>
{chart != null && (
<EditTimeChartForm
dashboardId={dashboardId}
chartConfig={chart.config}
setChartConfig={config => {}}
dateRange={dateRange}
isSaving={isSaving}
onSave={config => {
onSave({
...chart,
config: config,
});
}}
onClose={onClose}
/>
<ZIndexContext.Provider value={modalZIndex + 10}>
<EditTimeChartForm
dashboardId={dashboardId}
chartConfig={chart.config}
setChartConfig={config => {}}
dateRange={dateRange}
isSaving={isSaving}
onSave={config => {
onSave({
...chart,
config: config,
});
}}
onClose={onClose}
/>
</ZIndexContext.Provider>
)}
</Modal>
);
@ -1068,13 +1056,6 @@ function DBDashboardPage({ presetConfig }: { presetConfig?: Dashboard }) {
>
+ Add New Tile
</Button>
{rowId && rowSidePanelSource && (
<DBRowSidePanel
source={rowSidePanelSource}
rowId={rowId}
onClose={handleSidePanelClose}
/>
)}
</Box>
);
}

View file

@ -58,13 +58,9 @@ import { notifications } from '@mantine/notifications';
import { useIsFetching } from '@tanstack/react-query';
import CodeMirror from '@uiw/react-codemirror';
import { useTimeChartSettings } from '@/ChartUtils';
import { ContactSupportText } from '@/components/ContactSupportText';
import DBDeltaChart from '@/components/DBDeltaChart';
import DBHeatmapChart from '@/components/DBHeatmapChart';
import DBRowSidePanel from '@/components/DBRowSidePanel';
import { RowSidePanelContext } from '@/components/DBRowSidePanel';
import { DBSqlRowTable } from '@/components/DBRowTable';
import { DBSearchPageFilters } from '@/components/DBSearchPageFilters';
import { DBTimeChart } from '@/components/DBTimeChart';
import { ErrorBoundary } from '@/components/ErrorBoundary';
@ -103,6 +99,7 @@ import { parseTimeQuery, useNewTimeQuery } from '@/timeQuery';
import { QUERY_LOCAL_STORAGE, useLocalStorage, usePrevious } from '@/utils';
import { SQLPreview } from './components/ChartSQLPreview';
import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar';
import PatternTable from './components/PatternTable';
import { useTableMetadata } from './hooks/useMetadata';
import { useSqlSuggestions } from './hooks/useSqlSuggestions';
@ -696,7 +693,6 @@ function DBSearchPage() {
const inputSourceObj = inputSourceObjs?.find(s => s.id === inputSource);
const defaultOrderBy = useDefaultOrderBy(inputSource);
const [rowId, setRowId] = useQueryState('rowWhere');
const [displayedTimeInputValue, setDisplayedTimeInputValue] =
useState('Live Tail');
@ -880,13 +876,9 @@ function DBSearchPage() {
[isLive, setIsLive],
);
const onRowExpandClick = useCallback(
(rowWhere: string) => {
setIsLive(false);
setRowId(rowWhere);
},
[setRowId, setIsLive],
);
const onSidebarOpen = useCallback(() => {
setIsLive(false);
}, [setIsLive]);
const [modelFormExpanded, setModelFormExpanded] = useState(false); // Used in local mode
const [saveSearchModalState, setSaveSearchModalState] = useState<
@ -1010,7 +1002,7 @@ function DBSearchPage() {
const { data: me } = api.useMe();
// Callback to handle when rows are expanded - kick user out of live tail
const handleExpandedRowsChange = useCallback(
const onExpandedRowsChange = useCallback(
(hasExpandedRows: boolean) => {
if (hasExpandedRows && isLive) {
setIsLive(false);
@ -1472,25 +1464,6 @@ function DBSearchPage() {
</Button>
</Flex>
</form>
<RowSidePanelContext.Provider
value={{
onPropertyAddClick: searchFilters.setFilterValue,
displayedColumns,
toggleColumn,
generateSearchUrl,
dbSqlRowTableConfig,
isChildModalOpen: isDrawerChildModalOpen,
setChildModalOpen: setDrawerChildModalOpen,
}}
>
{searchedSource && (
<DBRowSidePanel
source={searchedSource}
rowId={rowId ?? undefined}
onClose={() => setRowId(null)}
/>
)}
</RowSidePanelContext.Provider>
{searchedConfig != null && searchedSource != null && (
<SaveSearchModal
opened={saveSearchModalState != null}
@ -1837,32 +1810,31 @@ function DBSearchPage() {
</div>
)}
{chartConfig &&
searchedConfig.source &&
dbSqlRowTableConfig &&
analysisMode === 'results' && (
<RowSidePanelContext.Provider
value={{
<DBSqlRowTableWithSideBar
context={{
onPropertyAddClick: searchFilters.setFilterValue,
displayedColumns,
toggleColumn,
generateSearchUrl,
dbSqlRowTableConfig,
isChildModalOpen: isDrawerChildModalOpen,
setChildModalOpen: setDrawerChildModalOpen,
}}
>
<DBSqlRowTable
config={dbSqlRowTableConfig}
sourceId={searchedConfig.source ?? ''}
onRowExpandClick={onRowExpandClick}
highlightedLineId={rowId ?? undefined}
enabled={isReady}
isLive={isLive ?? true}
queryKeyPrefix={QUERY_KEY_PREFIX}
onScroll={onTableScroll}
onError={handleTableError}
denoiseResults={denoiseResults}
onExpandedRowsChange={handleExpandedRowsChange}
collapseAllRows={collapseAllRows}
/>
</RowSidePanelContext.Provider>
config={dbSqlRowTableConfig}
sourceId={searchedConfig.source}
onSidebarOpen={onSidebarOpen}
onExpandedRowsChange={onExpandedRowsChange}
enabled={isReady}
isLive={isLive ?? true}
queryKeyPrefix={QUERY_KEY_PREFIX}
onScroll={onTableScroll}
onError={handleTableError}
denoiseResults={denoiseResults}
collapseAllRows={collapseAllRows}
/>
)}
</>
)}

View file

@ -38,8 +38,7 @@ import {
import { TimePicker } from '@/components/TimePicker';
import { ConnectionSelectControlled } from './components/ConnectionSelect';
import DBRowSidePanel from './components/DBRowSidePanel';
import { DBSqlRowTable } from './components/DBRowTable';
import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar';
import { DBTimeChart } from './components/DBTimeChart';
import { FormatPodStatus } from './components/KubeComponents';
import { KubernetesFilters } from './components/KubernetesFilters';
@ -830,24 +829,6 @@ function KubernetesDashboardPage() {
[_searchQuery, setSearchQuery],
);
// Row details side panel
const [rowId, setRowId] = useQueryState('rowWhere');
const [rowSource, setRowSource] = useQueryState('rowSource');
const { data: rowSidePanelSource } = useSource({ id: rowSource || '' });
const handleSidePanelClose = React.useCallback(() => {
setRowId(null);
setRowSource(null);
}, [setRowId, setRowSource]);
const handleRowExpandClick = React.useCallback(
(rowWhere: string) => {
setRowId(rowWhere);
setRowSource(logSource?.id ?? null);
},
[logSource?.id, setRowId, setRowSource],
);
return (
<Box data-testid="kubernetes-dashboard-page" p="sm">
<OnboardingModal requireSource={false} />
@ -869,13 +850,6 @@ function KubernetesDashboardPage() {
logSource={logSource}
/>
)}
{rowId && rowSidePanelSource && (
<DBRowSidePanel
source={rowSidePanelSource}
rowId={rowId}
onClose={handleSidePanelClose}
/>
)}
<Group justify="space-between">
<Group>
<Text c="gray.4" size="xl">
@ -1047,7 +1021,7 @@ function KubernetesDashboardPage() {
</Card.Section>
<Card.Section p="md" py="sm" h={CHART_HEIGHT}>
{logSource && (
<DBSqlRowTable
<DBSqlRowTableWithSideBar
sourceId={logSource.id}
config={{
...logSource,
@ -1090,11 +1064,8 @@ function KubernetesDashboardPage() {
limit: { limit: 200, offset: 0 },
dateRange,
}}
onRowExpandClick={handleRowExpandClick}
highlightedLineId={rowId ?? undefined}
isLive={false}
queryKeyPrefix="k8s-dashboard-events"
onScroll={() => {}}
/>
)}
</Card.Section>

View file

@ -22,7 +22,6 @@ import {
K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
K8S_MEM_NUMBER_FORMAT,
} from '@/ChartUtils';
import { DBSqlRowTable } from '@/components/DBRowTable';
import { DBTimeChart } from '@/components/DBTimeChart';
import { DrawerBody, DrawerHeader } from '@/components/DrawerUtils';
import { useQueriedChartConfig } from '@/hooks/useChartConfig';
@ -31,6 +30,7 @@ import { getEventBody } from '@/source';
import { parseTimeQuery, useTimeQuery } from '@/timeQuery';
import { useZIndex, ZIndexContext } from '@/zIndex';
import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar';
import { useGetKeyValues, useTableMetadata } from './hooks/useMetadata';
import styles from '../styles/LogSidePanel.module.scss';
@ -182,7 +182,10 @@ function NamespaceLogs({
</Flex>
</Card.Section>
<Card.Section p="md" py="sm" h={CHART_HEIGHT}>
<DBSqlRowTable
<DBSqlRowTableWithSideBar
sourceId={logSource.id}
isNestedPanel
breadcrumbPath={[{ label: 'Namespace Details' }]}
config={{
...logSource,
where: _where,
@ -214,12 +217,8 @@ function NamespaceLogs({
limit: { limit: 200, offset: 0 },
dateRange,
}}
onRowExpandClick={() => {}}
highlightedLineId={undefined}
isLive={false}
queryKeyPrefix="k8s-dashboard-namespace-logs"
onScroll={() => {}}
showExpandButton={false}
/>
</Card.Section>
</Card>

View file

@ -25,7 +25,6 @@ import {
K8S_CPU_PERCENTAGE_NUMBER_FORMAT,
K8S_MEM_NUMBER_FORMAT,
} from '@/ChartUtils';
import { DBSqlRowTable } from '@/components/DBRowTable';
import { DBTimeChart } from '@/components/DBTimeChart';
import { DrawerBody, DrawerHeader } from '@/components/DrawerUtils';
import { InfraPodsStatusTable } from '@/KubernetesDashboardPage';
@ -34,6 +33,7 @@ import { parseTimeQuery, useTimeQuery } from '@/timeQuery';
import { formatUptime } from '@/utils';
import { useZIndex, ZIndexContext } from '@/zIndex';
import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar';
import { useQueriedChartConfig } from './hooks/useChartConfig';
import { useGetKeyValues, useTableMetadata } from './hooks/useMetadata';
@ -201,7 +201,10 @@ function NodeLogs({
</Flex>
</Card.Section>
<Card.Section p="md" py="sm" h={CHART_HEIGHT}>
<DBSqlRowTable
<DBSqlRowTableWithSideBar
sourceId={logSource.id}
isNestedPanel
breadcrumbPath={[{ label: 'Node Details' }]}
config={{
...logSource,
where: _where,
@ -233,12 +236,8 @@ function NodeLogs({
limit: { limit: 200, offset: 0 },
dateRange,
}}
onRowExpandClick={() => {}}
highlightedLineId={undefined}
isLive={false}
queryKeyPrefix="k8s-dashboard-node-logs"
showExpandButton={false}
onScroll={() => {}}
/>
</Card.Section>
</Card>

View file

@ -22,13 +22,13 @@ import {
K8S_MEM_NUMBER_FORMAT,
} from '@/ChartUtils';
import DBRowSidePanel from '@/components/DBRowSidePanel';
import { DBSqlRowTable } from '@/components/DBRowTable';
import { DBTimeChart } from '@/components/DBTimeChart';
import { DrawerBody, DrawerHeader } from '@/components/DrawerUtils';
import { KubeTimeline, useV2LogBatch } from '@/components/KubeComponents';
import { parseTimeQuery, useTimeQuery } from '@/timeQuery';
import { useZIndex, ZIndexContext } from '@/zIndex';
import DBSqlRowTableWithSideBar from './components/DBSqlRowTableWithSidebar';
import { useGetKeyValues, useTableMetadata } from './hooks/useMetadata';
import { getEventBody } from './source';
@ -206,14 +206,13 @@ function PodLogs({
</Flex>
</Card.Section>
<Card.Section p="md" py="sm" h={CHART_HEIGHT}>
<DBSqlRowTable
<DBSqlRowTableWithSideBar
sourceId={logSource.id}
config={tableConfig}
onRowExpandClick={onRowClick}
highlightedLineId={rowId ?? undefined}
isLive={false}
isNestedPanel
breadcrumbPath={[{ label: 'Pods' }]}
queryKeyPrefix="k8s-dashboard-pod-logs"
onScroll={() => {}}
/>
</Card.Section>
</Card>

View file

@ -319,10 +319,11 @@ export default function ContextSubpanel({
<DBSqlRowTable
sourceId={source.id}
highlightedLineId={rowId}
showExpandButton={false}
isLive={false}
config={config}
queryKeyPrefix={QUERY_KEY_PREFIX}
onRowExpandClick={handleRowExpandClick}
onRowDetailsClick={handleRowExpandClick}
onChildModalOpen={setChildModalOpen}
/>
</div>

View file

@ -50,7 +50,6 @@ import {
import { AGG_FNS } from '@/ChartUtils';
import { AlertChannelForm, getAlertReferenceLines } from '@/components/Alerts';
import ChartSQLPreview from '@/components/ChartSQLPreview';
import { DBSqlRowTable } from '@/components/DBRowTable';
import DBTableChart from '@/components/DBTableChart';
import { DBTimeChart } from '@/components/DBTimeChart';
import { SQLInlineEditorControlled } from '@/components/SQLInlineEditor';
@ -75,6 +74,7 @@ import HDXMarkdownChart from '../HDXMarkdownChart';
import { AggFnSelectControlled } from './AggFnSelect';
import DBNumberChart from './DBNumberChart';
import DBSqlRowTableWithSideBar from './DBSqlRowTableWithSidebar';
import {
CheckBoxControlled,
InputControlled,
@ -979,7 +979,8 @@ export default function EditTimeChartForm({
className="flex-grow-1 d-flex flex-column"
style={{ height: 400 }}
>
<DBSqlRowTable
<DBSqlRowTableWithSideBar
sourceId={sourceId}
config={{
...queriedConfig,
orderBy: [
@ -1002,13 +1003,9 @@ export default function EditTimeChartForm({
groupBy: undefined,
granularity: undefined,
}}
onRowExpandClick={() => {}}
highlightedLineId={undefined}
enabled
isLive={false}
showExpandButton={false}
queryKeyPrefix={'search'}
onScroll={() => {}}
/>
</div>
)}
@ -1029,13 +1026,12 @@ export default function EditTimeChartForm({
className="flex-grow-1 d-flex flex-column"
style={{ height: 400 }}
>
<DBSqlRowTable
<DBSqlRowTableWithSideBar
sourceId={sourceId}
config={sampleEventsConfig}
highlightedLineId={undefined}
enabled
isLive={false}
queryKeyPrefix={'search'}
showExpandButton={false}
/>
</div>
)}

View file

@ -15,7 +15,7 @@ import { useHotkeys } from 'react-hotkeys-hook';
import Drawer from 'react-modern-drawer';
import { TSource } from '@hyperdx/common-utils/dist/types';
import { ChartConfigWithDateRange } from '@hyperdx/common-utils/dist/types';
import { Box, Stack } from '@mantine/core';
import { Box, OptionalPortal, Stack } from '@mantine/core';
import { useClickOutside } from '@mantine/hooks';
import DBRowSidePanelHeader, {
@ -39,7 +39,7 @@ import DBTracePanel from './DBTracePanel';
import 'react-modern-drawer/dist/index.css';
import styles from '@/../styles/LogSidePanel.module.scss';
export const RowSidePanelContext = createContext<{
export type RowSidePanelContextProps = {
onPropertyAddClick?: (keyPath: string, value: string) => void;
generateSearchUrl?: ({
where,
@ -59,7 +59,9 @@ export const RowSidePanelContext = createContext<{
dbSqlRowTableConfig?: ChartConfigWithDateRange;
isChildModalOpen?: boolean;
setChildModalOpen?: (open: boolean) => void;
}>({});
};
export const RowSidePanelContext = createContext<RowSidePanelContextProps>({});
enum Tab {
Overview = 'overview',
@ -148,7 +150,7 @@ const DBRowSidePanel = ({
: Tab.Parsed;
const [queryTab, setQueryTab] = useQueryState(
'tab',
'sidePanelTab',
parseAsStringEnum<Tab>(Object.values(Tab)).withDefault(defaultTab),
);
@ -498,50 +500,52 @@ export default function DBRowSidePanelErrorBoundary({
}, ['mouseup', 'touchend']);
return (
<Drawer
data-testid="row-side-panel"
customIdSuffix={`log-side-panel-${rowId}`}
duration={0}
open={rowId != null}
onClose={() => {
if (!subDrawerOpen) {
_onClose();
}
}}
direction="right"
size={`${width}vw`}
zIndex={drawerZIndex}
enableOverlay={subDrawerOpen}
>
<ZIndexContext.Provider value={drawerZIndex}>
<div className={styles.panel} ref={drawerRef}>
<Box className={styles.panelDragBar} onMouseDown={startResize} />
<OptionalPortal withinPortal={!isNestedPanel}>
<Drawer
data-testid="row-side-panel"
customIdSuffix={`log-side-panel-${rowId}`}
duration={300}
open={rowId != null}
onClose={() => {
if (!subDrawerOpen) {
_onClose();
}
}}
direction="right"
size={`${width}vw`}
zIndex={drawerZIndex}
enableOverlay={subDrawerOpen}
>
<ZIndexContext.Provider value={drawerZIndex}>
<div className={styles.panel} ref={drawerRef}>
<Box className={styles.panelDragBar} onMouseDown={startResize} />
<ErrorBoundary
fallbackRender={error => (
<Stack>
<div className="text-danger px-2 py-1 m-2 fs-7 font-monospace bg-danger-transparent p-4">
An error occurred while rendering this event.
</div>
<ErrorBoundary
fallbackRender={error => (
<Stack>
<div className="text-danger px-2 py-1 m-2 fs-7 font-monospace bg-danger-transparent p-4">
An error occurred while rendering this event.
</div>
<div className="px-2 py-1 m-2 fs-7 font-monospace bg-dark-grey p-4">
{error?.error?.message}
</div>
</Stack>
)}
>
<DBRowSidePanel
source={source}
rowId={rowId}
onClose={_onClose}
isNestedPanel={isNestedPanel}
breadcrumbPath={breadcrumbPath}
setSubDrawerOpen={setSubDrawerOpen}
onBreadcrumbClick={onBreadcrumbClick}
/>
</ErrorBoundary>
</div>
</ZIndexContext.Provider>
</Drawer>
<div className="px-2 py-1 m-2 fs-7 font-monospace bg-dark-grey p-4">
{error?.error?.message}
</div>
</Stack>
)}
>
<DBRowSidePanel
source={source}
rowId={rowId}
onClose={_onClose}
isNestedPanel={isNestedPanel}
breadcrumbPath={breadcrumbPath}
setSubDrawerOpen={setSubDrawerOpen}
onBreadcrumbClick={onBreadcrumbClick}
/>
</ErrorBoundary>
</div>
</ZIndexContext.Provider>
</Drawer>
</OptionalPortal>
);
}

View file

@ -281,7 +281,7 @@ export const RawLogTable = memo(
generateRowId,
onInstructionsClick,
// onPropertySearchClick,
onRowExpandClick,
onRowDetailsClick,
onScroll,
onSettingsClick,
onShowPatternsClick,
@ -296,6 +296,7 @@ export const RawLogTable = memo(
loadingDate,
config,
onChildModalOpen,
renderRowDetails,
source,
onExpandedRowsChange,
collapseAllRows,
@ -306,16 +307,16 @@ export const RawLogTable = memo(
onSettingsClick?: () => void;
onInstructionsClick?: () => void;
rows: Record<string, any>[];
isLoading: boolean;
fetchNextPage: (options?: FetchNextPageOptions | undefined) => any;
onRowExpandClick: (row: Record<string, any>) => void;
isLoading?: boolean;
fetchNextPage?: (options?: FetchNextPageOptions | undefined) => any;
onRowDetailsClick: (row: Record<string, any>) => void;
generateRowId: (row: Record<string, any>) => string;
// onPropertySearchClick: (
// name: string,
// value: string | number | boolean,
// ) => void;
hasNextPage: boolean;
highlightedLineId: string | undefined;
hasNextPage?: boolean;
highlightedLineId?: string;
onScroll?: (scrollTop: number) => void;
isLive: boolean;
onShowPatternsClick?: () => void;
@ -335,6 +336,7 @@ export const RawLogTable = memo(
onExpandedRowsChange?: (hasExpandedRows: boolean) => void;
collapseAllRows?: boolean;
showExpandButton?: boolean;
renderRowDetails?: (row: Record<string, any>) => React.ReactNode;
}) => {
const generateRowMatcher = generateRowId;
@ -359,9 +361,9 @@ export const RawLogTable = memo(
const _onRowExpandClick = useCallback(
({ __hyperdx_id, ...row }: Record<string, any>) => {
onRowExpandClick(row);
onRowDetailsClick?.(row);
},
[onRowExpandClick],
[onRowDetailsClick],
);
const { width } = useWindowSize();
@ -514,7 +516,7 @@ export const RawLogTable = memo(
hasNextPage
) {
// Cancel refetch is important to ensure we wait for the last fetch to finish
fetchNextPage({ cancelRefetch: false });
fetchNextPage?.({ cancelRefetch: false });
}
}
},
@ -604,7 +606,7 @@ export const RawLogTable = memo(
useEffect(() => {
if (
scrolledToHighlightedLine ||
highlightedLineId == null ||
!highlightedLineId ||
rowVirtualizer == null
) {
return;
@ -613,13 +615,13 @@ export const RawLogTable = memo(
const rowIdx = dedupedRows.findIndex(
l => getRowId(l) === highlightedLineId,
);
if (rowIdx == -1) {
if (rowIdx == -1 && highlightedLineId) {
if (
dedupedRows.length < MAX_SCROLL_FETCH_LINES &&
!isLoading &&
hasNextPage
) {
fetchNextPage({ cancelRefetch: false });
fetchNextPage?.({ cancelRefetch: false });
}
} else {
setScrolledToHighlightedLine(true);
@ -825,7 +827,8 @@ export const RawLogTable = memo(
<tr
data-testid={`table-row-${rowId}`}
className={cx(styles.tableRow, {
[styles.tableRow__selected]: highlightedLineId === rowId,
[styles.tableRow__selected]:
highlightedLineId && highlightedLineId === rowId,
})}
data-index={virtualRow.index}
ref={rowVirtualizer.measureElement}
@ -909,7 +912,12 @@ export const RawLogTable = memo(
rowId={rowId}
measureElement={rowVirtualizer.measureElement}
virtualIndex={virtualRow.index}
/>
>
{renderRowDetails?.({
id: rowId,
...row.original,
})}
</ExpandedLogRow>
)}
</React.Fragment>
);
@ -1124,7 +1132,7 @@ function DBSqlRowTableComponent({
config,
sourceId,
onError,
onRowExpandClick,
onRowDetailsClick,
highlightedLineId,
enabled = true,
isLive = false,
@ -1135,14 +1143,16 @@ function DBSqlRowTableComponent({
onExpandedRowsChange,
collapseAllRows,
showExpandButton = true,
renderRowDetails,
}: {
config: ChartConfigWithDateRange;
sourceId?: string;
onRowExpandClick?: (where: string) => void;
highlightedLineId: string | undefined;
onRowDetailsClick?: (where: string) => void;
highlightedLineId?: string;
queryKeyPrefix?: string;
enabled?: boolean;
isLive?: boolean;
renderRowDetails?: (r: { [key: string]: unknown }) => React.ReactNode;
onScroll?: (scrollTop: number) => void;
onError?: (error: Error | ClickHouseQueryError) => void;
denoiseResults?: boolean;
@ -1213,11 +1223,11 @@ function DBSqlRowTableComponent({
const getRowWhere = useRowWhere({ meta: data?.meta, aliasMap });
const _onRowExpandClick = useCallback(
const _onRowDetailsClick = useCallback(
(row: Record<string, any>) => {
return onRowExpandClick?.(getRowWhere(row));
return onRowDetailsClick?.(getRowWhere(row));
},
[onRowExpandClick, getRowWhere],
[onRowDetailsClick, getRowWhere],
);
useEffect(() => {
@ -1331,11 +1341,12 @@ function DBSqlRowTableComponent({
displayedColumns={columns}
highlightedLineId={highlightedLineId}
rows={denoiseResults ? (denoisedRows?.data ?? []) : processedRows}
renderRowDetails={renderRowDetails}
isLoading={isLoading}
fetchNextPage={fetchNextPage}
// onPropertySearchClick={onPropertySearchClick}
hasNextPage={hasNextPage}
onRowExpandClick={_onRowExpandClick}
onRowDetailsClick={_onRowDetailsClick}
onScroll={onScroll}
generateRowId={getRowWhere}
isError={isError}

View file

@ -0,0 +1,162 @@
import { useCallback } from 'react';
import { useQueryState } from 'nuqs';
import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse';
import {
ChartConfigWithDateRange,
TSource,
} from '@hyperdx/common-utils/dist/types';
import { useSource } from '@/source';
import TabBar from '@/TabBar';
import { useLocalStorage } from '@/utils';
import { RowDataPanel } from './DBRowDataPanel';
import { RowOverviewPanel } from './DBRowOverviewPanel';
import DBRowSidePanel, {
RowSidePanelContext,
RowSidePanelContextProps,
} from './DBRowSidePanel';
import { BreadcrumbEntry } from './DBRowSidePanelHeader';
import { DBSqlRowTable } from './DBRowTable';
interface Props {
sourceId: string;
config: ChartConfigWithDateRange;
onError?: (error: Error | ClickHouseQueryError) => void;
onScroll?: (scrollTop: number) => void;
onSidebarOpen?: (rowId: string) => void;
onExpandedRowsChange?: (hasExpandedRows: boolean) => void;
onPropertyAddClick?: (keyPath: string, value: string) => void;
context?: RowSidePanelContextProps;
enabled?: boolean;
isLive?: boolean;
queryKeyPrefix?: string;
denoiseResults?: boolean;
collapseAllRows?: boolean;
isNestedPanel?: boolean;
breadcrumbPath?: BreadcrumbEntry[];
}
export default function DBSqlRowTableWithSideBar({
sourceId,
config,
onError,
onScroll,
context,
onExpandedRowsChange,
denoiseResults,
collapseAllRows,
isLive,
enabled,
isNestedPanel,
breadcrumbPath,
onSidebarOpen,
}: Props) {
const { data: sourceData } = useSource({ id: sourceId });
const [rowId, setRowId] = useQueryState('rowWhere');
const [, setRowSource] = useQueryState('rowSource');
const onOpenSidebar = useCallback(
(rowWhere: string) => {
setRowId(rowWhere);
setRowSource(sourceId);
onSidebarOpen?.(rowWhere);
},
[setRowId, setRowSource, sourceId, onSidebarOpen],
);
const onCloseSidebar = useCallback(() => {
setRowId(null);
setRowSource(null);
}, [setRowId, setRowSource]);
return (
<RowSidePanelContext.Provider value={context ?? {}}>
{sourceData && (
<DBRowSidePanel
source={sourceData}
rowId={rowId ?? undefined}
isNestedPanel={isNestedPanel}
breadcrumbPath={breadcrumbPath}
onClose={onCloseSidebar}
/>
)}
<DBSqlRowTable
config={config}
sourceId={sourceId}
onRowDetailsClick={onOpenSidebar}
highlightedLineId={rowId ?? undefined}
enabled={enabled}
isLive={isLive ?? true}
queryKeyPrefix={'dbSqlRowTable'}
denoiseResults={denoiseResults}
renderRowDetails={r => {
if (!sourceData) {
return <div className="p-3 text-muted">Loading...</div>;
}
return (
<RowOverviewPanelWrapper
source={sourceData}
rowId={r.id as string}
/>
);
}}
onScroll={onScroll}
onError={onError}
onExpandedRowsChange={onExpandedRowsChange}
collapseAllRows={collapseAllRows}
/>
</RowSidePanelContext.Provider>
);
}
enum InlineTab {
Overview = 'overview',
ColumnValues = 'columnValues',
}
function RowOverviewPanelWrapper({
source,
rowId,
}: {
source: TSource;
rowId: string;
}) {
// Use localStorage to persist the selected tab
const [activeTab, setActiveTab] = useLocalStorage<InlineTab>(
'hdx-expanded-row-default-tab',
InlineTab.ColumnValues,
);
return (
<div className="position-relative">
<div className="bg-body px-3 pt-2 position-relative">
<TabBar
className="fs-8"
items={[
{
text: 'Overview',
value: InlineTab.Overview,
},
{
text: 'Column Values',
value: InlineTab.ColumnValues,
},
]}
activeItem={activeTab}
onClick={setActiveTab}
/>
</div>
<div className="bg-body">
{activeTab === InlineTab.Overview && (
<div className="inline-overview-panel">
<RowOverviewPanel source={source} rowId={rowId} />
</div>
)}
{activeTab === InlineTab.ColumnValues && (
<RowDataPanel source={source} rowId={rowId} />
)}
</div>
</div>
);
}

View file

@ -4,29 +4,17 @@ import { useQueryState } from 'nuqs';
import { TSource } from '@hyperdx/common-utils/dist/types';
import { IconChevronRight } from '@tabler/icons-react';
import { useLocalStorage } from '@/utils';
import TabBar from '../TabBar';
import { RowDataPanel } from './DBRowDataPanel';
import { RowOverviewPanel } from './DBRowOverviewPanel';
import styles from '../../styles/LogTable.module.scss';
enum InlineTab {
Overview = 'overview',
ColumnValues = 'columnValues',
}
// Hook that provides a function to open the sidebar with specific row details
const useSidebarOpener = () => {
const [, setRowId] = useQueryState('rowWhere');
const [, setRowSource] = useQueryState('rowSource');
return useCallback(
(rowWhere: string, sourceId: string) => {
(rowWhere: string, sourceId?: string) => {
setRowId(rowWhere);
setRowSource(sourceId);
setRowSource(sourceId ?? null);
},
[setRowId, setRowSource],
);
@ -35,27 +23,23 @@ const useSidebarOpener = () => {
export const ExpandedLogRow = memo(
({
columnsLength,
children,
virtualKey,
source,
rowId,
measureElement,
virtualIndex,
}: {
children: React.ReactNode;
columnsLength: number;
virtualKey: string;
source: TSource | undefined;
source?: TSource;
rowId: string;
measureElement?: (element: HTMLElement | null) => void;
virtualIndex?: number;
}) => {
const openSidebar = useSidebarOpener();
// Use localStorage to persist the selected tab
const [activeTab, setActiveTab] = useLocalStorage<InlineTab>(
'hdx-expanded-row-default-tab',
InlineTab.ColumnValues,
);
return (
<tr
data-testid={`expanded-row-${rowId}`}
@ -66,60 +50,30 @@ export const ExpandedLogRow = memo(
>
<td colSpan={columnsLength} className="p-0 border-0">
<div className={cx('mx-2 mb-2 rounded', styles.expandedRowContent)}>
{source ? (
<>
<div className="position-relative">
<div className="bg-body px-3 pt-2 position-relative">
{openSidebar && (
<button
type="button"
className={cx(
'position-absolute top-0 end-0 mt-1 me-1 p-1 border-0 bg-transparent text-muted rounded',
styles.expandButton,
)}
onClick={() => openSidebar(rowId, source.id)}
title="Open in sidebar"
aria-label="Open in sidebar"
style={{
zIndex: 1,
fontSize: '12px',
lineHeight: 1,
}}
>
<i className="bi bi-arrows-angle-expand" />
</button>
<div className="position-relative">
<div className="bg-body px-3 pt-2 position-relative">
{openSidebar && (
<button
type="button"
className={cx(
'position-absolute top-0 end-0 mt-1 me-1 p-1 border-0 bg-transparent text-muted rounded',
styles.expandButton,
)}
<TabBar
className="fs-8"
items={[
{
text: 'Overview',
value: InlineTab.Overview,
},
{
text: 'Column Values',
value: InlineTab.ColumnValues,
},
]}
activeItem={activeTab}
onClick={setActiveTab}
/>
</div>
<div className="bg-body">
{activeTab === InlineTab.Overview && (
<div className="inline-overview-panel">
<RowOverviewPanel source={source} rowId={rowId} />
</div>
)}
{activeTab === InlineTab.ColumnValues && (
<RowDataPanel source={source} rowId={rowId} />
)}
</div>
</div>
</>
) : (
<div className="p-3 text-muted">Loading...</div>
)}
onClick={() => openSidebar(rowId, source?.id)}
title="Open in sidebar"
aria-label="Open in sidebar"
style={{
zIndex: 1,
fontSize: '12px',
lineHeight: 1,
}}
>
<i className="bi bi-arrows-angle-expand" />
</button>
)}
{children}
</div>
</div>
</div>
</td>
</tr>

View file

@ -126,12 +126,9 @@ export default function PatternSidePanel({
displayedColumns={displayedColumns}
columnTypeMap={columnTypeMap}
columnNameMap={columnNameMap}
isLoading={false}
fetchNextPage={() => {}}
onRowExpandClick={handleRowClick}
onRowDetailsClick={handleRowClick}
wrapLines={false}
highlightedLineId={''}
hasNextPage={false}
showExpandButton={false}
isLive={false}
/>
</Card>
@ -143,6 +140,7 @@ export default function PatternSidePanel({
rowId={selectedRowWhere}
onClose={handleCloseRowSidePanel}
isNestedPanel={true}
breadcrumbPath={[{ label: 'Pattern Overview' }]}
/>
)}
</div>

View file

@ -67,7 +67,7 @@ export default function PatternTable({
'severityText',
'pattern',
]}
onRowExpandClick={row => setSelectedPattern(row as Pattern)}
onRowDetailsClick={row => setSelectedPattern(row as Pattern)}
hasNextPage={false}
fetchNextPage={() => {}}
highlightedLineId={''}
@ -80,6 +80,7 @@ export default function PatternTable({
severityText: 'level',
}}
config={patternQueryConfig}
showExpandButton={false}
/>
{selectedPattern && source && (
<PatternSidePanel

View file

@ -1,18 +1,14 @@
import { useCallback } from 'react';
import { parseAsString, useQueryState } from 'nuqs';
import { ClickHouseQueryError } from '@hyperdx/common-utils/dist/clickhouse';
import type { Filter, TSource } from '@hyperdx/common-utils/dist/types';
import { Box, Code, Group, Text } from '@mantine/core';
import { ChartBox } from '@/components/ChartBox';
import DBRowSidePanel from '@/components/DBRowSidePanel';
import { DBSqlRowTable } from '@/components/DBRowTable';
import { useQueriedChartConfig } from '@/hooks/useChartConfig';
import { useJsonColumns } from '@/hooks/useMetadata';
import { getExpressions } from '@/serviceDashboard';
import { useSource } from '@/source';
import { SQLPreview } from './ChartSQLPreview';
import DBSqlRowTableWithSideBar from './DBSqlRowTableWithSidebar';
export default function SlowestEventsTile({
source,
@ -38,23 +34,6 @@ export default function SlowestEventsTile({
});
const expressions = getExpressions(source, jsonColumns);
const [rowId, setRowId] = useQueryState('rowId', parseAsString);
const [rowSource, setRowSource] = useQueryState('rowSource', parseAsString);
const { data: rowSidePanelSource } = useSource({ id: rowSource || '' });
const handleSidePanelClose = useCallback(() => {
setRowId(null);
setRowSource(null);
}, [setRowId, setRowSource]);
const handleRowExpandClick = useCallback(
(rowWhere: string) => {
setRowId(rowWhere);
setRowSource(source.id);
},
[source.id, setRowId, setRowSource],
);
const { data, isLoading, isError, error } = useQueriedChartConfig(
{
...source,
@ -130,7 +109,9 @@ export default function SlowestEventsTile({
) : (
source && (
<>
<DBSqlRowTable
<DBSqlRowTableWithSideBar
isNestedPanel
breadcrumbPath={[{ label: 'Endpoint' }]}
sourceId={source.id}
config={{
...source,
@ -170,19 +151,9 @@ export default function SlowestEventsTile({
},
],
}}
onRowExpandClick={handleRowExpandClick}
highlightedLineId={rowId ?? undefined}
isLive={false}
queryKeyPrefix="service-dashboard-slowest-transactions"
onScroll={() => {}}
/>
{rowId && rowSidePanelSource && (
<DBRowSidePanel
source={rowSidePanelSource}
rowId={rowId}
onClose={handleSidePanelClose}
/>
)}
</>
)
)}